From d962070d7c84a1c1b0fa3877b41fe5d6365fdf69 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Sat, 30 Sep 2023 00:01:10 +0200 Subject: [PATCH 001/191] Fix release fade-out not being applied This fixes to bugs leading to clicks on instrument note-off in most instruments. The first bug was introduced as part of a refactoring done by Vesa [1] and caused each note play handle's last buffer being dropped because the play handles were deleted before being processed in AudioPort. Thanks to @lleroy for pointing this out. [2] The second bug / typo has always been there [3] and was a misplaced parenthesis in Instrument::applyRelease breaking the calculation of the note-off envelope's start frame, sometimes putting it outside of the buffer. Fixes #3086 [1] 857de8d2c829dc688745f41ba8eddbe148a63a20 and related commits [1] https://github.com/LMMS/lmms/issues/3086#issuecomment-519087089 [2] 02433380c629457ad021a1f9c91b8148769c33dc --- src/core/AudioEngine.cpp | 22 +++++++++++----------- src/core/Instrument.cpp | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 29c54647c..df274c188 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -394,6 +394,17 @@ void AudioEngine::renderStageInstruments() AudioEngineWorkerThread::fillJobQueue(m_playHandles); AudioEngineWorkerThread::startAndWaitForJobs(); +} + + + +void AudioEngine::renderStageEffects() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Effects); + + // STAGE 2: process effects of all instrument- and sampletracks + AudioEngineWorkerThread::fillJobQueue(m_audioPorts); + AudioEngineWorkerThread::startAndWaitForJobs(); // removed all play handles which are done for( PlayHandleList::Iterator it = m_playHandles.begin(); @@ -424,17 +435,6 @@ void AudioEngine::renderStageInstruments() -void AudioEngine::renderStageEffects() -{ - AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Effects); - - // STAGE 2: process effects of all instrument- and sampletracks - AudioEngineWorkerThread::fillJobQueue(m_audioPorts); - AudioEngineWorkerThread::startAndWaitForJobs(); -} - - - void AudioEngine::renderStageMix() { AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Mixing); diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index b715bcac0..fd729e3ab 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -186,7 +186,7 @@ void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n ) { for( fpp_t f = (fpp_t)( ( fl > desiredReleaseFrames() ) ? (std::max(fpp - desiredReleaseFrames(), 0) + - fl % fpp) : 0); f < frames; ++f) + fl) % fpp : 0); f < frames; ++f) { const float fac = (float)( fl-f-1 ) / desiredReleaseFrames(); From 84aca0a2d26e2d2b5d5f65c524107b517594efaa Mon Sep 17 00:00:00 2001 From: Bimal Poudel Date: Tue, 10 Oct 2023 13:56:46 -0600 Subject: [PATCH 002/191] Wrap resource urls in double quotes (#6898) --- data/themes/classic/style.css | 30 ++++++++++++------------- data/themes/default/style.css | 41 ++++++++++++++++------------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 522d694ff..2b853395d 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -8,7 +8,7 @@ QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar { } QMdiArea { - background-image: url(resources:background_artwork.png); + background-image: url("resources:background_artwork.png"); } lmms--gui--Knob { @@ -205,7 +205,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; - background: url(resources:cpuload_bg.png); + background: url("resources:cpuload_bg.png"); qproperty-stepSize: 4; } @@ -322,14 +322,14 @@ QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { height: 5px; } -QScrollBar::left-arrow:horizontal { background-image: url(resources:sbarrow_left.png);} -QScrollBar::right-arrow:horizontal { background-image: url(resources:sbarrow_right.png);} -QScrollBar::up-arrow:vertical { background-image: url(resources:sbarrow_up.png);} -QScrollBar::down-arrow:vertical { background-image: url(resources:sbarrow_down.png);} -QScrollBar::left-arrow:horizontal:disabled { background-image: url(resources:sbarrow_left_d.png);} -QScrollBar::right-arrow:horizontal:disabled { background-image: url(resources:sbarrow_right_d.png);} -QScrollBar::up-arrow:vertical:disabled { background-image: url(resources:sbarrow_up_d.png);} -QScrollBar::down-arrow:vertical:disabled { background-image: url(resources:sbarrow_down_d.png);} +QScrollBar::left-arrow:horizontal { background-image: url("resources:sbarrow_left.png");} +QScrollBar::right-arrow:horizontal { background-image: url("resources:sbarrow_right.png");} +QScrollBar::up-arrow:vertical { background-image: url("resources:sbarrow_up.png");} +QScrollBar::down-arrow:vertical { background-image: url("resources:sbarrow_down.png");} +QScrollBar::left-arrow:horizontal:disabled { background-image: url("resources:sbarrow_left_d.png");} +QScrollBar::right-arrow:horizontal:disabled { background-image: url("resources:sbarrow_right_d.png");} +QScrollBar::up-arrow:vertical:disabled { background-image: url("resources:sbarrow_up_d.png");} +QScrollBar::down-arrow:vertical:disabled { background-image: url("resources:sbarrow_down_d.png");} /* background for song editor and pattern editor */ @@ -367,7 +367,7 @@ lmms--gui--TrackOperationsWidget > QPushButton { } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { - image: url(resources:trackop.png); + image: url("resources:trackop.png"); subcontrol-origin: padding; subcontrol-position: center; position: relative; @@ -375,12 +375,12 @@ lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:hover { - image: url(resources:trackop_h.png); + image: url("resources:trackop_h.png"); } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:pressed, lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:checked { - image: url(resources:trackop_c.png); + image: url("resources:trackop_c.png"); position: relative; top: 2px; } @@ -409,7 +409,7 @@ lmms--gui--AutomatableSlider::groove:vertical { lmms--gui--AutomatableSlider::handle:vertical { background: none; - border-image: url(resources:main_slider.png); + border-image: url("resources:main_slider.png"); width: 26px; height: 10px; border-radius: 2px; @@ -428,7 +428,7 @@ lmms--gui--AutomatableSlider::groove:horizontal { lmms--gui--AutomatableSlider::handle:horizontal { background: none; - border-image: url(resources:horizontal_slider.png); + border-image: url("resources:horizontal_slider.png"); width: 10px; height: 26px; border-radius: 2px; diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 26b2dd14b..9cbf5885b 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -9,6 +9,7 @@ QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar, QCheckBox { QTreeView { outline: none; + alternate-background-color: #111314; } QTreeWidget::item { @@ -236,7 +237,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; - background: url(resources:cpuload_bg.png); + background: url("resources:cpuload_bg.png"); qproperty-stepSize: 1; } @@ -355,16 +356,16 @@ QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { margin-left: 3px; } -QScrollBar::left-arrow:horizontal { background-image: url(resources:sbarrow_left.png);} -QScrollBar::right-arrow:horizontal { background-image: url(resources:sbarrow_right.png);} -QScrollBar::up-arrow:vertical { background-image: url(resources:sbarrow_up.png);} -QScrollBar::down-arrow:vertical { background-image: url(resources:sbarrow_down.png);} -QScrollBar::left-arrow:horizontal:disabled { background-image: url(resources:sbarrow_left_d.png);} -QScrollBar::right-arrow:horizontal:disabled { background-image: url(resources:sbarrow_right_d.png);} -QScrollBar::up-arrow:vertical:disabled { background-image: url(resources:sbarrow_up_d.png);} -QScrollBar::down-arrow:vertical:disabled { background-image: url(resources:sbarrow_down_d.png);} -lmms--gui--EffectRackView QScrollBar::up-arrow:vertical:disabled { background-image: url(resources:sbarrow_up.png);} -lmms--gui--EffectRackView QScrollBar::down-arrow:vertical:disabled { background-image: url(resources:sbarrow_down.png);} +QScrollBar::left-arrow:horizontal { background-image: url("resources:sbarrow_left.png");} +QScrollBar::right-arrow:horizontal { background-image: url("resources:sbarrow_right.png");} +QScrollBar::up-arrow:vertical { background-image: url("resources:sbarrow_up.png");} +QScrollBar::down-arrow:vertical { background-image: url("resources:sbarrow_down.png");} +QScrollBar::left-arrow:horizontal:disabled { background-image: url("resources:sbarrow_left_d.png");} +QScrollBar::right-arrow:horizontal:disabled { background-image: url("resources:sbarrow_right_d.png");} +QScrollBar::up-arrow:vertical:disabled { background-image: url("resources:sbarrow_up_d.png");} +QScrollBar::down-arrow:vertical:disabled { background-image: url("resources:sbarrow_down_d.png");} +lmms--gui--EffectRackView QScrollBar::up-arrow:vertical:disabled { background-image: url("resources:sbarrow_up.png");} +lmms--gui--EffectRackView QScrollBar::down-arrow:vertical:disabled { background-image: url("resources:sbarrow_down.png");} /* background for song editor and pattern editor */ @@ -401,7 +402,7 @@ lmms--gui--TrackOperationsWidget > QPushButton { } lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { - image: url(resources:trackop.png); + image: url("resources:trackop.png"); subcontrol-origin: padding; subcontrol-position: center; position: relative; @@ -410,7 +411,7 @@ lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:pressed, lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:checked { - image: url(resources:trackop.png); + image: url("resources:trackop.png"); position: relative; top: 2px; } @@ -433,7 +434,7 @@ lmms--gui--AutomatableSlider::groove:vertical { lmms--gui--AutomatableSlider::handle:vertical { background: none; - border-image: url(resources:main_slider.png); + border-image: url("resources:main_slider.png"); width: 26px; height: 10px; border-radius: 2px; @@ -452,7 +453,7 @@ lmms--gui--AutomatableSlider::groove:horizontal { lmms--gui--AutomatableSlider::handle:horizontal { background: none; - border-image: url(resources:horizontal_slider.png); + border-image: url("resources:horizontal_slider.png"); width: 10px; height: 26px; border-radius: 2px; @@ -542,7 +543,7 @@ QToolButton:checked { border-top: 1px solid #1b1f22; border-bottom: 1px solid #4a515e; background: qlineargradient(spread:reflect, x1:0, y1:0, x2:0, y2:1, stop:0 #1b1f22, stop:1 #13161a); - background-image: url(resources:shadow_p.png); + background-image: url("resources:shadow_p.png"); } /* buttons with combined menu */ @@ -591,7 +592,7 @@ lmms--gui--TrackLabelButton:pressed { lmms--gui--TrackLabelButton:checked { border: 1px solid #485059; background: #1C1F24; - background-image: url(resources:track_shadow_p.png); + background-image: url("resources:track_shadow_p.png"); border-radius: none; font-size: 11px; font-weight: normal; @@ -601,7 +602,7 @@ lmms--gui--TrackLabelButton:checked { lmms--gui--TrackLabelButton:checked:pressed { border: 1px solid #2f353b; background: #0e1012; - background-image: url(resources:track_shadow_p.png); + background-image: url("resources:track_shadow_p.png"); font-size: 11px; padding: 2px 1px; font-weight: solid; @@ -718,10 +719,6 @@ lmms--gui--TimeLineWidget { qproperty-barNumberColor: rgb( 192, 192, 192 ); } -QTreeView { - alternate-background-color: #111314; -} - lmms--gui--TrackContainerView QLabel { background: none; From 21dc88c37a6d4dbefa056ab29aba73388d34a7bd Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Tue, 10 Oct 2023 21:33:50 +0100 Subject: [PATCH 003/191] Set SF2 voice tuning relative to initial value (#6924) --- plugins/Sf2Player/Sf2Player.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 79bd4b976..ee97e98ef 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -73,8 +73,8 @@ Plugin::Descriptor PLUGIN_EXPORT sf2player_plugin_descriptor = } /** - * A non-owning reference to a single FluidSynth voice, for tracking whether the - * referenced voice is still the same voice that was passed to the constructor. + * A non-owning reference to a single FluidSynth voice. Captures some initial + * properties of the referenced voice to help manage changes to it over time. */ class FluidVoice { @@ -82,12 +82,16 @@ public: //! Create a reference to the voice currently pointed at by `voice`. explicit FluidVoice(fluid_voice_t* voice) : m_voice{voice}, - m_id{fluid_voice_get_id(voice)} + m_id{fluid_voice_get_id(voice)}, + m_coarseTune{fluid_voice_gen_get(voice, GEN_COARSETUNE)} { } //! Get a pointer to the referenced voice. fluid_voice_t* get() const noexcept { return m_voice; } + //! Get the original coarse tuning of the referenced voice. + float coarseTune() const noexcept { return m_coarseTune; } + //! Test whether this object still refers to the original voice. bool isValid() const { @@ -97,6 +101,7 @@ public: private: fluid_voice_t* m_voice; unsigned int m_id; + float m_coarseTune; }; struct Sf2PluginData @@ -740,7 +745,7 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) const auto detuning = _n->currentDetuning(); for (const auto& voice : data->fluidVoices) { if (voice.isValid()) { - fluid_voice_gen_set(voice.get(), GEN_COARSETUNE, detuning); + fluid_voice_gen_set(voice.get(), GEN_COARSETUNE, voice.coarseTune() + detuning); fluid_voice_update_param(voice.get(), GEN_COARSETUNE); } } From 5c9adeb9a6057e8ff6ce1c3b2c63c59a00619abc Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Sun, 15 Oct 2023 05:28:29 +0530 Subject: [PATCH 004/191] Bump SWH and TAP LADSPA plugins (#6937) --- plugins/LadspaEffect/swh/ladspa | 2 +- plugins/LadspaEffect/tap/tap-plugins | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/LadspaEffect/swh/ladspa b/plugins/LadspaEffect/swh/ladspa index d99a0db52..02bda2320 160000 --- a/plugins/LadspaEffect/swh/ladspa +++ b/plugins/LadspaEffect/swh/ladspa @@ -1 +1 @@ -Subproject commit d99a0db521d13a87bdaa418c674ca8858e484452 +Subproject commit 02bda232041380c2846414945798cbbfecb2f3f2 diff --git a/plugins/LadspaEffect/tap/tap-plugins b/plugins/LadspaEffect/tap/tap-plugins index 198b84e6a..856402230 160000 --- a/plugins/LadspaEffect/tap/tap-plugins +++ b/plugins/LadspaEffect/tap/tap-plugins @@ -1 +1 @@ -Subproject commit 198b84e6ab37a9c979435cdb8f0a27a0e9a2934f +Subproject commit 85640223047d49a305e90ba1b92303eb066ba474 From 732e5cc3c9959dbab08668469d7f2ba6a5d81682 Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:08:03 +0530 Subject: [PATCH 005/191] fixed broken 21pink LCD display (#6914) --- data/themes/default/lcd_21pink.png | Bin 803 -> 10600 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/themes/default/lcd_21pink.png b/data/themes/default/lcd_21pink.png index c2009eedac64d7101c283db71982d39a50d493a6..719730427bc387f3adc9ba671ad795ccfa540897 100644 GIT binary patch literal 10600 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 WHH0!1t)BoB7lWs(pUXO@geCw=saY}r From 476a56e713bca22ce6760317de41176bff50610e Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sun, 15 Oct 2023 15:30:35 +0200 Subject: [PATCH 006/191] Update remote of zyn's submodule `instruments` This PR makes changes in the submodule `zynaddsubfx`. It affects its submodule `instruments`: * updates the URL to use github (since SF is less reliable) * changes the protocl from SSH to HTTPS (see https://stackoverflow.com/a/50299434) * does not change the submodule's commit ID You must now run ``` git submodule sync --recursive ``` once to reflect this change. --- plugins/ZynAddSubFx/zynaddsubfx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ZynAddSubFx/zynaddsubfx b/plugins/ZynAddSubFx/zynaddsubfx index 551e816a6..7ad5663cb 160000 --- a/plugins/ZynAddSubFx/zynaddsubfx +++ b/plugins/ZynAddSubFx/zynaddsubfx @@ -1 +1 @@ -Subproject commit 551e816a6334fd190c74ce971378063b2757b47b +Subproject commit 7ad5663cbeebc02d73fd3ad666e428c1287f2cda From 259d1207256c1f93ef030e3ccc91d357bfd388b9 Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Sat, 21 Oct 2023 17:46:15 +0530 Subject: [PATCH 007/191] Fix invalid for loop to while (#6943) --- src/gui/editors/PianoRoll.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index cef2205d2..6bf1b5daf 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -2517,7 +2517,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) // We iterate from last note in MIDI clip to the first, // chronologically auto it = notes.rbegin(); - for( int i = 0; i < notes.size(); ++i ) + while (it != notes.rend()) { Note* n = *it; From 6bde53e4a9c069e83d8891f5af609dc06c8f3040 Mon Sep 17 00:00:00 2001 From: consolegrl <5942029+consolegrl@users.noreply.github.com> Date: Sat, 21 Oct 2023 11:50:15 -0400 Subject: [PATCH 008/191] Update old file filtering code (#6882) * Updated some old file filtering code. I was trying to add a way to show/hide the .bak and other files with some filter buttons when I noticed the code in FileBrowser::addItems was copy pasta that had lava flowed from the much more modern Directory::addItems. In addition, only, FileBrowser::addItems was not respecting the filter's at all. I brought both of them into line with Qt 5 practices which now respects the m_filter list of extensions for both FileBrowser and Directory. In Directory::addItems I only needed to change where the match was being done, FileBrowser::addItems didn't try to filter/match at all. * Set name filters inside entryInfoList call, const Some fixes for const iterating the file list. Setting file name filters along with the call instead of seperately. * Style changes src/gui/FileBrowser.cpp Co-authored-by: saker * Fixed style/format FileBrowser.cpp --------- Co-authored-by: saker --- src/gui/FileBrowser.cpp | 60 ++++++++++++++++++++--------------------- src/gui/MainWindow.cpp | 2 +- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 181e67cd7..74d8f755a 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -328,68 +328,63 @@ void FileBrowser::addItems(const QString & path ) } // try to add all directories from file system alphabetically into the tree - QDir cdir( path ); - QStringList files = cdir.entryList( QDir::Dirs, QDir::Name ); - files.sort(Qt::CaseInsensitive); - for( QStringList::const_iterator it = files.constBegin(); - it != files.constEnd(); ++it ) + QDir cdir(path); + if (!cdir.isReadable()) { return; } + QFileInfoList entries = cdir.entryInfoList( + m_filter.split(' '), + QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, + QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + for (const auto& entry : entries) { - QString cur_file = *it; - if( cur_file[0] != '.' ) + QString fileName = entry.fileName(); + if (entry.isDir()) { + // Merge dir's together bool orphan = true; - for( int i = 0; i < m_fileBrowserTreeWidget->topLevelItemCount(); ++i ) + for (int i = 0; i < m_fileBrowserTreeWidget->topLevelItemCount(); ++i) { auto d = dynamic_cast(m_fileBrowserTreeWidget->topLevelItem(i)); - if( d == nullptr || cur_file < d->text( 0 ) ) + if (d == nullptr || fileName < d->text(0)) { // insert before item, we're done - auto dd = new Directory(cur_file, path, m_filter); - m_fileBrowserTreeWidget->insertTopLevelItem( i,dd ); + auto dd = new Directory(fileName, path, m_filter); + m_fileBrowserTreeWidget->insertTopLevelItem(i,dd); dd->update(); // add files to the directory orphan = false; break; } - else if( cur_file == d->text( 0 ) ) + else if (fileName == d->text(0)) { // imagine we have subdirs named "TripleOscillator/xyz" in // two directories from m_directories // then only add one tree widget for both // so we don't add a new Directory - we just // add the path to the current directory - d->addDirectory( path ); + d->addDirectory(path); d->update(); orphan = false; break; } } - if( orphan ) + if (orphan) { // it has not yet been added yet, so it's (lexically) // larger than all other dirs => append it at the bottom - auto d = new Directory(cur_file, path, m_filter); + auto d = new Directory(fileName, path, m_filter); d->update(); - m_fileBrowserTreeWidget->addTopLevelItem( d ); + m_fileBrowserTreeWidget->addTopLevelItem(d); } } - } - - files = cdir.entryList( QDir::Files, QDir::Name ); - for( QStringList::const_iterator it = files.constBegin(); - it != files.constEnd(); ++it ) - { - QString cur_file = *it; - if( cur_file[0] != '.' ) + else if (entry.isFile()) { // TODO: don't insert instead of removing, order changed // remove existing file-items - QList existing = m_fileBrowserTreeWidget->findItems( - cur_file, Qt::MatchFixedString ); - if( !existing.empty() ) + QList existing = m_fileBrowserTreeWidget->findItems(fileName, Qt::MatchFixedString); + if (!existing.empty()) { delete existing.front(); } - (void) new FileItem( m_fileBrowserTreeWidget, cur_file, path ); + (void) new FileItem(m_fileBrowserTreeWidget, fileName, path); } } } @@ -1063,8 +1058,11 @@ bool Directory::addItems(const QString& path) treeWidget()->setUpdatesEnabled(false); - QFileInfoList entries = thisDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDir::LocaleAware | QDir::DirsFirst | QDir::Name); - for (auto& entry : entries) + QFileInfoList entries = thisDir.entryInfoList( + m_filter.split(' '), + QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, + QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + for (const auto& entry : entries) { QString fileName = entry.fileName(); if (entry.isDir()) @@ -1073,7 +1071,7 @@ bool Directory::addItems(const QString& path) addChild(dir); m_dirCount++; } - else if (entry.isFile() && thisDir.match(m_filter, fileName.toLower())) + else if (entry.isFile()) { auto fileItem = new FileItem(fileName, path); addChild(fileItem); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 10805fe01..f867d86d9 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -112,7 +112,7 @@ MainWindow::MainWindow() : sideBar->appendTab( new FileBrowser( confMgr->userProjectsDir() + "*" + confMgr->factoryProjectsDir(), - "*.mmp *.mmpz *.xml *.mid", + "*.mmp *.mmpz *.xml *.mid *.mpt", tr( "My Projects" ), embed::getIconPixmap( "project_file" ).transformed( QTransform().rotate( 90 ) ), splitter, false, true, From 63d03fa3a70ce538ecb8417a9c55d95ee17c5648 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sat, 21 Oct 2023 17:39:20 +0200 Subject: [PATCH 009/191] Fix Uninitialized Value Conditional Jump Introduced in #5970 , `CPULoadWidget::m_stepSize` is uninitialized, leading to valgrind warnings: ``` ==9429== Conditional jump or move depends on uninitialised value(s) ==9429== at 0x3715A9: int const& std::max(int const&, int const&) (stl_algobase.h:262) ==9429== by 0x59E3BB: lmms::gui::CPULoadWidget::stepSize() const (CPULoadWidget.h:59) ==9429== by 0x59DA2E: lmms::gui::CPULoadWidget::paintEvent(QPaintEvent*) (CPULoadWidget.cpp:78) ``` Even though `grep` shows: ``` data/themes/classic/style.css: qproperty-stepSize: 4; data/themes/default/style.css: qproperty-stepSize: 1; ``` It seems like the issue is caused by paint events occurring even before the style sheet has been applied. In order to fix this, and to be future proof with style sheets which do not set it, we initialize `CPULoadWidget::m_stepSize` to `1`. --- include/CPULoadWidget.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/CPULoadWidget.h b/include/CPULoadWidget.h index dfa5bac73..bed10b05e 100644 --- a/include/CPULoadWidget.h +++ b/include/CPULoadWidget.h @@ -68,7 +68,7 @@ private: QTimer m_updateTimer; - int m_stepSize; + int m_stepSize = 1; } ; From d5e6ac6dc5c5a21fcf4f5bfad9aff429402dc999 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:51:00 +0200 Subject: [PATCH 010/191] Lv2Proc: Delay setting worker iface (Fixes #6946) (#6947) * Lv2Proc: Delay worker iface (Fixes #6946) (#6947) This delays passing the `LV2_Worker_Interface` to the `Lv2Worker` class, because prior to the patch, the instance, which provides the interface, has not been initialized yet, which resulted in a segfault. * Update src/core/lv2/Lv2Worker.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * setIface -> setInterface * `if(` -> `if (` * Update src/core/lv2/Lv2Proc.cpp Co-authored-by: saker * Rework, editorial, from @sakertooth * Fixup: `interface` is reserved on MSVC https://stackoverflow.com/a/25234279 * Apply suggestions from code review Co-authored-by: saker * Initialize handle/interface as nullptr --------- Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker --- include/Lv2Worker.h | 13 ++++++----- src/core/lv2/Lv2Proc.cpp | 13 ++++++----- src/core/lv2/Lv2Worker.cpp | 46 +++++++++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/include/Lv2Worker.h b/include/Lv2Worker.h index 7931f8e7c..90a3d9d4f 100644 --- a/include/Lv2Worker.h +++ b/include/Lv2Worker.h @@ -47,16 +47,17 @@ class Lv2Worker { public: // CTOR/DTOR/feature access - Lv2Worker(const LV2_Worker_Interface* iface, Semaphore* common_work_lock, bool threaded); + Lv2Worker(Semaphore* commonWorkLock, bool threaded); ~Lv2Worker(); - void setHandle(LV2_Handle handle) { m_handle = handle; } + void setHandle(LV2_Handle handle); + void setInterface(const LV2_Worker_Interface* newInterface); LV2_Worker_Schedule* feature() { return &m_scheduleFeature; } // public API void emitResponses(); void notifyPluginThatRunFinished() { - if(m_iface->end_run) { m_iface->end_run(m_scheduleFeature.handle); } + if(m_interface->end_run) { m_interface->end_run(m_scheduleFeature.handle); } } // to be called only by static functions @@ -69,9 +70,9 @@ private: std::size_t bufferSize() const; //!< size of internal buffers // parameters - const LV2_Worker_Interface* m_iface; - bool m_threaded; - LV2_Handle m_handle; + const bool m_threaded; + const LV2_Worker_Interface* m_interface = nullptr; + LV2_Handle m_handle = nullptr; LV2_Worker_Schedule m_scheduleFeature; // threading/synchronization diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 11290013e..bd89cea0a 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -442,11 +442,16 @@ void Lv2Proc::initPlugin() if (m_instance) { - if(m_worker) { + const auto iface = static_cast( + lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface)); + if (iface) { m_worker->setHandle(lilv_instance_get_handle(m_instance)); + m_worker->setInterface(iface); } for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum) + { connectPort(portNum); + } lilv_instance_activate(m_instance); } else @@ -528,12 +533,10 @@ void Lv2Proc::initPluginSpecificFeatures() // worker (if plugin has worker extension) Lv2Manager* mgr = Engine::getLv2Manager(); if (lilv_plugin_has_extension_data(m_plugin, mgr->uri(LV2_WORKER__interface).get())) { - const auto iface = static_cast( - lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface)); bool threaded = !Engine::audioEngine()->renderOnly(); - m_worker.emplace(iface, &m_workLock, threaded); + m_worker.emplace(&m_workLock, threaded); m_features[LV2_WORKER__schedule] = m_worker->feature(); - // Note: m_worker::setHandle will still need to be called later + // note: the worker interface can not be instantiated yet - it requires m_instance. see initPlugin() } } diff --git a/src/core/lv2/Lv2Worker.cpp b/src/core/lv2/Lv2Worker.cpp index 5af955ff7..c763bacad 100644 --- a/src/core/lv2/Lv2Worker.cpp +++ b/src/core/lv2/Lv2Worker.cpp @@ -60,10 +60,7 @@ std::size_t Lv2Worker::bufferSize() const -Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, - Semaphore* common_work_lock, - bool threaded) : - m_iface(iface), +Lv2Worker::Lv2Worker(Semaphore* commonWorkLock, bool threaded) : m_threaded(threaded), m_response(bufferSize()), m_requests(bufferSize()), @@ -71,9 +68,8 @@ Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, m_requestsReader(m_requests), m_responsesReader(m_responses), m_sem(0), - m_workLock(common_work_lock) + m_workLock(commonWorkLock) { - assert(iface); m_scheduleFeature.handle = static_cast(this); m_scheduleFeature.schedule_work = [](LV2_Worker_Schedule_Handle handle, uint32_t size, const void* data) -> LV2_Worker_Status @@ -91,6 +87,24 @@ Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, +void Lv2Worker::setHandle(LV2_Handle handle) +{ + assert(handle); + m_handle = handle; +} + + + + +void Lv2Worker::setInterface(const LV2_Worker_Interface* newInterface) +{ + assert(newInterface); + m_interface = newInterface; +} + + + + Lv2Worker::~Lv2Worker() { m_exit = true; @@ -120,7 +134,9 @@ LV2_Worker_Status Lv2Worker::respond(uint32_t size, const void* data) } else { - m_iface->work_response(m_handle, size, data); + assert(m_handle); + assert(m_interface); + m_interface->work_response(m_handle, size, data); } return LV2_WORKER_SUCCESS; } @@ -136,6 +152,7 @@ void Lv2Worker::workerFunc() while (true) { m_sem.wait(); if (m_exit) { break; } + const std::size_t readSpace = m_requestsReader.read_space(); if (readSpace <= sizeof(size)) { continue; } // (should not happen) @@ -144,8 +161,10 @@ void Lv2Worker::workerFunc() if(size > buf.size()) { buf.resize(size); } if(size) { m_requestsReader.read(size).copy(buf.data(), size); } + assert(m_handle); + assert(m_interface); m_workLock->wait(); - m_iface->work(m_handle, staticWorkerRespond, this, size, buf.data()); + m_interface->work(m_handle, staticWorkerRespond, this, size, buf.data()); m_workLock->post(); } } @@ -172,9 +191,11 @@ LV2_Worker_Status Lv2Worker::scheduleWork(uint32_t size, const void *data) } else { + assert(m_handle); + assert(m_interface); // Execute work immediately in this thread m_workLock->wait(); - m_iface->work(m_handle, staticWorkerRespond, this, size, data); + m_interface->work(m_handle, staticWorkerRespond, this, size, data); m_workLock->post(); } @@ -189,10 +210,13 @@ void Lv2Worker::emitResponses() { std::size_t read_space = m_responsesReader.read_space(); uint32_t size; - while (read_space > sizeof(size)) { + while (read_space > sizeof(size)) + { + assert(m_handle); + assert(m_interface); m_responsesReader.read(sizeof(size)).copy((char*)&size, sizeof(size)); if(size) { m_responsesReader.read(size).copy(m_response.data(), size); } - m_iface->work_response(m_handle, size, m_response.data()); + m_interface->work_response(m_handle, size, m_response.data()); read_space -= sizeof(size) + size; } } From c6ed4a274abeab4e811460fd4c5cc06832821f15 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 29 Oct 2023 11:16:39 +0100 Subject: [PATCH 011/191] First version of a new dynamic LADSPA control dialog (#2068) Introduce a new version of the LADSPA control dialog which uses "bar controllers" and arranges them in a grid layout. Long parameter names are elided long if needed. The new dialog is implemented in the class `LadspaMatrixControlDialog`. Note: it is still possible to reactivate the old dialog as it has not been removed yet. You can do so in `plugins/LadspaEffect/LadspaControls.h` by replacing the implementation of `createView()` with the following: ``` gui::EffectControlDialog* createView() override { return new gui::LadspaControlDialog( this ); } ``` Introduce the new base class `FloatModelEditorBase`. It serves as the base widget for widgets that manipulate a float model and provides some base functionalities like context menus, edit dialogs, mouse handling, etc. Currently `BarModelEditor` and `Knob` inherit from `FloatModelEditorBase`. Therefore lots of code was moved from `Knob` to `FloatModelEditorBase`. `BarModelEditor` supports style sheets. See the changes in style.css for the available options and their usage. Extend `LedCheckBox` so that it adds support to render the text with the default font in the default size. The class now supports two modes: * Legacy mode * Non-legacy mode Legacy mode is the default and renders the LedCheckBox as before. The font is set to 7 pixels and all the text is rendered with a shadow. Non-legacy mode uses the current font to render the text. The text is rendered without a shadow and the pixmap is centered vertically to the left side of the text. Non-legacy mode is currently only used in the context of the matrix LADSPA dialog. Toggle options are rendered using it as well as the "Link Channels" button. Add `TempoSyncBarModelEditor` which adds a tempo sync option to a `BarModelEditor`. Please note that the code was mostly copied and adjusted from the `TempoSyncKnob` class. It was not attempted yet to unify some of the code because with the current design it seems to be a road to "inheritance hell". A better solution might be to refactor the code so that it becomes more composable. What follows are the individual commit messages of the 64 commits that have been squashed into a single merge commit. * First version of a new dynamic LADSPA control dialog The new dialog shows the LADSPA controls in a matrix layout. Each row corresponds to a LADSPA parameter. Each parameter can have several channels which can be linked. Each channel has its own row of controls. The class LadspaMatrixControlView was added by copying and modifying LadspaControlView to get rid of the link buttons which are associated with some controls and some labels. * Remove trailing whitespaces * Merge fix * Use the new connect syntax * Remove empty destructors * Use override keyword * Reformat a bit * Use nullptr * Use range-based loops * Add BarModelEditor and improve layouts Add the new class `BarModelEditor` which is intended to become a new way to adjust values of float models. Simplify the layout in `LadspaMatrixControlDialog` by removing some nested layouts. Remove the "Parameters" column. Adjust `LadspaMatrixControlView` to implement the following changes: * Show the name of the control next to toggle buttons (`LedCheckBox`). * Use the new `BarModelEditor` for integer and float types. * SHow the name of the control next to time based parameters that use `TempoSyncKnob`. The names are shown so that the "Parameters" column can be removed. Technical details ------------------ The class `LadspaMatrixControlDialog` now creates a widget that contains the matrix layout with the controls. This widget is then added to a scroll area. The layout is populated in the new method `arrangeControls`. Add some helper methods to `LadspaMatrixControlDialog` which retrieve the `LadspaControls` instance and the number of channels. Add the implementation of `BarModelEditor` to `src/gui/CMakeLists.txt`. TODOs ------ Extract common code out of the `Knob` class so that it can be reused by `BarModelEditor`. * Use factory to create LADSPA control widgets Replace the class `LadspaMatrixControlView` with the factory class `LadspaWidgetFactory`. The former was a widget that wrapped the widget representation of a LADSPA control in yet another widget with a layout. The factory simply returns the configured widget so that it can be incorporated directly in layouts or other widgets. Adjust `LadspaMatrixControlDialog` so that it uses the `LadspaWidgetFactory` instead of the `LadspaMatrixControlView`. * Initial version of FloatModelEditorBase Create an initial version of `FloatModelEditorBase`. It is intended to become the base widget for all widgets that manipulate a float model and provides some base functionalities like context menus, edit dialogs, mouse handling, etc. The initial version is a copy of `Knob`. The enum and its values have been suffixed with "Temp" as they will be removed later anyway. Same applies for the function `convertPixmapToGrayScaleTemp`. * Remove Knob related code from FloatModelEditorBase First removal of Knob related code from FloatModelEditorBase. * Remove setHtmlLabel * Remove enum for knob types * Remove label related code Remove setLabel and the members m_label and m_isHtmlLabel. * Remove several Qt properties * Remove angle related members and methods * Remove unused members and includes * Clean up includes * Make BarModelEditor inherit from FloatModelEditorBase Make `BarModelEditor` inherit from `FloatModelEditorBase` so that it inherits all shared functionality. Currently the class mostly implements size related methods and overrides the paint method. * Move LadspaWidgetFactory to LadspaEffect Move the class `LadspaWidgetFactory` to the project LadspaEffect to make it hopefully compile with mingw32 and mingw64. Interestingly the code compiled and worked under Linux and MacOS. * Code adjustments after scripted checks Add an include guard and an additional `#pragme once`. Add comments to closing namespace scopes. * Export BarModelEditor Export BarModelEditor so that for example the LadspaEffect project/ plugin can use it. * Improve rendering of bar model editor Increase the margin from 2 to 3 so that we have a more prominent border around the filled area. Fix a problem in the rendering code which led to situations where the bar was drawn to the left of the start position for small values. Return a more exact minimum size hint. * Elide long parameter names Elide long parameter names if needed. * Enable horizontal direction of manipulation Extend `FloatModelEditorBase` so that it also allows manipulation of the values when the mouse is moved in horizontal directions. The default is to use the vertical direction. Make use of this new feature in `BarModelEditor` which now reacts to horizontal movements when changing values. * Represent enum parameters using the bar widget The implementation of the current LADSPA dialog in master uses knobs to represent enum parameters. Therefore it should also work with the new bar widget. This gets rid of many labels with the parameter name followed by "(unsupported)". * Remove TODO in LadspaWidgetFactory * Render text of LedCheckBox with default font Extend LedCheckBox so that it adds support to render the text with the default font in the default size. The class now supports two modes: * Legacy mode * Non-legacy mode Legacy mode is the default and renders the LedCheckBox as before. The font is set to 7 pixels and all the text is rendered with a shadow. Non-legacy mode uses the current font to render the text. The text is rendered without a shadow and the pixmap is centered vertically to the left side of the text. The non-legacy mode is currently only used in the context of the matrix LADSPA dialog. Toggle options are rendered using it as well as the "Link Channels" button. * Use "yellow" LEDs for bool parameters Use "yellow" LEDs (which are actually blue) for dynamically added bool parameters so that the dialog is consistent with regards to the LED colors. The "Link Channels" button also uses a "yellow" LED. * Fix Qt5 builds Fix the Qt5 builds which do not know horizontalAdvance as a member of FontMetrics. Also refactor LedCheckBox::onTextUpdated to make it more compact. * Style sheets for BarModelEditor Add style sheets options for BarModelEditor. See the changes in style.css for the available options and their usage. The members that can be manipulated by the style sheet options are initialized accoriding to the palette so that the BarModelEditor should also render something useful if no style is set. Adjust the paintEvent method to use the style sheet brushes and colors. * Layout optimizations Set the vertical scroll bar to always show so that the controls do not move around if the scroll bar is hidden or shown. Set the margin of the grid layout to 0 so that the whole dialog becomes tighter. * Get rid of initial scroll bars Make sure that the widget is shown without a scrollbar whenever possible using the following rules. If the widget fits on the workspace we use the height of the widget as the minimum size of the scroll area. This will ensure that the scrollbar is not shown initially (and never will be). If the widget is larger than the workspace then we want it to mostly cover the workspace. In that case the minimum height is set to 90 % of the workspace. * Switch to a green theme Switch to a green theme to better match the default theme. * Adjust classic theme Adjust the classic theme so that it renders the bars in a legible way. * Fixes for CodeFactor Remove virtual keyword from methods that are marked as override. Remove whitespaces that make CodeFactor unhappy. One of these fixes includes moving the implementation of LadspaMatrixControlDialog::isResizable into the cpp file. * CodeFactor is still unhappy Remove a blank line after the constructor. * Center align time based controls Align time based controls, i.e. knobs, in the center. The implementation defeats the purpose of the widget factory a bit but it makes the design look nicer. * Fix build Fix the build by adjusting the enums. I added the changes while writing the commit message for the merge commit but they did not make it. * Attempt at CodeFactor warning Try to make the last CodeFactor warning disappear. * Add bar controller with tempo sync option Add `TempoSyncBarModelEditor` which adds a tempo sync option to a `BarModelEditor`. Please note that the code was mostly copied and adjusted from the `TempoSyncKnob` class. It was not attempted yet to unify some of the code because with the current design it seems to be a road to "inheritance hell". A better solution might be to refactor the code so that it is more composable. Another option might be to move the tempo sync functionality into a class like `FloatModelEditorBase`. Although this would not be a 100% right place either because `TempoSyncKnobModel` inherits from `FloatModel`. In that case we'd have specialized code in a generic base class. Adjust `LadspaWidgetFactory` so that it now returns an instance of a `TempoSyncBarModelEditor` instead of a `TempoSyncKnob`. Remove the extra layout code from `LadspaMatrixControlDialog.cpp` as most things are bar editors now. Adjust `TempoSyncKnobModel` so we do not have to make `TempoSyncBarModelEditor` a friend of it. * Another attempt to please CodeFactor Have another attempt to fix CodeFactor's complaints about `LadspaMatrixControlDialog.h`. * Coding conventions, blanks and blank lines Remove lots of unnecessary white space. Remove blank lines. Remove leading underscores from parameters. Remove line breaks in `TempoSyncBarModelEditor.cpp` to make the code more readable. Remove repeated method calls by introducing local variables. * Break down complex method Break down the complex method `TempoSyncBarModelEditor::updateDescAndIcon` by delegating to the new methods `updateTextDescription` and `updateIcon`. * Another attempt to please CodeFactor Another attempt to fix `LadspaMatrixControlDialog.h` to please CodeFactor. * Work on TODOs The TODO "Assumes that all processors are equal..." has been turned into a comment when we start iterating over the channels. If the channels are not of the same structure this would likely also have been a problem in the old implementation. The TODO "Use a factory..." has been removed as this is already done. The include of `FloatModelEditorBase.h` in LadspaWidgetFactory has been removed. * Modifier keys for mouse wheel adjustments Integrate the changes of commit 7000afb2eac into `FloatModelEditorBase`. This commit added modifier keys for mouse wheel adjustments to the `Knob` class. The port to `FloatModelEditorBase` results in the bar control now also supporting different scales of adjustments that can be switched with the Shift, Ctrl and Alt keys. * Show current value on mouse over Integrate the changes of commit fcdf4c05684 into `FloatModelEditorBase`. This commit lets users show the current value of a float model when the mouse is moved over the control. * Make Knob inherit from FloatModelEditorBase Make `Knob` inherit from `FloatModelEditorBase`. This is mostly a continuation for the changes introduced with commit c63d86f. The idea is that `FloatModelEditorBase` contains the underlying functionality and logic to deal with float models. `Knob` and other classes then only override the presentation aspects. This way `Knob` and `BarModelEditor` can share the same functionality but can differ in how they present the data. Technical details ------------------ Remove all methods that are already defined in `FloatModelEditorBase` from `Knob`. These are all methods that are defined in the same way in `FloatModelEditorBase`. The method `paintEvent` is not removed because it is overridden by `Knob` as it has its own presentation. Remove forward declaration of `QPixmap` from `FloatModelEditorBase` as it was not used. Remove unused function `convertPixmapToGrayScaleTemp` from `FloatModelEditorBase`. * Cleanup includes in Knob class * Code style changes in FloatModelEditorBase `FloatModelEditorBase` is a new class/file. Therefore we can do some extensive code cleanup on the code that was initially copied from `Knob`. Adjust whitespace for methods and some if-statements. Remove underscores from parameter names. Use only two blank lines between method definitions. * Cleanup include for FloatModelEditorBase * Show effect name as title of dialog Add the effect name as the title in the content of the window. This improves the structure of the dialog as it is now clearer from the content itself to which effect the controls belong to. This duplicates the information from the window title. However, the window title is rather small and the larger font in the content makes it easier to find an effect in a set of opened dialogs. * Revert "Show effect name as title of dialog" This reverts commit 8ce0d366d0c0b7fbc629ebf0227a3ed155f91d5c. * Fix problem with LedCheckBox in grid layout The LedCheckBox does not play nice when being used in a grid layout as it disables the resizing behavior of every column it appears in. The solution is to wrap it into the layout of a parent widget that knows how to behave. * Reduce minimum width of BarModelEditor Reduce the minimum width of `BarModelEditor` from 200 to 50. --------- Co-authored-by: Hyunjin Song --- data/themes/classic/style.css | 5 + data/themes/default/style.css | 5 + include/BarModelEditor.h | 76 +++ include/FloatModelEditorBase.h | 121 +++++ include/Knob.h | 67 +-- include/LadspaControl.h | 2 + include/LedCheckBox.h | 11 +- include/TempoSyncBarModelEditor.h | 90 ++++ include/TempoSyncKnobModel.h | 3 + plugins/LadspaEffect/CMakeLists.txt | 2 +- plugins/LadspaEffect/LadspaControls.h | 4 +- .../LadspaMatrixControlDialog.cpp | 243 +++++++++ .../LadspaEffect/LadspaMatrixControlDialog.h | 91 ++++ plugins/LadspaEffect/LadspaWidgetFactory.cpp | 83 ++++ plugins/LadspaEffect/LadspaWidgetFactory.h | 49 ++ src/gui/CMakeLists.txt | 3 + src/gui/widgets/BarModelEditor.cpp | 117 +++++ src/gui/widgets/FloatModelEditorBase.cpp | 464 ++++++++++++++++++ src/gui/widgets/Knob.cpp | 422 +--------------- src/gui/widgets/LedCheckBox.cpp | 75 ++- src/gui/widgets/TempoSyncBarModelEditor.cpp | 302 ++++++++++++ 21 files changed, 1727 insertions(+), 508 deletions(-) create mode 100644 include/BarModelEditor.h create mode 100644 include/FloatModelEditorBase.h create mode 100644 include/TempoSyncBarModelEditor.h create mode 100644 plugins/LadspaEffect/LadspaMatrixControlDialog.cpp create mode 100644 plugins/LadspaEffect/LadspaMatrixControlDialog.h create mode 100644 plugins/LadspaEffect/LadspaWidgetFactory.cpp create mode 100644 plugins/LadspaEffect/LadspaWidgetFactory.h create mode 100644 src/gui/widgets/BarModelEditor.cpp create mode 100644 src/gui/widgets/FloatModelEditorBase.cpp create mode 100644 src/gui/widgets/TempoSyncBarModelEditor.cpp diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 2b853395d..c4fe0e153 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -975,6 +975,11 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--BarModelEditor { + qproperty-backgroundBrush: rgba(28, 73, 51, 255); + qproperty-barBrush: rgba(17, 136, 71, 255); +} + /* palette information */ lmms--gui--LmmsPalette { diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 9cbf5885b..80d56a4bb 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -1019,6 +1019,11 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--BarModelEditor { + qproperty-backgroundBrush: rgba(28, 73, 51, 255); + qproperty-barBrush: rgba(17, 136, 71, 255); +} + /* palette information */ lmms--gui--LmmsPalette { diff --git a/include/BarModelEditor.h b/include/BarModelEditor.h new file mode 100644 index 000000000..79a320a7d --- /dev/null +++ b/include/BarModelEditor.h @@ -0,0 +1,76 @@ +/* + * BarModelEditor.h - edit model values using a bar display + * + * Copyright (c) 2023-now Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#ifndef LMMS_GUI_BAR_MODEL_EDITOR_H +#define LMMS_GUI_BAR_MODEL_EDITOR_H + +#include "FloatModelEditorBase.h" + + +namespace lmms::gui +{ + +class LMMS_EXPORT BarModelEditor : public FloatModelEditorBase +{ + Q_OBJECT + +public: + Q_PROPERTY(QBrush backgroundBrush READ getBackgroundBrush WRITE setBackgroundBrush) + Q_PROPERTY(QBrush barBrush READ getBarBrush WRITE setBarBrush) + Q_PROPERTY(QColor textColor READ getTextColor WRITE setTextColor) + + BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + + // Define how the widget will behave in a layout + QSizePolicy sizePolicy() const; + + QSize minimumSizeHint() const override; + + QSize sizeHint() const override; + + QBrush const & getBackgroundBrush() const; + void setBackgroundBrush(QBrush const & backgroundBrush); + + QBrush const & getBarBrush() const; + void setBarBrush(QBrush const & barBrush); + + QColor const & getTextColor() const; + void setTextColor(QColor const & textColor); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QString const m_text; + + QBrush m_backgroundBrush; + QBrush m_barBrush; + QColor m_textColor; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_BAR_MODEL_EDITOR_H diff --git a/include/FloatModelEditorBase.h b/include/FloatModelEditorBase.h new file mode 100644 index 000000000..72f1450de --- /dev/null +++ b/include/FloatModelEditorBase.h @@ -0,0 +1,121 @@ +/* + * FloatModelEditorBase.h - Base editor for float models + * + * Copyright (c) 2004-2008 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H +#define LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H + +#include +#include + +#include "AutomatableModelView.h" + + +namespace lmms::gui +{ + +class SimpleTextFloat; + +class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView +{ + Q_OBJECT + + mapPropertyFromModel(bool, isVolumeKnob, setVolumeKnob, m_volumeKnob); + mapPropertyFromModel(float, volumeRatio, setVolumeRatio, m_volumeRatio); + + void initUi(const QString & name); //!< to be called by ctors + +public: + enum class DirectionOfManipulation + { + Vertical, + Horizontal + }; + + FloatModelEditorBase(DirectionOfManipulation directionOfManipulation = DirectionOfManipulation::Vertical, QWidget * _parent = nullptr, const QString & _name = QString()); //!< default ctor + FloatModelEditorBase(const FloatModelEditorBase& other) = delete; + + // TODO: remove + inline void setHintText(const QString & txt_before, const QString & txt_after) + { + setDescription(txt_before); + setUnit(txt_after); + } + +signals: + void sliderPressed(); + void sliderReleased(); + void sliderMoved(float value); + + +protected: + void contextMenuEvent(QContextMenuEvent * me) override; + void dragEnterEvent(QDragEnterEvent * dee) override; + void dropEvent(QDropEvent * de) override; + void focusOutEvent(QFocusEvent * fe) override; + void mousePressEvent(QMouseEvent * me) override; + void mouseReleaseEvent(QMouseEvent * me) override; + void mouseMoveEvent(QMouseEvent * me) override; + void mouseDoubleClickEvent(QMouseEvent * me) override; + void paintEvent(QPaintEvent * me) override; + void wheelEvent(QWheelEvent * me) override; + + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual float getValue(const QPoint & p); + +private slots: + virtual void enterValue(); + void friendlyUpdate(); + void toggleScale(); + +private: + virtual QString displayValue() const; + + void doConnections() override; + + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); + void setPosition(const QPoint & p); + + inline float pageSize() const + { + return (model()->maxValue() - model()->minValue()) / 100.0f; + } + + static SimpleTextFloat * s_textFloat; + + BoolModel m_volumeKnob; + FloatModel m_volumeRatio; + + QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent + float m_leftOver; + bool m_buttonPressed; + + DirectionOfManipulation m_directionOfManipulation; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H diff --git a/include/Knob.h b/include/Knob.h index d5739bb1c..3c3339a6f 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -26,12 +26,9 @@ #define LMMS_GUI_KNOB_H #include -#include -#include -#include #include -#include "AutomatableModelView.h" +#include "FloatModelEditorBase.h" class QPixmap; @@ -50,7 +47,7 @@ enum class KnobType void convertPixmapToGrayScale(QPixmap &pixMap); -class LMMS_EXPORT Knob : public QWidget, public FloatModelView +class LMMS_EXPORT Knob : public FloatModelEditorBase { Q_OBJECT Q_ENUMS( KnobType ) @@ -72,9 +69,6 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView Q_PROPERTY(QColor arcActiveColor MEMBER m_arcActiveColor) Q_PROPERTY(QColor arcInactiveColor MEMBER m_arcInactiveColor) - mapPropertyFromModel(bool,isVolumeKnob,setVolumeKnob,m_volumeKnob); - mapPropertyFromModel(float,volumeRatio,setVolumeRatio,m_volumeRatio); - Q_PROPERTY(KnobType knobNum READ knobNum WRITE setknobNum) Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor) @@ -87,13 +81,6 @@ public: Knob( QWidget * _parent = nullptr, const QString & _name = QString() ); //!< default ctor Knob( const Knob& other ) = delete; - // TODO: remove - inline void setHintText( const QString & _txt_before, - const QString & _txt_after ) - { - setDescription( _txt_before ); - setUnit( _txt_after ); - } void setLabel( const QString & txt ); void setHtmlLabel( const QString &htmltxt ); @@ -125,46 +112,16 @@ public: void setTextColor( const QColor & c ); -signals: - void sliderPressed(); - void sliderReleased(); - void sliderMoved( float value ); - - protected: - void contextMenuEvent( QContextMenuEvent * _me ) override; - void dragEnterEvent( QDragEnterEvent * _dee ) override; - void dropEvent( QDropEvent * _de ) override; - void focusOutEvent( QFocusEvent * _fe ) override; - void mousePressEvent( QMouseEvent * _me ) override; - void mouseReleaseEvent( QMouseEvent * _me ) override; - void mouseMoveEvent( QMouseEvent * _me ) override; - void mouseDoubleClickEvent( QMouseEvent * _me ) override; void paintEvent( QPaintEvent * _me ) override; - void wheelEvent( QWheelEvent * _me ) override; + void changeEvent(QEvent * ev) override; - void enterEvent(QEvent *event) override; - void leaveEvent(QEvent *event) override; - - virtual float getValue( const QPoint & _p ); - -private slots: - virtual void enterValue(); - void friendlyUpdate(); - void toggleScale(); - private: - virtual QString displayValue() const; - - void doConnections() override; - QLineF calculateLine( const QPointF & _mid, float _radius, float _innerRadius = 1) const; void drawKnob( QPainter * _p ); - void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); - void setPosition( const QPoint & _p ); bool updateAngle(); int angleFromValue( float value, float minValue, float maxValue, float totalAngle ) const @@ -172,25 +129,11 @@ private: return static_cast( ( value - 0.5 * ( minValue + maxValue ) ) / ( maxValue - minValue ) * m_totalAngle ) % 360; } - inline float pageSize() const - { - return ( model()->maxValue() - model()->minValue() ) / 100.0f; - } - - - static SimpleTextFloat * s_textFloat; - QString m_label; bool m_isHtmlLabel; QTextDocument* m_tdRenderer; std::unique_ptr m_knobPixmap; - BoolModel m_volumeKnob; - FloatModel m_volumeRatio; - - QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent - float m_leftOver; - bool m_buttonPressed; float m_totalAngle; int m_angle; @@ -211,9 +154,7 @@ private: QColor m_textColor; KnobType m_knobNum; - -} ; - +}; } // namespace lmms::gui diff --git a/include/LadspaControl.h b/include/LadspaControl.h index e4f0cd745..8af8f9923 100644 --- a/include/LadspaControl.h +++ b/include/LadspaControl.h @@ -41,6 +41,7 @@ namespace gui { class LadspaControlView; +class LadspaMatrixControlDialog; } // namespace gui @@ -125,6 +126,7 @@ private: friend class gui::LadspaControlView; + friend class gui::LadspaMatrixControlDialog; } ; diff --git a/include/LedCheckBox.h b/include/LedCheckBox.h index e3629e143..aaafffaa1 100644 --- a/include/LedCheckBox.h +++ b/include/LedCheckBox.h @@ -47,10 +47,12 @@ public: LedCheckBox( const QString & _txt, QWidget * _parent, const QString & _name = QString(), - LedColor _color = LedColor::Yellow ); + LedColor _color = LedColor::Yellow, + bool legacyMode = true); LedCheckBox( QWidget * _parent, const QString & _name = QString(), - LedColor _color = LedColor::Yellow ); + LedColor _color = LedColor::Yellow, + bool legacyMode = true); ~LedCheckBox() override; @@ -74,8 +76,13 @@ private: QString m_text; + bool m_legacyMode; + void initUi( LedColor _color ); //!< to be called by ctors + void onTextUpdated(); //!< to be called when you updated @a m_text + void paintLegacy(QPaintEvent * p); + void paintNonLegacy(QPaintEvent * p); } ; diff --git a/include/TempoSyncBarModelEditor.h b/include/TempoSyncBarModelEditor.h new file mode 100644 index 000000000..c1b0bb26f --- /dev/null +++ b/include/TempoSyncBarModelEditor.h @@ -0,0 +1,90 @@ +/* + * TempoSyncBarModelEditor.h - adds bpm to ms conversion for the bar editor class + * + * Copyright (c) 2005-2008 Danny McRae + * Copyright (c) 2009-2014 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H +#define LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H + +#include +#include + +#include "BarModelEditor.h" +#include "TempoSyncKnobModel.h" + +namespace lmms::gui +{ + +class MeterDialog; + +class LMMS_EXPORT TempoSyncBarModelEditor : public BarModelEditor +{ + Q_OBJECT +public: + TempoSyncBarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + ~TempoSyncBarModelEditor() override; + + const QString & syncDescription(); + void setSyncDescription(const QString & new_description); + + const QPixmap & syncIcon(); + void setSyncIcon(const QPixmap & new_pix); + + TempoSyncKnobModel * model() + { + return castModel(); + } + + void modelChanged() override; + + +signals: + void syncDescriptionChanged(const QString & new_description); + void syncIconChanged(); + + +protected: + void contextMenuEvent(QContextMenuEvent * me) override; + + +protected slots: + void updateDescAndIcon(); + void showCustom(); + + +private: + void updateTextDescription(); + void updateIcon(); + + +private: + QPixmap m_tempoSyncIcon; + QString m_tempoSyncDescription; + + QPointer m_custom; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H diff --git a/include/TempoSyncKnobModel.h b/include/TempoSyncKnobModel.h index 5cd2db067..af92a58aa 100644 --- a/include/TempoSyncKnobModel.h +++ b/include/TempoSyncKnobModel.h @@ -82,6 +82,9 @@ public: void setScale( float _new_scale ); + MeterModel & getCustomMeterModel() { return m_custom; } + MeterModel const & getCustomMeterModel() const { return m_custom; } + signals: void syncModeChanged( lmms::TempoSyncKnobModel::SyncMode _new_mode ); void scaleChanged( float _new_scale ); diff --git a/plugins/LadspaEffect/CMakeLists.txt b/plugins/LadspaEffect/CMakeLists.txt index a01eb950f..202a8dd04 100644 --- a/plugins/LadspaEffect/CMakeLists.txt +++ b/plugins/LadspaEffect/CMakeLists.txt @@ -1,6 +1,6 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(ladspaeffect LadspaEffect.cpp LadspaControls.cpp LadspaControlDialog.cpp LadspaSubPluginFeatures.cpp LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaSubPluginFeatures.h MOCFILES LadspaEffect.h LadspaControls.h LadspaControlDialog.h EMBEDDED_RESOURCES logo.png) +BUILD_PLUGIN(ladspaeffect LadspaEffect.cpp LadspaControls.cpp LadspaControlDialog.cpp LadspaMatrixControlDialog.cpp LadspaSubPluginFeatures.cpp LadspaWidgetFactory.cpp LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaMatrixControlDialog.h LadspaSubPluginFeatures.h LadspaWidgetFactory.h MOCFILES LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaMatrixControlDialog.h EMBEDDED_RESOURCES logo.png) SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/ladspa") diff --git a/plugins/LadspaEffect/LadspaControls.h b/plugins/LadspaEffect/LadspaControls.h index 2bef0c856..c91f3badd 100644 --- a/plugins/LadspaEffect/LadspaControls.h +++ b/plugins/LadspaEffect/LadspaControls.h @@ -27,6 +27,7 @@ #include "EffectControls.h" #include "LadspaControlDialog.h" +#include "LadspaMatrixControlDialog.h" namespace lmms { @@ -59,7 +60,7 @@ public: gui::EffectControlDialog* createView() override { - return new gui::LadspaControlDialog( this ); + return new gui::LadspaMatrixControlDialog( this ); } @@ -79,6 +80,7 @@ private: friend class gui::LadspaControlDialog; + friend class gui::LadspaMatrixControlDialog; friend class LadspaEffect; diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp new file mode 100644 index 000000000..88810cee6 --- /dev/null +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp @@ -0,0 +1,243 @@ +/* + * LadspaMatrixControlDialog.h - Dialog for displaying and editing control port + * values for LADSPA plugins in a matrix display + * + * Copyright (c) 2015 Michael Gregorius + * + * This file is part of LMMS - http://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#include +#include +#include +#include +#include +#include +#include + + +#include "LadspaBase.h" +#include "LadspaControl.h" +#include "LadspaEffect.h" +#include "LadspaMatrixControlDialog.h" +#include "LadspaWidgetFactory.h" +#include "LadspaControlView.h" +#include "LedCheckBox.h" + +#include "GuiApplication.h" +#include "MainWindow.h" + + +namespace lmms::gui +{ + +LadspaMatrixControlDialog::LadspaMatrixControlDialog(LadspaControls * ladspaControls) : + EffectControlDialog(ladspaControls), + m_scrollArea(nullptr), + m_stereoLink(nullptr) +{ + QVBoxLayout * mainLayout = new QVBoxLayout(this); + + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(true); + m_scrollArea->setFrameShape(QFrame::NoFrame); + // Set to always on so that the elements do not move around when the + // scroll bar is hidden or shown. + m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + + // Add a scroll area that grows + mainLayout->addWidget(m_scrollArea, 1); + + // Populate the parameter matrix and put it into the scroll area + updateEffectView(ladspaControls); + + // Add button to link all channels if there's more than one channel + if (getChannelCount() > 1) + { + mainLayout->addSpacing(3); + + m_stereoLink = new LedCheckBox(tr("Link Channels"), this, QString(), LedCheckBox::LedColor::Green, false); + m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); + mainLayout->addWidget(m_stereoLink, 0, Qt::AlignCenter); + } +} + +bool LadspaMatrixControlDialog::isResizable() const +{ + return true; +} + +bool LadspaMatrixControlDialog::needsLinkColumn() const +{ + LadspaControls * ladspaControls = getLadspaControls(); + + ch_cnt_t const channelCount = getChannelCount(); + for (ch_cnt_t i = 0; i < channelCount; ++i) + { + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + if (ladspaControl->m_link) + { + return true; + } + } + } + + return false; +} + +void LadspaMatrixControlDialog::arrangeControls(QWidget * parent, QGridLayout* gridLayout) +{ + LadspaControls * ladspaControls = getLadspaControls(); + + int const headerRow = 0; + int const linkColumn = 0; + + bool const linkColumnNeeded = needsLinkColumn(); + if (linkColumnNeeded) + { + gridLayout->addWidget(new QLabel("" + tr("Link") + "", parent), headerRow, linkColumn, Qt::AlignHCenter); + + // If there's a link column then it should not stretch + gridLayout->setColumnStretch(linkColumn, 0); + } + + int const channelStartColumn = linkColumnNeeded ? 1 : 0; + + // The header row should not grow vertically + gridLayout->setRowStretch(0, 0); + + // Records the maximum row with parameters so that we can add a vertical spacer after that row + int maxRow = 0; + + // Iterate the channels and add widgets for each control + // Note: the code assumes that all channels have the same structure, i.e. that all channels + // have the same number of parameters which are in the same order. + ch_cnt_t const numberOfChannels = getChannelCount(); + for (ch_cnt_t i = 0; i < numberOfChannels; ++i) + { + int currentChannelColumn = channelStartColumn + i; + gridLayout->setColumnStretch(currentChannelColumn, 1); + + // First add the channel header with the channel number + gridLayout->addWidget(new QLabel("" + tr("Channel %1").arg(QString::number(i + 1)) + "", parent), headerRow, currentChannelColumn, Qt::AlignHCenter); + + int currentRow = 1; + + if (i == 0) + { + // Configure the current parameter row to not stretch. + // Only do this once, i.e. when working with the first channel. + gridLayout->setRowStretch(currentRow, 0); + } + + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + // Only use the first channel to determine if we need to add link controls + if (i == 0 && ladspaControl->m_link) + { + LedCheckBox * linkCheckBox = new LedCheckBox("", parent, "", LedCheckBox::LedColor::Green); + linkCheckBox->setModel(&ladspaControl->m_linkEnabledModel); + linkCheckBox->setToolTip(tr("Link channels")); + gridLayout->addWidget(linkCheckBox, currentRow, linkColumn, Qt::AlignHCenter); + } + + QWidget * controlWidget = LadspaWidgetFactory::createWidget(ladspaControl, this); + if (controlWidget) + { + gridLayout->addWidget(controlWidget, currentRow, currentChannelColumn); + } + + // Record the maximum row so that we add a vertical spacer after that row + maxRow = std::max(maxRow, currentRow); + + ++currentRow; + } + } + + // Add a spacer item after the maximum row + QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + gridLayout->addItem(spacer, maxRow + 1, 0); +} + +QWidget * LadspaMatrixControlDialog::createMatrixWidget() +{ + QWidget *widget = new QWidget(this); + QGridLayout *gridLayout = new QGridLayout(widget); + gridLayout->setMargin(0); + widget->setLayout(gridLayout); + + arrangeControls(widget, gridLayout); + + return widget; +} + +void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls) +{ + m_effectControls = ladspaControls; + + // No need to delete the existing widget as it's deleted + // by the scroll view when we replace it. + QWidget * matrixWidget = createMatrixWidget(); + m_scrollArea->setWidget(matrixWidget); + + // Make sure that the horizontal scroll bar does not show + // From: https://forum.qt.io/topic/13374/solved-qscrollarea-vertical-scroll-only/4 + m_scrollArea->setMinimumWidth(matrixWidget->minimumSizeHint().width() + m_scrollArea->verticalScrollBar()->width()); + + + // Make sure that the widget is shown without a scrollbar whenever possible + // If the widget fits on the workspace we use the height of the widget as the minimum size of the scroll area. + // This will ensure that the scrollbar is not shown initially (and never will be). + // If the widget is larger than the workspace then we want it to mostly cover the workspace. + // + // This is somewhat ugly but I have no idea how to control the initial size of the scroll area otherwise + auto const workspaceSize = getGUI()->mainWindow()->workspace()->viewport()->size(); + // Make sure that we always account a minumum height for the workspace, i.e. that we never compute + // something close to 0 if the LMMS window is very small + int workspaceHeight = qMax(200, static_cast(workspaceSize.height() * 0.9)); + int minOfWidgetAndWorkspace = qMin(matrixWidget->minimumSizeHint().height(), workspaceHeight); + m_scrollArea->setMinimumHeight(minOfWidgetAndWorkspace); + + if (getChannelCount() > 1 && m_stereoLink != nullptr) + { + m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); + } + + connect(ladspaControls, &LadspaControls::effectModelChanged, + this, &LadspaMatrixControlDialog::updateEffectView, + Qt::DirectConnection); +} + +LadspaControls * LadspaMatrixControlDialog::getLadspaControls() const +{ + return dynamic_cast(m_effectControls); +} + +ch_cnt_t LadspaMatrixControlDialog::getChannelCount() const +{ + return getLadspaControls()->m_processors; +} + +} // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.h b/plugins/LadspaEffect/LadspaMatrixControlDialog.h new file mode 100644 index 000000000..c5949fa15 --- /dev/null +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.h @@ -0,0 +1,91 @@ +/* + * LadspaMatrixControlDialog.h - Dialog for displaying and editing control port + * values for LADSPA plugins in a matrix display + * + * Copyright (c) 2015 Michael Gregorius + * + * This file is part of LMMS - http://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LADSPA_MATRIX_CONTROL_DIALOG_H +#define LADSPA_MATRIX_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" + +#include "lmms_basics.h" + + +class QGridLayout; +class QScrollArea; + +namespace lmms +{ + +class LadspaControls; + +namespace gui +{ + +class LedCheckBox; + + +class LadspaMatrixControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + LadspaMatrixControlDialog(LadspaControls* ctl); + bool isResizable() const override; + +private slots: + void updateEffectView(LadspaControls* ctl); + +private: + /** + * @brief Checks if a link column is needed for the current effect controls. + * @return true if a link column is needed. + */ + bool needsLinkColumn() const; + + /** + * @brief Arranges widgets for the current controls in a grid/matrix layout. + * @param parent The parent of all created widgets + * @param gridLayout The layout into which the controls are organized + */ + void arrangeControls(QWidget * parent, QGridLayout* gridLayout); + + /** + * @brief Creates a widget that holds the widgets of the current controls in a matrix arrangement. + * @param ladspaControls + * @return + */ + QWidget * createMatrixWidget(); + + LadspaControls * getLadspaControls() const; + ch_cnt_t getChannelCount() const; + +private: + QScrollArea* m_scrollArea; + LedCheckBox* m_stereoLink; +}; + +} // namespace gui + +} // namespace lmms + +#endif diff --git a/plugins/LadspaEffect/LadspaWidgetFactory.cpp b/plugins/LadspaEffect/LadspaWidgetFactory.cpp new file mode 100644 index 000000000..0491fd661 --- /dev/null +++ b/plugins/LadspaEffect/LadspaWidgetFactory.cpp @@ -0,0 +1,83 @@ +/* + * LadspaWidgetFactory.cpp - Factory that creates widgets for LADSPA ports + * + * Copyright (c) 2006-2008 Danny McRae + * Copyright (c) 2009 Tobias Doerffel + * Copyright (c) 2015-2023 Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#include "LadspaWidgetFactory.h" + +#include "LadspaControl.h" +#include "LadspaBase.h" + +#include "BarModelEditor.h" +#include "LedCheckBox.h" +#include "TempoSyncBarModelEditor.h" + +#include +#include + + +namespace lmms::gui +{ + +QWidget * LadspaWidgetFactory::createWidget(LadspaControl * ladspaControl, QWidget * parent) +{ + auto const * port = ladspaControl->port(); + + QString const name = port->name; + + switch (port->data_type) + { + case BufferDataType::Toggled: + { + // The actual check box is put into a widget because LedCheckBox does not play nice with layouts. + // Putting it directly into a grid layout disables the resizing behavior of all columns where it + // appears. Hence we put it into the layout of a widget that knows how to play nice with layouts. + QWidget * widgetWithLayout = new QWidget(parent); + QHBoxLayout * layout = new QHBoxLayout(widgetWithLayout); + layout->setContentsMargins(0, 0, 0, 0); + LedCheckBox * toggle = new LedCheckBox( + name, parent, QString(), LedCheckBox::LedColor::Green, false); + toggle->setModel(ladspaControl->toggledModel()); + layout->addWidget(toggle, 0, Qt::AlignLeft); + + return widgetWithLayout; + } + + case BufferDataType::Integer: + case BufferDataType::Enum: + case BufferDataType::Floating: + return new BarModelEditor(name, ladspaControl->knobModel(), parent); + + case BufferDataType::Time: + return new TempoSyncBarModelEditor(name, ladspaControl->tempoSyncKnobModel(), parent); + + default: + return new QLabel(QObject::tr("%1 (unsupported)").arg(name), parent); + } + + return nullptr; +} + +} // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaWidgetFactory.h b/plugins/LadspaEffect/LadspaWidgetFactory.h new file mode 100644 index 000000000..807334d32 --- /dev/null +++ b/plugins/LadspaEffect/LadspaWidgetFactory.h @@ -0,0 +1,49 @@ +/* + * LadspaWidgetFactory.h - Factory that creates widgets for LADSPA ports + * + * Copyright (c) 2023 Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_LADSPA_WIDGET_FACTORY_H +#define LMMS_GUI_LADSPA_WIDGET_FACTORY_H + + +class QWidget; + +namespace lmms +{ + +class LadspaControl; + +namespace gui +{ + +class LadspaWidgetFactory +{ +public: + static QWidget * createWidget(LadspaControl * ladspaControl, QWidget * parent); +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LADSPA_WIDGET_FACTORY_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index afed153f9..1e809e9d7 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -94,11 +94,13 @@ SET(LMMS_SRCS gui/widgets/AutomatableButton.cpp gui/widgets/AutomatableSlider.cpp + gui/widgets/BarModelEditor.cpp gui/widgets/CPULoadWidget.cpp gui/widgets/CaptionMenu.cpp gui/widgets/ComboBox.cpp gui/widgets/CustomTextKnob.cpp gui/widgets/Fader.cpp + gui/widgets/FloatModelEditorBase.cpp gui/widgets/Graph.cpp gui/widgets/GroupBox.cpp gui/widgets/Knob.cpp @@ -115,6 +117,7 @@ SET(LMMS_SRCS gui/widgets/SimpleTextFloat.cpp gui/widgets/TabBar.cpp gui/widgets/TabWidget.cpp + gui/widgets/TempoSyncBarModelEditor.cpp gui/widgets/TempoSyncKnob.cpp gui/widgets/TextFloat.cpp gui/widgets/TimeDisplayWidget.cpp diff --git a/src/gui/widgets/BarModelEditor.cpp b/src/gui/widgets/BarModelEditor.cpp new file mode 100644 index 000000000..4b02c9634 --- /dev/null +++ b/src/gui/widgets/BarModelEditor.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include + + +namespace lmms::gui +{ + +BarModelEditor::BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + FloatModelEditorBase(DirectionOfManipulation::Horizontal, parent), + m_text(text), + m_backgroundBrush(palette().base()), + m_barBrush(palette().button()), + m_textColor(palette().text().color()) +{ + setModel(floatModel); +} + +QSizePolicy BarModelEditor::sizePolicy() const +{ + return QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize BarModelEditor::minimumSizeHint() const +{ + auto const fm = fontMetrics(); + return QSize(50, fm.height() + 6); +} + +QSize BarModelEditor::sizeHint() const +{ + return minimumSizeHint(); +} + +QBrush const & BarModelEditor::getBackgroundBrush() const +{ + return m_backgroundBrush; +} + +void BarModelEditor::setBackgroundBrush(QBrush const & backgroundBrush) +{ + m_backgroundBrush = backgroundBrush; +} + +QBrush const & BarModelEditor::getBarBrush() const +{ + return m_barBrush; +} + +void BarModelEditor::setBarBrush(QBrush const & barBrush) +{ + m_barBrush = barBrush; +} + +QColor const & BarModelEditor::getTextColor() const +{ + return m_textColor; +} + +void BarModelEditor::setTextColor(QColor const & textColor) +{ + m_textColor = textColor; +} + +void BarModelEditor::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + QRect const r = rect(); + + QPainter painter(this); + + // Paint the base rectangle into which the bar and the text go + QBrush const & backgroundBrush = getBackgroundBrush(); + painter.setPen(backgroundBrush.color()); + painter.setBrush(backgroundBrush); + painter.drawRect(r); + + + // Paint the bar + // Compute the percentage as: + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + int const margin = 3; + QMargins const margins(margin, margin, margin, margin); + QRect const valueRect = r.marginsRemoved(margins); + + QBrush const & barBrush = getBarBrush(); + painter.setPen(barBrush.color()); + painter.setBrush(barBrush); + QPoint const startPoint = valueRect.topLeft(); + QPoint endPoint = valueRect.bottomRight(); + endPoint.setX(startPoint.x() + percentage * (endPoint.x() - startPoint.x())); + + painter.drawRect(QRect(startPoint, endPoint)); + + + // Draw the text into the value rectangle but move it slightly to the right + QRect const textRect = valueRect.marginsRemoved(QMargins(3, 0, 0, 0)); + + // Elide the text if needed + auto const fm = fontMetrics(); + QString const elidedText = fm.elidedText(m_text, Qt::ElideRight, textRect.width()); + + // Now draw the text + painter.setPen(getTextColor()); + painter.drawText(textRect, elidedText); +} + +} // namespace lmms::gui diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp new file mode 100644 index 000000000..7421908e2 --- /dev/null +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -0,0 +1,464 @@ +/* + * FloatModelEditorBase.cpp - Base editor for float models + * + * Copyright (c) 2004-2014 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "FloatModelEditorBase.h" + +#include +#include +#include + +#ifndef __USE_XOPEN +#define __USE_XOPEN +#endif + +#include "lmms_math.h" +#include "CaptionMenu.h" +#include "ControllerConnection.h" +#include "GuiApplication.h" +#include "LocaleHelper.h" +#include "MainWindow.h" +#include "ProjectJournal.h" +#include "SimpleTextFloat.h" +#include "StringPairDrag.h" + + +namespace lmms::gui +{ + +SimpleTextFloat * FloatModelEditorBase::s_textFloat = nullptr; + +FloatModelEditorBase::FloatModelEditorBase(DirectionOfManipulation directionOfManipulation, QWidget * parent, const QString & name) : + QWidget(parent), + FloatModelView(new FloatModel(0, 0, 0, 1, nullptr, name, true), this), + m_volumeKnob(false), + m_volumeRatio(100.0, 0.0, 1000000.0), + m_buttonPressed(false), + m_directionOfManipulation(directionOfManipulation) +{ + initUi(name); +} + + +void FloatModelEditorBase::initUi(const QString & name) +{ + if (s_textFloat == nullptr) + { + s_textFloat = new SimpleTextFloat; + } + + setWindowTitle(name); + + setFocusPolicy(Qt::ClickFocus); + + doConnections(); +} + + +void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) +{ + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime); +} + + +float FloatModelEditorBase::getValue(const QPoint & p) +{ + // Find out which direction/coordinate is relevant for this control + int const coordinate = m_directionOfManipulation == DirectionOfManipulation::Vertical ? p.y() : -p.x(); + + // knob value increase is linear to mouse movement + float value = .4f * coordinate; + + // if shift pressed we want slower movement + if (getGUI()->mainWindow()->isShiftPressed()) + { + value /= 4.0f; + value = qBound(-4.0f, value, 4.0f); + } + + return value * pageSize(); +} + + +void FloatModelEditorBase::contextMenuEvent(QContextMenuEvent *) +{ + // for the case, the user clicked right while pressing left mouse- + // button, the context-menu appears while mouse-cursor is still hidden + // and it isn't shown again until user does something which causes + // an QApplication::restoreOverrideCursor()-call... + mouseReleaseEvent(nullptr); + + CaptionMenu contextMenu(model()->displayName(), this); + addDefaultActions(&contextMenu); + contextMenu.addAction(QPixmap(), + model()->isScaleLogarithmic() ? tr("Set linear") : tr("Set logarithmic"), + this, SLOT(toggleScale())); + contextMenu.addSeparator(); + contextMenu.exec(QCursor::pos()); +} + + +void FloatModelEditorBase::toggleScale() +{ + model()->setScaleLogarithmic(! model()->isScaleLogarithmic()); + update(); +} + + +void FloatModelEditorBase::dragEnterEvent(QDragEnterEvent * dee) +{ + StringPairDrag::processDragEnterEvent(dee, "float_value," + "automatable_model"); +} + + +void FloatModelEditorBase::dropEvent(QDropEvent * de) +{ + QString type = StringPairDrag::decodeKey(de); + QString val = StringPairDrag::decodeValue(de); + if (type == "float_value") + { + model()->setValue(LocaleHelper::toFloat(val)); + de->accept(); + } + else if (type == "automatable_model") + { + auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); + if (mod != nullptr) + { + AutomatableModel::linkModels(model(), mod); + mod->setValue(model()->value()); + } + } +} + + +void FloatModelEditorBase::mousePressEvent(QMouseEvent * me) +{ + if (me->button() == Qt::LeftButton && + ! (me->modifiers() & Qt::ControlModifier) && + ! (me->modifiers() & Qt::ShiftModifier)) + { + AutomatableModel *thisModel = model(); + if (thisModel) + { + thisModel->addJournalCheckPoint(); + thisModel->saveJournallingState(false); + } + + const QPoint & p = me->pos(); + m_lastMousePos = p; + m_leftOver = 0.0f; + + emit sliderPressed(); + + showTextFloat(0, 0); + + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, + QPoint(width() + 2, 0)); + s_textFloat->show(); + m_buttonPressed = true; + } + else if (me->button() == Qt::LeftButton && + (me->modifiers() & Qt::ShiftModifier)) + { + new StringPairDrag("float_value", + QString::number(model()->value()), + QPixmap(), this); + } + else + { + FloatModelView::mousePressEvent(me); + } +} + + +void FloatModelEditorBase::mouseMoveEvent(QMouseEvent * me) +{ + if (m_buttonPressed && me->pos() != m_lastMousePos) + { + // knob position is changed depending on last mouse position + setPosition(me->pos() - m_lastMousePos); + emit sliderMoved(model()->value()); + // original position for next time is current position + m_lastMousePos = me->pos(); + } + s_textFloat->setText(displayValue()); + s_textFloat->show(); +} + + +void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event) +{ + if (event && event->button() == Qt::LeftButton) + { + AutomatableModel *thisModel = model(); + if (thisModel) + { + thisModel->restoreJournallingState(); + } + } + + m_buttonPressed = false; + + emit sliderReleased(); + + QApplication::restoreOverrideCursor(); + + s_textFloat->hide(); +} + + +void FloatModelEditorBase::enterEvent(QEvent *event) +{ + showTextFloat(700, 2000); +} + + +void FloatModelEditorBase::leaveEvent(QEvent *event) +{ + s_textFloat->hide(); +} + + +void FloatModelEditorBase::focusOutEvent(QFocusEvent * fe) +{ + // make sure we don't loose mouse release event + mouseReleaseEvent(nullptr); + QWidget::focusOutEvent(fe); +} + + +void FloatModelEditorBase::mouseDoubleClickEvent(QMouseEvent *) +{ + enterValue(); +} + + +void FloatModelEditorBase::paintEvent(QPaintEvent *) +{ + QPainter p(this); + + QColor const foreground(3, 94, 97); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + // Compute the percentage + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + QRect r = rect(); + p.setPen(foreground); + p.setBrush(foreground); + p.drawRect(QRect(r.topLeft(), QPoint(r.width() * percentage, r.height()))); +} + + +void FloatModelEditorBase::wheelEvent(QWheelEvent * we) +{ + we->accept(); + const int deltaY = we->angleDelta().y(); + float direction = deltaY > 0 ? 1 : -1; + + auto * m = model(); + float const step = m->step(); + float const range = m->range(); + + // This is the default number of steps or mouse wheel events that it takes to sweep + // from the lowest value to the highest value. + // It might be modified if the user presses modifier keys. See below. + float numberOfStepsForFullSweep = 100.; + + auto const modKeys = we->modifiers(); + if (modKeys == Qt::ShiftModifier) + { + // The shift is intended to go through the values in very coarse steps as in: + // "Shift into overdrive" + numberOfStepsForFullSweep = 10; + } + else if (modKeys == Qt::ControlModifier) + { + // The control key gives more control, i.e. it enables more fine-grained adjustments + numberOfStepsForFullSweep = 1000; + } + else if (modKeys == Qt::AltModifier) + { + // The alt key enables even finer adjustments + numberOfStepsForFullSweep = 2000; + + // It seems that on some systems pressing Alt with mess with the directions, + // i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel + // left and right. Account for this quirk. + if (deltaY == 0) + { + int const deltaX = we->angleDelta().x(); + if (deltaX != 0) + { + direction = deltaX > 0 ? 1 : -1; + } + } + } + + // Compute the number of steps but make sure that we always do at least one step + const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); + const int inc = direction * stepMult; + model()->incValue(inc); + + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->setVisibilityTimeOut(1000); + + emit sliderMoved(model()->value()); +} + + +void FloatModelEditorBase::setPosition(const QPoint & p) +{ + const float value = getValue(p) + m_leftOver; + const auto step = model()->step(); + const float oldValue = model()->value(); + + if (model()->isScaleLogarithmic()) // logarithmic code + { + const float pos = model()->minValue() < 0 + ? oldValue / qMax(qAbs(model()->maxValue()), qAbs(model()->minValue())) + : (oldValue - model()->minValue()) / model()->range(); + const float ratio = 0.1f + qAbs(pos) * 15.f; + float newValue = value * ratio; + if (qAbs(newValue) >= step) + { + float roundedValue = qRound((oldValue - value) / step) * step; + model()->setValue(roundedValue); + m_leftOver = 0.0f; + } + else + { + m_leftOver = value; + } + } + + else // linear code + { + if (qAbs(value) >= step) + { + float roundedValue = qRound((oldValue - value) / step) * step; + model()->setValue(roundedValue); + m_leftOver = 0.0f; + } + else + { + m_leftOver = value; + } + } +} + + +void FloatModelEditorBase::enterValue() +{ + bool ok; + float new_val; + + if (isVolumeKnob() && + ConfigManager::inst()->value("app", "displaydbfs").toInt()) + { + new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "-96.0 dBFS and 6.0 dBFS:"), + ampToDbfs(model()->getRoundedValue() / 100.0), + -96.0, 6.0, model()->getDigitCount(), &ok); + if (new_val <= -96.0) + { + new_val = 0.0f; + } + else + { + new_val = dbfsToAmp(new_val) * 100.0; + } + } + else + { + new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "%1 and %2:"). + arg(model()->minValue()). + arg(model()->maxValue()), + model()->getRoundedValue(), + model()->minValue(), + model()->maxValue(), model()->getDigitCount(), &ok); + } + + if (ok) + { + model()->setValue(new_val); + } +} + + +void FloatModelEditorBase::friendlyUpdate() +{ + if (model() && (model()->controllerConnection() == nullptr || + model()->controllerConnection()->getController()->frequentUpdates() == false || + Controller::runningFrames() % (256*4) == 0)) + { + update(); + } +} + + +QString FloatModelEditorBase::displayValue() const +{ + if (isVolumeKnob() && + ConfigManager::inst()->value("app", "displaydbfs").toInt()) + { + return m_description.trimmed() + QString(" %1 dBFS"). + arg(ampToDbfs(model()->getRoundedValue() / volumeRatio()), + 3, 'f', 2); + } + + return m_description.trimmed() + QString(" %1"). + arg(model()->getRoundedValue()) + m_unit; +} + + +void FloatModelEditorBase::doConnections() +{ + if (model() != nullptr) + { + QObject::connect(model(), SIGNAL(dataChanged()), + this, SLOT(friendlyUpdate())); + + QObject::connect(model(), SIGNAL(propertiesChanged()), + this, SLOT(update())); + } +} + +} // namespace lmms::gui diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 56cf29345..00a9363c8 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -24,11 +24,6 @@ #include "Knob.h" -#include -#include -#include -#include -#include #include #ifndef __USE_XOPEN @@ -36,36 +31,19 @@ #endif #include "lmms_math.h" -#include "CaptionMenu.h" -#include "ConfigManager.h" -#include "ControllerConnection.h" #include "DeprecationHelper.h" #include "embed.h" #include "gui_templates.h" -#include "GuiApplication.h" -#include "LocaleHelper.h" -#include "MainWindow.h" -#include "ProjectJournal.h" -#include "SimpleTextFloat.h" -#include "StringPairDrag.h" + namespace lmms::gui { -SimpleTextFloat * Knob::s_textFloat = nullptr; - - - - Knob::Knob( KnobType _knob_num, QWidget * _parent, const QString & _name ) : - QWidget( _parent ), - FloatModelView( new FloatModel( 0, 0, 0, 1, nullptr, _name, true ), this ), + FloatModelEditorBase(DirectionOfManipulation::Vertical, _parent, _name), m_label( "" ), m_isHtmlLabel(false), m_tdRenderer(nullptr), - m_volumeKnob( false ), - m_volumeRatio( 100.0, 0.0, 1000000.0 ), - m_buttonPressed( false ), m_angle( -10 ), m_lineWidth( 0 ), m_textColor( 255, 255, 255 ), @@ -84,18 +62,10 @@ Knob::Knob( QWidget * _parent, const QString & _name ) : void Knob::initUi( const QString & _name ) { - if( s_textFloat == nullptr ) - { - s_textFloat = new SimpleTextFloat; - } - - setWindowTitle( _name ); - onKnobNumUpdated(); setTotalAngle( 270.0f ); setInnerRadius( 1.0f ); setOuterRadius( 10.0f ); - setFocusPolicy( Qt::ClickFocus ); // This is a workaround to enable style sheets for knobs which are not styled knobs. // @@ -123,13 +93,9 @@ void Knob::initUi( const QString & _name ) default: break; } - - doConnections(); } - - void Knob::onKnobNumUpdated() { if( m_knobNum != KnobType::Styled ) @@ -484,195 +450,6 @@ void Knob::drawKnob( QPainter * _p ) _p->drawImage( 0, 0, m_cache ); } -void Knob::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) -{ - s_textFloat->setText(displayValue()); - s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); - s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime); -} - -float Knob::getValue( const QPoint & _p ) -{ - float value; - - // knob value increase is linear to mouse movement - value = .4f * _p.y(); - - // if shift pressed we want slower movement - if( getGUI()->mainWindow()->isShiftPressed() ) - { - value /= 4.0f; - value = qBound( -4.0f, value, 4.0f ); - } - return value * pageSize(); -} - - - - -void Knob::contextMenuEvent( QContextMenuEvent * ) -{ - // for the case, the user clicked right while pressing left mouse- - // button, the context-menu appears while mouse-cursor is still hidden - // and it isn't shown again until user does something which causes - // an QApplication::restoreOverrideCursor()-call... - mouseReleaseEvent( nullptr ); - - CaptionMenu contextMenu( model()->displayName(), this ); - addDefaultActions( &contextMenu ); - contextMenu.addAction( QPixmap(), - model()->isScaleLogarithmic() ? tr( "Set linear" ) : tr( "Set logarithmic" ), - this, SLOT(toggleScale())); - contextMenu.addSeparator(); - contextMenu.exec( QCursor::pos() ); -} - - -void Knob::toggleScale() -{ - model()->setScaleLogarithmic( ! model()->isScaleLogarithmic() ); - update(); -} - - - -void Knob::dragEnterEvent( QDragEnterEvent * _dee ) -{ - StringPairDrag::processDragEnterEvent( _dee, "float_value," - "automatable_model" ); -} - - - - -void Knob::dropEvent( QDropEvent * _de ) -{ - QString type = StringPairDrag::decodeKey( _de ); - QString val = StringPairDrag::decodeValue( _de ); - if( type == "float_value" ) - { - model()->setValue( LocaleHelper::toFloat(val) ); - _de->accept(); - } - else if( type == "automatable_model" ) - { - auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); - if( mod != nullptr ) - { - AutomatableModel::linkModels( model(), mod ); - mod->setValue( model()->value() ); - } - } -} - - - - -void Knob::mousePressEvent( QMouseEvent * _me ) -{ - if( _me->button() == Qt::LeftButton && - ! ( _me->modifiers() & Qt::ControlModifier ) && - ! ( _me->modifiers() & Qt::ShiftModifier ) ) - { - AutomatableModel *thisModel = model(); - if( thisModel ) - { - thisModel->addJournalCheckPoint(); - thisModel->saveJournallingState( false ); - } - - const QPoint & p = _me->pos(); - m_lastMousePos = p; - m_leftOver = 0.0f; - - emit sliderPressed(); - - showTextFloat(0, 0); - - m_buttonPressed = true; - } - else if( _me->button() == Qt::LeftButton && - (_me->modifiers() & Qt::ShiftModifier) ) - { - new StringPairDrag( "float_value", - QString::number( model()->value() ), - QPixmap(), this ); - } - else - { - FloatModelView::mousePressEvent( _me ); - } -} - - - - -void Knob::mouseMoveEvent( QMouseEvent * _me ) -{ - if( m_buttonPressed && _me->pos() != m_lastMousePos ) - { - // knob position is changed depending on last mouse position - setPosition( _me->pos() - m_lastMousePos ); - emit sliderMoved( model()->value() ); - // original position for next time is current position - m_lastMousePos = _me->pos(); - } - s_textFloat->setText( displayValue() ); - s_textFloat->show(); -} - - - - -void Knob::mouseReleaseEvent( QMouseEvent* event ) -{ - if( event && event->button() == Qt::LeftButton ) - { - AutomatableModel *thisModel = model(); - if( thisModel ) - { - thisModel->restoreJournallingState(); - } - } - - m_buttonPressed = false; - - emit sliderReleased(); - - QApplication::restoreOverrideCursor(); - - s_textFloat->hide(); -} - -void Knob::enterEvent(QEvent *event) -{ - showTextFloat(700, 2000); -} - -void Knob::leaveEvent(QEvent *event) -{ - s_textFloat->hide(); -} - - -void Knob::focusOutEvent( QFocusEvent * _fe ) -{ - // make sure we don't loose mouse release event - mouseReleaseEvent( nullptr ); - QWidget::focusOutEvent( _fe ); -} - - - - -void Knob::mouseDoubleClickEvent( QMouseEvent * ) -{ - enterValue(); -} - - - - void Knob::paintEvent( QPaintEvent * _me ) { QPainter p( this ); @@ -697,201 +474,6 @@ void Knob::paintEvent( QPaintEvent * _me ) } } - - - -void Knob::wheelEvent(QWheelEvent * we) -{ - we->accept(); - const int deltaY = we->angleDelta().y(); - float direction = deltaY > 0 ? 1 : -1; - - auto * m = model(); - float const step = m->step(); - float const range = m->range(); - - // This is the default number of steps or mouse wheel events that it takes to sweep - // from the lowest value to the highest value. - // It might be modified if the user presses modifier keys. See below. - float numberOfStepsForFullSweep = 100.; - - auto const modKeys = we->modifiers(); - if (modKeys == Qt::ShiftModifier) - { - // The shift is intended to go through the values in very coarse steps as in: - // "Shift into overdrive" - numberOfStepsForFullSweep = 10; - } - else if (modKeys == Qt::ControlModifier) - { - // The control key gives more control, i.e. it enables more fine-grained adjustments - numberOfStepsForFullSweep = 1000; - } - else if (modKeys == Qt::AltModifier) - { - // The alt key enables even finer adjustments - numberOfStepsForFullSweep = 2000; - - // It seems that on some systems pressing Alt with mess with the directions, - // i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel - // left and right. Account for this quirk. - if (deltaY == 0) - { - int const deltaX = we->angleDelta().x(); - if (deltaX != 0) - { - direction = deltaX > 0 ? 1 : -1; - } - } - } - - // Compute the number of steps but make sure that we always do at least one step - const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); - const int inc = direction * stepMult; - model()->incValue(inc); - - s_textFloat->setText( displayValue() ); - s_textFloat->moveGlobal( this, QPoint( width() + 2, 0 ) ); - s_textFloat->setVisibilityTimeOut( 1000 ); - - emit sliderMoved( model()->value() ); -} - - - - -void Knob::setPosition( const QPoint & _p ) -{ - const float value = getValue( _p ) + m_leftOver; - const auto step = model()->step(); - const float oldValue = model()->value(); - - - - if( model()->isScaleLogarithmic() ) // logarithmic code - { - const float pos = model()->minValue() < 0 - ? oldValue / qMax( qAbs( model()->maxValue() ), qAbs( model()->minValue() ) ) - : ( oldValue - model()->minValue() ) / model()->range(); - const float ratio = 0.1f + qAbs( pos ) * 15.f; - float newValue = value * ratio; - if( qAbs( newValue ) >= step ) - { - float roundedValue = qRound( ( oldValue - value ) / step ) * step; - model()->setValue( roundedValue ); - m_leftOver = 0.0f; - } - else - { - m_leftOver = value; - } - } - - else // linear code - { - if( qAbs( value ) >= step ) - { - float roundedValue = qRound( ( oldValue - value ) / step ) * step; - model()->setValue( roundedValue ); - m_leftOver = 0.0f; - } - else - { - m_leftOver = value; - } - } -} - - - - -void Knob::enterValue() -{ - bool ok; - float new_val; - - if( isVolumeKnob() && - ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ) - { - new_val = QInputDialog::getDouble( - this, tr( "Set value" ), - tr( "Please enter a new value between " - "-96.0 dBFS and 6.0 dBFS:" ), - ampToDbfs( model()->getRoundedValue() / 100.0 ), - -96.0, 6.0, model()->getDigitCount(), &ok ); - if( new_val <= -96.0 ) - { - new_val = 0.0f; - } - else - { - new_val = dbfsToAmp( new_val ) * 100.0; - } - } - else - { - new_val = QInputDialog::getDouble( - this, tr( "Set value" ), - tr( "Please enter a new value between " - "%1 and %2:" ). - arg( model()->minValue() ). - arg( model()->maxValue() ), - model()->getRoundedValue(), - model()->minValue(), - model()->maxValue(), model()->getDigitCount(), &ok ); - } - - if( ok ) - { - model()->setValue( new_val ); - } -} - - - - -void Knob::friendlyUpdate() -{ - if (model() && (model()->controllerConnection() == nullptr || - model()->controllerConnection()->getController()->frequentUpdates() == false || - Controller::runningFrames() % (256*4) == 0)) - { - update(); - } -} - - - - -QString Knob::displayValue() const -{ - if( isVolumeKnob() && - ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ) - { - return m_description.trimmed() + QString( " %1 dBFS" ). - arg( ampToDbfs( model()->getRoundedValue() / volumeRatio() ), - 3, 'f', 2 ); - } - return m_description.trimmed() + QString( " %1" ). - arg( model()->getRoundedValue() ) + m_unit; -} - - - - -void Knob::doConnections() -{ - if( model() != nullptr ) - { - QObject::connect( model(), SIGNAL(dataChanged()), - this, SLOT(friendlyUpdate())); - - QObject::connect( model(), SIGNAL(propertiesChanged()), - this, SLOT(update())); - } -} - - void Knob::changeEvent(QEvent * ev) { if (ev->type() == QEvent::EnabledChange) diff --git a/src/gui/widgets/LedCheckBox.cpp b/src/gui/widgets/LedCheckBox.cpp index 0c16bf391..75e73328f 100644 --- a/src/gui/widgets/LedCheckBox.cpp +++ b/src/gui/widgets/LedCheckBox.cpp @@ -44,9 +44,10 @@ static const auto names = std::array LedCheckBox::LedCheckBox( const QString & _text, QWidget * _parent, - const QString & _name, LedColor _color ) : + const QString & _name, LedColor _color, bool legacyMode ) : AutomatableButton( _parent, _name ), - m_text( _text ) + m_text( _text ), + m_legacyMode(legacyMode) { initUi( _color ); } @@ -55,8 +56,8 @@ LedCheckBox::LedCheckBox( const QString & _text, QWidget * _parent, LedCheckBox::LedCheckBox( QWidget * _parent, - const QString & _name, LedColor _color ) : - LedCheckBox( QString(), _parent, _name, _color ) + const QString & _name, LedColor _color, bool legacyMode ) : + LedCheckBox( QString(), _parent, _name, _color, legacyMode ) { } @@ -80,24 +81,16 @@ void LedCheckBox::setText( const QString &s ) -void LedCheckBox::paintEvent( QPaintEvent * ) +void LedCheckBox::paintEvent( QPaintEvent * pe ) { - QPainter p( this ); - p.setFont( pointSize<7>( font() ) ); - - if( model()->value() == true ) - { - p.drawPixmap( 0, 0, *m_ledOnPixmap ); + if (!m_legacyMode) + { + paintNonLegacy(pe); } else { - p.drawPixmap( 0, 0, *m_ledOffPixmap ); + paintLegacy(pe); } - - p.setPen( QColor( 64, 64, 64 ) ); - p.drawText( m_ledOffPixmap->width() + 4, 11, text() ); - p.setPen( QColor( 255, 255, 255 ) ); - p.drawText( m_ledOffPixmap->width() + 3, 10, text() ); } @@ -111,7 +104,11 @@ void LedCheckBox::initUi( LedColor _color ) names[static_cast(_color)].toUtf8().constData() ) ); m_ledOffPixmap = new QPixmap( embed::getIconPixmap( "led_off" ) ); - setFont( pointSize<7>( font() ) ); + if (m_legacyMode) + { + setFont( pointSize<7>( font() ) ); + } + setText( m_text ); } @@ -120,9 +117,45 @@ void LedCheckBox::initUi( LedColor _color ) void LedCheckBox::onTextUpdated() { - setFixedSize(m_ledOffPixmap->width() + 5 + horizontalAdvance(QFontMetrics(font()), - text()), - m_ledOffPixmap->height()); + QFontMetrics const fm = fontMetrics(); + + int const width = m_ledOffPixmap->width() + 5 + horizontalAdvance(fm, text()); + int const height = m_legacyMode ? m_ledOffPixmap->height() : qMax(m_ledOffPixmap->height(), fm.height()); + + setFixedSize(width, height); +} + +void LedCheckBox::paintLegacy(QPaintEvent * pe) +{ + QPainter p( this ); + p.setFont( pointSize<7>( font() ) ); + + if( model()->value() == true ) + { + p.drawPixmap( 0, 0, *m_ledOnPixmap ); + } + else + { + p.drawPixmap( 0, 0, *m_ledOffPixmap ); + } + + p.setPen( QColor( 64, 64, 64 ) ); + p.drawText( m_ledOffPixmap->width() + 4, 11, text() ); + p.setPen( QColor( 255, 255, 255 ) ); + p.drawText( m_ledOffPixmap->width() + 3, 10, text() ); +} + +void LedCheckBox::paintNonLegacy(QPaintEvent * pe) +{ + QPainter p(this); + + QPixmap * drawnPixmap = model()->value() ? m_ledOnPixmap : m_ledOffPixmap; + + p.drawPixmap( 0, rect().height() / 2 - drawnPixmap->height() / 2, *drawnPixmap); + + QRect r = rect(); + r -= QMargins(m_ledOffPixmap->width() + 5, 0, 0, 0); + p.drawText(r, text()); } diff --git a/src/gui/widgets/TempoSyncBarModelEditor.cpp b/src/gui/widgets/TempoSyncBarModelEditor.cpp new file mode 100644 index 000000000..5ff2332e0 --- /dev/null +++ b/src/gui/widgets/TempoSyncBarModelEditor.cpp @@ -0,0 +1,302 @@ +/* + * TempoSyncBarModelEditor.cpp - adds bpm to ms conversion for the bar editor class + * + * Copyright (c) 2005-2007 Danny McRae + * Copyright (c) 2005-2009 Tobias Doerffel + * Copyright (c) 2023 Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#include + +#include "TempoSyncBarModelEditor.h" +#include "Engine.h" +#include "CaptionMenu.h" +#include "embed.h" +#include "GuiApplication.h" +#include "MainWindow.h" +#include "MeterDialog.h" +#include "Song.h" +#include "SubWindow.h" + + +namespace lmms::gui +{ + +TempoSyncBarModelEditor::TempoSyncBarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + BarModelEditor(text, floatModel, parent), + m_tempoSyncIcon(embed::getIconPixmap("tempo_sync")), + m_tempoSyncDescription(tr("Tempo Sync")), + m_custom(nullptr) +{ + modelChanged(); +} + + +TempoSyncBarModelEditor::~TempoSyncBarModelEditor() +{ + if(m_custom) + { + delete m_custom->parentWidget(); + } +} + + +void TempoSyncBarModelEditor::modelChanged() +{ + TempoSyncKnobModel * tempoSyncModel = model(); + + if(tempoSyncModel == nullptr) + { + qWarning("no TempoSyncKnobModel has been set!"); + } + + if(m_custom != nullptr) + { + m_custom->setModel(&tempoSyncModel->getCustomMeterModel()); + } + + connect(tempoSyncModel, &TempoSyncKnobModel::syncModeChanged, this, &TempoSyncBarModelEditor::updateDescAndIcon); + connect(this, SIGNAL(sliderMoved(float)), tempoSyncModel, SLOT(disableSync())); + + updateDescAndIcon(); +} + + +void TempoSyncBarModelEditor::contextMenuEvent(QContextMenuEvent *) +{ + mouseReleaseEvent(nullptr); + + TempoSyncKnobModel * tempoSyncModel = model(); + + CaptionMenu contextMenu(tempoSyncModel->displayName(), this); + addDefaultActions(&contextMenu); + + contextMenu.addSeparator(); + + float limit = 60000.0f / (Engine::getSong()->getTempo() * tempoSyncModel->scale()); + + QMenu * syncMenu = contextMenu.addMenu(m_tempoSyncIcon, m_tempoSyncDescription); + + float const maxValue = tempoSyncModel->maxValue(); + + if(limit / 8.0f <= maxValue) + { + connect(syncMenu, SIGNAL(triggered(QAction*)), tempoSyncModel, SLOT(setTempoSync(QAction*))); + + syncMenu->addAction(embed::getIconPixmap("note_none"), + tr("No Sync"))->setData((int) TempoSyncKnobModel::SyncMode::None); + + if(limit / 0.125f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_double_whole"), + tr("Eight beats"))->setData((int) TempoSyncKnobModel::SyncMode::DoubleWholeNote); + } + + if(limit / 0.25f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_whole"), + tr("Whole note"))->setData((int) TempoSyncKnobModel::SyncMode::WholeNote); + } + + if(limit / 0.5f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_half"), + tr("Half note"))->setData((int) TempoSyncKnobModel::SyncMode::HalfNote); + } + + if(limit <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_quarter"), + tr("Quarter note"))->setData((int) TempoSyncKnobModel::SyncMode::QuarterNote); + } + + if(limit / 2.0f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_eighth"), + tr("8th note"))->setData((int) TempoSyncKnobModel::SyncMode::EighthNote); + } + + if(limit / 4.0f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_sixteenth"), + tr("16th note"))->setData((int) TempoSyncKnobModel::SyncMode::SixteenthNote); + } + + syncMenu->addAction(embed::getIconPixmap("note_thirtysecond"), + tr("32nd note"))->setData((int) TempoSyncKnobModel::SyncMode::ThirtysecondNote); + + syncMenu->addAction(embed::getIconPixmap("dont_know"), + tr("Custom..."), this, SLOT(showCustom()))->setData((int) TempoSyncKnobModel::SyncMode::Custom); + + contextMenu.addSeparator(); + } + + contextMenu.exec(QCursor::pos()); + + delete syncMenu; +} + +void TempoSyncBarModelEditor::updateDescAndIcon() +{ + updateTextDescription(); + + if(m_custom != nullptr && model()->syncMode() != TempoSyncKnobModel::SyncMode::Custom) + { + m_custom->parentWidget()->hide(); + } + + updateIcon(); + + emit syncDescriptionChanged(m_tempoSyncDescription); + emit syncIconChanged(); +} + + +const QString & TempoSyncBarModelEditor::syncDescription() +{ + return m_tempoSyncDescription; +} + + +void TempoSyncBarModelEditor::setSyncDescription(const QString & new_description) +{ + m_tempoSyncDescription = new_description; + emit syncDescriptionChanged(new_description); +} + + +const QPixmap & TempoSyncBarModelEditor::syncIcon() +{ + return m_tempoSyncIcon; +} + + +void TempoSyncBarModelEditor::setSyncIcon(const QPixmap & new_icon) +{ + m_tempoSyncIcon = new_icon; + emit syncIconChanged(); +} + + +void TempoSyncBarModelEditor::showCustom() +{ + if(m_custom == nullptr) + { + m_custom = new MeterDialog(getGUI()->mainWindow()->workspace()); + QMdiSubWindow * subWindow = getGUI()->mainWindow()->addWindowedWidget(m_custom); + Qt::WindowFlags flags = subWindow->windowFlags(); + flags &= ~Qt::WindowMaximizeButtonHint; + subWindow->setWindowFlags(flags); + subWindow->setFixedSize(subWindow->size()); + m_custom->setWindowTitle("Meter"); + m_custom->setModel(&model()->getCustomMeterModel()); + } + + m_custom->parentWidget()->show(); + model()->setTempoSync(TempoSyncKnobModel::SyncMode::Custom); +} + + +void TempoSyncBarModelEditor::updateTextDescription() +{ + TempoSyncKnobModel * tempoSyncModel = model(); + + auto const syncMode = tempoSyncModel->syncMode(); + + switch(syncMode) + { + case TempoSyncKnobModel::SyncMode::None: + m_tempoSyncDescription = tr("Tempo Sync"); + break; + case TempoSyncKnobModel::SyncMode::Custom: + m_tempoSyncDescription = tr("Custom ") + + "(" + + QString::number(tempoSyncModel->getCustomMeterModel().numeratorModel().value()) + + "/" + + QString::number(tempoSyncModel->getCustomMeterModel().denominatorModel().value()) + + ")"; + break; + case TempoSyncKnobModel::SyncMode::DoubleWholeNote: + m_tempoSyncDescription = tr("Synced to Eight Beats"); + break; + case TempoSyncKnobModel::SyncMode::WholeNote: + m_tempoSyncDescription = tr("Synced to Whole Note"); + break; + case TempoSyncKnobModel::SyncMode::HalfNote: + m_tempoSyncDescription = tr("Synced to Half Note"); + break; + case TempoSyncKnobModel::SyncMode::QuarterNote: + m_tempoSyncDescription = tr("Synced to Quarter Note"); + break; + case TempoSyncKnobModel::SyncMode::EighthNote: + m_tempoSyncDescription = tr("Synced to 8th Note"); + break; + case TempoSyncKnobModel::SyncMode::SixteenthNote: + m_tempoSyncDescription = tr("Synced to 16th Note"); + break; + case TempoSyncKnobModel::SyncMode::ThirtysecondNote: + m_tempoSyncDescription = tr("Synced to 32nd Note"); + break; + default: ; + } +} + +void TempoSyncBarModelEditor::updateIcon() +{ + switch(model()->syncMode()) + { + case TempoSyncKnobModel::SyncMode::None: + m_tempoSyncIcon = embed::getIconPixmap("tempo_sync"); + break; + case TempoSyncKnobModel::SyncMode::Custom: + m_tempoSyncIcon = embed::getIconPixmap("dont_know"); + break; + case TempoSyncKnobModel::SyncMode::DoubleWholeNote: + m_tempoSyncIcon = embed::getIconPixmap("note_double_whole"); + break; + case TempoSyncKnobModel::SyncMode::WholeNote: + m_tempoSyncIcon = embed::getIconPixmap("note_whole"); + break; + case TempoSyncKnobModel::SyncMode::HalfNote: + m_tempoSyncIcon = embed::getIconPixmap("note_half"); + break; + case TempoSyncKnobModel::SyncMode::QuarterNote: + m_tempoSyncIcon = embed::getIconPixmap("note_quarter"); + break; + case TempoSyncKnobModel::SyncMode::EighthNote: + m_tempoSyncIcon = embed::getIconPixmap("note_eighth"); + break; + case TempoSyncKnobModel::SyncMode::SixteenthNote: + m_tempoSyncIcon = embed::getIconPixmap("note_sixteenth"); + break; + case TempoSyncKnobModel::SyncMode::ThirtysecondNote: + m_tempoSyncIcon = embed::getIconPixmap("note_thirtysecond"); + break; + default: + qWarning("TempoSyncKnob::calculateTempoSyncTime:" + "invalid TempoSyncMode"); + break; + } +} + + +} // namespace lmms::gui From bf5f4a7994756c099c353623053de0c8f1fb2413 Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Fri, 13 Oct 2023 22:34:41 +0000 Subject: [PATCH 012/191] Fix instrument release being applied early As discovered by @michaelgregorious & @zonkmachine, applyRelease's condition to determine whether the release ramp starts within the current buffer was off by 1 frame, running the release code when the ramp should only start in frame 0 of the next buffer. This could cause the ramp to be miscalculated, starting at a value greater than 1.0 and and thus actually amplifying the signal. --- src/core/Instrument.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index fd729e3ab..2f987aaed 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -182,7 +182,7 @@ void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n ) const fpp_t frames = _n->framesLeftForCurrentPeriod(); const fpp_t fpp = Engine::audioEngine()->framesPerPeriod(); const f_cnt_t fl = _n->framesLeft(); - if( fl <= desiredReleaseFrames()+fpp ) + if( fl < desiredReleaseFrames()+fpp ) { for( fpp_t f = (fpp_t)( ( fl > desiredReleaseFrames() ) ? (std::max(fpp - desiredReleaseFrames(), 0) + From 253a9f334e5a3a9d1a4fb15ce5fc96e606f19ea8 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Mon, 30 Oct 2023 08:56:28 +0100 Subject: [PATCH 013/191] Fix CLI render crash Fix unchecked access to GUI leading to a segfault in headless render, a regression from 005ee47d439f0ccf023de4c73f552f8c5119ec63. Fixes #6942 --- src/tracks/InstrumentTrack.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 29fda075e..8804833ee 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -727,7 +727,8 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, // Handle automation: detuning for (const auto& processHandle : m_processHandles) { - processHandle->processTimePos(_start, m_pitchModel.value(), gui::GuiApplication::instance()->pianoRoll()->isRecording()); + processHandle->processTimePos( + _start, m_pitchModel.value(), gui::getGUI() && gui::getGUI()->pianoRoll()->isRecording()); } if ( clips.size() == 0 ) From 3d224cb4554603374d35cbe5f5e353df1350052d Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Tue, 31 Oct 2023 03:59:29 +0000 Subject: [PATCH 014/191] Push policy stack in InstallDependencies module (#6878) --- cmake/modules/InstallDependencies.cmake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmake/modules/InstallDependencies.cmake b/cmake/modules/InstallDependencies.cmake index 167a93f35..29e5b207c 100644 --- a/cmake/modules/InstallDependencies.cmake +++ b/cmake/modules/InstallDependencies.cmake @@ -1,7 +1,8 @@ include(GetPrerequisites) include(CMakeParseArguments) -# Project's cmake_minimum_required doesn't always propagate +# Project's cmake_minimum_required doesn't propagate to install scripts +cmake_policy(PUSH) cmake_policy(SET CMP0057 NEW) # Support new if() IN_LIST operator. function(make_absolute var) @@ -182,3 +183,5 @@ function(FIND_PREREQUISITES target RESULT_VAR exclude_system recurse set(${RESULT_VAR} ${RESULTS} PARENT_SCOPE) endfunction() + +cmake_policy(POP) From cd018c04ffa4438183a5617fb97b499700b45b88 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Tue, 31 Oct 2023 05:19:00 +0000 Subject: [PATCH 015/191] Improve CI cache usage (#6868) * Update third-party actions to latest version * Use vcpkg in manifest mode * Only trim ccache after build * Use ccache with MSVC * Use Brewfile and cache Homebrew downloads * Use --print-config for ccache 3 * Attempt to make ccache actually work with MSVC * Zero ccache stats before building * Use SDL2 on macOS --- .github/workflows/build.yml | 115 ++++++++++++++++++++++--------- .gitignore | 1 + Brewfile | 20 ++++++ CMakeLists.txt | 29 +++++--- cmake/modules/CompileCache.cmake | 47 ++++++++----- vcpkg.json | 77 +++++++++++++++++++++ 6 files changed, 233 insertions(+), 56 deletions(-) create mode 100644 Brewfile create mode 100644 vcpkg.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 007842b82..e8cec8867 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,8 @@ jobs: -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_COMPILE_CACHE=ON - CCACHE_MAXSIZE: 500M + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j2 steps: - name: Update and configure Git @@ -38,6 +39,7 @@ jobs: path: ~/.ccache - name: Configure run: | + ccache --zero-stats source /opt/qt5*/bin/qt5*-env.sh || true mkdir build && cd build cmake .. $CMAKE_OPTS -DCMAKE_INSTALL_PREFIX=./install @@ -56,12 +58,15 @@ jobs: with: name: linux path: build/lmms-*.AppImage - - name: Print ccache statistics + - name: Trim ccache and print statistics run: | + ccache --cleanup echo "[ccache config]" - ccache -p + ccache --print-config echo "[ccache stats]" - ccache -s + ccache --show-stats + env: + CCACHE_MAXSIZE: 500M macos: name: macos runs-on: macos-11 @@ -70,7 +75,8 @@ jobs: -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_COMPILE_CACHE=ON - CCACHE_MAXSIZE: 500M + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j3 DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer steps: @@ -79,6 +85,15 @@ jobs: with: fetch-depth: 0 submodules: recursive + - name: Clean up Homebrew download cache + run: rm -rf ~/Library/Caches/Homebrew/downloads + - name: Restore Homebrew download cache + uses: actions/cache/restore@v3 + with: + key: n/a - only restore from restore-keys + restore-keys: | + homebrew- + path: ~/Library/Caches/Homebrew/downloads - name: Cache ccache data uses: actions/cache@v3 with: @@ -89,12 +104,15 @@ jobs: path: ~/Library/Caches/ccache - name: Install dependencies run: | - brew install ccache fftw pkg-config libogg libvorbis lame libsndfile \ - libsamplerate jack sdl libgig libsoundio lilv lv2 stk \ - fluid-synth portaudio fltk qt@5 carla + brew bundle install --verbose npm install --location=global appdmg + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALL_UPGRADE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - name: Configure run: | + ccache --zero-stats mkdir build cmake -S . \ -B build \ @@ -117,12 +135,20 @@ jobs: with: name: macos path: build/lmms-*.dmg - - name: Print ccache statistics + - name: Trim ccache and print statistics run: | + ccache --cleanup echo "[ccache config]" - ccache -p + ccache --show-config echo "[ccache stats]" - ccache -s + ccache --show-stats --verbose + env: + CCACHE_MAXSIZE: 500MB + - name: Save Homebrew download cache + uses: actions/cache/save@v3 + with: + key: homebrew-${{ hashFiles('Brewfile.lock.json') }} + path: ~/Library/Caches/Homebrew/downloads mingw: strategy: fail-fast: false @@ -136,7 +162,8 @@ jobs: -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_COMPILE_CACHE=ON - CCACHE_MAXSIZE: 500M + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j2 steps: - name: Update and configure Git @@ -161,6 +188,7 @@ jobs: path: ~/.ccache - name: Configure run: | + ccache --zero-stats mkdir build && cd build ../cmake/build_win${{ matrix.arch }}.sh - name: Build @@ -174,12 +202,15 @@ jobs: with: name: mingw${{ matrix.arch }} path: build/lmms-*.exe - - name: Print ccache statistics + - name: Trim ccache and print statistics run: | + ccache --cleanup echo "[ccache config]" - ccache -p + ccache --print-config echo "[ccache stats]" - ccache -s + ccache --show-stats + env: + CCACHE_MAXSIZE: 500M msvc: strategy: fail-fast: false @@ -189,6 +220,8 @@ jobs: runs-on: windows-2019 env: qt-version: '5.15.2' + CCACHE_MAXSIZE: 0 + CCACHE_NOCOMPRESS: 1 steps: - name: Check out uses: actions/checkout@v3 @@ -196,49 +229,60 @@ jobs: fetch-depth: 0 submodules: recursive - name: Cache vcpkg dependencies + id: cache-deps uses: actions/cache@v3 with: - key: vcpkg-${{ matrix.arch }}-${{ github.ref }}-${{ github.run_id }} + key: vcpkg-${{ matrix.arch }}-${{ hashFiles('vcpkg.json') }} restore-keys: | - vcpkg-${{ matrix.arch }}-${{ github.ref }}- vcpkg-${{ matrix.arch }}- - path: C:\vcpkg\installed + path: build\vcpkg_installed + - name: Cache ccache data + uses: actions/cache@v3 + with: + key: "ccache-${{ github.job }}-${{ matrix.arch }}-${{ github.ref }}\ + -${{ github.run_id }}" + restore-keys: | + ccache-${{ github.job }}-${{ matrix.arch }}-${{ github.ref }}- + ccache-${{ github.job }}-${{ matrix.arch }}- + path: ~\AppData\Local\ccache + - name: Install tools + run: choco install ccache - name: Install 64-bit Qt if: matrix.arch == 'x64' - uses: jurplel/install-qt-action@64bdb64f2c14311d23733a8463e5fcbc65e8775e + uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946 with: version: ${{ env.qt-version }} arch: win64_msvc2019_64 archives: qtbase qtsvg qttools cache: true - name: Install 32-bit Qt - uses: jurplel/install-qt-action@64bdb64f2c14311d23733a8463e5fcbc65e8775e + uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946 with: version: ${{ env.qt-version }} arch: win32_msvc2019 archives: qtbase qtsvg qttools cache: true set-env: ${{ matrix.arch == 'x86' }} - - name: Install dependencies - run: | - vcpkg install ` - --triplet=${{ matrix.arch }}-windows ` - --host-triplet=${{ matrix.arch }}-windows ` - --recurse ` - fftw3 fltk fluidsynth[sndfile] libsamplerate libsndfile libstk ` - lilv lv2 portaudio sdl2 - name: Set up build environment - uses: ilammy/msvc-dev-cmd@d8610e2b41c6d0f0c3b4c46dad8df0fd826c68e1 + uses: ilammy/msvc-dev-cmd@cec98b9d092141f74527d0afa6feb2af698cfe89 with: arch: ${{ matrix.arch }} - name: Configure run: | - mkdir build + ccache --zero-stats + mkdir build -Force cmake -S . ` -B build ` -G Ninja ` --toolchain C:/vcpkg/scripts/buildsystems/vcpkg.cmake ` - -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` + -DUSE_COMPILE_CACHE=ON ` + -DVCPKG_TARGET_TRIPLET="${{ matrix.arch }}-windows" ` + -DVCPKG_HOST_TRIPLET="${{ matrix.arch }}-windows" ` + -DVCPKG_MANIFEST_INSTALL="${{ env.should_install_manifest }}" + env: + should_install_manifest: + ${{ steps.cache-deps.outputs.cache-hit == 'true' && 'NO' || 'YES' }} - name: Build run: cmake --build build - name: Build tests @@ -250,3 +294,12 @@ jobs: with: name: msvc-${{ matrix.arch }} path: build\lmms-*.exe + - name: Trim ccache and print statistics + run: | + ccache --cleanup + echo "[ccache config]" + ccache --show-config + echo "[ccache stats]" + ccache --show-stats --verbose + env: + CCACHE_MAXSIZE: 500MB diff --git a/.gitignore b/.gitignore index ee289379f..1b855f204 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /plugins/ZynAddSubFx/zynaddsubfx/doc/Makefile /plugins/ZynAddSubFx/zynaddsubfx/doc/gen/Makefile /data/locale/*.qm +Brewfile.lock.json diff --git a/Brewfile b/Brewfile new file mode 100644 index 000000000..1bfbd7b01 --- /dev/null +++ b/Brewfile @@ -0,0 +1,20 @@ +brew "carla" +brew "ccache" +brew "fftw" +brew "fltk" +brew "fluid-synth" +brew "jack" +brew "lame" +brew "libgig" +brew "libogg" +brew "libsamplerate" +brew "libsndfile" +brew "libsoundio" +brew "libvorbis" +brew "lilv" +brew "lv2" +brew "pkg-config" +brew "portaudio" +brew "qt@5" +brew "sdl2" +brew "stk" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b6d3b4ff..9ef6aaba3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,31 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.9) +# Set the given policy to NEW. If it does not exist, it will not be set. If it +# is already set to NEW (most likely due to predating the minimum required CMake +# version), a developer warning is emitted indicating that the policy need no +# longer be explicitly set. +function(enable_policy_if_exists id) + if(POLICY "${id}") + cmake_policy(GET "${id}" current_value) + if(current_value STREQUAL "NEW") + message(AUTHOR_WARNING "${id} is now set to NEW by default, and no longer needs to be explicitly set.") + else() + cmake_policy(SET "${id}" NEW) + endif() + endif() +endfunction() + +# Needed for the SWH Ladspa plugins. See below. +enable_policy_if_exists(CMP0074) # find_package() uses _ROOT variables. +# Needed for ccache support with MSVC +enable_policy_if_exists(CMP0141) # MSVC debug information format flags are selected by an abstraction. + PROJECT(lmms) SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules" ${CMAKE_MODULE_PATH}) SET(LMMS_BINARY_DIR ${CMAKE_BINARY_DIR}) SET(LMMS_SOURCE_DIR ${CMAKE_SOURCE_DIR}) -# CMAKE_POLICY Section -IF(COMMAND CMAKE_POLICY) - # TODO: Keep CMP0074 but remove this condition when cmake 3.12+ is guaranteed - IF(${CMAKE_VERSION} VERSION_GREATER_EQUAL 3.12) - # Needed for the SWH Ladspa plugins. See below. - CMAKE_POLICY(SET CMP0074 NEW) # find_package() uses _ROOT variables - ENDIF() -ENDIF(COMMAND CMAKE_POLICY) - # Import of windows.h breaks min()/max() ADD_DEFINITIONS(-DNOMINMAX) diff --git a/cmake/modules/CompileCache.cmake b/cmake/modules/CompileCache.cmake index ed4622bd9..56486e24f 100644 --- a/cmake/modules/CompileCache.cmake +++ b/cmake/modules/CompileCache.cmake @@ -1,25 +1,40 @@ -option(USE_COMPILE_CACHE "Use ccache or clcache for compilation" OFF) +option(USE_COMPILE_CACHE "Use a compiler cache for compilation" OFF) # Compatibility for old option name if(USE_CCACHE) set(USE_COMPILE_CACHE ON) endif() -if(USE_COMPILE_CACHE) - if(MSVC) - set(CACHE_TOOL_NAME clcache) - elseif(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|AppleClang|Clang)") - set(CACHE_TOOL_NAME ccache) - else() - message(WARNING "Compile cache only available with MSVC or GNU") +if(NOT USE_COMPILE_CACHE) + return() +endif() + +if(NOT CMAKE_CXX_COMPILER_ID MATCHES "(GNU|AppleClang|Clang|MSVC)") + message(WARNING "Compiler cache only available with MSVC or GNU") + return() +endif() + +set(CACHE_TOOL_NAME ccache) +find_program(CACHE_TOOL "${CACHE_TOOL_NAME}") +if(NOT CACHE_TOOL) + message(WARNING "USE_COMPILE_CACHE enabled, but no ${CACHE_TOOL_NAME} found") + return() +endif() + +if(MSVC) + # ccache doesn't support debug information in the PDB format. Setting the + # debug information format requires CMP0141, introduced with CMake 3.25, to + # be set to NEW prior to the initial `project` command. + if(CMAKE_VERSION VERSION_LESS "3.25") + message(WARNING "Use of compiler cache with MSVC requires at least CMake 3.25") + return() endif() - find_program(CACHE_TOOL ${CACHE_TOOL_NAME}) - if (CACHE_TOOL) - message(STATUS "Using ${CACHE_TOOL} found for caching") - set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CACHE_TOOL}) - set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CACHE_TOOL}) - else() - message(WARNING "USE_COMPILE_CACHE enabled, but no ${CACHE_TOOL_NAME} found") - endif() + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") endif() + +message(STATUS "Using ${CACHE_TOOL} for compiler caching") + +# TODO CMake 3.21: Use CMAKE___LAUNCHER variables instead +set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CACHE_TOOL}") +set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CACHE_TOOL}") diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..48a3e3c28 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + { + "name": "fftw3", + "default-features": false, + "features": [ + "sse", + "sse2", + "avx", + "avx2" + ] + }, + { + "name": "fltk", + "default-features": false + }, + { + "name": "fluidsynth", + "default-features": false, + "features": [ + "sndfile" + ] + }, + { + "name": "libogg", + "default-features": false + }, + { + "name": "libsamplerate", + "default-features": false + }, + { + "name": "libsndfile", + "default-features": false, + "features": [ + "external-libs", + "mpeg" + ] + }, + { + "name": "libstk", + "default-features": false + }, + { + "name": "libvorbis", + "default-features": false + }, + { + "name": "lilv", + "default-features": false + }, + { + "name": "lv2", + "default-features": false + }, + { + "name": "mp3lame", + "default-features": false + }, + { + "name": "portaudio", + "default-features": false + }, + { + "name": "sdl2", + "default-features": false, + "features": [ + "base" + ] + }, + { + "name": "zlib", + "default-features": false + } + ] +} From d277916c010d1975fbf71288ecadcc60f56b0074 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Tue, 31 Oct 2023 20:25:55 +0100 Subject: [PATCH 016/191] Fix instrument release ending one frame early See discussion in https://github.com/LMMS/lmms/pull/6908#issuecomment-1784637574 and following comments. --- src/core/Instrument.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index 2f987aaed..27529bd20 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -188,7 +188,7 @@ void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n ) (std::max(fpp - desiredReleaseFrames(), 0) + fl) % fpp : 0); f < frames; ++f) { - const float fac = (float)( fl-f-1 ) / + const float fac = (float)( fl-f ) / desiredReleaseFrames(); for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) { From 98ae7a6973087faa102b00506a9505b49c49007e Mon Sep 17 00:00:00 2001 From: Lukas W Date: Tue, 31 Oct 2023 20:28:17 +0100 Subject: [PATCH 017/191] Refactor Instrument::applyRelease to be more readable --- src/core/Instrument.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index 27529bd20..a7cfc467b 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -179,21 +179,18 @@ void Instrument::applyFadeIn(sampleFrame * buf, NotePlayHandle * n) void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n ) { - const fpp_t frames = _n->framesLeftForCurrentPeriod(); - const fpp_t fpp = Engine::audioEngine()->framesPerPeriod(); - const f_cnt_t fl = _n->framesLeft(); - if( fl < desiredReleaseFrames()+fpp ) + const auto fpp = Engine::audioEngine()->framesPerPeriod(); + const auto releaseFrames = desiredReleaseFrames(); + + const auto endFrame = _n->framesLeft(); + const auto startFrame = std::max(0, endFrame - releaseFrames); + + for (auto f = startFrame; f < endFrame && f < fpp; f++) { - for( fpp_t f = (fpp_t)( ( fl > desiredReleaseFrames() ) ? - (std::max(fpp - desiredReleaseFrames(), 0) + - fl) % fpp : 0); f < frames; ++f) + const float fac = (float)(endFrame - f) / (float)releaseFrames; + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ch++) { - const float fac = (float)( fl-f ) / - desiredReleaseFrames(); - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) - { - buf[f][ch] *= fac; - } + buf[f][ch] *= fac; } } } From 54cc4cf1e9e65dba06d22f004bb33b43c09d029e Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sun, 1 Oct 2023 16:51:27 +0200 Subject: [PATCH 018/191] AudioJack: Fix segfault for some bufsizes The segfault happens when you use buffersize 224: Then, in AudioJack.cpp, L424, `done` can be greater than `_nframes`. --- src/core/audio/AudioJack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index 83fa3c177..dc6bc2861 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -390,7 +390,7 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) while( done < _nframes && m_stopped == false ) { jack_nframes_t todo = std::min( - _nframes, + _nframes-done, m_framesToDoInCurBuf - m_framesDoneInCurBuf); const float gain = audioEngine()->masterGain(); From f6eacb31ba3b4f0e6e36272698f89bfaa008c42c Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sun, 1 Oct 2023 18:53:06 +0200 Subject: [PATCH 019/191] AudioJack: Style fixes These were done with `clang-format`, and slight patching where `clang-format` was a bit weird. --- include/AudioJack.h | 57 +++--- src/core/audio/AudioJack.cpp | 351 +++++++++++++++-------------------- 2 files changed, 170 insertions(+), 238 deletions(-) diff --git a/include/AudioJack.h b/include/AudioJack.h index 164258e5f..6efb262ed 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -57,42 +57,37 @@ class AudioJack : public QObject, public AudioDevice { Q_OBJECT public: - AudioJack( bool & _success_ful, AudioEngine* audioEngine ); + AudioJack(bool& successful, AudioEngine* audioEngine); ~AudioJack() override; // this is to allow the jack midi connection to use the same jack client connection // the jack callback is handled here, we call the midi client so that it can read // it's midi data during the callback - AudioJack * addMidiClient(MidiJack *midiClient); + AudioJack* addMidiClient(MidiJack* midiClient); void removeMidiClient() { m_midiClient = nullptr; } - jack_client_t * jackClient() {return m_client;}; + jack_client_t* jackClient() { return m_client; }; inline static QString name() { - return QT_TRANSLATE_NOOP( "AudioDeviceSetupWidget", - "JACK (JACK Audio Connection Kit)" ); + return QT_TRANSLATE_NOOP("AudioDeviceSetupWidget", "JACK (JACK Audio Connection Kit)"); } - -class setupWidget : public gui::AudioDeviceSetupWidget + class setupWidget : public gui::AudioDeviceSetupWidget { public: - setupWidget( QWidget * _parent ); + setupWidget(QWidget* parent); ~setupWidget() override; void saveSettings() override; private: - QLineEdit * m_clientName; - gui::LcdSpinBox * m_channels; - - } ; - + QLineEdit* m_clientName; + gui::LcdSpinBox* m_channels; + }; private slots: void restartAfterZombified(); - private: bool initJackClient(); @@ -100,45 +95,41 @@ private: void stopProcessing() override; void applyQualitySettings() override; - void registerPort( AudioPort * _port ) override; - void unregisterPort( AudioPort * _port ) override; - void renamePort( AudioPort * _port ) override; + void registerPort(AudioPort* port) override; + void unregisterPort(AudioPort* port) override; + void renamePort(AudioPort* port) override; - int processCallback( jack_nframes_t _nframes, void * _udata ); + int processCallback(jack_nframes_t nframes); - static int staticProcessCallback( jack_nframes_t _nframes, - void * _udata ); - static void shutdownCallback( void * _udata ); + static int staticProcessCallback(jack_nframes_t nframes, void* udata); + static void shutdownCallback(void* _udata); - - jack_client_t * m_client; + jack_client_t* m_client; bool m_active; std::atomic m_stopped; - std::atomic m_midiClient; - std::vector m_outputPorts; - jack_default_audio_sample_t * * m_tempOutBufs; - surroundSampleFrame * m_outBuf; + std::atomic m_midiClient; + std::vector m_outputPorts; + jack_default_audio_sample_t** m_tempOutBufs; + surroundSampleFrame* m_outBuf; f_cnt_t m_framesDoneInCurBuf; f_cnt_t m_framesToDoInCurBuf; - #ifdef AUDIO_PORT_SUPPORT struct StereoPort { - jack_port_t * ports[2]; - } ; + jack_port_t* ports[2]; + }; - using JackPortMap = QMap; + using JackPortMap = QMap; JackPortMap m_portMap; #endif signals: void zombified(); - -} ; +}; } // namespace lmms diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index dc6bc2861..55382f715 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -30,42 +30,43 @@ #include #include +#include "AudioEngine.h" +#include "ConfigManager.h" #include "Engine.h" #include "GuiApplication.h" -#include "gui_templates.h" -#include "ConfigManager.h" #include "LcdSpinBox.h" #include "MainWindow.h" -#include "AudioEngine.h" #include "MidiJack.h" - +#include "gui_templates.h" namespace lmms { -AudioJack::AudioJack( bool & _success_ful, AudioEngine* _audioEngine ) : - AudioDevice(std::clamp( - ConfigManager::inst()->value("audiojack", "channels").toInt(), - DEFAULT_CHANNELS, - SURROUND_CHANNELS), _audioEngine), - m_client( nullptr ), - m_active( false ), - m_midiClient( nullptr ), - m_tempOutBufs( new jack_default_audio_sample_t *[channels()] ), - m_outBuf( new surroundSampleFrame[audioEngine()->framesPerPeriod()] ), - m_framesDoneInCurBuf( 0 ), - m_framesToDoInCurBuf( 0 ) + +AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam) + : AudioDevice( + // clang-format off + std::clamp( + ConfigManager::inst()->value("audiojack", "channels").toInt(), + DEFAULT_CHANNELS, + SURROUND_CHANNELS + ), + // clang-format on + audioEngineParam) + , m_client(nullptr) + , m_active(false) + , m_midiClient(nullptr) + , m_tempOutBufs(new jack_default_audio_sample_t*[channels()]) + , m_outBuf(new surroundSampleFrame[audioEngine()->framesPerPeriod()]) + , m_framesDoneInCurBuf(0) + , m_framesToDoInCurBuf(0) { m_stopped = true; - _success_ful = initJackClient(); - if( _success_ful ) - { - connect( this, SIGNAL(zombified()), - this, SLOT(restartAfterZombified()), - Qt::QueuedConnection ); + successful = initJackClient(); + if (successful) { + connect(this, SIGNAL(zombified()), this, SLOT(restartAfterZombified()), Qt::QueuedConnection); } - } @@ -73,21 +74,18 @@ AudioJack::AudioJack( bool & _success_ful, AudioEngine* _audioEngine ) : AudioJack::~AudioJack() { - stopProcessing(); + AudioJack::stopProcessing(); #ifdef AUDIO_PORT_SUPPORT - while( m_portMap.size() ) + while (m_portMap.size()) { - unregisterPort( m_portMap.begin().key() ); + unregisterPort(m_portMap.begin().key()); } #endif - if( m_client != nullptr ) + if (m_client != nullptr) { - if( m_active ) - { - jack_deactivate( m_client ); - } - jack_client_close( m_client ); + if (m_active) { jack_deactivate(m_client); } + jack_client_close(m_client); } delete[] m_tempOutBufs; @@ -100,97 +98,79 @@ AudioJack::~AudioJack() void AudioJack::restartAfterZombified() { - if( initJackClient() ) + if (initJackClient()) { m_active = false; startProcessing(); - QMessageBox::information(gui::getGUI()->mainWindow(), - tr( "JACK client restarted" ), - tr( "LMMS was kicked by JACK for some reason. " + QMessageBox::information(gui::getGUI()->mainWindow(), tr("JACK client restarted"), + tr( "LMMS was kicked by JACK for some reason. " "Therefore the JACK backend of LMMS has been " "restarted. You will have to make manual " - "connections again." ) ); + "connections again.")); } else { - QMessageBox::information(gui::getGUI()->mainWindow(), - tr( "JACK server down" ), - tr( "The JACK server seems to have been shutdown " + QMessageBox::information(gui::getGUI()->mainWindow(), tr("JACK server down"), + tr( "The JACK server seems to have been shutdown " "and starting a new instance failed. " "Therefore LMMS is unable to proceed. " "You should save your project and restart " - "JACK and LMMS." ) ); + "JACK and LMMS.")); } } -AudioJack* AudioJack::addMidiClient(MidiJack *midiClient) + +AudioJack* AudioJack::addMidiClient(MidiJack* midiClient) { - if( m_client == nullptr ) - return nullptr; + if (m_client == nullptr) { return nullptr; } m_midiClient = midiClient; return this; } + + + bool AudioJack::initJackClient() { - QString clientName = ConfigManager::inst()->value( "audiojack", - "clientname" ); - if( clientName.isEmpty() ) - { - clientName = "lmms"; - } + QString clientName = ConfigManager::inst()->value("audiojack", "clientname"); + if (clientName.isEmpty()) { clientName = "lmms"; } - const char * serverName = nullptr; + const char* serverName = nullptr; jack_status_t status; - m_client = jack_client_open( clientName.toLatin1().constData(), - JackNullOption, &status, - serverName ); - if( m_client == nullptr ) + m_client = jack_client_open(clientName.toLatin1().constData(), JackNullOption, &status, serverName); + if (m_client == nullptr) { - printf( "jack_client_open() failed, status 0x%2.0x\n", status ); - if( status & JackServerFailed ) - { - printf( "Could not connect to JACK server.\n" ); - } + printf("jack_client_open() failed, status 0x%2.0x\n", status); + if (status & JackServerFailed) { printf("Could not connect to JACK server.\n"); } return false; } - if( status & JackNameNotUnique ) + if (status & JackNameNotUnique) { - printf( "there's already a client with name '%s', so unique " - "name '%s' was assigned\n", clientName. - toLatin1().constData(), - jack_get_client_name( m_client ) ); + printf( "there's already a client with name '%s', so unique " + "name '%s' was assigned\n", + clientName.toLatin1().constData(), jack_get_client_name(m_client)); } // set process-callback - jack_set_process_callback( m_client, staticProcessCallback, this ); + jack_set_process_callback(m_client, staticProcessCallback, this); // set shutdown-callback - jack_on_shutdown( m_client, shutdownCallback, this ); + jack_on_shutdown(m_client, shutdownCallback, this); + if (jack_get_sample_rate(m_client) != sampleRate()) { setSampleRate(jack_get_sample_rate(m_client)); } - - if( jack_get_sample_rate( m_client ) != sampleRate() ) + for (ch_cnt_t ch = 0; ch < channels(); ++ch) { - setSampleRate( jack_get_sample_rate( m_client ) ); - } - - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) - { - QString name = QString( "master out " ) + - ( ( ch % 2 ) ? "R" : "L" ) + - QString::number( ch / 2 + 1 ); - m_outputPorts.push_back( jack_port_register( m_client, - name.toLatin1().constData(), - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0 ) ); - if( m_outputPorts.back() == nullptr ) + QString name = QString("master out ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1); + m_outputPorts.push_back( + jack_port_register(m_client, name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0)); + if (m_outputPorts.back() == nullptr) { - printf( "no more JACK-ports available!\n" ); + printf("no more JACK-ports available!\n"); return false; } } @@ -203,51 +183,43 @@ bool AudioJack::initJackClient() void AudioJack::startProcessing() { - if( m_active || m_client == nullptr ) + if (m_active || m_client == nullptr) { m_stopped = false; return; } - if( jack_activate( m_client ) ) + if (jack_activate(m_client)) { - printf( "cannot activate client\n" ); + printf("cannot activate client\n"); return; } m_active = true; - // try to sync JACK's and LMMS's buffer-size -// jack_set_buffer_size( m_client, audioEngine()->framesPerPeriod() ); + // jack_set_buffer_size( m_client, audioEngine()->framesPerPeriod() ); - - - const char * * ports = jack_get_ports( m_client, nullptr, nullptr, - JackPortIsPhysical | - JackPortIsInput ); - if( ports == nullptr ) + const char** ports = jack_get_ports(m_client, nullptr, nullptr, JackPortIsPhysical | JackPortIsInput); + if (ports == nullptr) { - printf( "no physical playback ports. you'll have to do " - "connections at your own!\n" ); + printf("no physical playback ports. you'll have to do " + "connections at your own!\n"); } else { - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) + for (ch_cnt_t ch = 0; ch < channels(); ++ch) { - if( jack_connect( m_client, jack_port_name( - m_outputPorts[ch] ), - ports[ch] ) ) + if (jack_connect(m_client, jack_port_name(m_outputPorts[ch]), ports[ch])) { - printf( "cannot connect output ports. you'll " - "have to do connections at your own!\n" - ); + printf("cannot connect output ports. you'll " + "have to do connections at your own!\n"); } } } m_stopped = false; - free( ports ); + free(ports); } @@ -263,14 +235,11 @@ void AudioJack::stopProcessing() void AudioJack::applyQualitySettings() { - if( hqAudio() ) + if (hqAudio()) { - setSampleRate( Engine::audioEngine()->processingSampleRate() ); + setSampleRate(Engine::audioEngine()->processingSampleRate()); - if( jack_get_sample_rate( m_client ) != sampleRate() ) - { - setSampleRate( jack_get_sample_rate( m_client ) ); - } + if (jack_get_sample_rate(m_client) != sampleRate()) { setSampleRate(jack_get_sample_rate(m_client)); } } AudioDevice::applyQualitySettings(); @@ -279,106 +248,91 @@ void AudioJack::applyQualitySettings() -void AudioJack::registerPort( AudioPort * _port ) +void AudioJack::registerPort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT // make sure, port is not already registered - unregisterPort( _port ); - const QString name[2] = { _port->name() + " L", - _port->name() + " R" } ; + unregisterPort(port); + const QString name[2] = {port->name() + " L", port->name() + " R"}; - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) { - m_portMap[_port].ports[ch] = jack_port_register( m_client, - name[ch].toLatin1().constData(), - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0 ); + m_portMap[port].ports[ch] = jack_port_register( + m_client, name[ch].toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); } +#else + (void)port; #endif } -void AudioJack::unregisterPort( AudioPort * _port ) +void AudioJack::unregisterPort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT - if( m_portMap.contains( _port ) ) + if (m_portMap.contains(port)) { - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) { - if( m_portMap[_port].ports[ch] != nullptr ) - { - jack_port_unregister( m_client, - m_portMap[_port].ports[ch] ); - } + if (m_portMap[port].ports[ch] != nullptr) { jack_port_unregister(m_client, m_portMap[port].ports[ch]); } } - m_portMap.erase( m_portMap.find( _port ) ); + m_portMap.erase(m_portMap.find(port)); } +#else + (void)port; #endif } - - - -void AudioJack::renamePort( AudioPort * _port ) +void AudioJack::renamePort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT - if( m_portMap.contains( _port ) ) + if (m_portMap.contains(port)) { - const QString name[2] = { _port->name() + " L", - _port->name() + " R" }; - for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) + const QString name[2] = {port->name() + " L", port->name() + " R"}; + for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) { #ifdef LMMS_HAVE_JACK_PRENAME - jack_port_rename( m_client, m_portMap[_port].ports[ch], - name[ch].toLatin1().constData() ); + jack_port_rename(m_client, m_portMap[port].ports[ch], name[ch].toLatin1().constData()); #else - jack_port_set_name( m_portMap[_port].ports[ch], - name[ch].toLatin1().constData() ); + jack_port_set_name(m_portMap[port].ports[ch], name[ch].toLatin1().constData()); #endif } } +#else + (void)port; #endif // AUDIO_PORT_SUPPORT } -int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) +int AudioJack::processCallback(jack_nframes_t nframes) { // do midi processing first so that midi input can // add to the following sound processing - if( m_midiClient && _nframes > 0 ) + if (m_midiClient && nframes > 0) { - m_midiClient.load()->JackMidiRead(_nframes); - m_midiClient.load()->JackMidiWrite(_nframes); + m_midiClient.load()->JackMidiRead(nframes); + m_midiClient.load()->JackMidiWrite(nframes); } - for( int c = 0; c < channels(); ++c ) + for (int c = 0; c < channels(); ++c) { - m_tempOutBufs[c] = - (jack_default_audio_sample_t *) jack_port_get_buffer( - m_outputPorts[c], _nframes ); + m_tempOutBufs[c] = (jack_default_audio_sample_t*)jack_port_get_buffer(m_outputPorts[c], nframes); } #ifdef AUDIO_PORT_SUPPORT - const int frames = std::min(_nframes, audioEngine()->framesPerPeriod()); - for( JackPortMap::iterator it = m_portMap.begin(); - it != m_portMap.end(); ++it ) + const int frames = std::min(nframes, audioEngine()->framesPerPeriod()); + for (JackPortMap::iterator it = m_portMap.begin(); it != m_portMap.end(); ++it) { - for( ch_cnt_t ch = 0; ch < channels(); ++ch ) + for (ch_cnt_t ch = 0; ch < channels(); ++ch) { - if( it.value().ports[ch] == nullptr ) - { - continue; - } - jack_default_audio_sample_t * buf = - (jack_default_audio_sample_t *) jack_port_get_buffer( - it.value().ports[ch], - _nframes ); - for( int frame = 0; frame < frames; ++frame ) + if (it.value().ports[ch] == nullptr) { continue; } + jack_default_audio_sample_t* buf + = (jack_default_audio_sample_t*)jack_port_get_buffer(it.value().ports[ch], nframes); + for (int frame = 0; frame < frames; ++frame) { buf[frame] = it.key()->buffer()[frame][ch]; } @@ -387,28 +341,25 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) #endif jack_nframes_t done = 0; - while( done < _nframes && m_stopped == false ) + while (done < nframes && !m_stopped) { - jack_nframes_t todo = std::min( - _nframes-done, - m_framesToDoInCurBuf - - m_framesDoneInCurBuf); + jack_nframes_t todo = std::min(nframes - done, m_framesToDoInCurBuf - m_framesDoneInCurBuf); const float gain = audioEngine()->masterGain(); - for( int c = 0; c < channels(); ++c ) + for (int c = 0; c < channels(); ++c) { - jack_default_audio_sample_t * o = m_tempOutBufs[c]; - for( jack_nframes_t frame = 0; frame < todo; ++frame ) + jack_default_audio_sample_t* o = m_tempOutBufs[c]; + for (jack_nframes_t frame = 0; frame < todo; ++frame) { - o[done+frame] = m_outBuf[m_framesDoneInCurBuf+frame][c] * gain; + o[done + frame] = m_outBuf[m_framesDoneInCurBuf + frame][c] * gain; } } done += todo; m_framesDoneInCurBuf += todo; - if( m_framesDoneInCurBuf == m_framesToDoInCurBuf ) + if (m_framesDoneInCurBuf == m_framesToDoInCurBuf) { - m_framesToDoInCurBuf = getNextBuffer( m_outBuf ); + m_framesToDoInCurBuf = getNextBuffer(m_outBuf); m_framesDoneInCurBuf = 0; - if( !m_framesToDoInCurBuf ) + if (!m_framesToDoInCurBuf) { m_stopped = true; break; @@ -416,12 +367,12 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) } } - if( _nframes != done ) + if (nframes != done) { - for( int c = 0; c < channels(); ++c ) + for (int c = 0; c < channels(); ++c) { - jack_default_audio_sample_t * b = m_tempOutBufs[c] + done; - memset( b, 0, sizeof( *b ) * ( _nframes - done ) ); + jack_default_audio_sample_t* b = m_tempOutBufs[c] + done; + memset(b, 0, sizeof(*b) * (nframes - done)); } } @@ -431,51 +382,44 @@ int AudioJack::processCallback( jack_nframes_t _nframes, void * _udata ) -int AudioJack::staticProcessCallback( jack_nframes_t _nframes, void * _udata ) +int AudioJack::staticProcessCallback(jack_nframes_t nframes, void* udata) { - return static_cast( _udata )-> - processCallback( _nframes, _udata ); + return static_cast(udata)->processCallback(nframes); } -void AudioJack::shutdownCallback( void * _udata ) +void AudioJack::shutdownCallback(void* udata) { - auto _this = static_cast(_udata); - _this->m_client = nullptr; - _this->zombified(); + auto thisClass = static_cast(udata); + thisClass->m_client = nullptr; + emit thisClass->zombified(); } - -AudioJack::setupWidget::setupWidget( QWidget * _parent ) : - AudioDeviceSetupWidget( AudioJack::name(), _parent ) +AudioJack::setupWidget::setupWidget(QWidget* parent) + : AudioDeviceSetupWidget(AudioJack::name(), parent) { QFormLayout * form = new QFormLayout(this); - QString cn = ConfigManager::inst()->value( "audiojack", "clientname" ); - if( cn.isEmpty() ) - { - cn = "lmms"; - } - m_clientName = new QLineEdit( cn, this ); + QString cn = ConfigManager::inst()->value("audiojack", "clientname"); + if (cn.isEmpty()) { cn = "lmms"; } + m_clientName = new QLineEdit(cn, this); form->addRow(tr("Client name"), m_clientName); auto m = new gui::LcdSpinBoxModel(/* this */); - m->setRange( DEFAULT_CHANNELS, SURROUND_CHANNELS ); - m->setStep( 2 ); - m->setValue( ConfigManager::inst()->value( "audiojack", - "channels" ).toInt() ); + m->setRange(DEFAULT_CHANNELS, SURROUND_CHANNELS); + m->setStep(2); + m->setValue(ConfigManager::inst()->value("audiojack", "channels").toInt()); - m_channels = new gui::LcdSpinBox( 1, this ); - m_channels->setModel( m ); + m_channels = new gui::LcdSpinBox(1, this); + m_channels->setModel(m); form->addRow(tr("Channels"), m_channels); - } @@ -491,14 +435,11 @@ AudioJack::setupWidget::~setupWidget() void AudioJack::setupWidget::saveSettings() { - ConfigManager::inst()->setValue( "audiojack", "clientname", - m_clientName->text() ); - ConfigManager::inst()->setValue( "audiojack", "channels", - QString::number( m_channels->value() ) ); + ConfigManager::inst()->setValue("audiojack", "clientname", m_clientName->text()); + ConfigManager::inst()->setValue("audiojack", "channels", QString::number(m_channels->value())); } - } // namespace lmms #endif // LMMS_HAVE_JACK From 5d60035c021d509a0dee59db12fab53a4f7dd6f6 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sun, 22 Oct 2023 22:00:28 +0200 Subject: [PATCH 020/191] Use "jack_free" instead of "free" (#5954?) Thanks to @messmerd for the hint. Might fix #5954. --- src/core/audio/AudioJack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index 55382f715..a4fd2c095 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -219,7 +219,7 @@ void AudioJack::startProcessing() } m_stopped = false; - free(ports); + jack_free(ports); } From 7b99c589269436c33d50db9769d35ecfe94d65de Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Wed, 1 Nov 2023 19:34:32 +0000 Subject: [PATCH 021/191] Expose targets and versions for more dependencies (#6842) --- CMakeLists.txt | 2 - cmake/modules/BuildPlugin.cmake | 9 +- cmake/modules/FindFluidSynth.cmake | 2 +- cmake/modules/FindLame.cmake | 50 +++--- cmake/modules/FindOggVorbis.cmake | 132 +++++++--------- cmake/modules/FindPortaudio.cmake | 56 +++---- cmake/modules/FindSTK.cmake | 50 +++--- cmake/modules/FindSamplerate.cmake | 53 +++---- cmake/modules/FindSndFile.cmake | 63 ++++---- cmake/modules/ImportedTargetHelpers.cmake | 177 +++++++++++++++++++++- plugins/CMakeLists.txt | 5 +- plugins/GigPlayer/CMakeLists.txt | 13 +- plugins/Sf2Player/CMakeLists.txt | 5 +- plugins/Watsyn/CMakeLists.txt | 11 +- src/CMakeLists.txt | 28 ++-- 15 files changed, 382 insertions(+), 274 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef6aaba3..5f388bf30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -439,8 +439,6 @@ IF(WANT_MP3LAME) SET(STATUS_MP3LAME "OK") ELSE(LAME_FOUND) SET(STATUS_MP3LAME "not found, please install libmp3lame-dev (or similar)") - SET(LAME_LIBRARIES "") - SET(LAME_INCLUDE_DIRS "") ENDIF(LAME_FOUND) ELSE(WANT_MP3LAME) SET(STATUS_MP3LAME "Disabled for build") diff --git a/cmake/modules/BuildPlugin.cmake b/cmake/modules/BuildPlugin.cmake index f8b3d3153..70e518c93 100644 --- a/cmake/modules/BuildPlugin.cmake +++ b/cmake/modules/BuildPlugin.cmake @@ -56,11 +56,7 @@ MACRO(BUILD_PLUGIN PLUGIN_NAME) ADD_LIBRARY(${PLUGIN_NAME} ${PLUGIN_LINK} ${PLUGIN_SOURCES} ${plugin_MOC_out} ${RCC_OUT}) - TARGET_LINK_LIBRARIES(${PLUGIN_NAME} Qt5::Widgets Qt5::Xml) - - IF(LMMS_BUILD_WIN32) - TARGET_LINK_LIBRARIES(${PLUGIN_NAME} lmms) - ENDIF(LMMS_BUILD_WIN32) + target_link_libraries("${PLUGIN_NAME}" lmms Qt5::Widgets Qt5::Xml) INSTALL(TARGETS ${PLUGIN_NAME} LIBRARY DESTINATION "${PLUGIN_DIR}" @@ -70,10 +66,7 @@ MACRO(BUILD_PLUGIN PLUGIN_NAME) IF(LMMS_BUILD_APPLE) IF ("${PLUGIN_LINK}" STREQUAL "SHARED") SET_TARGET_PROPERTIES(${PLUGIN_NAME} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") - ELSE() - SET_TARGET_PROPERTIES(${PLUGIN_NAME} PROPERTIES LINK_FLAGS "-bundle_loader \"${CMAKE_BINARY_DIR}/lmms\"") ENDIF() - ADD_DEPENDENCIES(${PLUGIN_NAME} lmms) ENDIF(LMMS_BUILD_APPLE) IF(LMMS_BUILD_WIN32) add_custom_command( diff --git a/cmake/modules/FindFluidSynth.cmake b/cmake/modules/FindFluidSynth.cmake index fcc00cd7d..70c40b8d8 100644 --- a/cmake/modules/FindFluidSynth.cmake +++ b/cmake/modules/FindFluidSynth.cmake @@ -34,7 +34,7 @@ if(FluidSynth_INCLUDE_DIR AND FluidSynth_LIBRARY) if(VCPKG_INSTALLED_DIR) include(ImportedTargetHelpers) - _get_vcpkg_library_configs(FluidSynth_IMPLIB_RELEASE FluidSynth_IMPLIB_DEBUG "${FluidSynth_LIBRARY}") + get_vcpkg_library_configs(FluidSynth_IMPLIB_RELEASE FluidSynth_IMPLIB_DEBUG "${FluidSynth_LIBRARY}") else() set(FluidSynth_IMPLIB_RELEASE "${FluidSynth_LIBRARY}") endif() diff --git a/cmake/modules/FindLame.cmake b/cmake/modules/FindLame.cmake index c3fb09c5b..3017dc5aa 100644 --- a/cmake/modules/FindLame.cmake +++ b/cmake/modules/FindLame.cmake @@ -1,37 +1,31 @@ -# - Try to find LAME -# Once done this will define +# Copyright (c) 2023 Dominic Clark # -# Lame_FOUND - system has liblame -# Lame_INCLUDE_DIRS - the liblame include directory -# Lame_LIBRARIES - The liblame libraries -# mp3lame::mp3lame - an imported target providing lame +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. -find_package(mp3lame CONFIG QUIET) +include(ImportedTargetHelpers) -if(TARGET mp3lame::mp3lame) - # Extract details for find_package_handle_standard_args - get_target_property(Lame_LIBRARIES mp3lame::mp3lame LOCATION) - get_target_property(Lame_INCLUDE_DIRS mp3lame::mp3lame INTERFACE_INCLUDE_DIRECTORIES) -else() - find_path(Lame_INCLUDE_DIRS lame/lame.h) - find_library(Lame_LIBRARIES mp3lame) +find_package_config_mode_with_fallback(mp3lame mp3lame::mp3lame + LIBRARY_NAMES "mp3lame" + INCLUDE_NAMES "lame/lame.h" + PREFIX Lame +) - list(APPEND Lame_DEFINITIONS HAVE_LIBMP3LAME=1) +determine_version_from_source(Lame_VERSION mp3lame::mp3lame [[ + #include + #include - mark_as_advanced(Lame_INCLUDE_DIRS Lame_LIBRARIES Lame_DEFINITIONS) - - if(Lame_LIBRARIES AND Lame_INCLUDE_DIRS) - add_library(mp3lame::mp3lame UNKNOWN IMPORTED) - - set_target_properties(mp3lame::mp3lame PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${Lame_INCLUDE_DIRS}" - INTERFACE_COMPILE_DEFINITIONS "${Lame_DEFINITIONS}" - IMPORTED_LOCATION "${Lame_LIBRARIES}" - ) - endif() -endif() + auto main() -> int + { + auto version = lame_version_t{}; + get_lame_version_numerical(&version); + std::cout << version.major << "." << version.minor; + } +]]) include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Lame - REQUIRED_VARS Lame_LIBRARIES Lame_INCLUDE_DIRS + REQUIRED_VARS Lame_LIBRARY Lame_INCLUDE_DIRS + VERSION_VAR Lame_VERSION ) diff --git a/cmake/modules/FindOggVorbis.cmake b/cmake/modules/FindOggVorbis.cmake index 79a9ab406..cfbd73256 100644 --- a/cmake/modules/FindOggVorbis.cmake +++ b/cmake/modules/FindOggVorbis.cmake @@ -1,86 +1,68 @@ -# - Try to find the OggVorbis libraries -# Once done this will define +# Copyright (c) 2023 Dominic Clark # -# OGGVORBIS_FOUND - system has OggVorbis -# OGGVORBIS_VERSION - set either to 1 or 2 -# OGGVORBIS_INCLUDE_DIR - the OggVorbis include directory -# OGGVORBIS_LIBRARIES - The libraries needed to use OggVorbis -# OGG_LIBRARY - The Ogg library -# VORBIS_LIBRARY - The Vorbis library -# VORBISFILE_LIBRARY - The VorbisFile library -# VORBISENC_LIBRARY - The VorbisEnc library - -# Copyright (c) 2006, Richard Laerkaeng, -# -# Redistribution and use is allowed according to the terms of the BSD license. +# Redistribution and use is allowed according to the terms of the New BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. +include(ImportedTargetHelpers) -include (CheckLibraryExists) +find_package_config_mode_with_fallback(Ogg Ogg::ogg + LIBRARY_NAMES "ogg" + INCLUDE_NAMES "ogg/ogg.h" + PKG_CONFIG ogg +) -find_path(VORBIS_INCLUDE_DIR vorbis/vorbisfile.h) -find_path(OGG_INCLUDE_DIR ogg/ogg.h) +find_package_config_mode_with_fallback(Vorbis Vorbis::vorbis + LIBRARY_NAMES "vorbis" + INCLUDE_NAMES "vorbis/codec.h" + PKG_CONFIG vorbis + DEPENDS Ogg::ogg +) -find_library(OGG_LIBRARY NAMES ogg) -find_library(VORBIS_LIBRARY NAMES vorbis) -find_library(VORBISFILE_LIBRARY NAMES vorbisfile) -find_library(VORBISENC_LIBRARY NAMES vorbisenc) +find_package_config_mode_with_fallback(Vorbis Vorbis::vorbisfile + LIBRARY_NAMES "vorbisfile" + INCLUDE_NAMES "vorbis/vorbisfile.h" + PKG_CONFIG vorbisfile + DEPENDS Vorbis::vorbis + PREFIX VorbisFile +) +find_package_config_mode_with_fallback(Vorbis Vorbis::vorbisenc + LIBRARY_NAMES "vorbisenc" + INCLUDE_NAMES "vorbis/vorbisenc.h" + PKG_CONFIG vorbisenc + DEPENDS Vorbis::vorbis + PREFIX VorbisEnc +) -if (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) - set(OGGVORBIS_FOUND TRUE) +determine_version_from_source(Vorbis_VERSION Vorbis::vorbis [[ + #include + #include + #include - set(OGGVORBIS_LIBRARIES ${OGG_LIBRARY} ${VORBIS_LIBRARY} ${VORBISFILE_LIBRARY} ${VORBISENC_LIBRARY}) + auto main() -> int + { + // Version string has the format "org name version" + const auto version = std::string_view{vorbis_version_string()}; + const auto nameBegin = version.find(' ') + 1; + const auto versionBegin = version.find(' ', nameBegin) + 1; + std::cout << version.substr(versionBegin); + } +]]) - set(_CMAKE_REQUIRED_LIBRARIES_TMP ${CMAKE_REQUIRED_LIBRARIES}) - set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} ${OGGVORBIS_LIBRARIES}) - check_library_exists(vorbis vorbis_bitrate_addblock "" HAVE_LIBVORBISENC2) - set(CMAKE_REQUIRED_LIBRARIES ${_CMAKE_REQUIRED_LIBRARIES_TMP}) - - if (HAVE_LIBVORBISENC2) - set (OGGVORBIS_VERSION 2) - else (HAVE_LIBVORBISENC2) - set (OGGVORBIS_VERSION 1) - endif (HAVE_LIBVORBISENC2) - -else (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) - set (OGGVORBIS_VERSION) - set(OGGVORBIS_FOUND FALSE) -endif (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) - - -if (OGGVORBIS_FOUND) - if (NOT OggVorbis_FIND_QUIETLY) - message(STATUS "Found OggVorbis: ${OGGVORBIS_LIBRARIES}") - endif (NOT OggVorbis_FIND_QUIETLY) -else (OGGVORBIS_FOUND) - if (OggVorbis_FIND_REQUIRED) - message(FATAL_ERROR "Could NOT find OggVorbis libraries") - endif (OggVorbis_FIND_REQUIRED) - if (NOT OggVorbis_FIND_QUITELY) - message(STATUS "Could NOT find OggVorbis libraries") - endif (NOT OggVorbis_FIND_QUITELY) -endif (OGGVORBIS_FOUND) - -#check_include_files(vorbis/vorbisfile.h HAVE_VORBISFILE_H) -#check_library_exists(ogg ogg_page_version "" HAVE_LIBOGG) -#check_library_exists(vorbis vorbis_info_init "" HAVE_LIBVORBIS) -#check_library_exists(vorbisfile ov_open "" HAVE_LIBVORBISFILE) -#check_library_exists(vorbisenc vorbis_info_clear "" HAVE_LIBVORBISENC) -#check_library_exists(vorbis vorbis_bitrate_addblock "" HAVE_LIBVORBISENC2) - -#if (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) -# message(STATUS "Ogg/Vorbis found") -# set (VORBIS_LIBS "-lvorbis -logg") -# set (VORBISFILE_LIBS "-lvorbisfile") -# set (VORBISENC_LIBS "-lvorbisenc") -# set (OGGVORBIS_FOUND TRUE) -# if (HAVE_LIBVORBISENC2) -# set (HAVE_VORBIS 2) -# else (HAVE_LIBVORBISENC2) -# set (HAVE_VORBIS 1) -# endif (HAVE_LIBVORBISENC2) -#else (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) -# message(STATUS "Ogg/Vorbis not found") -#endif (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OggVorbis + REQUIRED_VARS + Ogg_LIBRARY + Ogg_INCLUDE_DIRS + Vorbis_LIBRARY + Vorbis_INCLUDE_DIRS + VorbisFile_LIBRARY + VorbisFile_INCLUDE_DIRS + VorbisEnc_LIBRARY + VorbisEnc_INCLUDE_DIRS + # This only reports the Vorbis version - Ogg can have a different version, + # so if we ever care about that, it should be split off into a different + # find module. + VERSION_VAR Vorbis_VERSION +) diff --git a/cmake/modules/FindPortaudio.cmake b/cmake/modules/FindPortaudio.cmake index f9c7699f4..e7cfa1383 100644 --- a/cmake/modules/FindPortaudio.cmake +++ b/cmake/modules/FindPortaudio.cmake @@ -1,44 +1,34 @@ -# Copyright (c) 2022 Dominic Clark +# Copyright (c) 2023 Dominic Clark # # Redistribution and use is allowed according to the terms of the New BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. -# Try config mode if possible -find_package(portaudio CONFIG QUIET) +include(ImportedTargetHelpers) -if(TARGET portaudio) - # Extract details for find_package_handle_standard_args - get_target_property(Portaudio_LIBRARY portaudio LOCATION) - get_target_property(Portaudio_INCLUDE_DIR portaudio INTERFACE_INCLUDE_DIRECTORIES) -else() - # Attempt to find PortAudio using PkgConfig, if we have it - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(PORTAUDIO_PKG portaudio-2.0) - endif() +find_package_config_mode_with_fallback(portaudio portaudio + LIBRARY_NAMES "portaudio" + INCLUDE_NAMES "portaudio.h" + PKG_CONFIG portaudio-2.0 + PREFIX Portaudio +) - # Find the library and headers using the results from PkgConfig as a guide - find_library(Portaudio_LIBRARY - NAMES "portaudio" - HINTS ${PORTAUDIO_PKG_LIBRARY_DIRS} - ) +determine_version_from_source(Portaudio_VERSION portaudio [[ + #include + #include "portaudio.h" - find_path(Portaudio_INCLUDE_DIR - NAMES "portaudio.h" - HINTS ${PORTAUDIO_PKG_INCLUDE_DIRS} - ) - - # Create an imported target for PortAudio if we succeeded in finding it. - if(Portaudio_LIBRARY AND Portaudio_INCLUDE_DIR) - add_library(portaudio UNKNOWN IMPORTED) - set_target_properties(portaudio PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${Portaudio_INCLUDE_DIR}" - IMPORTED_LOCATION "${Portaudio_LIBRARY}" - ) - endif() -endif() + auto main() -> int + { + // Version number has the format 0xMMmmpp + const auto version = Pa_GetVersion(); + std::cout << ((version >> 16) & 0xff) + << "." << ((version >> 8) & 0xff) + << "." << ((version >> 0) & 0xff); + } +]]) include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Portaudio - REQUIRED_VARS Portaudio_LIBRARY Portaudio_INCLUDE_DIR + REQUIRED_VARS Portaudio_LIBRARY Portaudio_INCLUDE_DIRS + VERSION_VAR Portaudio_VERSION ) diff --git a/cmake/modules/FindSTK.cmake b/cmake/modules/FindSTK.cmake index 0718f5039..5564d24f8 100644 --- a/cmake/modules/FindSTK.cmake +++ b/cmake/modules/FindSTK.cmake @@ -1,39 +1,27 @@ -# Try config mode first -find_package(unofficial-libstk CONFIG QUIET) +include(ImportedTargetHelpers) -if(TARGET unofficial::libstk::libstk) - # Extract details for find_package_handle_standard_args - get_target_property(STK_LIBRARY unofficial::libstk::libstk LOCATION) - get_target_property(STK_INCLUDE_DIR unofficial::libstk::libstk INTERFACE_INCLUDE_DIRECTORIES) -else() - find_path(STK_INCLUDE_DIR - NAMES stk/Stk.h - PATH /usr/include /usr/local/include "${CMAKE_INSTALL_PREFIX}/include" "${CMAKE_FIND_ROOT_PATH}/include" +# TODO CMake 3.18: Alias this target to something less hideous +find_package_config_mode_with_fallback(unofficial-libstk unofficial::libstk::libstk + LIBRARY_NAMES "stk" + INCLUDE_NAMES "stk/Stk.h" + LIBRARY_HINTS "/usr/lib" "/usr/local/lib" "${CMAKE_INSTALL_PREFIX}/lib" "${CMAKE_FIND_ROOT_PATH}/lib" + INCLUDE_HINTS "/usr/include" "/usr/local/include" "${CMAKE_INSTALL_PREFIX}/include" "${CMAKE_FIND_ROOT_PATH}/include" + PREFIX STK +) + +# Find STK rawwave path +if(STK_INCLUDE_DIRS) + list(GET STK_INCLUDE_DIRS 0 STK_INCLUDE_DIR) + find_path(STK_RAWWAVE_ROOT + NAMES silence.raw sinewave.raw + HINTS "${STK_INCLUDE_DIR}/.." + PATH_SUFFIXES share/stk/rawwaves share/libstk/rawwaves ) - - find_library(STK_LIBRARY - NAMES stk - PATH /usr/lib /usr/local/lib "${CMAKE_INSTALL_PREFIX}/lib" "${CMAKE_FIND_ROOT_PATH}/lib" - ) - - if(STK_INCLUDE_DIR AND STK_LIBRARY) - # Yes, this target name is hideous, but it matches that provided by vcpkg - add_library(unofficial::libstk::libstk UNKNOWN IMPORTED) - set_target_properties(unofficial::libstk::libstk PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${STK_INCLUDE_DIR}" - IMPORTED_LOCATION "${STK_LIBRARY}" - ) - endif() endif() -# find STK rawwave path -find_path(STK_RAWWAVE_ROOT - NAMES silence.raw sinewave.raw - HINTS "${STK_INCLUDE_DIR}/.." - PATH_SUFFIXES share/stk/rawwaves share/libstk/rawwaves -) - include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(STK REQUIRED_VARS STK_LIBRARY STK_INCLUDE_DIR + # STK doesn't appear to expose its version, so we can't pass it here ) diff --git a/cmake/modules/FindSamplerate.cmake b/cmake/modules/FindSamplerate.cmake index 53b69f6c7..683748c59 100644 --- a/cmake/modules/FindSamplerate.cmake +++ b/cmake/modules/FindSamplerate.cmake @@ -1,34 +1,35 @@ -# FindFFTW.cmake - Try to find FFTW3 -# Copyright (c) 2018 Lukas W -# This file is MIT licensed. -# See http://opensource.org/licenses/MIT +# Copyright (c) 2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. -find_package(PkgConfig QUIET) -if(PKG_CONFIG_FOUND) - pkg_check_modules(SAMPLERATE_PKG samplerate) -endif() +include(ImportedTargetHelpers) -find_path(SAMPLERATE_INCLUDE_DIR - NAMES samplerate.h - PATHS ${SAMPLERATE_PKG_INCLUDE_DIRS} +find_package_config_mode_with_fallback(SampleRate SampleRate::samplerate + LIBRARY_NAMES "samplerate" "libsamplerate" "libsamplerate-0" + INCLUDE_NAMES "samplerate.h" + PKG_CONFIG samplerate + PREFIX Samplerate ) -set(SAMPLERATE_NAMES samplerate libsamplerate) -if(Samplerate_FIND_VERSION_MAJOR) - list(APPEND SAMPLERATE_NAMES libsamplerate-${Samplerate_FIND_VERSION_MAJOR}) -else() - list(APPEND SAMPLERATE_NAMES libsamplerate-0) -endif() +determine_version_from_source(Samplerate_VERSION SampleRate::samplerate [[ + #include + #include + #include -find_library(SAMPLERATE_LIBRARY - NAMES ${SAMPLERATE_NAMES} - PATHS ${SAMPLERATE_PKG_LIBRARY_DIRS} -) + auto main() -> int + { + // Version string has the format "name-version copyright" + const auto version = std::string_view{src_get_version()}; + const auto begin = version.find('-') + 1; + const auto end = version.find(' ', begin); + std::cout << version.substr(begin, end - begin); + } +]]) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(SAMPLERATE DEFAULT_MSG SAMPLERATE_LIBRARY SAMPLERATE_INCLUDE_DIR) -mark_as_advanced(SAMPLERATE_INCLUDE_DIR SAMPLERATE_LIBRARY ) - -set(SAMPLERATE_LIBRARIES ${SAMPLERATE_LIBRARY} ) -set(SAMPLERATE_INCLUDE_DIRS ${SAMPLERATE_INCLUDE_DIR}) +find_package_handle_standard_args(Samplerate + REQUIRED_VARS Samplerate_LIBRARY Samplerate_INCLUDE_DIRS + VERSION_VAR Samplerate_VERSION +) diff --git a/cmake/modules/FindSndFile.cmake b/cmake/modules/FindSndFile.cmake index 28ebb7bb7..d69fa6331 100644 --- a/cmake/modules/FindSndFile.cmake +++ b/cmake/modules/FindSndFile.cmake @@ -1,39 +1,34 @@ -# FindSndFile.cmake - Try to find libsndfile -# Copyright (c) 2018 Lukas W -# This file is MIT licensed. -# See http://opensource.org/licenses/MIT +# Copyright (c) 2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. -# Try pkgconfig for hints -find_package(PkgConfig QUIET) -if(PKG_CONFIG_FOUND) - pkg_check_modules(SNDFILE_PKG sndfile) -endif(PKG_CONFIG_FOUND) -set(SndFile_DEFINITIONS ${SNDFILE_PKG_CFLAGS_OTHER}) +include(ImportedTargetHelpers) -if(WIN32) - # Try Vcpkg - find_package(LibSndFile ${SndFile_FIND_VERSION} CONFIG QUIET) - if(LibSndFile_FOUND) - get_target_property(LibSndFile_Location sndfile-shared LOCATION) - get_target_property(LibSndFile_Include_Path sndfile-shared INTERFACE_INCLUDE_DIRECTORIES) - get_filename_component(LibSndFile_Path LibSndFile_Location PATH) - endif() -endif() - -find_path(SNDFILE_INCLUDE_DIR - NAMES sndfile.h - PATHS ${SNDFILE_PKG_INCLUDE_DIRS} ${LibSndFile_Include_Path} +find_package_config_mode_with_fallback(SndFile SndFile::sndfile + LIBRARY_NAMES "sndfile" "libsndfile" "libsndfile-1" + INCLUDE_NAMES "sndfile.h" + PKG_CONFIG sndfile ) -find_library(SNDFILE_LIBRARY - NAMES sndfile libsndfile libsndfile-1 - PATHS ${SNDFILE_PKG_LIBRARY_DIRS} ${LibSndFile_Path} +determine_version_from_source(SndFile_VERSION SndFile::sndfile [[ + #include + #include + #include + + auto main() -> int + { + // Version string has the format "name-version", optionally followed by "-exp" + const auto version = std::string_view{sf_version_string()}; + const auto begin = version.find('-') + 1; + const auto end = version.find('-', begin); + std::cout << version.substr(begin, end - begin); + } +]]) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(SndFile + REQUIRED_VARS SndFile_LIBRARY SndFile_INCLUDE_DIRS + VERSION_VAR SndFile_VERSION ) - -find_package(PackageHandleStandardArgs) -find_package_handle_standard_args(SndFile DEFAULT_MSG SNDFILE_LIBRARY SNDFILE_INCLUDE_DIR) - -set(SNDFILE_LIBRARIES ${SNDFILE_LIBRARY}) -set(SNDFILE_INCLUDE_DIRS ${SNDFILE_INCLUDE_DIR}) - -mark_as_advanced(SNDFILE_LIBRARY SNDFILE_LIBRARIES SNDFILE_INCLUDE_DIR SNDFILE_INCLUDE_DIRS) diff --git a/cmake/modules/ImportedTargetHelpers.cmake b/cmake/modules/ImportedTargetHelpers.cmake index 87b3aeedc..d3d979901 100644 --- a/cmake/modules/ImportedTargetHelpers.cmake +++ b/cmake/modules/ImportedTargetHelpers.cmake @@ -1,7 +1,178 @@ +# ImportedTargetHelpers.cmake - various helper functions for use in find modules. +# +# Copyright (c) 2022-2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# If the version variable is not yet set, build the source linked to the target, +# run it, and set the version variable to the output. Useful for libraries which +# do not expose the version information in a header where it can be extracted +# with regular expressions, but do provide a function to get the version. +# +# Usage: +# determine_version_from_source( +# # The cache variable in which to store the computed version +# # The target which the source will link to +# # The source code to determine the version +# ) +function(determine_version_from_source _version_out _target _source) + # Return if we already know the version, or the target was not found + if(NOT "${${_version_out}}" STREQUAL "" OR NOT TARGET "${_target}") + return() + endif() + + # Return with a notice if cross-compiling, since we are unlikely to be able + # to run the compiled source + if(CMAKE_CROSSCOMPILING) + message( + "${_target} was found but the version could not be determined automatically.\n" + "Set the cache variable `${_version_out}` to the version you have installed." + ) + return() + endif() + + # Write the source code to a temporary file + string(SHA1 _source_hash "${_source}") + set(_source_file "${CMAKE_CURRENT_BINARY_DIR}/${_source_hash}.cpp") + file(WRITE "${_source_file}" "${_source}") + + # Build and run the temporary file to get the version + # TODO CMake 3.25: Use the new signature for try_run which has a NO_CACHE + # option and doesn't require separate file management. + try_run( + _dvfs_run_result _dvfs_compile_result "${CMAKE_CURRENT_BINARY_DIR}" + SOURCES "${_source_file}" + LINK_LIBRARIES "${_target}" + CXX_STANDARD 17 + RUN_OUTPUT_VARIABLE _run_output + COMPILE_OUTPUT_VARIABLE _compile_output + ) + + # Clean up the temporary file + file(REMOVE "${_source_file}") + + # Set the version if the run was successful, using a cache variable since + # this version check may be relatively expensive. Otherwise, log the error + # and inform the user. + if(_dvfs_run_result EQUAL "0") + set("${_version_out}" "${_run_output}" CACHE INTERNAL "Version of ${_target}") + else() + message(DEBUG "${_compile_output}") + message( + "${_target} was found but the version could not be determined automatically.\n" + "Set the cache variable `${_version_out}` to the version you have installed." + ) + endif() +endfunction() + +# Search for a package using config mode. If this fails to find the desired +# target, use the specified fallbacks and add the target if they succeed. Set +# the variables `prefix_LIBRARY`, `prefix_INCLUDE_DIRS`, and `prefix_VERSION` +# if found for the caller to pass to `find_package_handle_standard_args`. +# +# Usage: +# find_package_config_mode_with_fallback( +# # The package to search for with config mode +# # The target to expect from config mode, or define if not found +# LIBRARY_NAMES names... # Possible library names to search for as a fallback +# INCLUDE_NAMES names... # Possible header names to search for as a fallback +# [PKG_CONFIG ] # The pkg-config name to search for as a fallback +# [LIBRARY_HINTS hints...] # Locations to look for libraries +# [INCLUDE_HINTS hints...] # Locations to look for headers +# [DEPENDS dependencies...] # Dependencies of the target - added to INTERFACE_LINK_LIBRARIES, and will fail if not found +# [PREFIX ] # The prefix for result variables - defaults to the package name +# ) +function(find_package_config_mode_with_fallback _fpcmwf_PACKAGE_NAME _fpcmwf_TARGET_NAME) + # Parse remaining arguments + set(_options "") + set(_one_value_args "PKG_CONFIG" "PREFIX") + set(_multi_value_args "LIBRARY_NAMES" "LIBRARY_HINTS" "INCLUDE_NAMES" "INCLUDE_HINTS" "DEPENDS") + cmake_parse_arguments(PARSE_ARGV 2 _fpcmwf "${_options}" "${_one_value_args}" "${_multi_value_args}") + + # Compute result variable names + if(NOT DEFINED _fpcmwf_PREFIX) + set(_fpcmwf_PREFIX "${_fpcmwf_PACKAGE_NAME}") + endif() + set(_version_var "${_fpcmwf_PREFIX}_VERSION") + set(_library_var "${_fpcmwf_PREFIX}_LIBRARY") + set(_include_var "${_fpcmwf_PREFIX}_INCLUDE_DIRS") + + # Try config mode if possible + find_package("${_fpcmwf_PACKAGE_NAME}" CONFIG QUIET) + + if(TARGET "${_fpcmwf_TARGET_NAME}") + # Extract package details from existing target + get_target_property("${_library_var}" "${_fpcmwf_TARGET_NAME}" LOCATION) + get_target_property("${_include_var}" "${_fpcmwf_TARGET_NAME}" INTERFACE_INCLUDE_DIRECTORIES) + if(DEFINED "${_fpcmwf_PACKAGE_NAME}_VERSION") + set("${_version_var}" "${${_fpcmwf_PACKAGE_NAME}_VERSION}") + endif() + else() + # Check whether the dependencies exist + foreach(_dependency IN LISTS _fpcmwf_DEPENDS) + if(NOT TARGET "${_dependency}") + return() + endif() + endforeach() + + # Attempt to find the package using pkg-config, if we have it and it was requested + set(_pkg_config_prefix "${_fpcmwf_PKG_CONFIG}_PKG") + if(DEFINED _fpcmwf_PKG_CONFIG) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules("${_pkg_config_prefix}" QUIET "${_fpcmwf_PKG_CONFIG}") + if("${${_pkg_config_prefix}_FOUND}") + set("${_version_var}" "${${_pkg_config_prefix}_VERSION}") + endif() + endif() + endif() + + # Find the library and headers using the results from pkg-config as a guide + find_library("${_library_var}" + NAMES ${_fpcmwf_LIBRARY_NAMES} + HINTS ${${_pkg_config_prefix}_LIBRARY_DIRS} ${_fpcmwf_LIBRARY_HINTS} + ) + + find_path("${_include_var}" + NAMES ${_fpcmwf_INCLUDE_NAMES} + HINTS ${${_pkg_config_prefix}_INCLUDE_DIRS} ${_fpcmwf_INCLUDE_HINTS} + ) + + # Create an imported target if we succeeded in finding the package + if(${_library_var} AND ${_include_var}) + add_library("${_fpcmwf_TARGET_NAME}" UNKNOWN IMPORTED) + set_target_properties("${_fpcmwf_TARGET_NAME}" PROPERTIES + IMPORTED_LOCATION "${${_library_var}}" + INTERFACE_INCLUDE_DIRECTORIES "${${_include_var}}" + INTERFACE_LINK_LIBRARIES "${_fpcmwf_DEPENDS}" + ) + endif() + + mark_as_advanced("${_library_var}" "${_include_var}") + endif() + + # Return results to caller + if(DEFINED "${_version_var}") + set("${_version_var}" "${${_version_var}}" PARENT_SCOPE) + else() + unset("${_version_var}" PARENT_SCOPE) + endif() + set("${_library_var}" "${${_library_var}}" PARENT_SCOPE) + set("${_include_var}" "${${_include_var}}" PARENT_SCOPE) +endfunction() + # Given a library in vcpkg, find appropriate debug and release versions. If only -# one version exists, it is used as the release version, and the debug version -# is not set. -function(_get_vcpkg_library_configs _release_out _debug_out _library) +# one version exists, use it as the release version, and do not set the debug +# version. +# +# Usage: +# get_vcpkg_library_configs( +# # Variable in which to store the path to the release version of the library +# # Variable in which to store the path to the debug version of the library +# # Known path to some version of the library +# ) +function(get_vcpkg_library_configs _release_out _debug_out _library) # We want to do all operations within the vcpkg directory file(RELATIVE_PATH _lib_relative "${VCPKG_INSTALLED_DIR}" "${_library}") diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9a71be4b8..04862cac1 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -9,10 +9,7 @@ IF(LMMS_BUILD_APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") ENDIF() -INCLUDE_DIRECTORIES( - ${SAMPLERATE_INCLUDE_DIRS} - "${CMAKE_BINARY_DIR}/src" -) +include_directories("${CMAKE_BINARY_DIR}/src") # See cmake/modules/PluginList.cmake FOREACH(PLUGIN ${PLUGIN_LIST}) diff --git a/plugins/GigPlayer/CMakeLists.txt b/plugins/GigPlayer/CMakeLists.txt index 24db813bd..7b634b605 100644 --- a/plugins/GigPlayer/CMakeLists.txt +++ b/plugins/GigPlayer/CMakeLists.txt @@ -12,8 +12,13 @@ if(LMMS_HAVE_GIG) add_definitions(${GCC_GIG_COMPILE_FLAGS}) endif(LMMS_BUILD_WIN32) - LINK_DIRECTORIES(${GIG_LIBRARY_DIRS} ${SAMPLERATE_LIBRARY_DIRS}) - LINK_LIBRARIES(${GIG_LIBRARIES} ${SAMPLERATE_LIBRARIES}) - BUILD_PLUGIN(gigplayer GigPlayer.cpp GigPlayer.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES GigPlayer.h PatchesDialog.h UICFILES PatchesDialog.ui EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") + link_directories(${GIG_LIBRARY_DIRS}) + link_libraries(${GIG_LIBRARIES}) + build_plugin(gigplayer + GigPlayer.cpp GigPlayer.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui + MOCFILES GigPlayer.h PatchesDialog.h + UICFILES PatchesDialog.ui + EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" + ) + target_link_libraries(gigplayer SampleRate::samplerate) endif(LMMS_HAVE_GIG) - diff --git a/plugins/Sf2Player/CMakeLists.txt b/plugins/Sf2Player/CMakeLists.txt index 4679a94bd..1d004a6c5 100644 --- a/plugins/Sf2Player/CMakeLists.txt +++ b/plugins/Sf2Player/CMakeLists.txt @@ -1,13 +1,10 @@ if(LMMS_HAVE_FLUIDSYNTH) include(BuildPlugin) - include_directories(${SAMPLERATE_INCLUDE_DIRS}) - link_directories(${SAMPLERATE_LIBRARY_DIRS}) - link_libraries(${SAMPLERATE_LIBRARIES}) build_plugin(sf2player Sf2Player.cpp Sf2Player.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES Sf2Player.h PatchesDialog.h UICFILES PatchesDialog.ui EMBEDDED_RESOURCES *.png ) - target_link_libraries(sf2player fluidsynth) + target_link_libraries(sf2player fluidsynth SampleRate::samplerate) endif() diff --git a/plugins/Watsyn/CMakeLists.txt b/plugins/Watsyn/CMakeLists.txt index 5aec12a46..b43abbb07 100644 --- a/plugins/Watsyn/CMakeLists.txt +++ b/plugins/Watsyn/CMakeLists.txt @@ -1,5 +1,8 @@ -INCLUDE(BuildPlugin) +include(BuildPlugin) -LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) -LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) -BUILD_PLUGIN(watsyn Watsyn.cpp Watsyn.h MOCFILES Watsyn.h EMBEDDED_RESOURCES *.png) +build_plugin(watsyn + Watsyn.cpp Watsyn.h + MOCFILES Watsyn.h + EMBEDDED_RESOURCES *.png +) +target_link_libraries(watsyn SampleRate::samplerate) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f483d8b41..c074ea2ef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,8 +58,6 @@ FILE(RELATIVE_PATH PLUGIN_DIR_RELATIVE "/${BIN_DIR}" "/${PLUGIN_DIR}") ADD_DEFINITIONS(-DLIB_DIR="${LIB_DIR_RELATIVE}" -DPLUGIN_DIR="${PLUGIN_DIR_RELATIVE}" ${PULSEAUDIO_DEFINITIONS}) INCLUDE_DIRECTORIES( ${JACK_INCLUDE_DIRS} - ${SAMPLERATE_INCLUDE_DIRS} - ${SNDFILE_INCLUDE_DIRS} ${SNDIO_INCLUDE_DIRS} ${FFTW3F_INCLUDE_DIRS} ) @@ -79,10 +77,6 @@ IF(NOT ("${PULSEAUDIO_INCLUDE_DIR}" STREQUAL "")) INCLUDE_DIRECTORIES("${PULSEAUDIO_INCLUDE_DIR}") ENDIF() -IF(NOT ("${OGGVORBIS_INCLUDE_DIR}" STREQUAL "")) - INCLUDE_DIRECTORIES("${OGGVORBIS_INCLUDE_DIR}") -ENDIF() - IF(NOT ("${LV2_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES(${LV2_INCLUDE_DIRS}) ENDIF() @@ -170,6 +164,10 @@ if(LMMS_HAVE_MP3LAME) list(APPEND EXTRA_LIBRARIES mp3lame::mp3lame) endif() +if(LMMS_HAVE_OGGVORBIS) + list(APPEND EXTRA_LIBRARIES Vorbis::vorbisenc Vorbis::vorbisfile) +endif() + if(LMMS_USE_MINGW_STD_THREADS) list(APPEND EXTRA_LIBRARIES mingw_stdthreads) endif() @@ -184,15 +182,14 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${SNDIO_LIBRARIES} ${PULSEAUDIO_LIBRARIES} ${JACK_LIBRARIES} - ${OGGVORBIS_LIBRARIES} ${LV2_LIBRARIES} ${SUIL_LIBRARIES} ${LILV_LIBRARIES} - ${SAMPLERATE_LIBRARIES} - ${SNDFILE_LIBRARIES} ${FFTW3F_LIBRARIES} - ${EXTRA_LIBRARIES} rpmalloc + SampleRate::samplerate + SndFile::sndfile + ${EXTRA_LIBRARIES} ) # Expose required libs for tests binary @@ -211,10 +208,11 @@ FOREACH(LIB ${LMMS_REQUIRED_LIBS}) ENDIF() ENDFOREACH() +set_target_properties(lmms PROPERTIES + ENABLE_EXPORTS ON +) + IF(LMMS_BUILD_WIN32) - SET_TARGET_PROPERTIES(lmms PROPERTIES - ENABLE_EXPORTS ON - ) IF(NOT MSVC) SET_PROPERTY(TARGET lmms APPEND_STRING PROPERTY LINK_FLAGS " -mwindows" @@ -228,10 +226,6 @@ IF(LMMS_BUILD_WIN32) ) ENDIF() ELSE() - IF(NOT LMMS_BUILD_APPLE) - SET_TARGET_PROPERTIES(lmms PROPERTIES LINK_FLAGS "${LINK_FLAGS} -Wl,-E") - ENDIF(NOT LMMS_BUILD_APPLE) - if(CMAKE_INSTALL_MANDIR) SET(INSTALL_MANDIR ${CMAKE_INSTALL_MANDIR}) ELSE(CMAKE_INSTALL_MANDIR) From fccbe5d517539f1c45bb54c04646569d0d3c00ec Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:37:56 -0700 Subject: [PATCH 022/191] Add MP3 import (#6750) --- CMakeLists.txt | 9 ++++++++- plugins/AudioFileProcessor/AudioFileProcessor.cpp | 6 +++++- src/core/DataFile.cpp | 7 +++++-- src/core/SampleBuffer.cpp | 10 ++++++++-- src/lmmsconfig.h.in | 1 + 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f388bf30..ee3ac9e87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,7 +212,14 @@ SET(QT_QTTEST_LIBRARY Qt5::Test) # check for libsndfile FIND_PACKAGE(SndFile REQUIRED) -IF(NOT SNDFILE_FOUND) +IF(SNDFILE_FOUND) + IF(SndFile_VERSION VERSION_GREATER_EQUAL "1.1.0") + SET(LMMS_HAVE_SNDFILE_MP3 TRUE) + ELSE() + MESSAGE("libsndfile version is < 1.1.0; MP3 import disabled") + SET(LMMS_HAVE_SNDFILE_MP3 FALSE) + ENDIF() +ELSE() MESSAGE(FATAL_ERROR "LMMS requires libsndfile1 and libsndfile1-dev >= 1.0.18 - please install, remove CMakeCache.txt and try again!") ENDIF() # check if we can use SFC_SET_COMPRESSION_LEVEL diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 6e9d00688..864bda5b6 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -68,7 +68,11 @@ Plugin::Descriptor PLUGIN_EXPORT audiofileprocessor_plugin_descriptor = 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader( "logo" ), - "wav,ogg,ds,spx,au,voc,aif,aiff,flac,raw", + "wav,ogg,ds,spx,au,voc,aif,aiff,flac,raw" +#ifdef LMMS_HAVE_SNDFILE_MP3 + ",mp3" +#endif + , nullptr, } ; diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 90e819077..2f7a141a8 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -231,8 +231,11 @@ bool DataFile::validate( QString extension ) { return true; } - if( extension == "wav" || extension == "ogg" || - extension == "ds" ) + if( extension == "wav" || extension == "ogg" || extension == "ds" +#ifdef LMMS_HAVE_SNDFILE_MP3 + || extension == "mp3" +#endif + ) { return true; } diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index f583067f8..2a0076a28 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -1185,14 +1185,20 @@ QString SampleBuffer::openAudioFile() const // set filters QStringList types; - types << tr("All Audio-Files (*.wav *.ogg *.ds *.flac *.spx *.voc " + types << tr("All Audio-Files (*.wav *.ogg " +#ifdef LMMS_HAVE_SNDFILE_MP3 + "*.mp3 " +#endif + "*.ds *.flac *.spx *.voc " "*.aif *.aiff *.au *.raw)") << tr("Wave-Files (*.wav)") << tr("OGG-Files (*.ogg)") +#ifdef LMMS_HAVE_SNDFILE_MP3 + << tr("MP3-Files (*.mp3)") +#endif << tr("DrumSynth-Files (*.ds)") << tr("FLAC-Files (*.flac)") << tr("SPEEX-Files (*.spx)") - //<< tr("MP3-Files (*.mp3)") //<< tr("MIDI-Files (*.mid)") << tr("VOC-Files (*.voc)") << tr("AIFF-Files (*.aif *.aiff)") diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index d130d6fc2..89db21a7b 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -23,6 +23,7 @@ #cmakedefine LMMS_HAVE_LV2 #cmakedefine LMMS_HAVE_SUIL #cmakedefine LMMS_HAVE_MP3LAME +#cmakedefine LMMS_HAVE_SNDFILE_MP3 #cmakedefine LMMS_HAVE_OGGVORBIS #cmakedefine LMMS_HAVE_OSS #cmakedefine LMMS_HAVE_SNDIO From 7839a5760cc4f8777be41f824757ecd7f20620d6 Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Fri, 3 Nov 2023 22:37:19 +0100 Subject: [PATCH 023/191] Use QSaveFile on file write (#6107) Updating to use the recommended method QSaveFile instead of QFile. Hopefully fix issue where LMMS in rare occasions would produce empty files on save. --------- Co-authored-by: Kevin Zander --- src/core/DataFile.cpp | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 2f7a141a8..b87d06931 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include "base64.h" #include "ConfigManager.h" @@ -379,12 +380,12 @@ bool DataFile::writeFile(const QString& filename, bool withResources) } } - QFile outfile (fullNameTemp); + QSaveFile outfile(fullNameTemp); if (!outfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { showError(SongEditor::tr("Could not write file"), - SongEditor::tr("Could not open %1 for writing. You probably are not permitted to" + SongEditor::tr("Could not open %1 for writing. You probably are not permitted to " "write to this file. Please make sure you have write-access to " "the file and try again.").arg(fullName)); @@ -405,30 +406,29 @@ bool DataFile::writeFile(const QString& filename, bool withResources) write( ts ); } - outfile.close(); - - // make sure the file has been written correctly - if( QFileInfo( outfile.fileName() ).size() > 0 ) + if (!outfile.commit()) { - if( ConfigManager::inst()->value( "app", "disablebackup" ).toInt() ) - { - // remove current file - QFile::remove( fullName ); - } - else - { - // remove old backup file - QFile::remove( fullNameBak ); - // move current file to backup file - QFile::rename( fullName, fullNameBak ); - } - // move temporary file to current file - QFile::rename( fullNameTemp, fullName ); - - return true; + showError(SongEditor::tr("Could not write file"), + SongEditor::tr("An unknown error has occured and the file could not be saved.")); + return false; } - return false; + if (ConfigManager::inst()->value("app", "disablebackup").toInt()) + { + // remove current file + QFile::remove(fullName); + } + else + { + // remove old backup file + QFile::remove(fullNameBak); + // move current file to backup file + QFile::rename(fullName, fullNameBak); + } + // move temporary file to current file + QFile::rename(fullNameTemp, fullName); + + return true; } From 620cd3e1043461c1f7b0751d62fdd54dbd931675 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Sat, 4 Nov 2023 17:40:08 +0100 Subject: [PATCH 024/191] CI: Update to macOS 12 Homebrew doesn't provide bottles for macOS 11 anymore, causing dependencies to be compiled in CI which takes approximately 173 years. Therefore, upgrade to macOS 12 and - remove DEVELOPER_DIR environment variable which caused problems but its removal doesn't appear to break anything - upgrade npm because versions before 10.2.2 use Python's distutils package which was removed in Python 12. See - https://github.com/npm/cli/pull/6937 - https://github.com/nodejs/node-gyp/issues/2869 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8cec8867..2a5b66558 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: CCACHE_MAXSIZE: 500M macos: name: macos - runs-on: macos-11 + runs-on: macos-12 env: CMAKE_OPTS: >- -DUSE_WERROR=ON @@ -78,7 +78,6 @@ jobs: CCACHE_MAXSIZE: 0 CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j3 - DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer steps: - name: Check out uses: actions/checkout@v3 @@ -105,6 +104,7 @@ jobs: - name: Install dependencies run: | brew bundle install --verbose + npm update -g npm npm install --location=global appdmg env: HOMEBREW_NO_AUTO_UPDATE: 1 From f6a1f25cf9219a0b6ea9d16b051f3da7f2f6fbc3 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Sun, 5 Nov 2023 12:27:53 +0100 Subject: [PATCH 025/191] CI: Use lowest Xcode version available For compatibility with older macOS versions, see discussion at https://github.com/LMMS/lmms/pull/6960#discussion_r1382432764 --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a5b66558..5fe1ec7bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,6 +78,7 @@ jobs: CCACHE_MAXSIZE: 0 CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j3 + DEVELOPER_DIR: /Applications/Xcode_13.1.app/Contents/Developer steps: - name: Check out uses: actions/checkout@v3 From 6d4343ca948d81d30698cf9442c84fc595bd2bed Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 6 Nov 2023 20:24:37 +0100 Subject: [PATCH 026/191] More prominent search (#6968) Make the search features more prominent by adding an icon with a magnifying glass to the line edits. Because there is no specific icon for "search" the same icon as for "zoom" is used. Make the "Search" text translatable in `PluginBrowser.cpp` and add a search icon. Make the search in the effects selection dialog more similar to the other ones by adding a search icon and a placeholder text and by enabling the clear button. Make some whitespace adjustments in the vicinity of the other adjustments. Use function pointers instead of signals and slots for some connections. Use a lambda for the slot in the file browser code because GCC does not compile if the parameters differ between the connected functions. It seems that it cannot deal with default parameters. It could be considered to create the search line edits in a factory although this would not work for the effects select dialog because its line edit is created in the UI file. In that case we'd have to add an additional method like `embellishLineEditForSearch(QLineEdit*)` or something similar to the factory. Fixes #6966. --- src/gui/FileBrowser.cpp | 11 ++++++----- src/gui/PluginBrowser.cpp | 7 ++++--- src/gui/modals/EffectSelectDialog.cpp | 13 +++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 74d8f755a..1be344c98 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -121,11 +121,12 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, searchWidgetLayout->setContentsMargins(0, 0, 0, 0); searchWidgetLayout->setSpacing( 0 ); - m_filterEdit = new QLineEdit( searchWidget ); - m_filterEdit->setPlaceholderText( tr("Search") ); - m_filterEdit->setClearButtonEnabled( true ); - connect( m_filterEdit, SIGNAL( textEdited( const QString& ) ), - this, SLOT( filterAndExpandItems( const QString& ) ) ); + m_filterEdit = new QLineEdit(searchWidget); + m_filterEdit->setPlaceholderText(tr("Search")); + m_filterEdit->setClearButtonEnabled(true); + m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); + + connect(m_filterEdit, &QLineEdit::textEdited, [this](const QString & filter) { filterAndExpandItems(filter); }); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); diff --git a/src/gui/PluginBrowser.cpp b/src/gui/PluginBrowser.cpp index 7ba8bcc53..0217e3037 100644 --- a/src/gui/PluginBrowser.cpp +++ b/src/gui/PluginBrowser.cpp @@ -68,9 +68,10 @@ PluginBrowser::PluginBrowser( QWidget * _parent ) : hint->setWordWrap( true ); auto searchBar = new QLineEdit(m_view); - searchBar->setPlaceholderText( "Search" ); - searchBar->setMaxLength( 64 ); - searchBar->setClearButtonEnabled( true ); + searchBar->setPlaceholderText(tr("Search")); + searchBar->setMaxLength(64); + searchBar->setClearButtonEnabled(true); + searchBar->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); m_descTree = new QTreeWidget( m_view ); m_descTree->setColumnCount( 1 ); diff --git a/src/gui/modals/EffectSelectDialog.cpp b/src/gui/modals/EffectSelectDialog.cpp index 31ffd7728..993052fab 100644 --- a/src/gui/modals/EffectSelectDialog.cpp +++ b/src/gui/modals/EffectSelectDialog.cpp @@ -98,12 +98,13 @@ EffectSelectDialog::EffectSelectDialog( QWidget * _parent ) : m_model.setSourceModel( &m_sourceModel ); m_model.setFilterCaseSensitivity( Qt::CaseInsensitive ); - connect( ui->filterEdit, SIGNAL( textChanged( const QString& ) ), - &m_model, SLOT( setFilterFixedString( const QString& ) ) ); - connect( ui->filterEdit, SIGNAL( textChanged( const QString& ) ), - this, SLOT(updateSelection())); - connect( ui->filterEdit, SIGNAL( textChanged( const QString& ) ), - SLOT(sortAgain())); + ui->filterEdit->setPlaceholderText(tr("Search")); + ui->filterEdit->setClearButtonEnabled(true); + ui->filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); + + connect(ui->filterEdit, &QLineEdit::textChanged, &m_model, &QSortFilterProxyModel::setFilterFixedString); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::updateSelection); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::sortAgain); ui->pluginList->setModel( &m_model ); From 8c194fc29cd2cf67b56460c9e2f37fafffb2b850 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:53:59 -0800 Subject: [PATCH 027/191] Improve Compressor lookahead algorithm (#6953) --- plugins/Compressor/Compressor.cpp | 123 +++++------------- plugins/Compressor/Compressor.h | 14 +- .../Compressor/CompressorControlDialog.cpp | 1 + 3 files changed, 38 insertions(+), 100 deletions(-) diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp index 0fe139420..e59562b02 100755 --- a/plugins/Compressor/Compressor.cpp +++ b/plugins/Compressor/Compressor.cpp @@ -60,9 +60,6 @@ CompressorEffect::CompressorEffect(Model* parent, const Descriptor::SubPluginFea m_yL[0] = m_yL[1] = COMP_NOISE_FLOOR; - m_maxLookaheadVal[0] = 0; - m_maxLookaheadVal[1] = 0; - // 200 ms m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); @@ -183,9 +180,7 @@ void CompressorEffect::resizeRMS() void CompressorEffect::calcLookaheadLength() { - m_lookaheadLength = qMax(m_compressorControls.m_lookaheadLengthModel.value() * 0.001f * m_sampleRate, 1.f); - - m_preLookaheadLength = ceil(m_lookaheadDelayLength - m_lookaheadLength); + m_lookaheadLength = std::ceil((m_compressorControls.m_lookaheadLengthModel.value() / 1000.f) * m_sampleRate); } void CompressorEffect::calcThreshold() @@ -249,17 +244,10 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) m_gainResult[0] = m_gainResult[1] = 1; m_displayPeak[0] = m_displayPeak[1] = COMP_NOISE_FLOOR; m_displayGain[0] = m_displayGain[1] = COMP_NOISE_FLOOR; - std::fill(std::begin(m_lookaheadBuf[0]), std::end(m_lookaheadBuf[0]), 0); - std::fill(std::begin(m_lookaheadBuf[1]), std::end(m_lookaheadBuf[1]), 0); - m_lookaheadBufLoc[0] = 0; - m_lookaheadBufLoc[1] = 0; - std::fill(std::begin(m_preLookaheadBuf[0]), std::end(m_preLookaheadBuf[0]), 0); - std::fill(std::begin(m_preLookaheadBuf[1]), std::end(m_preLookaheadBuf[1]), 0); - m_preLookaheadBufLoc[0] = 0; - m_preLookaheadBufLoc[1] = 0; - std::fill(std::begin(m_inputBuf[0]), std::end(m_inputBuf[0]), 0); - std::fill(std::begin(m_inputBuf[1]), std::end(m_inputBuf[1]), 0); - m_inputBufLoc = 0; + std::fill(std::begin(m_scLookBuf[0]), std::end(m_scLookBuf[0]), COMP_NOISE_FLOOR); + std::fill(std::begin(m_scLookBuf[1]), std::end(m_scLookBuf[1]), COMP_NOISE_FLOOR); + std::fill(std::begin(m_inLookBuf[0]), std::end(m_inLookBuf[0]), 0); + std::fill(std::begin(m_inLookBuf[1]), std::end(m_inLookBuf[1]), 0); m_cleanedBuffers = true; } return false; @@ -318,7 +306,7 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) for (int i = 0; i < 2; i++) { - float inputValue = feedback ? m_prevOut[i] : s[i]; + float inputValue = (feedback && !lookahead) ? m_prevOut[i] : s[i]; // Calculate the crest factor of the audio by diving the peak by the RMS m_crestPeakVal[i] = qMax(qMax(COMP_NOISE_FLOOR, inputValue * inputValue), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); @@ -330,53 +318,6 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) // Grab the peak or RMS value inputValue = qMax(COMP_NOISE_FLOOR, peakmode ? std::abs(inputValue) : std::sqrt(m_rmsVal[i])); - // The following code uses math magic to semi-efficiently - // find the largest value in the lookahead buffer. - // This can probably be improved. - if (lookahead) - { - // Pre-lookahead delay, so the total delay always matches 20 ms - ++m_preLookaheadBufLoc[i]; - if (m_preLookaheadBufLoc[i] >= m_preLookaheadLength) - { - m_preLookaheadBufLoc[i] = 0; - } - const float tempInputValue = inputValue; - inputValue = m_preLookaheadBuf[i][m_preLookaheadBufLoc[i]]; - m_preLookaheadBuf[i][m_preLookaheadBufLoc[i]] = tempInputValue; - - - // Increment ring buffer location - ++m_lookaheadBufLoc[i]; - if (m_lookaheadBufLoc[i] >= m_lookaheadLength) - { - m_lookaheadBufLoc[i] = 0; - } - - m_lookaheadBuf[i][m_lookaheadBufLoc[i]] = inputValue; - - // If the new input value is larger than the stored maximum, - // store that as the maximum - if (inputValue >= m_maxLookaheadVal[i]) - { - m_maxLookaheadVal[i] = inputValue; - m_maxLookaheadTimer[i] = m_lookaheadLength; - } - - // Decrement timer. When the timer reaches 0, that means the - // stored maximum value has left the buffer and a new - // maximum value must be found. - if (--m_maxLookaheadTimer[i] <= 0) - { - m_maxLookaheadTimer[i] = std::distance(std::begin(m_lookaheadBuf[i]), - std::max_element(std::begin(m_lookaheadBuf[i]), std::begin(m_lookaheadBuf[i]) + m_lookaheadLength)); - m_maxLookaheadVal[i] = m_lookaheadBuf[i][m_maxLookaheadTimer[i]]; - m_maxLookaheadTimer[i] = realmod(m_maxLookaheadTimer[i] - m_lookaheadBufLoc[i], m_lookaheadLength); - } - - inputValue = m_maxLookaheadVal[i]; - } - float t = inputValue; if (t > m_yL[i])// Attack phase @@ -415,11 +356,22 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) // Keep it above the noise floor m_yL[i] = qMax(COMP_NOISE_FLOOR, m_yL[i]); + + float scVal = m_yL[i]; + + if (lookahead) + { + const float temp = scVal; + // Lookahead is calculated by picking the largest value between + // the current sidechain signal and the delayed sidechain signal. + scVal = std::max(m_scLookBuf[i][m_lookWrite], m_scLookBuf[i][(m_lookWrite + m_lookBufLength - m_lookaheadLength) % m_lookBufLength]); + m_scLookBuf[i][m_lookWrite] = temp; + } // For the visualizer - m_displayPeak[i] = qMax(m_yL[i], m_displayPeak[i]); + m_displayPeak[i] = qMax(scVal, m_displayPeak[i]); - const float currentPeakDbfs = ampToDbfs(m_yL[i]); + const float currentPeakDbfs = ampToDbfs(scVal); // Now find the gain change that should be applied, // depending on the measured input value. @@ -439,7 +391,7 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) : m_thresholdVal + (currentPeakDbfs - m_thresholdVal) * m_ratioVal; } - m_gainResult[i] = dbfsToAmp(m_gainResult[i]) / m_yL[i]; + m_gainResult[i] = dbfsToAmp(m_gainResult[i]) / scVal; m_gainResult[i] = qMax(m_rangeVal, m_gainResult[i]); } @@ -507,18 +459,10 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) // Delay the signal by 20 ms via ring buffer if lookahead is enabled if (lookahead) { - ++m_inputBufLoc; - if (m_inputBufLoc >= m_lookaheadDelayLength) - { - m_inputBufLoc = 0; - } - - const auto temp = std::array{drySignal[0], drySignal[1]}; - s[0] = m_inputBuf[0][m_inputBufLoc]; - s[1] = m_inputBuf[1][m_inputBufLoc]; - - m_inputBuf[0][m_inputBufLoc] = temp[0]; - m_inputBuf[1][m_inputBufLoc] = temp[1]; + s[0] = m_inLookBuf[0][m_lookWrite]; + s[1] = m_inLookBuf[1][m_lookWrite]; + m_inLookBuf[0][m_lookWrite] = drySignal[0]; + m_inLookBuf[1][m_lookWrite] = drySignal[1]; } else { @@ -573,6 +517,8 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) buf[f][1] = (1 - m_mixVal) * temp2 + m_mixVal * buf[f][1]; outSum += buf[f][0] * buf[f][0] + buf[f][1] * buf[f][1]; + + if (--m_lookWrite < 0) { m_lookWrite = m_lookBufLength - 1; } lInPeak = drySignal[0] > lInPeak ? drySignal[0] : lInPeak; rInPeak = drySignal[1] > rInPeak ? drySignal[1] : rInPeak; @@ -621,16 +567,13 @@ void CompressorEffect::changeSampleRate() // 200 ms m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); - // 20 ms - m_lookaheadDelayLength = 0.02 * m_sampleRate; - m_inputBuf[0].resize(m_lookaheadDelayLength); - m_inputBuf[1].resize(m_lookaheadDelayLength); - - m_lookaheadBuf[0].resize(m_lookaheadDelayLength); - m_lookaheadBuf[1].resize(m_lookaheadDelayLength); - - m_preLookaheadBuf[0].resize(m_lookaheadDelayLength); - m_preLookaheadBuf[1].resize(m_lookaheadDelayLength); + m_lookBufLength = std::ceil((20.f / 1000.f) * m_sampleRate) + 2; + for (int i = 0; i < 2; ++i) + { + m_inLookBuf[i].resize(m_lookBufLength); + m_scLookBuf[i].resize(m_lookBufLength, COMP_NOISE_FLOOR); + } + m_lookWrite = 0; calcThreshold(); calcKnee(); diff --git a/plugins/Compressor/Compressor.h b/plugins/Compressor/Compressor.h index da6ab52bc..3fc90b752 100755 --- a/plugins/Compressor/Compressor.h +++ b/plugins/Compressor/Compressor.h @@ -81,14 +81,10 @@ private: enum class StereoLinkMode { Unlinked, Maximum, Average, Minimum, Blend }; - std::vector m_preLookaheadBuf[2]; - int m_preLookaheadBufLoc[2] = {0}; - - std::vector m_lookaheadBuf[2]; - int m_lookaheadBufLoc[2] = {0}; - - std::vector m_inputBuf[2]; - int m_inputBufLoc = 0; + std::array, 2> m_inLookBuf; + std::array, 2> m_scLookBuf; + int m_lookWrite; + int m_lookBufLength; float m_attCoeff; float m_relCoeff; @@ -99,8 +95,6 @@ private: int m_holdTimer[2] = {0, 0}; int m_lookaheadLength; - int m_lookaheadDelayLength; - int m_preLookaheadLength; float m_thresholdAmpVal; float m_autoMakeupVal; float m_outGainVal; diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 1516456a2..917054c6b 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -358,6 +358,7 @@ void CompressorControlDialog::lookaheadChanged() { m_lookaheadLengthKnob->setVisible(m_controls->m_lookaheadModel.value()); m_lookaheadEnabledLabel->setVisible(m_controls->m_lookaheadModel.value()); + feedbackButton->setVisible(!m_controls->m_lookaheadModel.value()); } From 3b9e8c5ea1a5b6804102953f6f1253d8108e81e5 Mon Sep 17 00:00:00 2001 From: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Date: Thu, 9 Nov 2023 02:10:32 -0500 Subject: [PATCH 028/191] Fix LV2 instrument instantiation crash (#6977) Fixes LV2 instrument instantiation crash which occurs when right-clicking an LV2 instrument in the plugin browser and then clicking "Send to new instrument track" --- src/gui/PluginBrowser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/PluginBrowser.cpp b/src/gui/PluginBrowser.cpp index 0217e3037..963609c43 100644 --- a/src/gui/PluginBrowser.cpp +++ b/src/gui/PluginBrowser.cpp @@ -282,9 +282,9 @@ void PluginDescWidget::leaveEvent( QEvent * _e ) void PluginDescWidget::mousePressEvent( QMouseEvent * _me ) { + Engine::setDndPluginKey(&m_pluginKey); if ( _me->button() == Qt::LeftButton ) { - Engine::setDndPluginKey(&m_pluginKey); new StringPairDrag("instrument", QString::fromUtf8(m_pluginKey.desc->name), m_logo, this); leaveEvent( _me ); From 89c98a77a5130a9ed31c6d2acdb3fad6590f9c78 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Fri, 10 Nov 2023 07:10:44 -0800 Subject: [PATCH 029/191] LOMM (upward/downward multiband compressor) (#6925) --- cmake/modules/PluginList.cmake | 1 + data/themes/classic/lcd_11green.png | Bin 0 -> 10455 bytes data/themes/classic/lcd_11green_dot.png | Bin 0 -> 5964 bytes data/themes/default/lcd_11green.png | Bin 0 -> 10455 bytes data/themes/default/lcd_11green_dot.png | Bin 0 -> 5964 bytes include/Effect.h | 5 + plugins/LOMM/CMakeLists.txt | 3 + plugins/LOMM/LOMM.cpp | 444 ++++++++++++++++++ plugins/LOMM/LOMM.h | 106 +++++ plugins/LOMM/LOMMControlDialog.cpp | 275 +++++++++++ plugins/LOMM/LOMMControlDialog.h | 129 +++++ plugins/LOMM/LOMMControls.cpp | 277 +++++++++++ plugins/LOMM/LOMMControls.h | 136 ++++++ plugins/LOMM/artwork.png | Bin 0 -> 70032 bytes plugins/LOMM/crossover_led_green.png | Bin 0 -> 290 bytes plugins/LOMM/crossover_led_off.png | Bin 0 -> 6684 bytes plugins/LOMM/depthScaling_active.png | Bin 0 -> 867 bytes plugins/LOMM/depthScaling_inactive.png | Bin 0 -> 886 bytes plugins/LOMM/feedback_active.png | Bin 0 -> 11378 bytes plugins/LOMM/feedback_inactive.png | Bin 0 -> 8904 bytes plugins/LOMM/high_band_active.png | Bin 0 -> 8801 bytes plugins/LOMM/high_band_inactive.png | Bin 0 -> 1818 bytes plugins/LOMM/init_active.png | Bin 0 -> 1204 bytes plugins/LOMM/init_inactive.png | Bin 0 -> 1187 bytes plugins/LOMM/logo.png | Bin 0 -> 774 bytes plugins/LOMM/lookahead_active.png | Bin 0 -> 1091 bytes plugins/LOMM/lookahead_inactive.png | Bin 0 -> 1029 bytes plugins/LOMM/lowSideUpwardSuppress_active.png | Bin 0 -> 972 bytes .../LOMM/lowSideUpwardSuppress_inactive.png | Bin 0 -> 4725 bytes plugins/LOMM/low_band_active.png | Bin 0 -> 8855 bytes plugins/LOMM/low_band_inactive.png | Bin 0 -> 1783 bytes plugins/LOMM/mid_band_active.png | Bin 0 -> 8837 bytes plugins/LOMM/mid_band_inactive.png | Bin 0 -> 1843 bytes plugins/LOMM/midside_active.png | Bin 0 -> 11242 bytes plugins/LOMM/midside_inactive.png | Bin 0 -> 9190 bytes plugins/LOMM/split1Enabled_active.png | Bin 0 -> 11035 bytes plugins/LOMM/split1Enabled_inactive.png | Bin 0 -> 1041 bytes plugins/LOMM/split2Enabled_active.png | Bin 0 -> 11351 bytes plugins/LOMM/split2Enabled_inactive.png | Bin 0 -> 1052 bytes plugins/LOMM/stereoLink_active.png | Bin 0 -> 1042 bytes plugins/LOMM/stereoLink_inactive.png | Bin 0 -> 1050 bytes 41 files changed, 1376 insertions(+) create mode 100644 data/themes/classic/lcd_11green.png create mode 100644 data/themes/classic/lcd_11green_dot.png create mode 100644 data/themes/default/lcd_11green.png create mode 100644 data/themes/default/lcd_11green_dot.png create mode 100644 plugins/LOMM/CMakeLists.txt create mode 100644 plugins/LOMM/LOMM.cpp create mode 100644 plugins/LOMM/LOMM.h create mode 100644 plugins/LOMM/LOMMControlDialog.cpp create mode 100644 plugins/LOMM/LOMMControlDialog.h create mode 100644 plugins/LOMM/LOMMControls.cpp create mode 100644 plugins/LOMM/LOMMControls.h create mode 100644 plugins/LOMM/artwork.png create mode 100644 plugins/LOMM/crossover_led_green.png create mode 100644 plugins/LOMM/crossover_led_off.png create mode 100644 plugins/LOMM/depthScaling_active.png create mode 100644 plugins/LOMM/depthScaling_inactive.png create mode 100644 plugins/LOMM/feedback_active.png create mode 100644 plugins/LOMM/feedback_inactive.png create mode 100644 plugins/LOMM/high_band_active.png create mode 100644 plugins/LOMM/high_band_inactive.png create mode 100644 plugins/LOMM/init_active.png create mode 100644 plugins/LOMM/init_inactive.png create mode 100644 plugins/LOMM/logo.png create mode 100644 plugins/LOMM/lookahead_active.png create mode 100644 plugins/LOMM/lookahead_inactive.png create mode 100644 plugins/LOMM/lowSideUpwardSuppress_active.png create mode 100644 plugins/LOMM/lowSideUpwardSuppress_inactive.png create mode 100644 plugins/LOMM/low_band_active.png create mode 100644 plugins/LOMM/low_band_inactive.png create mode 100644 plugins/LOMM/mid_band_active.png create mode 100644 plugins/LOMM/mid_band_inactive.png create mode 100644 plugins/LOMM/midside_active.png create mode 100644 plugins/LOMM/midside_inactive.png create mode 100644 plugins/LOMM/split1Enabled_active.png create mode 100644 plugins/LOMM/split1Enabled_inactive.png create mode 100644 plugins/LOMM/split2Enabled_active.png create mode 100644 plugins/LOMM/split2Enabled_inactive.png create mode 100644 plugins/LOMM/stereoLink_active.png create mode 100644 plugins/LOMM/stereoLink_inactive.png diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 0a4686fb2..afeca3548 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -41,6 +41,7 @@ SET(LMMS_PLUGIN_LIST HydrogenImport LadspaBrowser LadspaEffect + LOMM Lv2Effect Lv2Instrument Lb302 diff --git a/data/themes/classic/lcd_11green.png b/data/themes/classic/lcd_11green.png new file mode 100644 index 0000000000000000000000000000000000000000..32e923fe887c82a01bd3c2175cc0f7109d5ffc98 GIT binary patch literal 10455 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 OGSODoQ>#_63;RD*ho7?m literal 0 HcmV?d00001 diff --git a/data/themes/classic/lcd_11green_dot.png b/data/themes/classic/lcd_11green_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..9f5a660d33f6b0242163cee1c3eb41821ba4f92a GIT binary patch literal 5964 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 OGSODoQ>#_63;RD*ho7?m literal 0 HcmV?d00001 diff --git a/data/themes/default/lcd_11green_dot.png b/data/themes/default/lcd_11green_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..9f5a660d33f6b0242163cee1c3eb41821ba4f92a GIT binary patch literal 5964 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 + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "LOMM.h" + +#include "embed.h" +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ + Plugin::Descriptor PLUGIN_EXPORT lomm_plugin_descriptor = + { + LMMS_STRINGIFY(PLUGIN_NAME), + "LOMM", + QT_TRANSLATE_NOOP("PluginBrowser", "Upwards/downwards multiband compression plugin powered by the eldritch elder god LOMMUS."), + "Lost Robot ", + 0x0100, + Plugin::Type::Effect, + new PluginPixmapLoader("logo"), + nullptr, + nullptr + }; +} + + +LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&lomm_plugin_descriptor, parent, key), + m_lommControls(this), + m_sampleRate(Engine::audioEngine()->processingSampleRate()), + m_lp1(m_sampleRate), + m_lp2(m_sampleRate), + m_hp1(m_sampleRate), + m_hp2(m_sampleRate), + m_needsUpdate(true), + m_coeffPrecalc(-0.05), + m_crestTimeConst(0.999), + m_lookWrite(0), + m_lookBufLength(2) +{ + autoQuitModel()->setValue(autoQuitModel()->maxValue()); + + m_yL[0][0] = m_yL[0][1] = LOMM_MIN_FLOOR; + m_yL[1][0] = m_yL[1][1] = LOMM_MIN_FLOOR; + m_yL[2][0] = m_yL[2][1] = LOMM_MIN_FLOOR; + + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); + emit changeSampleRate(); +} + +void LOMMEffect::changeSampleRate() +{ + m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_lp1.setSampleRate(m_sampleRate); + m_lp2.setSampleRate(m_sampleRate); + m_hp1.setSampleRate(m_sampleRate); + m_hp2.setSampleRate(m_sampleRate); + + m_coeffPrecalc = -2.2f / (m_sampleRate * 0.001f); + m_needsUpdate = true; + + m_crestTimeConst = exp(-1.f / (0.2f * m_sampleRate)); + + m_lookBufLength = std::ceil((LOMM_MAX_LOOKAHEAD / 1000.f) * m_sampleRate) + 2; + for (int i = 0; i < 2; ++i) + { + for (int j = 0; j < 3; ++j) + { + m_inLookBuf[j][i].resize(m_lookBufLength); + m_scLookBuf[j][i].resize(m_lookBufLength, LOMM_MIN_FLOOR); + } + } +} + +void LOMMEffect::clearFilterHistories() +{ + m_lp1.clearHistory(); + m_lp2.clearHistory(); + m_hp1.clearHistory(); + m_hp2.clearHistory(); +} + + + + +bool LOMMEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) +{ + if (!isEnabled() || !isRunning()) + { + return false; + } + + if (m_needsUpdate || m_lommControls.m_split1Model.isValueChanged()) + { + m_lp1.setLowpass(m_lommControls.m_split1Model.value()); + m_hp1.setHighpass(m_lommControls.m_split1Model.value()); + } + if (m_needsUpdate || m_lommControls.m_split2Model.isValueChanged()) + { + m_lp2.setLowpass(m_lommControls.m_split2Model.value()); + m_hp2.setHighpass(m_lommControls.m_split2Model.value()); + } + m_needsUpdate = false; + + float outSum = 0.f; + const float d = dryLevel(); + const float w = wetLevel(); + + const float depth = m_lommControls.m_depthModel.value(); + const float time = m_lommControls.m_timeModel.value(); + const float inVol = dbfsToAmp(m_lommControls.m_inVolModel.value()); + const float outVol = dbfsToAmp(m_lommControls.m_outVolModel.value()); + const float upward = m_lommControls.m_upwardModel.value(); + const float downward = m_lommControls.m_downwardModel.value(); + const bool split1Enabled = m_lommControls.m_split1EnabledModel.value(); + const bool split2Enabled = m_lommControls.m_split2EnabledModel.value(); + const bool band1Enabled = m_lommControls.m_band1EnabledModel.value(); + const bool band2Enabled = m_lommControls.m_band2EnabledModel.value(); + const bool band3Enabled = m_lommControls.m_band3EnabledModel.value(); + const float inHigh = dbfsToAmp(m_lommControls.m_inHighModel.value()); + const float inMid = dbfsToAmp(m_lommControls.m_inMidModel.value()); + const float inLow = dbfsToAmp(m_lommControls.m_inLowModel.value()); + float inBandVol[3] = {inHigh, inMid, inLow}; + const float outHigh = dbfsToAmp(m_lommControls.m_outHighModel.value()); + const float outMid = dbfsToAmp(m_lommControls.m_outMidModel.value()); + const float outLow = dbfsToAmp(m_lommControls.m_outLowModel.value()); + float outBandVol[3] = {outHigh, outMid, outLow}; + const float aThreshH = m_lommControls.m_aThreshHModel.value(); + const float aThreshM = m_lommControls.m_aThreshMModel.value(); + const float aThreshL = m_lommControls.m_aThreshLModel.value(); + float aThresh[3] = {aThreshH, aThreshM, aThreshL}; + const float aRatioH = m_lommControls.m_aRatioHModel.value(); + const float aRatioM = m_lommControls.m_aRatioMModel.value(); + const float aRatioL = m_lommControls.m_aRatioLModel.value(); + float aRatio[3] = {1.f / aRatioH, 1.f / aRatioM, 1.f / aRatioL}; + const float bThreshH = m_lommControls.m_bThreshHModel.value(); + const float bThreshM = m_lommControls.m_bThreshMModel.value(); + const float bThreshL = m_lommControls.m_bThreshLModel.value(); + float bThresh[3] = {bThreshH, bThreshM, bThreshL}; + const float bRatioH = m_lommControls.m_bRatioHModel.value(); + const float bRatioM = m_lommControls.m_bRatioMModel.value(); + const float bRatioL = m_lommControls.m_bRatioLModel.value(); + float bRatio[3] = {1.f / bRatioH, 1.f / bRatioM, 1.f / bRatioL}; + const float atkH = m_lommControls.m_atkHModel.value() * time; + const float atkM = m_lommControls.m_atkMModel.value() * time; + const float atkL = m_lommControls.m_atkLModel.value() * time; + const float atkCoefH = msToCoeff(atkH); + const float atkCoefM = msToCoeff(atkM); + const float atkCoefL = msToCoeff(atkL); + float atk[3] = {atkH, atkM, atkL}; + float atkCoef[3] = {atkCoefH, atkCoefM, atkCoefL}; + const float relH = m_lommControls.m_relHModel.value() * time; + const float relM = m_lommControls.m_relMModel.value() * time; + const float relL = m_lommControls.m_relLModel.value() * time; + const float relCoefH = msToCoeff(relH); + const float relCoefM = msToCoeff(relM); + const float relCoefL = msToCoeff(relL); + float rel[3] = {relH, relM, relL}; + float relCoef[3] = {relCoefH, relCoefM, relCoefL}; + const float rmsTime = m_lommControls.m_rmsTimeModel.value(); + const float rmsTimeConst = (rmsTime == 0) ? 0 : exp(-1.f / (rmsTime * 0.001f * m_sampleRate)); + const float knee = m_lommControls.m_kneeModel.value() * 0.5f; + const float range = m_lommControls.m_rangeModel.value(); + const float rangeAmp = dbfsToAmp(range); + const float balance = m_lommControls.m_balanceModel.value(); + const float balanceAmpTemp = dbfsToAmp(balance); + const float balanceAmp[2] = {1.f / balanceAmpTemp, balanceAmpTemp}; + const bool depthScaling = m_lommControls.m_depthScalingModel.value(); + const bool stereoLink = m_lommControls.m_stereoLinkModel.value(); + const float autoTime = m_lommControls.m_autoTimeModel.value() * m_lommControls.m_autoTimeModel.value(); + const float mix = m_lommControls.m_mixModel.value(); + const bool midside = m_lommControls.m_midsideModel.value(); + const bool lookaheadEnable = m_lommControls.m_lookaheadEnableModel.value(); + const int lookahead = std::ceil((m_lommControls.m_lookaheadModel.value() / 1000.f) * m_sampleRate); + const bool feedback = m_lommControls.m_feedbackModel.value() && !lookaheadEnable; + const bool lowSideUpwardSuppress = m_lommControls.m_lowSideUpwardSuppressModel.value() && midside; + + for (fpp_t f = 0; f < frames; ++f) + { + std::array s = {buf[f][0], buf[f][1]}; + + // Convert left/right to mid/side. Side channel is intentionally made + // to be 6 dB louder to bring it into volume ranges comparable to the mid channel. + if (midside) + { + float tempS0 = s[0]; + s[0] = (s[0] + s[1]) * 0.5f; + s[1] = tempS0 - s[1]; + } + + std::array, 3> bands = {{}}; + std::array, 3> bandsDry = {{}}; + + for (int i = 0; i < 2; ++i)// Channels + { + // These values are for the Auto time knob. Higher crest factor allows for faster attack/release. + float inSquared = s[i] * s[i]; + m_crestPeakVal[i] = std::max(std::max(LOMM_MIN_FLOOR, inSquared), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inSquared)); + m_crestRmsVal[i] = std::max(LOMM_MIN_FLOOR, m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inSquared))); + m_crestFactorVal[i] = m_crestPeakVal[i] / m_crestRmsVal[i]; + float crestFactorValTemp = ((m_crestFactorVal[i] - LOMM_AUTO_TIME_ADJUST) * autoTime) + LOMM_AUTO_TIME_ADJUST; + + // Crossover filters + bands[0][i] = m_hp1.update(s[i], i); + bands[1][i] = m_hp2.update(m_lp1.update(s[i], i), i); + bands[2][i] = m_lp2.update(s[i], i); + + if (!split1Enabled) + { + bands[1][i] += bands[0][i]; + bands[0][i] = 0; + } + if (!split2Enabled) + { + bands[1][i] += bands[2][i]; + bands[2][i] = 0; + } + + // Mute disabled bands + bands[0][i] *= band1Enabled; + bands[1][i] *= band2Enabled; + bands[2][i] *= band3Enabled; + + std::array detect = {0, 0, 0}; + for (int j = 0; j < 3; ++j)// Bands + { + bandsDry[j][i] = bands[j][i]; + + if (feedback && !lookaheadEnable) + { + bands[j][i] = m_prevOut[j][i]; + } + + bands[j][i] *= inBandVol[j] * inVol * balanceAmp[i]; + + if (rmsTime > 0)// RMS + { + m_rms[j][i] = rmsTimeConst * m_rms[j][i] + ((1 - rmsTimeConst) * (bands[j][i] * bands[j][i])); + detect[j] = std::max(LOMM_MIN_FLOOR, std::sqrt(m_rms[j][i])); + } + else// Peak + { + detect[j] = std::max(LOMM_MIN_FLOOR, std::abs(bands[j][i])); + } + + if (detect[j] > m_yL[j][i])// Attack phase + { + // Calculate attack value depending on crest factor + const float currentAttack = autoTime + ? msToCoeff(LOMM_AUTO_TIME_ADJUST * atk[j] / crestFactorValTemp) + : atkCoef[j]; + + m_yL[j][i] = m_yL[j][i] * currentAttack + (1 - currentAttack) * detect[j]; + } + else// Release phase + { + // Calculate release value depending on crest factor + const float currentRelease = autoTime + ? msToCoeff(LOMM_AUTO_TIME_ADJUST * rel[j] / crestFactorValTemp) + : relCoef[j]; + + m_yL[j][i] = m_yL[j][i] * currentRelease + (1 - currentRelease) * detect[j]; + } + + m_yL[j][i] = std::max(LOMM_MIN_FLOOR, m_yL[j][i]); + + float yAmp = m_yL[j][i]; + if (lookaheadEnable) + { + float temp = yAmp; + // Lookahead is calculated by picking the largest value between + // the current sidechain signal and the delayed sidechain signal. + yAmp = std::max(m_scLookBuf[j][i][m_lookWrite], m_scLookBuf[j][i][(m_lookWrite + m_lookBufLength - lookahead) % m_lookBufLength]); + m_scLookBuf[j][i][m_lookWrite] = temp; + } + + const float yDbfs = ampToDbfs(yAmp); + + float aboveGain = 0; + float belowGain = 0; + + // Downward compression + if (yDbfs - aThresh[j] < -knee)// Below knee + { + aboveGain = yDbfs; + } + else if (yDbfs - aThresh[j] < knee)// Within knee + { + const float temp = yDbfs - aThresh[j] + knee; + aboveGain = yDbfs + (aRatio[j] - 1) * temp * temp / (4 * knee); + } + else// Above knee + { + aboveGain = aThresh[j] + (yDbfs - aThresh[j]) * aRatio[j]; + } + if (aboveGain < yDbfs) + { + if (downward * depth <= 1) + { + aboveGain = linearInterpolate(yDbfs, aboveGain, downward * depth); + } + else + { + aboveGain = linearInterpolate(aboveGain, aThresh[j], downward * depth - 1); + } + } + + // Upward compression + if (yDbfs - bThresh[j] > knee)// Above knee + { + belowGain = yDbfs; + } + else if (bThresh[j] - yDbfs < knee)// Within knee + { + const float temp = bThresh[j] - yDbfs + knee; + belowGain = yDbfs + (1 - bRatio[j]) * temp * temp / (4 * knee); + } + else// Below knee + { + belowGain = bThresh[j] + (yDbfs - bThresh[j]) * bRatio[j]; + } + if (belowGain > yDbfs) + { + if (upward * depth <= 1) + { + belowGain = linearInterpolate(yDbfs, belowGain, upward * depth); + } + else + { + belowGain = linearInterpolate(belowGain, bThresh[j], upward * depth - 1); + } + } + + m_displayIn[j][i] = yDbfs; + m_gainResult[j][i] = (dbfsToAmp(aboveGain) / yAmp) * (dbfsToAmp(belowGain) / yAmp); + if (lowSideUpwardSuppress && m_gainResult[j][i] > 1 && j == 2 && i == 1) //undo upward compression if low side band + { + m_gainResult[j][i] = 1; + } + m_gainResult[j][i] = std::min(m_gainResult[j][i], rangeAmp); + m_displayOut[j][i] = ampToDbfs(std::max(LOMM_MIN_FLOOR, yAmp * m_gainResult[j][i])); + + // Apply the same gain reduction to both channels if stereo link is enabled. + if (stereoLink && i == 1) + { + if (m_gainResult[j][1] < m_gainResult[j][0]) + { + m_gainResult[j][0] = m_gainResult[j][1]; + m_displayOut[j][0] = m_displayIn[j][0] - (m_displayIn[j][1] - m_displayOut[j][1]); + } + else + { + m_gainResult[j][1] = m_gainResult[j][0]; + m_displayOut[j][1] = m_displayIn[j][1] - (m_displayIn[j][0] - m_displayOut[j][0]); + } + } + } + } + + for (int i = 0; i < 2; ++i)// Channels + { + for (int j = 0; j < 3; ++j)// Bands + { + if (lookaheadEnable) + { + float temp = bands[j][i]; + bands[j][i] = m_inLookBuf[j][i][m_lookWrite]; + m_inLookBuf[j][i][m_lookWrite] = temp; + bandsDry[j][i] = bands[j][i]; + } + else if (feedback) + { + bands[j][i] = bandsDry[j][i] * inBandVol[j] * inVol * balanceAmp[i]; + } + + // Apply gain reduction + bands[j][i] *= m_gainResult[j][i]; + + // Store for Feedback + m_prevOut[j][i] = bands[j][i]; + + bands[j][i] *= outBandVol[j]; + + bands[j][i] = linearInterpolate(bandsDry[j][i], bands[j][i], mix); + } + + s[i] = bands[0][i] + bands[1][i] + bands[2][i]; + + s[i] *= linearInterpolate(1.f, outVol, mix * (depthScaling ? depth : 1)); + } + + // Convert mid/side back to left/right. + // Note that the side channel was intentionally made to be 6 dB louder prior to compression. + if (midside) + { + float tempS0 = s[0]; + s[0] = s[0] + (s[1] * 0.5f); + s[1] = tempS0 - (s[1] * 0.5f); + } + + if (--m_lookWrite < 0) { m_lookWrite = m_lookBufLength - 1; } + + buf[f][0] = d * buf[f][0] + w * s[0]; + buf[f][1] = d * buf[f][1] + w * s[1]; + outSum += buf[f][0] + buf[f][1]; + } + + checkGate(outSum / frames); + return isRunning(); +} + +extern "C" +{ + // necessary for getting instance out of shared lib + PLUGIN_EXPORT Plugin * lmms_plugin_main(Model* parent, void* data) + { + return new LOMMEffect(parent, static_cast(data)); + } +} + +} // namespace lmms diff --git a/plugins/LOMM/LOMM.h b/plugins/LOMM/LOMM.h new file mode 100644 index 000000000..039f80b6a --- /dev/null +++ b/plugins/LOMM/LOMM.h @@ -0,0 +1,106 @@ +/* + * LOMM.h + * + * Copyright (c) 2023 Lost Robot + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#ifndef LMMS_LOMM_H +#define LMMS_LOMM_H + +#include "LOMMControls.h" +#include "Effect.h" + +#include "BasicFilters.h" +#include "lmms_math.h" + +namespace lmms +{ + +constexpr inline float LOMM_MIN_FLOOR = 0.00012589;// -72 dBFS +constexpr inline float LOMM_MAX_LOOKAHEAD = 20.f; +constexpr inline float LOMM_AUTO_TIME_ADJUST = 5.f; + +class LOMMEffect : public Effect +{ + Q_OBJECT +public: + LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); + ~LOMMEffect() override = default; + bool processAudioBuffer(sampleFrame* buf, const fpp_t frames) override; + + EffectControls* controls() override + { + return &m_lommControls; + } + + void clearFilterHistories(); + + inline float msToCoeff(float ms) + { + return (ms == 0) ? 0 : exp(m_coeffPrecalc / ms); + } + +private slots: + void changeSampleRate(); + +private: + LOMMControls m_lommControls; + + float m_sampleRate; + + StereoLinkwitzRiley m_lp1; + StereoLinkwitzRiley m_lp2; + + StereoLinkwitzRiley m_hp1; + StereoLinkwitzRiley m_hp2; + + bool m_needsUpdate; + float m_coeffPrecalc; + + std::array, 3> m_yL; + std::array, 3> m_rms; + std::array, 3> m_gainResult; + + std::array, 3> m_displayIn; + std::array, 3> m_displayOut; + + std::array m_crestPeakVal; + std::array m_crestRmsVal; + std::array m_crestFactorVal; + float m_crestTimeConst = 0.0f; + + std::array, 3> m_prevOut; + + std::array, 2>, 3> m_inLookBuf; + std::array, 2>, 3> m_scLookBuf; + + int m_lookWrite = 0; + int m_lookBufLength = 0; + + friend class LOMMControls; + friend class gui::LOMMControlDialog; +}; + + +} // namespace lmms + +#endif // LMMS_LOMM_H diff --git a/plugins/LOMM/LOMMControlDialog.cpp b/plugins/LOMM/LOMMControlDialog.cpp new file mode 100644 index 000000000..e53987a05 --- /dev/null +++ b/plugins/LOMM/LOMMControlDialog.cpp @@ -0,0 +1,275 @@ +/* + * LOMMControlDialog.cpp + * + * Copyright (c) 2023 Lost Robot + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#include "LOMM.h" +#include "LOMMControlDialog.h" +#include "LOMMControls.h" + + +namespace lmms::gui +{ + +LOMMControlDialog::LOMMControlDialog(LOMMControls* controls) : + EffectControlDialog(controls), + m_controls(controls) +{ + setAutoFillBackground(true); + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(400, 256); + + createKnob(KnobType::Bright26, this, 10, 4, &controls->m_depthModel, tr("Depth:"), "", tr("Compression amount for all bands")); + createKnob(KnobType::Bright26, this, 10, 41, &controls->m_timeModel, tr("Time:"), "", tr("Attack/release scaling for all bands")); + createKnob(KnobType::Bright26, this, 10, 220, &controls->m_inVolModel, tr("Input Volume:"), " dB", tr("Input volume")); + createKnob(KnobType::Bright26, this, 363, 220, &controls->m_outVolModel, tr("Output Volume:"), " dB", tr("Output volume")); + createKnob(KnobType::Bright26, this, 10, 179, &controls->m_upwardModel, tr("Upward Depth:"), "", tr("Upward compression amount for all bands")); + createKnob(KnobType::Bright26, this, 363, 179, &controls->m_downwardModel, tr("Downward Depth:"), "", tr("Downward compression amount for all bands")); + + createLcdFloatSpinBox(5, 2, "11green", tr("High/Mid Crossover"), this, 352, 76, &controls->m_split1Model, tr("High/Mid Crossover")); + createLcdFloatSpinBox(5, 2, "11green", tr("Mid/Low Crossover"), this, 352, 156, &controls->m_split2Model, tr("Mid/Low Crossover")); + + createPixmapButton(tr("High/mid band split"), this, 369, 104, &controls->m_split1EnabledModel, "crossover_led_green", "crossover_led_off", tr("High/mid band split")); + createPixmapButton(tr("Mid/low band split"), this, 369, 126, &controls->m_split2EnabledModel, "crossover_led_green", "crossover_led_off", tr("Mid/low band split")); + + createPixmapButton(tr("Enable High Band"), this, 143, 66, &controls->m_band1EnabledModel, "high_band_active", "high_band_inactive", tr("Enable High Band")); + createPixmapButton(tr("Enable Mid Band"), this, 143, 146, &controls->m_band2EnabledModel, "mid_band_active", "mid_band_inactive", tr("Enable Mid Band")); + createPixmapButton(tr("Enable Low Band"), this, 143, 226, &controls->m_band3EnabledModel, "low_band_active", "low_band_inactive", tr("Enable Low Band")); + + createKnob(KnobType::Bright26, this, 53, 43, &controls->m_inHighModel, tr("High Input Volume:"), " dB", tr("Input volume for high band")); + createKnob(KnobType::Bright26, this, 53, 123, &controls->m_inMidModel, tr("Mid Input Volume:"), " dB", tr("Input volume for mid band")); + createKnob(KnobType::Bright26, this, 53, 203, &controls->m_inLowModel, tr("Low Input Volume:"), " dB", tr("Input volume for low band")); + createKnob(KnobType::Bright26, this, 320, 43, &controls->m_outHighModel, tr("High Output Volume:"), " dB", tr("Output volume for high band")); + createKnob(KnobType::Bright26, this, 320, 123, &controls->m_outMidModel, tr("Mid Output Volume:"), " dB", tr("Output volume for mid band")); + createKnob(KnobType::Bright26, this, 320, 203, &controls->m_outLowModel, tr("Low Output Volume:"), " dB", tr("Output volume for low band")); + + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold High"), this, 300, 13, &controls->m_aThreshHModel, tr("Downward compression threshold for high band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold Mid"), this, 300, 93, &controls->m_aThreshMModel, tr("Downward compression threshold for mid band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Above Threshold Low"), this, 300, 173, &controls->m_aThreshLModel, tr("Downward compression threshold for low band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio High"), this, 284, 44, &controls->m_aRatioHModel, tr("Downward compression ratio for high band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio Mid"), this, 284, 124, &controls->m_aRatioMModel, tr("Downward compression ratio for mid band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Above Ratio Low"), this, 284, 204, &controls->m_aRatioLModel, tr("Downward compression ratio for low band")); + + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold High"), this, 59, 13, &controls->m_bThreshHModel, tr("Upward compression threshold for high band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold Mid"), this, 59, 93, &controls->m_bThreshMModel, tr("Upward compression threshold for mid band")); + createLcdFloatSpinBox(3, 3, "11green", tr("Below Threshold Low"), this, 59, 173, &controls->m_bThreshLModel, tr("Upward compression threshold for low band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio High"), this, 87, 44, &controls->m_bRatioHModel, tr("Upward compression ratio for high band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio Mid"), this, 87, 124, &controls->m_bRatioMModel, tr("Upward compression ratio for mid band")); + createLcdFloatSpinBox(2, 2, "11green", tr("Below Ratio Low"), this, 87, 204, &controls->m_bRatioLModel, tr("Upward compression ratio for low band")); + + createKnob(KnobType::Small17, this, 120, 61, &controls->m_atkHModel, tr("Attack High:"), " ms", tr("Attack time for high band")); + createKnob(KnobType::Small17, this, 120, 141, &controls->m_atkMModel, tr("Attack Mid:"), " ms", tr("Attack time for mid band")); + createKnob(KnobType::Small17, this, 120, 221, &controls->m_atkLModel, tr("Attack Low:"), " ms", tr("Attack time for low band")); + createKnob(KnobType::Small17, this, 261, 61, &controls->m_relHModel, tr("Release High:"), " ms", tr("Release time for high band")); + createKnob(KnobType::Small17, this, 261, 141, &controls->m_relMModel, tr("Release Mid:"), " ms", tr("Release time for mid band")); + createKnob(KnobType::Small17, this, 261, 221, &controls->m_relLModel, tr("Release Low:"), " ms", tr("Release time for low band")); + + createKnob(KnobType::Small17, this, 380, 42, &controls->m_rmsTimeModel, tr("RMS Time:"), " ms", tr("RMS size for sidechain signal (set to 0 for Peak mode)")); + createKnob(KnobType::Small17, this, 356, 42, &controls->m_kneeModel, tr("Knee:"), " dB", tr("Knee size for all compressors")); + createKnob(KnobType::Small17, this, 24, 146, &controls->m_rangeModel, tr("Range:"), " dB", tr("Maximum gain increase for all bands")); + createKnob(KnobType::Small17, this, 13, 114, &controls->m_balanceModel, tr("Balance:"), " dB", tr("Bias input volume towards one channel")); + + createPixmapButton(tr("Scale output volume with Depth"), this, 51, 0, &controls->m_depthScalingModel, "depthScaling_active", "depthScaling_inactive", + tr("Scale output volume with Depth parameter")); + createPixmapButton(tr("Stereo Link"), this, 52, 237, &controls->m_stereoLinkModel, "stereoLink_active", "stereoLink_inactive", + tr("Apply same gain change to both channels")); + + createKnob(KnobType::Small17, this, 24, 80, &controls->m_autoTimeModel, tr("Auto Time:"), "", tr("Speed up attack and release times when transients occur")); + createKnob(KnobType::Bright26, this, 363, 4, &controls->m_mixModel, tr("Mix:"), "", tr("Wet/Dry of all bands")); + + m_feedbackButton = createPixmapButton(tr("Feedback"), this, 317, 238, &controls->m_feedbackModel, "feedback_active", "feedback_inactive", + tr("Use output as sidechain signal instead of input")); + createPixmapButton(tr("Mid/Side"), this, 285, 238, &controls->m_midsideModel, "midside_active", "midside_inactive", tr("Compress mid/side channels instead of left/right")); + m_lowSideUpwardSuppressButton = createPixmapButton(tr("Suppress upward compression for side band"), this, 106, 180, &controls->m_lowSideUpwardSuppressModel, + "lowSideUpwardSuppress_active", "lowSideUpwardSuppress_inactive", tr("Suppress upward compression for side band")); + createPixmapButton(tr("Lookahead"), this, 147, 0, &controls->m_lookaheadEnableModel, "lookahead_active", "lookahead_inactive", + tr(("Enable lookahead with fixed " + std::to_string(int(LOMM_MAX_LOOKAHEAD)) + " ms latency").c_str())); + createLcdFloatSpinBox(2, 2, "11green", tr("Lookahead"), this, 214, 2, &controls->m_lookaheadModel, tr("Lookahead length")); + + PixmapButton* initButton = createPixmapButton(tr("Clear all parameters"), this, 84, 237, nullptr, "init_active", "init_inactive", tr("Clear all parameters")); + + connect(initButton, SIGNAL(clicked()), m_controls, SLOT(resetAllParameters())); + connect(&controls->m_lookaheadEnableModel, SIGNAL(dataChanged()), this, SLOT(updateFeedbackVisibility())); + connect(&controls->m_midsideModel, SIGNAL(dataChanged()), this, SLOT(updateLowSideUpwardSuppressVisibility())); + connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(updateDisplay())); + + emit updateFeedbackVisibility(); + emit updateLowSideUpwardSuppressVisibility(); +} + +void LOMMControlDialog::updateFeedbackVisibility() +{ + m_feedbackButton->setVisible(!m_controls->m_lookaheadEnableModel.value()); +} + +void LOMMControlDialog::updateLowSideUpwardSuppressVisibility() +{ + m_lowSideUpwardSuppressButton->setVisible(m_controls->m_midsideModel.value()); +} + +void LOMMControlDialog::updateDisplay() +{ + update(); +} + +void LOMMControlDialog::paintEvent(QPaintEvent *event) +{ + if (!isVisible()) { return; } + + QPainter p; + p.begin(this); + + // Draw threshold lines + QColor aColor(255, 255, 0, 31); + QColor bColor(255, 0, 0, 31); + QPen aPen(QColor(255, 255, 0, 255), 1); + QPen bPen(QColor(255, 0, 0, 255), 1); + int thresholdsX[] = {dbfsToX(m_controls->m_aThreshHModel.value()), + dbfsToX(m_controls->m_aThreshMModel.value()), + dbfsToX(m_controls->m_aThreshLModel.value()), + dbfsToX(m_controls->m_bThreshHModel.value()), + dbfsToX(m_controls->m_bThreshMModel.value()), + dbfsToX(m_controls->m_bThreshLModel.value())}; + for (int i = 0; i < 3; ++i) { + p.setPen(aPen); + p.fillRect(thresholdsX[i], LOMM_DISPLAY_Y[2 * i], LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH - thresholdsX[i], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - LOMM_DISPLAY_Y[2 * i], aColor); + p.drawLine(thresholdsX[i], LOMM_DISPLAY_Y[2 * i], thresholdsX[i], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + + p.setPen(bPen); + p.fillRect(LOMM_DISPLAY_X, LOMM_DISPLAY_Y[2 * i], thresholdsX[i + 3] - LOMM_DISPLAY_X, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - LOMM_DISPLAY_Y[2 * i], bColor); + p.drawLine(thresholdsX[i + 3], LOMM_DISPLAY_Y[2 * i], thresholdsX[i + 3], LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + } + + QPen inputPen(QColor(200, 200, 200, 80), 1); + QPen outputPen(QColor(255, 255, 255, 255), 1); + for (int i = 0; i < 3; ++i) { + // Draw input lines + p.setPen(inputPen); + int inL = dbfsToX(m_controls->m_effect->m_displayIn[i][0]); + p.drawLine(inL, LOMM_DISPLAY_Y[2 * i] + 4, inL, LOMM_DISPLAY_Y[2 * i] + LOMM_DISPLAY_HEIGHT); + int inR = dbfsToX(m_controls->m_effect->m_displayIn[i][1]); + p.drawLine(inR, LOMM_DISPLAY_Y[2 * i + 1], inR, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT - 4); + + // Draw output lines + p.setPen(outputPen); + int outL = dbfsToX(m_controls->m_effect->m_displayOut[i][0]); + p.drawLine(outL, LOMM_DISPLAY_Y[2 * i], outL, LOMM_DISPLAY_Y[2 * i] + LOMM_DISPLAY_HEIGHT); + int outR = dbfsToX(m_controls->m_effect->m_displayOut[i][1]); + p.drawLine(outR, LOMM_DISPLAY_Y[2 * i + 1], outR, LOMM_DISPLAY_Y[2 * i + 1] + LOMM_DISPLAY_HEIGHT); + } + + p.end(); +} + +int LOMMControlDialog::dbfsToX(float dbfs) +{ + float returnX = (dbfs - LOMM_DISPLAY_MIN) / (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN); + returnX = qBound(LOMM_DISPLAY_X, LOMM_DISPLAY_X + returnX * LOMM_DISPLAY_WIDTH, LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH); + return returnX; +} + +float LOMMControlDialog::xToDbfs(int x) +{ + float xNorm = static_cast(x - LOMM_DISPLAY_X) / LOMM_DISPLAY_WIDTH; + float dbfs = xNorm * (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN) + LOMM_DISPLAY_MIN; + return dbfs; +} + +void LOMMControlDialog::mousePressEvent(QMouseEvent* event) +{ + if ((event->button() == Qt::LeftButton || event->button() == Qt::MiddleButton) && !(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) + { + const QPoint& p = event->pos(); + + if (LOMM_DISPLAY_X - 10 <= p.x() && p.x() <= LOMM_DISPLAY_X + LOMM_DISPLAY_WIDTH + 10) + { + FloatModel* aThresh[] = {&m_controls->m_aThreshHModel, &m_controls->m_aThreshMModel, &m_controls->m_aThreshLModel}; + FloatModel* bThresh[] = {&m_controls->m_bThreshHModel, &m_controls->m_bThreshMModel, &m_controls->m_bThreshLModel}; + + for (int i = 0; i < 3; ++i) + { + if (LOMM_DISPLAY_Y[i * 2] <= p.y() && p.y() <= LOMM_DISPLAY_Y[i * 2 + 1] + LOMM_DISPLAY_HEIGHT) + { + int behavior = (p.x() < dbfsToX(bThresh[i]->value())) ? 0 : (p.x() > dbfsToX(aThresh[i]->value())) ? 1 : 2; + if (event->button() == Qt::MiddleButton) + { + if (behavior == 0 || behavior == 2) {bThresh[i]->reset();} + if (behavior == 1 || behavior == 2) {aThresh[i]->reset();} + return; + } + + m_bandDrag = i; + m_lastMousePos = p; + m_buttonPressed = true; + + m_dragType = behavior; + return; + } + } + } + } +} + +void LOMMControlDialog::mouseMoveEvent(QMouseEvent * event) +{ + if (m_buttonPressed && event->pos() != m_lastMousePos) + { + const float distance = event->pos().x() - m_lastMousePos.x(); + float dbDistance = distance * LOMM_DISPLAY_DB_PER_PIXEL; + m_lastMousePos = event->pos(); + + FloatModel* aModel[] = {&m_controls->m_aThreshHModel, &m_controls->m_aThreshMModel, &m_controls->m_aThreshLModel}; + FloatModel* bModel[] = {&m_controls->m_bThreshHModel, &m_controls->m_bThreshMModel, &m_controls->m_bThreshLModel}; + + float bVal = bModel[m_bandDrag]->value(); + float aVal = aModel[m_bandDrag]->value(); + if (m_dragType == 0) + { + bModel[m_bandDrag]->setValue(bVal + dbDistance); + } + else if (m_dragType == 1) + { + aModel[m_bandDrag]->setValue(aVal + dbDistance); + } + else + { + dbDistance = qBound(bModel[m_bandDrag]->minValue(), bVal + dbDistance, bModel[m_bandDrag]->maxValue()) - bVal; + dbDistance = qBound(aModel[m_bandDrag]->minValue(), aVal + dbDistance, aModel[m_bandDrag]->maxValue()) - aVal; + bModel[m_bandDrag]->setValue(bVal + dbDistance); + aModel[m_bandDrag]->setValue(aVal + dbDistance); + } + } +} + +void LOMMControlDialog::mouseReleaseEvent(QMouseEvent* event) +{ + if (event && event->button() == Qt::LeftButton) + { + m_buttonPressed = false; + } +} + + +} // namespace lmms::gui diff --git a/plugins/LOMM/LOMMControlDialog.h b/plugins/LOMM/LOMMControlDialog.h new file mode 100644 index 000000000..bf7e67c4c --- /dev/null +++ b/plugins/LOMM/LOMMControlDialog.h @@ -0,0 +1,129 @@ +/* + * LOMMControlDialog.h + * + * Copyright (c) 2023 Lost Robot + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_LOMM_CONTROL_DIALOG_H +#define LMMS_GUI_LOMM_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" + +#include +#include + +#include "embed.h" +#include "GuiApplication.h" +#include "Knob.h" +#include "LcdFloatSpinBox.h" +#include "LcdSpinBox.h" +#include "LedCheckBox.h" +#include "MainWindow.h" +#include "PixmapButton.h" + +namespace lmms +{ + +inline constexpr float LOMM_DISPLAY_MIN = -72; +inline constexpr float LOMM_DISPLAY_MAX = 0; +inline constexpr float LOMM_DISPLAY_X = 125; +inline constexpr float LOMM_DISPLAY_Y[6] = {24, 41, 106, 123, 186, 203}; +inline constexpr float LOMM_DISPLAY_WIDTH = 150; +inline constexpr float LOMM_DISPLAY_HEIGHT = 13; +inline constexpr float LOMM_DISPLAY_DB_PER_PIXEL = (LOMM_DISPLAY_MAX - LOMM_DISPLAY_MIN) / LOMM_DISPLAY_WIDTH; + +class LOMMControls; + + +namespace gui +{ + +class LOMMControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + LOMMControlDialog(LOMMControls* controls); + ~LOMMControlDialog() override = default; + + int dbfsToX(float dbfs); + float xToDbfs(int x); + + Knob* createKnob(KnobType knobType, QWidget* parent, int x, int y, FloatModel* model, const QString& hintText, const QString& unit, const QString& toolTip) + { + Knob* knob = new Knob(knobType, parent); + knob->move(x, y); + knob->setModel(model); + knob->setHintText(hintText, unit); + knob->setToolTip(toolTip); + return knob; + } + + LcdFloatSpinBox* createLcdFloatSpinBox(int integerDigits, int decimalDigits, const QString& color, const QString& unit, QWidget* parent, int x, int y, FloatModel* model, const QString& toolTip) + { + LcdFloatSpinBox* spinBox = new LcdFloatSpinBox(integerDigits, decimalDigits, color, unit, parent); + spinBox->move(x, y); + spinBox->setModel(model); + spinBox->setSeamless(true, true); + spinBox->setToolTip(toolTip); + return spinBox; + } + + PixmapButton* createPixmapButton(const QString& text, QWidget* parent, int x, int y, BoolModel* model, const QString& activeIcon, const QString& inactiveIcon, const QString& tooltip) + { + PixmapButton* button = new PixmapButton(parent, text); + button->move(x, y); + button->setCheckable(true); + if (model) { button->setModel(model); } + button->setActiveGraphic(PLUGIN_NAME::getIconPixmap(activeIcon)); + button->setInactiveGraphic(PLUGIN_NAME::getIconPixmap(inactiveIcon)); + button->setToolTip(tooltip); + return button; + } + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + +private: + LOMMControls* m_controls; + + QPoint m_lastMousePos; + bool m_buttonPressed = false; + int m_bandDrag = 0; + int m_dragType = -1; + + PixmapButton* m_feedbackButton; + PixmapButton* m_lowSideUpwardSuppressButton; + +private slots: + void updateFeedbackVisibility(); + void updateLowSideUpwardSuppressVisibility(); + void updateDisplay(); +}; + + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LOMM_CONTROL_DIALOG_H diff --git a/plugins/LOMM/LOMMControls.cpp b/plugins/LOMM/LOMMControls.cpp new file mode 100644 index 000000000..d695cf483 --- /dev/null +++ b/plugins/LOMM/LOMMControls.cpp @@ -0,0 +1,277 @@ +/* + * LOMMControls.cpp + * + * Copyright (c) 2023 Lost Robot + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#include "LOMMControls.h" +#include "LOMM.h" + +#include +#include + +namespace lmms +{ + +LOMMControls::LOMMControls(LOMMEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_depthModel(0.4, 0, 1, 0.00001, this, tr("Depth")), + m_timeModel(1, 0, 10, 0.00001, this, tr("Time")), + m_inVolModel(0, -48, 48, 0.00001, this, tr("Input Volume")), + m_outVolModel(8, -48, 48, 0.00001, this, tr("Output Volume")), + m_upwardModel(1, 0, 2, 0.00001, this, tr("Upward Depth")), + m_downwardModel(1, 0, 2, 0.00001, this, tr("Downward Depth")), + m_split1Model(2500, 20, 20000, 0.01, this, tr("High/Mid Split")), + m_split2Model(88.3, 20, 20000, 0.01, this, tr("Mid/Low Split")), + m_split1EnabledModel(true, this, tr("Enable High/Mid Split")), + m_split2EnabledModel(true, this, tr("Enable Mid/Low Split")), + m_band1EnabledModel(true, this, tr("Enable High Band")), + m_band2EnabledModel(true, this, tr("Enable Mid Band")), + m_band3EnabledModel(true, this, tr("Enable Low Band")), + m_inHighModel(0, -48, 48, 0.00001, this, tr("High Input Volume")), + m_inMidModel(0, -48, 48, 0.00001, this, tr("Mid Input Volume")), + m_inLowModel(0, -48, 48, 0.00001, this, tr("Low Input Volume")), + m_outHighModel(4.6, -48, 48, 0.00001, this, tr("High Output Volume")), + m_outMidModel(0.0, -48, 48, 0.00001, this, tr("Mid Output Volume")), + m_outLowModel(4.6, -48, 48, 0.00001, this, tr("Low Output Volume")), + m_aThreshHModel(-30.3, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold High")), + m_aThreshMModel(-25.0, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold Mid")), + m_aThreshLModel(-28.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Above Threshold Low")), + m_aRatioHModel(99.99, 1, 99.99, 0.01, this, tr("Above Ratio High")), + m_aRatioMModel(66.7, 1, 99.99, 0.01, this, tr("Above Ratio Mid")), + m_aRatioLModel(66.7, 1, 99.99, 0.01, this, tr("Above Ratio Low")), + m_bThreshHModel(-35.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold High")), + m_bThreshMModel(-36.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold Mid")), + m_bThreshLModel(-35.6, LOMM_DISPLAY_MIN, LOMM_DISPLAY_MAX, 0.001, this, tr("Below Threshold Low")), + m_bRatioHModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio High")), + m_bRatioMModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio Mid")), + m_bRatioLModel(4.17, 1, 99.99, 0.01, this, tr("Below Ratio Low")), + m_atkHModel(13.5, 0, 1000, 0.001, this, tr("Attack High")), + m_atkMModel(22.4, 0, 1000, 0.001, this, tr("Attack Mid")), + m_atkLModel(47.8, 0, 1000, 0.001, this, tr("Attack Low")), + m_relHModel(132, 0, 1000, 0.001, this, tr("Release High")), + m_relMModel(282, 0, 1000, 0.001, this, tr("Release Mid")), + m_relLModel(282, 0, 1000, 0.001, this, tr("Release Low")), + m_rmsTimeModel(10, 0, 500, 0.001, this, tr("RMS Time")), + m_kneeModel(6, 0, 36, 0.00001, this, tr("Knee")), + m_rangeModel(36, 0, 96, 0.00001, this, tr("Range")), + m_balanceModel(0, -18, 18, 0.00001, this, tr("Balance")), + m_depthScalingModel(true, this, tr("Scale output volume with Depth")), + m_stereoLinkModel(false, this, tr("Stereo Link")), + m_autoTimeModel(0, 0, 1, 0.00001, this, tr("Auto Time")), + m_mixModel(1, 0, 1, 0.00001, this, tr("Mix")), + m_feedbackModel(false, this, tr("Feedback")), + m_midsideModel(false, this, tr("Mid/Side")), + m_lookaheadEnableModel(false, this, tr("Lookahead")), + m_lookaheadModel(0.f, 0.f, LOMM_MAX_LOOKAHEAD, 0.01, this, tr("Lookahead Length")), + m_lowSideUpwardSuppressModel(false, this, tr("Suppress upward compression for side band")) +{ + auto models = {&m_timeModel, &m_inVolModel, &m_outVolModel, &m_inHighModel, &m_inMidModel, + &m_inLowModel, &m_outHighModel, &m_outMidModel, &m_outLowModel, &m_aRatioHModel, + &m_aRatioMModel, &m_aRatioLModel, &m_bRatioHModel, &m_bRatioMModel, &m_bRatioLModel, + &m_atkHModel, &m_atkMModel, &m_atkLModel, &m_relHModel, &m_relMModel, &m_relLModel, + &m_rmsTimeModel, &m_balanceModel}; + for (auto model : models) { model->setScaleLogarithmic(true); } +} + + +void LOMMControls::resetAllParameters() +{ + int choice = QMessageBox::question(m_view, "Clear Plugin Settings", "Are you sure you want to clear all parameters?\n(This wipes LOMM to a clean slate, not the default preset.)", QMessageBox::Yes | QMessageBox::No); + if (choice != QMessageBox::Yes) { return; } + + // give the user a chance to beg LMMS for forgiveness + addJournalCheckPoint(); + + // This plugin's normal default values are fairly close to what they'd want in most applications. + // The Init button is there so the user can start from a clean slate instead. + // These are those values. + setInitAndReset(m_depthModel, 1); + setInitAndReset(m_timeModel, 1); + setInitAndReset(m_inVolModel, 0); + setInitAndReset(m_outVolModel, 0); + setInitAndReset(m_upwardModel, 1); + setInitAndReset(m_downwardModel, 1); + setInitAndReset(m_split1Model, 2500); + setInitAndReset(m_split2Model, 88); + setInitAndReset(m_split1EnabledModel, true); + setInitAndReset(m_split2EnabledModel, true); + setInitAndReset(m_band1EnabledModel, true); + setInitAndReset(m_band2EnabledModel, true); + setInitAndReset(m_band3EnabledModel, true); + setInitAndReset(m_inHighModel, 0); + setInitAndReset(m_inMidModel, 0); + setInitAndReset(m_inLowModel, 0); + setInitAndReset(m_outHighModel, 0); + setInitAndReset(m_outMidModel, 0); + setInitAndReset(m_outLowModel, 0); + setInitAndReset(m_aThreshHModel, m_aThreshHModel.maxValue()); + setInitAndReset(m_aThreshMModel, m_aThreshMModel.maxValue()); + setInitAndReset(m_aThreshLModel, m_aThreshLModel.maxValue()); + setInitAndReset(m_aRatioHModel, 1); + setInitAndReset(m_aRatioMModel, 1); + setInitAndReset(m_aRatioLModel, 1); + setInitAndReset(m_bThreshHModel, m_bThreshHModel.minValue()); + setInitAndReset(m_bThreshMModel, m_bThreshMModel.minValue()); + setInitAndReset(m_bThreshLModel, m_bThreshLModel.minValue()); + setInitAndReset(m_bRatioHModel, 1); + setInitAndReset(m_bRatioMModel, 1); + setInitAndReset(m_bRatioLModel, 1); + setInitAndReset(m_atkHModel, 13.5); + setInitAndReset(m_atkMModel, 22.4); + setInitAndReset(m_atkLModel, 47.8); + setInitAndReset(m_relHModel, 132); + setInitAndReset(m_relMModel, 282); + setInitAndReset(m_relLModel, 282); + setInitAndReset(m_rmsTimeModel, 10); + setInitAndReset(m_kneeModel, 6); + setInitAndReset(m_rangeModel, 36); + setInitAndReset(m_balanceModel, 0); + setInitAndReset(m_depthScalingModel, true); + setInitAndReset(m_stereoLinkModel, false); + setInitAndReset(m_autoTimeModel, 0); + setInitAndReset(m_mixModel, 1); + setInitAndReset(m_feedbackModel, false); + setInitAndReset(m_midsideModel, false); + setInitAndReset(m_lookaheadEnableModel, false); + setInitAndReset(m_lookaheadModel, 0.f); + setInitAndReset(m_lowSideUpwardSuppressModel, false); +} + + + +void LOMMControls::loadSettings(const QDomElement& parent) +{ + m_depthModel.loadSettings(parent, "depth"); + m_timeModel.loadSettings(parent, "time"); + m_inVolModel.loadSettings(parent, "inVol"); + m_outVolModel.loadSettings(parent, "outVol"); + m_upwardModel.loadSettings(parent, "upward"); + m_downwardModel.loadSettings(parent, "downward"); + m_split1Model.loadSettings(parent, "split1"); + m_split2Model.loadSettings(parent, "split2"); + m_split1EnabledModel.loadSettings(parent, "split1Enabled"); + m_split2EnabledModel.loadSettings(parent, "split2Enabled"); + m_band1EnabledModel.loadSettings(parent, "band1Enabled"); + m_band2EnabledModel.loadSettings(parent, "band2Enabled"); + m_band3EnabledModel.loadSettings(parent, "band3Enabled"); + m_inHighModel.loadSettings(parent, "inHigh"); + m_inMidModel.loadSettings(parent, "inMid"); + m_inLowModel.loadSettings(parent, "inLow"); + m_outHighModel.loadSettings(parent, "outHigh"); + m_outMidModel.loadSettings(parent, "outMid"); + m_outLowModel.loadSettings(parent, "outLow"); + m_aThreshHModel.loadSettings(parent, "aThreshH"); + m_aThreshMModel.loadSettings(parent, "aThreshM"); + m_aThreshLModel.loadSettings(parent, "aThreshL"); + m_aRatioHModel.loadSettings(parent, "aRatioH"); + m_aRatioMModel.loadSettings(parent, "aRatioM"); + m_aRatioLModel.loadSettings(parent, "aRatioL"); + m_bThreshHModel.loadSettings(parent, "bThreshH"); + m_bThreshMModel.loadSettings(parent, "bThreshM"); + m_bThreshLModel.loadSettings(parent, "bThreshL"); + m_bRatioHModel.loadSettings(parent, "bRatioH"); + m_bRatioMModel.loadSettings(parent, "bRatioM"); + m_bRatioLModel.loadSettings(parent, "bRatioL"); + m_atkHModel.loadSettings(parent, "atkH"); + m_atkMModel.loadSettings(parent, "atkM"); + m_atkLModel.loadSettings(parent, "atkL"); + m_relHModel.loadSettings(parent, "relH"); + m_relMModel.loadSettings(parent, "relM"); + m_relLModel.loadSettings(parent, "relL"); + m_rmsTimeModel.loadSettings(parent, "rmsTime"); + m_kneeModel.loadSettings(parent, "knee"); + m_rangeModel.loadSettings(parent, "range"); + m_balanceModel.loadSettings(parent, "balance"); + m_depthScalingModel.loadSettings(parent, "depthScaling"); + m_stereoLinkModel.loadSettings(parent, "stereoLink"); + m_autoTimeModel.loadSettings(parent, "autoTime"); + m_mixModel.loadSettings(parent, "mix"); + m_feedbackModel.loadSettings(parent, "feedback"); + m_midsideModel.loadSettings(parent, "midside"); + m_lookaheadEnableModel.loadSettings(parent, "lookaheadEnable"); + m_lookaheadModel.loadSettings(parent, "lookahead"); + m_lowSideUpwardSuppressModel.loadSettings(parent, "lowSideUpwardSuppress"); +} + + + + +void LOMMControls::saveSettings(QDomDocument& doc, QDomElement& parent) +{ + m_depthModel.saveSettings(doc, parent, "depth"); + m_timeModel.saveSettings(doc, parent, "time"); + m_inVolModel.saveSettings(doc, parent, "inVol"); + m_outVolModel.saveSettings(doc, parent, "outVol"); + m_upwardModel.saveSettings(doc, parent, "upward"); + m_downwardModel.saveSettings(doc, parent, "downward"); + m_split1Model.saveSettings(doc, parent, "split1"); + m_split2Model.saveSettings(doc, parent, "split2"); + m_split1EnabledModel.saveSettings(doc, parent, "split1Enabled"); + m_split2EnabledModel.saveSettings(doc, parent, "split2Enabled"); + m_band1EnabledModel.saveSettings(doc, parent, "band1Enabled"); + m_band2EnabledModel.saveSettings(doc, parent, "band2Enabled"); + m_band3EnabledModel.saveSettings(doc, parent, "band3Enabled"); + m_inHighModel.saveSettings(doc, parent, "inHigh"); + m_inMidModel.saveSettings(doc, parent, "inMid"); + m_inLowModel.saveSettings(doc, parent, "inLow"); + m_outHighModel.saveSettings(doc, parent, "outHigh"); + m_outMidModel.saveSettings(doc, parent, "outMid"); + m_outLowModel.saveSettings(doc, parent, "outLow"); + m_aThreshHModel.saveSettings(doc, parent, "aThreshH"); + m_aThreshMModel.saveSettings(doc, parent, "aThreshM"); + m_aThreshLModel.saveSettings(doc, parent, "aThreshL"); + m_aRatioHModel.saveSettings(doc, parent, "aRatioH"); + m_aRatioMModel.saveSettings(doc, parent, "aRatioM"); + m_aRatioLModel.saveSettings(doc, parent, "aRatioL"); + m_bThreshHModel.saveSettings(doc, parent, "bThreshH"); + m_bThreshMModel.saveSettings(doc, parent, "bThreshM"); + m_bThreshLModel.saveSettings(doc, parent, "bThreshL"); + m_bRatioHModel.saveSettings(doc, parent, "bRatioH"); + m_bRatioMModel.saveSettings(doc, parent, "bRatioM"); + m_bRatioLModel.saveSettings(doc, parent, "bRatioL"); + m_atkHModel.saveSettings(doc, parent, "atkH"); + m_atkMModel.saveSettings(doc, parent, "atkM"); + m_atkLModel.saveSettings(doc, parent, "atkL"); + m_relHModel.saveSettings(doc, parent, "relH"); + m_relMModel.saveSettings(doc, parent, "relM"); + m_relLModel.saveSettings(doc, parent, "relL"); + m_rmsTimeModel.saveSettings(doc, parent, "rmsTime"); + m_kneeModel.saveSettings(doc, parent, "knee"); + m_rangeModel.saveSettings(doc, parent, "range"); + m_balanceModel.saveSettings(doc, parent, "balance"); + m_depthScalingModel.saveSettings(doc, parent, "depthScaling"); + m_stereoLinkModel.saveSettings(doc, parent, "stereoLink"); + m_autoTimeModel.saveSettings(doc, parent, "autoTime"); + m_mixModel.saveSettings(doc, parent, "mix"); + m_feedbackModel.saveSettings(doc, parent, "feedback"); + m_midsideModel.saveSettings(doc, parent, "midside"); + m_lookaheadEnableModel.saveSettings(doc, parent, "lookaheadEnable"); + m_lookaheadModel.saveSettings(doc, parent, "lookahead"); + m_lowSideUpwardSuppressModel.saveSettings(doc, parent, "lowSideUpwardSuppress"); +} + + +} // namespace lmms + + diff --git a/plugins/LOMM/LOMMControls.h b/plugins/LOMM/LOMMControls.h new file mode 100644 index 000000000..3e5325426 --- /dev/null +++ b/plugins/LOMM/LOMMControls.h @@ -0,0 +1,136 @@ +/* + * LOMMControls.h + * + * Copyright (c) 2023 Lost Robot + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_LOMM_CONTROLS_H +#define LMMS_LOMM_CONTROLS_H + +#include "LOMMControlDialog.h" +#include "EffectControls.h" + +namespace lmms +{ +class LOMMEffect; + +namespace gui +{ +class LOMMControlDialog; +} + +class LOMMControls : public EffectControls +{ + Q_OBJECT +public: + LOMMControls(LOMMEffect* effect); + ~LOMMControls() override = default; + + void saveSettings(QDomDocument & doc, QDomElement & parent) override; + void loadSettings(const QDomElement & parent) override; + inline QString nodeName() const override + { + return "LOMMControls"; + } + + int controlCount() override + { + return 49; + } + + gui::EffectControlDialog* createView() override + { + m_view = new gui::LOMMControlDialog(this); + return m_view; + } + + template + void setInitAndReset(AutomatableModel& model, T initValue) + { + model.setInitValue(initValue); + model.reset(); + } + +public slots: + void resetAllParameters(); + +private: + LOMMEffect* m_effect; + gui::LOMMControlDialog* m_view; + + FloatModel m_depthModel; + FloatModel m_timeModel; + FloatModel m_inVolModel; + FloatModel m_outVolModel; + FloatModel m_upwardModel; + FloatModel m_downwardModel; + FloatModel m_split1Model; + FloatModel m_split2Model; + BoolModel m_split1EnabledModel; + BoolModel m_split2EnabledModel; + BoolModel m_band1EnabledModel; + BoolModel m_band2EnabledModel; + BoolModel m_band3EnabledModel; + FloatModel m_inHighModel; + FloatModel m_inMidModel; + FloatModel m_inLowModel; + FloatModel m_outHighModel; + FloatModel m_outMidModel; + FloatModel m_outLowModel; + FloatModel m_aThreshHModel; + FloatModel m_aThreshMModel; + FloatModel m_aThreshLModel; + FloatModel m_aRatioHModel; + FloatModel m_aRatioMModel; + FloatModel m_aRatioLModel; + FloatModel m_bThreshHModel; + FloatModel m_bThreshMModel; + FloatModel m_bThreshLModel; + FloatModel m_bRatioHModel; + FloatModel m_bRatioMModel; + FloatModel m_bRatioLModel; + FloatModel m_atkHModel; + FloatModel m_atkMModel; + FloatModel m_atkLModel; + FloatModel m_relHModel; + FloatModel m_relMModel; + FloatModel m_relLModel; + FloatModel m_rmsTimeModel; + FloatModel m_kneeModel; + FloatModel m_rangeModel; + FloatModel m_balanceModel; + BoolModel m_depthScalingModel; + BoolModel m_stereoLinkModel; + FloatModel m_autoTimeModel; + FloatModel m_mixModel; + BoolModel m_feedbackModel; + BoolModel m_midsideModel; + BoolModel m_lookaheadEnableModel; + FloatModel m_lookaheadModel; + BoolModel m_lowSideUpwardSuppressModel; + + friend class gui::LOMMControlDialog; + friend class LOMMEffect; +}; + +} // namespace lmms + +#endif // LMMS_LOMM_CONTROLS_H diff --git a/plugins/LOMM/artwork.png b/plugins/LOMM/artwork.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc65908ff17596ab9a64e15b8b255041299b6e8 GIT binary patch literal 70032 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

Mv;?M#zTV?^Kf$O8on#Sdn!)c7<+v{2US!r;ajG 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

y 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

5{{ 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 JPDHLkV1gv)PwM~x literal 0 HcmV?d00001 diff --git a/plugins/LOMM/crossover_led_green.png b/plugins/LOMM/crossover_led_green.png new file mode 100644 index 0000000000000000000000000000000000000000..440eb82dd1f891316ff59478df12f792391e8081 GIT binary patch literal 290 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 literal 0 HcmV?d00001 diff --git a/plugins/LOMM/depthScaling_inactive.png b/plugins/LOMM/depthScaling_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..ba806b6c35fa5c9c2d2683e9c7fd4a7a696ec97a GIT binary patch literal 886 zcmV-+1Bv{JP)EX>4Tx04R}tkvmAkKpe)uKBOWQ2Rn!l$WWauf{HlSDionYs1;guFuC*vO&XFE z7e~Rh;NWAi>fqw6tAnc`2tGhuU7QqMq{ROvg%&X$9QWh9`#$cz1N@B&Q_Wx;P&La) zClf+8wiKB|DQNBOx zvch?bvs$UK);;+P!+C9Gnd>x%k;EdFAVPqQ8p^1^LX1|86ccIMPk8tT9luB}nOtQs zax9<<6_Voz|AXJ%nuV!JHz}L|x?XJiV-yJO0?oQ@e;?a+^8^Sy16NwxUu^)hpQP8@ zTJ#9$+XgPK+nT%wTc`thv3l_Hp_EWT>mu4RCM> zj1?(+-Q(TeoxS~grq$mMt?F{VmYf}100006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru=L8-U05>5)sZ{^~02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00B-(L_t(2&%KhrPr^_TfUoUq3J5Wh5-li7AQXa8V`|Fc z!a^9FO#CBFoEQiTi&JA`peZ3CG1~H@8VL?sWOTUt@=Srzo4)(*?p{rSa zV4^?hyWV)6M?NVZXOs-`#G6-k~P|5RszvPyjHxCZ@EBdbLoZDLaKeB1PHi zN+zNxavVsJSa6Bc^IZ0bUGwa|7`;biwlmC|tHC~=7r&QFK_Hl$wq@ED{0&-VJHx!W z8tmf)-e34?F8|~PkCD$Frg!)5AB@WjefsvwdVk@Q-$|UJOau}(Vdi4-ZADHUC~5XW zBX9cg`L&aXuk^@@ET@u5-}WI4?f0sbp~0E+;oH2>v;=>Px# M07*qoM6N<$f*VnW_W%F@ literal 0 HcmV?d00001 diff --git a/plugins/LOMM/feedback_active.png b/plugins/LOMM/feedback_active.png new file mode 100644 index 0000000000000000000000000000000000000000..426abb50683157fed7f88616a749b8448be0b67c GIT binary patch literal 11378 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 IEtiY`1(q$GDF6Tf literal 0 HcmV?d00001 diff --git a/plugins/LOMM/high_band_inactive.png b/plugins/LOMM/high_band_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..43a24cc8f7b42f548896c1dce256c2ad442f144c GIT binary patch literal 1818 zcmV+#2j%#QP)EX>4Tx04R}tkvmAkP!xv$rWGGl9PA+CkfA!Yi;6hbDionYs1;guFuCaqnlvOS zE{=k0!NJF3)xpJCR|i)?5PX2Rx;QDiNQvhrg%&YhINXo_-v8&^a{-}QWttUA0Ge)_ znN(8Dq;L}Gd2yVNaiDh>Xf+(?``B?>CqVESxY9fRS`(Q4B)!qm zqDR2cHgIv>(Ud*lat9cBGGtSBr6841ECTOm^i6qS_!byg^XAq*$LRx*rCFtKfP+I| zqD0y2KJV`9p4-27+VlGXHYjqA!#?Ry00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru=L8D`G#k@WGMoSa02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00i<$L_t(&-tC!NZ`(E$$N!Y5pay33#gUvmG#IeeZ4xH| z+FXWa5BmiDtosD(fE9Dn3+Bj9;K%KuwUWNnUXUV`#63u)NnLzV)Gfe#07K*v4-dhg z4tYqE$4~Y+2tYs}g(x*qCb5RZjgwad(vlDa0YhWe!Ml@JSW2-xKl}-wK3#!Q6W{;` z0M6qlI5_9Btt`v;dRw71WSw)!rkQfmq0nMJvaH4{D4Dv%ibWO#2!a64s*69~|F)!4LiV2hh^s3flu`*Ui5Pv! z<#I8VWeCUs$ajx-cm|-{|5D}Dei;M-9LGiX{cD(}xujA;!Z5_%vtwLdreNkl%-cH? zprt_ujKOi6=yf|FgupNi&1NaHg>#N5ioiL?_U;e3x*UR1s}x9DW&n&aI8F?aQVP>F zCB!An`5}sD7>4L|f5-Oj(Ihe#l!Yx=5JT*r#SksalF?WO;?2#CEZ1r^t*%O~R-0)% zkB?b>-ev4%U~5wJGWhTFp%}#Rtw$1ps?ROv27cp(P-+}%Pg;H+x}Sx zTRVriO!mymK(ZvU>3h{f1c2JgN^bqA-39=3JWs0&>3AM$D=V75<9Qg3Mj4nzKGOZF zzB-<#jX^+Eb?JT=5kiPuUaizp+CMwN*3Kb(e+X-iJ(JEAiS8QCz0)kjD2fMU7Bva$cYm5j_2X!i(>%5%NNJ^`Ss+iD)g@XFr!!Nst;iUKx-r5srI71XeY{P-Cczc z0-j{9xiH&@@Gn2Ynh}|FRwRCHh-olombYnoRf>QY`Of91drl#C#eRaKl-dig}T)FqtDuNUVWYq6}YG$wc8pn;k&5) zy#C6-1djlog?a9sVzYe!-!G0#V`?N8KukPWcBZaUXE6xVdb0Amb65Xk4|_CAKgj|ynz zt~Cf4Tz4H8{TQOSsn8&DnQHy(S6buoSo`_~#HuIop4{K*)11H*ml#11;OX{0{NV>! z*2)AVW4CBF*KyH*DT5wY#uBM7^ zGQ_@V6dX(LYG2e}+5K8vXhhL`AFXJo|NAhJFbt>mcUSu$48ekM0&{CW-ZNbVqFi%{ zfgtHH$CJp4u$2EMDuGb4y6C^b)2-(qwr%rFVzZQcFX;s6AL{{nkvo0QZU6uP07*qo IM6N<$f_F<@>i_@% literal 0 HcmV?d00001 diff --git a/plugins/LOMM/init_active.png b/plugins/LOMM/init_active.png new file mode 100644 index 0000000000000000000000000000000000000000..3401a74c1925715de160a6430b6d4ef3bd3ebf8e GIT binary patch literal 1204 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 bSS0`8G&1+&#;`c+ND~D0 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 literal 0 HcmV?d00001 diff --git a/plugins/LOMM/mid_band_inactive.png b/plugins/LOMM/mid_band_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..6ccabc0ad9f766bbdbb6d471af856dc9fa0636cf GIT binary patch literal 1843 zcmV-32h8}1P)EX>4Tx04R}tkvmAkP!xv$rWGGl9PA+CkfA!Yi;6hbDionYs1;guFuCaqnlvOS zE{=k0!NJF3)xpJCR|i)?5PX2Rx;QDiNQvhrg%&YhINXo_-v8&^a{-}QWttUA0Ge)_ znN(8Dq;L}Gd2yVNaiDh>Xf+(?``B?>CqVESxY9fRS`(Q4B)!qm zqDR2cHgIv>(Ud*lat9cBGGtSBr6841ECTOm^i6qS_!byg^XAq*$LRx*rCFtKfP+I| zqD0y2KJV`9p4-27+VlGXHYjqA!#?Ry00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru=L8D`G!Zk0+kyZ902y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00j$4L_t(&-tC$}Q`<%q$Nwu!OnktdDYj%dz;vdG2w+T` zLP*Q0U!b2gUm(4NiGfU8V;e@tiM>@vilHkao3_EG!$R&B1T5wfpqmTh0Fb zcAs`&o7*pl0YLN>HiLH1WU`?wn6zh+y$}NBl?u)pZ?KSTIe7IeZvMW3Sug?kkkR7pc9I;0}K#>Kp+Z@C~zktztr|;fi<=Zm2 z;MyEUS4uL8&3D8`XJAkW0ttwSL6m_bo&1#$0bC4GUMb^!-$b091$AZZ=?=CJKdu@?XHr4^du6qY;D_?J79uxnh~QGSOMZMe8&T(f5M~G7LOcoYFS{ zfJUo@-+np5>*Kd$^-^>jU7rA_ZZf8{*W}K-i~y$Zr(w2EuvI%iuX`0$v(td& zd&`%16dB-`WqS+Vt)hqYP^m|QZm?57!X7MLI( zpft>j)+x4X2k3UQGt+o56Eh%&BhNJp2gBh+eO9~fx|#mEt=#8iKPyHu)+GBm(hRF0 zKecCGv`*pJtC@SIIW`lkYq)5iWkMt{fM~8S|M!FY2lejy2JQ#5S*XuEo&m#NNj5&K1%fLY6GqZh)t(^mOyPs0kEc^c1F3E!c&N-aw8ZIY6 zoU`2C-m3QU^72@HUeWz$jiJZN~$eu?EEf1%^^&AkRgP?^PPQkuRg)DmLiax4^geI;j;5yL5%H(3;8-E z1e%6vfo*)ZNhyeC0cOT&g*=|hI?a&#rjhYlGL646;nijyd$c^z@cn#0T4@*mmy1Y7 zqtS!$9_uegBXBW_U~caRd#0m7RBJ9XAi{uSMv_(0B18VCtOQ1i)n(@l&$oX9E0s!= hDr^?=)l0Gf`WHH+v=fF4M&SSe002ovPDHLkV1l0!RZ0K= literal 0 HcmV?d00001 diff --git a/plugins/LOMM/midside_active.png b/plugins/LOMM/midside_active.png new file mode 100644 index 0000000000000000000000000000000000000000..9caf8b69147bd7f94411fe814000140075aecd8e GIT binary patch literal 11242 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 literal 0 HcmV?d00001 diff --git a/plugins/LOMM/midside_inactive.png b/plugins/LOMM/midside_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..7b14b75df0199da8d1dc7a0422cd906898a57308 GIT binary patch literal 9190 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 SRTbl|M-1A*s%ZOxsQ&?()v&4n literal 0 HcmV?d00001 diff --git a/plugins/LOMM/split1Enabled_active.png b/plugins/LOMM/split1Enabled_active.png new file mode 100644 index 0000000000000000000000000000000000000000..6b3f8ec62d854a55aadfa38570387a6671bdf5b7 GIT binary patch literal 11035 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 Wk5`zv_zjf+0000ly literal 0 HcmV?d00001 diff --git a/plugins/LOMM/stereoLink_active.png b/plugins/LOMM/stereoLink_active.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba9c4b8857d1e557a106929e378200d38e465d3 GIT binary patch literal 1042 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 literal 0 HcmV?d00001 From 5596abb66a27720c5107cbdb7348491f8f0c49e1 Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 10 Nov 2023 14:26:31 -0500 Subject: [PATCH 030/191] Improve search performance in `FileBrowser` (#6962) Improves the search performance of the file browser by delegating the search to a worker thread. The main thread then builds the tree and displays it to the user. --- include/FileBrowser.h | 59 ++++++++++++ src/gui/FileBrowser.cpp | 199 +++++++++++++++++++++++++++++++++++++++- src/gui/MainWindow.cpp | 24 ++--- 3 files changed, 262 insertions(+), 20 deletions(-) diff --git a/include/FileBrowser.h b/include/FileBrowser.h index eafb827da..02fec2719 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -28,6 +28,17 @@ #include #include #include + +#ifdef __MINGW32__ +#include +#include +#include +#else +#include +#include +#include +#endif + #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include #endif @@ -72,6 +83,8 @@ public: ~FileBrowser() override = default; + static QDir::Filters dirFilters(); + private slots: void reloadTree(); void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); @@ -86,7 +99,12 @@ private: void saveDirectoriesStates(); void restoreDirectoriesStates(); + void buildSearchTree(QStringList matches, QString id); + void onSearch(const QString& filter); + void toggleSearch(bool on); + FileBrowserTreeWidget * m_fileBrowserTreeWidget; + FileBrowserTreeWidget * m_searchTreeWidget; QLineEdit * m_filterEdit; @@ -165,6 +183,46 @@ private slots: } ; +class FileBrowserSearcher : public QObject +{ + Q_OBJECT +public: + struct SearchTask + { + QString directories; + QString userFilter; + QDir::Filters dirFilters; + QStringList nameFilters; + QString id; + }; + + FileBrowserSearcher(); + ~FileBrowserSearcher() noexcept override; + + void search(SearchTask task); + void cancel(); + + bool inHiddenDirectory(const QString& path); + + static FileBrowserSearcher* instance(); + +signals: + void searchComplete(QStringList matches, QString id); + +private: + void run(); + void filter(); + SearchTask m_currentTask; + std::thread m_worker; + std::mutex m_runMutex; + std::mutex m_cancelMutex; + std::condition_variable m_runCond; + std::atomic m_cancel = false; + bool m_stopped = false; + bool m_run = false; + inline static std::unique_ptr s_instance = nullptr; +}; + @@ -274,6 +332,7 @@ public: QString extension(); static QString extension( const QString & file ); + static QString defaultFilters(); private: diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 1be344c98..776ac0810 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -23,8 +23,11 @@ * */ +#include "FileBrowser.h" + #include #include +#include #include #include #include @@ -126,7 +129,8 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_filterEdit->setClearButtonEnabled(true); m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); - connect(m_filterEdit, &QLineEdit::textEdited, [this](const QString & filter) { filterAndExpandItems(filter); }); + connect(m_filterEdit, &QLineEdit::textEdited, this, &FileBrowser::onSearch); + connect(FileBrowserSearcher::instance(), &FileBrowserSearcher::searchComplete, this, &FileBrowser::buildSearchTree); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); @@ -141,6 +145,10 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_fileBrowserTreeWidget = new FileBrowserTreeWidget( contentParent() ); addContentWidget( m_fileBrowserTreeWidget ); + m_searchTreeWidget = new FileBrowserTreeWidget(contentParent()); + m_searchTreeWidget->hide(); + addContentWidget(m_searchTreeWidget); + // Whenever the FileBrowser has focus, Ctrl+F should direct focus to its filter box. auto filterFocusShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this, SLOT(giveFocusToFilter())); filterFocusShortcut->setContext(Qt::WidgetWithChildrenShortcut); @@ -151,6 +159,11 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, show(); } +QDir::Filters FileBrowser::dirFilters() +{ + return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; +} + void FileBrowser::saveDirectoriesStates() { m_savedExpandedDirs = m_fileBrowserTreeWidget->expandedDirs(); @@ -161,6 +174,87 @@ void FileBrowser::restoreDirectoriesStates() expandItems(nullptr, m_savedExpandedDirs); } +void FileBrowser::buildSearchTree(QStringList matches, QString id) +{ + if (title() != id) { return; } + + m_searchTreeWidget->clear(); + + const auto rootPaths = m_directories.split('*'); + for (const auto& rootPath : rootPaths) + { + const auto rootPathDir = QDir{rootPath}; + const auto absoluteRootPath = rootPathDir.absolutePath(); + + for (const auto& match : matches) + { + if (!match.startsWith(absoluteRootPath)) { continue; } + + const auto childInfo = QFileInfo{match}; + const auto childName = childInfo.fileName(); + const auto parentPath = childInfo.dir().path(); + auto childWidget = static_cast(nullptr); + + if (childInfo.isDir()) + { + auto dirChildWidget = new Directory(childName, parentPath, m_filter); + dirChildWidget->update(); + childWidget = dirChildWidget; + } + else if (childInfo.isFile()) { childWidget = new FileItem(childName, parentPath); } + else { continue; } + + const auto relativeParentPath = rootPathDir.relativeFilePath(parentPath); + if (relativeParentPath == ".") + { + m_searchTreeWidget->addTopLevelItem(childWidget); + if (childInfo.isDir()) { m_searchTreeWidget->expandItem(childWidget); } + continue; + } + + const auto grandParentPath = QFileInfo{parentPath}.dir().path(); + const auto parentItems = m_searchTreeWidget->findItems(relativeParentPath, Qt::MatchExactly); + + if (parentItems.isEmpty()) + { + auto parentItem = new Directory(relativeParentPath, grandParentPath, m_filter); + parentItem->addChild(childWidget); + m_searchTreeWidget->addTopLevelItem(parentItem); + m_searchTreeWidget->expandItem(parentItem); + } + else { parentItems[0]->addChild(childWidget); } + } + } + + toggleSearch(true); +} + + +void FileBrowser::onSearch(const QString& filter) +{ + auto instance = FileBrowserSearcher::instance(); + if (filter.isEmpty()) + { + toggleSearch(false); + instance->cancel(); + return; + } + instance->search({m_directories, filter, dirFilters(), m_filter.split(' '), title()}); +} + +void FileBrowser::toggleSearch(bool on) +{ + if (on) + { + m_searchTreeWidget->show(); + m_fileBrowserTreeWidget->hide(); + return; + } + + m_searchTreeWidget->hide(); + m_fileBrowserTreeWidget->show(); +} + bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item) { // Call with item = nullptr to filter the entire tree @@ -332,9 +426,7 @@ void FileBrowser::addItems(const QString & path ) QDir cdir(path); if (!cdir.isReadable()) { return; } QFileInfoList entries = cdir.entryInfoList( - m_filter.split(' '), - QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, - QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + m_filter.split(' '), dirFilters(), QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); for (const auto& entry : entries) { QString fileName = entry.fileName(); @@ -956,7 +1048,93 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item ) +FileBrowserSearcher::FileBrowserSearcher() + : m_worker([this] { run(); }) +{ +} +FileBrowserSearcher::~FileBrowserSearcher() noexcept +{ + m_cancel = true; + { + const auto runLock = std::lock_guard{m_runMutex}; + m_stopped = true; + m_cancel = false; + } + m_runCond.notify_one(); + m_worker.join(); +} + +void FileBrowserSearcher::search(SearchTask task) +{ + m_cancel = true; + { + const auto runLock = std::lock_guard{m_runMutex}; + m_currentTask = std::move(task); + m_run = true; + m_cancel = false; + } + m_runCond.notify_one(); +} + +void FileBrowserSearcher::cancel() +{ + m_cancel = true; +} + +void FileBrowserSearcher::run() +{ + while (true) + { + auto lock = std::unique_lock{m_runMutex}; + m_runCond.wait(lock, [this] { return m_run || m_stopped; }); + + if (m_stopped) { break; } + + filter(); + m_run = false; + } +} + +void FileBrowserSearcher::filter() +{ + const auto& [directories, userFilter, filters, nameFilters, id] = m_currentTask; + const auto paths = directories.split('*'); + auto matches = QStringList{}; + + for (const auto& path : paths) + { + auto it = QDirIterator{path, nameFilters, filters, QDirIterator::Subdirectories}; + while (it.hasNext()) + { + it.next(); + const auto name = it.fileName(); + const auto path = it.filePath(); + if (!inHiddenDirectory(path) && name.contains(userFilter, Qt::CaseInsensitive)) { matches.push_back(path); } + if (m_cancel) { return; } + } + } + + emit searchComplete(matches, id); +} + +FileBrowserSearcher* FileBrowserSearcher::instance() +{ + if (!s_instance) { s_instance = std::make_unique(); } + return s_instance.get(); +} + +bool FileBrowserSearcher::inHiddenDirectory(const QString& path) +{ + auto dir = QDir{path}; + while (!dir.isRoot()) + { + auto info = QFileInfo{dir.path()}; + if (info.isHidden()) { return true; } + dir.cdUp(); + } + return false; +} QPixmap * Directory::s_folderPixmap = nullptr; @@ -1276,5 +1454,18 @@ QString FileItem::extension(const QString & file ) return QFileInfo( file ).suffix().toLower(); } +QString FileItem::defaultFilters() +{ + // TODO: Supported extensions should be in a centralized location + auto simpleExtensions + = QString{"*.mmp *.mpt *.mmpz *.xpf *.xml *.xiz *.sf2 *.sf3 *.pat *.mid *.midi *.rmi *.dll *.lv2"}; +#ifdef LMMS_BUILD_LINUX + simpleExtensions += " *.so"; +#endif + auto audioExtensions = QString{"*.wav *.ogg *.ds *.flac *.spx *.voc *.aif *.aiff *.au *.raw *.wav *.ogg *.ds " + "*.flac *.spx *.voc *.aif *.aiff *.au *.raw"}; + return simpleExtensions + " " + audioExtensions; +} + } // namespace lmms::gui diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f867d86d9..145467ed7 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -118,14 +118,10 @@ MainWindow::MainWindow() : splitter, false, true, confMgr->userProjectsDir(), confMgr->factoryProjectsDir())); - sideBar->appendTab( new FileBrowser( - confMgr->userSamplesDir() + "*" + - confMgr->factorySamplesDir(), - "*", tr( "My Samples" ), - embed::getIconPixmap( "sample_file" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, true, - confMgr->userSamplesDir(), - confMgr->factorySamplesDir())); + sideBar->appendTab( + new FileBrowser(confMgr->userSamplesDir() + "*" + confMgr->factorySamplesDir(), FileItem::defaultFilters(), + tr("My Samples"), embed::getIconPixmap("sample_file").transformed(QTransform().rotate(90)), splitter, false, + true, confMgr->userSamplesDir(), confMgr->factorySamplesDir())); sideBar->appendTab( new FileBrowser( confMgr->userPresetsDir() + "*" + confMgr->factoryPresetsDir(), @@ -135,11 +131,8 @@ MainWindow::MainWindow() : splitter , false, true, confMgr->userPresetsDir(), confMgr->factoryPresetsDir())); - sideBar->appendTab( new FileBrowser( QDir::homePath(), "*", - tr( "My Home" ), - embed::getIconPixmap( "home" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, false ) ); - + sideBar->appendTab(new FileBrowser(QDir::homePath(), FileItem::defaultFilters(), tr("My Home"), + embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false, false)); QStringList root_paths; QString title = tr( "Root directory" ); @@ -161,9 +154,8 @@ MainWindow::MainWindow() : } #endif - sideBar->appendTab( new FileBrowser( root_paths.join( "*" ), "*", title, - embed::getIconPixmap( "computer" ).transformed( QTransform().rotate( 90 ) ), - splitter, dirs_as_items) ); + sideBar->appendTab(new FileBrowser(root_paths.join("*"), FileItem::defaultFilters(), title, + embed::getIconPixmap("computer").transformed(QTransform().rotate(90)), splitter, dirs_as_items)); m_workspace = new QMdiArea(splitter); From 8b2769bb69d3d8e6780f389ae37d8f38f4f934b0 Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 10 Nov 2023 18:45:25 -0500 Subject: [PATCH 031/191] Fix regression involving missed filters in #6962 (#6978) --- src/gui/FileBrowser.cpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 776ac0810..dc16a3bac 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -1456,15 +1456,27 @@ QString FileItem::extension(const QString & file ) QString FileItem::defaultFilters() { - // TODO: Supported extensions should be in a centralized location - auto simpleExtensions - = QString{"*.mmp *.mpt *.mmpz *.xpf *.xml *.xiz *.sf2 *.sf3 *.pat *.mid *.midi *.rmi *.dll *.lv2"}; + const auto projectFilters = QStringList{"*.mmp", "*.mpt", "*.mmpz"}; + const auto presetFilters = QStringList{"*.xpf", "*.xml", "*.xiz", "*.lv2"}; + const auto soundFontFilters = QStringList{"*.sf2", "*.sf3"}; + const auto patchFilters = QStringList{"*.pat"}; + const auto midiFilters = QStringList{"*.mid", "*.midi", "*.rmi"}; + + auto vstPluginFilters = QStringList{"*.dll"}; #ifdef LMMS_BUILD_LINUX - simpleExtensions += " *.so"; + vstPluginFilters.append("*.so"); #endif - auto audioExtensions = QString{"*.wav *.ogg *.ds *.flac *.spx *.voc *.aif *.aiff *.au *.raw *.wav *.ogg *.ds " - "*.flac *.spx *.voc *.aif *.aiff *.au *.raw"}; - return simpleExtensions + " " + audioExtensions; + + auto audioFilters + = QStringList{"*.wav", "*.ogg", "*.ds", "*.flac", "*.spx", "*.voc", "*.aif", "*.aiff", "*.au", "*.raw"}; +#ifdef LMMS_HAVE_SNDFILE_MP3 + audioFilters.append("*.mp3"); +#endif + + const auto extensions = projectFilters + presetFilters + soundFontFilters + patchFilters + midiFilters + + vstPluginFilters + audioFilters; + + return extensions.join(" "); } From c7795217304e2a9b1950d8c050bcda7f62e491e6 Mon Sep 17 00:00:00 2001 From: DanielKauss Date: Sun, 12 Nov 2023 00:09:38 +0100 Subject: [PATCH 032/191] Add slicer plugin (#6857) * extremly basic slicer, note playback and gui works * very simple peak detection working * basic phase vocoder implementation, no effects yet * phase vocoder slight rewrite * pitch shifting works more or less * basic timeshift working * PV timeshift working (no pitch shift) * basic functions work (UI, editing, playback) * slice editor Ui working * fundamental functionality done * Everything basic works fully * cleanup and code guidelines * more file cleanup * Tried fixing multi slice playback (still broken) * remove includes, add license * code factoring issues * more code factoring * fixed multinote playback and bpm check * UI performance improvments + code style * initial UI changes + more code style * threadsafe(maybe) + UI finished * preparing for dinamic timeshifting * dynamic timeshifting start * realtime time scaling (no stereo) * stereo added, very slow * playback performance improvments * Roxas new UI start * fixed cmake * Waveform UI finished * Roxas UI knobs + layout * Spectral flux onset detection * build + PV fixes * clang-format formatting * slice snap + better defaults * windows build fixes * windows build fixes part 2 * Fixed slice bug + Waveform code cleanup * UI button text + reorder + file cleanup * Added knob colors * comments + code cleanup * var names fit convention * PV better windowing * waveform zoom * Minor style fixes. * Initial artistic rebalancing of the plugin artwork. * PV phase ghosting fix * Use base note as keyboard slice start * Good draft of Artwork, renamed bg to artwork * Removed soft glow. * Fixed load crashes + findSlices cleanup * Added sync button * added pitch shifting, sometimes crashes * pitch fixes * MacOs build fixes * use linear interpolation * copyright + div by 0 fixes * Fixed rare crash + name changes + license * review: memcpy, no array, LMMS header, name change * static constexpr added * static vars to public + LMMS guards * remove references in classes * remove constexpr and parent pointer in waveform * std::array for fft * fixed wrong name in style * remove c style casts * use src_process * use note vector for return * Moved PhaseVocoder into core * removed PV from plugin * remove pointers in waveform * clang-format again * Use std:: + review suggestions Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker * More review changes * new signal slot + more review * Fixed pitch shifting * Fixed buffer overflow in PV * Fixed mouse bug + better empty screen * Small editor refactor + improvments * Editor playback visual + small fixes * Roxas UI improvments * initial timeshift removing * Remove timeshift + slice refactor * Removed unused files * Fix export bug * Fix zoom bug * Review changes SakerTooth#2 * Remove most comments * Performance + click to load * update PlaybackState + zerocross snapping * Fix windows build issue * Review + version * Fixed fade out bug * Use cosine interpolation * Apply suggestions from code review Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * More review changes * Renamed files * Full sample only at base note * Fix memory leak Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Style fixes --------- Co-authored-by: Katherine Pratt Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker --- cmake/modules/PluginList.cmake | 1 + data/themes/classic/style.css | 8 + data/themes/default/lcd_19purple.png | Bin 0 -> 9764 bytes data/themes/default/lcd_19purple_dot.png | Bin 0 -> 5403 bytes data/themes/default/style.css | 8 + include/Clipboard.h | 6 +- plugins/SlicerT/CMakeLists.txt | 10 + plugins/SlicerT/SlicerT.cpp | 410 +++++++++++++++++++++ plugins/SlicerT/SlicerT.h | 108 ++++++ plugins/SlicerT/SlicerTView.cpp | 193 ++++++++++ plugins/SlicerT/SlicerTView.h | 85 +++++ plugins/SlicerT/SlicerTWaveform.cpp | 418 ++++++++++++++++++++++ plugins/SlicerT/SlicerTWaveform.h | 125 +++++++ plugins/SlicerT/artwork.png | Bin 0 -> 13791 bytes plugins/SlicerT/copy_midi.png | Bin 0 -> 4249 bytes plugins/SlicerT/logo.png | Bin 0 -> 759 bytes plugins/SlicerT/reset_slices.png | Bin 0 -> 493 bytes plugins/SlicerT/slice_indicator_arrow.png | Bin 0 -> 234 bytes 18 files changed, 1370 insertions(+), 2 deletions(-) create mode 100644 data/themes/default/lcd_19purple.png create mode 100644 data/themes/default/lcd_19purple_dot.png create mode 100644 plugins/SlicerT/CMakeLists.txt create mode 100644 plugins/SlicerT/SlicerT.cpp create mode 100644 plugins/SlicerT/SlicerT.h create mode 100644 plugins/SlicerT/SlicerTView.cpp create mode 100644 plugins/SlicerT/SlicerTView.h create mode 100644 plugins/SlicerT/SlicerTWaveform.cpp create mode 100644 plugins/SlicerT/SlicerTWaveform.h create mode 100644 plugins/SlicerT/artwork.png create mode 100644 plugins/SlicerT/copy_midi.png create mode 100644 plugins/SlicerT/logo.png create mode 100644 plugins/SlicerT/reset_slices.png create mode 100644 plugins/SlicerT/slice_indicator_arrow.png diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index afeca3548..8c444aca2 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -60,6 +60,7 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SlicerT SpectrumAnalyzer StereoEnhancer StereoMatrix diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index c4fe0e153..b378c4b8e 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -899,6 +899,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--SlicerTView lmms--gui--Knob { + color: rgb(162, 128, 226); + qproperty-outerColor: rgb( 162, 128, 226 ); + qproperty-innerRadius: 1; + qproperty-outerRadius: 11; + qproperty-lineWidth: 3; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/data/themes/default/lcd_19purple.png b/data/themes/default/lcd_19purple.png new file mode 100644 index 0000000000000000000000000000000000000000..35ecc4ace0e094e7702bb744acc17df00cf726e5 GIT binary patch literal 9764 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 literal 0 HcmV?d00001 diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 80d56a4bb..323f6d03d 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -943,6 +943,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--SlicerTView lmms--gui--Knob { + color: rgb(162, 128, 226); + qproperty-outerColor: rgb( 162, 128, 226 ); + qproperty-innerRadius: 1; + qproperty-outerRadius: 11; + qproperty-lineWidth: 3; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/include/Clipboard.h b/include/Clipboard.h index c6ae66ee8..cee40b33a 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -25,8 +25,10 @@ #ifndef LMMS_CLIPBOARD_H #define LMMS_CLIPBOARD_H -#include #include +#include + +#include "lmms_export.h" class QMimeData; @@ -44,7 +46,7 @@ namespace lmms::Clipboard bool hasFormat( MimeType mT ); // Helper methods for String data - void copyString( const QString & str, MimeType mT ); + void LMMS_EXPORT copyString(const QString& str, MimeType mT); QString getString( MimeType mT ); // Helper methods for String Pair data diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt new file mode 100644 index 000000000..49a80ca03 --- /dev/null +++ b/plugins/SlicerT/CMakeLists.txt @@ -0,0 +1,10 @@ +INCLUDE(BuildPlugin) + +INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) +LINK_LIBRARIES(${FFTW3F_LIBRARIES}) + +INCLUDE_DIRECTORIES(${SAMPLERATE_INCLUDE_DIRS}) +LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) +LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) + +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTView.cpp SlicerTView.h SlicerTWaveform.cpp SlicerTWaveform.h MOCFILES SlicerT.h SlicerTView.h SlicerTWaveform.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp new file mode 100644 index 000000000..2918265ce --- /dev/null +++ b/plugins/SlicerT/SlicerT.cpp @@ -0,0 +1,410 @@ +/* + * SlicerT.cpp - simple slicer plugin + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlicerT.h" + +#include +#include +#include + +#include "Engine.h" +#include "InstrumentTrack.h" +#include "PathUtil.h" +#include "Song.h" +#include "embed.h" +#include "lmms_constants.h" +#include "plugin_export.h" + +namespace lmms { + +extern "C" { +Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { + LMMS_STRINGIFY(PLUGIN_NAME), + "SlicerT", + QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), + "Daniel Kauss Serna ", + 0x0100, + Plugin::Type::Instrument, + new PluginPixmapLoader("logo"), + nullptr, + nullptr, +}; +} // end extern + +// ################################# SlicerT #################################### + +SlicerT::SlicerT(InstrumentTrack* instrumentTrack) + : Instrument(instrumentTrack, &slicert_plugin_descriptor) + , m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold")) + , m_fadeOutFrames(10.0f, 0.0f, 100.0f, 0.1f, this, tr("FadeOut")) + , m_originalBPM(1, 1, 999, this, tr("Original bpm")) + , m_sliceSnap(this, tr("Slice snap")) + , m_enableSync(false, this, tr("BPM sync")) + , m_originalSample() + , m_parentTrack(instrumentTrack) +{ + m_sliceSnap.addItem("Off"); + m_sliceSnap.addItem("1/1"); + m_sliceSnap.addItem("1/2"); + m_sliceSnap.addItem("1/4"); + m_sliceSnap.addItem("1/8"); + m_sliceSnap.addItem("1/16"); + m_sliceSnap.addItem("1/32"); + m_sliceSnap.setValue(0); +} + +void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) +{ + if (m_originalSample.frames() <= 1) { return; } + + int noteIndex = handle->key() - m_parentTrack->baseNote(); + const fpp_t frames = handle->framesLeftForCurrentPeriod(); + const f_cnt_t offset = handle->noteOffset(); + const int bpm = Engine::getSong()->getTempo(); + const float pitchRatio = 1 / std::exp2(m_parentTrack->pitchModel()->value() / 1200); + + float speedRatio = static_cast(m_originalBPM.value()) / bpm; + if (!m_enableSync.value()) { speedRatio = 1; } + speedRatio *= pitchRatio; + speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()); + + float sliceStart, sliceEnd; + if (noteIndex == 0) // full sample at base note + { + sliceStart = 0; + sliceEnd = 1; + } + else if (noteIndex > 0 && noteIndex < m_slicePoints.size()) + { + noteIndex -= 1; + sliceStart = m_slicePoints[noteIndex]; + sliceEnd = m_slicePoints[noteIndex + 1]; + } + else + { + emit isPlaying(-1, 0, 0); + return; + } + + if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); } + auto playbackState = static_cast(handle->m_pluginData); + + float noteDone = playbackState->noteDone(); + float noteLeft = sliceEnd - noteDone; + + if (noteLeft > 0) + { + int noteFrame = noteDone * m_originalSample.frames(); + + SRC_STATE* resampleState = playbackState->resamplingState(); + SRC_DATA resampleData; + resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); + resampleData.data_out = (workingBuffer + offset)->data(); + resampleData.input_frames = noteLeft * m_originalSample.frames(); + resampleData.output_frames = frames; + resampleData.src_ratio = speedRatio; + + src_process(resampleState, &resampleData); + + float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); + playbackState->setNoteDone(nextNoteDone); + + // exponential fade out, applyRelease() not used since it extends the note length + int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate(); + int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio; + for (int i = 0; i < frames; i++) + { + float fadeValue = static_cast(noteFramesLeft - i) / fadeOutFrames; + fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); + fadeValue = cosinusInterpolate(0, 1, fadeValue); + + workingBuffer[i + offset][0] *= fadeValue; + workingBuffer[i + offset][1] *= fadeValue; + } + + instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); + + emit isPlaying(noteDone, sliceStart, sliceEnd); + } + else { emit isPlaying(-1, 0, 0); } +} + +void SlicerT::deleteNotePluginData(NotePlayHandle* handle) +{ + delete static_cast(handle->m_pluginData); +} + +// uses the spectral flux to determine the change in magnitude +// resources: +// http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf +void SlicerT::findSlices() +{ + if (m_originalSample.frames() <= 1) { return; } + m_slicePoints = {}; + + const int windowSize = 512; + const float minBeatLength = 0.05f; // in seconds, ~ 1/4 length at 220 bpm + + int sampleRate = m_originalSample.sampleRate(); + int minDist = sampleRate * minBeatLength; + + float maxMag = -1; + std::vector singleChannel(m_originalSample.frames(), 0); + for (int i = 0; i < m_originalSample.frames(); i++) + { + singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; + maxMag = std::max(maxMag, singleChannel[i]); + } + + // normalize and find 0 crossings + std::vector zeroCrossings; + float lastValue = 1; + for (int i = 0; i < singleChannel.size(); i++) + { + singleChannel[i] /= maxMag; + if (sign(lastValue) != sign(singleChannel[i])) + { + zeroCrossings.push_back(i); + lastValue = singleChannel[i]; + } + } + + std::vector prevMags(windowSize / 2, 0); + std::vector fftIn(windowSize, 0); + std::array fftOut; + + fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut.data(), FFTW_MEASURE); + + int lastPoint = -minDist - 1; // to always store 0 first + float spectralFlux = 0; + float prevFlux = 1E-10; // small value, no divison by zero + float real, imag, magnitude, diff; + + for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) + { + // fft + std::copy_n(singleChannel.data() + i, windowSize, fftIn.data()); + fftwf_execute(fftPlan); + + // calculate spectral flux in regard to last window + for (int j = 0; j < windowSize / 2; j++) // only use niquistic frequencies + { + real = fftOut[j][0]; + imag = fftOut[j][1]; + magnitude = std::sqrt(real * real + imag * imag); + + // using L2-norm (euclidean distance) + diff = std::sqrt(std::pow(magnitude - prevMags[j], 2)); + spectralFlux += diff; + + prevMags[j] = magnitude; + } + + if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist) + { + m_slicePoints.push_back(i); + lastPoint = i; + if (m_slicePoints.size() > 128) { break; } // no more keys on the keyboard + } + + prevFlux = spectralFlux; + spectralFlux = 1E-10; // again for no divison by zero + } + + m_slicePoints.push_back(m_originalSample.frames()); + + for (float& sliceValue : m_slicePoints) + { + int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); + if (std::abs(sliceValue - closestZeroCrossing) < windowSize) { sliceValue = closestZeroCrossing; } + } + + float beatsPerMin = m_originalBPM.value() / 60.0f; + float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; + int noteSnap = m_sliceSnap.value(); + int sliceLock = samplesPerBeat / std::exp2(noteSnap + 1); + if (noteSnap == 0) { sliceLock = 1; } + for (float& sliceValue : m_slicePoints) + { + sliceValue += sliceLock / 2; + sliceValue -= static_cast(sliceValue) % sliceLock; + } + + m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end()); + + for (float& sliceIndex : m_slicePoints) + { + sliceIndex /= m_originalSample.frames(); + } + + m_slicePoints[0] = 0; + m_slicePoints[m_slicePoints.size() - 1] = 1; + + emit dataChanged(); +} + +// find the bpm of the sample by assuming its in 4/4 time signature , +// and lies in the 100 - 200 bpm range +void SlicerT::findBPM() +{ + if (m_originalSample.frames() <= 1) { return; } + + float sampleRate = m_originalSample.sampleRate(); + float totalFrames = m_originalSample.frames(); + float sampleLength = totalFrames / sampleRate; + + float bpmEstimate = 240.0f / sampleLength; + + while (bpmEstimate < 100) + { + bpmEstimate *= 2; + } + + while (bpmEstimate > 200) + { + bpmEstimate /= 2; + } + + m_originalBPM.setValue(bpmEstimate); + m_originalBPM.setInitValue(bpmEstimate); +} + +std::vector SlicerT::getMidi() +{ + std::vector outputNotes; + + float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); + float outFrames = m_originalSample.frames() * speedRatio; + + float framesPerTick = Engine::framesPerTick(); + float totalTicks = outFrames / framesPerTick; + float lastEnd = 0; + + for (int i = 0; i < m_slicePoints.size() - 1; i++) + { + float sliceStart = lastEnd; + float sliceEnd = totalTicks * m_slicePoints[i + 1]; + + Note sliceNote = Note(); + sliceNote.setKey(i + m_parentTrack->baseNote() + 1); + sliceNote.setPos(sliceStart); + sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 so that the notes allign + outputNotes.push_back(sliceNote); + + lastEnd = sliceEnd; + } + + return outputNotes; +} + +void SlicerT::updateFile(QString file) +{ + m_originalSample.setAudioFile(file); + + findBPM(); + findSlices(); + + emit dataChanged(); +} + +void SlicerT::updateSlices() +{ + findSlices(); +} + +void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) +{ + element.setAttribute("version", "1"); + element.setAttribute("src", m_originalSample.audioFile()); + if (m_originalSample.audioFile().isEmpty()) + { + QString s; + element.setAttribute("sampledata", m_originalSample.toBase64(s)); + } + + element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); + for (int i = 0; i < m_slicePoints.size(); i++) + { + element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]); + } + + m_fadeOutFrames.saveSettings(document, element, "fadeOut"); + m_noteThreshold.saveSettings(document, element, "threshold"); + m_originalBPM.saveSettings(document, element, "origBPM"); + m_enableSync.saveSettings(document, element, "syncEnable"); +} + +void SlicerT::loadSettings(const QDomElement& element) +{ + if (!element.attribute("src").isEmpty()) + { + m_originalSample.setAudioFile(element.attribute("src")); + + QString absolutePath = PathUtil::toAbsolute(m_originalSample.audioFile()); + if (!QFileInfo(absolutePath).exists()) + { + QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile()); + Engine::getSong()->collectError(message); + } + } + else if (!element.attribute("sampledata").isEmpty()) + { + m_originalSample.loadFromBase64(element.attribute("srcdata")); + } + + if (!element.attribute("totalSlices").isEmpty()) + { + int totalSlices = element.attribute("totalSlices").toInt(); + m_slicePoints = {}; + for (int i = 0; i < totalSlices; i++) + { + m_slicePoints.push_back(element.attribute(tr("slice_%1").arg(i)).toFloat()); + } + } + + m_fadeOutFrames.loadSettings(element, "fadeOut"); + m_noteThreshold.loadSettings(element, "threshold"); + m_originalBPM.loadSettings(element, "origBPM"); + m_enableSync.loadSettings(element, "syncEnable"); + + emit dataChanged(); +} + +QString SlicerT::nodeName() const +{ + return slicert_plugin_descriptor.name; +} + +gui::PluginView* SlicerT::instantiateView(QWidget* parent) +{ + return new gui::SlicerTView(this, parent); +} + +extern "C" { +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) +{ + return new SlicerT(static_cast(m)); +} +} // extern +} // namespace lmms diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h new file mode 100644 index 000000000..8671eecd1 --- /dev/null +++ b/plugins/SlicerT/SlicerT.h @@ -0,0 +1,108 @@ +/* + * SlicerT.h - declaration of class SlicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SLICERT_H +#define LMMS_SLICERT_H + +#include +#include +#include + +#include "AutomatableModel.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "Note.h" +#include "SampleBuffer.h" +#include "SlicerTView.h" +#include "lmms_basics.h" + +namespace lmms { + +class PlaybackState +{ +public: + explicit PlaybackState(float startFrame) + : m_currentNoteDone(startFrame) + , m_resamplingState(src_new(SRC_LINEAR, DEFAULT_CHANNELS, nullptr)) + { + if (!m_resamplingState) { throw std::runtime_error{"Failed to create sample rate converter object"}; } + } + ~PlaybackState() noexcept { src_delete(m_resamplingState); } + + float noteDone() const { return m_currentNoteDone; } + void setNoteDone(float newNoteDone) { m_currentNoteDone = newNoteDone; } + + SRC_STATE* resamplingState() const { return m_resamplingState; } + +private: + float m_currentNoteDone; + SRC_STATE* m_resamplingState; +}; + +class SlicerT : public Instrument +{ + Q_OBJECT + +public slots: + void updateFile(QString file); + void updateSlices(); + +signals: + void isPlaying(float current, float start, float end); + +public: + SlicerT(InstrumentTrack* instrumentTrack); + + void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; + void deleteNotePluginData(NotePlayHandle* handle) override; + + void saveSettings(QDomDocument& document, QDomElement& element) override; + void loadSettings(const QDomElement& element) override; + + void findSlices(); + void findBPM(); + + QString nodeName() const override; + gui::PluginView* instantiateView(QWidget* parent) override; + + std::vector getMidi(); + +private: + FloatModel m_noteThreshold; + FloatModel m_fadeOutFrames; + IntModel m_originalBPM; + ComboBoxModel m_sliceSnap; + BoolModel m_enableSync; + + SampleBuffer m_originalSample; + + std::vector m_slicePoints; + + InstrumentTrack* m_parentTrack; + + friend class gui::SlicerTView; + friend class gui::SlicerTWaveform; +}; +} // namespace lmms +#endif // LMMS_SLICERT_H diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp new file mode 100644 index 000000000..833d4b434 --- /dev/null +++ b/plugins/SlicerT/SlicerTView.cpp @@ -0,0 +1,193 @@ +/* + * SlicerTView.cpp - controls the UI for slicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlicerTView.h" + +#include +#include + +#include "Clipboard.h" +#include "DataFile.h" +#include "Engine.h" +#include "InstrumentTrack.h" +#include "SlicerT.h" +#include "Song.h" +#include "StringPairDrag.h" +#include "Track.h" +#include "embed.h" + +namespace lmms { + +namespace gui { + +SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) + : InstrumentViewFixedSize(instrument, parent) + , m_slicerTParent(instrument) +{ + // window settings + setAcceptDrops(true); + setAutoFillBackground(true); + + // render background + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + + m_wf = new SlicerTWaveform(248, 128, instrument, this); + m_wf->move(2, 6); + + m_snapSetting = new ComboBox(this, tr("Slice snap")); + m_snapSetting->setGeometry(185, 200, 55, ComboBox::DEFAULT_HEIGHT); + m_snapSetting->setToolTip(tr("Set slice snapping for detection")); + m_snapSetting->setModel(&m_slicerTParent->m_sliceSnap); + + m_syncToggle = new LedCheckBox("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green); + m_syncToggle->move(135, 187); + m_syncToggle->setToolTip(tr("Enable BPM sync")); + m_syncToggle->setModel(&m_slicerTParent->m_enableSync); + + m_bpmBox = new LcdSpinBox(3, "19purple", this); + m_bpmBox->move(130, 201); + m_bpmBox->setToolTip(tr("Original sample BPM")); + m_bpmBox->setModel(&m_slicerTParent->m_originalBPM); + + m_noteThresholdKnob = createStyledKnob(); + m_noteThresholdKnob->move(10, 197); + m_noteThresholdKnob->setToolTip(tr("Threshold used for slicing")); + m_noteThresholdKnob->setModel(&m_slicerTParent->m_noteThreshold); + + m_fadeOutKnob = createStyledKnob(); + m_fadeOutKnob->move(64, 197); + m_fadeOutKnob->setToolTip(tr("Fade Out per note in milliseconds")); + m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames); + + m_midiExportButton = new QPushButton(this); + m_midiExportButton->move(199, 150); + m_midiExportButton->setIcon(PLUGIN_NAME::getIconPixmap("copy_midi")); + m_midiExportButton->setToolTip(tr("Copy midi pattern to clipboard")); + connect(m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); + + m_resetButton = new QPushButton(this); + m_resetButton->move(18, 150); + m_resetButton->setIcon(PLUGIN_NAME::getIconPixmap("reset_slices")); + m_resetButton->setToolTip(tr("Reset Slices")); + connect(m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); +} + +Knob* SlicerTView::createStyledKnob() +{ + Knob* newKnob = new Knob(KnobType::Styled, this); + newKnob->setFixedSize(50, 40); + newKnob->setCenterPointX(24.0); + newKnob->setCenterPointY(15.0); + return newKnob; +} + +// copied from piano roll +void SlicerTView::exportMidi() +{ + using namespace Clipboard; + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + + DataFile dataFile(DataFile::Type::ClipboardData); + QDomElement noteList = dataFile.createElement("note-list"); + dataFile.content().appendChild(noteList); + + auto notes = m_slicerTParent->getMidi(); + if (notes.empty()) { return; } + + TimePos startPos(notes.front().pos().getBar(), 0); + for (Note& note : notes) + { + note.setPos(note.pos(startPos)); + note.saveState(dataFile, noteList); + } + + copyString(dataFile.toString(), MimeType::Default); +} + +void SlicerTView::openFiles() +{ + QString audioFile = m_slicerTParent->m_originalSample.openAudioFile(); + if (audioFile.isEmpty()) { return; } + m_slicerTParent->updateFile(audioFile); +} + +// all the drag stuff is copied from AudioFileProcessor +void SlicerTView::dragEnterEvent(QDragEnterEvent* dee) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + if (dee->mimeData()->hasFormat(mimeType(MimeType::StringPair))) + { + QString txt = dee->mimeData()->data(mimeType(MimeType::StringPair)); + if (txt.section(':', 0, 0) == QString("clip_%1").arg(static_cast(Track::Type::Sample))) + { + dee->acceptProposedAction(); + } + else if (txt.section(':', 0, 0) == "samplefile") { dee->acceptProposedAction(); } + else { dee->ignore(); } + } + else { dee->ignore(); } +} + +void SlicerTView::dropEvent(QDropEvent* de) +{ + QString type = StringPairDrag::decodeKey(de); + QString value = StringPairDrag::decodeValue(de); + if (type == "samplefile") + { + // set m_wf wave file + m_slicerTParent->updateFile(value); + return; + } + else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) + { + DataFile dataFile(value.toUtf8()); + m_slicerTParent->updateFile(dataFile.content().firstChild().toElement().attribute("src")); + de->accept(); + return; + } + + de->ignore(); +} + +void SlicerTView::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); + brush.setPen(QColor(255, 255, 255)); + brush.setFont(QFont(brush.font().family(), 7, -1, false)); + + brush.drawText(8, s_topTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Reset")); + brush.drawText(188, s_topTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Midi")); + + brush.drawText(8, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Threshold")); + brush.drawText(63, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Fade Out")); + brush.drawText(127, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("BPM")); + brush.drawText(188, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Snap")); +} + +} // namespace gui +} // namespace lmms diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h new file mode 100644 index 000000000..ea2b979fc --- /dev/null +++ b/plugins/SlicerT/SlicerTView.h @@ -0,0 +1,85 @@ +/* + * SlicerTView.h - declaration of class SlicerTView + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SLICERT_VIEW_H +#define LMMS_GUI_SLICERT_VIEW_H + +#include + +#include "ComboBox.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "Knob.h" +#include "LcdSpinBox.h" +#include "LedCheckBox.h" +#include "PixmapButton.h" +#include "SlicerTWaveform.h" + +namespace lmms { + +class SlicerT; + +namespace gui { + +class SlicerTView : public InstrumentViewFixedSize +{ + Q_OBJECT + +public slots: + void exportMidi(); + void openFiles(); + +public: + SlicerTView(SlicerT* instrument, QWidget* parent); + + static constexpr int s_textBoxHeight = 20; + static constexpr int s_textBoxWidth = 50; + static constexpr int s_topTextY = 170; + static constexpr int s_bottomTextY = 220; + +protected: + virtual void dragEnterEvent(QDragEnterEvent* dee); + virtual void dropEvent(QDropEvent* de); + + virtual void paintEvent(QPaintEvent* pe); + +private: + SlicerT* m_slicerTParent; + + Knob* m_noteThresholdKnob; + Knob* m_fadeOutKnob; + LcdSpinBox* m_bpmBox; + ComboBox* m_snapSetting; + LedCheckBox* m_syncToggle; + + QPushButton* m_resetButton; + QPushButton* m_midiExportButton; + + SlicerTWaveform* m_wf; + + Knob* createStyledKnob(); +}; +} // namespace gui +} // namespace lmms +#endif // LMMS_GUI_SLICERT_VIEW_H diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp new file mode 100644 index 000000000..6685f4f8c --- /dev/null +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -0,0 +1,418 @@ +/* + * SlicerTWaveform.cpp - slice editor for SlicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlicerTWaveform.h" + +#include + +#include "SlicerT.h" +#include "SlicerTView.h" +#include "embed.h" + +namespace lmms { + +namespace gui { + +static QColor s_emptyColor = QColor(0, 0, 0, 0); +static QColor s_waveformColor = QColor(123, 49, 212); +static QColor s_waveformBgColor = QColor(255, 255, 255, 0); +static QColor s_waveformMaskColor = QColor(151, 65, 255); // update this if s_waveformColor changes +static QColor s_waveformInnerColor = QColor(183, 124, 255); + +static QColor s_playColor = QColor(255, 255, 255, 200); +static QColor s_playHighlightColor = QColor(255, 255, 255, 70); + +static QColor s_sliceColor = QColor(218, 193, 255); +static QColor s_sliceShadowColor = QColor(136, 120, 158); +static QColor s_sliceHighlightColor = QColor(255, 255, 255); + +static QColor s_seekerColor = QColor(178, 115, 255); +static QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); +static QColor s_seekerShadowColor = QColor(0, 0, 0, 120); + +SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent) + : QWidget(parent) + , m_width(totalWidth) + , m_height(totalHeight) + , m_seekerWidth(totalWidth - s_seekerHorMargin * 2) + , m_editorHeight(totalHeight - s_seekerHeight - s_middleMargin) + , m_editorWidth(totalWidth) + , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slice_indicator_arrow")) + , m_seeker(QPixmap(m_seekerWidth, s_seekerHeight)) + , m_seekerWaveform(QPixmap(m_seekerWidth, s_seekerHeight)) + , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) + , m_sliceEditor(QPixmap(totalWidth, m_editorHeight)) + , m_emptySampleIcon(embed::getIconPixmap("sample_track")) + , m_slicerTParent(instrument) +{ + setFixedSize(m_width, m_height); + setMouseTracking(true); + + m_seekerWaveform.fill(s_waveformBgColor); + m_editorWaveform.fill(s_waveformBgColor); + + connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying); + connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI); + + m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor); + + m_updateTimer.start(); + updateUI(); +} + +void SlicerTWaveform::drawSeekerWaveform() +{ + m_seekerWaveform.fill(s_waveformBgColor); + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + QPainter brush(&m_seekerWaveform); + brush.setPen(s_waveformColor); + + m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), + 0, m_slicerTParent->m_originalSample.frames()); + + // increase brightness in inner color + QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); + brush.setPen(s_waveformInnerColor); + brush.drawPixmap(0, 0, innerMask); +} + +void SlicerTWaveform::drawSeeker() +{ + m_seeker.fill(s_emptyColor); + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + QPainter brush(&m_seeker); + + brush.setPen(s_sliceColor); + for (float sliceValue : m_slicerTParent->m_slicePoints) + { + float xPos = sliceValue * m_seekerWidth; + brush.drawLine(xPos, 0, xPos, s_seekerHeight); + } + + float seekerStartPosX = m_seekerStart * m_seekerWidth; + float seekerEndPosX = m_seekerEnd * m_seekerWidth; + float seekerMiddleWidth = (m_seekerEnd - m_seekerStart) * m_seekerWidth; + + float noteCurrentPosX = m_noteCurrent * m_seekerWidth; + float noteStartPosX = m_noteStart * m_seekerWidth; + float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth; + + brush.setPen(s_playColor); + brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, s_seekerHeight); + brush.fillRect(noteStartPosX, 0, noteEndPosX, s_seekerHeight, s_playHighlightColor); + + brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight, s_seekerHighlightColor); + + brush.fillRect(0, 0, seekerStartPosX, s_seekerHeight, s_seekerShadowColor); + brush.fillRect(seekerEndPosX - 1, 0, m_seekerWidth, s_seekerHeight, s_seekerShadowColor); + + brush.setPen(QPen(s_seekerColor, 1)); + brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight - 1); // -1 needed +} + +void SlicerTWaveform::drawEditorWaveform() +{ + m_editorWaveform.fill(s_emptyColor); + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + + QPainter brush(&m_editorWaveform); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + + brush.setPen(s_waveformColor); + float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; + m_slicerTParent->m_originalSample.visualize( + brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); + + // increase brightness in inner color + QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); + brush.setPen(s_waveformInnerColor); + brush.drawPixmap(0, 0, innerMask); +} + +void SlicerTWaveform::drawEditor() +{ + m_sliceEditor.fill(s_waveformBgColor); + QPainter brush(&m_sliceEditor); + + // No sample loaded + if (m_slicerTParent->m_originalSample.frames() <= 1) + { + brush.setPen(s_playHighlightColor); + brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); + brush.drawText( + m_editorWidth / 2 - 100, m_editorHeight / 2 - 110, 200, 200, Qt::AlignCenter, tr("Click to load sample")); + int iconOffsetX = m_emptySampleIcon.width() / 2.0f; + int iconOffsetY = m_emptySampleIcon.height() / 2.0f - 13; + brush.drawPixmap(m_editorWidth / 2.0f - iconOffsetX, m_editorHeight / 2.0f - iconOffsetY, m_emptySampleIcon); + return; + } + + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + float numFramesToDraw = endFrame - startFrame; + + // playback state + float noteCurrentPos = (m_noteCurrent - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + float noteStartPos = (m_noteStart - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + float noteLength = (m_noteEnd - m_noteStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + + brush.setPen(s_playHighlightColor); + brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2); + + brush.drawPixmap(0, 0, m_editorWaveform); + + brush.setPen(s_playColor); + brush.drawLine(noteCurrentPos, 0, noteCurrentPos, m_editorHeight); + brush.fillRect(noteStartPos, 0, noteLength, m_editorHeight, s_playHighlightColor); + + brush.setPen(QPen(s_sliceColor, 2)); + + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) + { + float xPos = (m_slicerTParent->m_slicePoints.at(i) - startFrame) / numFramesToDraw * m_editorWidth; + + if (i == m_closestSlice) + { + brush.setPen(QPen(s_sliceHighlightColor, 2)); + brush.drawLine(xPos, 0, xPos, m_editorHeight); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + continue; + } + else + { + brush.setPen(QPen(s_sliceShadowColor, 1)); + brush.drawLine(xPos - 1, 0, xPos - 1, m_editorHeight); + brush.setPen(QPen(s_sliceColor, 1)); + brush.drawLine(xPos, 0, xPos, m_editorHeight); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + } + } +} + +void SlicerTWaveform::isPlaying(float current, float start, float end) +{ + if (!m_updateTimer.hasExpired(s_minMilisPassed)) { return; } + m_noteCurrent = current; + m_noteStart = start; + m_noteEnd = end; + drawSeeker(); + drawEditor(); + update(); + m_updateTimer.restart(); +} + +// this should only be called if one of the waveforms has to update +void SlicerTWaveform::updateUI() +{ + drawSeekerWaveform(); + drawEditorWaveform(); + drawSeeker(); + drawEditor(); + update(); +} + +// updates the closest object and changes the cursor respectivly +void SlicerTWaveform::updateClosest(QMouseEvent* me) +{ + float normalizedClickSeeker = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; + + m_closestObject = UIObjects::Nothing; + m_closestSlice = -1; + + if (me->y() < s_seekerHeight) + { + if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) + { + m_closestObject = UIObjects::SeekerStart; + } + else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) + { + m_closestObject = UIObjects::SeekerEnd; + } + else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) + { + m_closestObject = UIObjects::SeekerMiddle; + } + } + else + { + m_closestSlice = -1; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) + { + float sliceIndex = m_slicerTParent->m_slicePoints.at(i); + float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); + + if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick) + { + m_closestObject = UIObjects::SlicePoint; + m_closestSlice = i; + } + } + } + updateCursor(); + drawSeeker(); + drawEditor(); + update(); +} + +void SlicerTWaveform::updateCursor() +{ + if (m_closestObject == UIObjects::SlicePoint || m_closestObject == UIObjects::SeekerStart + || m_closestObject == UIObjects::SeekerEnd) + { + setCursor(Qt::SizeHorCursor); + } + else if (m_closestObject == UIObjects::SeekerMiddle && m_seekerEnd - m_seekerStart != 1.0f) + { + setCursor(Qt::SizeAllCursor); + } + else { setCursor(Qt::ArrowCursor); } +} + +// handles deletion, reset and middles seeker +void SlicerTWaveform::mousePressEvent(QMouseEvent* me) +{ + switch (me->button()) + { + case Qt::MouseButton::MiddleButton: + m_seekerStart = 0; + m_seekerEnd = 1; + m_zoomLevel = 1; + drawEditorWaveform(); + break; + case Qt::MouseButton::LeftButton: + if (m_slicerTParent->m_originalSample.frames() <= 1) { static_cast(parent())->openFiles(); } + // update seeker middle for correct movement + m_seekerMiddle = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + break; + case Qt::MouseButton::RightButton: + if (m_slicerTParent->m_slicePoints.size() > 2 && m_closestObject == UIObjects::SlicePoint) + { + m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); + } + break; + default:; + } + updateClosest(me); +} + +// sort slices after moving and remove draggable object +void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) +{ + std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); + updateClosest(me); +} + +// this handles dragging and mouse cursor changes +// what is being dragged is determined in mousePressEvent +void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) +{ + // if no button pressed, update closest and cursor + if (me->buttons() == Qt::MouseButton::NoButton) + { + updateClosest(me); + return; + } + + float normalizedClickSeeker = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; + + float distStart = m_seekerStart - m_seekerMiddle; + float distEnd = m_seekerEnd - m_seekerMiddle; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + + switch (m_closestObject) + { + case UIObjects::SeekerStart: + m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - s_minSeekerDistance); + drawEditorWaveform(); + break; + + case UIObjects::SeekerEnd: + m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + s_minSeekerDistance, 1.0f); + drawEditorWaveform(); + break; + + case UIObjects::SeekerMiddle: + m_seekerMiddle = normalizedClickSeeker; + + if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) + { + m_seekerStart = m_seekerMiddle + distStart; + m_seekerEnd = m_seekerMiddle + distEnd; + } + drawEditorWaveform(); + break; + + case UIObjects::SlicePoint: + if (m_closestSlice == -1) { break; } + m_slicerTParent->m_slicePoints.at(m_closestSlice) + = startFrame + normalizedClickEditor * (endFrame - startFrame); + m_slicerTParent->m_slicePoints.at(m_closestSlice) + = std::clamp(m_slicerTParent->m_slicePoints.at(m_closestSlice), 0.0f, 1.0f); + break; + case UIObjects::Nothing: + break; + } + // dont update closest, and update seeker waveform + drawSeeker(); + drawEditor(); + update(); +} + +void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) +{ + if (me->button() != Qt::MouseButton::LeftButton) { return; } + + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; + float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); + + m_slicerTParent->m_slicePoints.insert(m_slicerTParent->m_slicePoints.begin(), slicePosition); + std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); +} + +void SlicerTWaveform::wheelEvent(QWheelEvent* we) +{ + m_zoomLevel += we->angleDelta().y() / 360.0f * s_zoomSensitivity; + m_zoomLevel = std::max(0.0f, m_zoomLevel); + + updateUI(); +} + +void SlicerTWaveform::paintEvent(QPaintEvent* pe) +{ + QPainter p(this); + p.drawPixmap(s_seekerHorMargin, 0, m_seekerWaveform); + p.drawPixmap(s_seekerHorMargin, 0, m_seeker); + p.drawPixmap(0, s_seekerHeight + s_middleMargin, m_sliceEditor); +} +} // namespace gui +} // namespace lmms diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h new file mode 100644 index 000000000..6478e7f86 --- /dev/null +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -0,0 +1,125 @@ +/* + * SlicerTWaveform.h - declaration of class SlicerTWaveform + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SLICERT_WAVEFORM_H +#define LMMS_GUI_SLICERT_WAVEFORM_H + +#include +#include +#include +#include +#include +#include + +#include "Instrument.h" +#include "SampleBuffer.h" + +namespace lmms { + +class SlicerT; + +namespace gui { + +class SlicerTWaveform : public QWidget +{ + Q_OBJECT + +public slots: + void updateUI(); + void isPlaying(float current, float start, float end); + +public: + SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent); + + // predefined sizes + static constexpr int s_seekerHorMargin = 5; + static constexpr int s_seekerHeight = 38; // used to calcualte all vertical sizes + static constexpr int s_middleMargin = 6; + + // interaction behavior values + static constexpr float s_distanceForClick = 0.02f; + static constexpr float s_minSeekerDistance = 0.13f; + static constexpr float s_zoomSensitivity = 0.5f; + static constexpr int s_minMilisPassed = 10; + + enum class UIObjects + { + Nothing, + SeekerStart, + SeekerEnd, + SeekerMiddle, + SlicePoint, + }; + +protected: + void mousePressEvent(QMouseEvent* me) override; + void mouseReleaseEvent(QMouseEvent* me) override; + void mouseMoveEvent(QMouseEvent* me) override; + void mouseDoubleClickEvent(QMouseEvent* me) override; + void wheelEvent(QWheelEvent* we) override; + + void paintEvent(QPaintEvent* pe) override; + +private: + int m_width; + int m_height; + + int m_seekerWidth; + int m_editorHeight; + int m_editorWidth; + + UIObjects m_closestObject; + int m_closestSlice = -1; + + float m_seekerStart = 0; + float m_seekerEnd = 1; + float m_seekerMiddle = 0.5f; + + float m_noteCurrent; + float m_noteStart; + float m_noteEnd; + + float m_zoomLevel = 1.0f; + + QPixmap m_sliceArrow; + QPixmap m_seeker; + QPixmap m_seekerWaveform; + QPixmap m_editorWaveform; + QPixmap m_sliceEditor; + QPixmap m_emptySampleIcon; + + SlicerT* m_slicerTParent; + + QElapsedTimer m_updateTimer; + void drawSeekerWaveform(); + void drawSeeker(); + void drawEditorWaveform(); + void drawEditor(); + + void updateClosest(QMouseEvent* me); + void updateCursor(); +}; +} // namespace gui +} // namespace lmms +#endif // LMMS_GUI_SLICERT_WAVEFORM_H diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png new file mode 100644 index 0000000000000000000000000000000000000000..e166273c705362e4e07b907a853492b138c566d8 GIT binary patch literal 13791 zcmb8Wbx<6^*DkyyAvgplxI4k!-QAtw?y$H!!6En(+(K}-;O;Jq1lPrV;qrUyt6Sec z?|bj9sh&A=rfbgW^E^G%bGj#5MM)Y35g!o%0HDaqNT|IZpZ-(eKfmvzPW1!<0GLo4 zad8z{adA>-S0_swdkX-7HpxFpP^M3WaKKnyl1>wjKgxYrCHJ&|JiZZ8vh0g(xE7JZ zH)seoifl}5;g+8Bg<0+K*1zACp_QcY0YNQoJala`(QopIOr`=n=R@tj3GKk3>ECP< z$FhJd$LvfEwedaefiCT^F9W2XL@a-@(#YC8#8>-%dho$L(0x64CH!dHV=5v-sl?LP z5X~KB#pMusE#wlGW|8)g^?L0yKw7Myb+WYIv=l`FppRYkEMO&iFKer_scRme$m;t1N}T#GFOQ z$&S@SS(zzgL_|_Dm`Bn_`tiE^)A2Z&A&>te^U$&53ZltShf7(b2ZPAVL_Tt%a40G- zgE{WpODo!}s_%y~?}XHI)jcX4!DCONx1G)BRc?11R&RIscdK?Y4By^(BSdQ8|Bi1j zZNai+z*wFI#%){+TNnh3eELF1i`^O3vB_k?1Gp$8@e4%oj2j=yehPTmR-1&>Ri|^x z(q5J;<&;)&M2wF2*D;t z;!)qez|X*b=t-`fssGg&andF~E+s8WAP8}aUDMR~$8O+Zc@lydmMxtB`gBXZ^0&NP-U!m_Iq9cN9NeJUsO?F(Z7p)EvWEH|R^ zAQsc`KMJEdT1JN_G? zVC>8PWaX0}dZ$)pzESnqA^fLUc!m>#nKH$q*GXB6?wE&7$7YL9upyFSS6Qwxn1V`o z0|;zgh4FzYZ@|m$;@*gQ znUfdqQ7Z3mGau`ije4!na}hCe5aQ5&BQ-P3Zo@04$9{40lo?pF$4E7ri9%a&yyazrag){C_cIdEt7JUwmo% z5hhY&e));;erT#Mls)f1b$-%LLN7TB+K@h(79xH6K=uy8e<1j8g#Q_o|HAA40LuTy z>;DFw|JINH?c)EHL*oA<2mV9z|Dwl#-|YW4bN@e5;=tq-uW`zGCp^15d~=2{I;ZF9 z|JFH%?h)@>y{(Gb#S4PCXY6{?lHPGy8ZkP!`?Xr1_(Ie)BooH;RQYS+B>uol+DrC# zbLw;dD3d)_yB?@X#Bh=vKRFuxHaN;vI;02S#!+AZr6|hJiOzi*HJqFXPze$Rt?pH8_~ zab|F4@ahS)KHYh}F|Xr!LvO9(-~6?NLm$%|i_E8{j`M6-rZ@?MYFzj5X*I7&vflpk z7;NAYE!0P_ChvhL=>?A%d(r9*&W&;~0pu3w8^>wP7Mai_WSR58sxCoQ0(c}A8k9IH zQoqv2zUaWDY54L4Mu2x;QEJG^_QzISN*_7;Olz40LX`Al+L(f%lng@I#Wa;tXY_M$k=9gUG%r|GMx;H}8 z!tKW=-t;-NIv-}SJb~=`xIc&++#7i2Ta(`f+?gmKBW=GQPn+%Hl~(CDu}~(x(?OlU z|D)`_81TlhzD#P$VneGo9rNomuL8^nx;3NF$k%XO4yB}vnR?6EtIoA^dqeUq=t~C@ zOAlEh(2n|x9Sf%WML$oXT5RaCf;oyg-;94_N?~4X+bveBNtw(5tS(!;8Jg>Z!O{SA z%jaO-;Iz*v+}s7CcbpXQl)QF-G_v%?3*hmCuGS>Sq3Ort`6?GO#GPWAAo&RefcZEV z7l(*ChlYq(yn^DjH&4mg(ZH1T7r=yhOLc_q9~v;>`gg*NiE0DqQ@kt$i5cyfuP7bE znh%NMSum#7mLfKZ1=G!a03AE)GkN}r{QgP}UxZf$b{SS3IQ*d?8OKC@xYx+!KSVnH zdst^vKkrgm3+3&^=)_MY?#YxZo*B@>!?yqI&iw^D!%iTx;c#(1X&G$z7%9}m%oqLW zE)Xl^GN2gC-!bqV>$=l;sHy7WnS*J4CL7stO@Pi6nZTUQW$Y#9#IAZ1SsEGanA=S* zANy;EB``GDV+e|V#QwG!8+_X27K&J=s_GQ$Q1>-~obc26mNb*x)pqJBh`JQ|2Y(00 zrBg3kz$$8a3DgV8tjLSfrL11PPu%X=&JePmnj@wZ_$Ohdq@>$NO`ue#PCF4>Il9ZA zT6Qysge$)jzp+J7pq6P-R_R(L6I9JZan*s${W46G^ty@S6YpZVbPy8 z5sYea#5hrj!P2GMhc7WoIF_vDuVY4UMo(qsgog5))4mc?HcQt`O36&Iox;{dfB@ND z!|GG?2 zPp|sDOgAK2lui60*$NZHBvv}{*E9?mv+mV0>{xcHA^A)6)||%7eK>}SV4p(gPE6wK zMz~Cu3a&ZCA+Uw5)Zk00FjjI$OPDf{(2Xo@gU)B}JB9C7P~jgcM3`ToJX`t|=!C-B zAGvm1CO~Qcfme zNt@hCr5kbDIgu!r<7!W+5KT0ABPI!X=y@r;OHIEi@mUF!Hv5IxcbaL!tX~*Jhvcd8R@~_d&ba=MmmL?ECLtks=}W zq@z4O72}w;tve#oCu*bQV~6bOy^4SxMyX60q8+E`pf+{s!i~SyW>|%{=o~qG^9B6< zl9}wPec6#cZZ2(wBVsb1h#nIgO>@aT@CGXe$cFv-9QTXHh;V~H1jhTMo8AOoy};b8 z-p6Eszw?+3F4yte9$d1tN{XW!udyB=OayFNAZov+%(H>S?c`9A`XmQcvV&%kO69^Y z5$pMrGJX^uPB?(*iFX5-YxRsv?~Ka;-1`s4YSLB;>x12SCJ#8-vBo^1H1H65tU8UX zDex6#ZJp+Fv_KE%C_1Q=jp*F=6s8<(7uFPT_UKB9t5>v#1sUnV2 zgq5R*BDcVzMj~3cA=0@YHe~-%-!Gsm&e9jvZ9umx-Pwa=a`t{@ski(Ubo$Y{ z!E}X>_-kgrpT2lIt-nYQ-yY`Jx>y!yQgP~QF2%r!ZsJB;WKnn#tC^D!q!wtA;!Fj1I!sS3T8nn$oVT4gzN}+whYkxM z_wlZV-M?SO@!fj}nwZ4agxh9_5uuv$;gc4)Fd35MS)cm?5eG#MG z@gZLQq|O2=7i@@ZW@fhB;zGgYxE>YD+y$R*_*C!p=IeO(rwr>@38LPx;OKjB04L}A z$N6`8P~m()WYr72fu_;@YchCpUJ6@Dq1s}}=sC9K>Dc%bbEKXmzR^xpR+G?XlKss| zYVi1>`R##xlJDgAhC&8eono4r*Um!q(46nxHd0;(J14J&HWYa(7aVvDszKp?Q0fzdJY-4((^#xj35B1VozL7*Fz3>Ew#& z#3uM^x#N*+{kqeam6J1qpQtfwZhjjZKOd^=& zn(KQJ-2KE71+P>EBh6c0BzWF$<}wpIHKbYX%}Gs7Eh5qKG*LZdC3IH}2y$|I{6NSV zV(EtCHc`^Z0(KsFo_3ZJR6l|v=>z3C#+I~_0N5c-KXmT4RXgib&u#VbMs8ym8a0&f zUS+O>Y>i^$g%CoXF&HjNv(^F4T~9Bf+2nCk=wR|g@Ns`+?_jo z_RBRe0Ou8p^eyRohY3o_EG(21?%r|Kl%WA50dOh{Ou6ZsBc1|8zjfs@qD1@YuSr#)$d3xNvxTiPzU3>1ca*4=HWszWce8 zlNHxf-VQJad>^udtaalioO^L-OgS(O`bOlBh**B5XwPGAe-Zm_+%5FteNvb}#w?OK z_bGFdzA=qP3x^_~Kf@}N#wX|02%|3QR9C&-N7=Wu{OYdx4d%S_-e}!xCywX!VW-A+ zp`v%qVJw|_2!(q+D3aur)vks>;EzM`}ol^9Ri_x1PlELQ2`-bH4*Jf9hla_qQ5 zc|}HR@%3hlCVklibfQ7ho;v`b&V22?S=rvM3J`(z?p!b|Tlgu#>uTie;VRt;Zp#5F zZBFc^NI@Y9jtvR{`^=_9_1dKQ9 z2MtTs&n@fJTcS+z?A(MPB@Cu_-B@%yCkX4j8tqEXhA;5`Y?+@7kg_b+Hi9pp8JRrQ zjF8oKd1jcXloPp(3v4H-udl}=Am|%SW9aK+*kfV{A@8lbl%3Z=!8A)LqZ_iW?j5xl z^74FyqHUkL1$+iv^x_&u$yf7`0R686V&^CJb0C*OF@1B=9e~VHb4k_@Y0+PAe7?6cUP%|K?BU*z($T(P;(ZO{{3+j}OH)bjQJ; zq>TcUa)o5MVnk|uAoRJT&8k1^7Z+gjfY0JAxmVtvYYxDNyqs-*01*0~^1TaCPNVBK zr=4Ip;(jpDA5A)5A%D*CGE~@){L-juKMW{v-+O?IA3M}^`u9|UU$F?zhFfjz;vDD? zZ?{x6!2EWV4FkyLa_CL(awQ1ZKG+QFgR^qu2sbx3NaP$EJ1^`dWcdD;r#?%R8)a0I z=*-s|d#>k7e>B`SNa`Ai2}AOF5!p*o+_@Fc-xaYime5moxu?Z*7;Egu`U=$tVs%Xp z2|XeB?TR<|dK?s9nhu^r+xzl1pU=Dd%1d)&j32@)0RRdLik7QU?VV8uW5Yw@2}{hA zcwUOjSE}?54nMxl$H{KownhDoD=xpiJE!7!CbtY6?z9VT!#8Q2c2A~n-7jf?p!z8x zB;%)1E0=}J&73Z;-9x`3_{kC_EhX!v;O%|>eiks>&?kd*$C;j=iul$61UJv*$ zkCX!kxxYT;f6%f3?Ik=C7z2rjy|#ic4v@aJ0F1kK>NAn*A30r`SzyAil2sq7IheLu z|3$IeyMGg%Bu_|HGt$NJNvxR;_#(hk2ju-d&f|W_0`l@B5PC{V*tvInk!QFlvN}?| zxbA+|hYS#GURe6wRsu~9CUY}Dp!3H=I*;5~<|(PMA?CN)wTs;pRrl=yT;s56|DF1y zszQ5dW1h;?gB*3CZP{`v`#RvFj`d?*qW~LQ@FSOayG4KtTmNQtjA?9jn@`#-ZTXF{i+)J;6S^(u0wq?Z ztepJdG}an&%B+JVyb;Iv&YpnmJzwU4jwL1UKkx3|$hCr_LU9y%Y&_1Tf4&I|q6 zhR|SPm(b!XZSQ8Kyhmtm$mPxq{HW~?FCH=RiU6AGW!$#boTb|mk7Mp+oRvV%3^#N5 zV!p&HjJf*|lL03lj~{VAzBjP$Q(?Q~JKGw0S%)lVu~x5HH)cCG#{cnpXb^>tC;CjlKG4DwTd4V|CUecm;%q{$1hcq;GKEZ>*$H` zX+5(I2eXY_Y2U86)hS&L%j8I!5Dm!N4pW;p*QO?>^0lu|1Ck-5F<} zAl0z66#0JGR3l9uBGuK5l?@*9UE!Y{^8_{!Gi;q3Ok@uGA@xd_qWX|x1)T2mP9%2A z{H}(C^*jNdjaUDlrDLNmCt>Uw%fZui4!vup79ZmGPp)x0T(>IThJAemxeQpw3SlpC z*6dg)qH?N!aI2dh?IcSkJ-r#aoSt#k+R!3>@@@_LRpREIg0JAkgWHc&PFibgVI8|m zOria#Ldu2QZF27y)|G{>e`;MR?lk1eOeD8Twn@UQS!dMyR%_N1&D+P`@3u04YoIDn z%F{{{tR~sW>~ls^QlfA%v7Um3)g-xB)3*hrkr7&42YL5jro~_`q%yCQ5#|`&^i$4W zjO74^MNcbdN@aF*K3s&FJwmIpUq@=?CAOrJ)=m2?g8V2|g&{sJkp`u*HYpwH-N+u(Vv_MBdcS{Bztybhapul_> z#-QMtc#vkf_iXmUx)3`hYqfGPo(aP~xrML+mKfv9w#R|3n`#E8kaPQQh9lD?me<`c zdE0y$NIjNPtzt}evm}`w3Lj~hdR9%2rDshWrZ@vy_=O< z;?0detU29%=3xFb^DKJB(GE6RAU~R<I%ml>W+SX-=3sC6_1hk@t)F z>WCIhOw9N-eU3Acq}1_U_EqyH4$=y)L;-MgNtbg)5Y1M^k*gUKN#?%c6V`sy#^26U zA}z-c`fGDD&m3RqK$C+HudR8krEGQ2V|M9EExk%@iKjt}H6ub{Va#)cR@;t-#pER5 z+~FvX{ixHCPl`5l#T3mO(`Bb3|JF^Ku!UOgiqG=kTo4z|^0Q-Q0&!Q|>oBptE*M3` zpI&0xeI)bIGjzvi0aJ_+mf)d|5NnUki?xVq_INWF|b&Y(h7r%)+MX+=6_%3MivA2 zDPhcQv}}}b_)=+8t5D7fo#;DpYKiH2BLl5*mfeK-LDB)LzAZLp?zQ{@V-TB@{60@U zo#m3{8Q*=I)fge)zsKGq%&d5SJ_Qrm^7JF4vN%T)jZf{=mHuVETXo~<&|MldO`JI6V97}UAcb+P7T z;dG(_920doaxWdVDnH)ule=b@Xrg*+}KcpKz598T~%6pf(mfPZShTj{TQkhT+in?`7945W`z;Ri=jPAy%8$i;jlCr`IcGLHS#Jeu9wcV%7Z%bEyyPfa5Y zt%H&U8>SdmwPJIeu^1Y7d1wsK+x_#KH+gpKBqc};tnC_i<7cJn$&{;vK7ELu1EL_# zY#582rY^|(R%B!;8Q!M4Hio<^_#B5`63ShXDfa&dkVoEiU(zR2x%Q-WwpcafQu9Cx zaaR>JRmc`UUWo-$9a$>8naD!(u|IO+Kb;z>9=d3_%23MV60vfL z6HAE%$D64$8)I5}gAuFYb)SPNU7%P#d0Caf0^y=(BoOLU8C`( z@b9gGYFuVyQ6icB+moVR=<4J6&S1P@W5pe*hS!OXZTR=5NiU)D}b+gUG#FRG@cdgR3dN!k#DofGiYKt z&>8nms2%e}+o``zljdLQ&Qy==3HSHCI*$xILzpVVaZalX8WMk^)$0l5HT_bVn+#6k z#B3}LSjy@;9(USdb8ytSKgJU zaY6t_Ij`Z8oy1NJy=uN(&Vwv`=>#TM8`i=EKG@~;TALBpR~0UIzOk-*-w$2(gCh9@ z?!1Tc!YjYE!qEKYCo+q($grE(`k2>>kFQO(r=MMp?EFd&+2oLE{DU)wcaP4 zJZx_LY7=?g+k^&hZTDsiyL{u5iaX73uU1L9IwR{smEp%ed?T^GrIm+E^Y4$GWA!Dc zx~E%Tl2!qWT3QBhrRFyKnOm_dZlV%~;C5Ig6N(M-l46fdvLNk}p+Oz4OMhD8VW)3C zGcP32q=_@U`sIGYYgbnPUv9u?X3I^uaDs=O(%*+=S01I05}$@_O#JM5<8{3mm3S|a zt((LBg`u)#4?@1~%&S#j1>X9vS-JRvOFf+iU;EnUxcL1z!Ode#2LvAL>qo$DgXZXu zkst(Np!2aVN49itfMDxM03lCZv&b{w?Es~(leBDT=UL)9mh4S@lWl|qx4HzKxVX-5 zrIu5g?-CDtvYzyHUbt&Y@+pV(x*9djFkP1@BtdGNrz>wf=iUz|TH?&D7ua5x-qrnw z!(Lr60&KytQ|*Mzfir0l3RZsaP?}7=4&`g0aCcscf2BmDt$NRldT7+PpWw>fpI9+> ztmB_Sg#1bunAr%UwV=i0H`*)a2h+U$R8#Y%h6!rdl|VPDx$^|+UMB2h8XWn&@KJ8{ zD1pe5dw;8A7qn;@jkMgA8XpQNf}IQy{|_g>H!ZsmP{1g4PLm5J2X1-JKO5Xq2S1oe zzFUDxGvl{c8WuqyUSXVzy@8ug#VRq+W<`Z()<(5VGH|Y%U3tSCx_G)2u|U8+9FJA! zh9El~4o6$8CO1VPC+qN?E}1|5baeAO>D*?L;@z(3PuD3`>^M}0kb32IED6NO`17Gc z{(wX27uz2RaoaiU#>wIlH?qSDJXUk^wf25&evqCs;Lh|6lvIu9-aEZ`bx&1Afir2@ zvpc|iZCCKd&V3#su-;-Q;WR*Z0I@CYTx!HdYBV1d;<~OtpsBlTXOLxPy|el(=IK@8 zu}&yQ;|odRN|U=GeYrmRxc0Io4B2#>Y@rpeI$IB}O+v^C+^1UfRVMNJ0C4Wf)W)l5 z`-(N!9w}I9OkHt=TYASJP$H`l><*C(=SI}gz`3(EUy9R~9&nScgKHN{q9>tghbpv6 z9G3}w5lqB1@;}PlS3TXhZ#ghDN3q0ZNUuJo-Mgx>eY8C}FnlKNQ@AcWGeI<4(lqWB z4h_gGt1!-)Wv>4%;WV4emcvU`)aFw6qioEj7%KrWe-_g@Htux0M#s}vU{aWP7MyDi z;kC+Xg;c!=E?S|U0s2rJW=Ep21GCQh~M3^i}Y z?83p!DHs8(v>&1h%@|});!Gh_%G*hrA-u-g@mb$-`HDa^yg$Nu`7rJcYUbF%C0Lmt zM8qru2GtnDs>o->8znfo4lT%X`@*u)caGy7L6m-((`)53uqkXvqK8iLZ@rcp#(zu6}1=pC98tAf{|) z0oh4QG9KBmxBiOZRwnSSeF^km$k~3vShpJw6 zg=@)l8&f~6RCh$`PI^x1|K^7})H!2UBp*hoJC*Zp)B&g9L+oOY+yk{%E{#Mnck)>rE~zdGWXS3}b)ITx!|t-XN>N+icNTO}oE`hted z$CS+fVpjOd)PC+tVw2Xt^WhNpE6AnZm?+n**x_EMn^(7meDkX`HW6F2{Pn<57=yg! z2|UhozvWr3^L63pM%4Hw{3S8AUwq`Xb$)89srhtQ9d9}Au4{L|R~uG+q4$ABU-ZdT z%^#nO;d&k4aRfuO;~t7BJq(APC-&ZJaBiq+g=qJ4xK1}BdP&2Xkx_W_-BkIK*CXCa zBxoa-z2A9J*}n4lXTv_Ns?Cercux;*AQ{WNXJtr;rec(uBusd;7IL&>>F>Cr=JG6G zVdd^Pp_{ zj(3$Umupq-2?w(~8f)uUZQ8$@v-Rcs{)Mr_W~wf&j;#dJRwaGWp076P@3S6yEOz!AcSOiy-!h;+J#@^-rP98r zg141|xShZ|I$V7yeHAKh6ZoqP;n{@NcGVdOqyempp+?PGUop2F+e|CAb(671Im4W# zs8^z5w@Ty{I4tDU@jaOTSkKLKfth13iOuK0{;chaofezi>~(=hbnXqF#E`D`FwM|1(Lw%LT>`m}q$LtSpXert$p3KNr zC!D9n-AQyQF{Hv+S>QHo!Li=h7gb;Q&<2Iy87|NqJ@%6V+Be6$mQsv_7R=(5DbHWSvcptsC;fq!d@QGoY z7LMfYs{LX-UodSVRS9VjWYrV_on7y}D@IGC9a=V3BuN6UU{05xc_xLg)qd-B;iq6Syr=V|=Y*U6-#bjp+f2Kkhv4y+cd22_ zn6W#`pft^l{lwPx+5Jrp8k1)RJ8pt~H}gz2(^{al%pDElJ6o(a?dIrk^`NUA(DvHd zMDX43zg@n+2rzV$(9Ln0YEIpbE2zu~M;O(J8u{3Ua!qRLUMQzX@$^t=)_Obb!C2(p z?r#+ns#yk2G15}h)Akd69$;t56WFmKt!*ge0U!UCm5`?auG#Na`Ql+NyS?cI1YBQf zd2M_{veP0?(DKCz^tnD^S<9&s$x7!3t>Kl^JT=-uQVcht{kNr5?+L~nu|}%0vs9^Q zQ;cmNQ)$a2@_7xW@tX?U+^M`FbfAzo#(KMFQnW#%G=J>)KIBRM(3BD~9%YJQxBw{q zGi|TrO`gPXK9aN&`j9d-}`B#KW03O^=d{ZPN z3Ww;DGp~{N$f(_3F+h74B{`kwdU6sFr;>~T@(DPyHFT};gkeX^B7_*!#93!U5!GEz zW+``ygwA}TjIXMSNBlZ)%e@msYj9#zA#B(_j{zYZW|f3>61ud$wtPeV6D8$=JekkW}0w7 zuv1R%!DC9j;`kTOyb08t6D*wYodI!W&jTNo9TjHwUn#&g^ew(wMb`Wd*MJn$8>_L)4IDhUAdxD z4p!TxoG8JDMjD(YFmW+Qu*@0971C#67*^RS@=8UX9aChTG%rgmb< z+VRabT-o)L7eLFe&ZA5P9p^@SSy30Csygj!^*&A`Qwt0ETN8O|+UMPBqq*Aym$%NY z4%)7(q8c}{6%G_i^?5O)O{+`t2GCM7fP@%~CEtse$SzD;p}dZLj1%6C89$ zMqDh|=cN@pz51E-->VYxcP4yz#MXY=xi&-=W#yp!lu!v-$e?m5Ea#U;&pq>?w|xCn zga!|>y9M}u5UJ;8ZoEoRIoT%jn+c^`I6)aXoBqJXxAKQL&-Wizw^mr*>28^7Stk1fgM*XIDWQZ5qkDKnWa03`U_?NqN1uy^7Ur#iP>nI=0TnHB#gI zVq_CSy@$E68Gk)BcdzuYy(}ajYdy^Tz9K|InM2PTx8U!^ckx{2{R)C}1=f)vYX}lr zO0nj*t4`bOj`4ZL$4zaWv2v8{UDTJbkt(V zml=~kkIKrpiRZHIeh(q0F%<)S#e;5gOgB8OHYTx}ZJor`M^c)btf3d9qykA-_uFwf zr@@wbB6OphLi*NrIdgrcbkNEauDmFRgmP|bMA8VZdGC15pIc$8cq==7Bv^ePlY#}? zcYCe+=%zgLJy}ha(}F~49W%qMBGi&llvSq9HHfdepK_+i)F{UjZIc=28RG)TDZFI_ zSm+j{XxL7)hP2xc-PvGg!S{n7o31Hrqv{BqM`>Q2aeL#}lly4KpyWuYPO;4ESRB-f zDl$prw}M%b>v_eAAxvI*7d<}-(98DMF0%gq-e!BuwBc=TK9bE2~An(Y+l=n#OQ4e1c`|GJSRz|5ZQ8Qz67&U>gr;W0$CIK?TYU}$TDPBWK z_T&4#%%qW;?7y(kGN(2)H!LlJocwzvX%vG;SpG1-2F6Ie&y4(b7YgA#LB?p>xO=NJfG5mS3J(X;(YnoDADV8GTU=c>2F}8># z54vP;G-!TV?0LDi4uTTN&sNT}?79*|{KY1}eBk;RLE%JpAQ(7jaF2!ngA6yFktwu$ zA~cUJyIfHloS`0t0~2gwM=rjh6fd~DtwNJhpqcc6t~yoS9moH#bYhkJtm@>`uMFDf ze*(GZ!gcSGn19i36-T&jgyZ6gem>+=H?DQ??QuioQ1EmZw7gK8Q26>JD{u>a b`#58@uaXJwm3{xw2Ouk{B=JYgB;{tc3^jBc4pdc zNl>{IOd&u(C4w;o!zDyef(R+$iWiCzFH96L@g|^xQUQZ+X1CjIBr#3ozs}~{dGo&a z{oZ@O?|pA~metl&78Q;x#4xNVR28g)`!H)14219H%SIQ$?PJssl0%{KSOGkMrQ;UN zVcEcN2bX1Q7qg9X{b-UdT? zHtnkuWa}!hKV2Z@!|1+OhQhTt>ttxB%MI&k*2Ob4Pct}6^ByPop{%f$9@|9)Q&;NpIrUTkIlQrHff{Ydh2}i`(w0yc^&={yG!=h zt$ew*;-2zUw*&8DgG6xG(&=4^90RXs{EKA(@G zS&C&zupo{0gej!RgfZ5FNMi(%A?k`|Dry3^FolTPVg?8T=JD?M;#xSIg-;k67Jv^b zC1@1mq^WqE%3WcY<*fjc3Fw9^jQVyBQFX{rTXYeXx1xkOHW$J=L0JSVrKl3u?4*Fa zUd|*jYp1p7G22EGDHKC-05u>!rkA~`$US8Bip`p_)5#44+-325S$F4d8-rCi%m-Dm z#c~=71_&!&UQ$Ix;%!l6Bu4Of+@y>4BGN5Qa*;m7a3rW1#QMBUgky7{LJ7kZ5+bso z0Nkknj_h^O5n1w*5!&q`-KcIFwUTH_^_bQx zaWumeEYcV+WW{;i9+vg`Je;q`XeQDPC^!q#ZjttkB=Qx2C_v#TaUqH*EfKXRtZm^T z86Z}$>J*S|J>LBT}eE!&6dyZi72n&s(qUKU(f zl@z)CzpSmpgZsfAw!BI)z<;}~N}s41D4BjrKgJaMDB-w$D0o3kr(g)JNV4Mutn`%F zEF_``x<{s5yY28T+Y!apJcPa+(aR zOY5L{K`*4T%`n3m#CtQZ%wF6~7dYPUq)+_z)74K`pBU(qaesC7)72*i`efW+UH=-=vn}#Q!zGL0R>u_m_>WGwRIx7DAvp53xw r>{nA8__7-H%cB)Xw1VN>00(y4=6i=EUwv*JAYh^Ln&8f9O$+}4hIpt@ literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/logo.png b/plugins/SlicerT/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c4fabf14facdba53038995a70700bd9a14ad06 GIT binary patch literal 759 zcmVe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Li0L_t(&-tC%AXcJKsM$g1( zO9)OEDF{*!@h3D0y3#C^P!MVnbfa{kM07J11ee)}h%SO|G*S&!Qlp?kMT=Teiedz@ zRTo975~bCGDYOPFQwfc2(sA<|CJka2NjtC1x4iS1`3`sPeRqbZghMe_f}8-M@bQwM zs;awyAP@k&7V(+_l0aP7^#SvRa43dj;c8V?uLJ#nW+4cK7Y*na9!um{0Fe-jtu9S? zEe8>l%@UPGvpF*kQ%z_R*0?6T7c&x+xM2m&AOVR%0%j1e#30_Xt|hZ<5KE3I%eoo( zPU7Es00pHaUYN^suDhkOAgwJLr-ElH3h?dw7uv72vvc$QG6-p2t)IVs5b3x`lkW(t z98NiFWIC7SO6LW3Z#+uPYL}eFUMS@0?253(wHMXpk+;|%Kf6!giv;ePH~(F07;}{e z>3j8v(Xru0-LP@f4o)0CT@uUvnPzzO6CdBE$z(^!WJi{#{Z39zFz|k2Q8!%YSzi|W z{B?-Cce>cqc!;13D;vZHu|aGQ8^i{&L2M8k#G0zwf33lE4}-vmXaVUnmzyy0|Ltik~Q2Lw1U7OI!%|$-36MPWU}Jl{r2D8nGNcVCS^vY zR2opt6B1`vfjwYd6X02M5Ju+Jn;VCbi2bm0Rr3rBXxy3?Eqci3OE8x(UE%!`vCUC$ULOu@9aXIWx@Pt z11njA|H`{S4d@ra5@%!JEKm23+|qGW;0%4uI8XN-`lYdSlwR0A&@YWYMdIvc;;baj zK4t-*bnN`Y(1DJ_EZ`k51-gl|?LWBN0Cs@yOrZAW2@n9cMbO0A7I4q3N>0Pbyv?=1 z|GqL>ug{|OzJN7#b*N^BVPv-b&4ToQsL&p>uBqjioC4>-Wf+;yLZAmue_s{gMe{n0 j%zM_lT;Q@%dSU+nYpk5`^k80=00000NkvXXu0mjf#CFZh literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/slice_indicator_arrow.png b/plugins/SlicerT/slice_indicator_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1f60fc3bfcc85a43cc42cb0137d9f5da04b3e999 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^96-#CCT&G`<3$!H1Ou^vFmQ^Y-C8V`j#&Dcq033 zLlwSz_YV|2yD#LX#>U3hHaC8^bP0l+XkKOgTya literal 0 HcmV?d00001 From 379acb970bd6e533f84326c712c921765cc44a39 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sat, 21 Oct 2023 17:04:05 +0200 Subject: [PATCH 033/191] Lv2Proc: Fix shutdown routine (#6944) --- include/Lv2Options.h | 2 ++ include/Lv2Proc.h | 2 +- src/core/lv2/Lv2Features.cpp | 7 ++++++- src/core/lv2/Lv2Options.cpp | 10 ++++++++++ src/core/lv2/Lv2Proc.cpp | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/include/Lv2Options.h b/include/Lv2Options.h index ca4fe2b7f..603cdda43 100644 --- a/include/Lv2Options.h +++ b/include/Lv2Options.h @@ -84,6 +84,8 @@ public: return m_options.data(); } + void clear(); + private: //! Initialize an option internally void initOption(LV2_URID key, diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 76fa5eec2..7cfa5f7a8 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -175,7 +175,7 @@ private: bool m_valid = true; const LilvPlugin* m_plugin; - LilvInstance* m_instance; + LilvInstance* m_instance = nullptr; Lv2Features m_features; // options diff --git a/src/core/lv2/Lv2Features.cpp b/src/core/lv2/Lv2Features.cpp index c8fc05465..25ee11544 100644 --- a/src/core/lv2/Lv2Features.cpp +++ b/src/core/lv2/Lv2Features.cpp @@ -109,7 +109,12 @@ void *&Lv2Features::operator[](const char *featName) void Lv2Features::clear() { - m_featureByUri.clear(); + m_features.clear(); + for (auto& [uri, feature] : m_featureByUri) + { + (void) uri; + feature = nullptr; + } } diff --git a/src/core/lv2/Lv2Options.cpp b/src/core/lv2/Lv2Options.cpp index 36281ee63..7b4528cb6 100644 --- a/src/core/lv2/Lv2Options.cpp +++ b/src/core/lv2/Lv2Options.cpp @@ -94,6 +94,16 @@ void Lv2Options::initOption(LV2_URID key, uint32_t size, LV2_URID type, } + + +void Lv2Options::clear() +{ + m_options.clear(); + m_optionValues.clear(); + m_optionByUrid.clear(); +} + + } // namespace lmms #endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index bd89cea0a..1dfb0e099 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -477,6 +477,7 @@ void Lv2Proc::shutdownPlugin() m_instance = nullptr; m_features.clear(); + m_options.clear(); } m_valid = true; } From 5c37aa2e2ea1237fa4be2e6710924b5efca130af Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sat, 21 Oct 2023 17:04:40 +0200 Subject: [PATCH 034/191] Fixes #6786: Lv2Proc: Fix losing automation on export (#6944) This PR is a about reloading an `Lv2Proc`, e.g. in case of a sample rate change. Prior to this PR, #6419 handled this by first saving the models into XML, then destroying and re-initializing the whole `Lv2Proc` and finally reloading the saved XML. However, #6786 shows that the automation is not properly restored in such a case. This PR thus attempts to not destroy the automatable models, just everything else. This is done by moving `Lv2Proc::createPorts` into the CTOR before calling `Lv2Proc::initPlugin`, which makes `initPlugin()` and `shutdownPlugin()` proper inverses of each other (note that in jalv, the ports are also created before the features are). The new class `Lv2ProcSuspender` adds an RAII interface for reloading the `Lv2Proc`. Note that another, possibly more clean approach would be to separate the features and the plugin from the models ("controls"), to then only destroy the features and the plugin. This could be done by having `Lv2Effect` contain an `Lv2Proc` and `Lv2FxControls` contain an `Lv2ProcControls`. Then the effect classes are the usual way round, and you still maintain the separation between processor and controls in the core LV2 code. (Similarly for the instrument, except we don't have a processor/control split for instruments, so an instance of each class would be contained within the same instrument instance.) - Thanks for this proposal to @DomClark . --- include/Lv2Proc.h | 1 + include/NoCopyNoMove.h | 47 ++++++++++++++++++++++++++++++++++++++++ src/core/lv2/Lv2Proc.cpp | 42 ++++++++++++++++++++--------------- 3 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 include/NoCopyNoMove.h diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 7cfa5f7a8..1259aeede 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -66,6 +66,7 @@ namespace Lv2Ports //! For Mono effects, 1 Lv2ControlBase references 2 Lv2Proc. class Lv2Proc : public LinkedModelGroup { + friend class Lv2ProcSuspender; public: static Plugin::Type check(const LilvPlugin* plugin, std::vector &issues); diff --git a/include/NoCopyNoMove.h b/include/NoCopyNoMove.h new file mode 100644 index 000000000..d59ddee83 --- /dev/null +++ b/include/NoCopyNoMove.h @@ -0,0 +1,47 @@ +/* + * NoCopyNoMove.h - NoCopyNoMove class + * + * Copyright (c) 2023-2023 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_NOCOPYNOMOVE_H +#define LMMS_NOCOPYNOMOVE_H + +namespace lmms +{ + +/** + * Inherit this class to make your class non-copyable and non-movable + */ +class NoCopyNoMove +{ +protected: + NoCopyNoMove() = default; + NoCopyNoMove(const NoCopyNoMove& other) = delete; + NoCopyNoMove& operator=(const NoCopyNoMove& other) = delete; + NoCopyNoMove(NoCopyNoMove&& other) = delete; + NoCopyNoMove& operator=(NoCopyNoMove&& other) = delete; +}; + +} // namespace lmms + +#endif // LMMS_NOCOPYNOMOVE_H + diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 1dfb0e099..158196fdb 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -45,6 +45,7 @@ #include "Lv2Evbuf.h" #include "MidiEvent.h" #include "MidiEventToByteSeq.h" +#include "NoCopyNoMove.h" namespace lmms @@ -168,6 +169,27 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin, +class Lv2ProcSuspender : NoCopyNoMove +{ +public: + Lv2ProcSuspender(Lv2Proc* proc) + : m_proc(proc) + , m_wasActive(proc->m_instance) + { + if (m_wasActive) { m_proc->shutdownPlugin(); } + } + ~Lv2ProcSuspender() + { + if (m_wasActive) { m_proc->initPlugin(); } + } +private: + Lv2Proc* const m_proc; + const bool m_wasActive; +}; + + + + Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : LinkedModelGroup(parent), m_plugin(plugin), @@ -175,6 +197,7 @@ Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : m_midiInputBuf(m_maxMidiInputEvents), m_midiInputReader(m_midiInputBuf) { + createPorts(); initPlugin(); } @@ -186,22 +209,7 @@ Lv2Proc::~Lv2Proc() { shutdownPlugin(); } -void Lv2Proc::reload() -{ - // save controls, which we want to keep - QDomDocument doc; - QDomElement controls = doc.createElement("controls"); - saveValues(doc, controls); - // backup construction variables - const LilvPlugin* plugin = m_plugin; - Model* parent = Model::parentModel(); - // destroy everything using RAII ... - this->~Lv2Proc(); - // ... and reuse it ("placement new") - new (this) Lv2Proc(plugin, parent); - // reload the controls - loadValues(controls); -} +void Lv2Proc::reload() { Lv2ProcSuspender(this); } @@ -434,8 +442,6 @@ void Lv2Proc::initPlugin() initPluginSpecificFeatures(); m_features.createFeatureVectors(); - createPorts(); - m_instance = lilv_plugin_instantiate(m_plugin, Engine::audioEngine()->processingSampleRate(), m_features.featurePointers()); From 652b1fa57a82d9f9288e46e1d47bb4f9920fbd09 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> Date: Sun, 12 Nov 2023 02:21:51 +0100 Subject: [PATCH 035/191] SubWindow: Increase to respect child's sizeHint (#6956) Before this commit, on creation, `SubWindow` gets resized to exactly the children's `sizeHint`. This makes the child too small, since the `SubWindow` already contains a title bar and borders. With this commit, the `SubWindow` is calculated such that after rendering, the child window gets exactly the `size` that its `sizeHint` has previously suggested. Most of LMMS widgets are not resizable, but the Lv2 help window is a good example to test this out. The help windows now in most cases contain enough space to fit the help text. In some cases, it still does not fit, though debug prints show that the `size` matches the `sizeHint`. --- include/SubWindow.h | 2 ++ src/gui/MainWindow.cpp | 10 +++++++++- src/gui/SubWindow.cpp | 32 +++++++++++++++++++++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/include/SubWindow.h b/include/SubWindow.h index fdda6de42..d1cc6a7af 100644 --- a/include/SubWindow.h +++ b/include/SubWindow.h @@ -68,6 +68,8 @@ public: void setActiveColor( const QBrush & b ); void setTextShadowColor( const QColor &c ); void setBorderColor( const QColor &c ); + int titleBarHeight() const; + int frameWidth() const; protected: // hook the QWidget move/resize events to update the tracked geometry diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 145467ed7..6acaa4b86 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -562,13 +562,21 @@ void MainWindow::addSpacingToToolBar( int _size ) 7, _size ); } + + + SubWindow* MainWindow::addWindowedWidget(QWidget *w, Qt::WindowFlags windowFlags) { // wrap the widget in our own *custom* window that patches some errors in QMdiSubWindow auto win = new SubWindow(m_workspace->viewport(), windowFlags); win->setAttribute(Qt::WA_DeleteOnClose); win->setWidget(w); - if (w && w->sizeHint().isValid()) {win->resize(w->sizeHint());} + if (w && w->sizeHint().isValid()) { + auto titleBarHeight = win->titleBarHeight(); + auto frameWidth = win->frameWidth(); + QSize delta(2* frameWidth, titleBarHeight + frameWidth); + win->resize(delta + w->sizeHint()); + } m_workspace->addSubWindow(win); return win; } diff --git a/src/gui/SubWindow.cpp b/src/gui/SubWindow.cpp index 78e4f586c..dc6e49297 100644 --- a/src/gui/SubWindow.cpp +++ b/src/gui/SubWindow.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "embed.h" @@ -41,11 +42,11 @@ namespace lmms::gui { -SubWindow::SubWindow( QWidget *parent, Qt::WindowFlags windowFlags ) : - QMdiSubWindow( parent, windowFlags ), - m_buttonSize( 17, 17 ), - m_titleBarHeight( 24 ), - m_hasFocus( false ) +SubWindow::SubWindow(QWidget *parent, Qt::WindowFlags windowFlags) : + QMdiSubWindow(parent, windowFlags), + m_buttonSize(17, 17), + m_titleBarHeight(titleBarHeight()), + m_hasFocus(false) { // initialize the tracked geometry to whatever Qt thinks the normal geometry currently is. // this should always work, since QMdiSubWindows will not start as maximized @@ -240,6 +241,27 @@ void SubWindow::setBorderColor( const QColor &c ) + +int SubWindow::titleBarHeight() const +{ + QStyleOptionTitleBar so; + so.titleBarState = Qt::WindowActive; // kThemeStateActiv + so.titleBarFlags = Qt::Window; + return style()->pixelMetric(QStyle::PM_TitleBarHeight, &so, this); +} + + + + +int SubWindow::frameWidth() const +{ + QStyleOptionFrame so; + return style()->pixelMetric(QStyle::PM_MdiSubWindowFrameWidth, &so, this); +} + + + + /** * @brief SubWindow::moveEvent * From ecc5ff8ca7b826ab22e4b68c456129782ef0eee2 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> Date: Sun, 12 Nov 2023 02:25:26 +0100 Subject: [PATCH 036/191] Fixes #6753: Lv2 help window issues (#6957) This fixes at least three issues: * Help window is not synched with window manager's `X`-button * Help window is not being closed on track destruction or on closing the plugin window * Trims help window strings and force-adds a newline, because `QLabel::sizeHint` sometimes computes too small values. Now, together with #6956, all help windows fit their strings. Some help windows are too large by one line, but this seems better than forcing the user to resize them if they are too small by one line. --- include/Lv2ViewBase.h | 21 +++++++++++- plugins/Lv2Effect/Lv2FxControlDialog.cpp | 9 +++++ plugins/Lv2Effect/Lv2FxControlDialog.h | 1 + plugins/Lv2Instrument/Lv2Instrument.cpp | 9 +++++ plugins/Lv2Instrument/Lv2Instrument.h | 1 + src/gui/Lv2ViewBase.cpp | 43 ++++++++++++++++++++++-- 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/include/Lv2ViewBase.h b/include/Lv2ViewBase.h index 3c8f1bc3f..43086849c 100644 --- a/include/Lv2ViewBase.h +++ b/include/Lv2ViewBase.h @@ -37,7 +37,7 @@ class QPushButton; class QMdiSubWindow; - +class QLabel; namespace lmms { @@ -64,9 +64,25 @@ private: }; + + +class HelpWindowEventFilter : public QObject +{ + Q_OBJECT + class Lv2ViewBase* const m_viewBase; +protected: + bool eventFilter(QObject* obj, QEvent* event) override; +public: + HelpWindowEventFilter(class Lv2ViewBase* viewBase); +}; + + + + //! Base class for view for one Lv2 plugin class LMMS_EXPORT Lv2ViewBase : public LinkedModelGroupsView { + friend class HelpWindowEventFilter; protected: //! @param pluginWidget A child class which inherits QWidget Lv2ViewBase(class QWidget *pluginWidget, Lv2ControlBase *ctrlBase); @@ -79,6 +95,7 @@ protected: void toggleUI(); void toggleHelp(bool visible); + void closeHelpWindow(); // to be called by child virtuals //! Reconnect models if model changed @@ -94,12 +111,14 @@ private: static AutoLilvNode uri(const char *uriStr); LinkedModelGroupView* getGroupView() override { return m_procView; } + void onHelpWindowClosed(); Lv2ViewProc* m_procView; //! Numbers of controls per row; must be multiple of 2 for mono effects const int m_colNum = 6; QMdiSubWindow* m_helpWindow = nullptr; + HelpWindowEventFilter m_helpWindowEventFilter; }; diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.cpp b/plugins/Lv2Effect/Lv2FxControlDialog.cpp index 5265cb181..73890937c 100644 --- a/plugins/Lv2Effect/Lv2FxControlDialog.cpp +++ b/plugins/Lv2Effect/Lv2FxControlDialog.cpp @@ -72,4 +72,13 @@ void Lv2FxControlDialog::modelChanged() } + + +void Lv2FxControlDialog::hideEvent(QHideEvent *event) +{ + closeHelpWindow(); + QWidget::hideEvent(event); +} + + } // namespace lmms::gui diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.h b/plugins/Lv2Effect/Lv2FxControlDialog.h index 45c14c2c0..f38c0364b 100644 --- a/plugins/Lv2Effect/Lv2FxControlDialog.h +++ b/plugins/Lv2Effect/Lv2FxControlDialog.h @@ -46,6 +46,7 @@ public: private: Lv2FxControls *lv2Controls(); void modelChanged() final; + void hideEvent(QHideEvent *event) override; }; diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 32f81d23c..841b8a89a 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -295,6 +295,15 @@ void Lv2InsView::dropEvent(QDropEvent *_de) +void Lv2InsView::hideEvent(QHideEvent *event) +{ + closeHelpWindow(); + QWidget::hideEvent(event); +} + + + + void Lv2InsView::modelChanged() { Lv2ViewBase::modelChanged(castModel()); diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h index 2cd73632d..5e255e0df 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.h +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -124,6 +124,7 @@ public: protected: void dragEnterEvent(QDragEnterEvent *_dee) override; void dropEvent(QDropEvent *_de) override; + void hideEvent(QHideEvent* event) override; private: void modelChanged() override; diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 830a994c8..77268bb9b 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -137,7 +137,8 @@ AutoLilvNode Lv2ViewProc::uri(const char *uriStr) -Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) +Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) : + m_helpWindowEventFilter(this) { auto grid = new QGridLayout(meAsWidget); @@ -172,7 +173,7 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) LILV_FOREACH(nodes, itr, props.get()) { const LilvNode* node = lilv_nodes_get(props.get(), itr); - auto infoLabel = new QLabel(lilv_node_as_string(node)); + auto infoLabel = new QLabel(QString(lilv_node_as_string(node)).trimmed() + "\n"); infoLabel->setWordWrap(true); infoLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); @@ -181,8 +182,9 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) btnBox->addWidget(m_helpButton); m_helpWindow = getGUI()->mainWindow()->addWindowedWidget(infoLabel); - m_helpWindow->setSizePolicy(QSizePolicy::Minimum, + m_helpWindow->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_helpWindow->installEventFilter(&m_helpWindowEventFilter); m_helpWindow->setAttribute(Qt::WA_DeleteOnClose, false); m_helpWindow->hide(); @@ -203,6 +205,7 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) Lv2ViewBase::~Lv2ViewBase() { + closeHelpWindow(); // TODO: hide UI if required } @@ -228,6 +231,14 @@ void Lv2ViewBase::toggleHelp(bool visible) +void Lv2ViewBase::closeHelpWindow() +{ + if (m_helpWindow) { m_helpWindow->close(); } +} + + + + void Lv2ViewBase::modelChanged(Lv2ControlBase *ctrlBase) { // reconnect models @@ -248,6 +259,32 @@ AutoLilvNode Lv2ViewBase::uri(const char *uriStr) } + + +void Lv2ViewBase::onHelpWindowClosed() +{ + m_helpButton->setChecked(true); +} + + + + +HelpWindowEventFilter::HelpWindowEventFilter(Lv2ViewBase* viewBase) : + m_viewBase(viewBase) {} + + + + +bool HelpWindowEventFilter::eventFilter(QObject* , QEvent* event) +{ + if (event->type() == QEvent::Close) { + m_viewBase->m_helpButton->setChecked(false); + return true; + } + return false; +} + + } // namespace lmms::gui #endif // LMMS_HAVE_LV2 From 609c008a710b1f4eee0433c5dc90c43589496b5b Mon Sep 17 00:00:00 2001 From: Bernhard <93736385+spechtstatt@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:37:28 +0100 Subject: [PATCH 037/191] Keep master bus color on FX Mixer when switching project Fixes #6398 --- include/Mixer.h | 9 ++------- include/Track.h | 8 ++++---- src/core/Mixer.cpp | 7 +++---- src/core/Track.cpp | 14 ++++++-------- src/gui/MixerLine.cpp | 8 ++++---- 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/include/Mixer.h b/include/Mixer.h index 35787a414..6e5a12786 100644 --- a/include/Mixer.h +++ b/include/Mixer.h @@ -31,7 +31,7 @@ #include "ThreadableJob.h" #include - +#include #include namespace lmms @@ -76,18 +76,13 @@ class MixerChannel : public ThreadableJob bool requiresProcessing() const override { return true; } void unmuteForSolo(); - void setColor (QColor newColor) { m_color = newColor; - m_hasColor = true; } - // TODO C++17 and above: use std::optional instead - QColor m_color; - bool m_hasColor; + std::optional m_color; - std::atomic_int m_dependenciesMet; void incrementDeps(); void processed(); diff --git a/include/Track.h b/include/Track.h index 33d1ad233..71c6b0457 100644 --- a/include/Track.h +++ b/include/Track.h @@ -32,6 +32,7 @@ #include "AutomatableModel.h" #include "JournallingObject.h" #include "lmms_basics.h" +#include namespace lmms @@ -191,11 +192,11 @@ public: QColor color() { - return m_color; + return m_color.value(); } bool useColor() { - return m_hasColor; + return m_color.has_value(); } bool isMutedBeforeSolo() const @@ -241,8 +242,7 @@ private: QMutex m_processingLock; - QColor m_color; - bool m_hasColor; + std::optional m_color; friend class gui::TrackView; diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index 59c2dd72e..31212313d 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -72,7 +72,6 @@ MixerChannel::MixerChannel( int idx, Model * _parent ) : m_lock(), m_channelIndex( idx ), m_queued( false ), - m_hasColor( false ), m_dependenciesMet(0) { BufferManager::clear( m_buffer, Engine::audioEngine()->framesPerPeriod() ); @@ -722,6 +721,7 @@ void Mixer::clearChannel(mix_ch_t index) ch->m_volumeModel.setDisplayName( ch->m_name + ">" + tr( "Volume" ) ); ch->m_muteModel.setDisplayName( ch->m_name + ">" + tr( "Mute" ) ); ch->m_soloModel.setDisplayName( ch->m_name + ">" + tr( "Solo" ) ); + ch->m_color = std::nullopt; // send only to master if( index > 0) @@ -759,7 +759,7 @@ void Mixer::saveSettings( QDomDocument & _doc, QDomElement & _this ) ch->m_soloModel.saveSettings( _doc, mixch, "soloed" ); mixch.setAttribute( "num", i ); mixch.setAttribute( "name", ch->m_name ); - if( ch->m_hasColor ) mixch.setAttribute( "color", ch->m_color.name() ); + if (ch->m_color.has_value()) { mixch.setAttribute("color", ch->m_color->name()); } // add the channel sends for (const auto& send : ch->m_sends) @@ -807,8 +807,7 @@ void Mixer::loadSettings( const QDomElement & _this ) m_mixerChannels[num]->m_name = mixch.attribute( "name" ); if( mixch.hasAttribute( "color" ) ) { - m_mixerChannels[num]->m_hasColor = true; - m_mixerChannels[num]->m_color.setNamedColor( mixch.attribute( "color" ) ); + m_mixerChannels[num]->m_color = QColor(mixch.attribute("color")); } m_mixerChannels[num]->m_fxChain.restoreState( mixch.firstChildElement( diff --git a/src/core/Track.cpp b/src/core/Track.cpp index b034b95fb..2d4a2d840 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -65,9 +65,8 @@ Track::Track( Type type, TrackContainer * tc ) : m_soloModel( false, this, tr( "Solo" ) ), /*!< For controlling track soloing */ m_simpleSerializingMode( false ), m_clips(), /*!< The clips (segments) */ - m_color( 0, 0, 0 ), - m_hasColor( false ) -{ + m_color(std::nullopt) +{ m_trackContainer->addTrack( this ); m_height = -1; } @@ -209,9 +208,9 @@ void Track::saveSettings( QDomDocument & doc, QDomElement & element ) element.setAttribute( "trackheight", m_height ); } - if( m_hasColor ) + if (m_color.has_value()) { - element.setAttribute( "color", m_color.name() ); + element.setAttribute("color", m_color->name()); } QDomElement tsDe = doc.createElement( nodeName() ); @@ -636,14 +635,13 @@ void Track::toggleSolo() void Track::setColor(const QColor& c) { - m_hasColor = true; m_color = c; emit colorChanged(); } void Track::resetColor() { - m_hasColor = false; + m_color = std::nullopt; emit colorChanged(); } @@ -653,4 +651,4 @@ BoolModel *Track::getMutedModel() return &m_mutedModel; } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/gui/MixerLine.cpp b/src/gui/MixerLine.cpp index a90f13f83..d3435787e 100644 --- a/src/gui/MixerLine.cpp +++ b/src/gui/MixerLine.cpp @@ -174,9 +174,9 @@ void MixerLine::drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isA int width = mixerLine->rect().width(); int height = mixerLine->rect().height(); - if( channel->m_hasColor && !muted ) + if (channel->m_color.has_value() && !muted) { - p->fillRect( mixerLine->rect(), channel->m_color.darker( isActive ? 120 : 150 ) ); + p->fillRect(mixerLine->rect(), channel->m_color->darker(isActive ? 120 : 150)); } else { @@ -435,7 +435,7 @@ void MixerLine::setStrokeInnerInactive( const QColor & c ) void MixerLine::selectColor() { auto channel = Engine::mixer()->mixerChannel( m_channelIndex ); - auto new_color = ColorChooser(this).withPalette(ColorChooser::Palette::Mixer)->getColor(channel->m_color); + auto new_color = ColorChooser(this).withPalette(ColorChooser::Palette::Mixer)->getColor(channel->m_color.value_or(backgroundActive().color())); if(!new_color.isValid()) { return; } channel->setColor (new_color); Engine::getSong()->setModified(); @@ -446,7 +446,7 @@ void MixerLine::selectColor() // Disable the usage of color on this mixer line void MixerLine::resetColor() { - Engine::mixer()->mixerChannel( m_channelIndex )->m_hasColor = false; + Engine::mixer()->mixerChannel(m_channelIndex)->m_color = std::nullopt; Engine::getSong()->setModified(); update(); } From a64bbc7633c41251ad75f85d87d7513a4d4fe7bf Mon Sep 17 00:00:00 2001 From: Mirko Di <84203046+mirk0dex@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:46:28 +0000 Subject: [PATCH 038/191] Add BPM tags to built-in beat loops (#5439) (#6747) * Added floating-point vorbis BPM tags to files in lmms/data/samples/beats * Added rounded BPM to filenames, surrounded by square brackets and separated from the rest of the filename by an underscore --- .../{briff01.ogg => briff01 - 140 BPM.ogg} | Bin 27665 -> 27680 bytes ...e_bass01.ogg => rave_bass01 - 180 BPM.ogg} | Bin 21713 -> 21728 bytes ...e_bass02.ogg => rave_bass02 - 180 BPM.ogg} | Bin 19925 -> 19940 bytes .../{tb303_01.ogg => tb303_01 - 123 BPM.ogg} | Bin 57560 -> 57575 bytes ...bass01.ogg => techno_bass01 - 140 BPM.ogg} | Bin 12535 -> 12550 bytes ...bass02.ogg => techno_bass02 - 140 BPM.ogg} | Bin 14282 -> 14297 bytes ...nth01.ogg => techno_synth01 - 140 BPM.ogg} | Bin 57923 -> 57911 bytes ...nth02.ogg => techno_synth02 - 140 BPM.ogg} | Bin 79129 -> 79117 bytes ...nth03.ogg => techno_synth03 - 130 BPM.ogg} | Bin 58176 -> 58164 bytes ...nth04.ogg => techno_synth04 - 140 BPM.ogg} | Bin 68143 -> 68158 bytes ...{909beat01.ogg => 909beat01 - 122 BPM.ogg} | Bin 36745 -> 36760 bytes .../{break01.ogg => break01 - 168 BPM.ogg} | Bin 18545 -> 18560 bytes .../{break02.ogg => break02 - 141 BPM.ogg} | Bin 23345 -> 23360 bytes .../{break03.ogg => break03 - 168 BPM.ogg} | Bin 20580 -> 20595 bytes ...eat01.ogg => electro_beat01 - 120 BPM.ogg} | Bin 45021 -> 45036 bytes ...eat02.ogg => electro_beat02 - 119 BPM.ogg} | Bin 19679 -> 19694 bytes ..._loop01.ogg => house_loop01 - 142 BPM.ogg} | Bin 21398 -> 21413 bytes .../{jungle01.ogg => jungle01 - 168 BPM.ogg} | Bin 33772 -> 33787 bytes ...hihat01.ogg => rave_hihat01 - 180 BPM.ogg} | Bin 23405 -> 23420 bytes ...hihat02.ogg => rave_hihat02 - 180 BPM.ogg} | Bin 21052 -> 21067 bytes ...e_kick01.ogg => rave_kick01 - 180 BPM.ogg} | Bin 21596 -> 21611 bytes ...e_kick02.ogg => rave_kick02 - 180 BPM.ogg} | Bin 19349 -> 19364 bytes ...snare01.ogg => rave_snare01 - 180 BPM.ogg} | Bin 20955 -> 20970 bytes ...rass01.ogg => latin_brass01 - 140 BPM.ogg} | Bin 22401 -> 22412 bytes ...tar01.ogg => latin_guitar01 - 126 BPM.ogg} | Bin 43831 -> 43846 bytes ...tar02.ogg => latin_guitar02 - 140 BPM.ogg} | Bin 64762 -> 64750 bytes ...tar03.ogg => latin_guitar03 - 120 BPM.ogg} | Bin 135040 -> 135028 bytes include/DataFile.h | 3 +- src/core/DataFile.cpp | 75 +++++++++++++++++- 29 files changed, 74 insertions(+), 4 deletions(-) rename data/samples/bassloops/{briff01.ogg => briff01 - 140 BPM.ogg} (98%) rename data/samples/bassloops/{rave_bass01.ogg => rave_bass01 - 180 BPM.ogg} (98%) rename data/samples/bassloops/{rave_bass02.ogg => rave_bass02 - 180 BPM.ogg} (98%) rename data/samples/bassloops/{tb303_01.ogg => tb303_01 - 123 BPM.ogg} (98%) rename data/samples/bassloops/{techno_bass01.ogg => techno_bass01 - 140 BPM.ogg} (92%) rename data/samples/bassloops/{techno_bass02.ogg => techno_bass02 - 140 BPM.ogg} (93%) rename data/samples/bassloops/{techno_synth01.ogg => techno_synth01 - 140 BPM.ogg} (86%) rename data/samples/bassloops/{techno_synth02.ogg => techno_synth02 - 140 BPM.ogg} (87%) rename data/samples/bassloops/{techno_synth03.ogg => techno_synth03 - 130 BPM.ogg} (92%) rename data/samples/bassloops/{techno_synth04.ogg => techno_synth04 - 140 BPM.ogg} (99%) rename data/samples/beats/{909beat01.ogg => 909beat01 - 122 BPM.ogg} (99%) rename data/samples/beats/{break01.ogg => break01 - 168 BPM.ogg} (94%) rename data/samples/beats/{break02.ogg => break02 - 141 BPM.ogg} (89%) rename data/samples/beats/{break03.ogg => break03 - 168 BPM.ogg} (95%) rename data/samples/beats/{electro_beat01.ogg => electro_beat01 - 120 BPM.ogg} (99%) rename data/samples/beats/{electro_beat02.ogg => electro_beat02 - 119 BPM.ogg} (98%) rename data/samples/beats/{house_loop01.ogg => house_loop01 - 142 BPM.ogg} (98%) rename data/samples/beats/{jungle01.ogg => jungle01 - 168 BPM.ogg} (98%) rename data/samples/beats/{rave_hihat01.ogg => rave_hihat01 - 180 BPM.ogg} (98%) rename data/samples/beats/{rave_hihat02.ogg => rave_hihat02 - 180 BPM.ogg} (98%) rename data/samples/beats/{rave_kick01.ogg => rave_kick01 - 180 BPM.ogg} (98%) rename data/samples/beats/{rave_kick02.ogg => rave_kick02 - 180 BPM.ogg} (98%) rename data/samples/beats/{rave_snare01.ogg => rave_snare01 - 180 BPM.ogg} (98%) rename data/samples/latin/{latin_brass01.ogg => latin_brass01 - 140 BPM.ogg} (95%) rename data/samples/latin/{latin_guitar01.ogg => latin_guitar01 - 126 BPM.ogg} (95%) rename data/samples/latin/{latin_guitar02.ogg => latin_guitar02 - 140 BPM.ogg} (92%) rename data/samples/latin/{latin_guitar03.ogg => latin_guitar03 - 120 BPM.ogg} (90%) diff --git a/data/samples/bassloops/briff01.ogg b/data/samples/bassloops/briff01 - 140 BPM.ogg similarity index 98% rename from data/samples/bassloops/briff01.ogg rename to data/samples/bassloops/briff01 - 140 BPM.ogg index a307df85f6151c40ff0a6c3fd5b8a582339da978..0b9cc32f7f835256fca45fe5ece8c27a9a252fd2 100644 GIT binary patch delta 42 wcmbPugK@zP#t8vzcDw#t8vz<&$bc1#~BdnX@ne!Nvs**#M7c2|oY; diff --git a/data/samples/bassloops/rave_bass01.ogg b/data/samples/bassloops/rave_bass01 - 180 BPM.ogg similarity index 98% rename from data/samples/bassloops/rave_bass01.ogg rename to data/samples/bassloops/rave_bass01 - 180 BPM.ogg index 920ff4a740fbd4b30956fb8fe2dad63cdcd7d933..335195747d6511aea4b5bff8c02b0b64cff13e43 100644 GIT binary patch delta 42 wcmcb(lJUVx#t8vz`{Q3d;j@_-W-83cz`(!_#7+Uewua`GdKLy|8~dF?06bm{yZ`_I delta 27 hcmaE`lJVk7#t8vz&S`(t_;e?RnX)hd!Nz$`ApnsI2=@R0 diff --git a/data/samples/bassloops/rave_bass02.ogg b/data/samples/bassloops/rave_bass02 - 180 BPM.ogg similarity index 98% rename from data/samples/bassloops/rave_bass02.ogg rename to data/samples/bassloops/rave_bass02 - 180 BPM.ogg index ff38123df91394ec91ae66491387332d88c9dc7f..230d99d2e95952ce7cbfe2fd30daa25118a4f8e6 100644 GIT binary patch delta 42 wcmcaQoAJqP#t8vz&8lkNd^QuqOobU47#O&L*eSr**3iO0&(PRnW51g(002)4$N&HU delta 27 hcmaDdoAK&w#t8vz|I>>X@##(sGi6}_f{pXsd;yy#36KB) diff --git a/data/samples/bassloops/tb303_01.ogg b/data/samples/bassloops/tb303_01 - 123 BPM.ogg similarity index 98% rename from data/samples/bassloops/tb303_01.ogg rename to data/samples/bassloops/tb303_01 - 123 BPM.ogg index 41e1b1fc478c57418c6c540c4c22f94da81428bf..1057201756ed450c4c6b871ec2676095d220e741 100644 GIT binary patch delta 118 zcmca{kooyR<_Q68+ox)Z3ffEzvk+!vU|`?|Vy6IKTSFrwJu@?djgv~`MUsJHKwvUA z(u^5M-wu>$(3mWxpw7s?*-=4GK&eJ?0UrwkvwwPeFatwFFIX{HH4Bir;qZ@Ym&w%z LE}M@T2yFlW)Bhd> delta 103 zcmaEUkom?z<_Q68TC@M+&r>6%qFgV13Rf3f=1DS_J zR%^>mUau(2n6&wwBCEh;eWN6((gVE+r7S?^=0Dpq6({o>DKN%to@m4@F!`cW3{+{u zZiG@+Aaiwq?j5Dc8=MpwZ8z&WU*w-`8*B?R^(8_n8;I#TZ==d&sbFPB_03hm!2*+c zQ`BKf#Xz2hc#9oKxBT3&Qf2bXWR=Y(DWU=*9tc?upsaki&K0G}u_cN?pVXBs7MPsW z3iC-rDMBeHP^tO1=_zWHZCh0t-8Ua;-OE4u&{TJro%0Y%xqwPf%Ify1O`bbdl~Hjs z=d>66lXaFU!aRKup%fG|0TzGuD@^8HCeP@)*>~9={>jbTT&*FYet;PiEfAmZ0O^@G z7-uQCxj6lI{crO>^MA6ptE-pmf3^SO{}ui#{zEp|`+tkgW+%4sZI-=W$EXLklcm8Ip_&h0m~n`hrx!3h9+a@wi@ delta 670 zcmdmfg!%9h<_Q5}DuU}@3+nzy0~6CVSQvm{NbE(nz0xEsR+q_>7G zh-I{yJXyd=ltBwD1D0Y2GN-k~)v8T?C7`xhP_SIU5~ANB0-*?Gc1C-tg`CoV!~Zt_ zJ^zROF9G6oM@JhwCo5m~|BnCl{zv?u+@Ywn`LQCa0MrQwIuS;&0-2ju&p)G>G&$8s zVe$*Zq|I$c%rKP>I}s|`fXvRx)_O`YlbjVNcRIyvR&u@w^%cW`rwE1YK&EuogQ+UE zlf8qLCw~sI-JBmB3^Ve87$gWl{^tOy^eVX&s;WMjB}HZOp=9;V8Y!XzA}$CeoIoXN z0Vfm{CkK`&ZZ0la3^RwJ6rqRezyAUMqit+$ zJRJTj0|f*A$J*NZI!%7IRB^M@vOh5E4*W+L!V5HnX*E}nyzAt7+vFzOZFAkcZ`)OV zS+I2s4Z5HN07(jbKsxT4!ZD5i;geZTsBNBnER1op?ul)DhG4ZU4ZaAq{6MvVSH1Ja k-2YqrcLqY2|33e%{wFv)d$>4-gl&F&t&VYX|BV%#0L($_y#N3J diff --git a/data/samples/bassloops/techno_synth02.ogg b/data/samples/bassloops/techno_synth02 - 140 BPM.ogg similarity index 87% rename from data/samples/bassloops/techno_synth02.ogg rename to data/samples/bassloops/techno_synth02 - 140 BPM.ogg index dfa972e1defc4d07af5166dfe30c0e23d61d43bd..afb27172b0d900ccf2702afee29ff1de55ea7054 100644 GIT binary patch delta 963 zcmZ9Kdo0yq7{|}=_;D_7Kb*N{rG%6uBy-C((&^y1EKBP)v>BExu{AQ3#&B4(X)T+T ztOOZ$ETrd3xpNj-$e{(}=Eu@#!;$)Xpv_G`46D3ROnSu?Uy~*7%JDz#MAg z5hmqSg$Zg0MMj2FAULMpU^ieR>PBRi5AIVb6I_K@C$7pSbIrvyMxR?e!sm*p`?`1$ z&Z^l9yI2GKP?EJv5;NkT7b85PmtNu1#ni2|v(I%%MnC3JqexDSiPRNm5vGX83jP98 zax;I@lvI_nte0NEUN@i3#w66PN6uWc*7frVQz*k_T)_==;SV4pwG~Y!B&}Y{dg|F= zo|RH|(#U;}3}69UEa3^%cm{%(XeP2onF&d0vDqfmc~bW_s{G&)89vhQ74D(|*HMXX zbYh%jw>F!Qpw6gmvYIDNDEV!-%1DMmj9?n0c#ko>!$UOFJ4?IgjLE&BUPHglY6seQ z(v*@eWy|Y3P#qi+5*ZPRui@bV;lc4qfDp@A#b?Zu)5Bv@a_&Q)p^v`b30}dBDtIbZ zf7)f|_+1dT6mdctMF7 z4C4z1Fhm|NHAqO#FH7+j>CTfJC2ObJ&dTULEqH|IbjbOrKrK3u+fl+4inSgRMlJHz z*PP0u78Ld9Ykg`9IRfa8MHcSRS>M75L?8(#wG$k}Nwkjk%zlxtp0DP4OUm!fE@`kw zCKSk{WEv9S4?hGUkt#+ZMH}tJ#7eaN?#x@!&J6|jO+Bw*MHPzXLS0y#hnu$UFjFD? E2YDi;{{R30 delta 957 zcmZ9~X-HI26bJBm;~QsjmN#yrk+=joh1ne=sq6z9Js%8&%5tlUuBzD*_+4R z$(Pegbs+v54Mr_1prf#K%`D^y=^Pqbzs!W2OkA48fu_0P>tkev z3*#hK_<76!GST@S4Sy`Ef;CFKXoxqvE~(P6FONaqZ*XMqNt5z&UkRICCDz#W@d z0aPG-H9j{IG4B zDDWYDXSNvfqd=n;au7)*I3?qs_tNl3~|<`luU%6g^k2&}!! z=;~XwRKu%V&EUB+)mD?zz58VJg zkBi50Y;0I;WJVSs#5a;RIw&HB@hMq|{it5@B~-LTep&jo20!r;OI83$RALG)A{)7= zCfavHBI3DFB@43SH6C*mcWK9}T`-8pwCyeo&?W}(gvR8~(!3+-SZk8nAftD;UQ65M zQqx(nInv_t9^?3g5scCb?RbD;^peP5&XVpcOqt|gyD>a;rqEA)ZI1>es6hj|aRI3a zqi+fdQ4ha*S{nB;Z^V}B5T-`>7ejrW!Tg4k@Gu;X_ff=Ei<0J+rR*YZ59?qzY|B=wRS&Lpx}aEKA-K!5(nn^!ri02B%>d w20?~{AkgRQt2Vm)k!KH8s?;%a#)Vd_G9u^xSdn!UL^uu}d_!mHv(2>S9|*4%{Qv*} delta 719 zcmdmTjQPMZ<_Q5}MNbE(lcJxEsR+r0t(A z(qXijJXye5L?0{xmSF}mJ8J%{|0n-d)Y=@SbO&a)!(@bJRuJ=RK$Lvg#TMuoJ1ZMAOJ@g{|8Dz*IWKAyje#Ri=7d9FzB(e7#g|a$c$5=5M8{FqH>-5h}TWDp%YTiIw%7 zT;C=$S-Q=0^PDy*m=7EdB2;n%RZiZrrd!?-=yJXPj{k%Hd;Yio?*W8CmNqul)=m!p zP5&GHpS*3F<7UI@88F)%J|fKE0h-ZXU|l0uJXw8(?Btoti#NNjcmRuo11g|chr~ZG zkajt7>!`fj`i63gG|% diff --git a/data/samples/beats/909beat01.ogg b/data/samples/beats/909beat01 - 122 BPM.ogg similarity index 99% rename from data/samples/beats/909beat01.ogg rename to data/samples/beats/909beat01 - 122 BPM.ogg index 1892eb91bc40140077687f3f4fb60e7a471199e0..2bae5135716ada4ddce0b7e3e6336c56b318004e 100644 GIT binary patch delta 42 wcmeC2&opB`(}Vyv{Z)5-_-rPInF=#9Ffecfu~UGrt)Y>To{@#+#{OS@00(Id8UO$Q delta 27 icmbO+pQ&>`(}V!F6@QAJ@aawrGi6}_f{pWj^#K5uGYWP9 diff --git a/data/samples/beats/break01.ogg b/data/samples/beats/break01 - 168 BPM.ogg similarity index 94% rename from data/samples/beats/break01.ogg rename to data/samples/beats/break01 - 168 BPM.ogg index d1f5769bd5a57625ca541e829037daf4ef5c864e..5d9bd2f4bb1782c4c5c8155a66dcf0c9e143968b 100644 GIT binary patch delta 42 wcmex3fw5sCf}A>| z;${)~NP)>~^cA5>8FnL-vH+QKnm624CU@#9Ga79^tN((3@+*5Is8WZ&2&Jq*=G%?M QERvHi+DmShaS)XT0CA2p_5c6? delta 230 zcmX@Gjd9~P#t8vz(f<0k_;n|SnXxbc!N&QMctxy0ECvQ0rAwkrK$^?ZWt+leS3ZT! zX?$@4GXCl5!3>NC%D`&CikN}Sw=*n`X~<6gBd0D~wT#c7g_Yrju!FrNccf#%Q>9x9 zS67F=npXaDE9+(z`AC=<4Z9F#fNYPx*gjcBaq>=m<;k`Bikt81zu-3o+s5GV7on0B w$P78I?j~hq@cGH>XWu^m{iLa-reSDe@}KF$+fVPm{P{WgsJ+o_-!VJnF%v8Ffecfu~UGrt)ZEPo{6!^#tB#a0UeJFJOBUy delta 27 hcmeyofbq!!#t8vzk$E}q_;n|SnXxbc!N&Pl`~jID3Gn~` diff --git a/data/samples/beats/electro_beat01.ogg b/data/samples/beats/electro_beat01 - 120 BPM.ogg similarity index 99% rename from data/samples/beats/electro_beat01.ogg rename to data/samples/beats/electro_beat01 - 120 BPM.ogg index 57cd690fc9675d432f9f5c7647aaa7c05208e6b4..29352b68375f4e0700d3494889ebc61eb31e32a0 100644 GIT binary patch delta 42 wcmcb6pXtqgrU?OT;WF8R0yY!F%!L^l7#O&L*eSr**3i&W&&rU?OTtn2282GiPA{f{hD&)&l^S_6dLh diff --git a/data/samples/beats/electro_beat02.ogg b/data/samples/beats/electro_beat02 - 119 BPM.ogg similarity index 98% rename from data/samples/beats/electro_beat02.ogg rename to data/samples/beats/electro_beat02 - 119 BPM.ogg index b89260bab5299a6d8c1c2b07520c775011cb1564..775b64d8851a52a38e7e7093d3a95dcf603335b5 100644 GIT binary patch delta 42 xcmcaVlkwe5#t8vz4L@fb6|k8YW-iRgz`(!_#7+UewuXildgcaZ8z=ht0023o45R=6 delta 27 hcmaDilkxsc#t8vz*H-aJ3g}J@GiPA{f{hFOd;pLk2-g4r diff --git a/data/samples/beats/house_loop01.ogg b/data/samples/beats/house_loop01 - 142 BPM.ogg similarity index 98% rename from data/samples/beats/house_loop01.ogg rename to data/samples/beats/house_loop01 - 142 BPM.ogg index 09f3a260bc1d8a1927de247c4aa6a36f21700a4b..9f04d1debf75f6298ed9953335fc8b0a9bafb753 100644 GIT binary patch delta 42 wcmbQXoN?)L#t8vz4?}&81Z*aTnF}*AFfecfu~UGrt)YpLo{@q1#)+)K00Yem!~g&Q delta 27 hcmZ3woN?N6#t8vz{!$e^0=g5!%vl(KVB-STU;ut72j>6) diff --git a/data/samples/beats/jungle01.ogg b/data/samples/beats/jungle01 - 168 BPM.ogg similarity index 98% rename from data/samples/beats/jungle01.ogg rename to data/samples/beats/jungle01 - 168 BPM.ogg index 9662e4514ab7d3491eb4b26e780468f4c79cfe6a..b7196044fd3599bd3a1fa97e04b01b7e3132c4ca 100644 GIT binary patch delta 42 wcmaFU&h)#TX+i+oO($&=L7RzT7Q&1S3=G^r>=fW@YiMSnXJT%$aZ*$>01vDSjQ{`u delta 27 hcmey}&h)08X+i)Shj{RMLEVXA7Ay=vuyJ8jGXR9z2$28) diff --git a/data/samples/beats/rave_hihat01.ogg b/data/samples/beats/rave_hihat01 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_hihat01.ogg rename to data/samples/beats/rave_hihat01 - 180 BPM.ogg index 236f447a8bf8342f798ea91225847a70d1ffc4ad..632bb0becfc1cebd84f8cc84cc199e630e370f94 100644 GIT binary patch delta 42 xcmaF6jq%Sm#t8vz>Oa@+;j@_-W-83cz`(!_#7+UewuTl4dWM!}8~g7?0RTPD4MzX~ delta 27 icmeyfjq&X^#t8vzuTD;T%cnar%#?)z2sX~U7X<*Z^9xo0 diff --git a/data/samples/beats/rave_hihat02.ogg b/data/samples/beats/rave_hihat02 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_hihat02.ogg rename to data/samples/beats/rave_hihat02 - 180 BPM.ogg index 33329bd559d6bf3c8165d085b4979392dcd79b67..a6e5426f4df2fca0565b682772a62a870abaa6fc 100644 GIT binary patch delta 42 wcmdn9gz@we#t8vzha!TH^Vv)cGZkiJU|`?|Vy6IKTSE&2Jp)UNjs0r_0UnkNGXMYp delta 27 hcmX@TgmKRj#t8vzVJ9Rr`E)0SnX)hd!Nz%O0|AOo2><{9 diff --git a/data/samples/beats/rave_kick01.ogg b/data/samples/beats/rave_kick01 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_kick01.ogg rename to data/samples/beats/rave_kick01 - 180 BPM.ogg index 79f99ffb89cddd1487f2cdf6c3f25cf6de4974a0..5633f6e1c91f3e80936a503a500fe4614f061ad6 100644 GIT binary patch delta 42 wcmcb!g7NhV#t8vzALdt^^4UxbGZkiJU|`?|Vy6IKTSIe8Jqt5~js0hX0WAXztpET3 delta 27 icmaF8g7MA@#t8vzGp`*>;nSTMX3D|<1RLj_4F&+A-3ns> diff --git a/data/samples/beats/rave_kick02.ogg b/data/samples/beats/rave_kick02 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_kick02.ogg rename to data/samples/beats/rave_kick02 - 180 BPM.ogg index 463112869217c41f4354ebe1002d39b6b9a3eda3..c57cfc0d9dd594e373a121186e14373e42e7ff17 100644 GIT binary patch delta 42 wcmbO_opH%@#t8vz-BytXd^QuqOobU47#O&L*eSr**3iO0&(gqTV?UcW0Pz0`Gynhq delta 27 hcmZ27opI`P#t8vzs@FXq@aawrGi6}_f{pXoya9$j2yg%Z diff --git a/data/samples/beats/rave_snare01.ogg b/data/samples/beats/rave_snare01 - 180 BPM.ogg similarity index 98% rename from data/samples/beats/rave_snare01.ogg rename to data/samples/beats/rave_snare01 - 180 BPM.ogg index ceec2d6e0a4a305f16f6905ffefaca070ae60daf..6e17a6af37ae690e59568e1efd89dec494fe8995 100644 GIT binary patch delta 42 wcmcb;nDNzO#t8vzFBAVQ=d+m@W-83cz`(!_#7+Uewua`GdX~l}8~c3%0Y1JA&;S4c delta 27 icmaF0nDO>v#t8vzC%!H(;?tcNX3D|<1RLl11Ofn~@Cp$C diff --git a/data/samples/latin/latin_brass01.ogg b/data/samples/latin/latin_brass01 - 140 BPM.ogg similarity index 95% rename from data/samples/latin/latin_brass01.ogg rename to data/samples/latin/latin_brass01 - 140 BPM.ogg index 3bf7dcd27d08db510681b1e8ed3070860c652a18..ac9a2c59b25b03af3f81bfd7bb4c86ab4c282e04 100644 GIT binary patch delta 38 scmZo%&)Bn`aY6u_^ZPUL{1y|#%=j4@7#P@r*eSr**3iUYW+u$Yz`(!_#7+UewuVNgdgcZu8z-D<647J=3NkQc v-?$sY38Wn!yz*90H}1x80%@)8}e`oEvvzSb#L+k8LkiCLc6ZX7t_s z#L!(}a=w$Vw10YfFi>tGNFhWiE09jB43O5G?Czw&7_fPd({cXE`-1|YN)H?ds|1_M z24wCodX}a*d47-r&{Y25|NN8vl3}Jgd_pK?2QqUtPf4pz)=E}kEZ96dIYVHwT}c7d zR0m~{Cm{~t0Mb+1#4jsMmM@WKwBNk0q)1?LOshT20p19uoIs@)^k%$LoNUvoz?iam zcB_NHWRYnpFmF{LlyU);@*no>ke~d0sw`80+vbXC%lId!FPjEc+b|cQ78KaNpVR*9 zPPSgA!x*&r@3Kw&lb>%3f;sjCLMabW>2LlQoU)ToZ0e>;U;|hh!Vm@s01bHB`c{VJf6#x+&4s@@nEV$(H&HL9prFSfhU&@mGQQ1+3(jTU!0-KM{%>BTs#~w> zGIGbs<7ck7_rQNKuh`ot5{nOi8O@TSgs2v^R3lpYdZLx&8TsR>78W7eZ_ttQ>HE_Q zxJ)awLB06nb&A0wwjtUyMJf{NEj`JxtnWz|dfD5BS9{%bLwfdsiqxSEBzu0YT-qg~ zn}QuNf`}J|qLHl)LFEfIuqL;W9L@2L37Z@cIbIs}`QbLq!!%q5Wq{`e7weKjZ~+uR z7lSfx3qjKgU(r+yu+%N0iO5c*i-76*HO;YBx-LitiBvX@f$TPa368@;?5NfXjhS&f=G zVBs+D;tmZ5BoXdG7OuiA(meqZAhFEhLb-=#Y6AM%uqMz>1ZGUmxB4&yVLw`{hVj|; Gxz@i&;193> diff --git a/data/samples/latin/latin_guitar03.ogg b/data/samples/latin/latin_guitar03 - 120 BPM.ogg similarity index 90% rename from data/samples/latin/latin_guitar03.ogg rename to data/samples/latin/latin_guitar03 - 120 BPM.ogg index 0ae9bb3f78ddbd0d6ec59540d00ec1e06b02898b..1f6a9275ea22f7952f402b8de8212f9d0cb7bb07 100644 GIT binary patch delta 1580 zcmZA1e@sPe6p=UIYSszjzMSLT4Zv zn6eha!A=>7rV%S6>?U`))sPa?e;R0ut!d8b6k27hlCjz8`*`nlx9;}#y?f_#zW001 z{od0(>htGGpJUe0>-XL)-zc#EUBWA&Q+1Wu zW-ej9>;!v`l`s<%SuV?C6{2KLJHnBm+~a~X$@Irc5-+^n5wdSaXA*37OQGFtd8T@o zn7_?tV{L3NJA$cNFv(7qC5`9a<;bi(OYJJ|{o3{+Pv&|= z7Zn>7x&i6AZ1pcOWTGcV3hYhiw-9C27e{X;?0po@-erezUC**wTvzK(O9ks-1FW4@ zNv|9~$dlBuDt8%QDYG4K-f}^GJC+2D#XzUjH@3);3sar$G8Ps31XABtua0=qKNSbf zQktB?6MCiDO(PvG;@k&+jPIG}f8Ip8u7pFSkd5qSVKNpi~^(NwUp13zEz`}exy(yY5e}J7vo67>SoB0 zF0bbCG}jL@T3ua=Tu^xbe7Z*A!N`w=?AaYlTLp-Lc-cD#s(3nF3i0mbZ!0)e$vj9$ZI9RD_KXdr^7_JbPo z<&u7|@$}v(Hz;}&>fa-eez1jpISR>8FDIUYZhRw?Q15>BkfPP7zi((;j-i() zAsQ;>qVsT#qlK5?zdoE*bOh?d>8EDZH02V6tJpUA^dWdqPIb7bm}SB&Gc zh{IS}o|qYU5qJ|po4(DAmvs1%QYXeGXqBPvZif7!zbq3js~z}bT?RJj9O9JjhQbsW zNapQhGLkkQ8Du|bYZ}ZZPN=IiH+$+E>g!+_K7kjZ4$Qzq8I%L=diH{7-E&5f5uB}e zf(419&G^rvzsPj)xVKD;zp;#n7t59vdxLb~9-~jv=GVI-3p7Tk0y~sKH5j1~O5i!* z=wq#@6)dGTkAMdqMsdvnTbm=3MF;oCzd6w~M(0%FZJiT8->Su{FF3`)R-d9|Yjs6T z#!$(v=Dn-7*ILT$u6j5QgHQlEI-ZHHsTB6o-(tX?Zo9bDJs#RhzKw<0Mnh=nd zUK+~6r-yhka=1{Lc$Se^N@|lkze~ZwNQ&4tG8l^Eon|PGqNtv?lJp*YU?K}Y9QTNC zOss^?p8ti(cq;GT@tw)-J1^znBRBfQTq!>kl2`dtBbX%HY27os>vn1<{&`-7_s@5V z|I9Zjxvc}LWKuz;@QNx{=Z0FSplhx#G489fx!`51yTMWBs)HsF;0QPY+k9^EzVAwS zypt@7l133xkyFFrA6L|PXoVBkSFB-qmB|Du?_CqDIVSwUdNww%o5Y#*@{sg?%j8p3 zPJXrLbxjcwVv(BsyA2gd9kHl5ga@Mh86RBm)3kP+KG>$Qk+^h<$I_5ZGNhxMTS+PN z?q)cV;(htoEIFK!^cs+!{I&yiOJfE!5!N22lc?succ02Om5{p}$|6-9Iw5Uv$P=1~ z>^-KFsh-~G(`V+A4+W$nP66dgQv&)soUStrKSOcC1-&7&nPeZP`1&E#EL9#xiy_QE zU^s>1*XG}`X&cGuE~FuA9ow$ix+f}_*HZuX_)#GbtiTcBG15z)uc3aq^y*_o p; UPGRADE_METHODS; diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index b87d06931..b83e1bebb 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -80,7 +80,8 @@ const std::vector DataFile::UPGRADE_METHODS = { &DataFile::upgrade_automationNodes , &DataFile::upgrade_extendedNoteRange, &DataFile::upgrade_defaultTripleOscillatorHQ, &DataFile::upgrade_mixerRename , &DataFile::upgrade_bbTcoRename, - &DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing + &DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing, + &DataFile::upgrade_loopsRename }; // Vector of all versions that have upgrade routines. @@ -1807,7 +1808,76 @@ void DataFile::upgrade_sampleAndHold() // Correct old random wave LFO speeds if (e.attribute("wave").toInt() == 6) { - e.setAttribute("speed",0.01f); + e.setAttribute("speed", 0.01f); + } + } +} + + +// Change loops' filenames in s +void DataFile::upgrade_loopsRename() +{ + static constexpr auto loopBPMs = std::array{ + std::pair{"bassloops/briff01", "140"}, + std::pair{"bassloops/rave_bass01", "180"}, + std::pair{"bassloops/rave_bass02", "180"}, + std::pair{"bassloops/tb303_01", "123"}, + std::pair{"bassloops/techno_bass01", "140"}, + std::pair{"bassloops/techno_bass02", "140"}, + std::pair{"bassloops/techno_synth01", "140"}, + std::pair{"bassloops/techno_synth02", "140"}, + std::pair{"bassloops/techno_synth03", "130"}, + std::pair{"bassloops/techno_synth04", "140"}, + std::pair{"beats/909beat01", "122"}, + std::pair{"beats/break01", "168"}, + std::pair{"beats/break02", "141"}, + std::pair{"beats/break03", "168"}, + std::pair{"beats/electro_beat01", "120"}, + std::pair{"beats/electro_beat02", "119"}, + std::pair{"beats/house_loop01", "142"}, + std::pair{"beats/jungle01", "168"}, + std::pair{"beats/rave_hihat01", "180"}, + std::pair{"beats/rave_hihat02", "180"}, + std::pair{"beats/rave_kick01", "180"}, + std::pair{"beats/rave_kick02", "180"}, + std::pair{"beats/rave_snare01", "180"}, + std::pair{"latin/latin_brass01", "140"}, + std::pair{"latin/latin_guitar01", "126"}, + std::pair{"latin/latin_guitar02", "140"}, + std::pair{"latin/latin_guitar03", "120"}, + }; + + const QString prefix = "factorysample:", + extension = ".ogg"; + + // Replace loop sample names + for (const auto& [elem, srcAttrs] : ELEMENTS_WITH_RESOURCES) + { + auto elements = elementsByTagName(elem); + + for (const auto& srcAttr : srcAttrs) + { + for (int i = 0; i < elements.length(); ++i) + { + auto item = elements.item(i).toElement(); + + if (item.isNull() || !item.hasAttribute(srcAttr)) { continue; } + for (const auto& cur : loopBPMs) + { + QString x = cur.first, // loop name + y = cur.second, // BPM + srcVal = item.attribute(srcAttr), + pattern = prefix + x + extension; + + if (srcVal == pattern) + { + // Add " - X BPM" to filename + item.setAttribute(srcAttr, + prefix + x + " - " + y + " BPM" + + extension); + } + } + } } } } @@ -1984,5 +2054,4 @@ unsigned int DataFile::legacyFileVersion() return std::distance( UPGRADE_VERSIONS.begin(), firstRequiredUpgrade ); } - } // namespace lmms From 1e2167d005be3a335cf7802c9842f5ba8f8fc434 Mon Sep 17 00:00:00 2001 From: Hyunjin Song Date: Sat, 18 Nov 2023 21:27:09 +0900 Subject: [PATCH 039/191] Fix Sf2 player freezing on project unloading with shared SoundFonts (#6950) * Require FluidSynth >= 1.1.7 * Use `fluid_sfont_t` directly without sharing * Adjust formatting a bit --- CMakeLists.txt | 2 +- plugins/Sf2Player/Sf2Player.cpp | 81 ++++++++------------------------- plugins/Sf2Player/Sf2Player.h | 22 +-------- 3 files changed, 20 insertions(+), 85 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ee3ac9e87..858849abd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -540,7 +540,7 @@ ENDIF() # check for Fluidsynth IF(WANT_SF2) - find_package(FluidSynth 1.1.0) + find_package(FluidSynth 1.1.7) if(FluidSynth_FOUND) SET(LMMS_HAVE_FLUIDSYNTH TRUE) if(FluidSynth_VERSION_STRING VERSION_GREATER_EQUAL 2) diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index ee97e98ef..7795671c5 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -121,12 +121,6 @@ struct Sf2PluginData -// Static map of current sfonts -QMap Sf2Instrument::s_fonts; -QMutex Sf2Instrument::s_fontsMutex; - - - Sf2Instrument::Sf2Instrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &sf2player_plugin_descriptor ), m_srcState( nullptr ), @@ -375,31 +369,12 @@ void Sf2Instrument::freeFont() { m_synthMutex.lock(); - if ( m_font != nullptr ) + if (m_font != nullptr) { - s_fontsMutex.lock(); - --(m_font->refCount); - - // No more references - if( m_font->refCount <= 0 ) - { - qDebug() << "Really deleting " << m_filename; - - fluid_synth_sfunload( m_synth, m_fontId, true ); - s_fonts.remove( m_filename ); - delete m_font; - } - // Just remove our reference - else - { - qDebug() << "un-referencing " << m_filename; - - fluid_synth_remove_sfont( m_synth, m_font->fluidFont ); - } - s_fontsMutex.unlock(); - + fluid_synth_sfunload(m_synth, m_fontId, true); m_font = nullptr; } + m_synthMutex.unlock(); } @@ -413,49 +388,29 @@ void Sf2Instrument::openFile( const QString & _sf2File, bool updateTrackName ) char * sf2Ascii = qstrdup( qPrintable( PathUtil::toAbsolute( _sf2File ) ) ); QString relativePath = PathUtil::toShortestRelative( _sf2File ); - // free reference to soundfont if one is selected + // free the soundfont if one is selected freeFont(); m_synthMutex.lock(); - s_fontsMutex.lock(); - // Increment Reference - if( s_fonts.contains( relativePath ) ) + bool loaded = false; + if (fluid_is_soundfont(sf2Ascii)) { - qDebug() << "Using existing reference to " << relativePath; + m_fontId = fluid_synth_sfload(m_synth, sf2Ascii, true); - m_font = s_fonts[ relativePath ]; - - m_font->refCount++; - - m_fontId = fluid_synth_add_sfont( m_synth, m_font->fluidFont ); - } - - // Add to map, if doesn't exist. - else - { - bool loaded = false; - if( fluid_is_soundfont( sf2Ascii ) ) + if (fluid_synth_sfcount(m_synth) > 0) { - m_fontId = fluid_synth_sfload( m_synth, sf2Ascii, true ); - - if( fluid_synth_sfcount( m_synth ) > 0 ) - { - // Grab this sf from the top of the stack and add to list - m_font = new Sf2Font( fluid_synth_get_sfont( m_synth, 0 ) ); - s_fonts.insert( relativePath, m_font ); - loaded = true; - } - } - - if(!loaded) - { - collectErrorForUI( Sf2Instrument::tr( "A soundfont %1 could not be loaded." ). - arg( QFileInfo( _sf2File ).baseName() ) ); + // Grab this sf from the top of the stack and add to list + m_font = fluid_synth_get_sfont(m_synth, 0); + loaded = true; } } - s_fontsMutex.unlock(); + if (!loaded) + { + collectErrorForUI(Sf2Instrument::tr("A soundfont %1 could not be loaded.").arg(QFileInfo(_sf2File).baseName())); + } + m_synthMutex.unlock(); if( m_fontId >= 0 ) @@ -627,12 +582,12 @@ void Sf2Instrument::reloadSynth() { // Now, delete the old one and replace m_synthMutex.lock(); - fluid_synth_remove_sfont( m_synth, m_font->fluidFont ); + fluid_synth_remove_sfont( m_synth, m_font ); delete_fluid_synth( m_synth ); // New synth m_synth = new_fluid_synth( m_settings ); - m_fontId = fluid_synth_add_sfont( m_synth, m_font->fluidFont ); + m_fontId = fluid_synth_add_sfont( m_synth, m_font ); m_synthMutex.unlock(); // synth program change (set bank and patch) diff --git a/plugins/Sf2Player/Sf2Player.h b/plugins/Sf2Player/Sf2Player.h index bd7fa1b81..17ddf5500 100644 --- a/plugins/Sf2Player/Sf2Player.h +++ b/plugins/Sf2Player/Sf2Player.h @@ -114,16 +114,12 @@ public slots: void updateTuning(); private: - static QMutex s_fontsMutex; - static QMap s_fonts; - static int (* s_origFree)( fluid_sfont_t * ); - SRC_STATE * m_srcState; fluid_settings_t* m_settings; fluid_synth_t* m_synth; - Sf2Font* m_font; + fluid_sfont_t* m_font; int m_fontId; QString m_filename; @@ -177,22 +173,6 @@ signals: } ; - -// A soundfont in our font-map -class Sf2Font -{ - MM_OPERATORS -public: - Sf2Font( fluid_sfont_t * f ) : - fluidFont( f ), - refCount( 1 ) - {}; - - fluid_sfont_t * fluidFont; - int refCount; -}; - - namespace gui { From 7268827624346697f2ed34451e4e7e595d95675c Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 18 Nov 2023 15:28:01 -0500 Subject: [PATCH 040/191] Revamp synchronization with the audio engine (#6881) The revamp consists of one lock. When the audio thread needs to render audio or another thread wants to run a change, acquiring the lock grants mutual exclusion to do one of the two. The intention is that this will provide stronger guarantees that changes do not run concurrently with the audio thread, as well as having the synchronization mechanism itself be free of data races (verified with TSan). --- include/AudioEngine.h | 29 +++---------- src/core/AudioEngine.cpp | 94 +++++----------------------------------- 2 files changed, 17 insertions(+), 106 deletions(-) diff --git a/include/AudioEngine.h b/include/AudioEngine.h index d3d0d025f..67c2edd86 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -25,14 +25,13 @@ #ifndef LMMS_AUDIO_ENGINE_H #define LMMS_AUDIO_ENGINE_H -#include - -#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) - #include +#ifdef __MINGW32__ +#include +#else +#include #endif #include -#include #include #include @@ -420,10 +419,6 @@ private: void clearInternal(); - //! Called by the audio thread to give control to other threads, - //! such that they can do changes in the model (like e.g. removing effects) - void runChangesInModel(); - bool m_renderOnly; std::vector m_audioPorts; @@ -453,8 +448,6 @@ private: struct qualitySettings m_qualitySettings; float m_masterGain; - bool m_isProcessing; - // audio device stuff void doSetAudioDevice( AudioDevice *_dev ); AudioDevice * m_audioDev; @@ -476,19 +469,7 @@ private: bool m_clearSignal; - bool m_changesSignal; - unsigned int m_changes; - QMutex m_changesMutex; -#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) - QRecursiveMutex m_doChangesMutex; -#else - QMutex m_doChangesMutex; -#endif - QMutex m_waitChangesMutex; - QWaitCondition m_changesAudioEngineCondition; - QWaitCondition m_changesRequestCondition; - - bool m_waitingForWrite; + std::mutex m_changeMutex; friend class Engine; friend class AudioEngineWorkerThread; diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 59bb87a30..47b42e11b 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -67,6 +67,7 @@ namespace lmms using LocklessListElement = LocklessList::Element; static thread_local bool s_renderingThread; +static thread_local bool s_runningChange; @@ -83,19 +84,12 @@ AudioEngine::AudioEngine( bool renderOnly ) : m_newPlayHandles( PlayHandle::MaxNumber ), m_qualitySettings( qualitySettings::Mode::Draft ), m_masterGain( 1.0f ), - m_isProcessing( false ), m_audioDev( nullptr ), m_oldAudioDev( nullptr ), m_audioDevStartFailed( false ), m_profiler(), m_metronomeActive(false), - m_clearSignal( false ), - m_changesSignal( false ), - m_changes( 0 ), -#if (QT_VERSION < QT_VERSION_CHECK(5,14,0)) - m_doChangesMutex( QMutex::Recursive ), -#endif - m_waitingForWrite( false ) + m_clearSignal(false) { for( int i = 0; i < 2; ++i ) { @@ -165,8 +159,6 @@ AudioEngine::AudioEngine( bool renderOnly ) : AudioEngine::~AudioEngine() { - runChangesInModel(); - for( int w = 0; w < m_numWorkers; ++w ) { m_workers[w]->quit(); @@ -232,8 +224,6 @@ void AudioEngine::startProcessing(bool needsFifo) } m_audioDev->startProcessing(); - - m_isProcessing = true; } @@ -241,8 +231,6 @@ void AudioEngine::startProcessing(bool needsFifo) void AudioEngine::stopProcessing() { - m_isProcessing = false; - if( m_fifoWriter != nullptr ) { m_fifoWriter->finish(); @@ -447,8 +435,6 @@ void AudioEngine::renderStageMix() emit nextAudioBuffer(m_outputBufferRead); - runChangesInModel(); - // and trigger LFOs EnvelopeAndLfoParameters::instances()->trigger(); Controller::triggerFrameCounter(); @@ -459,6 +445,8 @@ void AudioEngine::renderStageMix() const surroundSampleFrame *AudioEngine::renderNextBuffer() { + const auto lock = std::lock_guard{m_changeMutex}; + m_profiler.startPeriod(); s_renderingThread = true; @@ -811,57 +799,16 @@ void AudioEngine::removePlayHandlesOfTypes(Track * track, PlayHandle::Types type void AudioEngine::requestChangeInModel() { - if( s_renderingThread ) - return; - - m_changesMutex.lock(); - m_changes++; - m_changesMutex.unlock(); - - m_doChangesMutex.lock(); - m_waitChangesMutex.lock(); - if (m_isProcessing && !m_waitingForWrite && !m_changesSignal) - { - m_changesSignal = true; - m_changesRequestCondition.wait( &m_waitChangesMutex ); - } - m_waitChangesMutex.unlock(); + if (s_renderingThread || s_runningChange) { return; } + m_changeMutex.lock(); + s_runningChange = true; } - - - void AudioEngine::doneChangeInModel() { - if( s_renderingThread ) - return; - - m_changesMutex.lock(); - bool moreChanges = --m_changes; - m_changesMutex.unlock(); - - if( !moreChanges ) - { - m_changesSignal = false; - m_changesAudioEngineCondition.wakeOne(); - } - m_doChangesMutex.unlock(); -} - - - - -void AudioEngine::runChangesInModel() -{ - if( m_changesSignal ) - { - m_waitChangesMutex.lock(); - // allow changes in the model from other threads ... - m_changesRequestCondition.wakeOne(); - // ... and wait until they are done - m_changesAudioEngineCondition.wait( &m_waitChangesMutex ); - m_waitChangesMutex.unlock(); - } + if (s_renderingThread || !s_runningChange) { return; } + m_changeMutex.unlock(); + s_runningChange = false; } bool AudioEngine::isAudioDevNameValid(QString name) @@ -1297,29 +1244,12 @@ void AudioEngine::fifoWriter::run() auto buffer = new surroundSampleFrame[frames]; const surroundSampleFrame * b = m_audioEngine->renderNextBuffer(); memcpy( buffer, b, frames * sizeof( surroundSampleFrame ) ); - write( buffer ); + m_fifo->write(buffer); } // Let audio backend stop processing - write( nullptr ); + m_fifo->write(nullptr); m_fifo->waitUntilRead(); } - - - -void AudioEngine::fifoWriter::write( surroundSampleFrame * buffer ) -{ - m_audioEngine->m_waitChangesMutex.lock(); - m_audioEngine->m_waitingForWrite = true; - m_audioEngine->m_waitChangesMutex.unlock(); - m_audioEngine->runChangesInModel(); - - m_fifo->write( buffer ); - - m_audioEngine->m_doChangesMutex.lock(); - m_audioEngine->m_waitingForWrite = false; - m_audioEngine->m_doChangesMutex.unlock(); -} - } // namespace lmms From 02557041385f5b45ba00f44e6e4e6a3994a97f70 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Sat, 18 Nov 2023 13:06:26 -0800 Subject: [PATCH 041/191] Disable Compressor background autofill (#6986) --- plugins/Compressor/CompressorControlDialog.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 917054c6b..2d04b690e 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -58,7 +58,10 @@ CompressorControlDialog::CompressorControlDialog(CompressorControls* controls) : m_graphColor(209, 216, 228, 50), m_resetColor(200, 100, 15, 200) { - setAutoFillBackground(true); + setAutoFillBackground(false); + setAttribute(Qt::WA_OpaquePaintEvent, true); + setAttribute(Qt::WA_NoSystemBackground, true); + QPalette pal; pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); setPalette(pal); From 17c919879ff5f9e0451fa8c1f4d5e345e4a576e3 Mon Sep 17 00:00:00 2001 From: IanCaio Date: Sat, 18 Nov 2023 19:14:27 -0300 Subject: [PATCH 042/191] Implement Note Types (#5902) * Initial Commit Starts implementing Note Types. The two available types are RegularNote and StepNote. PianoRoll now paints the color with a different color for StepNotes. Pattern::addStep now sets the type of the note to StepNote. Negative size is still used to signal a step note. * Update Pattern.cpp to account for the Note::Type Updates the methods noteAtStep(), addStepNote() and checkType() from Pattern.cpp to account for the note type and not the note length. * Update PatternView::paintEvent to draw step notes PatternView::paintEvent now draws the pattern if the pattern type is BeatPattern and TCOs aren't fixed (Song Editor). Color used is still the BeatPattern color (grey) and the conditional doesn't look very nice and can be improved. Pattern::beatPatternLength was also updated so it accounts for the note type not note length. Review this method, as it looks a bit weird (particularly the second conditional). * Implements StepNotes setting a NPH with 0 frames Now, instead of TimePos returning 0 for negative lengths, we create a NotePlayHandle with 0 frames when the note type is StepNote on InstrumentTrack::play. * Improves PatternView::paintEvent conditional Improves a conditional inside PatternView::paintEvent by reversing the order in which they are executed. * Adds upgrade method for backwards compatibility Adds an upgrade method that converts notes with negative length to StepNotes, so old projects can be loaded properly. Explicitly set the Note::RegularNote value as 0. Make the default "type" value "0", so notes without a type are loaded as RegularNotes. * Addresses Veratil's review - Changes "addStepNote" so "checkType" isn't called twice in a row. - Changes style on a one line conditional. * Uses ternary expression on statement Reduces number of lines by using ternary expression. * Addresses PR review (sakertooth) - Changes class setter to inline - Uses enum class instead of enum - Uses auto and const where appropriate * Finished changes from review (sakertooth) - Used std::max instead of qMax - Fixed style on lines changed in the PR * Uses std::find_if to save codelines As suggested by sakertooth, by using std::find_if we are able to simplify the checkType method to two lines. * Addresses review from sakertooth - Reverts m_detuning in-class initialization - Removes testing warning - Removes unnecessary comment * Addresses DomClark's review - Rename the Note Types enum to avoid redundancy - Uses std::all_of instead of std::find_if on MidiClip checkType - Rewrites addStepNote so it sets the note type before adding it to the clip, avoiding having to manually change the type of the clip after adding the note * Updates MidiExport to use Note Types - Now MidiExport is updated to use note types instead of relying on negative length notes. - For that change it was necessary to find a way of letting MidiExport know how long step notes should be. The solution found was to add an attribute to the Instrument XML called "beatlen", which would hold the number of frames of the instrument's beat. That would be converted to ticks, so we could calculate how long the MIDI notes would have to be to play the whole step note. If the attribute was not found, the default value of 16 ticks would be used as a length of step notes, as a fallback. * Fixes ambiguity on enum usage Due to changes in the name of enum classes, there was an ambiguity caused in NotePlayHandle.cpp. That was fixed. * Addresses new code reviews - Addresses code review from PhysSong and Messmerd * Fixes note drawing on Song Editor - Notes were not being draw on the song editor for BeatClips. This commit fixes this. * Adds cassert header to TimePos.cpp - Adds header to use assert() on TimePos.cpp * Apply suggestions from code review Fixes style on some lines Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Reverts some changes on MidiExport - Some changes were reverted on MidiExport and InstrumentTrack. We were storing the beat length on the XML of Instrument Tracks, but in reality the beat length is a per note attribute, and some instruments could run into a segmentation fault when calling beat length without a NotePlayHandle (i.e.: AFP). Because of that I reverted this change, so the beat length is not stored on the XML anymore, and instead we have a magic number on the MidiExport class that holds a default beat length which is actually an upper limit for the MIDI notes of step notes. In the future we can improve this by finding a way to store the beat length on the note class to use it instead. The MidiExport logic is not worsened at all because previously the beat length wasn't even considered during export (it was actually improved making the exported notes extend until the next one instead of cutting shorter). * Fix the order of included files --------- Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> --- data/themes/classic/style.css | 1 + data/themes/default/style.css | 1 + include/DataFile.h | 3 +- include/Note.h | 12 +++ include/PianoRoll.h | 2 + plugins/MidiExport/MidiExport.cpp | 7 +- plugins/MidiExport/MidiExport.h | 12 +++ src/core/DataFile.cpp | 21 ++++- src/core/Note.cpp | 7 +- src/core/NotePlayHandle.cpp | 2 +- src/core/TimePos.cpp | 13 +-- src/gui/clips/MidiClipView.cpp | 138 ++++++++++++++++-------------- src/gui/editors/PianoRoll.cpp | 10 ++- src/tracks/InstrumentTrack.cpp | 7 +- src/tracks/MidiClip.cpp | 37 ++++---- 15 files changed, 170 insertions(+), 103 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index b378c4b8e..9b50851a3 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -143,6 +143,7 @@ lmms--gui--PianoRoll { qproperty-backgroundShade: rgba( 255, 255, 255, 10 ); qproperty-noteModeColor: rgb( 255, 255, 255 ); qproperty-noteColor: rgb( 119, 199, 216 ); + qproperty-stepNoteColor: #9b1313; qproperty-noteTextColor: rgb( 255, 255, 255 ); qproperty-noteOpacity: 128; qproperty-noteBorders: true; /* boolean property, set false to have borderless notes */ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 323f6d03d..172a67d8e 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -175,6 +175,7 @@ lmms--gui--PianoRoll { qproperty-backgroundShade: rgba(255, 255, 255, 10); qproperty-noteModeColor: #0bd556; qproperty-noteColor: #0bd556; + qproperty-stepNoteColor: #9b1313; qproperty-noteTextColor: #ffffff; qproperty-noteOpacity: 165; qproperty-noteBorders: false; /* boolean property, set false to have borderless notes */ diff --git a/include/DataFile.h b/include/DataFile.h index ceda9b829..3f1706229 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -127,8 +127,9 @@ private: void upgrade_mixerRename(); void upgrade_bbTcoRename(); void upgrade_sampleAndHold(); - void upgrade_midiCCIndexing(); + void upgrade_midiCCIndexing(); void upgrade_loopsRename(); + void upgrade_noteTypes(); // List of all upgrade methods static const std::vector UPGRADE_METHODS; diff --git a/include/Note.h b/include/Note.h index 2df196af2..08cbce3db 100644 --- a/include/Note.h +++ b/include/Note.h @@ -107,6 +107,16 @@ public: Note( const Note & note ); ~Note() override; + // Note types + enum class Type + { + Regular = 0, + Step + }; + + Type type() const { return m_type; } + inline void setType(Type t) { m_type = t; } + // used by GUI inline void setSelected( const bool selected ) { m_selected = selected; } inline void setOldKey( const int oldKey ) { m_oldKey = oldKey; } @@ -253,6 +263,8 @@ private: TimePos m_length; TimePos m_pos; DetuningHelper * m_detuning; + + Type m_type = Type::Regular; }; using NoteVector = std::vector; diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 38788180f..bcaea8637 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -73,6 +73,7 @@ class PianoRoll : public QWidget Q_PROPERTY(QColor lineColor MEMBER m_lineColor) Q_PROPERTY(QColor noteModeColor MEMBER m_noteModeColor) Q_PROPERTY(QColor noteColor MEMBER m_noteColor) + Q_PROPERTY(QColor stepNoteColor MEMBER m_stepNoteColor) Q_PROPERTY(QColor ghostNoteColor MEMBER m_ghostNoteColor) Q_PROPERTY(QColor noteTextColor MEMBER m_noteTextColor) Q_PROPERTY(QColor ghostNoteTextColor MEMBER m_ghostNoteTextColor) @@ -466,6 +467,7 @@ private: QColor m_lineColor; QColor m_noteModeColor; QColor m_noteColor; + QColor m_stepNoteColor; QColor m_noteTextColor; QColor m_ghostNoteColor; QColor m_ghostNoteTextColor; diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index df968e36a..2600a40f2 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -27,6 +27,7 @@ #include "MidiExport.h" +#include "Engine.h" #include "TrackContainer.h" #include "DataFile.h" #include "InstrumentTrack.h" @@ -279,6 +280,7 @@ void MidiExport::writeMidiClip(MidiNoteVector &midiClip, const QDomNode& n, mnote.volume = qMin(qRound(base_volume * LocaleHelper::toDouble(note.attribute("vol", "100")) * (127.0 / 200.0)), 127); mnote.time = base_time + note.attribute("pos", "0").toInt(); mnote.duration = note.attribute("len", "0").toInt(); + mnote.type = static_cast(note.attribute("type", "0").toInt()); midiClip.push_back(mnote); } } @@ -311,6 +313,7 @@ void MidiExport::writePatternClip(MidiNoteVector& src, MidiNoteVector& dst, note.pitch = srcNote.pitch; note.time = base + time; note.volume = srcNote.volume; + note.type = srcNote.type; dst.push_back(note); } } @@ -329,9 +332,9 @@ void MidiExport::processPatternNotes(MidiNoteVector& nv, int cutPos) next = cur; cur = it->time; } - if (it->duration < 0) + if (it->type == Note::Type::Step) { - it->duration = qMin(qMin(-it->duration, next - cur), cutPos - it->time); + it->duration = qMin(qMin(DefaultBeatLength, next - cur), cutPos - it->time); } } } diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 1e355e45a..7c77c7af2 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -30,6 +30,7 @@ #include "ExportFilter.h" #include "MidiFile.hpp" +#include "Note.h" class QDomNode; @@ -46,6 +47,7 @@ struct MidiNote uint8_t pitch; int duration; uint8_t volume; + Note::Type type; inline bool operator<(const MidiNote &b) const { @@ -63,6 +65,16 @@ public: MidiExport(); ~MidiExport() override = default; + // Default Beat Length in ticks for step notes + // TODO: The beat length actually varies per note, however the method that + // calculates it (InstrumentTrack::beatLen) requires a NotePlayHandle to do + // so. While we don't figure out a way to hold the beat length of each note + // on its member variables, we will use a default value as a beat length that + // will be used as an upper limit of the midi note length. This doesn't worsen + // the current logic used for MidiExport because right now the beat length is + // not even considered during the generation of the MIDI. + static constexpr int DefaultBeatLength = 1500; + gui::PluginView* instantiateView(QWidget *) override { return nullptr; diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index b83e1bebb..a520e6bc5 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -43,6 +43,7 @@ #include "embed.h" #include "GuiApplication.h" #include "LocaleHelper.h" +#include "Note.h" #include "PluginFactory.h" #include "ProjectVersion.h" #include "SongEditor.h" @@ -81,7 +82,7 @@ const std::vector DataFile::UPGRADE_METHODS = { &DataFile::upgrade_defaultTripleOscillatorHQ, &DataFile::upgrade_mixerRename , &DataFile::upgrade_bbTcoRename, &DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing, - &DataFile::upgrade_loopsRename + &DataFile::upgrade_loopsRename , &DataFile::upgrade_noteTypes }; // Vector of all versions that have upgrade routines. @@ -1666,6 +1667,24 @@ void DataFile::upgrade_automationNodes() } } +// Convert the negative length notes to StepNotes +void DataFile::upgrade_noteTypes() +{ + const auto notes = elementsByTagName("note"); + + for (int i = 0; i < notes.size(); ++i) + { + auto note = notes.item(i).toElement(); + + const auto noteSize = note.attribute("len").toInt(); + if (noteSize < 0) + { + note.setAttribute("len", DefaultTicksPerBar / 16); + note.setAttribute("type", static_cast(Note::Type::Step)); + } + } +} + /** \brief Note range has been extended to match MIDI specification * diff --git a/src/core/Note.cpp b/src/core/Note.cpp index a4ad61412..ed3a00f10 100644 --- a/src/core/Note.cpp +++ b/src/core/Note.cpp @@ -74,7 +74,8 @@ Note::Note( const Note & note ) : m_panning( note.m_panning ), m_length( note.m_length ), m_pos( note.m_pos ), - m_detuning( nullptr ) + m_detuning(nullptr), + m_type(note.m_type) { if( note.m_detuning ) { @@ -179,6 +180,7 @@ void Note::saveSettings( QDomDocument & doc, QDomElement & parent ) parent.setAttribute( "pan", m_panning ); parent.setAttribute( "len", m_length ); parent.setAttribute( "pos", m_pos ); + parent.setAttribute("type", static_cast(m_type)); if( m_detuning && m_length ) { @@ -197,6 +199,9 @@ void Note::loadSettings( const QDomElement & _this ) m_panning = _this.attribute( "pan" ).toInt(); m_length = _this.attribute( "len" ).toInt(); m_pos = _this.attribute( "pos" ).toInt(); + // Default m_type value is 0, which corresponds to RegularNote + static_assert(0 == static_cast(Type::Regular)); + m_type = static_cast(_this.attribute("type", "0").toInt()); if( _this.hasChildNodes() ) { diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index eb9c7ddbf..712b64e89 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -53,7 +53,7 @@ NotePlayHandle::NotePlayHandle( InstrumentTrack* instrumentTrack, NotePlayHandle *parent, int midiEventChannel, Origin origin ) : - PlayHandle( Type::NotePlayHandle, _offset ), + PlayHandle( PlayHandle::Type::NotePlayHandle, _offset ), Note( n.length(), n.pos(), n.key(), n.getVolume(), n.getPanning(), n.detuning() ), m_pluginData( nullptr ), m_instrumentTrack( instrumentTrack ), diff --git a/src/core/TimePos.cpp b/src/core/TimePos.cpp index 86a65f103..09c1019bc 100644 --- a/src/core/TimePos.cpp +++ b/src/core/TimePos.cpp @@ -25,6 +25,7 @@ #include "TimePos.h" +#include #include "MeterModel.h" namespace lmms @@ -161,11 +162,11 @@ tick_t TimePos::getTickWithinBeat( const TimeSig &sig ) const f_cnt_t TimePos::frames( const float framesPerTick ) const { - if( m_ticks >= 0 ) - { - return static_cast( m_ticks * framesPerTick ); - } - return 0; + // Before, step notes used to have negative length. This + // assert is a safeguard against negative length being + // introduced again (now using Note Types instead #5902) + assert(m_ticks >= 0); + return static_cast(m_ticks * framesPerTick); } double TimePos::getTimeInMilliseconds( bpm_t beatsPerMinute ) const @@ -221,4 +222,4 @@ double TimePos::ticksToMilliseconds(double ticks, bpm_t beatsPerMinute) } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 151df8d3c..b13d6e003 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -25,6 +25,7 @@ #include "MidiClipView.h" +#include #include #include #include @@ -458,9 +459,78 @@ void MidiClipView::paintEvent( QPaintEvent * ) const int x_base = BORDER_WIDTH; bool displayPattern = fixedClips() || (pixelsPerBar >= 96 && m_legacySEPattern); - // melody clip paint event NoteVector const & noteCollection = m_clip->m_notes; - if( m_clip->m_clipType == MidiClip::Type::MelodyClip && !noteCollection.empty() ) + + // Beat clip paint event (on BB Editor) + if (beatClip && displayPattern) + { + QPixmap stepon0; + QPixmap stepon200; + QPixmap stepoff; + QPixmap stepoffl; + const int steps = std::max(1, m_clip->m_steps); + const int w = width() - 2 * BORDER_WIDTH; + + // scale step graphics to fit the beat clip length + stepon0 = s_stepBtnOn0->scaled(w / steps, + s_stepBtnOn0->height(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + stepon200 = s_stepBtnOn200->scaled(w / steps, + s_stepBtnOn200->height(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + stepoff = s_stepBtnOff->scaled(w / steps, + s_stepBtnOff->height(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + stepoffl = s_stepBtnOffLight->scaled(w / steps, + s_stepBtnOffLight->height(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + + for (int it = 0; it < steps; it++) // go through all the steps in the beat clip + { + Note* n = m_clip->noteAtStep(it); + + // figure out x and y coordinates for step graphic + const int x = BORDER_WIDTH + static_cast(it * w / steps); + const int y = height() - s_stepBtnOff->height() - 1; + + if (n) + { + const int vol = n->getVolume(); + p.drawPixmap(x, y, stepoffl); + p.drawPixmap(x, y, stepon0); + p.setOpacity(std::sqrt(vol / 200.0)); + p.drawPixmap(x, y, stepon200); + p.setOpacity(1); + } + else if ((it / 4) % 2) + { + p.drawPixmap(x, y, stepoffl); + } + else + { + p.drawPixmap(x, y, stepoff); + } + } // end for loop + + // draw a transparent rectangle over muted clips + if (muted) + { + p.setBrush(mutedBackgroundColor()); + p.setOpacity(0.5); + p.drawRect(0, 0, width(), height()); + } + } + // Melody clip and Beat clip (on Song Editor) paint event + else if + ( + !noteCollection.empty() && + (m_clip->m_clipType == MidiClip::Type::MelodyClip || + m_clip->m_clipType == MidiClip::Type::BeatClip) + ) { // Compute the minimum and maximum key in the clip // so that we know how much there is to draw. @@ -574,70 +644,6 @@ void MidiClipView::paintEvent( QPaintEvent * ) p.restore(); } - // beat clip paint event - else if (beatClip && displayPattern) - { - QPixmap stepon0; - QPixmap stepon200; - QPixmap stepoff; - QPixmap stepoffl; - const int steps = qMax( 1, - m_clip->m_steps ); - const int w = width() - 2 * BORDER_WIDTH; - - // scale step graphics to fit the beat clip length - stepon0 = s_stepBtnOn0->scaled( w / steps, - s_stepBtnOn0->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - stepon200 = s_stepBtnOn200->scaled( w / steps, - s_stepBtnOn200->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - stepoff = s_stepBtnOff->scaled( w / steps, - s_stepBtnOff->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - stepoffl = s_stepBtnOffLight->scaled( w / steps, - s_stepBtnOffLight->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation ); - - for( int it = 0; it < steps; it++ ) // go through all the steps in the beat clip - { - Note * n = m_clip->noteAtStep( it ); - - // figure out x and y coordinates for step graphic - const int x = BORDER_WIDTH + static_cast( it * w / steps ); - const int y = height() - s_stepBtnOff->height() - 1; - - if( n ) - { - const int vol = n->getVolume(); - p.drawPixmap( x, y, stepoffl ); - p.drawPixmap( x, y, stepon0 ); - p.setOpacity( sqrt( vol / 200.0 ) ); - p.drawPixmap( x, y, stepon200 ); - p.setOpacity( 1 ); - } - else if( ( it / 4 ) % 2 ) - { - p.drawPixmap( x, y, stepoffl ); - } - else - { - p.drawPixmap( x, y, stepoff ); - } - } // end for loop - - // draw a transparent rectangle over muted clips - if ( muted ) - { - p.setBrush( mutedBackgroundColor() ); - p.setOpacity( 0.5 ); - p.drawRect( 0, 0, width(), height() ); - } - } // bar lines const int lineSize = 3; diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 6bf1b5daf..2d8f9cbc2 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -3498,11 +3498,15 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) // is the note in visible area? if (note->key() > bottomKey && note->key() <= topKey) { - // we've done and checked all, let's draw the note + // We've done and checked all, let's draw the note with + // the appropriate color + const auto fillColor = note->type() == Note::Type::Regular ? m_noteColor : m_stepNoteColor; + drawNoteRect( p, x + m_whiteKeyWidth, noteYPos(note->key()), note_width, - note, m_noteColor, m_noteTextColor, m_selectedNoteColor, - m_noteOpacity, m_noteBorders, drawNoteNames); + note, fillColor, m_noteTextColor, m_selectedNoteColor, + m_noteOpacity, m_noteBorders, drawNoteNames + ); } // draw note editing stuff diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 8804833ee..4b00e0d79 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -776,8 +776,11 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, while( nit != notes.end() && ( cur_note = *nit )->pos() == cur_start ) { - const f_cnt_t note_frames = - cur_note->length().frames( frames_per_tick ); + // If the note is a Step Note, frames will be 0 so the NotePlayHandle + // plays for the whole length of the sample + const auto note_frames = cur_note->type() == Note::Type::Step + ? 0 + : cur_note->length().frames(frames_per_tick); NotePlayHandle* notePlayHandle = NotePlayHandleManager::acquire( this, _offset, note_frames, *cur_note ); notePlayHandle->setPatternTrack(pattern_track); diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index 490f6e6d0..087079dc8 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -25,6 +25,7 @@ #include "MidiClip.h" +#include #include #include "GuiApplication.h" @@ -174,19 +175,18 @@ TimePos MidiClip::beatClipLength() const for (const auto& note : m_notes) { - if (note->length() < 0) + if (note->type() == Note::Type::Step) { max_length = std::max(max_length, note->pos() + 1); } } - if( m_steps != TimePos::stepsPerBar() ) + if (m_steps != TimePos::stepsPerBar()) { - max_length = m_steps * TimePos::ticksPerBar() / - TimePos::stepsPerBar(); + max_length = m_steps * TimePos::ticksPerBar() / TimePos::stepsPerBar(); } - return TimePos( max_length ).nextFullBar() * TimePos::ticksPerBar(); + return TimePos{max_length}.nextFullBar() * TimePos::ticksPerBar(); } @@ -235,13 +235,13 @@ void MidiClip::removeNote( Note * _note_to_del ) } -// returns a pointer to the note at specified step, or NULL if note doesn't exist - -Note * MidiClip::noteAtStep( int _step ) +// Returns a pointer to the note at specified step, or nullptr if note doesn't exist +Note * MidiClip::noteAtStep(int step) { for (const auto& note : m_notes) { - if (note->pos() == TimePos::stepPosition(_step) && note->length() < 0) + if (note->pos() == TimePos::stepPosition(step) + && note->type() == Note::Type::Step) { return note; } @@ -278,8 +278,10 @@ void MidiClip::clearNotes() Note * MidiClip::addStepNote( int step ) { - return addNote( Note( TimePos( -DefaultTicksPerBar ), - TimePos::stepPosition( step ) ), false ); + Note stepNote = Note(TimePos(DefaultTicksPerBar / 16), TimePos::stepPosition(step)); + stepNote.setType(Note::Type::Step); + + return addNote(stepNote, false); } @@ -351,15 +353,10 @@ void MidiClip::setType( Type _new_clip_type ) void MidiClip::checkType() { - for (auto& note : m_notes) - { - if (note->length() > 0) - { - setType(Type::MelodyClip); - return; - } - } - setType( Type::BeatClip ); + // If all notes are StepNotes, we have a BeatClip + const auto beatClip = std::all_of(m_notes.begin(), m_notes.end(), [](auto note) { return note->type() == Note::Type::Step; }); + + setType(beatClip ? Type::BeatClip : Type::MelodyClip); } From fad001150858cb3dd74e44e82545a906904781b5 Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 18 Nov 2023 20:47:15 -0500 Subject: [PATCH 043/191] Analyze and improve search in the file browser (again) (#6985) Improves performance when searching in the file browser (confirmed with profiling using KCacheGrind), adds a search indicator at the bottom to let the user know a search is in progress, blacklists unnecessary system directories (speeding up both the search speed and potentially load times as well by reducing the number of filesystem entries to consider), and fixes an issue that causes not all of the search results to appear. --- include/FileBrowser.h | 82 +++----- include/FileBrowserSearcher.h | 148 ++++++++++++++ src/gui/CMakeLists.txt | 1 + src/gui/FileBrowser.cpp | 352 +++++++++++--------------------- src/gui/FileBrowserSearcher.cpp | 135 ++++++++++++ 5 files changed, 422 insertions(+), 296 deletions(-) create mode 100644 include/FileBrowserSearcher.h create mode 100644 src/gui/FileBrowserSearcher.cpp diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 02fec2719..4d6fa745e 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -29,24 +29,16 @@ #include #include -#ifdef __MINGW32__ -#include -#include -#include -#else -#include -#include -#include -#endif +#include "FileBrowserSearcher.h" +#include #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include #endif #include - #include "SideBarWidget.h" - +#include "lmmsconfig.h" class QLineEdit; @@ -83,12 +75,25 @@ public: ~FileBrowser() override = default; - static QDir::Filters dirFilters(); + static QStringList directoryBlacklist() + { + static auto s_blacklist = QStringList{ +#ifdef LMMS_BUILD_LINUX + "/bin", "/boot", "/dev", "/etc", "/proc", "/run", "/sbin", + "/sys" +#endif +#ifdef LMMS_BUILD_WIN32 + "C:\\Windows" +#endif + }; + return s_blacklist; + } + static QDir::Filters dirFilters() { return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; } + static QDir::SortFlags sortFlags() { return QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase; } private slots: void reloadTree(); void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); - bool filterAndExpandItems(const QString & filter, QTreeWidgetItem * item = nullptr); void giveFocusToFilter(); private: @@ -99,7 +104,7 @@ private: void saveDirectoriesStates(); void restoreDirectoriesStates(); - void buildSearchTree(QStringList matches, QString id); + void buildSearchTree(); void onSearch(const QString& filter); void toggleSearch(bool on); @@ -108,6 +113,9 @@ private: QLineEdit * m_filterEdit; + std::shared_ptr m_currentSearch; + QProgressBar* m_searchIndicator = nullptr; + QString m_directories; //!< Directories to search, split with '*' QString m_filter; //!< Filter as used in QDir::match() @@ -183,54 +191,12 @@ private slots: } ; -class FileBrowserSearcher : public QObject -{ - Q_OBJECT -public: - struct SearchTask - { - QString directories; - QString userFilter; - QDir::Filters dirFilters; - QStringList nameFilters; - QString id; - }; - - FileBrowserSearcher(); - ~FileBrowserSearcher() noexcept override; - - void search(SearchTask task); - void cancel(); - - bool inHiddenDirectory(const QString& path); - - static FileBrowserSearcher* instance(); - -signals: - void searchComplete(QStringList matches, QString id); - -private: - void run(); - void filter(); - SearchTask m_currentTask; - std::thread m_worker; - std::mutex m_runMutex; - std::mutex m_cancelMutex; - std::condition_variable m_runCond; - std::atomic m_cancel = false; - bool m_stopped = false; - bool m_run = false; - inline static std::unique_ptr s_instance = nullptr; -}; - - class Directory : public QTreeWidgetItem { public: - Directory( const QString & filename, const QString & path, - const QString & filter ); + Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation = false); void update(); @@ -275,7 +241,7 @@ private: QString m_filter; int m_dirCount; - + bool m_disableEntryPopulation = false; } ; diff --git a/include/FileBrowserSearcher.h b/include/FileBrowserSearcher.h new file mode 100644 index 000000000..4f4d3ff1c --- /dev/null +++ b/include/FileBrowserSearcher.h @@ -0,0 +1,148 @@ +/* + * FileBrowserSearcher.h - Batch processor for searching the filesystem + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_FILE_BROWSER_SEARCHER_H +#define LMMS_FILE_BROWSER_SEARCHER_H + +#include +#include +#include +#include +#include + +#ifdef __MINGW32__ +#include +#include +#include +#else +#include +#include +#include +#endif + +namespace lmms::gui { + +//! An active object that handles searching for files that match a certain filter across the file system. +class FileBrowserSearcher +{ +public: + //! Number of milliseconds to wait for before a match should be processed by the user. + static constexpr int MillisecondsPerMatch = 1; + + //! The future object for FileBrowserSearcher. It is used to track the current state of search operations, as + // well as retrieve matches. + class SearchFuture + { + public: + //! Possible state values of the future object. + enum class State + { + Idle, + Running, + Cancelled, + Completed + }; + + //! Constructs a future object using the specified filter, paths, and valid file extensions in the Idle state. + SearchFuture(const QString& filter, const QStringList& paths, const QStringList& extensions) + : m_filter(filter) + , m_paths(paths) + , m_extensions(extensions) + { + } + + //! Retrieves a match from the match list. + auto match() -> QString + { + const auto lock = std::lock_guard{m_matchesMutex}; + return m_matches.empty() ? QString{} : m_matches.takeFirst(); + } + + //! Returns the current state of this future object. + auto state() -> State { return m_state; } + + //! Returns the filter used. + auto filter() -> const QString& { return m_filter; } + + //! Returns the paths to filter. + auto paths() -> const QStringList& { return m_paths; } + + //! Returns the valid file extensions. + auto extensions() -> const QStringList& { return m_extensions; } + + private: + //! Adds a match to the match list. + auto addMatch(const QString& match) -> void + { + const auto lock = std::lock_guard{m_matchesMutex}; + m_matches.append(match); + } + + QString m_filter; + QStringList m_paths; + QStringList m_extensions; + + QStringList m_matches; + std::mutex m_matchesMutex; + + std::atomic m_state = State::Idle; + + friend FileBrowserSearcher; + }; + + ~FileBrowserSearcher(); + + //! Enqueues a search to be ran by the worker thread. + //! Returns a future that the caller can use to track state and results of the operation. + auto search(const QString& filter, const QStringList& paths, const QStringList& extensions) + -> std::shared_ptr; + + //! Sends a signal to cancel a running search. + auto cancel() -> void { m_cancelRunningSearch = true; } + + //! Returns the global instance of the searcher object. + static auto instance() -> FileBrowserSearcher* + { + static auto s_instance = FileBrowserSearcher{}; + return &s_instance; + } + +private: + //! Event loop for the worker thread. + auto run() -> void; + + //! Using Depth-first search (DFS), filters the specified path and adds any matches to the future list. + auto process(SearchFuture* searchFuture, const QString& path) -> bool; + + std::queue> m_searchQueue; + std::atomic m_cancelRunningSearch = false; + + bool m_workerStopped = false; + std::mutex m_workerMutex; + std::condition_variable m_workerCond; + std::thread m_worker{[this] { run(); }}; +}; +} // namespace lmms::gui + +#endif // LMMS_FILE_BROWSER_SEARCHER_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1e809e9d7..e050d14bd 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -14,6 +14,7 @@ SET(LMMS_SRCS gui/EffectView.cpp gui/embed.cpp gui/FileBrowser.cpp + gui/FileBrowserSearcher.cpp gui/GuiApplication.cpp gui/LadspaControlView.cpp gui/LfoControllerDialog.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index dc16a3bac..f0b7e83ec 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -39,7 +39,10 @@ #include #include +#include + #include "FileBrowser.h" +#include "FileBrowserSearcher.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "DataFile.h" @@ -130,7 +133,6 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); connect(m_filterEdit, &QLineEdit::textEdited, this, &FileBrowser::onSearch); - connect(FileBrowserSearcher::instance(), &FileBrowserSearcher::searchComplete, this, &FileBrowser::buildSearchTree); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); @@ -149,6 +151,15 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_searchTreeWidget->hide(); addContentWidget(m_searchTreeWidget); + auto searchTimer = new QTimer(this); + connect(searchTimer, &QTimer::timeout, this, &FileBrowser::buildSearchTree); + searchTimer->start(FileBrowserSearcher::MillisecondsPerMatch); + + m_searchIndicator = new QProgressBar(this); + m_searchIndicator->setMinimum(0); + m_searchIndicator->setMaximum(100); + addContentWidget(m_searchIndicator); + // Whenever the FileBrowser has focus, Ctrl+F should direct focus to its filter box. auto filterFocusShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this, SLOT(giveFocusToFilter())); filterFocusShortcut->setContext(Qt::WidgetWithChildrenShortcut); @@ -159,11 +170,6 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, show(); } -QDir::Filters FileBrowser::dirFilters() -{ - return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; -} - void FileBrowser::saveDirectoriesStates() { m_savedExpandedDirs = m_fileBrowserTreeWidget->expandedDirs(); @@ -174,72 +180,104 @@ void FileBrowser::restoreDirectoriesStates() expandItems(nullptr, m_savedExpandedDirs); } -void FileBrowser::buildSearchTree(QStringList matches, QString id) +void FileBrowser::buildSearchTree() { - if (title() != id) { return; } + if (!m_currentSearch) { return; } - m_searchTreeWidget->clear(); - - const auto rootPaths = m_directories.split('*'); - for (const auto& rootPath : rootPaths) + const auto match = m_currentSearch->match(); + using State = FileBrowserSearcher::SearchFuture::State; + if ((m_currentSearch->state() == State::Completed && match.isEmpty()) + || m_currentSearch->state() == State::Cancelled) { - const auto rootPathDir = QDir{rootPath}; - const auto absoluteRootPath = rootPathDir.absolutePath(); + m_currentSearch = nullptr; + m_searchIndicator->setMaximum(100); + return; + } + else if (match.isEmpty()) { return; } - for (const auto& match : matches) - { - if (!match.startsWith(absoluteRootPath)) { continue; } - - const auto childInfo = QFileInfo{match}; - const auto childName = childInfo.fileName(); - const auto parentPath = childInfo.dir().path(); - auto childWidget = static_cast(nullptr); - - if (childInfo.isDir()) - { - auto dirChildWidget = new Directory(childName, parentPath, m_filter); - dirChildWidget->update(); - childWidget = dirChildWidget; - } - else if (childInfo.isFile()) { childWidget = new FileItem(childName, parentPath); } - else { continue; } - - const auto relativeParentPath = rootPathDir.relativeFilePath(parentPath); - if (relativeParentPath == ".") - { - m_searchTreeWidget->addTopLevelItem(childWidget); - if (childInfo.isDir()) { m_searchTreeWidget->expandItem(childWidget); } - continue; - } - - const auto grandParentPath = QFileInfo{parentPath}.dir().path(); - const auto parentItems = m_searchTreeWidget->findItems(relativeParentPath, Qt::MatchExactly); - - if (parentItems.isEmpty()) - { - auto parentItem = new Directory(relativeParentPath, grandParentPath, m_filter); - parentItem->addChild(childWidget); - m_searchTreeWidget->addTopLevelItem(parentItem); - m_searchTreeWidget->expandItem(parentItem); - } - else { parentItems[0]->addChild(childWidget); } - } + auto basePath = QString{}; + for (const auto& path : m_directories.split('*')) + { + if (!match.startsWith(QDir{path}.absolutePath())) { continue; } + basePath = path; + break; } - toggleSearch(true); + if (basePath.isEmpty()) { return; } + + const auto baseDir = QDir{basePath}; + const auto matchInfo = QFileInfo{match}; + const auto matchRelativeToBasePath = baseDir.relativeFilePath(match); + + auto pathParts = QDir::cleanPath(matchRelativeToBasePath).split("/"); + auto currentItem = static_cast(nullptr); + auto currentDir = baseDir; + + for (const auto& pathPart : pathParts) + { + auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount(); + auto childItem = static_cast(nullptr); + + for (int i = 0; i < childCount; ++i) + { + auto item = currentItem ? currentItem->child(i) : m_searchTreeWidget->topLevelItem(i); + if (item->text(0) == pathPart) + { + childItem = item; + break; + } + + } + + if (!childItem) + { + auto pathPartInfo = QFileInfo(currentDir, pathPart); + if (pathPartInfo.isDir()) + { + // Only update directory (i.e., add entries) when it is the matched directory (so do not update + // parents since entries would be added to them that did not match the filter) + const auto disablePopulation = pathParts.indexOf(pathPart) < pathParts.size() - 1; + + auto item = new Directory(pathPart, currentDir.path(), m_filter, disablePopulation); + currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item); + item->update(); + if (disablePopulation) { m_searchTreeWidget->expandItem(item); } + childItem = item; + } + else + { + auto item = new FileItem(pathPart, currentDir.path()); + currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item); + childItem = item; + } + } + + currentItem = childItem; + if (!currentDir.cd(pathPart)) { break; } + } } void FileBrowser::onSearch(const QString& filter) { - auto instance = FileBrowserSearcher::instance(); if (filter.isEmpty()) { toggleSearch(false); - instance->cancel(); + FileBrowserSearcher::instance()->cancel(); return; } - instance->search({m_directories, filter, dirFilters(), m_filter.split(' '), title()}); + + auto directories = m_directories.split('*'); + if (m_showUserContent && !m_showUserContent->isChecked()) { directories.removeAll(m_userDir); } + if (m_showFactoryContent && !m_showFactoryContent->isChecked()) { directories.removeAll(m_factoryDir); } + if (directories.isEmpty()) { return; } + + m_searchTreeWidget->clear(); + toggleSearch(true); + + auto browserExtensions = m_filter; + const auto searchExtensions = browserExtensions.remove("*.").split(' '); + m_currentSearch = FileBrowserSearcher::instance()->search(filter, directories, searchExtensions); } void FileBrowser::toggleSearch(bool on) @@ -248,90 +286,13 @@ void FileBrowser::toggleSearch(bool on) { m_searchTreeWidget->show(); m_fileBrowserTreeWidget->hide(); + m_searchIndicator->setMaximum(0); return; } m_searchTreeWidget->hide(); m_fileBrowserTreeWidget->show(); -} - -bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item) -{ - // Call with item = nullptr to filter the entire tree - - if (item == nullptr) - { - // First search character so need to save current expanded directories - if (m_previousFilterValue.isEmpty()) - { - saveDirectoriesStates(); - } - - m_previousFilterValue = filter; - } - - if (filter.isEmpty()) - { - // Restore previous expanded directories - if (item == nullptr) - { - restoreDirectoriesStates(); - } - - return false; - } - - bool anyMatched = false; - - int numChildren = item ? item->childCount() : m_fileBrowserTreeWidget->topLevelItemCount(); - - for (int i = 0; i < numChildren; ++i) - { - QTreeWidgetItem * it = item ? item->child( i ) : m_fileBrowserTreeWidget->topLevelItem(i); - - auto d = dynamic_cast(it); - if (d) - { - if (it->text(0).contains(filter, Qt::CaseInsensitive)) - { - it->setHidden(false); - it->setExpanded(true); - filterAndExpandItems(QString(), it); - anyMatched = true; - } - else - { - // Expanding is required when recursive to load in its contents, even if it's collapsed right afterward - it->setExpanded(true); - - bool didMatch = filterAndExpandItems(filter, it); - it->setHidden(!didMatch); - it->setExpanded(didMatch); - anyMatched = anyMatched || didMatch; - } - } - - else - { - auto f = dynamic_cast(it); - if (f) - { - // File - bool didMatch = it->text(0).contains(filter, Qt::CaseInsensitive); - it->setHidden(!didMatch); - anyMatched = anyMatched || didMatch; - } - - // A standard item (i.e. no file or directory item?) - else - { - // Hide if there's any filter - it->setHidden(!filter.isEmpty()); - } - } - } - - return anyMatched; + m_searchIndicator->setMaximum(100); } @@ -370,7 +331,7 @@ void FileBrowser::reloadTree() } else { - filterAndExpandItems(m_filterEdit->text()); + onSearch(m_filterEdit->text()); } } @@ -416,6 +377,8 @@ void FileBrowser::giveFocusToFilter() void FileBrowser::addItems(const QString & path ) { + if (FileBrowser::directoryBlacklist().contains(path)) { return; } + if( m_dirsAsItems ) { m_fileBrowserTreeWidget->addTopLevelItem( new Directory( path, QString(), m_filter ) ); @@ -429,6 +392,8 @@ void FileBrowser::addItems(const QString & path ) m_filter.split(' '), dirFilters(), QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); for (const auto& entry : entries) { + if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + QString fileName = entry.fileName(); if (entry.isDir()) { @@ -1047,107 +1012,16 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item ) } - -FileBrowserSearcher::FileBrowserSearcher() - : m_worker([this] { run(); }) -{ -} - -FileBrowserSearcher::~FileBrowserSearcher() noexcept -{ - m_cancel = true; - { - const auto runLock = std::lock_guard{m_runMutex}; - m_stopped = true; - m_cancel = false; - } - m_runCond.notify_one(); - m_worker.join(); -} - -void FileBrowserSearcher::search(SearchTask task) -{ - m_cancel = true; - { - const auto runLock = std::lock_guard{m_runMutex}; - m_currentTask = std::move(task); - m_run = true; - m_cancel = false; - } - m_runCond.notify_one(); -} - -void FileBrowserSearcher::cancel() -{ - m_cancel = true; -} - -void FileBrowserSearcher::run() -{ - while (true) - { - auto lock = std::unique_lock{m_runMutex}; - m_runCond.wait(lock, [this] { return m_run || m_stopped; }); - - if (m_stopped) { break; } - - filter(); - m_run = false; - } -} - -void FileBrowserSearcher::filter() -{ - const auto& [directories, userFilter, filters, nameFilters, id] = m_currentTask; - const auto paths = directories.split('*'); - auto matches = QStringList{}; - - for (const auto& path : paths) - { - auto it = QDirIterator{path, nameFilters, filters, QDirIterator::Subdirectories}; - while (it.hasNext()) - { - it.next(); - const auto name = it.fileName(); - const auto path = it.filePath(); - if (!inHiddenDirectory(path) && name.contains(userFilter, Qt::CaseInsensitive)) { matches.push_back(path); } - if (m_cancel) { return; } - } - } - - emit searchComplete(matches, id); -} - -FileBrowserSearcher* FileBrowserSearcher::instance() -{ - if (!s_instance) { s_instance = std::make_unique(); } - return s_instance.get(); -} - -bool FileBrowserSearcher::inHiddenDirectory(const QString& path) -{ - auto dir = QDir{path}; - while (!dir.isRoot()) - { - auto info = QFileInfo{dir.path()}; - if (info.isHidden()) { return true; } - dir.cdUp(); - } - return false; -} - - QPixmap * Directory::s_folderPixmap = nullptr; QPixmap * Directory::s_folderOpenedPixmap = nullptr; QPixmap * Directory::s_folderLockedPixmap = nullptr; - -Directory::Directory(const QString & filename, const QString & path, - const QString & filter ) : - QTreeWidgetItem( QStringList( filename ), TypeDirectoryItem ), - m_directories( path ), - m_filter( filter ), - m_dirCount( 0 ) +Directory::Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation) + : QTreeWidgetItem(QStringList(filename), TypeDirectoryItem) + , m_directories(path) + , m_filter(filter) + , m_dirCount(0) + , m_disableEntryPopulation(disableEntryPopulation) { initPixmaps(); @@ -1199,7 +1073,7 @@ void Directory::update() } setIcon( 0, *s_folderOpenedPixmap ); - if( !childCount() ) + if (!m_disableEntryPopulation && !childCount()) { m_dirCount = 0; // for all paths leading here, add their items @@ -1232,17 +1106,19 @@ void Directory::update() bool Directory::addItems(const QString& path) { + if (FileBrowser::directoryBlacklist().contains(path)) { return false; } + QDir thisDir(path); if (!thisDir.isReadable()) { return false; } treeWidget()->setUpdatesEnabled(false); - QFileInfoList entries = thisDir.entryInfoList( - m_filter.split(' '), - QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, - QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + QFileInfoList entries + = thisDir.entryInfoList(m_filter.split(' '), FileBrowser::dirFilters(), FileBrowser::sortFlags()); for (const auto& entry : entries) { + if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + QString fileName = entry.fileName(); if (entry.isDir()) { diff --git a/src/gui/FileBrowserSearcher.cpp b/src/gui/FileBrowserSearcher.cpp new file mode 100644 index 000000000..80c238058 --- /dev/null +++ b/src/gui/FileBrowserSearcher.cpp @@ -0,0 +1,135 @@ +/* + * FileBrowserSearcher.cpp - Batch processor for searching the filesystem + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "FileBrowserSearcher.h" + +#include +#include + +#include "FileBrowser.h" + +namespace lmms::gui { + +FileBrowserSearcher::~FileBrowserSearcher() +{ + m_cancelRunningSearch = true; + + { + const auto lock = std::lock_guard{m_workerMutex}; + m_workerStopped = true; + } + + m_workerCond.notify_one(); + m_worker.join(); +} + +auto FileBrowserSearcher::search(const QString& filter, const QStringList& paths, const QStringList& extensions) + -> std::shared_ptr +{ + m_cancelRunningSearch = true; + auto future = std::make_shared(filter, paths, extensions); + + { + const auto lock = std::lock_guard{m_workerMutex}; + m_searchQueue.push(future); + m_cancelRunningSearch = false; + } + + m_workerCond.notify_one(); + return future; +} + +auto FileBrowserSearcher::run() -> void +{ + while (true) + { + auto lock = std::unique_lock{m_workerMutex}; + m_workerCond.wait(lock, [this] { return m_workerStopped || !m_searchQueue.empty(); }); + + if (m_workerStopped) { return; } + + const auto future = m_searchQueue.front(); + future->m_state = SearchFuture::State::Running; + m_searchQueue.pop(); + + auto cancelled = false; + for (const auto& path : future->m_paths) + { + if (FileBrowser::directoryBlacklist().contains(path)) { continue; } + + if (!process(future.get(), path)) + { + future->m_state = SearchFuture::State::Cancelled; + cancelled = true; + break; + } + } + + if (!cancelled) { future->m_state = SearchFuture::State::Completed; } + } +} + +auto FileBrowserSearcher::process(SearchFuture* searchFuture, const QString& path) -> bool +{ + auto stack = QFileInfoList{}; + + auto dir = QDir{path}; + stack.append(dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags())); + + while (!stack.empty()) + { + if (m_cancelRunningSearch) + { + m_cancelRunningSearch = false; + return false; + } + + const auto info = stack.takeFirst(); + const auto path = info.absoluteFilePath(); + if (FileBrowser::directoryBlacklist().contains(path)) { continue; } + + const auto name = info.fileName(); + const auto validFile = info.isFile() && searchFuture->m_extensions.contains(info.suffix(), Qt::CaseInsensitive); + const auto passesFilter = name.contains(searchFuture->m_filter, Qt::CaseInsensitive); + + // Only when a directory doesn't pass the filter should we search further + if (info.isDir() && !passesFilter) + { + dir.setPath(path); + auto entries = dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags()); + + // Reverse to maintain the sorting within this directory when popped + std::reverse(entries.begin(), entries.end()); + + for (const auto& entry : entries) + { + stack.push_front(entry); + } + } + else if ((validFile || info.isDir()) && passesFilter) { searchFuture->addMatch(path); } + } + return true; +} + +} // namespace lmms::gui From aa050ae0b71c85bf6291d3a5dda61c66b8814302 Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 19 Nov 2023 00:44:15 -0500 Subject: [PATCH 044/191] Fix memory leaks (#6879) * Replace knobFModel with std::vector * Create QPixmap's on the stack * Assign parent for QGraphicsScene A call to QGraphicsView::setScene does not make the view take ownership of the scene. * Do not allocate QList on the heap * Use static QPixmap's The QPixmap's need to be created within the constructor, and not outside where they are defined, since it can't find them otherwise. I'm not too sure why. * Clear m_vi2->knobFModel in destructor * Use local static QPixmap's * Do not allocate QPixmap with new in AudioFileProcessor * Do not allocate QPixmap with new in Nes * Do not allocate QPixmap with new in Organic * Do not allocate QPixmap with new in SaControlsDialog * Do not allocate QPixmap with new in Vestige * Do not allocate QPixmap with new for FileBrowser * Do not allocate QPixmap with new in MixerLine * Do not allocate QPixmap with new in SendButtonIndicator * Do not allocate QPixmap with new in AutomationClipView * Do not allocate QPixmap with new in MidiClipView * Do not allocate QPixmap with new in AutomationEditor * Do not allocate QPixmap with new in PianoRoll * Do not allocate QPixmap with new in TimeLineWidget * Do not allocate QPixmap with new in EnvelopeAndLfoView * Do not allocate QPixmap with new in PianoView * Do not allocate QPixmap with new in ComboBox * Do not allocate QPixmap with new in Fader * Do not allocate QPixmap with new for LcdWidget * Do not allocate QPixmap with new for LedCheckbox * Use m_ as prefix for members * Use uniform initialization I already started using uniform initialization for the QPixmap changes for some reason, so I'm finishing that up. * Uniform initiaization * And then he realized he was making copies... * Do not call QPixmap copy constructor * Do not call QPixmap copy constructor in SaControlsDialog * Do not make pixmap's static for Lcd's and Led's * Initialize pixmaps in-class * Fix few mistakes and formatting --- include/AutomationClipView.h | 3 - include/AutomationEditor.h | 14 +-- include/ComboBox.h | 6 +- include/EnvelopeAndLfoView.h | 5 +- include/Fader.h | 13 +- include/FileBrowser.h | 17 +-- include/LcdWidget.h | 4 +- include/LedCheckBox.h | 7 +- include/MidiClipView.h | 9 +- include/MixerLine.h | 2 - include/PianoRoll.h | 12 +- include/PianoView.h | 13 +- include/SendButtonIndicator.h | 5 +- include/TimeLineWidget.h | 3 +- .../AudioFileProcessor/AudioFileProcessor.cpp | 10 +- .../AudioFileProcessor/AudioFileProcessor.h | 1 - plugins/Eq/EqControlsDialog.cpp | 16 +-- plugins/Eq/EqParameterWidget.cpp | 89 ++++++------- plugins/Eq/EqParameterWidget.h | 2 +- plugins/Nes/Nes.cpp | 9 +- plugins/Nes/Nes.h | 1 - plugins/Organic/Organic.cpp | 11 +- plugins/Organic/Organic.h | 1 - plugins/SpectrumAnalyzer/SaControlsDialog.cpp | 92 +++++++------- plugins/Vestige/Vestige.cpp | 13 +- plugins/Vestige/Vestige.h | 3 - plugins/VstEffect/VstEffectControls.cpp | 18 ++- plugins/VstEffect/VstEffectControls.h | 2 +- src/gui/FileBrowser.cpp | 119 +++--------------- src/gui/MixerLine.cpp | 23 +--- src/gui/SendButtonIndicator.cpp | 19 +-- src/gui/clips/AutomationClipView.cpp | 9 +- src/gui/clips/MidiClipView.cpp | 58 ++------- src/gui/editors/AutomationEditor.cpp | 58 ++------- src/gui/editors/PianoRoll.cpp | 57 +++------ src/gui/editors/TimeLineWidget.cpp | 29 ++--- src/gui/instrument/EnvelopeAndLfoView.cpp | 43 ++----- src/gui/instrument/PianoView.cpp | 52 ++------ src/gui/widgets/ComboBox.cpp | 26 +--- src/gui/widgets/Fader.cpp | 58 +++------ src/gui/widgets/LcdWidget.cpp | 40 ++---- src/gui/widgets/LedCheckBox.cpp | 39 ++---- src/tracks/MidiClip.cpp | 7 -- 43 files changed, 289 insertions(+), 729 deletions(-) diff --git a/include/AutomationClipView.h b/include/AutomationClipView.h index a20e2ce28..bdd2f0568 100644 --- a/include/AutomationClipView.h +++ b/include/AutomationClipView.h @@ -74,9 +74,6 @@ private: QPixmap m_paintPixmap; QStaticText m_staticTextName; - - static QPixmap * s_clip_rec; - void scaleTimemapToFit( float oldMin, float oldMax ); } ; diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index dad0e4916..da6bdcaaf 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -187,13 +187,13 @@ private: AutomationEditor( const AutomationEditor & ); ~AutomationEditor() override; - static QPixmap * s_toolDraw; - static QPixmap * s_toolErase; - static QPixmap * s_toolDrawOut; - static QPixmap * s_toolEditTangents; - static QPixmap * s_toolMove; - static QPixmap * s_toolYFlip; - static QPixmap * s_toolXFlip; + QPixmap m_toolDraw = embed::getIconPixmap("edit_draw"); + QPixmap m_toolErase = embed::getIconPixmap("edit_erase"); + QPixmap m_toolDrawOut = embed::getIconPixmap("edit_draw_outvalue"); + QPixmap m_toolEditTangents = embed::getIconPixmap("edit_tangent"); + QPixmap m_toolMove = embed::getIconPixmap("edit_move"); + QPixmap m_toolYFlip = embed::getIconPixmap("flip_y"); + QPixmap m_toolXFlip = embed::getIconPixmap("flip_x"); ComboBoxModel m_zoomingXModel; ComboBoxModel m_zoomingYModel; diff --git a/include/ComboBox.h b/include/ComboBox.h index 8153451e8..cc4ad68dd 100644 --- a/include/ComboBox.h +++ b/include/ComboBox.h @@ -66,9 +66,9 @@ protected: private: - static QPixmap* s_background; - static QPixmap* s_arrow; - static QPixmap* s_arrowSelected; + QPixmap m_background = embed::getIconPixmap("combobox_bg"); + QPixmap m_arrow = embed::getIconPixmap("combobox_arrow"); + QPixmap m_arrowSelected = embed::getIconPixmap("combobox_arrow_selected"); QMenu m_menu; diff --git a/include/EnvelopeAndLfoView.h b/include/EnvelopeAndLfoView.h index b5c7a67d4..d545aaa06 100644 --- a/include/EnvelopeAndLfoView.h +++ b/include/EnvelopeAndLfoView.h @@ -29,6 +29,7 @@ #include #include "ModelView.h" +#include "embed.h" class QPaintEvent; class QPixmap; @@ -71,8 +72,8 @@ protected slots: private: - static QPixmap * s_envGraph; - static QPixmap * s_lfoGraph; + QPixmap m_envGraph = embed::getIconPixmap("envelope_graph"); + QPixmap m_lfoGraph = embed::getIconPixmap("lfo_graph"); EnvelopeAndLfoParameters * m_params; diff --git a/include/Fader.h b/include/Fader.h index b46bed11b..c44d976a7 100644 --- a/include/Fader.h +++ b/include/Fader.h @@ -53,6 +53,7 @@ #include "AutomatableModelView.h" +#include "embed.h" namespace lmms::gui @@ -131,7 +132,7 @@ private: float fRange = model()->maxValue() - model()->minValue(); float realVal = model()->value() - model()->minValue(); - return height() - ( ( height() - m_knob->height() ) * ( realVal / fRange ) ); + return height() - ((height() - m_knob.height()) * (realVal / fRange)); } void setPeak( float fPeak, float &targetPeak, float &persistentPeak, QElapsedTimer &lastPeakTimer ); @@ -151,13 +152,9 @@ private: QElapsedTimer m_lastPeakTimer_L; QElapsedTimer m_lastPeakTimer_R; - static QPixmap * s_back; - static QPixmap * s_leds; - static QPixmap * s_knob; - - QPixmap * m_back; - QPixmap * m_leds; - QPixmap * m_knob; + QPixmap m_back = embed::getIconPixmap("fader_background"); + QPixmap m_leds = embed::getIconPixmap("fader_leds"); + QPixmap m_knob = embed::getIconPixmap("fader_knob"); bool m_levelsDisplayedInDBFS; diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 4d6fa745e..00fb07d63 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -28,6 +28,7 @@ #include #include #include +#include "embed.h" #include "FileBrowserSearcher.h" #include @@ -221,14 +222,12 @@ public: private: - void initPixmaps(); - bool addItems( const QString & path ); - static QPixmap * s_folderPixmap; - static QPixmap * s_folderOpenedPixmap; - static QPixmap * s_folderLockedPixmap; + QPixmap m_folderPixmap = embed::getIconPixmap("folder"); + QPixmap m_folderOpenedPixmap = embed::getIconPixmap("folder_opened"); + QPixmap m_folderLockedPixmap = embed::getIconPixmap("folder_locked"); //! Directories that lead here //! Initially, this is just set to the current path of a directory @@ -305,14 +304,6 @@ private: void initPixmaps(); void determineFileType(); - static QPixmap * s_projectFilePixmap; - static QPixmap * s_presetFilePixmap; - static QPixmap * s_sampleFilePixmap; - static QPixmap * s_soundfontFilePixmap; - static QPixmap * s_vstPluginFilePixmap; - static QPixmap * s_midiFilePixmap; - static QPixmap * s_unknownFilePixmap; - QString m_path; FileType m_type; FileHandling m_handling; diff --git a/include/LcdWidget.h b/include/LcdWidget.h index cef121b3f..f900fea15 100644 --- a/include/LcdWidget.h +++ b/include/LcdWidget.h @@ -47,8 +47,6 @@ public: LcdWidget(int numDigits, const QString& style, QWidget* parent, const QString& name = QString(), bool leadingZero = false); - ~LcdWidget() override; - void setValue(int value); void setValue(float value); void setLabel(const QString& label); @@ -98,7 +96,7 @@ private: QString m_display; QString m_label; - QPixmap* m_lcdPixmap; + QPixmap m_lcdPixmap; QColor m_textColor; QColor m_textShadowColor; diff --git a/include/LedCheckBox.h b/include/LedCheckBox.h index aaafffaa1..4f23cd74b 100644 --- a/include/LedCheckBox.h +++ b/include/LedCheckBox.h @@ -54,9 +54,6 @@ public: LedColor _color = LedColor::Yellow, bool legacyMode = true); - ~LedCheckBox() override; - - inline const QString & text() { return( m_text ); @@ -71,8 +68,8 @@ protected: private: - QPixmap * m_ledOnPixmap; - QPixmap * m_ledOffPixmap; + QPixmap m_ledOnPixmap; + QPixmap m_ledOffPixmap; QString m_text; diff --git a/include/MidiClipView.h b/include/MidiClipView.h index 6558688b4..a32be956a 100644 --- a/include/MidiClipView.h +++ b/include/MidiClipView.h @@ -27,6 +27,7 @@ #include #include "ClipView.h" +#include "embed.h" namespace lmms { @@ -85,10 +86,10 @@ protected: private: - static QPixmap * s_stepBtnOn0; - static QPixmap * s_stepBtnOn200; - static QPixmap * s_stepBtnOff; - static QPixmap * s_stepBtnOffLight; + QPixmap m_stepBtnOn0 = embed::getIconPixmap("step_btn_on_0"); + QPixmap m_stepBtnOn200 = embed::getIconPixmap("step_btn_on_200"); + QPixmap m_stepBtnOff = embed::getIconPixmap("step_btn_off"); + QPixmap m_stepBtnOffLight = embed::getIconPixmap("step_btn_off_light"); MidiClip* m_clip; QPixmap m_paintPixmap; diff --git a/include/MixerLine.h b/include/MixerLine.h index 68a61728c..655b30ec3 100644 --- a/include/MixerLine.h +++ b/include/MixerLine.h @@ -94,8 +94,6 @@ private: QColor m_strokeOuterInactive; QColor m_strokeInnerActive; QColor m_strokeInnerInactive; - static QPixmap * s_sendBgArrow; - static QPixmap * s_receiveBgArrow; bool m_inRename; QLineEdit * m_renameLineEdit; QGraphicsView * m_view; diff --git a/include/PianoRoll.h b/include/PianoRoll.h index bcaea8637..881732be1 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -340,12 +340,12 @@ private: static const int cm_scrollAmtHoriz = 10; static const int cm_scrollAmtVert = 1; - static QPixmap * s_toolDraw; - static QPixmap * s_toolErase; - static QPixmap * s_toolSelect; - static QPixmap * s_toolMove; - static QPixmap * s_toolOpen; - static QPixmap* s_toolKnife; + QPixmap m_toolDraw = embed::getIconPixmap("edit_draw"); + QPixmap m_toolErase = embed::getIconPixmap("edit_erase"); + QPixmap m_toolSelect = embed::getIconPixmap("edit_select"); + QPixmap m_toolMove = embed::getIconPixmap("edit_move"); + QPixmap m_toolOpen = embed::getIconPixmap("automation"); + QPixmap m_toolKnife = embed::getIconPixmap("edit_knife"); static std::array prKeyOrder; diff --git a/include/PianoView.h b/include/PianoView.h index 6421ff438..3f8d8026f 100644 --- a/include/PianoView.h +++ b/include/PianoView.h @@ -30,6 +30,7 @@ #include "AutomatableModel.h" #include "ModelView.h" +#include "embed.h" namespace lmms { @@ -73,12 +74,12 @@ private: int getKeyHeight(int key_num) const; IntModel *getNearestMarker(int key, QString* title = nullptr); - static QPixmap * s_whiteKeyPm; - static QPixmap * s_blackKeyPm; - static QPixmap * s_whiteKeyPressedPm; - static QPixmap * s_blackKeyPressedPm; - static QPixmap * s_whiteKeyDisabledPm; - static QPixmap * s_blackKeyDisabledPm; + QPixmap m_whiteKeyPm = embed::getIconPixmap("white_key"); + QPixmap m_blackKeyPm = embed::getIconPixmap("black_key"); + QPixmap m_whiteKeyPressedPm = embed::getIconPixmap("white_key_pressed"); + QPixmap m_blackKeyPressedPm = embed::getIconPixmap("black_key_pressed"); + QPixmap m_whiteKeyDisabledPm = embed::getIconPixmap("white_key_disabled"); + QPixmap m_blackKeyDisabledPm = embed::getIconPixmap("black_key_disabled"); Piano * m_piano; diff --git a/include/SendButtonIndicator.h b/include/SendButtonIndicator.h index f1ee2dbca..86f38318f 100644 --- a/include/SendButtonIndicator.h +++ b/include/SendButtonIndicator.h @@ -26,6 +26,7 @@ #define LMMS_GUI_SEND_BUTTON_INDICATOR_H #include +#include "embed.h" namespace lmms @@ -53,8 +54,8 @@ private: MixerLine * m_parent; MixerView * m_mv; - static QPixmap * s_qpmOn; - static QPixmap * s_qpmOff; + QPixmap m_qpmOff = embed::getIconPixmap("mixer_send_off", 29, 20); + QPixmap m_qpmOn = embed::getIconPixmap("mixer_send_on", 29, 20); FloatModel * getSendModel(); }; diff --git a/include/TimeLineWidget.h b/include/TimeLineWidget.h index 2e4ba6a97..2be73b77c 100644 --- a/include/TimeLineWidget.h +++ b/include/TimeLineWidget.h @@ -28,6 +28,7 @@ #include #include "Song.h" +#include "embed.h" class QPixmap; @@ -205,7 +206,7 @@ protected: private: - static QPixmap * s_posMarkerPixmap; + QPixmap m_posMarkerPixmap = embed::getIconPixmap("playpos_marker"); QColor m_inactiveLoopColor; QBrush m_inactiveLoopBrush; diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 864bda5b6..fbf054748 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -451,19 +451,12 @@ namespace gui { -QPixmap * AudioFileProcessorView::s_artwork = nullptr; AudioFileProcessorView::AudioFileProcessorView( Instrument * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ) { - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( - "artwork" ) ); - } - m_openAudioFileButton = new PixmapButton( this ); m_openAudioFileButton->setCursor( QCursor( Qt::PointingHandCursor ) ); m_openAudioFileButton->move( 227, 72 ); @@ -649,7 +642,8 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) { QPainter p( this ); - p.drawPixmap( 0, 0, *s_artwork ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + p.drawPixmap(0, 0, s_artwork); auto a = castModel(); diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 39bd11c3a..4a5f21cc0 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -145,7 +145,6 @@ protected: private: virtual void modelChanged(); - static QPixmap * s_artwork; AudioFileProcessorWaveView * m_waveView; Knob * m_ampKnob; diff --git a/plugins/Eq/EqControlsDialog.cpp b/plugins/Eq/EqControlsDialog.cpp index a26fa0db9..634bde846 100644 --- a/plugins/Eq/EqControlsDialog.cpp +++ b/plugins/Eq/EqControlsDialog.cpp @@ -72,17 +72,17 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : setBand( 6, &controls->m_highShelfActiveModel, &controls->m_highShelfFreqModel, &controls->m_highShelfResModel, &controls->m_highShelfGainModel, QColor(255 ,255, 255), tr( "High-shelf" ), &controls->m_highShelfPeakL, &controls->m_highShelfPeakR,0,0,0,0,0,0 ); setBand( 7, &controls->m_lpActiveModel, &controls->m_lpFreqModel, &controls->m_lpResModel, 0, QColor(255 ,255, 255), tr( "LP" ) ,0,0,0,0,0, &controls->m_lp12Model, &controls->m_lp24Model, &controls->m_lp48Model); - auto faderBg = new QPixmap(PLUGIN_NAME::getIconPixmap("faderback")); - auto faderLeds = new QPixmap(PLUGIN_NAME::getIconPixmap("faderleds")); - auto faderKnob = new QPixmap(PLUGIN_NAME::getIconPixmap("faderknob")); + static auto s_faderBg = PLUGIN_NAME::getIconPixmap("faderback"); + static auto s_faderLeds = PLUGIN_NAME::getIconPixmap("faderleds"); + static auto s_faderKnob = PLUGIN_NAME::getIconPixmap("faderknob"); - auto GainFaderIn = new EqFader(&controls->m_inGainModel, tr("Input gain"), this, faderBg, faderLeds, faderKnob, + auto GainFaderIn = new EqFader(&controls->m_inGainModel, tr("Input gain"), this, &s_faderBg, &s_faderLeds, &s_faderKnob, &controls->m_inPeakL, &controls->m_inPeakR); GainFaderIn->move( 23, 295 ); GainFaderIn->setDisplayConversion( false ); GainFaderIn->setHintText( tr( "Gain" ), "dBv"); - auto GainFaderOut = new EqFader(&controls->m_outGainModel, tr("Output gain"), this, faderBg, faderLeds, faderKnob, + auto GainFaderOut = new EqFader(&controls->m_outGainModel, tr("Output gain"), this, &s_faderBg, &s_faderLeds, &s_faderKnob, &controls->m_outPeakL, &controls->m_outPeakR); GainFaderOut->move( 453, 295); GainFaderOut->setDisplayConversion( false ); @@ -92,8 +92,8 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : int distance = 126; for( int i = 1; i < m_parameterWidget->bandCount() - 1; i++ ) { - auto gainFader = new EqFader(m_parameterWidget->getBandModels(i)->gain, tr(""), this, faderBg, faderLeds, - faderKnob, m_parameterWidget->getBandModels(i)->peakL, m_parameterWidget->getBandModels(i)->peakR); + auto gainFader = new EqFader(m_parameterWidget->getBandModels(i)->gain, tr(""), this, &s_faderBg, &s_faderLeds, + &s_faderKnob, m_parameterWidget->getBandModels(i)->peakL, m_parameterWidget->getBandModels(i)->peakR); gainFader->move( distance, 295 ); distance += 44; gainFader->setMinimumHeight(80); @@ -242,4 +242,4 @@ EqBand* EqControlsDialog::setBand(int index, BoolModel* active, FloatModel* freq } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/plugins/Eq/EqParameterWidget.cpp b/plugins/Eq/EqParameterWidget.cpp index b48f0f317..ceccb669f 100644 --- a/plugins/Eq/EqParameterWidget.cpp +++ b/plugins/Eq/EqParameterWidget.cpp @@ -56,7 +56,7 @@ EqParameterWidget::EqParameterWidget( QWidget *parent, EqControls * controls ) : m_pixelsPerOctave = EqHandle::freqToXPixel( 10000, m_displayWidth ) - EqHandle::freqToXPixel( 5000, m_displayWidth ); //GraphicsScene and GraphicsView stuff - auto scene = new QGraphicsScene(); + auto scene = new QGraphicsScene(this); scene->setSceneRect( 0, 0, m_displayWidth, m_displayHeigth ); auto view = new QGraphicsView(this); view->setStyleSheet( "border-style: none; background: transparent;" ); @@ -65,22 +65,22 @@ EqParameterWidget::EqParameterWidget( QWidget *parent, EqControls * controls ) : view->setScene( scene ); //adds the handles - m_handleList = new QList; + m_handleList.reserve(bandCount()); for ( int i = 0; i < bandCount(); i++ ) { m_handle = new EqHandle ( i, m_displayWidth, m_displayHeigth ); - m_handleList->append( m_handle ); + m_handleList.append(m_handle); m_handle->setZValue( 1 ); scene->addItem( m_handle ); } //adds the curve widget - m_eqcurve = new EqCurve( m_handleList, m_displayWidth, m_displayHeigth ); + m_eqcurve = new EqCurve(&m_handleList, m_displayWidth, m_displayHeigth); scene->addItem( m_eqcurve ); for ( int i = 0; i < bandCount(); i++ ) { // if the data of handle position has changed update the models - QObject::connect( m_handleList->at( i ) ,SIGNAL( positionChanged() ), this ,SLOT( updateModels() ) ); + QObject::connect(m_handleList.at(i), SIGNAL(positionChanged()), this, SLOT(updateModels())); } } @@ -112,16 +112,13 @@ void EqParameterWidget::updateHandle() m_eqcurve->setModelChanged( true ); for( int i = 0 ; i < bandCount(); i++ ) { - if ( !m_handleList->at( i )->mousePressed() ) //prevents a short circuit between handle and data model + if (!m_handleList.at(i)->mousePressed()) // prevents a short circuit between handle and data model { //sets the band on active if a fader or a knob is moved bool hover = false; // prevents an action if handle is moved for ( int j = 0; j < bandCount(); j++ ) { - if ( m_handleList->at(j)->isMouseHover() ) - { - hover = true; - } + if (m_handleList.at(j)->isMouseHover()) { hover = true; } } if ( !hover ) { @@ -131,17 +128,14 @@ void EqParameterWidget::updateHandle() } changeHandle( i ); } - else - { - m_handleList->at( i )->setHandleActive( m_bands[i].active->value() ); - } + else { m_handleList.at(i)->setHandleActive(m_bands[i].active->value()); } } - if ( m_bands[0].hp12->value() ) m_handleList->at( 0 )->sethp12(); - if ( m_bands[0].hp24->value() ) m_handleList->at( 0 )->sethp24(); - if ( m_bands[0].hp48->value() ) m_handleList->at( 0 )->sethp48(); - if ( m_bands[7].lp12->value() ) m_handleList->at( 7 )->setlp12(); - if ( m_bands[7].lp24->value() ) m_handleList->at( 7 )->setlp24(); - if ( m_bands[7].lp48->value() ) m_handleList->at( 7 )->setlp48(); + if (m_bands[0].hp12->value()) m_handleList.at(0)->sethp12(); + if (m_bands[0].hp24->value()) m_handleList.at(0)->sethp24(); + if (m_bands[0].hp48->value()) m_handleList.at(0)->sethp48(); + if (m_bands[7].lp12->value()) m_handleList.at(7)->setlp12(); + if (m_bands[7].lp24->value()) m_handleList.at(7)->setlp24(); + if (m_bands[7].lp48->value()) m_handleList.at(7)->setlp48(); } @@ -151,7 +145,7 @@ void EqParameterWidget::changeHandle( int i ) { //fill x, y, and bw with data from model float x = EqHandle::freqToXPixel( m_bands[i].freq->value(), m_displayWidth ); - float y = m_handleList->at( i )->y(); + float y = m_handleList.at(i)->y(); //for pass filters there is no gain model if( m_bands[i].gain ) { @@ -164,48 +158,45 @@ void EqParameterWidget::changeHandle( int i ) switch ( i ) { case 0 : - m_handleList->at( i )->setType( EqHandleType::HighPass ); - m_handleList->at( i )->setPos( x, m_displayHeigth / 2 ); + m_handleList.at(i)->setType(EqHandleType::HighPass); + m_handleList.at(i)->setPos(x, m_displayHeigth / 2); break; case 1: - m_handleList->at( i )->setType( EqHandleType::LowShelf ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::LowShelf); + m_handleList.at(i)->setPos(x, y); break; case 2: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 3: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 4: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 5: - m_handleList->at( i )->setType( EqHandleType::Para ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::Para); + m_handleList.at(i)->setPos(x, y); break; case 6: - m_handleList->at( i )->setType( EqHandleType::HighShelf ); - m_handleList->at( i )->setPos( x, y ); + m_handleList.at(i)->setType(EqHandleType::HighShelf); + m_handleList.at(i)->setPos(x, y); break; case 7: - m_handleList->at( i )->setType( EqHandleType::LowPass ); - m_handleList->at( i )->setPos( QPointF( x, m_displayHeigth / 2 ) ); + m_handleList.at(i)->setType(EqHandleType::LowPass); + m_handleList.at(i)->setPos(QPointF(x, m_displayHeigth / 2)); break; } // set resonance/bandwidth for each handle - if ( m_handleList->at( i )->getResonance() != bw ) - { - m_handleList->at( i )->setResonance( bw ); - } + if (m_handleList.at(i)->getResonance() != bw) { m_handleList.at(i)->setResonance(bw); } // and the active status - m_handleList->at( i )->setHandleActive( m_bands[i].active->value() ); - m_handleList->at( i )->update(); + m_handleList.at(i)->setHandleActive(m_bands[i].active->value()); + m_handleList.at(i)->update(); m_eqcurve->update(); } @@ -216,19 +207,17 @@ void EqParameterWidget::updateModels() { for ( int i=0 ; i < bandCount(); i++ ) { - m_bands[i].freq->setValue( EqHandle::xPixelToFreq( m_handleList->at( i )->x(), m_displayWidth ) ); + m_bands[i].freq->setValue(EqHandle::xPixelToFreq(m_handleList.at(i)->x(), m_displayWidth)); if( m_bands[i].gain ) { - m_bands[i].gain->setValue( EqHandle::yPixelToGain( m_handleList->at(i)->y(), m_displayHeigth, m_pixelsPerUnitHeight ) ); + m_bands[i].gain->setValue( + EqHandle::yPixelToGain(m_handleList.at(i)->y(), m_displayHeigth, m_pixelsPerUnitHeight)); } - m_bands[i].res->setValue( m_handleList->at( i )->getResonance() ); + m_bands[i].res->setValue(m_handleList.at(i)->getResonance()); //identifies the handle which is moved and set the band active - if ( sender() == m_handleList->at( i ) ) - { - m_bands[i].active->setValue( true ); - } + if (sender() == m_handleList.at(i)) { m_bands[i].active->setValue(true); } } m_eqcurve->update(); } diff --git a/plugins/Eq/EqParameterWidget.h b/plugins/Eq/EqParameterWidget.h index f80499395..c3444873b 100644 --- a/plugins/Eq/EqParameterWidget.h +++ b/plugins/Eq/EqParameterWidget.h @@ -75,7 +75,7 @@ class EqParameterWidget : public QWidget public: explicit EqParameterWidget( QWidget *parent = 0, EqControls * controls = 0 ); ~EqParameterWidget() override; - QList *m_handleList; + QList m_handleList; const int bandCount() { diff --git a/plugins/Nes/Nes.cpp b/plugins/Nes/Nes.cpp index 47122a0c6..2c0907a19 100644 --- a/plugins/Nes/Nes.cpp +++ b/plugins/Nes/Nes.cpp @@ -719,7 +719,6 @@ namespace gui { -QPixmap * NesInstrumentView::s_artwork = nullptr; NesInstrumentView::NesInstrumentView( Instrument * instrument, QWidget * parent ) : @@ -728,12 +727,8 @@ NesInstrumentView::NesInstrumentView( Instrument * instrument, QWidget * parent setAutoFillBackground( true ); QPalette pal; - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( "artwork" ) ); - } - - pal.setBrush( backgroundRole(), *s_artwork ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + pal.setBrush(backgroundRole(), s_artwork); setPalette( pal ); const int KNOB_Y1 = 24; diff --git a/plugins/Nes/Nes.h b/plugins/Nes/Nes.h index 3ddf0fc9a..b4102f31d 100644 --- a/plugins/Nes/Nes.h +++ b/plugins/Nes/Nes.h @@ -372,7 +372,6 @@ private: Knob * m_masterVolKnob; Knob * m_vibratoKnob; - static QPixmap * s_artwork; }; diff --git a/plugins/Organic/Organic.cpp b/plugins/Organic/Organic.cpp index a70da6421..761010922 100644 --- a/plugins/Organic/Organic.cpp +++ b/plugins/Organic/Organic.cpp @@ -60,7 +60,6 @@ Plugin::Descriptor PLUGIN_EXPORT organic_plugin_descriptor = } -QPixmap * gui::OrganicInstrumentView::s_artwork = nullptr; float * OrganicInstrument::s_harmonics = nullptr; /*********************************************************************** @@ -420,8 +419,8 @@ OrganicInstrumentView::OrganicInstrumentView( Instrument * _instrument, setAutoFillBackground( true ); QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( - "artwork" ) ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + pal.setBrush(backgroundRole(), s_artwork); setPalette( pal ); // setup knob for FX1 @@ -451,12 +450,6 @@ OrganicInstrumentView::OrganicInstrumentView( Instrument * _instrument, oi, SLOT( randomiseSettings() ) ); - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( - "artwork" ) ); - } - } diff --git a/plugins/Organic/Organic.h b/plugins/Organic/Organic.h index 6c53e84ec..a46b7882f 100644 --- a/plugins/Organic/Organic.h +++ b/plugins/Organic/Organic.h @@ -227,7 +227,6 @@ private: int m_numOscillators; - static QPixmap * s_artwork; protected slots: void updateKnobHint(); diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index eb09c793a..d36d8a3ee 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -89,28 +89,28 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // pause and freeze buttons auto pauseButton = new PixmapButton(this, tr("Pause")); pauseButton->setToolTip(tr("Pause data acquisition")); - auto pauseOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("play").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto pauseOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("pause").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - pauseOnPixmap->setDevicePixelRatio(devicePixelRatio()); - pauseOffPixmap->setDevicePixelRatio(devicePixelRatio()); - pauseButton->setActiveGraphic(*pauseOnPixmap); - pauseButton->setInactiveGraphic(*pauseOffPixmap); + static auto s_pauseOnPixmap + = PLUGIN_NAME::getIconPixmap("play").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_pauseOffPixmap + = PLUGIN_NAME::getIconPixmap("pause").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_pauseOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_pauseOffPixmap.setDevicePixelRatio(devicePixelRatio()); + pauseButton->setActiveGraphic(s_pauseOnPixmap); + pauseButton->setInactiveGraphic(s_pauseOffPixmap); pauseButton->setCheckable(true); pauseButton->setModel(&controls->m_pauseModel); config_layout->addWidget(pauseButton, 0, 0, 2, 1, Qt::AlignHCenter); auto refFreezeButton = new PixmapButton(this, tr("Reference freeze")); refFreezeButton->setToolTip(tr("Freeze current input as a reference / disable falloff in peak-hold mode.")); - auto freezeOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("freeze").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto freezeOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("freeze_off").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - freezeOnPixmap->setDevicePixelRatio(devicePixelRatio()); - freezeOffPixmap->setDevicePixelRatio(devicePixelRatio()); - refFreezeButton->setActiveGraphic(*freezeOnPixmap); - refFreezeButton->setInactiveGraphic(*freezeOffPixmap); + static auto s_freezeOnPixmap + = PLUGIN_NAME::getIconPixmap("freeze").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_freezeOffPixmap + = PLUGIN_NAME::getIconPixmap("freeze_off").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_freezeOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_freezeOffPixmap.setDevicePixelRatio(devicePixelRatio()); + refFreezeButton->setActiveGraphic(s_freezeOnPixmap); + refFreezeButton->setInactiveGraphic(s_freezeOffPixmap); refFreezeButton->setCheckable(true); refFreezeButton->setModel(&controls->m_refFreezeModel); config_layout->addWidget(refFreezeButton, 2, 0, 2, 1, Qt::AlignHCenter); @@ -147,14 +147,14 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // frequency: linear / log. switch and range selector auto logXButton = new PixmapButton(this, tr("Logarithmic frequency")); logXButton->setToolTip(tr("Switch between logarithmic and linear frequency scale")); - auto logXOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("x_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto logXOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("x_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - logXOnPixmap->setDevicePixelRatio(devicePixelRatio()); - logXOffPixmap->setDevicePixelRatio(devicePixelRatio()); - logXButton->setActiveGraphic(*logXOnPixmap); - logXButton->setInactiveGraphic(*logXOffPixmap); + static auto s_logXOnPixmap + = PLUGIN_NAME::getIconPixmap("x_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_logXOffPixmap + = PLUGIN_NAME::getIconPixmap("x_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_logXOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_logXOffPixmap.setDevicePixelRatio(devicePixelRatio()); + logXButton->setActiveGraphic(s_logXOnPixmap); + logXButton->setInactiveGraphic(s_logXOffPixmap); logXButton->setCheckable(true); logXButton->setModel(&controls->m_logXModel); config_layout->addWidget(logXButton, 0, 2, 2, 1, Qt::AlignRight); @@ -169,14 +169,14 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // amplitude: linear / log switch and range selector auto logYButton = new PixmapButton(this, tr("Logarithmic amplitude")); logYButton->setToolTip(tr("Switch between logarithmic and linear amplitude scale")); - auto logYOnPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("y_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto logYOffPixmap = new QPixmap( - PLUGIN_NAME::getIconPixmap("y_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - logYOnPixmap->setDevicePixelRatio(devicePixelRatio()); - logYOffPixmap->setDevicePixelRatio(devicePixelRatio()); - logYButton->setActiveGraphic(*logYOnPixmap); - logYButton->setInactiveGraphic(*logYOffPixmap); + static auto s_logYOnPixmap + = PLUGIN_NAME::getIconPixmap("y_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_logYOffPixmap + = PLUGIN_NAME::getIconPixmap("y_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_logYOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_logYOffPixmap.setDevicePixelRatio(devicePixelRatio()); + logYButton->setActiveGraphic(s_logYOnPixmap); + logYButton->setInactiveGraphic(s_logYOffPixmap); logYButton->setCheckable(true); logYButton->setModel(&controls->m_logYModel); config_layout->addWidget(logYButton, 2, 2, 2, 1, Qt::AlignRight); @@ -190,9 +190,9 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // FFT: block size: icon and selector auto blockSizeLabel = new QLabel("", this); - auto blockSizeIcon = new QPixmap(PLUGIN_NAME::getIconPixmap("block_size")); - blockSizeIcon->setDevicePixelRatio(devicePixelRatio()); - blockSizeLabel->setPixmap(blockSizeIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + static auto s_blockSizeIcon = PLUGIN_NAME::getIconPixmap("block_size"); + s_blockSizeIcon.setDevicePixelRatio(devicePixelRatio()); + blockSizeLabel->setPixmap(s_blockSizeIcon.scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); config_layout->addWidget(blockSizeLabel, 0, 4, 2, 1, Qt::AlignRight); auto blockSizeCombo = new ComboBox(this, tr("FFT block size")); @@ -206,9 +206,9 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // FFT: window type: icon and selector auto windowLabel = new QLabel("", this); - auto windowIcon = new QPixmap(PLUGIN_NAME::getIconPixmap("window")); - windowIcon->setDevicePixelRatio(devicePixelRatio()); - windowLabel->setPixmap(windowIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + static auto s_windowIcon = PLUGIN_NAME::getIconPixmap("window"); + s_windowIcon.setDevicePixelRatio(devicePixelRatio()); + windowLabel->setPixmap(s_windowIcon.scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); config_layout->addWidget(windowLabel, 2, 4, 2, 1, Qt::AlignRight); auto windowCombo = new ComboBox(this, tr("FFT window type")); @@ -307,14 +307,14 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // Advanced settings button auto advancedButton = new PixmapButton(this, tr("Advanced settings")); advancedButton->setToolTip(tr("Access advanced settings")); - auto advancedOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_on") - .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - auto advancedOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_off") - .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); - advancedOnPixmap->setDevicePixelRatio(devicePixelRatio()); - advancedOffPixmap->setDevicePixelRatio(devicePixelRatio()); - advancedButton->setActiveGraphic(*advancedOnPixmap); - advancedButton->setInactiveGraphic(*advancedOffPixmap); + static auto s_advancedOnPixmap = PLUGIN_NAME::getIconPixmap("advanced_on") + .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + static auto s_advancedOffPixmap = PLUGIN_NAME::getIconPixmap("advanced_off") + .scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + s_advancedOnPixmap.setDevicePixelRatio(devicePixelRatio()); + s_advancedOffPixmap.setDevicePixelRatio(devicePixelRatio()); + advancedButton->setActiveGraphic(s_advancedOnPixmap); + advancedButton->setInactiveGraphic(s_advancedOffPixmap); advancedButton->setCheckable(true); controls_layout->addStretch(0); controls_layout->addWidget(advancedButton); diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index a696a4b2d..583075c0c 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -485,21 +485,11 @@ gui::PluginView * VestigeInstrument::instantiateView( QWidget * _parent ) namespace gui { -QPixmap * VestigeInstrumentView::s_artwork = nullptr; -QPixmap * ManageVestigeInstrumentView::s_artwork = nullptr; - - VestigeInstrumentView::VestigeInstrumentView( Instrument * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ), lastPosInMenu (0) { - if( s_artwork == nullptr ) - { - s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( - "artwork" ) ); - } - m_openPluginButton = new PixmapButton( this, "" ); m_openPluginButton->setCheckable( false ); m_openPluginButton->setCursor( Qt::PointingHandCursor ); @@ -881,7 +871,8 @@ void VestigeInstrumentView::paintEvent( QPaintEvent * ) { QPainter p( this ); - p.drawPixmap( 0, 0, *s_artwork ); + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + p.drawPixmap(0, 0, s_artwork); QString plugin_name = ( m_vi->m_plugin != nullptr ) ? m_vi->m_plugin->name()/* + QString::number( diff --git a/plugins/Vestige/Vestige.h b/plugins/Vestige/Vestige.h index f740913ea..9ac66f74d 100644 --- a/plugins/Vestige/Vestige.h +++ b/plugins/Vestige/Vestige.h @@ -131,8 +131,6 @@ protected: private: - static QPixmap * s_artwork; - VestigeInstrument * m_vi; QWidget *widget; @@ -175,7 +173,6 @@ protected: private: virtual void modelChanged(); - static QPixmap * s_artwork; VestigeInstrument * m_vi; diff --git a/plugins/VstEffect/VstEffectControls.cpp b/plugins/VstEffect/VstEffectControls.cpp index cf0c831a6..af90e4646 100644 --- a/plugins/VstEffect/VstEffectControls.cpp +++ b/plugins/VstEffect/VstEffectControls.cpp @@ -50,7 +50,6 @@ VstEffectControls::VstEffectControls( VstEffect * _eff ) : EffectControls( _eff ), m_effect( _eff ), m_subWindow( nullptr ), - knobFModel( nullptr ), ctrHandle( nullptr ), lastPosInMenu (0), m_vstGuiVisible ( true ) @@ -84,7 +83,7 @@ void VstEffectControls::loadSettings( const QDomElement & _this ) const QMap & dump = m_effect->m_plugin->parameterDump(); paramCount = dump.size(); auto paramStr = std::array{}; - knobFModel = new FloatModel *[ paramCount ]; + knobFModel.resize(paramCount); QStringList s_dumpValues; for( int i = 0; i < paramCount; i++ ) { @@ -131,7 +130,7 @@ void VstEffectControls::saveSettings( QDomDocument & _doc, QDomElement & _this ) if( m_effect->m_plugin != nullptr ) { m_effect->m_plugin->saveSettings( _doc, _this ); - if (knobFModel != nullptr) { + if (!knobFModel.empty()) { const QMap & dump = m_effect->m_plugin->parameterDump(); paramCount = dump.size(); auto paramStr = std::array{}; @@ -376,8 +375,9 @@ ManageVSTEffectView::ManageVSTEffectView( VstEffect * _eff, VstEffectControls * vstKnobs = new CustomTextKnob *[ m_vi->paramCount ]; bool hasKnobModel = true; - if (m_vi->knobFModel == nullptr) { - m_vi->knobFModel = new FloatModel *[ m_vi->paramCount ]; + if (m_vi->knobFModel.empty()) + { + m_vi->knobFModel.resize(m_vi->paramCount); hasKnobModel = false; } @@ -543,7 +543,7 @@ void ManageVSTEffectView::syncParameterText() ManageVSTEffectView::~ManageVSTEffectView() { - if( m_vi2->knobFModel != nullptr ) + if (!m_vi2->knobFModel.empty()) { for( int i = 0; i < m_vi2->paramCount; i++ ) { @@ -558,11 +558,7 @@ ManageVSTEffectView::~ManageVSTEffectView() vstKnobs = nullptr; } - if( m_vi2->knobFModel != nullptr ) - { - delete [] m_vi2->knobFModel; - m_vi2->knobFModel = nullptr; - } + m_vi2->knobFModel.clear(); if( m_vi2->m_scrollArea != nullptr ) { diff --git a/plugins/VstEffect/VstEffectControls.h b/plugins/VstEffect/VstEffectControls.h index 42178b5b6..e2bea36e8 100644 --- a/plugins/VstEffect/VstEffectControls.h +++ b/plugins/VstEffect/VstEffectControls.h @@ -89,7 +89,7 @@ private: QMdiSubWindow * m_subWindow; QScrollArea * m_scrollArea; - FloatModel ** knobFModel; + std::vector knobFModel; int paramCount; QObject * ctrHandle; diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index f0b7e83ec..54ddaa317 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -1011,11 +1011,6 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item ) } } - -QPixmap * Directory::s_folderPixmap = nullptr; -QPixmap * Directory::s_folderOpenedPixmap = nullptr; -QPixmap * Directory::s_folderLockedPixmap = nullptr; - Directory::Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation) : QTreeWidgetItem(QStringList(filename), TypeDirectoryItem) , m_directories(path) @@ -1023,56 +1018,19 @@ Directory::Directory(const QString& filename, const QString& path, const QString , m_dirCount(0) , m_disableEntryPopulation(disableEntryPopulation) { - initPixmaps(); - + setIcon(0, !QDir{fullName()}.isReadable() ? m_folderLockedPixmap : m_folderPixmap); setChildIndicatorPolicy( QTreeWidgetItem::ShowIndicator ); - - if( !QDir( fullName() ).isReadable() ) - { - setIcon( 0, *s_folderLockedPixmap ); - } - else - { - setIcon( 0, *s_folderPixmap ); - } } - - - -void Directory::initPixmaps() -{ - if( s_folderPixmap == nullptr ) - { - s_folderPixmap = new QPixmap( - embed::getIconPixmap( "folder" ) ); - } - - if( s_folderOpenedPixmap == nullptr ) - { - s_folderOpenedPixmap = new QPixmap( - embed::getIconPixmap( "folder_opened" ) ); - } - - if( s_folderLockedPixmap == nullptr ) - { - s_folderLockedPixmap = new QPixmap( - embed::getIconPixmap( "folder_locked" ) ); - } -} - - - - void Directory::update() { if( !isExpanded() ) { - setIcon( 0, *s_folderPixmap ); + setIcon(0, m_folderPixmap); return; } - setIcon( 0, *s_folderOpenedPixmap ); + setIcon(0, m_folderOpenedPixmap); if (!m_disableEntryPopulation && !childCount()) { m_dirCount = 0; @@ -1142,15 +1100,6 @@ bool Directory::addItems(const QString& path) -QPixmap * FileItem::s_projectFilePixmap = nullptr; -QPixmap * FileItem::s_presetFilePixmap = nullptr; -QPixmap * FileItem::s_sampleFilePixmap = nullptr; -QPixmap * FileItem::s_soundfontFilePixmap = nullptr; -QPixmap * FileItem::s_vstPluginFilePixmap = nullptr; -QPixmap * FileItem::s_midiFilePixmap = nullptr; -QPixmap * FileItem::s_unknownFilePixmap = nullptr; - - FileItem::FileItem(QTreeWidget * parent, const QString & name, const QString & path ) : QTreeWidgetItem( parent, QStringList( name) , TypeFileItem ), @@ -1176,72 +1125,38 @@ FileItem::FileItem(const QString & name, const QString & path ) : void FileItem::initPixmaps() { - if( s_projectFilePixmap == nullptr ) - { - s_projectFilePixmap = new QPixmap( embed::getIconPixmap( - "project_file", 16, 16 ) ); - } - - if( s_presetFilePixmap == nullptr ) - { - s_presetFilePixmap = new QPixmap( embed::getIconPixmap( - "preset_file", 16, 16 ) ); - } - - if( s_sampleFilePixmap == nullptr ) - { - s_sampleFilePixmap = new QPixmap( embed::getIconPixmap( - "sample_file", 16, 16 ) ); - } - - if ( s_soundfontFilePixmap == nullptr ) - { - s_soundfontFilePixmap = new QPixmap( embed::getIconPixmap( - "soundfont_file", 16, 16 ) ); - } - - if ( s_vstPluginFilePixmap == nullptr ) - { - s_vstPluginFilePixmap = new QPixmap( embed::getIconPixmap( - "vst_plugin_file", 16, 16 ) ); - } - - if( s_midiFilePixmap == nullptr ) - { - s_midiFilePixmap = new QPixmap( embed::getIconPixmap( - "midi_file", 16, 16 ) ); - } - - if( s_unknownFilePixmap == nullptr ) - { - s_unknownFilePixmap = new QPixmap( embed::getIconPixmap( - "unknown_file" ) ); - } + static auto s_projectFilePixmap = embed::getIconPixmap("project_file", 16, 16); + static auto s_presetFilePixmap = embed::getIconPixmap("preset_file", 16, 16); + static auto s_sampleFilePixmap = embed::getIconPixmap("sample_file", 16, 16); + static auto s_soundfontFilePixmap = embed::getIconPixmap("soundfont_file", 16, 16); + static auto s_vstPluginFilePixmap = embed::getIconPixmap("vst_plugin_file", 16, 16); + static auto s_midiFilePixmap = embed::getIconPixmap("midi_file", 16, 16); + static auto s_unknownFilePixmap = embed::getIconPixmap("unknown_file"); switch( m_type ) { case FileType::Project: - setIcon( 0, *s_projectFilePixmap ); + setIcon(0, s_projectFilePixmap); break; case FileType::Preset: - setIcon( 0, *s_presetFilePixmap ); + setIcon(0, s_presetFilePixmap); break; case FileType::SoundFont: - setIcon( 0, *s_soundfontFilePixmap ); + setIcon(0, s_soundfontFilePixmap); break; case FileType::VstPlugin: - setIcon( 0, *s_vstPluginFilePixmap ); + setIcon(0, s_vstPluginFilePixmap); break; case FileType::Sample: case FileType::Patch: // TODO - setIcon( 0, *s_sampleFilePixmap ); + setIcon(0, s_sampleFilePixmap); break; case FileType::Midi: - setIcon( 0, *s_midiFilePixmap ); + setIcon(0, s_midiFilePixmap); break; case FileType::Unknown: default: - setIcon( 0, *s_unknownFilePixmap ); + setIcon(0, s_unknownFilePixmap); break; } } diff --git a/src/gui/MixerLine.cpp b/src/gui/MixerLine.cpp index d3435787e..bd0de8301 100644 --- a/src/gui/MixerLine.cpp +++ b/src/gui/MixerLine.cpp @@ -68,8 +68,6 @@ bool MixerLine::eventFilter( QObject *dist, QEvent *event ) } const int MixerLine::MixerLineHeight = 287; -QPixmap * MixerLine::s_sendBgArrow = nullptr; -QPixmap * MixerLine::s_receiveBgArrow = nullptr; MixerLine::MixerLine( QWidget * _parent, MixerView * _mv, int _channelIndex ) : QWidget( _parent ), @@ -82,15 +80,6 @@ MixerLine::MixerLine( QWidget * _parent, MixerView * _mv, int _channelIndex ) : m_strokeInnerInactive( 0, 0, 0 ), m_inRename( false ) { - if( !s_sendBgArrow ) - { - s_sendBgArrow = new QPixmap( embed::getIconPixmap( "send_bg_arrow", 29, 56 ) ); - } - if( !s_receiveBgArrow ) - { - s_receiveBgArrow = new QPixmap( embed::getIconPixmap( "receive_bg_arrow", 29, 56 ) ); - } - setFixedSize( 33, MixerLineHeight ); setAttribute( Qt::WA_OpaquePaintEvent, true ); setCursor( QCursor( embed::getIconPixmap( "hand" ), 3, 3 ) ); @@ -193,14 +182,10 @@ void MixerLine::drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isA p->drawRect( 0, 0, width-1, height-1 ); // draw the mixer send background - if( sendToThis ) - { - p->drawPixmap( 2, 0, 29, 56, *s_sendBgArrow ); - } - else if( receiveFromThis ) - { - p->drawPixmap( 2, 0, 29, 56, *s_receiveBgArrow ); - } + + static auto s_sendBgArrow = embed::getIconPixmap("send_bg_arrow", 29, 56); + static auto s_receiveBgArrow = embed::getIconPixmap("receive_bg_arrow", 29, 56); + p->drawPixmap(2, 0, 29, 56, sendToThis ? s_sendBgArrow : s_receiveBgArrow); } diff --git a/src/gui/SendButtonIndicator.cpp b/src/gui/SendButtonIndicator.cpp index cd1996c45..d6f8a8327 100644 --- a/src/gui/SendButtonIndicator.cpp +++ b/src/gui/SendButtonIndicator.cpp @@ -8,30 +8,17 @@ namespace lmms::gui { - -QPixmap * SendButtonIndicator::s_qpmOff = nullptr; -QPixmap * SendButtonIndicator::s_qpmOn = nullptr; - SendButtonIndicator:: SendButtonIndicator( QWidget * _parent, MixerLine * _owner, MixerView * _mv) : QLabel( _parent ), m_parent( _owner ), m_mv( _mv ) { - if( ! s_qpmOff ) - { - s_qpmOff = new QPixmap( embed::getIconPixmap( "mixer_send_off", 29, 20 ) ); - } - - if( ! s_qpmOn ) - { - s_qpmOn = new QPixmap( embed::getIconPixmap( "mixer_send_on", 29, 20 ) ); - } - + // don't do any initializing yet, because the MixerView and MixerLine // that were passed to this constructor are not done with their constructors // yet. - setPixmap( *s_qpmOff ); + setPixmap(m_qpmOff); } void SendButtonIndicator::mousePressEvent( QMouseEvent * e ) @@ -64,7 +51,7 @@ FloatModel * SendButtonIndicator::getSendModel() void SendButtonIndicator::updateLightStatus() { - setPixmap( getSendModel() == nullptr ? *s_qpmOff : *s_qpmOn ); + setPixmap(!getSendModel() ? m_qpmOff : m_qpmOn); } diff --git a/src/gui/clips/AutomationClipView.cpp b/src/gui/clips/AutomationClipView.cpp index 3e0e12b75..7ddb70151 100644 --- a/src/gui/clips/AutomationClipView.cpp +++ b/src/gui/clips/AutomationClipView.cpp @@ -44,8 +44,6 @@ namespace lmms::gui { -QPixmap * AutomationClipView::s_clip_rec = nullptr; - AutomationClipView::AutomationClipView( AutomationClip * _clip, TrackView * _parent ) : ClipView( _clip, _parent ), @@ -61,10 +59,6 @@ AutomationClipView::AutomationClipView( AutomationClip * _clip, setToolTip(m_clip->name()); setStyle( QApplication::style() ); - - if( s_clip_rec == nullptr ) { s_clip_rec = new QPixmap( embed::getIconPixmap( - "clip_rec" ) ); } - update(); } @@ -379,7 +373,8 @@ void AutomationClipView::paintEvent( QPaintEvent * ) // recording icon for when recording automation if( m_clip->isRecording() ) { - p.drawPixmap( 1, rect().bottom() - s_clip_rec->height(), *s_clip_rec ); + static auto s_clipRec = embed::getIconPixmap("clip_rec"); + p.drawPixmap(1, rect().bottom() - s_clipRec.height(), s_clipRec); } // clip name diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index b13d6e003..fb259dc77 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -57,31 +57,6 @@ MidiClipView::MidiClipView( MidiClip* clip, TrackView* parent ) : { connect( getGUI()->pianoRoll(), SIGNAL(currentMidiClipChanged()), this, SLOT(update())); - - if( s_stepBtnOn0 == nullptr ) - { - s_stepBtnOn0 = new QPixmap( embed::getIconPixmap( - "step_btn_on_0" ) ); - } - - if( s_stepBtnOn200 == nullptr ) - { - s_stepBtnOn200 = new QPixmap( embed::getIconPixmap( - "step_btn_on_200" ) ); - } - - if( s_stepBtnOff == nullptr ) - { - s_stepBtnOff = new QPixmap( embed::getIconPixmap( - "step_btn_off" ) ); - } - - if( s_stepBtnOffLight == nullptr ) - { - s_stepBtnOffLight = new QPixmap( embed::getIconPixmap( - "step_btn_off_light" ) ); - } - update(); setStyle( QApplication::style() ); @@ -253,9 +228,8 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) void MidiClipView::mousePressEvent( QMouseEvent * _me ) { bool displayPattern = fixedClips() || (pixelsPerBar() >= 96 && m_legacySEPattern); - if( _me->button() == Qt::LeftButton && - m_clip->m_clipType == MidiClip::Type::BeatClip && - displayPattern && _me->y() > height() - s_stepBtnOff->height() ) + if (_me->button() == Qt::LeftButton && m_clip->m_clipType == MidiClip::Type::BeatClip && displayPattern + && _me->y() > height() - m_stepBtnOff.height()) // when mouse button is pressed in pattern mode @@ -325,7 +299,7 @@ void MidiClipView::wheelEvent(QWheelEvent * we) { if(m_clip->m_clipType == MidiClip::Type::BeatClip && (fixedClips() || pixelsPerBar() >= 96) && - position(we).y() > height() - s_stepBtnOff->height()) + position(we).y() > height() - m_stepBtnOff.height()) { // get the step number that was wheeled on and // do calculations in floats to prevent rounding errors... @@ -472,22 +446,14 @@ void MidiClipView::paintEvent( QPaintEvent * ) const int w = width() - 2 * BORDER_WIDTH; // scale step graphics to fit the beat clip length - stepon0 = s_stepBtnOn0->scaled(w / steps, - s_stepBtnOn0->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - stepon200 = s_stepBtnOn200->scaled(w / steps, - s_stepBtnOn200->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - stepoff = s_stepBtnOff->scaled(w / steps, - s_stepBtnOff->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - stepoffl = s_stepBtnOffLight->scaled(w / steps, - s_stepBtnOffLight->height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); + stepon0 + = m_stepBtnOn0.scaled(w / steps, m_stepBtnOn0.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + stepon200 = m_stepBtnOn200.scaled( + w / steps, m_stepBtnOn200.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + stepoff + = m_stepBtnOff.scaled(w / steps, m_stepBtnOff.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + stepoffl = m_stepBtnOffLight.scaled( + w / steps, m_stepBtnOffLight.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); for (int it = 0; it < steps; it++) // go through all the steps in the beat clip { @@ -495,7 +461,7 @@ void MidiClipView::paintEvent( QPaintEvent * ) // figure out x and y coordinates for step graphic const int x = BORDER_WIDTH + static_cast(it * w / steps); - const int y = height() - s_stepBtnOff->height() - 1; + const int y = height() - m_stepBtnOff.height() - 1; if (n) { diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 282c335df..bd9566a05 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -64,15 +64,6 @@ namespace lmms::gui { - -QPixmap * AutomationEditor::s_toolDraw = nullptr; -QPixmap * AutomationEditor::s_toolErase = nullptr; -QPixmap * AutomationEditor::s_toolDrawOut = nullptr; -QPixmap * AutomationEditor::s_toolEditTangents = nullptr; -QPixmap * AutomationEditor::s_toolMove = nullptr; -QPixmap * AutomationEditor::s_toolYFlip = nullptr; -QPixmap * AutomationEditor::s_toolXFlip = nullptr; - const std::array AutomationEditor::m_zoomXLevels = { 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f }; @@ -135,17 +126,6 @@ AutomationEditor::AutomationEditor() : this, SLOT(setQuantization())); m_quantizeModel.setValue( m_quantizeModel.findText( "1/8" ) ); - if (s_toolYFlip == nullptr) - { - s_toolYFlip = new QPixmap( embed::getIconPixmap( - "flip_y" ) ); - } - if (s_toolXFlip == nullptr) - { - s_toolXFlip = new QPixmap( embed::getIconPixmap( - "flip_x" ) ); - } - // add time-line m_timeLine = new TimeLineWidget( VALUES_WIDTH, 0, m_ppb, Engine::getSong()->getPlayPos( @@ -169,28 +149,6 @@ AutomationEditor::AutomationEditor() : connect( m_topBottomScroll, SIGNAL(valueChanged(int)), this, SLOT(verScrolled(int))); - // init pixmaps - if (s_toolDraw == nullptr) - { - s_toolDraw = new QPixmap(embed::getIconPixmap("edit_draw")); - } - if (s_toolErase == nullptr) - { - s_toolErase= new QPixmap(embed::getIconPixmap("edit_erase")); - } - if (s_toolDrawOut == nullptr) - { - s_toolDrawOut = new QPixmap(embed::getIconPixmap("edit_draw_outvalue")); - } - if (s_toolEditTangents == nullptr) - { - s_toolEditTangents = new QPixmap(embed::getIconPixmap("edit_tangent")); - } - if (s_toolMove == nullptr) - { - s_toolMove = new QPixmap(embed::getIconPixmap("edit_move")); - } - setCurrentClip(nullptr); setMouseTracking( true ); @@ -1400,26 +1358,26 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) { case EditMode::Draw: { - if (m_action == Action::EraseValues) { cursor = s_toolErase; } - else if (m_action == Action::MoveValue) { cursor = s_toolMove; } - else { cursor = s_toolDraw; } + if (m_action == Action::EraseValues) { cursor = &m_toolErase; } + else if (m_action == Action::MoveValue) { cursor = &m_toolMove; } + else { cursor = &m_toolDraw; } break; } case EditMode::Erase: { - cursor = s_toolErase; + cursor = &m_toolErase; break; } case EditMode::DrawOutValues: { - if (m_action == Action::ResetOutValues) { cursor = s_toolErase; } - else if (m_action == Action::MoveOutValue) { cursor = s_toolMove; } - else { cursor = s_toolDrawOut; } + if (m_action == Action::ResetOutValues) { cursor = &m_toolErase; } + else if (m_action == Action::MoveOutValue) { cursor = &m_toolMove; } + else { cursor = &m_toolDrawOut; } break; } case EditMode::EditTangents: { - cursor = m_action == Action::MoveTangent ? s_toolMove : s_toolEditTangents; + cursor = m_action == Action::MoveTangent ? &m_toolMove : &m_toolEditTangents; break; } } diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 2d8f9cbc2..5ec8c323c 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -119,15 +119,6 @@ const int INITIAL_START_KEY = Octave::Octave_4 + Key::C; const int NUM_EVEN_LENGTHS = 6; const int NUM_TRIPLET_LENGTHS = 5; - - -QPixmap * PianoRoll::s_toolDraw = nullptr; -QPixmap * PianoRoll::s_toolErase = nullptr; -QPixmap * PianoRoll::s_toolSelect = nullptr; -QPixmap * PianoRoll::s_toolMove = nullptr; -QPixmap * PianoRoll::s_toolOpen = nullptr; -QPixmap* PianoRoll::s_toolKnife = nullptr; - SimpleTextFloat * PianoRoll::s_textFloat = nullptr; static std::array s_noteStrings { @@ -264,32 +255,6 @@ PianoRoll::PianoRoll() : m_semiToneMarkerMenu->addAction( unmarkAllAction ); m_semiToneMarkerMenu->addAction( copyAllNotesAction ); - // init pixmaps - if( s_toolDraw == nullptr ) - { - s_toolDraw = new QPixmap( embed::getIconPixmap( "edit_draw" ) ); - } - if( s_toolErase == nullptr ) - { - s_toolErase= new QPixmap( embed::getIconPixmap( "edit_erase" ) ); - } - if( s_toolSelect == nullptr ) - { - s_toolSelect = new QPixmap( embed::getIconPixmap( "edit_select" ) ); - } - if( s_toolMove == nullptr ) - { - s_toolMove = new QPixmap( embed::getIconPixmap( "edit_move" ) ); - } - if( s_toolOpen == nullptr ) - { - s_toolOpen = new QPixmap( embed::getIconPixmap( "automation" ) ); - } - if (s_toolKnife == nullptr) - { - s_toolKnife = new QPixmap(embed::getIconPixmap("edit_knife")); - } - // init text-float if( s_textFloat == nullptr ) { @@ -3703,21 +3668,29 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) case EditMode::Draw: if( m_mouseDownRight ) { - cursor = s_toolErase; + cursor = &m_toolErase; } else if( m_action == Action::MoveNote ) { - cursor = s_toolMove; + cursor = &m_toolMove; } else { - cursor = s_toolDraw; + cursor = &m_toolDraw; } break; - case EditMode::Erase: cursor = s_toolErase; break; - case EditMode::Select: cursor = s_toolSelect; break; - case EditMode::Detuning: cursor = s_toolOpen; break; - case EditMode::Knife: cursor = s_toolKnife; break; + case EditMode::Erase: + cursor = &m_toolErase; + break; + case EditMode::Select: + cursor = &m_toolSelect; + break; + case EditMode::Detuning: + cursor = &m_toolOpen; + break; + case EditMode::Knife: + cursor = &m_toolKnife; + break; } QPoint mousePosition = mapFromGlobal( QCursor::pos() ); if( cursor != nullptr && mousePosition.y() > keyAreaTop() && mousePosition.x() > noteEditLeft()) diff --git a/src/gui/editors/TimeLineWidget.cpp b/src/gui/editors/TimeLineWidget.cpp index 423485a25..049a8623f 100644 --- a/src/gui/editors/TimeLineWidget.cpp +++ b/src/gui/editors/TimeLineWidget.cpp @@ -45,9 +45,6 @@ namespace constexpr int MIN_BAR_LABEL_DISTANCE = 35; } - -QPixmap * TimeLineWidget::s_posMarkerPixmap = nullptr; - TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, Song::PlayPos & pos, const TimePos & begin, Song::PlayMode mode, QWidget * parent ) : @@ -80,16 +77,10 @@ TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, m_loopPos[0] = 0; m_loopPos[1] = DefaultTicksPerBar; - if( s_posMarkerPixmap == nullptr ) - { - s_posMarkerPixmap = new QPixmap( embed::getIconPixmap( - "playpos_marker" ) ); - } - setAttribute( Qt::WA_OpaquePaintEvent, true ); move( 0, yoff ); - m_xOffset -= s_posMarkerPixmap->width() / 2; + m_xOffset -= m_posMarkerPixmap.width() / 2; setMouseTracking(true); m_pos.m_timeLine = this; @@ -119,7 +110,7 @@ TimeLineWidget::~TimeLineWidget() void TimeLineWidget::setXOffset(const int x) { m_xOffset = x; - if (s_posMarkerPixmap != nullptr) { m_xOffset -= s_posMarkerPixmap->width() / 2; } + m_xOffset -= m_posMarkerPixmap.width() / 2; } @@ -245,7 +236,7 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) p.fillRect( 0, 0, width(), height(), p.background() ); // Clip so that we only draw everything starting from the offset - const int leftMargin = m_xOffset + s_posMarkerPixmap->width() / 2; + const int leftMargin = m_xOffset + m_posMarkerPixmap.width() / 2; p.setClipRect(leftMargin, 0, width() - leftMargin, height() ); // Draw the loop rectangle @@ -273,8 +264,8 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) QColor const & barNumberColor = getBarNumberColor(); bar_t barNumber = m_begin.getBar(); - int const x = m_xOffset + s_posMarkerPixmap->width() / 2 - - ( ( static_cast( m_begin * m_ppb ) / TimePos::ticksPerBar() ) % static_cast( m_ppb ) ); + int const x = m_xOffset + m_posMarkerPixmap.width() / 2 + - ((static_cast(m_begin * m_ppb) / TimePos::ticksPerBar()) % static_cast(m_ppb)); // Double the interval between bar numbers until they are far enough appart int barLabelInterval = 1; @@ -307,12 +298,12 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) p.drawRect( innerRectangle ); // Only draw the position marker if the position line is in view - if (m_posMarkerX >= m_xOffset && m_posMarkerX < width() - s_posMarkerPixmap->width() / 2) + if (m_posMarkerX >= m_xOffset && m_posMarkerX < width() - m_posMarkerPixmap.width() / 2) { // Let the position marker extrude to the left p.setClipping(false); p.setOpacity(0.6); - p.drawPixmap(m_posMarkerX, height() - s_posMarkerPixmap->height(), *s_posMarkerPixmap); + p.drawPixmap(m_posMarkerX, height() - m_posMarkerPixmap.height(), m_posMarkerPixmap); } } @@ -328,13 +319,13 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) if( event->button() == Qt::LeftButton && !(event->modifiers() & Qt::ShiftModifier) ) { m_action = Action::MovePositionMarker; - if( event->x() - m_xOffset < s_posMarkerPixmap->width() ) + if (event->x() - m_xOffset < m_posMarkerPixmap.width()) { m_moveXOff = event->x() - m_xOffset; } else { - m_moveXOff = s_posMarkerPixmap->width() / 2; + m_moveXOff = m_posMarkerPixmap.width() / 2; } } else if( event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier) ) @@ -344,7 +335,7 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) } else if( event->button() == Qt::RightButton ) { - m_moveXOff = s_posMarkerPixmap->width() / 2; + m_moveXOff = m_posMarkerPixmap.width() / 2; const TimePos t = m_begin + static_cast( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * TimePos::ticksPerBar() / m_ppb ); const TimePos loopMid = ( m_loopPos[0] + m_loopPos[1] ) / 2; diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index edb6c99c7..2a1928326 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -79,25 +79,11 @@ const int LFO_AMOUNT_KNOB_X = LFO_SPEED_KNOB_X+KNOB_X_SPACING; const int LFO_SHAPES_X = LFO_GRAPH_X;//PREDELAY_KNOB_X; const int LFO_SHAPES_Y = LFO_GRAPH_Y + 50; - -QPixmap * EnvelopeAndLfoView::s_envGraph = nullptr; -QPixmap * EnvelopeAndLfoView::s_lfoGraph = nullptr; - - - EnvelopeAndLfoView::EnvelopeAndLfoView( QWidget * _parent ) : QWidget( _parent ), ModelView( nullptr, this ), m_params( nullptr ) { - if( s_envGraph == nullptr ) - { - s_envGraph = new QPixmap( embed::getIconPixmap( "envelope_graph" ) ); - } - if( s_lfoGraph == nullptr ) - { - s_lfoGraph = new QPixmap( embed::getIconPixmap( "lfo_graph" ) ); - } m_predelayKnob = new Knob( KnobType::Bright26, this ); m_predelayKnob->setLabel( tr( "DEL" ) ); @@ -277,8 +263,7 @@ void EnvelopeAndLfoView::mousePressEvent( QMouseEvent * _me ) return; } - if( QRect( ENV_GRAPH_X, ENV_GRAPH_Y, s_envGraph->width(), - s_envGraph->height() ).contains( _me->pos() ) == true ) + if (QRect(ENV_GRAPH_X, ENV_GRAPH_Y, m_envGraph.width(), m_envGraph.height()).contains(_me->pos())) { if( m_params->m_amountModel.value() < 1.0f ) { @@ -289,8 +274,7 @@ void EnvelopeAndLfoView::mousePressEvent( QMouseEvent * _me ) m_params->m_amountModel.setValue( 0.0f ); } } - else if( QRect( LFO_GRAPH_X, LFO_GRAPH_Y, s_lfoGraph->width(), - s_lfoGraph->height() ).contains( _me->pos() ) == true ) + else if (QRect(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph.width(), m_lfoGraph.height()).contains(_me->pos())) { if( m_params->m_lfoAmountModel.value() < 1.0f ) { @@ -351,10 +335,9 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) p.setRenderHint( QPainter::Antialiasing ); // draw envelope-graph - p.drawPixmap( ENV_GRAPH_X, ENV_GRAPH_Y, *s_envGraph ); + p.drawPixmap(ENV_GRAPH_X, ENV_GRAPH_Y, m_envGraph); // draw LFO-graph - p.drawPixmap( LFO_GRAPH_X, LFO_GRAPH_Y, *s_lfoGraph ); - + p.drawPixmap(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph); p.setFont( pointSize<8>( p.font() ) ); @@ -368,8 +351,8 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) const QColor end_points_color( 0x99, 0xAF, 0xFF ); const QColor end_points_bg_color( 0, 0, 2 ); - const int y_base = ENV_GRAPH_Y + s_envGraph->height() - 3; - const int avail_height = s_envGraph->height() - 6; + const int y_base = ENV_GRAPH_Y + m_envGraph.height() - 3; + const int avail_height = m_envGraph.height() - 6; int x1 = static_cast( m_predelayKnob->value() * TIME_UNIT_WIDTH ); int x2 = x1 + static_cast( m_attackKnob->value() * TIME_UNIT_WIDTH ); @@ -422,9 +405,8 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) p.fillRect( x5 - 1, y_base - 2, 4, 4, end_points_bg_color ); p.fillRect( x5, y_base - 1, 2, 2, end_points_color ); - - int LFO_GRAPH_W = s_lfoGraph->width() - 3; // subtract border - int LFO_GRAPH_H = s_lfoGraph->height() - 6; // subtract border + int LFO_GRAPH_W = m_lfoGraph.width() - 3; // subtract border + int LFO_GRAPH_H = m_lfoGraph.height() - 6; // subtract border int graph_x_base = LFO_GRAPH_X + 2; int graph_y_base = LFO_GRAPH_Y + 3 + LFO_GRAPH_H / 2; @@ -505,11 +487,8 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) int ms_per_osc = static_cast( SECS_PER_LFO_OSCILLATION * m_lfoSpeedKnob->value() * 1000.0f ); - p.drawText( LFO_GRAPH_X + 4, LFO_GRAPH_Y + s_lfoGraph->height() - 6, - tr( "ms/LFO:" ) ); - p.drawText( LFO_GRAPH_X + 52, LFO_GRAPH_Y + s_lfoGraph->height() - 6, - QString::number( ms_per_osc ) ); - + p.drawText(LFO_GRAPH_X + 4, LFO_GRAPH_Y + m_lfoGraph.height() - 6, tr("ms/LFO:")); + p.drawText(LFO_GRAPH_X + 52, LFO_GRAPH_Y + m_lfoGraph.height() - 6, QString::number(ms_per_osc)); } @@ -536,4 +515,4 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() } // namespace gui -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index d20cbcac5..c8882898b 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -67,15 +67,6 @@ auto WhiteKeys = std::array Key::C, Key::D, Key::E, Key::F, Key::G, Key::A, Key::H } ; - -QPixmap * PianoView::s_whiteKeyPm = nullptr; /*!< A white key released */ -QPixmap * PianoView::s_blackKeyPm = nullptr; /*!< A black key released */ -QPixmap * PianoView::s_whiteKeyPressedPm = nullptr; /*!< A white key pressed */ -QPixmap * PianoView::s_blackKeyPressedPm = nullptr; /*!< A black key pressed */ -QPixmap * PianoView::s_whiteKeyDisabledPm = nullptr; /*!< A white key disabled */ -QPixmap * PianoView::s_blackKeyDisabledPm = nullptr; /*!< A black key disabled */ - - const int PIANO_BASE = 11; /*!< The height of the root note display */ const int PW_WHITE_KEY_WIDTH = 10; /*!< The width of a white key */ const int PW_BLACK_KEY_WIDTH = 8; /*!< The width of a black key */ @@ -99,31 +90,6 @@ PianoView::PianoView(QWidget *parent) : m_lastKey(-1), /*!< The last key displayed? */ m_movedNoteModel(nullptr) /*!< Key marker which is being moved */ { - if (s_whiteKeyPm == nullptr) - { - s_whiteKeyPm = new QPixmap(embed::getIconPixmap("white_key")); - } - if (s_blackKeyPm == nullptr) - { - s_blackKeyPm = new QPixmap(embed::getIconPixmap("black_key")); - } - if (s_whiteKeyPressedPm == nullptr) - { - s_whiteKeyPressedPm = new QPixmap(embed::getIconPixmap("white_key_pressed")); - } - if (s_blackKeyPressedPm == nullptr) - { - s_blackKeyPressedPm = new QPixmap(embed::getIconPixmap("black_key_pressed")); - } - if (s_whiteKeyDisabledPm == nullptr) - { - s_whiteKeyDisabledPm = new QPixmap(embed::getIconPixmap("white_key_disabled")); - } - if (s_blackKeyDisabledPm == nullptr) - { - s_blackKeyDisabledPm = new QPixmap(embed::getIconPixmap("black_key_disabled")); - } - setAttribute(Qt::WA_OpaquePaintEvent, true); setFocusPolicy(Qt::StrongFocus); @@ -894,16 +860,16 @@ void PianoView::paintEvent( QPaintEvent * ) { if (m_piano && m_piano->isKeyPressed(cur_key)) { - p.drawPixmap(x, PIANO_BASE, *s_whiteKeyPressedPm); + p.drawPixmap(x, PIANO_BASE, m_whiteKeyPressedPm); } else { - p.drawPixmap(x, PIANO_BASE, *s_whiteKeyPm); + p.drawPixmap(x, PIANO_BASE, m_whiteKeyPm); } } else { - p.drawPixmap(x, PIANO_BASE, *s_whiteKeyDisabledPm); + p.drawPixmap(x, PIANO_BASE, m_whiteKeyDisabledPm); } x += PW_WHITE_KEY_WIDTH; @@ -928,16 +894,16 @@ void PianoView::paintEvent( QPaintEvent * ) { if (m_piano && m_piano->isKeyPressed(startKey)) { - p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPressedPm); + p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPressedPm); } else { - p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPm); + p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPm); } } else { - p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyDisabledPm); + p.drawPixmap(0 - PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyDisabledPm); } } @@ -951,16 +917,16 @@ void PianoView::paintEvent( QPaintEvent * ) { if (m_piano && m_piano->isKeyPressed(cur_key)) { - p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPressedPm); + p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPressedPm); } else { - p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyPm); + p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyPm); } } else { - p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, *s_blackKeyDisabledPm); + p.drawPixmap(x + PW_WHITE_KEY_WIDTH / 2, PIANO_BASE, m_blackKeyDisabledPm); } x += PW_WHITE_KEY_WIDTH; white_cnt = 0; diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index 2377a37ab..ccc0c675b 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -38,11 +38,6 @@ namespace lmms::gui { - -QPixmap * ComboBox::s_background = nullptr; -QPixmap * ComboBox::s_arrow = nullptr; -QPixmap * ComboBox::s_arrowSelected = nullptr; - const int CB_ARROW_BTN_WIDTH = 18; @@ -54,21 +49,6 @@ ComboBox::ComboBox( QWidget * _parent, const QString & _name ) : { setFixedHeight( ComboBox::DEFAULT_HEIGHT ); - if( s_background == nullptr ) - { - s_background = new QPixmap( embed::getIconPixmap( "combobox_bg" ) ); - } - - if( s_arrow == nullptr ) - { - s_arrow = new QPixmap( embed::getIconPixmap( "combobox_arrow" ) ); - } - - if( s_arrowSelected == nullptr ) - { - s_arrowSelected = new QPixmap( embed::getIconPixmap( "combobox_arrow_selected" ) ); - } - setFont( pointSize<9>( font() ) ); connect( &m_menu, SIGNAL(triggered(QAction*)), @@ -172,7 +152,7 @@ void ComboBox::paintEvent( QPaintEvent * _pe ) { QPainter p( this ); - p.fillRect( 2, 2, width()-2, height()-4, *s_background ); + p.fillRect(2, 2, width() - 2, height() - 4, m_background); QColor shadow = palette().shadow().color(); QColor highlight = palette().highlight().color(); @@ -194,9 +174,9 @@ void ComboBox::paintEvent( QPaintEvent * _pe ) style()->drawPrimitive( QStyle::PE_Frame, &opt, &p, this ); - QPixmap * arrow = m_pressed ? s_arrowSelected : s_arrow; + auto arrow = m_pressed ? m_arrowSelected : m_arrow; - p.drawPixmap( width() - CB_ARROW_BTN_WIDTH + 3, 4, *arrow ); + p.drawPixmap(width() - CB_ARROW_BTN_WIDTH + 3, 4, arrow); if( model() && model()->size() > 0 ) { diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index dcf648c37..9ddbc74e1 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -59,11 +59,7 @@ namespace lmms::gui { - SimpleTextFloat * Fader::s_textFloat = nullptr; -QPixmap * Fader::s_back = nullptr; -QPixmap * Fader::s_leds = nullptr; -QPixmap * Fader::s_knob = nullptr; Fader::Fader( FloatModel * _model, const QString & _name, QWidget * _parent ) : QWidget( _parent ), @@ -85,22 +81,6 @@ Fader::Fader( FloatModel * _model, const QString & _name, QWidget * _parent ) : { s_textFloat = new SimpleTextFloat; } - if( ! s_back ) - { - s_back = new QPixmap( embed::getIconPixmap( "fader_background" ) ); - } - if( ! s_leds ) - { - s_leds = new QPixmap( embed::getIconPixmap( "fader_leds" ) ); - } - if( ! s_knob ) - { - s_knob = new QPixmap( embed::getIconPixmap( "fader_knob" ) ); - } - - m_back = s_back; - m_leds = s_leds; - m_knob = s_knob; init(_model, _name); @@ -128,10 +108,6 @@ Fader::Fader( FloatModel * model, const QString & name, QWidget * parent, QPixma s_textFloat = new SimpleTextFloat; } - m_back = back; - m_leds = leds; - m_knob = knob; - init(model, name); } @@ -139,7 +115,7 @@ void Fader::init(FloatModel * model, QString const & name) { setWindowTitle( name ); setAttribute( Qt::WA_OpaquePaintEvent, false ); - QSize backgroundSize = m_back->size(); + QSize backgroundSize = m_back.size(); setMinimumSize( backgroundSize ); setMaximumSize( backgroundSize ); resize( backgroundSize ); @@ -166,7 +142,7 @@ void Fader::mouseMoveEvent( QMouseEvent *mouseEvent ) { int dy = m_moveStartPoint - mouseEvent->globalY(); - float delta = dy * ( model()->maxValue() - model()->minValue() ) / (float) ( height() - ( *m_knob ).height() ); + float delta = dy * (model()->maxValue() - model()->minValue()) / (float)(height() - (m_knob).height()); const auto step = model()->step(); float newValue = static_cast( static_cast( ( m_startValue + delta ) / step + 0.5 ) ) * step; @@ -191,7 +167,7 @@ void Fader::mousePressEvent( QMouseEvent* mouseEvent ) thisModel->saveJournallingState( false ); } - if( mouseEvent->y() >= knobPosY() - ( *m_knob ).height() && mouseEvent->y() < knobPosY() ) + if (mouseEvent->y() >= knobPosY() - (m_knob).height() && mouseEvent->y() < knobPosY()) { updateTextFloat(); s_textFloat->show(); @@ -335,9 +311,9 @@ void Fader::updateTextFloat() inline int Fader::calculateDisplayPeak( float fPeak ) { - int peak = (int)( m_back->height() - ( fPeak / ( m_fMaxPeak - m_fMinPeak ) ) * m_back->height() ); + int peak = static_cast(m_back.height() - (fPeak / (m_fMaxPeak - m_fMinPeak)) * m_back.height()); - return qMin( peak, m_back->height() ); + return qMin(peak, m_back.height()); } @@ -346,7 +322,7 @@ void Fader::paintEvent( QPaintEvent * ev) QPainter painter(this); // Draw the background - painter.drawPixmap( ev->rect(), *m_back, ev->rect() ); + painter.drawPixmap(ev->rect(), m_back, ev->rect()); // Draw the levels with peaks if (getLevelsDisplayedInDBFS()) @@ -359,14 +335,14 @@ void Fader::paintEvent( QPaintEvent * ev) } // Draw the knob - painter.drawPixmap( 0, knobPosY() - m_knob->height(), *m_knob ); + painter.drawPixmap(0, knobPosY() - m_knob.height(), m_knob); } void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) { - int height = m_back->height(); - int width = m_back->width() / 2; - int center = m_back->width() - width; + int height = m_back.height(); + int width = m_back.width() / 2; + int center = m_back.width() - width; float const maxDB(ampToDbfs(m_fMaxPeak)); float const minDB(ampToDbfs(m_fMinPeak)); @@ -380,7 +356,7 @@ void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) float const leftSpan = ampToDbfs(qMax(0.0001, m_fPeakValue_L)) - minDB; int peak_L = height * leftSpan * fullSpanReciprocal; QRect drawRectL( 0, height - peak_L, width, peak_L ); // Source and target are identical - painter.drawPixmap( drawRectL, *m_leds, drawRectL ); + painter.drawPixmap(drawRectL, m_leds, drawRectL); float const persistentLeftPeakDBFS = ampToDbfs(qMax(0.0001, m_persistentPeak_L)); int persistentPeak_L = height * (1 - (persistentLeftPeakDBFS - minDB) * fullSpanReciprocal); @@ -402,7 +378,7 @@ void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) float const rightSpan = ampToDbfs(qMax(0.0001, m_fPeakValue_R)) - minDB; int peak_R = height * rightSpan * fullSpanReciprocal; QRect const drawRectR( center, height - peak_R, width, peak_R ); // Source and target are identical - painter.drawPixmap( drawRectR, *m_leds, drawRectR ); + painter.drawPixmap(drawRectR, m_leds, drawRectR); float const persistentRightPeakDBFS = ampToDbfs(qMax(0.0001, m_persistentPeak_R)); int persistentPeak_R = height * (1 - (persistentRightPeakDBFS - minDB) * fullSpanReciprocal); @@ -425,13 +401,13 @@ void Fader::paintLinearLevels(QPaintEvent * ev, QPainter & painter) // peak leds //float fRange = abs( m_fMaxPeak ) + abs( m_fMinPeak ); - int height = m_back->height(); - int width = m_back->width() / 2; - int center = m_back->width() - width; + int height = m_back.height(); + int width = m_back.width() / 2; + int center = m_back.width() - width; int peak_L = calculateDisplayPeak( m_fPeakValue_L - m_fMinPeak ); int persistentPeak_L = qMax( 3, calculateDisplayPeak( m_persistentPeak_L - m_fMinPeak ) ); - painter.drawPixmap( QRect( 0, peak_L, width, height - peak_L ), *m_leds, QRect( 0, peak_L, width, height - peak_L ) ); + painter.drawPixmap(QRect(0, peak_L, width, height - peak_L), m_leds, QRect(0, peak_L, width, height - peak_L)); if( m_persistentPeak_L > 0.05 ) { @@ -442,7 +418,7 @@ void Fader::paintLinearLevels(QPaintEvent * ev, QPainter & painter) int peak_R = calculateDisplayPeak( m_fPeakValue_R - m_fMinPeak ); int persistentPeak_R = qMax( 3, calculateDisplayPeak( m_persistentPeak_R - m_fMinPeak ) ); - painter.drawPixmap( QRect( center, peak_R, width, height - peak_R ), *m_leds, QRect( center, peak_R, width, height - peak_R ) ); + painter.drawPixmap(QRect(center, peak_R, width, height - peak_R), m_leds, QRect(center, peak_R, width, height - peak_R)); if( m_persistentPeak_R > 0.05 ) { diff --git a/src/gui/widgets/LcdWidget.cpp b/src/gui/widgets/LcdWidget.cpp index b8afcd46d..a409fee8b 100644 --- a/src/gui/widgets/LcdWidget.cpp +++ b/src/gui/widgets/LcdWidget.cpp @@ -66,17 +66,6 @@ LcdWidget::LcdWidget(int numDigits, const QString& style, QWidget* parent, const initUi( name, style ); } - - - -LcdWidget::~LcdWidget() -{ - delete m_lcdPixmap; -} - - - - void LcdWidget::setValue(int value) { QString s = m_textForValue[value]; @@ -162,11 +151,8 @@ void LcdWidget::paintEvent( QPaintEvent* ) { p.translate(margin, margin); // Left Margin - p.drawPixmap( - cellRect, - *m_lcdPixmap, - QRect(QPoint(charsPerPixmap * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize) - ); + p.drawPixmap(cellRect, m_lcdPixmap, + QRect(QPoint(charsPerPixmap * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize)); p.translate(m_marginWidth, 0); } @@ -174,8 +160,7 @@ void LcdWidget::paintEvent( QPaintEvent* ) // Padding for( int i=0; i < m_numDigits - m_display.length(); i++ ) { - p.drawPixmap( cellRect, *m_lcdPixmap, - QRect( QPoint( 10 * m_cellWidth, isEnabled()?0:m_cellHeight) , cellSize ) ); + p.drawPixmap(cellRect, m_lcdPixmap, QRect(QPoint(10 * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize)); p.translate( m_cellWidth, 0 ); } @@ -189,19 +174,14 @@ void LcdWidget::paintEvent( QPaintEvent* ) else val = 10; } - p.drawPixmap( cellRect, *m_lcdPixmap, - QRect( QPoint( val*m_cellWidth, - isEnabled()?0:m_cellHeight ), - cellSize ) ); + p.drawPixmap(cellRect, m_lcdPixmap, QRect(QPoint(val * m_cellWidth, isEnabled() ? 0 : m_cellHeight), cellSize)); p.translate( m_cellWidth, 0 ); } // Right Margin - p.drawPixmap(QRect(0, 0, m_seamlessRight ? 0 : m_marginWidth - 1, m_cellHeight), - *m_lcdPixmap, + p.drawPixmap(QRect(0, 0, m_seamlessRight ? 0 : m_marginWidth - 1, m_cellHeight), m_lcdPixmap, QRect(charsPerPixmap * m_cellWidth, isEnabled() ? 0 : m_cellHeight, m_cellWidth / 2, m_cellHeight)); - p.restore(); // Border @@ -294,12 +274,12 @@ void LcdWidget::initUi(const QString& name , const QString& style) setWindowTitle( name ); // We should make a factory for these or something. - //m_lcdPixmap = new QPixmap( embed::getIconPixmap( QString( "lcd_" + style ).toUtf8().constData() ) ); - //m_lcdPixmap = new QPixmap( embed::getIconPixmap( "lcd_19green" ) ); // TODO!! - m_lcdPixmap = new QPixmap( embed::getIconPixmap( QString( "lcd_" + style ).toUtf8().constData() ) ); + //m_lcdPixmap = embed::getIconPixmap(QString("lcd_" + style).toUtf8().constData()); + //m_lcdPixmap = embed::getIconPixmap("lcd_19green"); // TODO!! - m_cellWidth = m_lcdPixmap->size().width() / LcdWidget::charsPerPixmap; - m_cellHeight = m_lcdPixmap->size().height() / 2; + m_lcdPixmap = embed::getIconPixmap(QString("lcd_" + style).toUtf8().constData()); + m_cellWidth = m_lcdPixmap.size().width() / LcdWidget::charsPerPixmap; + m_cellHeight = m_lcdPixmap.size().height() / 2; m_marginWidth = m_cellWidth / 2; diff --git a/src/gui/widgets/LedCheckBox.cpp b/src/gui/widgets/LedCheckBox.cpp index 75e73328f..1dbf650ed 100644 --- a/src/gui/widgets/LedCheckBox.cpp +++ b/src/gui/widgets/LedCheckBox.cpp @@ -61,17 +61,6 @@ LedCheckBox::LedCheckBox( QWidget * _parent, { } - - -LedCheckBox::~LedCheckBox() -{ - delete m_ledOnPixmap; - delete m_ledOffPixmap; -} - - - - void LedCheckBox::setText( const QString &s ) { m_text = s; @@ -100,9 +89,8 @@ void LedCheckBox::initUi( LedColor _color ) { setCheckable( true ); - m_ledOnPixmap = new QPixmap( embed::getIconPixmap( - names[static_cast(_color)].toUtf8().constData() ) ); - m_ledOffPixmap = new QPixmap( embed::getIconPixmap( "led_off" ) ); + m_ledOnPixmap = embed::getIconPixmap(names[static_cast(_color)].toUtf8().constData()); + m_ledOffPixmap = embed::getIconPixmap("led_off"); if (m_legacyMode) { @@ -119,8 +107,8 @@ void LedCheckBox::onTextUpdated() { QFontMetrics const fm = fontMetrics(); - int const width = m_ledOffPixmap->width() + 5 + horizontalAdvance(fm, text()); - int const height = m_legacyMode ? m_ledOffPixmap->height() : qMax(m_ledOffPixmap->height(), fm.height()); + int const width = m_ledOffPixmap.width() + 5 + horizontalAdvance(fm, text()); + int const height = m_legacyMode ? m_ledOffPixmap.height() : qMax(m_ledOffPixmap.height(), fm.height()); setFixedSize(width, height); } @@ -130,31 +118,24 @@ void LedCheckBox::paintLegacy(QPaintEvent * pe) QPainter p( this ); p.setFont( pointSize<7>( font() ) ); - if( model()->value() == true ) - { - p.drawPixmap( 0, 0, *m_ledOnPixmap ); - } - else - { - p.drawPixmap( 0, 0, *m_ledOffPixmap ); - } + p.drawPixmap(0, 0, model()->value() ? m_ledOnPixmap : m_ledOffPixmap); p.setPen( QColor( 64, 64, 64 ) ); - p.drawText( m_ledOffPixmap->width() + 4, 11, text() ); + p.drawText(m_ledOffPixmap.width() + 4, 11, text()); p.setPen( QColor( 255, 255, 255 ) ); - p.drawText( m_ledOffPixmap->width() + 3, 10, text() ); + p.drawText(m_ledOffPixmap.width() + 3, 10, text()); } void LedCheckBox::paintNonLegacy(QPaintEvent * pe) { QPainter p(this); - QPixmap * drawnPixmap = model()->value() ? m_ledOnPixmap : m_ledOffPixmap; + auto drawnPixmap = model()->value() ? m_ledOnPixmap : m_ledOffPixmap; - p.drawPixmap( 0, rect().height() / 2 - drawnPixmap->height() / 2, *drawnPixmap); + p.drawPixmap(0, rect().height() / 2 - drawnPixmap.height() / 2, drawnPixmap); QRect r = rect(); - r -= QMargins(m_ledOffPixmap->width() + 5, 0, 0, 0); + r -= QMargins(m_ledOffPixmap.width() + 5, 0, 0, 0); p.drawText(r, text()); } diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index 087079dc8..ab54b9408 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -39,13 +39,6 @@ namespace lmms { -QPixmap * gui::MidiClipView::s_stepBtnOn0 = nullptr; -QPixmap * gui::MidiClipView::s_stepBtnOn200 = nullptr; -QPixmap * gui::MidiClipView::s_stepBtnOff = nullptr; -QPixmap * gui::MidiClipView::s_stepBtnOffLight = nullptr; - - - MidiClip::MidiClip( InstrumentTrack * _instrument_track ) : Clip( _instrument_track ), m_instrumentTrack( _instrument_track ), From dc8c49a5390015ef3ffcf42a9fd63719b2d9f1d4 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Sun, 19 Nov 2023 14:52:37 +0000 Subject: [PATCH 045/191] Use `std::optional` for colour interfaces and storage (#6991) --- include/Clip.h | 25 ++-------- include/ClipView.h | 4 +- include/Mixer.h | 11 ++--- include/Track.h | 15 ++---- src/core/AutomationClip.cpp | 11 ++--- src/core/Clip.cpp | 17 ++----- src/core/Mixer.cpp | 8 ++-- src/core/PatternClip.cpp | 16 ++----- src/core/SampleClip.cpp | 13 ++---- src/core/Track.cpp | 23 ++-------- src/gui/MixerLine.cpp | 24 +++++----- src/gui/clips/ClipView.cpp | 58 ++++++++---------------- src/gui/clips/MidiClipView.cpp | 2 +- src/gui/clips/SampleClipView.cpp | 6 +-- src/gui/tracks/InstrumentTrackView.cpp | 2 +- src/gui/tracks/SampleTrackView.cpp | 2 +- src/gui/tracks/TrackOperationsWidget.cpp | 27 +++++------ src/tracks/MidiClip.cpp | 17 +++---- 18 files changed, 92 insertions(+), 189 deletions(-) diff --git a/include/Clip.h b/include/Clip.h index 96394602f..0b540ccfb 100644 --- a/include/Clip.h +++ b/include/Clip.h @@ -25,6 +25,8 @@ #ifndef LMMS_CLIP_H #define LMMS_CLIP_H +#include + #include #include "AutomatableModel.h" @@ -109,24 +111,8 @@ public: return m_autoResize; } - QColor color() const - { - return m_color; - } - - void setColor( const QColor & c ) - { - m_color = c; - } - - bool hasColor(); - - void useCustomClipColor( bool b ); - - bool usesCustomClipColor() - { - return m_useCustomClipColor; - } + auto color() const -> const std::optional& { return m_color; } + void setColor(const std::optional& color); virtual void movePosition( const TimePos & pos ); virtual void changeLength( const TimePos & length ); @@ -177,8 +163,7 @@ private: bool m_selectViewOnCreate; - QColor m_color; - bool m_useCustomClipColor; + std::optional m_color; friend class ClipView; diff --git a/include/ClipView.h b/include/ClipView.h index 942258367..14898db65 100644 --- a/include/ClipView.h +++ b/include/ClipView.h @@ -25,6 +25,7 @@ #ifndef LMMS_GUI_CLIP_VIEW_H #define LMMS_GUI_CLIP_VIEW_H +#include #include @@ -184,6 +185,7 @@ protected: virtual void paintTextLabel(QString const & text, QPainter & painter); + auto hasCustomColor() const -> bool; protected slots: void updateLength(); @@ -241,7 +243,7 @@ private: bool mouseMovedDistance( QMouseEvent * me, int distance ); TimePos draggedClipPos( QMouseEvent * me ); int knifeMarkerPos( QMouseEvent * me ); - void setColor(const QColor* color); + void setColor(const std::optional& color); //! Return true iff the clip could be split. Currently only implemented for samples virtual bool splitClip( const TimePos pos ){ return false; }; void updateCursor(QMouseEvent * me); diff --git a/include/Mixer.h b/include/Mixer.h index 6e5a12786..302492cab 100644 --- a/include/Mixer.h +++ b/include/Mixer.h @@ -76,12 +76,8 @@ class MixerChannel : public ThreadableJob bool requiresProcessing() const override { return true; } void unmuteForSolo(); - void setColor (QColor newColor) - { - m_color = newColor; - } - - std::optional m_color; + auto color() const -> const std::optional& { return m_color; } + void setColor(const std::optional& color) { m_color = color; } std::atomic_int m_dependenciesMet; void incrementDeps(); @@ -89,8 +85,9 @@ class MixerChannel : public ThreadableJob private: void doProcessing() override; -}; + std::optional m_color; +}; class MixerRoute : public QObject { diff --git a/include/Track.h b/include/Track.h index 71c6b0457..248d56b04 100644 --- a/include/Track.h +++ b/include/Track.h @@ -189,15 +189,9 @@ public: { return m_processingLock.tryLock(); } - - QColor color() - { - return m_color.value(); - } - bool useColor() - { - return m_color.has_value(); - } + + auto color() const -> const std::optional& { return m_color; } + void setColor(const std::optional& color); bool isMutedBeforeSolo() const { @@ -220,9 +214,6 @@ public slots: void toggleSolo(); - void setColor(const QColor& c); - void resetColor(); - private: TrackContainer* m_trackContainer; Type m_type; diff --git a/src/core/AutomationClip.cpp b/src/core/AutomationClip.cpp index 3bfc5cf8e..603550270 100644 --- a/src/core/AutomationClip.cpp +++ b/src/core/AutomationClip.cpp @@ -830,10 +830,10 @@ void AutomationClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "prog", QString::number( static_cast(progressionType()) ) ); _this.setAttribute( "tens", QString::number( getTension() ) ); _this.setAttribute( "mute", QString::number( isMuted() ) ); - - if( usesCustomClipColor() ) + + if (const auto& c = color()) { - _this.setAttribute( "color", color().name() ); + _this.setAttribute("color", c->name()); } for( timeMap::const_iterator it = m_timeMap.begin(); @@ -919,10 +919,9 @@ void AutomationClip::loadSettings( const QDomElement & _this ) } } - if( _this.hasAttribute( "color" ) ) + if (_this.hasAttribute("color")) { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); + setColor(QColor{_this.attribute("color")}); } int len = _this.attribute( "len" ).toInt(); diff --git a/src/core/Clip.cpp b/src/core/Clip.cpp index db1200aae..b18391df1 100644 --- a/src/core/Clip.cpp +++ b/src/core/Clip.cpp @@ -48,9 +48,7 @@ Clip::Clip( Track * track ) : m_startPosition(), m_length(), m_mutedModel( false, this, tr( "Mute" ) ), - m_selectViewOnCreate( false ), - m_color( 128, 128, 128 ), - m_useCustomClipColor( false ) + m_selectViewOnCreate{false} { if( getTrack() ) { @@ -185,19 +183,10 @@ void Clip::setStartTimeOffset( const TimePos &startTimeOffset ) m_startTimeOffset = startTimeOffset; } - - -void Clip::useCustomClipColor( bool b ) +void Clip::setColor(const std::optional& color) { - if (b == m_useCustomClipColor) { return; } - m_useCustomClipColor = b; + m_color = color; emit colorChanged(); } - -bool Clip::hasColor() -{ - return usesCustomClipColor() || getTrack()->useColor(); -} - } // namespace lmms diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index 31212313d..6dd2e3451 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -721,7 +721,7 @@ void Mixer::clearChannel(mix_ch_t index) ch->m_volumeModel.setDisplayName( ch->m_name + ">" + tr( "Volume" ) ); ch->m_muteModel.setDisplayName( ch->m_name + ">" + tr( "Mute" ) ); ch->m_soloModel.setDisplayName( ch->m_name + ">" + tr( "Solo" ) ); - ch->m_color = std::nullopt; + ch->setColor(std::nullopt); // send only to master if( index > 0) @@ -759,7 +759,7 @@ void Mixer::saveSettings( QDomDocument & _doc, QDomElement & _this ) ch->m_soloModel.saveSettings( _doc, mixch, "soloed" ); mixch.setAttribute( "num", i ); mixch.setAttribute( "name", ch->m_name ); - if (ch->m_color.has_value()) { mixch.setAttribute("color", ch->m_color->name()); } + if (const auto& color = ch->color()) { mixch.setAttribute("color", color->name()); } // add the channel sends for (const auto& send : ch->m_sends) @@ -805,9 +805,9 @@ void Mixer::loadSettings( const QDomElement & _this ) m_mixerChannels[num]->m_muteModel.loadSettings( mixch, "muted" ); m_mixerChannels[num]->m_soloModel.loadSettings( mixch, "soloed" ); m_mixerChannels[num]->m_name = mixch.attribute( "name" ); - if( mixch.hasAttribute( "color" ) ) + if (mixch.hasAttribute("color")) { - m_mixerChannels[num]->m_color = QColor(mixch.attribute("color")); + m_mixerChannels[num]->setColor(QColor{mixch.attribute("color")}); } m_mixerChannels[num]->m_fxChain.restoreState( mixch.firstChildElement( diff --git a/src/core/PatternClip.cpp b/src/core/PatternClip.cpp index 1058da6ba..15a1d1f54 100644 --- a/src/core/PatternClip.cpp +++ b/src/core/PatternClip.cpp @@ -62,9 +62,9 @@ void PatternClip::saveSettings(QDomDocument& doc, QDomElement& element) element.setAttribute( "len", length() ); element.setAttribute("off", startTimeOffset()); element.setAttribute( "muted", isMuted() ); - if( usesCustomClipColor() ) + if (const auto& c = color()) { - element.setAttribute( "color", color().name() ); + element.setAttribute("color", c->name()); } } @@ -90,20 +90,14 @@ void PatternClip::loadSettings(const QDomElement& element) if (!element.hasAttribute("usestyle")) { // for colors saved in 1.3-onwards - setColor(element.attribute("color")); - useCustomClipColor(true); + setColor(QColor{element.attribute("color")}); } - else + else if (element.attribute("usestyle").toUInt() == 0) { // for colors saved before 1.3 - setColor(QColor(element.attribute("color").toUInt())); - useCustomClipColor(element.attribute("usestyle").toUInt() == 0); + setColor(QColor{element.attribute("color").toUInt()}); } } - else - { - useCustomClipColor(false); - } } diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index b09d7b3bb..2febaee2e 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -271,9 +271,9 @@ void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) } _this.setAttribute( "sample_rate", m_sampleBuffer->sampleRate()); - if( usesCustomClipColor() ) + if (const auto& c = color()) { - _this.setAttribute( "color", color().name() ); + _this.setAttribute("color", c->name()); } if (m_sampleBuffer->reversed()) { @@ -304,14 +304,9 @@ void SampleClip::loadSettings( const QDomElement & _this ) setMuted( _this.attribute( "muted" ).toInt() ); setStartTimeOffset( _this.attribute( "off" ).toInt() ); - if( _this.hasAttribute( "color" ) ) + if (_this.hasAttribute("color")) { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); - } - else - { - useCustomClipColor(false); + setColor(QColor{_this.attribute("color")}); } if(_this.hasAttribute("reversed")) diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 2d4a2d840..7a664a11e 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -64,8 +64,7 @@ Track::Track( Type type, TrackContainer * tc ) : m_mutedModel( false, this, tr( "Mute" ) ), /*!< For controlling track muting */ m_soloModel( false, this, tr( "Solo" ) ), /*!< For controlling track soloing */ m_simpleSerializingMode( false ), - m_clips(), /*!< The clips (segments) */ - m_color(std::nullopt) + m_clips() /*!< The clips (segments) */ { m_trackContainer->addTrack( this ); m_height = -1; @@ -263,14 +262,9 @@ void Track::loadSettings( const QDomElement & element ) // Older project files that didn't have this attribute will set the value to false (issue 5562) m_mutedBeforeSolo = QVariant( element.attribute( "mutedBeforeSolo", "0" ) ).toBool(); - if( element.hasAttribute( "color" ) ) + if (element.hasAttribute("color")) { - QColor newColor = QColor(element.attribute("color")); - setColor(newColor); - } - else - { - resetColor(); + setColor(QColor{element.attribute("color")}); } if( m_simpleSerializingMode ) @@ -633,19 +627,12 @@ void Track::toggleSolo() } } -void Track::setColor(const QColor& c) +void Track::setColor(const std::optional& color) { - m_color = c; + m_color = color; emit colorChanged(); } -void Track::resetColor() -{ - m_color = std::nullopt; - emit colorChanged(); -} - - BoolModel *Track::getMutedModel() { return &m_mutedModel; diff --git a/src/gui/MixerLine.cpp b/src/gui/MixerLine.cpp index bd0de8301..182e131d3 100644 --- a/src/gui/MixerLine.cpp +++ b/src/gui/MixerLine.cpp @@ -163,9 +163,9 @@ void MixerLine::drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isA int width = mixerLine->rect().width(); int height = mixerLine->rect().height(); - if (channel->m_color.has_value() && !muted) + if (channel->color().has_value() && !muted) { - p->fillRect(mixerLine->rect(), channel->m_color->darker(isActive ? 120 : 150)); + p->fillRect(mixerLine->rect(), channel->color()->darker(isActive ? 120 : 150)); } else { @@ -415,36 +415,34 @@ void MixerLine::setStrokeInnerInactive( const QColor & c ) m_strokeInnerInactive = c; } - // Ask user for a color, and set it as the mixer line color void MixerLine::selectColor() { - auto channel = Engine::mixer()->mixerChannel( m_channelIndex ); - auto new_color = ColorChooser(this).withPalette(ColorChooser::Palette::Mixer)->getColor(channel->m_color.value_or(backgroundActive().color())); - if(!new_color.isValid()) { return; } - channel->setColor (new_color); + const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + const auto newColor = ColorChooser{this} + .withPalette(ColorChooser::Palette::Mixer) + ->getColor(channel->color().value_or(backgroundActive().color())); + if (!newColor.isValid()) { return; } + channel->setColor(newColor); Engine::getSong()->setModified(); update(); } - // Disable the usage of color on this mixer line void MixerLine::resetColor() { - Engine::mixer()->mixerChannel(m_channelIndex)->m_color = std::nullopt; + Engine::mixer()->mixerChannel(m_channelIndex)->setColor(std::nullopt); Engine::getSong()->setModified(); update(); } - // Pick a random color from the mixer palette and set it as our color void MixerLine::randomizeColor() { - auto channel = Engine::mixer()->mixerChannel( m_channelIndex ); - channel->setColor (ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]); + const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + channel->setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[std::rand() % 48]); Engine::getSong()->setModified(); update(); } - } // namespace lmms::gui diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index de7690d26..b2ad5c99c 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -136,7 +136,7 @@ ClipView::ClipView( Clip * clip, connect(m_trackView->getTrack(), &Track::colorChanged, this, [this] { // redraw if clip uses track color - if (!m_clip->usesCustomClipColor()) { update(); } + if (!m_clip->color().has_value()) { update(); } }); m_trackView->getTrackContentWidget()->addClipView( this ); @@ -340,45 +340,35 @@ void ClipView::updatePosition() m_trackView->trackContainerView()->update(); } - - - void ClipView::selectColor() { // Get a color from the user - QColor new_color = ColorChooser( this ).withPalette( ColorChooser::Palette::Track )->getColor( m_clip->color() ); - if (new_color.isValid()) { setColor(&new_color); } + const auto newColor = ColorChooser{this} + .withPalette(ColorChooser::Palette::Track) + ->getColor(m_clip->color().value_or(palette().background().color())); + if (newColor.isValid()) { setColor(newColor); } } - - - void ClipView::randomizeColor() { - setColor(&ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]); + setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[std::rand() % 48]); } - - - void ClipView::resetColor() { - setColor(nullptr); + setColor(std::nullopt); } - - - /*! \brief Change color of all selected clips * - * \param color The new QColor. Pass nullptr to use the Track's color. + * \param color The new color. */ -void ClipView::setColor(const QColor* color) +void ClipView::setColor(const std::optional& color) { std::set journaledTracks; auto selectedClips = getClickedClips(); - for (auto clipv: selectedClips) + for (auto clipv : selectedClips) { auto clip = clipv->getClip(); auto track = clip->getTrack(); @@ -397,25 +387,13 @@ void ClipView::setColor(const QColor* color) track->addJournalCheckPoint(); } - if (color) - { - clip->useCustomClipColor(true); - clip->setColor(*color); - } - else - { - clip->useCustomClipColor(false); - } + clip->setColor(color); clipv->update(); } Engine::getSong()->setModified(); } - - - - /*! \brief Change the ClipView's display when something * being dragged enters it. * @@ -1483,11 +1461,7 @@ TimePos ClipView::quantizeSplitPos( TimePos midiPos, bool shiftMode ) QColor ClipView::getColorForDisplay( QColor defaultColor ) { // Get the pure Clip color - auto clipColor = m_clip->hasColor() - ? m_clip->usesCustomClipColor() - ? m_clip->color() - : m_clip->getTrack()->color() - : defaultColor; + auto clipColor = m_clip->color().value_or(m_clip->getTrack()->color().value_or(defaultColor)); // Set variables QColor c, mutedCustomColor; @@ -1498,7 +1472,7 @@ QColor ClipView::getColorForDisplay( QColor defaultColor ) // Change the pure color by state: selected, muted, colored, normal if( isSelected() ) { - c = m_clip->hasColor() + c = hasCustomColor() ? ( muted ? mutedCustomColor.darker( 350 ) : clipColor.darker( 150 ) ) @@ -1508,7 +1482,7 @@ QColor ClipView::getColorForDisplay( QColor defaultColor ) { if( muted ) { - c = m_clip->hasColor() + c = hasCustomColor() ? mutedCustomColor.darker( 250 ) : mutedBackgroundColor(); } @@ -1522,5 +1496,9 @@ QColor ClipView::getColorForDisplay( QColor defaultColor ) return c; } +auto ClipView::hasCustomColor() const -> bool +{ + return m_clip->color().has_value() || m_clip->getTrack()->color().has_value(); +} } // namespace lmms::gui diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index fb259dc77..669f0ae60 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -562,7 +562,7 @@ void MidiClipView::paintEvent( QPaintEvent * ) QColor noteFillColor = muted ? getMutedNoteFillColor().lighter(200) : (c.lightness() > 175 ? getNoteFillColor().darker(400) : getNoteFillColor()); QColor noteBorderColor = muted ? getMutedNoteBorderColor() - : ( m_clip->hasColor() ? c.lighter( 200 ) : getNoteBorderColor() ); + : (hasCustomColor() ? c.lighter(200) : getNoteBorderColor()); bool const drawAsLines = height() < 64; if (drawAsLines) diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index e21a7e30b..d0c089879 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -231,11 +231,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) p.fillRect( rect(), c ); } - auto clipColor = m_clip->hasColor() - ? (m_clip->usesCustomClipColor() - ? m_clip->color() - : m_clip->getTrack()->color()) - : painter.pen().brush().color(); + auto clipColor = m_clip->color().value_or(m_clip->getTrack()->color().value_or(painter.pen().brush().color())); p.setPen(clipColor); diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 87c0f0449..8087af423 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -227,7 +227,7 @@ void InstrumentTrackView::createMixerLine() auto channel = Engine::mixer()->mixerChannel(channelIndex); channel->m_name = getTrack()->name(); - if (getTrack()->useColor()) { channel->setColor (getTrack()->color()); } + channel->setColor(getTrack()->color()); assignMixerLine(channelIndex); } diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 8516eb5c2..ddb68ee99 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -228,7 +228,7 @@ void SampleTrackView::createMixerLine() auto channel = Engine::mixer()->mixerChannel(channelIndex); channel->m_name = getTrack()->name(); - if (getTrack()->useColor()) { channel->setColor (getTrack()->color()); } + channel->setColor(getTrack()->color()); assignMixerLine(channelIndex); } diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index 31edc4949..e846370e6 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -172,11 +172,11 @@ void TrackOperationsWidget::paintEvent( QPaintEvent * pe ) p.fillRect(rect(), palette().brush(QPalette::Window)); - if( m_trackView->getTrack()->useColor() && ! m_trackView->getTrack()->getMutedModel()->value() ) + if (m_trackView->getTrack()->color().has_value() && !m_trackView->getTrack()->getMutedModel()->value()) { QRect coloredRect( 0, 0, 10, m_trackView->getTrack()->getHeight() ); - - p.fillRect( coloredRect, m_trackView->getTrack()->color() ); + + p.fillRect(coloredRect, m_trackView->getTrack()->color().value()); } p.drawPixmap(2, 2, embed::getIconPixmap(m_trackView->isMovingTrack() ? "track_op_grip_c" : "track_op_grip")); @@ -265,15 +265,15 @@ void TrackOperationsWidget::removeTrack() void TrackOperationsWidget::selectTrackColor() { - QColor new_color = ColorChooser( this ).withPalette( ColorChooser::Palette::Track )-> \ - getColor( m_trackView->getTrack()->color() ); + const auto newColor = ColorChooser{this} + .withPalette(ColorChooser::Palette::Track) + ->getColor(m_trackView->getTrack()->color().value_or(Qt::white)); - if( ! new_color.isValid() ) - { return; } + if (!newColor.isValid()) { return; } - auto track = m_trackView->getTrack(); + const auto track = m_trackView->getTrack(); track->addJournalCheckPoint(); - track->setColor(new_color); + track->setColor(newColor); Engine::getSong()->setModified(); } @@ -281,7 +281,7 @@ void TrackOperationsWidget::resetTrackColor() { auto track = m_trackView->getTrack(); track->addJournalCheckPoint(); - track->resetColor(); + track->setColor(std::nullopt); Engine::getSong()->setModified(); } @@ -298,16 +298,13 @@ void TrackOperationsWidget::resetClipColors() { auto track = m_trackView->getTrack(); track->addJournalCheckPoint(); - for (auto clip: track->getClips()) + for (auto clip : track->getClips()) { - clip->useCustomClipColor(false); + clip->setColor(std::nullopt); } Engine::getSong()->setModified(); } - - - /*! \brief Update the trackOperationsWidget context menu * * For all track types, we have the Clone and Remove options. diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index ab54b9408..b5e764b17 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -360,9 +360,9 @@ void MidiClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "type", static_cast(m_clipType) ); _this.setAttribute( "name", name() ); - if( usesCustomClipColor() ) + if (const auto& c = color()) { - _this.setAttribute( "color", color().name() ); + _this.setAttribute("color", c->name()); } // as the target of copied/dragged MIDI clip is always an existing // MIDI clip, we must not store actual position, instead we store -1 @@ -394,17 +394,12 @@ void MidiClip::loadSettings( const QDomElement & _this ) m_clipType = static_cast( _this.attribute( "type" ).toInt() ); setName( _this.attribute( "name" ) ); - - if( _this.hasAttribute( "color" ) ) + + if (_this.hasAttribute("color")) { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); + setColor(QColor{_this.attribute("color")}); } - else - { - useCustomClipColor(false); - } - + if( _this.attribute( "pos" ).toInt() >= 0 ) { movePosition( _this.attribute( "pos" ).toInt() ); From ced1f18eca04a655933aa28ccffaf60a4b838e8e Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:30:40 -0800 Subject: [PATCH 046/191] Rewrite Amplifier plugin code (#6989) * rewrite amplifier plugin style * pomelo * oroblanco * grapefruit * calamlamondin --- plugins/Amplifier/Amplifier.cpp | 88 ++++++-------------- plugins/Amplifier/Amplifier.h | 16 ++-- plugins/Amplifier/AmplifierControlDialog.cpp | 59 +++++-------- plugins/Amplifier/AmplifierControlDialog.h | 16 ++-- plugins/Amplifier/AmplifierControls.cpp | 53 ++++-------- plugins/Amplifier/AmplifierControls.h | 30 ++----- 6 files changed, 87 insertions(+), 175 deletions(-) diff --git a/plugins/Amplifier/Amplifier.cpp b/plugins/Amplifier/Amplifier.cpp index 7de8fb180..ac5fdf23b 100644 --- a/plugins/Amplifier/Amplifier.cpp +++ b/plugins/Amplifier/Amplifier.cpp @@ -36,9 +36,9 @@ extern "C" Plugin::Descriptor PLUGIN_EXPORT amplifier_plugin_descriptor = { - LMMS_STRINGIFY( PLUGIN_NAME ), + LMMS_STRINGIFY(PLUGIN_NAME), "Amplifier", - QT_TRANSLATE_NOOP( "PluginBrowser", "A native amplifier plugin" ), + QT_TRANSLATE_NOOP("PluginBrowser", "A native amplifier plugin"), "Vesa Kivimäki ", 0x0100, Plugin::Type::Effect, @@ -50,99 +50,61 @@ Plugin::Descriptor PLUGIN_EXPORT amplifier_plugin_descriptor = } - -AmplifierEffect::AmplifierEffect( Model* parent, const Descriptor::SubPluginFeatures::Key* key ) : - Effect( &lifier_plugin_descriptor, parent, key ), - m_ampControls( this ) +AmplifierEffect::AmplifierEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&lifier_plugin_descriptor, parent, key), + m_ampControls(this) { } - - - - - - -bool AmplifierEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) +bool AmplifierEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) { - if( !isEnabled() || !isRunning () ) - { - return( false ); - } + if (!isEnabled() || !isRunning()) { return false ; } double outSum = 0.0; const float d = dryLevel(); const float w = wetLevel(); - const ValueBuffer * volBuf = m_ampControls.m_volumeModel.valueBuffer(); - const ValueBuffer * panBuf = m_ampControls.m_panModel.valueBuffer(); - const ValueBuffer * leftBuf = m_ampControls.m_leftModel.valueBuffer(); - const ValueBuffer * rightBuf = m_ampControls.m_rightModel.valueBuffer(); + const ValueBuffer* volumeBuf = m_ampControls.m_volumeModel.valueBuffer(); + const ValueBuffer* panBuf = m_ampControls.m_panModel.valueBuffer(); + const ValueBuffer* leftBuf = m_ampControls.m_leftModel.valueBuffer(); + const ValueBuffer* rightBuf = m_ampControls.m_rightModel.valueBuffer(); - for( fpp_t f = 0; f < frames; ++f ) + for (fpp_t f = 0; f < frames; ++f) { -// qDebug( "offset %d, value %f", f, m_ampControls.m_volumeModel.value( f ) ); + const float volume = (volumeBuf ? volumeBuf->value(f) : m_ampControls.m_volumeModel.value()) * 0.01f; + const float pan = (panBuf ? panBuf->value(f) : m_ampControls.m_panModel.value()) * 0.01f; + const float left = (leftBuf ? leftBuf->value(f) : m_ampControls.m_leftModel.value()) * 0.01f; + const float right = (rightBuf ? rightBuf->value(f) : m_ampControls.m_rightModel.value()) * 0.01f; + + const float panLeft = std::min(1.0f, 1.0f - pan); + const float panRight = std::min(1.0f, 1.0f + pan); auto s = std::array{buf[f][0], buf[f][1]}; - // vol knob - if( volBuf ) - { - s[0] *= volBuf->value( f ) * 0.01f; - s[1] *= volBuf->value( f ) * 0.01f; - } - else - { - s[0] *= m_ampControls.m_volumeModel.value() * 0.01f; - s[1] *= m_ampControls.m_volumeModel.value() * 0.01f; - } - - // convert pan values to left/right values - const float pan = panBuf - ? panBuf->value( f ) - : m_ampControls.m_panModel.value(); - const float left1 = pan <= 0 - ? 1.0 - : 1.0 - pan * 0.01f; - const float right1 = pan >= 0 - ? 1.0 - : 1.0 + pan * 0.01f; - - // second stage amplification - const float left2 = leftBuf - ? leftBuf->value( f ) - : m_ampControls.m_leftModel.value(); - const float right2 = rightBuf - ? rightBuf->value( f ) - : m_ampControls.m_rightModel.value(); - - s[0] *= left1 * left2 * 0.01; - s[1] *= right1 * right2 * 0.01; + s[0] *= volume * left * panLeft; + s[1] *= volume * right * panRight; buf[f][0] = d * buf[f][0] + w * s[0]; buf[f][1] = d * buf[f][1] + w * s[1]; outSum += buf[f][0] * buf[f][0] + buf[f][1] * buf[f][1]; } - checkGate( outSum / frames ); + checkGate(outSum / frames); return isRunning(); } - - - extern "C" { // necessary for getting instance out of shared lib -PLUGIN_EXPORT Plugin * lmms_plugin_main( Model* parent, void* data ) +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data) { - return new AmplifierEffect( parent, static_cast( data ) ); + return new AmplifierEffect(parent, static_cast(data)); } } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/Amplifier/Amplifier.h b/plugins/Amplifier/Amplifier.h index 38fd07c6f..8a39ffeb6 100644 --- a/plugins/Amplifier/Amplifier.h +++ b/plugins/Amplifier/Amplifier.h @@ -23,9 +23,8 @@ * */ - -#ifndef AMPLIFIER_H -#define AMPLIFIER_H +#ifndef LMMS_AMPLIFIER_H +#define LMMS_AMPLIFIER_H #include "Effect.h" #include "AmplifierControls.h" @@ -36,24 +35,21 @@ namespace lmms class AmplifierEffect : public Effect { public: - AmplifierEffect( Model* parent, const Descriptor::SubPluginFeatures::Key* key ); + AmplifierEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); ~AmplifierEffect() override = default; - bool processAudioBuffer( sampleFrame* buf, const fpp_t frames ) override; + bool processAudioBuffer(sampleFrame* buf, const fpp_t frames) override; EffectControls* controls() override { return &m_ampControls; } - private: AmplifierControls m_ampControls; friend class AmplifierControls; - -} ; - +}; } // namespace lmms -#endif +#endif // LMMS_AMPLIFIER_H diff --git a/plugins/Amplifier/AmplifierControlDialog.cpp b/plugins/Amplifier/AmplifierControlDialog.cpp index ed9e98f29..1fbc3729a 100644 --- a/plugins/Amplifier/AmplifierControlDialog.cpp +++ b/plugins/Amplifier/AmplifierControlDialog.cpp @@ -23,53 +23,38 @@ * */ - #include "AmplifierControlDialog.h" #include "AmplifierControls.h" #include "embed.h" #include "Knob.h" - namespace lmms::gui { - -AmplifierControlDialog::AmplifierControlDialog( AmplifierControls* controls ) : - EffectControlDialog( controls ) +AmplifierControlDialog::AmplifierControlDialog(AmplifierControls* controls) : + EffectControlDialog(controls) { - setAutoFillBackground( true ); + setAutoFillBackground(true); QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) ); - setPalette( pal ); - setFixedSize( 100, 110 ); + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(100, 110); + + auto makeKnob = [this](int x, int y, const QString& label, const QString& hintText, const QString& unit, FloatModel* model, bool isVolume) + { + Knob* newKnob = new Knob(KnobType::Bright26, this); + newKnob->move(x, y); + newKnob->setModel(model); + newKnob->setLabel(label); + newKnob->setHintText(hintText, unit); + newKnob->setVolumeKnob(isVolume); + return newKnob; + }; - auto volumeKnob = new Knob(KnobType::Bright26, this); - volumeKnob -> move( 16, 10 ); - volumeKnob -> setVolumeKnob( true ); - volumeKnob->setModel( &controls->m_volumeModel ); - volumeKnob->setLabel( tr( "VOL" ) ); - volumeKnob->setHintText( tr( "Volume:" ) , "%" ); - - auto panKnob = new Knob(KnobType::Bright26, this); - panKnob -> move( 57, 10 ); - panKnob->setModel( &controls->m_panModel ); - panKnob->setLabel( tr( "PAN" ) ); - panKnob->setHintText( tr( "Panning:" ) , "" ); - - auto leftKnob = new Knob(KnobType::Bright26, this); - leftKnob -> move( 16, 65 ); - leftKnob -> setVolumeKnob( true ); - leftKnob->setModel( &controls->m_leftModel ); - leftKnob->setLabel( tr( "LEFT" ) ); - leftKnob->setHintText( tr( "Left gain:" ) , "%" ); - - auto rightKnob = new Knob(KnobType::Bright26, this); - rightKnob -> move( 57, 65 ); - rightKnob -> setVolumeKnob( true ); - rightKnob->setModel( &controls->m_rightModel ); - rightKnob->setLabel( tr( "RIGHT" ) ); - rightKnob->setHintText( tr( "Right gain:" ) , "%" ); + makeKnob(16, 10, tr("VOL"), tr("Volume:"), "%", &controls->m_volumeModel, true); + makeKnob(57, 10, tr("PAN"), tr("Panning:"), "%", &controls->m_panModel, false); + makeKnob(16, 65, tr("LEFT"), tr("Left gain:"), "%", &controls->m_leftModel, true); + makeKnob(57, 65, tr("RIGHT"), tr("Right gain:"), "%", &controls->m_rightModel, true); } - -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/plugins/Amplifier/AmplifierControlDialog.h b/plugins/Amplifier/AmplifierControlDialog.h index ad0ed50ca..672830117 100644 --- a/plugins/Amplifier/AmplifierControlDialog.h +++ b/plugins/Amplifier/AmplifierControlDialog.h @@ -23,8 +23,8 @@ * */ -#ifndef AMPLIFIER_CONTROL_DIALOG_H -#define AMPLIFIER_CONTROL_DIALOG_H +#ifndef LMMS_GUI_AMPLIFIER_CONTROL_DIALOG_H +#define LMMS_GUI_AMPLIFIER_CONTROL_DIALOG_H #include "EffectControlDialog.h" @@ -32,23 +32,23 @@ namespace lmms { class AmplifierControls; - +class FloatModel; namespace gui { +class Knob; + class AmplifierControlDialog : public EffectControlDialog { Q_OBJECT public: - AmplifierControlDialog( AmplifierControls* controls ); + AmplifierControlDialog(AmplifierControls* controls); ~AmplifierControlDialog() override = default; - -} ; - +}; } // namespace gui } // namespace lmms -#endif +#endif // LMMS_GUI_AMPLIFIER_CONTROL_DIALOG_H diff --git a/plugins/Amplifier/AmplifierControls.cpp b/plugins/Amplifier/AmplifierControls.cpp index 307730460..72960dd3b 100644 --- a/plugins/Amplifier/AmplifierControls.cpp +++ b/plugins/Amplifier/AmplifierControls.cpp @@ -23,7 +23,6 @@ * */ - #include #include "AmplifierControls.h" @@ -32,51 +31,33 @@ namespace lmms { -AmplifierControls::AmplifierControls( AmplifierEffect* effect ) : - EffectControls( effect ), - m_effect( effect ), - m_volumeModel( 100.0f, 0.0f, 200.0f, 0.1f, this, tr( "Volume" ) ), - m_panModel( 0.0f, -100.0f, 100.0f, 0.1f, this, tr( "Panning" ) ), - m_leftModel( 100.0f, 0.0f, 200.0f, 0.1f, this, tr( "Left gain" ) ), - m_rightModel( 100.0f, 0.0f, 200.0f, 0.1f, this, tr( "Right gain" ) ) +AmplifierControls::AmplifierControls(AmplifierEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_volumeModel(100.0f, 0.0f, 200.0f, 0.1f, this, tr("Volume")), + m_panModel(0.0f, -100.0f, 100.0f, 0.1f, this, tr("Panning")), + m_leftModel(100.0f, 0.0f, 200.0f, 0.1f, this, tr("Left gain")), + m_rightModel(100.0f, 0.0f, 200.0f, 0.1f, this, tr("Right gain")) { -/* connect( &m_volumeModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) ); - connect( &m_panModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) ); - connect( &m_leftModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) ); - connect( &m_rightModel, SIGNAL( dataChanged() ), this, SLOT( changeControl() ) );*/ } - - -void AmplifierControls::changeControl() +void AmplifierControls::loadSettings(const QDomElement& parent) { -// engine::getSong()->setModified(); + m_volumeModel.loadSettings(parent, "volume"); + m_panModel.loadSettings(parent, "pan"); + m_leftModel.loadSettings(parent, "left"); + m_rightModel.loadSettings(parent, "right"); } - - -void AmplifierControls::loadSettings( const QDomElement& _this ) +void AmplifierControls::saveSettings(QDomDocument& doc, QDomElement& parent) { - m_volumeModel.loadSettings( _this, "volume" ); - m_panModel.loadSettings( _this, "pan" ); - m_leftModel.loadSettings( _this, "left" ); - m_rightModel.loadSettings( _this, "right" ); -} - - - - -void AmplifierControls::saveSettings( QDomDocument& doc, QDomElement& _this ) -{ - m_volumeModel.saveSettings( doc, _this, "volume" ); - m_panModel.saveSettings( doc, _this, "pan" ); - m_leftModel.saveSettings( doc, _this, "left" ); - m_rightModel.saveSettings( doc, _this, "right" ); + m_volumeModel.saveSettings(doc, parent, "volume"); + m_panModel.saveSettings(doc, parent, "pan"); + m_leftModel.saveSettings(doc, parent, "left"); + m_rightModel.saveSettings(doc, parent, "right"); } } // namespace lmms - - diff --git a/plugins/Amplifier/AmplifierControls.h b/plugins/Amplifier/AmplifierControls.h index 573f6f896..6b5063ddd 100644 --- a/plugins/Amplifier/AmplifierControls.h +++ b/plugins/Amplifier/AmplifierControls.h @@ -23,8 +23,8 @@ * */ -#ifndef AMPLIFIER_CONTROLS_H -#define AMPLIFIER_CONTROLS_H +#ifndef LMMS_AMPLIFIER_CONTROLS_H +#define LMMS_AMPLIFIER_CONTROLS_H #include "EffectControls.h" #include "AmplifierControlDialog.h" @@ -39,34 +39,24 @@ namespace gui class AmplifierControlDialog; } - class AmplifierControls : public EffectControls { Q_OBJECT public: - AmplifierControls( AmplifierEffect* effect ); + AmplifierControls(AmplifierEffect* effect); ~AmplifierControls() override = default; - void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; - void loadSettings( const QDomElement & _this ) override; + void saveSettings(QDomDocument& doc, QDomElement& parent) override; + void loadSettings(const QDomElement& parent) override; inline QString nodeName() const override { return "AmplifierControls"; } - - int controlCount() override - { - return 4; - } - gui::EffectControlDialog* createView() override { - return new gui::AmplifierControlDialog( this ); + return new gui::AmplifierControlDialog(this); } - - -private slots: - void changeControl(); + int controlCount() override { return 4; } private: AmplifierEffect* m_effect; @@ -77,10 +67,8 @@ private: friend class gui::AmplifierControlDialog; friend class AmplifierEffect; - -} ; - +}; } // namespace lmms -#endif +#endif // LMMS_AMPLIFIER_CONTROLS_H From 3a928d80b2b3bf5947a9a68b2b7e78d741dad9f1 Mon Sep 17 00:00:00 2001 From: saker Date: Thu, 23 Nov 2023 20:31:49 -0500 Subject: [PATCH 047/191] Enforce lazy loading for `FileBrowser` (#6996) --- include/FileBrowser.h | 5 ++--- src/gui/FileBrowser.cpp | 17 +++++++---------- src/gui/MainWindow.cpp | 8 ++++---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 00fb07d63..b0c8a5199 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -70,7 +70,7 @@ public: */ FileBrowser( const QString & directories, const QString & filter, const QString & title, const QPixmap & pm, - QWidget * parent, bool dirs_as_items = false, bool recurse = false, + QWidget * parent, bool dirs_as_items = false, const QString& userDir = "", const QString& factoryDir = ""); @@ -94,7 +94,7 @@ public: private slots: void reloadTree(); - void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); + void expandItems(const QList& expandedDirs, QTreeWidgetItem* item = nullptr); void giveFocusToFilter(); private: @@ -121,7 +121,6 @@ private: QString m_filter; //!< Filter as used in QDir::match() bool m_dirsAsItems; - bool m_recurse; void addContentCheckBox(); QCheckBox* m_showUserContent = nullptr; diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 54ddaa317..32f29988b 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -102,14 +102,13 @@ void FileBrowser::addContentCheckBox() FileBrowser::FileBrowser(const QString & directories, const QString & filter, const QString & title, const QPixmap & pm, - QWidget * parent, bool dirs_as_items, bool recurse, + QWidget * parent, bool dirs_as_items, const QString& userDir, const QString& factoryDir): SideBarWidget( title, pm, parent ), m_directories( directories ), m_filter( filter ), m_dirsAsItems( dirs_as_items ), - m_recurse( recurse ), m_userDir(userDir), m_factoryDir(factoryDir) { @@ -177,7 +176,7 @@ void FileBrowser::saveDirectoriesStates() void FileBrowser::restoreDirectoriesStates() { - expandItems(nullptr, m_savedExpandedDirs); + expandItems(m_savedExpandedDirs); } void FileBrowser::buildSearchTree() @@ -337,8 +336,10 @@ void FileBrowser::reloadTree() -void FileBrowser::expandItems(QTreeWidgetItem* item, QList expandedDirs) +void FileBrowser::expandItems(const QList& expandedDirs, QTreeWidgetItem* item) { + if (expandedDirs.isEmpty()) { return; } + int numChildren = item ? item->childCount() : m_fileBrowserTreeWidget->topLevelItemCount(); for (int i = 0; i < numChildren; ++i) { @@ -346,14 +347,10 @@ void FileBrowser::expandItems(QTreeWidgetItem* item, QList expandedDirs auto d = dynamic_cast(it); if (d) { - // Expanding is required when recursive to load in its contents, even if it's collapsed right afterward - if (m_recurse) { d->setExpanded(true); } - d->setExpanded(expandedDirs.contains(d->fullName())); - - if (m_recurse && it->childCount()) + if (it->childCount() > 0) { - expandItems(it, expandedDirs); + expandItems(expandedDirs, it); } } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 6acaa4b86..62ed84c7a 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -115,24 +115,24 @@ MainWindow::MainWindow() : "*.mmp *.mmpz *.xml *.mid *.mpt", tr( "My Projects" ), embed::getIconPixmap( "project_file" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, true, + splitter, false, confMgr->userProjectsDir(), confMgr->factoryProjectsDir())); sideBar->appendTab( new FileBrowser(confMgr->userSamplesDir() + "*" + confMgr->factorySamplesDir(), FileItem::defaultFilters(), tr("My Samples"), embed::getIconPixmap("sample_file").transformed(QTransform().rotate(90)), splitter, false, - true, confMgr->userSamplesDir(), confMgr->factorySamplesDir())); + confMgr->userSamplesDir(), confMgr->factorySamplesDir())); sideBar->appendTab( new FileBrowser( confMgr->userPresetsDir() + "*" + confMgr->factoryPresetsDir(), "*.xpf *.cs.xml *.xiz *.lv2", tr( "My Presets" ), embed::getIconPixmap( "preset_file" ).transformed( QTransform().rotate( 90 ) ), - splitter , false, true, + splitter , false, confMgr->userPresetsDir(), confMgr->factoryPresetsDir())); sideBar->appendTab(new FileBrowser(QDir::homePath(), FileItem::defaultFilters(), tr("My Home"), - embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false, false)); + embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false)); QStringList root_paths; QString title = tr( "Root directory" ); From c2811aebefbbaa33f70924c59cbd5d5e3b98db18 Mon Sep 17 00:00:00 2001 From: DanielKauss Date: Sat, 25 Nov 2023 12:16:56 +0100 Subject: [PATCH 048/191] Ghost notes for the automation editor (#6940) Show ghost notes or sample track as a visual aid in the Automation Editor. --------- Co-authored-by: IanCaio --- data/themes/classic/automation_ghost_note.png | Bin 0 -> 4467 bytes data/themes/classic/style.css | 4 + data/themes/default/automation_ghost_note.png | Bin 0 -> 4467 bytes data/themes/default/style.css | 3 + include/AutomationEditor.h | 42 ++++++- include/MidiClipView.h | 3 +- include/SampleClipView.h | 1 + src/gui/clips/MidiClipView.cpp | 39 ++++-- src/gui/clips/SampleClipView.cpp | 16 +++ src/gui/editors/AutomationEditor.cpp | 117 ++++++++++++++++-- src/gui/editors/PianoRoll.cpp | 1 + 11 files changed, 201 insertions(+), 25 deletions(-) create mode 100644 data/themes/classic/automation_ghost_note.png create mode 100644 data/themes/default/automation_ghost_note.png diff --git a/data/themes/classic/automation_ghost_note.png b/data/themes/classic/automation_ghost_note.png new file mode 100644 index 0000000000000000000000000000000000000000..d14c047d7aed311ea74bb7e725bc2b1681556b4e GIT binary patch literal 4467 zcmeHKdsGzH8J}Gca2qJ(sR2(n6K#YzJCA*jECks_R$QdA5~4OR%-mT9cXyWE0Ty#G zMn#dfrcE)8N=;5o4T(fat26~q#cGb2m}uK$Vnq$bM6D$$Hi?>8rFUkRSDSNsIH&oK z*>m^4e)o63?{~lZ?at<+!i9;N2Q>(SBs%l$OJRM#ToU5o_tE{{`LH?#N}asZnSmt0 z4n(SA5tWRAMK!LiR>dGOaBL$icf+=fk;{XsX-fTL*iMbMTVQ)`6eF)8Rca&=)~&Em zuzfGAUx!856xr+duq{6c%IAGGg4_dpBlSY3t4K@fNJ6JK!1V;Br%8e)jat$~>&-M_ zfcvXn8nZ}dT36Ct1?}4)+!kV>jby?E8@;46` zXt0V*%V)>BpL({tZtbZy;`Cs%>UDnR-|Q<-Tq=2c=i{--3$iy78D#N`1;2W3D6Q&joIZLjIOlhyg@rRn`o$L-zUdFU*{XFK=(_CRk+^4U1u-oEVz<5zx$ ztY6Z_ zc_XH2M{wV}N$$fi2E&58R4R2XrdiRaV>qz_=)%5$d|*gsb~wPWtAM1f03N|_L;ulz z2-OOl4Sn3`B3yx7P$}fE4uayH8BD%Q%O*;$&*FbxTOKw`9EpVuFv!!}gmrD0nh z#!;;TkyhEzQW!_MVi0HzI)jeD9ATlBLbEj5%pk|pOYM143fQxum68;oalEdsPFJVb zi9rueTCG-`pm2)9AOZ{3`z0of`9lxO6cG+P2(dvSAPJ&hD|0dxVvS@&Q8=$1vCkK9 zxkl;zp{NQ_4?N5Sa8gI$J|8}QMM!egLXxOK-?$>=t`7iwDF}%*K^8b_fnR!fJcWFN zMj2$6Jc2KvI0eO%=h(cGLbzb@=_fW1UVk4Bh(6pr}7J>ovWN+`b2bHC6JE?3WkvcMjHQ9Fg)ro zF6WF9k2CTA(j-#>j7u_bU1SU|YimquEr0d-^P6Tp6HE45G7IeAJB}vq>3iTD{=>lVi8b@* z-%7rga0Su#-*-lJ;CAN+JJhp|rx5BjO@B`f7d95Iefm$!VlK|uufEg!VagTZ`MzC^ zP15=4KOuL&mi$TfnKv&qn_ZurDLqlPsppZcH&tIe^GJOE=Sv>y_@L*W&%fHRdT>YJ zg^R~B?!?CRB;Q;#v@gS?EA_WL6>RQ&Y;Uuv{1;oVhO6;;zZ*Dpcj$W0E3GXnmM=T^ zoq{SyYES!)5AVPE-u0^82X{5O(WTA3IbY$X8@A3L9QyijT*>v$_qUo4Tv$~9Y)@9~ k&iTW0I=sCr)Sz;3CfYZ2w*Tk9mQ_WZjzat11?B7h4YN`c=>Px# literal 0 HcmV?d00001 diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 9b50851a3..505ab3483 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -33,6 +33,10 @@ lmms--gui--AutomationEditor { qproperty-scaleColor: qlineargradient(spread:reflect, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 #333, stop:1 #202020); + + qproperty-ghostNoteColor: rgba(248, 248, 255, 125); + qproperty-detuningNoteColor: rgba(248, 11, 11, 125); + qproperty-ghostSampleColor: rgba(125, 125, 125, 125); } /* text box */ diff --git a/data/themes/default/automation_ghost_note.png b/data/themes/default/automation_ghost_note.png new file mode 100644 index 0000000000000000000000000000000000000000..d14c047d7aed311ea74bb7e725bc2b1681556b4e GIT binary patch literal 4467 zcmeHKdsGzH8J}Gca2qJ(sR2(n6K#YzJCA*jECks_R$QdA5~4OR%-mT9cXyWE0Ty#G zMn#dfrcE)8N=;5o4T(fat26~q#cGb2m}uK$Vnq$bM6D$$Hi?>8rFUkRSDSNsIH&oK z*>m^4e)o63?{~lZ?at<+!i9;N2Q>(SBs%l$OJRM#ToU5o_tE{{`LH?#N}asZnSmt0 z4n(SA5tWRAMK!LiR>dGOaBL$icf+=fk;{XsX-fTL*iMbMTVQ)`6eF)8Rca&=)~&Em zuzfGAUx!856xr+duq{6c%IAGGg4_dpBlSY3t4K@fNJ6JK!1V;Br%8e)jat$~>&-M_ zfcvXn8nZ}dT36Ct1?}4)+!kV>jby?E8@;46` zXt0V*%V)>BpL({tZtbZy;`Cs%>UDnR-|Q<-Tq=2c=i{--3$iy78D#N`1;2W3D6Q&joIZLjIOlhyg@rRn`o$L-zUdFU*{XFK=(_CRk+^4U1u-oEVz<5zx$ ztY6Z_ zc_XH2M{wV}N$$fi2E&58R4R2XrdiRaV>qz_=)%5$d|*gsb~wPWtAM1f03N|_L;ulz z2-OOl4Sn3`B3yx7P$}fE4uayH8BD%Q%O*;$&*FbxTOKw`9EpVuFv!!}gmrD0nh z#!;;TkyhEzQW!_MVi0HzI)jeD9ATlBLbEj5%pk|pOYM143fQxum68;oalEdsPFJVb zi9rueTCG-`pm2)9AOZ{3`z0of`9lxO6cG+P2(dvSAPJ&hD|0dxVvS@&Q8=$1vCkK9 zxkl;zp{NQ_4?N5Sa8gI$J|8}QMM!egLXxOK-?$>=t`7iwDF}%*K^8b_fnR!fJcWFN zMj2$6Jc2KvI0eO%=h(cGLbzb@=_fW1UVk4Bh(6pr}7J>ovWN+`b2bHC6JE?3WkvcMjHQ9Fg)ro zF6WF9k2CTA(j-#>j7u_bU1SU|YimquEr0d-^P6Tp6HE45G7IeAJB}vq>3iTD{=>lVi8b@* z-%7rga0Su#-*-lJ;CAN+JJhp|rx5BjO@B`f7d95Iefm$!VlK|uufEg!VagTZ`MzC^ zP15=4KOuL&mi$TfnKv&qn_ZurDLqlPsppZcH&tIe^GJOE=Sv>y_@L*W&%fHRdT>YJ zg^R~B?!?CRB;Q;#v@gS?EA_WL6>RQ&Y;Uuv{1;oVhO6;;zZ*Dpcj$W0E3GXnmM=T^ zoq{SyYES!)5AVPE-u0^82X{5O(WTA3IbY$X8@A3L9QyijT*>v$_qUo4Tv$~9Y)@9~ k&iTW0I=sCr)Sz;3CfYZ2w*Tk9mQ_WZjzat11?B7h4YN`c=>Px# literal 0 HcmV?d00001 diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 172a67d8e..e05d52653 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -66,6 +66,9 @@ lmms--gui--AutomationEditor { qproperty-graphColor: rgba(69,42,153,180); qproperty-scaleColor: #262b30; + qproperty-ghostNoteColor: rgba(248, 248, 255, 125); + qproperty-detuningNoteColor: rgba(248, 11, 11, 125); + qproperty-ghostSampleColor: rgba(125, 125, 125, 125); } /* text box */ diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index da6bdcaaf..1110e8e4c 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -26,16 +26,18 @@ #ifndef LMMS_GUI_AUTOMATION_EDITOR_H #define LMMS_GUI_AUTOMATION_EDITOR_H +#include #include #include -#include "Editor.h" - -#include "lmms_basics.h" -#include "JournallingObject.h" -#include "TimePos.h" #include "AutomationClip.h" #include "ComboBoxModel.h" +#include "Editor.h" +#include "JournallingObject.h" +#include "MidiClip.h" +#include "SampleClip.h" +#include "TimePos.h" +#include "lmms_basics.h" class QPainter; class QPixmap; @@ -68,8 +70,13 @@ class AutomationEditor : public QWidget, public JournallingObject Q_PROPERTY(QBrush graphColor MEMBER m_graphColor) Q_PROPERTY(QColor crossColor MEMBER m_crossColor) Q_PROPERTY(QColor backgroundShade MEMBER m_backgroundShade) + Q_PROPERTY(QColor ghostNoteColor MEMBER m_ghostNoteColor) + Q_PROPERTY(QColor detuningNoteColor MEMBER m_detuningNoteColor) + Q_PROPERTY(QColor ghostSampleColor MEMBER m_ghostSampleColor) public: void setCurrentClip(AutomationClip * new_clip); + void setGhostMidiClip(MidiClip* newMidiClip); + void setGhostSample(SampleClip* newSample); inline const AutomationClip * currentClip() const { @@ -159,6 +166,13 @@ protected slots: /// Updates the clip's quantization using the current user selected value. void setQuantization(); + void resetGhostNotes() + { + m_ghostNotes = nullptr; + m_ghostSample = nullptr; + update(); + } + private: enum class Action @@ -183,6 +197,12 @@ private: static const int VALUES_WIDTH = 64; + static const int NOTE_HEIGHT = 10; // height of individual notes + static const int NOTE_MARGIN = 40; // total border margin for notes + static const int MIN_NOTE_RANGE = 20; // min number of keys for fixed size + static const int SAMPLE_MARGIN = 40; + static constexpr int MAX_SAMPLE_HEIGHT = 400; // constexpr for use in min + AutomationEditor(); AutomationEditor( const AutomationEditor & ); ~AutomationEditor() override; @@ -211,6 +231,10 @@ private: float m_bottomLevel; float m_topLevel; + MidiClip* m_ghostNotes = nullptr; + QPointer m_ghostSample = nullptr; // QPointer to set to nullptr on deletion + bool m_renderSample = false; + void centerTopBottomScroll(); void updateTopBottomLevels(); @@ -261,6 +285,9 @@ private: QBrush m_scaleColor; QColor m_crossColor; QColor m_backgroundShade; + QColor m_ghostNoteColor; + QColor m_detuningNoteColor; + QColor m_ghostSampleColor; friend class AutomationEditorWindow; @@ -284,6 +311,9 @@ public: ~AutomationEditorWindow() override = default; void setCurrentClip(AutomationClip* clip); + void setGhostMidiClip(MidiClip* clip) { m_editor->setGhostMidiClip(clip); }; + void setGhostSample(SampleClip* newSample) { m_editor->setGhostSample(newSample); }; + const AutomationClip* currentClip(); void dropEvent( QDropEvent * _de ) override; @@ -337,6 +367,8 @@ private: ComboBox * m_zoomingXComboBox; ComboBox * m_zoomingYComboBox; ComboBox * m_quantizeComboBox; + + QPushButton* m_resetGhostNotes; }; } // namespace gui diff --git a/include/MidiClipView.h b/include/MidiClipView.h index a32be956a..4285bf9da 100644 --- a/include/MidiClipView.h +++ b/include/MidiClipView.h @@ -71,6 +71,7 @@ public slots: protected slots: void openInPianoRoll(); void setGhostInPianoRoll(); + void setGhostInAutomationEditor(); void resetName(); void changeName(); @@ -100,7 +101,7 @@ private: QColor m_mutedNoteBorderColor; QStaticText m_staticTextName; - + bool m_legacySEPattern; } ; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index b3f53d790..4ff218fb0 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -47,6 +47,7 @@ public: public slots: void updateSample(); void reverseSample(); + void setAutomationGhost(); diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 669f0ae60..a9ee1cb40 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -25,13 +25,16 @@ #include "MidiClipView.h" + #include #include #include #include #include #include +#include +#include "AutomationEditor.h" #include "ConfigManager.h" #include "DeprecationHelper.h" #include "GuiApplication.h" @@ -85,10 +88,11 @@ void MidiClipView::update() void MidiClipView::openInPianoRoll() { - getGUI()->pianoRoll()->setCurrentMidiClip( m_clip ); - getGUI()->pianoRoll()->parentWidget()->show(); - getGUI()->pianoRoll()->show(); - getGUI()->pianoRoll()->setFocus(); + auto pRoll = getGUI()->pianoRoll(); + pRoll->setCurrentMidiClip(m_clip); + pRoll->parentWidget()->show(); + pRoll->show(); + pRoll->setFocus(); } @@ -97,14 +101,21 @@ void MidiClipView::openInPianoRoll() void MidiClipView::setGhostInPianoRoll() { - getGUI()->pianoRoll()->setGhostMidiClip( m_clip ); - getGUI()->pianoRoll()->parentWidget()->show(); - getGUI()->pianoRoll()->show(); - getGUI()->pianoRoll()->setFocus(); + auto pRoll = getGUI()->pianoRoll(); + pRoll->setGhostMidiClip(m_clip); + pRoll->parentWidget()->show(); + pRoll->show(); + pRoll->setFocus(); } - - +void MidiClipView::setGhostInAutomationEditor() +{ + auto aEditor = getGUI()->automationEditor(); + aEditor->setGhostMidiClip(m_clip); + aEditor->parentWidget()->show(); + aEditor->show(); + aEditor->setFocus(); +} void MidiClipView::resetName() { m_clip->setName(""); } @@ -192,7 +203,13 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) _cm->insertAction( _cm->actions()[1], b ); connect( b, SIGNAL(triggered(bool)), this, SLOT(setGhostInPianoRoll())); - _cm->insertSeparator( _cm->actions()[2] ); + + auto c = new QAction(embed::getIconPixmap("automation_ghost_note"), tr("Set as ghost in automation editor"), _cm); + if (m_clip->empty()) { c->setEnabled(false); } + _cm->insertAction(_cm->actions()[2], c); + connect(c, &QAction::triggered, this, &MidiClipView::setGhostInAutomationEditor); + + _cm->insertSeparator(_cm->actions()[3]); _cm->addSeparator(); _cm->addAction( embed::getIconPixmap( "edit_erase" ), diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index d0c089879..81bbd271d 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -28,6 +28,8 @@ #include #include +#include "GuiApplication.h" +#include "AutomationEditor.h" #include "embed.h" #include "PathUtil.h" #include "SampleBuffer.h" @@ -83,6 +85,12 @@ void SampleClipView::constructContextMenu(QMenu* cm) SLOT(reverseSample()) ); + cm->addAction( + embed::getIconPixmap("automation_ghost_note"), + tr("Set as ghost in automation editor"), + this, + SLOT(setAutomationGhost()) + ); } @@ -321,6 +329,14 @@ void SampleClipView::reverseSample() +void SampleClipView::setAutomationGhost() +{ + auto aEditor = gui::getGUI()->automationEditor(); + aEditor->setGhostSample(m_clip); + aEditor->parentWidget()->show(); + aEditor->show(); + aEditor->setFocus(); +} //! Split this Clip. /*! \param pos the position of the split, relative to the start of the clip */ diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index bd9566a05..c8ef19b79 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -27,8 +27,6 @@ #include "AutomationEditor.h" -#include - #include #include #include @@ -38,6 +36,9 @@ #include #include #include +#include + +#include "SampleClip.h" #ifndef __USE_XOPEN #define __USE_XOPEN @@ -46,20 +47,23 @@ #include "ActionGroup.h" #include "AutomationNode.h" #include "ComboBox.h" -#include "debug.h" #include "DeprecationHelper.h" -#include "embed.h" +#include "DetuningHelper.h" #include "Engine.h" #include "GuiApplication.h" -#include "gui_templates.h" #include "Knob.h" #include "MainWindow.h" +#include "MidiClip.h" #include "PatternStore.h" #include "PianoRoll.h" #include "ProjectJournal.h" +#include "SampleBuffer.h" #include "StringPairDrag.h" #include "TextFloat.h" #include "TimeLineWidget.h" +#include "debug.h" +#include "embed.h" +#include "gui_templates.h" namespace lmms::gui @@ -101,7 +105,8 @@ AutomationEditor::AutomationEditor() : m_nodeTangentLineColor(0, 0, 0), m_scaleColor(Qt::SolidPattern), m_crossColor(0, 0, 0), - m_backgroundShade(0, 0, 0) + m_backgroundShade(0, 0, 0), + m_ghostNoteColor(0, 0, 0) { connect( this, SIGNAL(currentClipChanged()), this, SLOT(updateAfterClipChange()), @@ -1032,8 +1037,19 @@ inline void AutomationEditor::drawAutomationTangents(QPainter& p, timeMap::itera p.drawEllipse(tx - 3, ty - 3, 6, 6); } +void AutomationEditor::setGhostMidiClip(MidiClip* newMidiClip) +{ + // Expects a pointer to a MIDI clip or nullptr. + m_ghostNotes = newMidiClip; + m_renderSample = false; +} - +void AutomationEditor::setGhostSample(SampleClip* newGhostSample) +{ + // Expects a pointer to a Sample buffer or nullptr. + m_ghostSample = newGhostSample; + m_renderSample = true; +} void AutomationEditor::paintEvent(QPaintEvent * pe ) { @@ -1219,6 +1235,81 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) p.drawLine( x, grid_bottom, x, x_line_end ); } + // draw ghost sample + if (m_ghostSample != nullptr && m_ghostSample->sampleBuffer()->frames() > 1 && m_renderSample) + { + int sampleFrames = m_ghostSample->sampleBuffer()->frames(); + int length = static_cast(sampleFrames) / Engine::framesPerTick(); + int editorHeight = grid_bottom - TOP_MARGIN; + + int startPos = xCoordOfTick(0); + int sampleWidth = xCoordOfTick(length) - startPos; + int sampleHeight = std::min(editorHeight - SAMPLE_MARGIN, MAX_SAMPLE_HEIGHT); + int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN; + + p.setPen(m_ghostSampleColor); + m_ghostSample->sampleBuffer()->visualize(p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); + } + + // draw ghost notes + if (m_ghostNotes != nullptr && !m_renderSample) + { + const NoteVector& notes = m_ghostNotes->notes(); + int minKey = 128; + int maxKey = 0; + int detuningOffset = 0; + const Note* detuningNote = nullptr; + + for (const Note* note : notes) + { + int noteKey = note->key(); + + if (note->detuning()->automationClip() == m_clip) { + detuningOffset = note->pos(); + detuningNote = note; + } + + maxKey = std::max(maxKey, noteKey); + minKey = std::min(minKey, noteKey); + } + + for (const Note* note : notes) + { + int lenTicks = note->length(); + int notePos = note->pos(); + + // offset note if detuning + if (notePos+lenTicks < detuningOffset) { continue; } + notePos -= detuningOffset; + + // remove/change after #5902 + if (lenTicks == 0) { continue; } + else if (lenTicks < 0) { lenTicks = 4; } + + int note_width = lenTicks * m_ppb / TimePos::ticksPerBar(); + int keyRange = maxKey - minKey; + + if (keyRange < MIN_NOTE_RANGE) + { + int padding = (MIN_NOTE_RANGE - keyRange) / 2.0f; + maxKey += padding; + minKey -= padding; + keyRange = MIN_NOTE_RANGE; + } + + float absNoteHeight = static_cast(note->key() - minKey) / (maxKey - minKey); + int graphHeight = grid_bottom - NOTE_HEIGHT - NOTE_MARGIN - TOP_MARGIN; + const int y = (graphHeight - graphHeight * absNoteHeight) + NOTE_HEIGHT / 2.0f + TOP_MARGIN; + const int x = xCoordOfTick(notePos); + + if (note == detuningNote) { + p.fillRect(x, y, note_width, NOTE_HEIGHT, m_detuningNoteColor); + } else { + p.fillRect(x, y, note_width, NOTE_HEIGHT, m_ghostNoteColor); + } + } + } + // and finally bars for( tick = m_currentPosition - m_currentPosition % TimePos::ticksPerBar(), x = xCoordOfTick( tick ); @@ -2117,8 +2208,18 @@ AutomationEditorWindow::AutomationEditorWindow() : quantizationActionsToolBar->addWidget( quantize_lbl ); quantizationActionsToolBar->addWidget( m_quantizeComboBox ); + m_resetGhostNotes = new QPushButton(m_toolBar); + m_resetGhostNotes->setIcon(embed::getIconPixmap("clear_ghost_note")); + m_resetGhostNotes->setToolTip(tr("Clear ghost notes")); + m_resetGhostNotes->setEnabled(true); + + connect(m_resetGhostNotes, &QPushButton::pressed, m_editor, &AutomationEditor::resetGhostNotes); + + quantizationActionsToolBar->addSeparator(); + quantizationActionsToolBar->addWidget(m_resetGhostNotes); + // Setup our actual window - setFocusPolicy( Qt::StrongFocus ); + setFocusPolicy(Qt::StrongFocus); setFocus(); setWindowIcon( embed::getIconPixmap( "automation" ) ); setAcceptDrops( true ); diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 5ec8c323c..67fb940aa 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -1635,6 +1635,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) } detuningClip = n->detuning()->automationClip(); connect(detuningClip.data(), SIGNAL(dataChanged()), this, SLOT(update())); + getGUI()->automationEditor()->setGhostMidiClip(m_midiClip); getGUI()->automationEditor()->open(detuningClip); return; } From 67ce1677754de567834417a700fa127c47930e99 Mon Sep 17 00:00:00 2001 From: szeli1 <143485814+szeli1@users.noreply.github.com> Date: Sat, 25 Nov 2023 23:24:05 +0100 Subject: [PATCH 049/191] Fix AudioFileProcessor reverse bug (#6958) --- .../AudioFileProcessor/AudioFileProcessor.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index fbf054748..459ff566c 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -1169,14 +1169,19 @@ void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) return; } const double v = static_cast( _frames ) / m_sampleBuffer.frames(); - if( m_startKnob ) { - m_startKnob->slideBy( v, false ); + // update knobs in the right order + // to avoid them clamping each other + if (v < 0) + { + m_startKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_endKnob->slideBy(v, false); } - if( m_endKnob ) { - m_endKnob->slideBy( v, false ); - } - if( m_loopKnob ) { - m_loopKnob->slideBy( v, false ); + else + { + m_endKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_startKnob->slideBy(v, false); } } From 8136b7002c74f5dc33450d64a1ba359fb013917c Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 10 Dec 2023 11:42:02 +0100 Subject: [PATCH 050/191] Fix floating point exception in Spectrum Analyzer (#7018) Fix a floating point exception in the Spectrum Analyzer which occurs if the amplitude is 0. The amplitude is used in the calculation of a logarithm which is not defined for values smaller or equal to 0. The fix is to replace a value of 0 with the minimum positive float value. Negative values are not handled because it seems that only values greater or equal to 0 are fed into the method. This assumption is asserted in case this ever changes. --- plugins/SpectrumAnalyzer/SaProcessor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index a79d52bdc..38e8e9e92 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -41,6 +41,9 @@ #include "LocklessRingBuffer.h" #include "SaControls.h" +#include +#include + namespace lmms { @@ -650,7 +653,8 @@ float SaProcessor::ampToYPixel(float amplitude, unsigned int height) const if (m_controls->m_logYModel.value()) { // logarithmic scale: convert linear amplitude to dB (relative to 1.0) - float amplitude_dB = 10 * log10(amplitude); + assert (amplitude >= 0); + float amplitude_dB = 10 * std::log10(std::max(amplitude, std::numeric_limits::min())); if (amplitude_dB < getAmpRangeMin()) { return height; From f3d3a1421e2f4eb7d8dd8b35fdef1e8df2cd2cf8 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Sat, 16 Dec 2023 14:19:36 +0000 Subject: [PATCH 051/191] Split `TimeLineWidget` into core and GUI parts (#7004) --- include/MainWindow.h | 1 - include/NStateButton.h | 15 +-- include/Song.h | 18 ++-- include/TimeLineWidget.h | 103 +++---------------- include/Timeline.h | 82 +++++++++++++++ src/core/CMakeLists.txt | 1 + src/core/SampleClip.cpp | 8 +- src/core/Song.cpp | 122 ++++++++++------------ src/core/Timeline.cpp | 83 +++++++++++++++ src/gui/MainWindow.cpp | 38 ------- src/gui/editors/AutomationEditor.cpp | 19 ++-- src/gui/editors/PianoRoll.cpp | 25 ++--- src/gui/editors/SongEditor.cpp | 21 ++-- src/gui/editors/TimeLineWidget.cpp | 147 ++++++++------------------- src/gui/widgets/NStateButton.cpp | 31 +++--- 15 files changed, 334 insertions(+), 380 deletions(-) create mode 100644 include/Timeline.h create mode 100644 src/core/Timeline.cpp diff --git a/include/MainWindow.h b/include/MainWindow.h index 30d52ec3a..4442a7ac2 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -248,7 +248,6 @@ private slots: void onExportProject(); void onExportProjectTracks(); void onImportProject(); - void onSongStopped(); void onSongModified(); void onProjectFileNameChanged(); diff --git a/include/NStateButton.h b/include/NStateButton.h index a6b4c4d9a..c948fa843 100644 --- a/include/NStateButton.h +++ b/include/NStateButton.h @@ -55,25 +55,20 @@ public: public slots: - void changeState( int _n ); - + void changeState(int state); signals: - void changedState( int _n ); - + void changedState(int state); protected: - void mousePressEvent( QMouseEvent * _me ) override; - + void mousePressEvent(QMouseEvent* me) override; private: - QVector > m_states; + QVector> m_states; QString m_generalToolTip; int m_curState; - -} ; - +}; } // namespace lmms::gui diff --git a/include/Song.h b/include/Song.h index 02714d8ac..2897b2131 100644 --- a/include/Song.h +++ b/include/Song.h @@ -25,16 +25,18 @@ #ifndef LMMS_SONG_H #define LMMS_SONG_H +#include #include #include #include -#include "TrackContainer.h" #include "AudioEngine.h" #include "Controller.h" #include "lmms_constants.h" #include "MeterModel.h" +#include "Timeline.h" +#include "TrackContainer.h" #include "VstSyncController.h" namespace lmms @@ -105,7 +107,6 @@ public: public: PlayPos( const int abs = 0 ) : TimePos( abs ), - m_timeLine( nullptr ), m_currentFrame( 0.0f ) { } @@ -125,13 +126,11 @@ public: { return m_jumped; } - gui::TimeLineWidget * m_timeLine; private: float m_currentFrame; bool m_jumped; - - } ; + }; void processNextBuffer(); @@ -274,6 +273,11 @@ public: return getPlayPos(m_playMode); } + auto getTimeline(PlayMode mode) -> Timeline& { return m_timelines[static_cast(mode)]; } + auto getTimeline(PlayMode mode) const -> const Timeline& { return m_timelines[static_cast(mode)]; } + auto getTimeline() -> Timeline& { return getTimeline(m_playMode); } + auto getTimeline() const -> const Timeline& { return getTimeline(m_playMode); } + void updateLength(); bar_t length() const { @@ -402,7 +406,7 @@ private slots: void masterVolumeChanged(); - void savePos(); + void savePlayStartPosition(); void updateFramesPerTick(); @@ -481,6 +485,8 @@ private: QHash m_errors; + std::array m_timelines; + PlayMode m_playMode; PlayPos m_playPos[PlayModeCount]; bar_t m_length; diff --git a/include/TimeLineWidget.h b/include/TimeLineWidget.h index 2be73b77c..c87458e6c 100644 --- a/include/TimeLineWidget.h +++ b/include/TimeLineWidget.h @@ -34,6 +34,12 @@ class QPixmap; class QToolBar; +namespace lmms { + +class Timeline; + +} // namespace lmms + namespace lmms::gui { @@ -42,7 +48,7 @@ class TextFloat; class SongEditor; -class TimeLineWidget : public QWidget, public JournallingObject +class TimeLineWidget : public QWidget { Q_OBJECT public: @@ -60,24 +66,10 @@ public: { Enabled, Disabled - } ; + }; - enum class LoopPointState - { - Disabled, - Enabled - } ; - - enum class BehaviourAtStopState - { - BackToZero, - BackToStart, - KeepStopPosition - } ; - - - TimeLineWidget(int xoff, int yoff, float ppb, Song::PlayPos & pos, - const TimePos & begin, Song::PlayMode mode, QWidget * parent); + TimeLineWidget(int xoff, int yoff, float ppb, Song::PlayPos& pos, Timeline& timeline, + const TimePos& begin, Song::PlayMode mode, QWidget* parent); ~TimeLineWidget() override; inline QColor const & getBarLineColor() const { return m_barLineColor; } @@ -117,42 +109,6 @@ public: return m_autoScroll; } - BehaviourAtStopState behaviourAtStop() const - { - return m_behaviourAtStop; - } - - void setBehaviourAtStop (int state) - { - emit loadBehaviourAtStop (state); - } - - bool loopPointsEnabled() const - { - return m_loopPoints == LoopPointState::Enabled; - } - - inline const TimePos & loopBegin() const - { - return ( m_loopPos[0] < m_loopPos[1] ) ? - m_loopPos[0] : m_loopPos[1]; - } - - inline const TimePos & loopEnd() const - { - return ( m_loopPos[0] > m_loopPos[1] ) ? - m_loopPos[0] : m_loopPos[1]; - } - - inline void savePos( const TimePos & pos ) - { - m_savedPos = pos; - } - inline const TimePos & savedPos() const - { - return m_savedPos; - } - inline void setPixelsPerBar( float ppb ) { m_ppb = ppb; @@ -163,14 +119,6 @@ public: void addToolButtons(QToolBar* _tool_bar ); - - void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; - void loadSettings( const QDomElement & _this ) override; - inline QString nodeName() const override - { - return "timeline"; - } - inline int markerX( const TimePos & _t ) const { return m_xOffset + static_cast( ( _t - m_begin ) * @@ -178,25 +126,17 @@ public: } signals: - + void positionChanged(const lmms::TimePos& postion); void regionSelectedFromPixels( int, int ); void selectionFinished(); - public slots: - void updatePosition( const lmms::TimePos & ); - void updatePosition() - { - updatePosition( TimePos() ); - } + void updatePosition(); void setSnapSize( const float snapSize ) { m_snapSize = snapSize; } void toggleAutoScroll( int _n ); - void toggleLoopPoints( int _n ); - void toggleBehaviourAtStop( int _n ); - protected: void paintEvent( QPaintEvent * _pe ) override; @@ -222,8 +162,6 @@ private: QColor m_barNumberColor; AutoScrollState m_autoScroll; - LoopPointState m_loopPoints; - BehaviourAtStopState m_behaviourAtStop; bool m_changedPosition; @@ -232,12 +170,9 @@ private: float m_ppb; float m_snapSize; Song::PlayPos & m_pos; + Timeline* m_timeline; const TimePos & m_begin; const Song::PlayMode m_mode; - TimePos m_loopPos[2]; - - TimePos m_savedPos; - TextFloat * m_hint; int m_initalXSelect; @@ -253,17 +188,7 @@ private: } m_action; int m_moveXOff; - - -signals: - void positionChanged( const lmms::TimePos & _t ); - void loopPointStateLoaded( int _n ); - void positionMarkerMoved(); - void loadBehaviourAtStop( int _n ); - -} ; - - +}; } // namespace lmms::gui diff --git a/include/Timeline.h b/include/Timeline.h new file mode 100644 index 000000000..dc2d293c4 --- /dev/null +++ b/include/Timeline.h @@ -0,0 +1,82 @@ +/* + * Timeline.h + * + * Copyright (c) 2023 Dominic Clark + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +#ifndef LMMS_TIMELINE_H +#define LMMS_TIMELINE_H + +#include + +#include "JournallingObject.h" +#include "TimePos.h" + +namespace lmms { + +class Timeline : public QObject, public JournallingObject +{ + Q_OBJECT + +public: + enum class StopBehaviour + { + BackToZero, + BackToStart, + KeepPosition + }; + + auto loopBegin() const -> TimePos { return m_loopBegin; } + auto loopEnd() const -> TimePos { return m_loopEnd; } + auto loopEnabled() const -> bool { return m_loopEnabled; } + + void setLoopBegin(TimePos begin); + void setLoopEnd(TimePos end); + void setLoopPoints(TimePos begin, TimePos end); + void setLoopEnabled(bool enabled); + + auto playStartPosition() const -> TimePos { return m_playStartPosition; } + auto stopBehaviour() const -> StopBehaviour { return m_stopBehaviour; } + + void setPlayStartPosition(TimePos position) { m_playStartPosition = position; } + void setStopBehaviour(StopBehaviour behaviour); + + auto nodeName() const -> QString override { return "timeline"; } + +signals: + void loopEnabledChanged(bool enabled); + void stopBehaviourChanged(lmms::Timeline::StopBehaviour behaviour); + +protected: + void saveSettings(QDomDocument& doc, QDomElement& element) override; + void loadSettings(const QDomElement& element) override; + +private: + TimePos m_loopBegin = TimePos{0}; + TimePos m_loopEnd = TimePos{DefaultTicksPerBar}; + bool m_loopEnabled = false; + + StopBehaviour m_stopBehaviour = StopBehaviour::BackToStart; + TimePos m_playStartPosition = TimePos{-1}; +}; + +} // namespace lmms + +#endif // LMMS_TIMELINE_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1155f5e0d..c2dc0bf78 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -74,6 +74,7 @@ set(LMMS_SRCS core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp + core/Timeline.cpp core/TimePos.cpp core/ToolPlugin.cpp core/Track.cpp diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 2febaee2e..ba8420a41 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -52,16 +52,10 @@ SampleClip::SampleClip( Track * _track ) : connect( Engine::getSong(), SIGNAL(timeSignatureChanged(int,int)), this, SLOT(updateLength())); - //care about positionmarker - gui::TimeLineWidget* timeLine = Engine::getSong()->getPlayPos( Song::PlayMode::Song ).m_timeLine; - if( timeLine ) - { - connect( timeLine, SIGNAL(positionMarkerMoved()), this, SLOT(playbackPositionChanged())); - } //playbutton clicked or space key / on Export Song set isPlaying to false connect( Engine::getSong(), SIGNAL(playbackStateChanged()), this, SLOT(playbackPositionChanged()), Qt::DirectConnection ); - //care about loops + //care about loops and jumps connect( Engine::getSong(), SIGNAL(updateSampleTracks()), this, SLOT(playbackPositionChanged()), Qt::DirectConnection ); //care about mute Clips diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 3a735331c..ddc55707c 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -184,14 +184,9 @@ void Song::setTimeSignature() -void Song::savePos() +void Song::savePlayStartPosition() { - gui::TimeLineWidget* tl = getPlayPos().m_timeLine; - - if( tl != nullptr ) - { - tl->savePos( getPlayPos() ); - } + getTimeline().setPlayStartPosition(getPlayPos()); } @@ -258,16 +253,17 @@ void Song::processNextBuffer() return false; }; - const auto timeline = getPlayPos().m_timeLine; - const auto loopEnabled = !m_exporting && timeline && timeline->loopPointsEnabled(); + const auto& timeline = getTimeline(); + const auto loopEnabled = !m_exporting && timeline.loopEnabled(); // Ensure playback begins within the loop if it is enabled - if (loopEnabled) { enforceLoop(timeline->loopBegin(), timeline->loopEnd()); } + if (loopEnabled) { enforceLoop(timeline.loopBegin(), timeline.loopEnd()); } - // Inform VST plugins if the user moved the play head + // Inform VST plugins and sample tracks if the user moved the play head if (getPlayPos().jumped()) { m_vstSyncController.setPlaybackJumped(true); + emit updateSampleTracks(); getPlayPos().setJumped(false); } @@ -301,13 +297,13 @@ void Song::processNextBuffer() } // Handle loop points, and inform VST plugins of the loop status - if (loopEnabled || (m_loopRenderRemaining > 1 && getPlayPos() >= timeline->loopBegin())) + if (loopEnabled || (m_loopRenderRemaining > 1 && getPlayPos() >= timeline.loopBegin())) { m_vstSyncController.startCycle( - timeline->loopBegin().getTicks(), timeline->loopEnd().getTicks()); + timeline.loopBegin().getTicks(), timeline.loopEnd().getTicks()); // Loop if necessary, and decrement the remaining loops if we did - if (enforceLoop(timeline->loopBegin(), timeline->loopEnd()) + if (enforceLoop(timeline.loopBegin(), timeline.loopEnd()) && m_loopRenderRemaining > 1) { m_loopRenderRemaining--; @@ -492,7 +488,7 @@ void Song::playSong() m_vstSyncController.setPlaybackState( true ); - savePos(); + savePlayStartPosition(); emit playbackStateChanged(); } @@ -531,7 +527,7 @@ void Song::playPattern() m_vstSyncController.setPlaybackState( true ); - savePos(); + savePlayStartPosition(); emit playbackStateChanged(); } @@ -556,7 +552,7 @@ void Song::playMidiClip( const MidiClip* midiClipToPlay, bool loop ) m_paused = false; } - savePos(); + savePlayStartPosition(); emit playbackStateChanged(); } @@ -644,40 +640,32 @@ void Song::stop() // To avoid race conditions with the processing threads Engine::audioEngine()->requestChangeInModel(); - TimeLineWidget * tl = getPlayPos().m_timeLine; + auto& timeline = getTimeline(); m_paused = false; m_recording = true; - - if( tl ) - { - switch( tl->behaviourAtStop() ) - { - case TimeLineWidget::BehaviourAtStopState::BackToZero: - getPlayPos().setTicks(0); - m_elapsedMilliSeconds[static_cast(m_playMode)] = 0; - break; - - case TimeLineWidget::BehaviourAtStopState::BackToStart: - if( tl->savedPos() >= 0 ) - { - getPlayPos().setTicks(tl->savedPos().getTicks()); - setToTime(tl->savedPos()); - - tl->savePos( -1 ); - } - break; - - case TimeLineWidget::BehaviourAtStopState::KeepStopPosition: - break; - } - } - else - { - getPlayPos().setTicks( 0 ); - m_elapsedMilliSeconds[static_cast(m_playMode)] = 0; - } m_playing = false; + switch (timeline.stopBehaviour()) + { + case Timeline::StopBehaviour::BackToZero: + getPlayPos().setTicks(0); + m_elapsedMilliSeconds[static_cast(m_playMode)] = 0; + break; + + case Timeline::StopBehaviour::BackToStart: + if (timeline.playStartPosition() >= 0) + { + getPlayPos().setTicks(timeline.playStartPosition().getTicks()); + setToTime(timeline.playStartPosition()); + + timeline.setPlayStartPosition(-1); + } + break; + + case Timeline::StopBehaviour::KeepPosition: + break; + } + m_elapsedMilliSeconds[static_cast(PlayMode::None)] = m_elapsedMilliSeconds[static_cast(m_playMode)]; getPlayPos(PlayMode::None).setTicks(getPlayPos().getTicks()); @@ -719,37 +707,35 @@ void Song::startExport() m_exporting = true; updateLength(); + const auto& timeline = getTimeline(PlayMode::Song); + if (m_renderBetweenMarkers) { - m_exportSongBegin = m_exportLoopBegin = getPlayPos(PlayMode::Song).m_timeLine->loopBegin(); - m_exportSongEnd = m_exportLoopEnd = getPlayPos(PlayMode::Song).m_timeLine->loopEnd(); + m_exportSongBegin = m_exportLoopBegin = timeline.loopBegin(); + m_exportSongEnd = m_exportLoopEnd = timeline.loopEnd(); - getPlayPos(PlayMode::Song).setTicks( getPlayPos(PlayMode::Song).m_timeLine->loopBegin().getTicks() ); + getPlayPos(PlayMode::Song).setTicks(timeline.loopBegin().getTicks()); } else { m_exportSongEnd = TimePos(m_length, 0); // Handle potentially ridiculous loop points gracefully. - if (m_loopRenderCount > 1 && getPlayPos(PlayMode::Song).m_timeLine->loopEnd() > m_exportSongEnd) + if (m_loopRenderCount > 1 && timeline.loopEnd() > m_exportSongEnd) { - m_exportSongEnd = getPlayPos(PlayMode::Song).m_timeLine->loopEnd(); + m_exportSongEnd = timeline.loopEnd(); } if (!m_exportLoop) m_exportSongEnd += TimePos(1,0); m_exportSongBegin = TimePos(0,0); - // FIXME: remove this check once we load timeline in headless mode - if (getPlayPos(PlayMode::Song).m_timeLine) - { - m_exportLoopBegin = getPlayPos(PlayMode::Song).m_timeLine->loopBegin() < m_exportSongEnd && - getPlayPos(PlayMode::Song).m_timeLine->loopEnd() <= m_exportSongEnd ? - getPlayPos(PlayMode::Song).m_timeLine->loopBegin() : TimePos(0,0); - m_exportLoopEnd = getPlayPos(PlayMode::Song).m_timeLine->loopBegin() < m_exportSongEnd && - getPlayPos(PlayMode::Song).m_timeLine->loopEnd() <= m_exportSongEnd ? - getPlayPos(PlayMode::Song).m_timeLine->loopEnd() : TimePos(0,0); - } + m_exportLoopBegin = timeline.loopBegin() < m_exportSongEnd && timeline.loopEnd() <= m_exportSongEnd + ? timeline.loopBegin() + : TimePos{0}; + m_exportLoopEnd = timeline.loopBegin() < m_exportSongEnd && timeline.loopEnd() <= m_exportSongEnd + ? timeline.loopEnd() + : TimePos{0}; getPlayPos(PlayMode::Song).setTicks( 0 ); } @@ -1080,11 +1066,7 @@ void Song::loadProject( const QString & fileName ) m_masterVolumeModel.loadSettings( dataFile.head(), "mastervol" ); m_masterPitchModel.loadSettings( dataFile.head(), "masterpitch" ); - if( getPlayPos(PlayMode::Song).m_timeLine ) - { - // reset loop-point-state - getPlayPos(PlayMode::Song).m_timeLine->toggleLoopPoints( 0 ); - } + getTimeline(PlayMode::Song).setLoopEnabled(false); if( !dataFile.content().firstChildElement( "track" ).isNull() ) { @@ -1167,9 +1149,9 @@ void Song::loadProject( const QString & fileName ) { getGUI()->getProjectNotes()->SerializingObject::restoreState( node.toElement() ); } - else if( node.nodeName() == getPlayPos(PlayMode::Song).m_timeLine->nodeName() ) + else if (node.nodeName() == getTimeline(PlayMode::Song).nodeName()) { - getPlayPos(PlayMode::Song).m_timeLine->restoreState( node.toElement() ); + getTimeline(PlayMode::Song).restoreState(node.toElement()); } } } @@ -1253,7 +1235,7 @@ bool Song::saveProjectFile(const QString & filename, bool withResources) getGUI()->pianoRoll()->saveState( dataFile, dataFile.content() ); getGUI()->automationEditor()->m_editor->saveState( dataFile, dataFile.content() ); getGUI()->getProjectNotes()->SerializingObject::saveState( dataFile, dataFile.content() ); - getPlayPos(PlayMode::Song).m_timeLine->saveState( dataFile, dataFile.content() ); + getTimeline(PlayMode::Song).saveState(dataFile, dataFile.content()); } saveControllerStates( dataFile, dataFile.content() ); diff --git a/src/core/Timeline.cpp b/src/core/Timeline.cpp new file mode 100644 index 000000000..f6f30c21c --- /dev/null +++ b/src/core/Timeline.cpp @@ -0,0 +1,83 @@ +/* + * Timeline.cpp + * + * Copyright (c) 2023 Dominic Clark + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +#include "Timeline.h" + +#include +#include + +#include +#include + +namespace lmms { + +void Timeline::setLoopBegin(TimePos begin) +{ + std::tie(m_loopBegin, m_loopEnd) = std::minmax(begin, TimePos{m_loopEnd}); +} + +void Timeline::setLoopEnd(TimePos end) +{ + std::tie(m_loopBegin, m_loopEnd) = std::minmax(TimePos{m_loopBegin}, end); +} + +void Timeline::setLoopPoints(TimePos begin, TimePos end) +{ + std::tie(m_loopBegin, m_loopEnd) = std::minmax(begin, end); +} + +void Timeline::setLoopEnabled(bool enabled) +{ + if (enabled != m_loopEnabled) { + m_loopEnabled = enabled; + emit loopEnabledChanged(m_loopEnabled); + } +} + +void Timeline::setStopBehaviour(StopBehaviour behaviour) +{ + if (behaviour != m_stopBehaviour) { + m_stopBehaviour = behaviour; + emit stopBehaviourChanged(m_stopBehaviour); + } +} + +void Timeline::saveSettings(QDomDocument& doc, QDomElement& element) +{ + element.setAttribute("lp0pos", static_cast(loopBegin())); + element.setAttribute("lp1pos", static_cast(loopEnd())); + element.setAttribute("lpstate", static_cast(loopEnabled())); + element.setAttribute("stopbehaviour", static_cast(stopBehaviour())); +} + +void Timeline::loadSettings(const QDomElement& element) +{ + setLoopPoints( + static_cast(element.attribute("lp0pos").toInt()), + static_cast(element.attribute("lp1pos").toInt()) + ); + setLoopEnabled(static_cast(element.attribute("lpstate").toInt())); + setStopBehaviour(static_cast(element.attribute("stopbehaviour", "1").toInt())); +} + +} // namespace lmms diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 62ed84c7a..0413e3bd6 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -226,8 +226,6 @@ MainWindow::MainWindow() : connect( Engine::getSong(), SIGNAL(playbackStateChanged()), this, SLOT(updatePlayPauseIcons())); - connect(Engine::getSong(), SIGNAL(stopped()), SLOT(onSongStopped())); - connect(Engine::getSong(), SIGNAL(modified()), SLOT(onSongModified())); connect(Engine::getSong(), SIGNAL(projectFileNameChanged()), SLOT(onProjectFileNameChanged())); @@ -1606,42 +1604,6 @@ void MainWindow::onImportProject() } } -void MainWindow::onSongStopped() -{ - Song * song = Engine::getSong(); - Song::PlayPos const & playPos = song->getPlayPos(); - - TimeLineWidget * tl = playPos.m_timeLine; - - if( tl ) - { - SongEditorWindow* songEditor = getGUI()->songEditor(); - switch( tl->behaviourAtStop() ) - { - case TimeLineWidget::BehaviourAtStopState::BackToZero: - if( songEditor && ( tl->autoScroll() == TimeLineWidget::AutoScrollState::Enabled ) ) - { - songEditor->m_editor->updatePosition(0); - } - break; - - case TimeLineWidget::BehaviourAtStopState::BackToStart: - if( tl->savedPos() >= 0 ) - { - if(songEditor && ( tl->autoScroll() == TimeLineWidget::AutoScrollState::Enabled ) ) - { - songEditor->m_editor->updatePosition( TimePos(tl->savedPos().getTicks() ) ); - } - tl->savePos( -1 ); - } - break; - - case TimeLineWidget::BehaviourAtStopState::KeepStopPosition: - break; - } - } -} - void MainWindow::onSongModified() { // Only update the window title if the code is executed from the GUI main thread. diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index c8ef19b79..e7153bfa3 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -132,13 +132,12 @@ AutomationEditor::AutomationEditor() : m_quantizeModel.setValue( m_quantizeModel.findText( "1/8" ) ); // add time-line - m_timeLine = new TimeLineWidget( VALUES_WIDTH, 0, m_ppb, - Engine::getSong()->getPlayPos( - Song::PlayMode::AutomationClip ), - m_currentPosition, - Song::PlayMode::AutomationClip, this ); - connect( this, SIGNAL( positionChanged( const lmms::TimePos& ) ), - m_timeLine, SLOT( updatePosition( const lmms::TimePos& ) ) ); + m_timeLine = new TimeLineWidget(VALUES_WIDTH, 0, m_ppb, + Engine::getSong()->getPlayPos(Song::PlayMode::AutomationClip), + Engine::getSong()->getTimeline(Song::PlayMode::AutomationClip), + m_currentPosition, Song::PlayMode::AutomationClip, this + ); + connect(this, &AutomationEditor::positionChanged, m_timeLine, &TimeLineWidget::updatePosition); connect( m_timeLine, SIGNAL( positionChanged( const lmms::TimePos& ) ), this, SLOT( updatePosition( const lmms::TimePos& ) ) ); @@ -1602,11 +1601,7 @@ void AutomationEditor::resizeEvent(QResizeEvent * re) } centerTopBottomScroll(); - if( Engine::getSong() ) - { - Engine::getSong()->getPlayPos( Song::PlayMode::AutomationClip - ).m_timeLine->setFixedWidth( width() ); - } + m_timeLine->setFixedWidth(width()); updateTopBottomLevels(); update(); diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 67fb940aa..8f2d964ff 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -265,12 +265,11 @@ PianoRoll::PianoRoll() : // add time-line m_timeLine = new TimeLineWidget(m_whiteKeyWidth, 0, m_ppb, - Engine::getSong()->getPlayPos( - Song::PlayMode::MidiClip ), - m_currentPosition, - Song::PlayMode::MidiClip, this ); - connect( this, SIGNAL( positionChanged( const lmms::TimePos& ) ), - m_timeLine, SLOT( updatePosition( const lmms::TimePos& ) ) ); + Engine::getSong()->getPlayPos(Song::PlayMode::MidiClip), + Engine::getSong()->getTimeline(Song::PlayMode::MidiClip), + m_currentPosition, Song::PlayMode::MidiClip, this + ); + connect(this, &PianoRoll::positionChanged, m_timeLine, &TimeLineWidget::updatePosition); connect( m_timeLine, SIGNAL( positionChanged( const lmms::TimePos& ) ), this, SLOT( updatePosition( const lmms::TimePos& ) ) ); @@ -282,10 +281,7 @@ PianoRoll::PianoRoll() : this, SLOT( updatePositionStepRecording( const lmms::TimePos& ) ) ); // update timeline when in record-accompany mode - connect( Engine::getSong()->getPlayPos( Song::PlayMode::Song ).m_timeLine, - SIGNAL( positionChanged( const lmms::TimePos& ) ), - this, - SLOT( updatePositionAccompany( const lmms::TimePos& ) ) ); + connect(m_timeLine, &TimeLineWidget::positionChanged, this, &PianoRoll::updatePositionAccompany); // TODO /* connect( engine::getSong()->getPlayPos( Song::PlayMode::Pattern ).m_timeLine, SIGNAL( positionChanged( const lmms::TimePos& ) ), @@ -3734,8 +3730,7 @@ void PianoRoll::resizeEvent(QResizeEvent* re) { updatePositionLineHeight(); updateScrollbars(); - Engine::getSong()->getPlayPos(Song::PlayMode::MidiClip) - .m_timeLine->setFixedWidth(width()); + m_timeLine->setFixedWidth(width()); update(); } @@ -5167,7 +5162,8 @@ void PianoRollWindow::saveSettings( QDomDocument & doc, QDomElement & de ) de.appendChild(markedSemiTonesRoot); } - de.setAttribute("stopbehaviour", static_cast(m_editor->m_timeLine->behaviourAtStop())); + de.setAttribute("stopbehaviour", static_cast( + Engine::getSong()->getTimeline(Song::PlayMode::MidiClip).stopBehaviour())); MainWindow::saveWidgetState( this, de ); } @@ -5182,7 +5178,8 @@ void PianoRollWindow::loadSettings( const QDomElement & de ) MainWindow::restoreWidgetState( this, de ); - m_editor->m_timeLine->setBehaviourAtStop(de.attribute("stopbehaviour").toInt()); + Engine::getSong()->getTimeline(Song::PlayMode::MidiClip).setStopBehaviour( + static_cast(de.attribute("stopbehaviour").toInt())); // update margins here because we're later in the startup process // We can't earlier because everything is still starting with the diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 518068759..69a41a764 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -97,14 +97,12 @@ SongEditor::SongEditor( Song * song ) : m_zoomingModel->setParent(this); m_snappingModel->setParent(this); - m_timeLine = new TimeLineWidget( m_trackHeadWidth, 32, - pixelsPerBar(), - m_song->getPlayPos(Song::PlayMode::Song), - m_currentPosition, - Song::PlayMode::Song, this ); - connect( this, SIGNAL( positionChanged( const lmms::TimePos& ) ), - m_song->getPlayPos(Song::PlayMode::Song).m_timeLine, - SLOT( updatePosition( const lmms::TimePos& ) ) ); + m_timeLine = new TimeLineWidget(m_trackHeadWidth, 32, pixelsPerBar(), + m_song->getPlayPos(Song::PlayMode::Song), + m_song->getTimeline(Song::PlayMode::Song), + m_currentPosition, Song::PlayMode::Song, this + ); + connect(this, &TrackContainerView::positionChanged, m_timeLine, &TimeLineWidget::updatePosition); connect( m_timeLine, SIGNAL( positionChanged( const lmms::TimePos& ) ), this, SLOT( updatePosition( const lmms::TimePos& ) ) ); connect( m_timeLine, SIGNAL(regionSelectedFromPixels(int,int)), @@ -560,7 +558,7 @@ void SongEditor::wheelEvent( QWheelEvent * we ) m_leftRightScroll->setValue(m_leftRightScroll->value() + bar - newBar); // update timeline - m_song->getPlayPos(Song::PlayMode::Song).m_timeLine->setPixelsPerBar(pixelsPerBar()); + m_timeLine->setPixelsPerBar(pixelsPerBar()); // and make sure, all Clip's are resized and relocated realignTracks(); } @@ -808,8 +806,7 @@ void SongEditor::updatePosition( const TimePos & t ) m_scrollBack = false; } - const int x = m_song->getPlayPos(Song::PlayMode::Song).m_timeLine-> - markerX( t ) + 8; + const int x = m_timeLine->markerX(t) + 8; if( x >= trackOpWidth + widgetWidth -1 ) { m_positionLine->show(); @@ -872,7 +869,7 @@ void SongEditor::zoomingChanged() int ppb = calculatePixelsPerBar(); setPixelsPerBar(ppb); - m_song->getPlayPos(Song::PlayMode::Song).m_timeLine->setPixelsPerBar(ppb); + m_timeLine->setPixelsPerBar(ppb); realignTracks(); updateRubberband(); m_timeLine->setSnapSize(getSnapSize()); diff --git a/src/gui/editors/TimeLineWidget.cpp b/src/gui/editors/TimeLineWidget.cpp index 049a8623f..f77361a91 100644 --- a/src/gui/editors/TimeLineWidget.cpp +++ b/src/gui/editors/TimeLineWidget.cpp @@ -45,9 +45,8 @@ namespace constexpr int MIN_BAR_LABEL_DISTANCE = 35; } -TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, - Song::PlayPos & pos, const TimePos & begin, Song::PlayMode mode, - QWidget * parent ) : +TimeLineWidget::TimeLineWidget(const int xoff, const int yoff, const float ppb, Song::PlayPos& pos, Timeline& timeline, + const TimePos& begin, Song::PlayMode mode, QWidget* parent) : QWidget( parent ), m_inactiveLoopColor( 52, 63, 53, 64 ), m_inactiveLoopBrush( QColor( 255, 255, 255, 32 ) ), @@ -59,35 +58,28 @@ TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, m_barLineColor( 192, 192, 192 ), m_barNumberColor( m_barLineColor.darker( 120 ) ), m_autoScroll( AutoScrollState::Enabled ), - m_loopPoints( LoopPointState::Disabled ), - m_behaviourAtStop( BehaviourAtStopState::BackToZero ), m_changedPosition( true ), m_xOffset( xoff ), m_posMarkerX( 0 ), m_ppb( ppb ), m_snapSize( 1.0 ), m_pos( pos ), + m_timeline{&timeline}, m_begin( begin ), m_mode( mode ), - m_savedPos( -1 ), m_hint( nullptr ), m_action( Action::NoAction ), m_moveXOff( 0 ) { - m_loopPos[0] = 0; - m_loopPos[1] = DefaultTicksPerBar; - setAttribute( Qt::WA_OpaquePaintEvent, true ); move( 0, yoff ); m_xOffset -= m_posMarkerPixmap.width() / 2; setMouseTracking(true); - m_pos.m_timeLine = this; auto updateTimer = new QTimer(this); - connect( updateTimer, SIGNAL(timeout()), - this, SLOT(updatePosition())); + connect(updateTimer, &QTimer::timeout, this, &TimeLineWidget::updatePosition); updateTimer->start( 1000 / 60 ); // 60 fps connect( Engine::getSong(), SIGNAL(timeSignatureChanged(int,int)), this, SLOT(update())); @@ -98,10 +90,6 @@ TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, TimeLineWidget::~TimeLineWidget() { - if( getGUI()->songEditor() ) - { - m_pos.m_timeLine = nullptr; - } delete m_hint; } @@ -129,10 +117,10 @@ void TimeLineWidget::addToolButtons( QToolBar * _tool_bar ) loopPoints->setGeneralToolTip( tr( "Loop points" ) ); loopPoints->addState( embed::getIconPixmap( "loop_points_off" ) ); loopPoints->addState( embed::getIconPixmap( "loop_points_on" ) ); - connect( loopPoints, SIGNAL(changedState(int)), this, - SLOT(toggleLoopPoints(int))); - connect( this, SIGNAL(loopPointStateLoaded(int)), loopPoints, - SLOT(changeState(int))); + connect(loopPoints, &NStateButton::changedState, m_timeline, &Timeline::setLoopEnabled); + connect(m_timeline, &Timeline::loopEnabledChanged, loopPoints, &NStateButton::changeState); + connect(m_timeline, &Timeline::loopEnabledChanged, this, static_cast(&QWidget::update)); + loopPoints->changeState(static_cast(m_timeline->loopEnabled())); auto behaviourAtStop = new NStateButton(_tool_bar); behaviourAtStop->addState( embed::getIconPixmap( "back_to_zero" ), @@ -144,50 +132,24 @@ void TimeLineWidget::addToolButtons( QToolBar * _tool_bar ) "started" ) ); behaviourAtStop->addState( embed::getIconPixmap( "keep_stop_position" ), tr( "After stopping keep position" ) ); - connect( behaviourAtStop, SIGNAL(changedState(int)), this, - SLOT(toggleBehaviourAtStop(int))); - connect( this, SIGNAL(loadBehaviourAtStop(int)), behaviourAtStop, - SLOT(changeState(int))); - behaviourAtStop->changeState( static_cast(BehaviourAtStopState::BackToStart) ); + connect(behaviourAtStop, &NStateButton::changedState, m_timeline, + [timeline = m_timeline](int value) { + timeline->setStopBehaviour(static_cast(value)); + } + ); + connect(m_timeline, &Timeline::stopBehaviourChanged, behaviourAtStop, + [button = behaviourAtStop](Timeline::StopBehaviour value) { + button->changeState(static_cast(value)); + } + ); + behaviourAtStop->changeState(static_cast(m_timeline->stopBehaviour())); _tool_bar->addWidget( autoScroll ); _tool_bar->addWidget( loopPoints ); _tool_bar->addWidget( behaviourAtStop ); } - - - -void TimeLineWidget::saveSettings( QDomDocument & _doc, QDomElement & _this ) -{ - _this.setAttribute( "lp0pos", (int) loopBegin() ); - _this.setAttribute( "lp1pos", (int) loopEnd() ); - _this.setAttribute( "lpstate", static_cast(m_loopPoints) ); - _this.setAttribute( "stopbehaviour", static_cast(m_behaviourAtStop) ); -} - - - - -void TimeLineWidget::loadSettings( const QDomElement & _this ) -{ - m_loopPos[0] = _this.attribute( "lp0pos" ).toInt(); - m_loopPos[1] = _this.attribute( "lp1pos" ).toInt(); - m_loopPoints = static_cast( - _this.attribute( "lpstate" ).toInt() ); - update(); - emit loopPointStateLoaded( static_cast(m_loopPoints) ); - - if( _this.hasAttribute( "stopbehaviour" ) ) - { - emit loadBehaviourAtStop( _this.attribute( "stopbehaviour" ).toInt() ); - } -} - - - - -void TimeLineWidget::updatePosition( const TimePos & ) +void TimeLineWidget::updatePosition() { const int new_x = markerX( m_pos ); @@ -200,34 +162,11 @@ void TimeLineWidget::updatePosition( const TimePos & ) } } - - - void TimeLineWidget::toggleAutoScroll( int _n ) { m_autoScroll = static_cast( _n ); } - - - -void TimeLineWidget::toggleLoopPoints( int _n ) -{ - m_loopPoints = static_cast( _n ); - update(); -} - - - - -void TimeLineWidget::toggleBehaviourAtStop( int _n ) -{ - m_behaviourAtStop = static_cast( _n ); -} - - - - void TimeLineWidget::paintEvent( QPaintEvent * ) { QPainter p( this ); @@ -242,11 +181,11 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) // Draw the loop rectangle int const & loopRectMargin = getLoopRectangleVerticalPadding(); int const loopRectHeight = this->height() - 2 * loopRectMargin; - int const loopStart = markerX( loopBegin() ) + 8; - int const loopEndR = markerX( loopEnd() ) + 9; + int const loopStart = markerX(m_timeline->loopBegin()) + 8; + int const loopEndR = markerX(m_timeline->loopEnd()) + 9; int const loopRectWidth = loopEndR - loopStart; - bool const loopPointsActive = loopPointsEnabled(); + bool const loopPointsActive = m_timeline->loopEnabled(); // Draw the main rectangle (inner fill only) QRect outerRectangle( loopStart, loopRectMargin, loopRectWidth - 1, loopRectHeight - 1 ); @@ -336,12 +275,12 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) else if( event->button() == Qt::RightButton ) { m_moveXOff = m_posMarkerPixmap.width() / 2; - const TimePos t = m_begin + static_cast( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * TimePos::ticksPerBar() / m_ppb ); - const TimePos loopMid = ( m_loopPos[0] + m_loopPos[1] ) / 2; - m_action = t < loopMid ? Action::MoveLoopBegin : Action::MoveLoopEnd; - std::sort(std::begin(m_loopPos), std::end(m_loopPos)); - m_loopPos[( m_action == Action::MoveLoopBegin ) ? 0 : 1] = t; + const auto cursorXOffset = std::max(event->x() - m_xOffset - m_moveXOff, 0); + const TimePos timeAtCursor = m_begin + static_cast(cursorXOffset * TimePos::ticksPerBar() / m_ppb); + const TimePos loopMid = (m_timeline->loopBegin() + m_timeline->loopEnd()) / 2; + + m_action = timeAtCursor < loopMid ? Action::MoveLoopBegin : Action::MoveLoopEnd; } if( m_action == Action::MoveLoopBegin || m_action == Action::MoveLoopEnd ) @@ -360,49 +299,53 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) { parentWidget()->update(); // essential for widgets that this timeline had taken their mouse move event from. - const TimePos t = m_begin + static_cast( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * TimePos::ticksPerBar() / m_ppb ); + + const auto cursorXOffset = std::max(event->x() - m_xOffset - m_moveXOff, 0); + TimePos timeAtCursor = m_begin + static_cast(cursorXOffset * TimePos::ticksPerBar() / m_ppb); switch( m_action ) { case Action::MovePositionMarker: - m_pos.setTicks(t.getTicks()); - Engine::getSong()->setToTime(t, m_mode); + m_pos.setTicks(timeAtCursor.getTicks()); + Engine::getSong()->setToTime(timeAtCursor, m_mode); if (!( Engine::getSong()->isPlaying())) { //Song::PlayMode::None is used when nothing is being played. - Engine::getSong()->setToTime(t, Song::PlayMode::None); + Engine::getSong()->setToTime(timeAtCursor, Song::PlayMode::None); } m_pos.setCurrentFrame( 0 ); m_pos.setJumped( true ); updatePosition(); - positionMarkerMoved(); break; case Action::MoveLoopBegin: case Action::MoveLoopEnd: { - const int i = m_action == Action::MoveLoopBegin ? 0 : 1; + const auto otherPoint = m_action == Action::MoveLoopBegin + ? m_timeline->loopEnd() + : m_timeline->loopBegin(); const bool control = event->modifiers() & Qt::ControlModifier; if (control) { // no ctrl-press-hint when having ctrl pressed delete m_hint; m_hint = nullptr; - m_loopPos[i] = t; } else { - m_loopPos[i] = t.quantize(m_snapSize); + timeAtCursor = timeAtCursor.quantize(m_snapSize); } // Catch begin == end - if (m_loopPos[0] == m_loopPos[1]) + if (timeAtCursor == otherPoint) { const int offset = control ? 1 : m_snapSize * TimePos::ticksPerBar(); - // Note, swap 1 and 0 below and the behavior "skips" the other - // marking instead of pushing it. - if (m_action == Action::MoveLoopBegin) { m_loopPos[0] -= offset; } - else { m_loopPos[1] += offset; } + if (m_action == Action::MoveLoopBegin) { timeAtCursor -= offset; } + else { timeAtCursor += offset; } } + // Update m_action so we still move the correct point even if it is + // dragged past the other. + m_action = timeAtCursor < otherPoint ? Action::MoveLoopBegin : Action::MoveLoopEnd; + m_timeline->setLoopPoints(timeAtCursor, otherPoint); update(); break; } diff --git a/src/gui/widgets/NStateButton.cpp b/src/gui/widgets/NStateButton.cpp index 4fbcc0d65..4dc0252fc 100644 --- a/src/gui/widgets/NStateButton.cpp +++ b/src/gui/widgets/NStateButton.cpp @@ -67,34 +67,27 @@ void NStateButton::addState( const QPixmap & _pm, const QString & _tooltip ) -void NStateButton::changeState( int _n ) +void NStateButton::changeState(int state) { - if( _n >= 0 && _n < (int) m_states.size() ) + if (state >= 0 && state < m_states.size() && state != m_curState) { - m_curState = _n; + m_curState = state; - const QString & _tooltip = - ( m_states[m_curState].second != "" ) ? - m_states[m_curState].second : - m_generalToolTip; - setToolTip(_tooltip); + const auto& [icon, tooltip] = m_states[m_curState]; + setToolTip(tooltip.isEmpty() ? m_generalToolTip : tooltip); + setIcon(icon); - setIcon( m_states[m_curState].first ); - - emit changedState( m_curState ); + emit changedState(m_curState); } } - - -void NStateButton::mousePressEvent( QMouseEvent * _me ) +void NStateButton::mousePressEvent(QMouseEvent* me) { - if( _me->button() == Qt::LeftButton && m_states.size() ) + if (me->button() == Qt::LeftButton && !m_states.empty()) { - changeState( ( ++m_curState ) % m_states.size() ); + changeState((m_curState + 1) % m_states.size()); } - ToolButton::mousePressEvent( _me ); + ToolButton::mousePressEvent(me); } - -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui From 3aefe7b3d3a447be6565364a1386fe148ac061eb Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:58:02 -0800 Subject: [PATCH 052/191] Fix LOMM crossovers (#7026) --- plugins/LOMM/LOMM.cpp | 10 ++++++++-- plugins/LOMM/LOMM.h | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/LOMM/LOMM.cpp b/plugins/LOMM/LOMM.cpp index 6dc640626..482a59b7e 100644 --- a/plugins/LOMM/LOMM.cpp +++ b/plugins/LOMM/LOMM.cpp @@ -55,6 +55,7 @@ LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* m_lp2(m_sampleRate), m_hp1(m_sampleRate), m_hp2(m_sampleRate), + m_ap(m_sampleRate), m_needsUpdate(true), m_coeffPrecalc(-0.05), m_crestTimeConst(0.999), @@ -67,6 +68,8 @@ LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* m_yL[1][0] = m_yL[1][1] = LOMM_MIN_FLOOR; m_yL[2][0] = m_yL[2][1] = LOMM_MIN_FLOOR; + m_ap.setFilterType(BasicFilters<2>::FilterType::AllPass); + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); emit changeSampleRate(); } @@ -117,6 +120,7 @@ bool LOMMEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) { m_lp1.setLowpass(m_lommControls.m_split1Model.value()); m_hp1.setHighpass(m_lommControls.m_split1Model.value()); + m_ap.calcFilterCoeffs(m_lommControls.m_split1Model.value(), 0.70710678118); } if (m_needsUpdate || m_lommControls.m_split2Model.isValueChanged()) { @@ -224,9 +228,11 @@ bool LOMMEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) float crestFactorValTemp = ((m_crestFactorVal[i] - LOMM_AUTO_TIME_ADJUST) * autoTime) + LOMM_AUTO_TIME_ADJUST; // Crossover filters - bands[0][i] = m_hp1.update(s[i], i); - bands[1][i] = m_hp2.update(m_lp1.update(s[i], i), i); bands[2][i] = m_lp2.update(s[i], i); + bands[1][i] = m_hp2.update(s[i], i); + bands[0][i] = m_hp1.update(bands[1][i], i); + bands[1][i] = m_lp1.update(bands[1][i], i); + bands[2][i] = m_ap.update(bands[2][i], i); if (!split1Enabled) { diff --git a/plugins/LOMM/LOMM.h b/plugins/LOMM/LOMM.h index 039f80b6a..c1a5aef70 100644 --- a/plugins/LOMM/LOMM.h +++ b/plugins/LOMM/LOMM.h @@ -73,6 +73,8 @@ private: StereoLinkwitzRiley m_hp1; StereoLinkwitzRiley m_hp2; + BasicFilters<2> m_ap; + bool m_needsUpdate; float m_coeffPrecalc; From ce722dd6b6cdfa5c7378161ce7d1fa98bfe5fec2 Mon Sep 17 00:00:00 2001 From: saker Date: Mon, 25 Dec 2023 07:07:11 -0500 Subject: [PATCH 053/191] Refactor ``SampleBuffer`` (#6610) * Add refactored SampleBuffer * Add Sample * Add SampleLoader * Integrate changes into AudioSampleRecorder * Integrate changes into Oscillator * Integrate changes into SampleClip/SamplePlayHandle * Integrate changes into Graph * Remove SampleBuffer include from SampleClipView * Integrate changes into Patman * Reduce indirection to sample buffer from Sample * Integrate changes into AudioFileProcessor * Remove old SampleBuffer * Include memory header in TripleOscillator * Include memory header in Oscillator * Use atomic_load within SampleClip::sample * Include memory header in EnvelopeAndLfoParameters * Use std::atomic_load for most calls to Oscillator::userWaveSample * Revert accidental change on SamplePlayHandle L.111 * Check if audio file is empty before loading * Add asserts to Sample * Add cassert include within Sample * Adjust assert expressions in Sample * Remove use of shared ownership for Sample Sample does not need to be wrapped around a std::shared_ptr. This was to work with the audio thread, but the audio thread can instead have their own Sample separate from the UI's Sample, so changes to the UI's Sample would not leave the audio worker thread using freed data if it had pointed to it. * Use ArrayVector in Sample * Enforce std::atomic_load for users of std::shared_ptr * Use requestChangesGuard in ClipView::remove Fixes data race when deleting SampleClip * Revert only formatting changes * Update ClipView::remove comment * Revert "Remove use of shared ownership for Sample" This reverts commit 1d452331d16626ac2f3c42bbd25f1267a61cc116. In some cases, you can infact do away with shared ownership on Sample if there are no writes being made to either of them, but to make sure changes are reflected to the object in cases where writes do happen, they should work with the same one. * Fix heap-use-after-free in Track::loadSettings * Remove m_buffer asserts * Refactor play functionality (again) The responsibility of resampling the buffer and moving the frame index is now in Sample::play, allowing the removal of both playSampleRangeLoop and playSampleRangePingPong. * Change copyright * Cast processingSampleRate to float Fixes division by zero error * Update include/SampleLoader.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Update include/SampleLoader.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Format SampleLoader.h * Remove SampleBuffer.h include in SampleRecordHandle.h * Update src/core/Oscillator.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Use typeInfo for float equality comparison Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Use std::min in Sample::visualize Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Move in result to m_data * Use if block in playSampleRange * Pass in unique_ptr to SampleClip::setSampleBuffer * Return const QString& from SampleBuffer::audioFile * Do not pass in unique_ptr by r-value reference * Use isEmpty() within SampleClipView::updateSample * Remove use of atomic_store and atomic_load * Remove ArrayVector comment * Use array specialization for unique_ptr when managing DrumSynth data Also made it so that we don't create result before checking if we failed to decode the file, potentially saving us an allocation. * Don't manually delete Clip if it has a Track * Clean up generateAntiAliasUserWaveTable function Also, make it so that we actually call this function when necessary in TripleOscillator. * Set user wave, even when value is empty If the value or file is empty, I think showing a error popup here is ideal. * Remove whitespace in EnvelopeAndLfoParameters.cpp L#121 * Fix error in c5f7ccba492dd867524156afa652c4eff99f9b40 We still have to delete the Clip's, or else we would just be eating up memory. But we should first make sure that the Track's no longer see this Clip in their m_clips vector. This has to happen as it's own operation because we have to wait for the audio thread(s) first. This would ensure that Track's do not create PlayHandle's that would refer to a Clip that is currently being destroyed. After that, then we call deleteLater on the Clip. * Convert std::shared_ptr to Sample This conversion does not apply to Patman as there seems to be issues with it causing heap-use-after-free issues, such as with PatmanInstrument::unloadCurrentPatch * Fix segfault when closing LMMS Song should be deleted before AudioEngine. * Construct buffer through SampleLoader in FileBrowser's previewFileItem function + Remove const qualification in SamplePlayHandle(const QString&) constructor for m_sample * Move guard out of removeClip and deleteClips + Revert commit 1769ed517da389997b40568f26dd54b800a5d62c since this would fix it anyway (we don't try to lock the engine to delete the global automation track when closing LMMS now) * Simplify the switch in play function for loopMode * Add SampleDecoder * Add LMMS_HAVE_OGGVORBIS comment * Fix unused variable error * Include unordered_map * Simplify SampleDecoder Instead of using the extension (which could be wrong) for the file, we simply loop through all the decoders available. First sndfile because it covers a lot of formats, then the ogg decoder for the few cases where sndfile would not work for certain audio codecs, and then the DrumSynth decoder. * Attempt to fix Mac builds * Attempt to fix Mac builds take 2 * Add vector include to SampleDecoder * Add TODO comment about shared ownership with clips Calls to ClipView::remove may occur at any point, which can cause a problem when the Track is using the clip about to be removed. A suitable solution would be to use shared ownership between the Track and ClipView for the clip. Track's can then simply remove the shared pointer in their m_clips vector, and ClipView can call reset on the shared pointer on calls to ClipView::remove. * Adjust TODO comment Disregard the shared ownership idea. Since we would be modifying the collection of Clip's in Track when removing the Clip, the Track could be iterating said collection while this happens, causing a bug. In this case, we do actually want a synchronization mechanism. However, I didn't mention another separate issue in the TODO comment that should've been addressed: ~Clip should not be responsible for actually removing the itself from it's Track. With calls to removeClip, one would expect that to already occur. * Remove Sample::playbackSize Inside SampleClip::sampleLength, we should be using Sample::sampleSize instead. * Fix issues involving length of Sample's SampleClip::sampleLength should be passing the Sample's sample rate to Engine::framesPerTick. I also changed sampleDuration to return a std::chrono::milliseconds instead of an int so that the callers know what time interval is being used. * Simplify if condition in src/gui/FileBrowser.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Simplify if condition in src/core/SampleBuffer.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Update style in include/Oscillator.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Format src/core/SampleDecoder.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Set the sample rate to be that of the AudioEngine by default I also removed some checks involving the state of the SampleBuffer. These functions should expect a valid SampleBuffer each time. This helps to simplify things since we don't have to validate it in each function. * Set single-argument constructors in Sample and SampleBuffer to be explicit * Do not make a copy when reading result from the decoder * Add constructor to pass in vector of sampleFrame's directly * Do a pass by value and move in SampleBuffer.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Pass vector by value in SampleBuffer.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Make Sample(std::shared_ptr) constructor explicit * Properly draw sample waveform when reversed * Collect sample not found errors when loading project Also return empty buffers when trying to load either an empty file or empty Base64 string * Use std::make_unique in SampleLoader * Fix loop modes * Limit sample duration to [start, end] and not the entire buffer * Use structured binding to access buffer * Check if GUI exists before displaying error * Make Base64 constructor pass in the string instead * Remove use of QByteArray::fromBase64Encoding * Inline simple functions in SampleBuffer * Dynamically include supported audio file types * Remove redundant inline specifier Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Translate file types * Cache calls to SampleDecoder::supportedAudioTypes * Fix translations in SampleLoader (again) Also ensure that all the file types are listed first. Also simplified the generation of the list a bit. * Store static local variable for supported audio types instead of in the header Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Clamp frame index depending on loop mode * Inline member functions of PlaybackState * Do not collect errors in SampleLoader when loading projects Also fix conflicts with surrounding codebase * Default construct shared pointers to SampleBuffer * Simplify and optimize Sample::visulaize() * Remove redundant gui:: prefix * Rearrange Sample::visualize after optimizations by DanielKauss * Apply amplification when visualizing sample waveforms * Set default min and max values to 1 and -1 * Treat waveform as mono signal when visualizing * Ensure visualization works when framesPerPixel < 1 * Simplify Sample::visualize a bit more * Fix CPU lag in Sample by using atomics (with relaxed ordering) Changing any of the frame markers originally took a writer lock on a mutex. The problem is that Sample::play took a reader lock first before executing. Because Sample::play has to wait on the writer, this created a lot of lag and raised the CPU meter. The solution would to be to use atomics instead. * Fix errors from merge * Fix broken LFO controller functionality The shared_ptr should have been taken by reference. * Remove TODO * Update EnvelopeAndLfoView.cpp Co-authored-by: Dalton Messmer * Update src/gui/clips/SampleClipView.cpp Co-authored-by: Dalton Messmer * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer * Store shortest relative path in SampleBuffer * Tie up a few loose ends * Use sample_rate_t when storing sample rate in SampleBuffer * Add missing named requirement functions and aliases * Use sampledata attribute when loading from Base64 in AFP * Remove initializer for m_userWave in the constructor * Do not use trailing return syntax when return is void * Move decoder functionality into unnamed namespace * Remove redundant gui:: prefix * Use PathUtil::toAbsolute to simplify code in SampleLoader::openAudioFile * Fix translations in SampleLoader::openAudioFile Co-authored-by: DomClark * Fix formatting for ternary operator * Remove redundant inlines * Resolve UB when decoding from Base64 data in SampleBuffer * Fix up SampleClip constructors * Add AudioResampler, a wrapper class around libsamplerate The wrapper has only been applied to Sample::PlaybackState for now. AudioResampler should be used by other classes in the future that do resampling with libsamplerate. * Move buffer when moving and simplify assignment functions in Sample * Move Sample::visualize out of Sample and into the GUI namespace * Initialize supportedAudioTypes in static lambda * Return shared pointer from SampleLoader * Create and use static empty SampleBuffer by default * Fix header guard in SampleWaveform.h * Remove use of src_clone CI seems to have an old version of libsamplerate and does not have this method. * Include memory header in SampleBuffer.h * Remove mutex and shared_mutex includes in Sample.h * Attempt to fix string operand error within AudioResampler * Include string header in AudioResampler.cpp * Add LMMS_EXPORT for SampleWaveform class declaration * Add LMMS_EXPORT for AudioResampler class declaration * Enforce returning std::shared_ptr * Restrict the size of the memcpy to the destination size, not the source size * Do not make resample const AudioResampler::resample, while seemingly not changing the data of the resampler, still alters its internal state and therefore should not be const. This is because libsamplerate manages state when resampling. * Initialize data.end_of_input * Add trailing new lines * Simplify AudioResampler interface * Fix header guard prefix to LMMS_GUI instead of LMMS * Remove Sample::resampleSampleRange --------- Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: Daniel Kauss Co-authored-by: Dalton Messmer Co-authored-by: DomClark --- include/AudioResampler.h | 64 + include/AudioSampleRecorder.h | 4 +- include/EnvelopeAndLfoParameters.h | 3 +- include/LfoController.h | 2 +- include/Oscillator.h | 30 +- include/Sample.h | 139 ++ include/SampleBuffer.h | 353 +--- include/SampleClip.h | 17 +- include/SampleDecoder.h | 57 + include/SampleLoader.h | 48 + include/SamplePlayHandle.h | 7 +- include/SampleRecordHandle.h | 3 +- include/SampleWaveform.h | 41 + .../AudioFileProcessor/AudioFileProcessor.cpp | 192 +- .../AudioFileProcessor/AudioFileProcessor.h | 16 +- plugins/GigPlayer/GigPlayer.cpp | 4 +- plugins/Patman/Patman.cpp | 26 +- plugins/Patman/Patman.h | 7 +- plugins/SlicerT/SlicerT.cpp | 55 +- plugins/SlicerT/SlicerT.h | 3 +- plugins/SlicerT/SlicerTView.cpp | 5 +- plugins/SlicerT/SlicerTWaveform.cpp | 25 +- plugins/TripleOscillator/TripleOscillator.cpp | 36 +- plugins/TripleOscillator/TripleOscillator.h | 10 +- src/core/AudioResampler.cpp | 64 + src/core/CMakeLists.txt | 3 + src/core/EnvelopeAndLfoParameters.cpp | 25 +- src/core/LfoController.cpp | 29 +- src/core/Oscillator.cpp | 22 +- src/core/Sample.cpp | 230 +++ src/core/SampleBuffer.cpp | 1616 +---------------- src/core/SampleClip.cpp | 88 +- src/core/SampleDecoder.cpp | 184 ++ src/core/SamplePlayHandle.cpp | 17 +- src/core/SampleRecordHandle.cpp | 31 +- src/core/Track.cpp | 5 +- src/core/audio/AudioSampleRecorder.cpp | 26 +- src/gui/CMakeLists.txt | 2 + src/gui/FileBrowser.cpp | 11 +- src/gui/LfoControllerDialog.cpp | 17 +- src/gui/SampleLoader.cpp | 126 ++ src/gui/SampleWaveform.cpp | 94 + src/gui/clips/ClipView.cpp | 11 + src/gui/clips/SampleClipView.cpp | 26 +- src/gui/editors/AutomationEditor.cpp | 7 +- src/gui/instrument/EnvelopeAndLfoView.cpp | 17 +- src/gui/widgets/Graph.cpp | 12 +- src/tracks/SampleTrack.cpp | 4 +- 48 files changed, 1551 insertions(+), 2263 deletions(-) create mode 100644 include/AudioResampler.h create mode 100644 include/Sample.h create mode 100644 include/SampleDecoder.h create mode 100644 include/SampleLoader.h create mode 100644 include/SampleWaveform.h create mode 100644 src/core/AudioResampler.cpp create mode 100644 src/core/Sample.cpp create mode 100644 src/core/SampleDecoder.cpp create mode 100644 src/gui/SampleLoader.cpp create mode 100644 src/gui/SampleWaveform.cpp diff --git a/include/AudioResampler.h b/include/AudioResampler.h new file mode 100644 index 000000000..379146962 --- /dev/null +++ b/include/AudioResampler.h @@ -0,0 +1,64 @@ +/* + * AudioResampler.h - wrapper around libsamplerate + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_AUDIO_RESAMPLER_H +#define LMMS_AUDIO_RESAMPLER_H + +#include + +#include "lmms_export.h" + +namespace lmms { + +class LMMS_EXPORT AudioResampler +{ +public: + struct ProcessResult + { + int error; + long inputFramesUsed; + long outputFramesGenerated; + }; + + AudioResampler(int interpolationMode, int channels); + AudioResampler(const AudioResampler&) = delete; + AudioResampler(AudioResampler&&) = delete; + ~AudioResampler(); + + AudioResampler& operator=(const AudioResampler&) = delete; + AudioResampler& operator=(AudioResampler&&) = delete; + + auto resample(const float* in, long inputFrames, float* out, long outputFrames, double ratio) -> ProcessResult; + auto interpolationMode() const -> int { return m_interpolationMode; } + auto channels() const -> int { return m_channels; } + +private: + int m_interpolationMode = -1; + int m_channels = 0; + int m_error = 0; + SRC_STATE* m_state = nullptr; +}; +} // namespace lmms + +#endif // LMMS_AUDIO_RESAMPLER_H diff --git a/include/AudioSampleRecorder.h b/include/AudioSampleRecorder.h index 8937ceb5e..d481cc16c 100644 --- a/include/AudioSampleRecorder.h +++ b/include/AudioSampleRecorder.h @@ -28,6 +28,7 @@ #include #include +#include #include "AudioDevice.h" @@ -44,8 +45,7 @@ public: ~AudioSampleRecorder() override; f_cnt_t framesRecorded() const; - void createSampleBuffer( SampleBuffer** sampleBuffer ); - + std::shared_ptr createSampleBuffer(); private: void writeBuffer( const surroundSampleFrame * _ab, diff --git a/include/EnvelopeAndLfoParameters.h b/include/EnvelopeAndLfoParameters.h index 7abc3910e..2a8d3a685 100644 --- a/include/EnvelopeAndLfoParameters.h +++ b/include/EnvelopeAndLfoParameters.h @@ -25,6 +25,7 @@ #ifndef LMMS_ENVELOPE_AND_LFO_PARAMETERS_H #define LMMS_ENVELOPE_AND_LFO_PARAMETERS_H +#include #include #include "JournallingObject.h" @@ -167,7 +168,7 @@ private: sample_t * m_lfoShapeData; sample_t m_random; bool m_bad_lfoShapeData; - SampleBuffer m_userWave; + std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); enum class LfoShape { diff --git a/include/LfoController.h b/include/LfoController.h index 109edbd3f..01b4b1862 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -87,7 +87,7 @@ protected: private: float m_heldSample; - SampleBuffer * m_userDefSampleBuffer; + std::shared_ptr m_userDefSampleBuffer = SampleBuffer::emptyBuffer(); protected slots: void updatePhase(); diff --git a/include/Oscillator.h b/include/Oscillator.h index dab0b948d..a480bf524 100644 --- a/include/Oscillator.h +++ b/include/Oscillator.h @@ -28,7 +28,9 @@ #include #include +#include #include +#include "interpolation.h" #include "Engine.h" #include "lmms_constants.h" @@ -46,7 +48,6 @@ class IntModel; class LMMS_EXPORT Oscillator { - MM_OPERATORS public: enum class WaveShape { @@ -91,18 +92,23 @@ public: static void waveTableInit(); static void destroyFFTPlans(); - static void generateAntiAliasUserWaveTable(SampleBuffer* sampleBuffer); + static std::unique_ptr generateAntiAliasUserWaveTable(const SampleBuffer* sampleBuffer); inline void setUseWaveTable(bool n) { m_useWaveTable = n; } - inline void setUserWave( const SampleBuffer * _wave ) + void setUserWave(std::shared_ptr _wave) { m_userWave = _wave; } + void setUserAntiAliasWaveTable(std::shared_ptr waveform) + { + m_userAntiAliasWaveTable = waveform; + } + void update(sampleFrame* ab, const fpp_t frames, const ch_cnt_t chnl, bool modulator = false); // now follow the wave-shape-routines... @@ -164,9 +170,18 @@ public: return 1.0f - fast_rand() * 2.0f / FAST_RAND_MAX; } - inline sample_t userWaveSample( const float _sample ) const + static sample_t userWaveSample(const SampleBuffer* buffer, const float sample) { - return m_userWave->userWaveSample( _sample ); + if (buffer == nullptr || buffer->size() == 0) { return 0; } + const auto frames = buffer->size(); + const auto frame = sample * frames; + auto f1 = static_cast(frame) % frames; + if (f1 < 0) + { + f1 += frames; + } + + return linearInterpolate(buffer->data()[f1][0], buffer->data()[(f1 + 1) % frames][0], fraction(frame)); } struct wtSampleControl { @@ -203,7 +218,7 @@ public: table[control.band][control.f2], fraction(control.frame)); } - inline sample_t wtSample(const std::unique_ptr& table, const float sample) const + sample_t wtSample(const OscillatorConstants::waveform_t* table, const float sample) const { assert(table != nullptr); wtSampleControl control = getWtSampleControl(sample); @@ -247,7 +262,8 @@ private: Oscillator * m_subOsc; float m_phaseOffset; float m_phase; - const SampleBuffer * m_userWave; + std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userAntiAliasWaveTable; bool m_useWaveTable; // There are many update*() variants; the modulator flag is stored as a member variable to avoid // adding more explicit parameters to all of them. Can be converted to a parameter if needed. diff --git a/include/Sample.h b/include/Sample.h new file mode 100644 index 000000000..2ccb78b19 --- /dev/null +++ b/include/Sample.h @@ -0,0 +1,139 @@ +/* + * Sample.h - State for container-class SampleBuffer + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SAMPLE_H +#define LMMS_SAMPLE_H + +#include +#include + +#include "AudioResampler.h" +#include "Note.h" +#include "SampleBuffer.h" +#include "lmms_export.h" + +class QPainter; +class QRect; + +namespace lmms { +class LMMS_EXPORT Sample +{ +public: + // values for buffer margins, used for various libsamplerate interpolation modes + // the array positions correspond to the converter_type parameter values in libsamplerate + // if there appears problems with playback on some interpolation mode, then the value for that mode + // may need to be higher - conversely, to optimize, some may work with lower values + static constexpr auto s_interpolationMargins = std::array{64, 64, 64, 4, 4}; + + enum class Loop + { + Off, + On, + PingPong + }; + + class LMMS_EXPORT PlaybackState + { + public: + PlaybackState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR) + : m_resampler(interpolationMode, DEFAULT_CHANNELS) + , m_varyingPitch(varyingPitch) + { + } + + auto resampler() -> AudioResampler& { return m_resampler; } + auto frameIndex() const -> f_cnt_t { return m_frameIndex; } + auto varyingPitch() const -> bool { return m_varyingPitch; } + auto backwards() const -> bool { return m_backwards; } + + void setFrameIndex(f_cnt_t frameIndex) { m_frameIndex = frameIndex; } + void setVaryingPitch(bool varyingPitch) { m_varyingPitch = varyingPitch; } + void setBackwards(bool backwards) { m_backwards = backwards; } + + private: + AudioResampler m_resampler; + f_cnt_t m_frameIndex = 0; + bool m_varyingPitch = false; + bool m_backwards = false; + friend class Sample; + }; + + Sample() = default; + Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const Sample& other); + Sample(Sample&& other); + explicit Sample(const QString& audioFile); + explicit Sample(std::shared_ptr buffer); + + auto operator=(const Sample&) -> Sample&; + auto operator=(Sample&&) -> Sample&; + + auto play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency = DefaultBaseFreq, + Loop loopMode = Loop::Off) -> bool; + + auto sampleDuration() const -> std::chrono::milliseconds; + auto sampleFile() const -> const QString& { return m_buffer->audioFile(); } + auto sampleRate() const -> int { return m_buffer->sampleRate(); } + auto sampleSize() const -> int { return m_buffer->size(); } + + auto toBase64() const -> QString { return m_buffer->toBase64(); } + + auto data() const -> const sampleFrame* { return m_buffer->data(); } + auto buffer() const -> std::shared_ptr { return m_buffer; } + auto startFrame() const -> int { return m_startFrame.load(std::memory_order_relaxed); } + auto endFrame() const -> int { return m_endFrame.load(std::memory_order_relaxed); } + auto loopStartFrame() const -> int { return m_loopStartFrame.load(std::memory_order_relaxed); } + auto loopEndFrame() const -> int { return m_loopEndFrame.load(std::memory_order_relaxed); } + auto amplification() const -> float { return m_amplification.load(std::memory_order_relaxed); } + auto frequency() const -> float { return m_frequency.load(std::memory_order_relaxed); } + auto reversed() const -> bool { return m_reversed.load(std::memory_order_relaxed); } + + void setStartFrame(int startFrame) { m_startFrame.store(startFrame, std::memory_order_relaxed); } + void setEndFrame(int endFrame) { m_endFrame.store(endFrame, std::memory_order_relaxed); } + void setLoopStartFrame(int loopStartFrame) { m_loopStartFrame.store(loopStartFrame, std::memory_order_relaxed); } + void setLoopEndFrame(int loopEndFrame) { m_loopEndFrame.store(loopEndFrame, std::memory_order_relaxed); } + void setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, int loopEndFrame); + void setAmplification(float amplification) { m_amplification.store(amplification, std::memory_order_relaxed); } + void setFrequency(float frequency) { m_frequency.store(frequency, std::memory_order_relaxed); } + void setReversed(bool reversed) { m_reversed.store(reversed, std::memory_order_relaxed); } + +private: + void playSampleRange(PlaybackState* state, sampleFrame* dst, size_t numFrames) const; + void amplifySampleRange(sampleFrame* src, int numFrames) const; + void copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const; + void copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const; + +private: + std::shared_ptr m_buffer = SampleBuffer::emptyBuffer(); + std::atomic m_startFrame = 0; + std::atomic m_endFrame = 0; + std::atomic m_loopStartFrame = 0; + std::atomic m_loopEndFrame = 0; + std::atomic m_amplification = 1.0f; + std::atomic m_frequency = DefaultBaseFreq; + std::atomic m_reversed = false; +}; +} // namespace lmms +#endif diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 3d1013baa..0db8aa4d3 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -25,333 +25,74 @@ #ifndef LMMS_SAMPLE_BUFFER_H #define LMMS_SAMPLE_BUFFER_H +#include +#include #include -#include -#include - +#include #include +#include -#include "lmms_export.h" -#include "interpolation.h" +#include "AudioEngine.h" +#include "Engine.h" #include "lmms_basics.h" -#include "lmms_math.h" -#include "shared_object.h" -#include "OscillatorConstants.h" -#include "MemoryManager.h" +#include "lmms_export.h" - -class QPainter; -class QRect; - -namespace lmms +namespace lmms { +class LMMS_EXPORT SampleBuffer { - -// values for buffer margins, used for various libsamplerate interpolation modes -// the array positions correspond to the converter_type parameter values in libsamplerate -// if there appears problems with playback on some interpolation mode, then the value for that mode -// may need to be higher - conversely, to optimize, some may work with lower values -const f_cnt_t MARGIN[] = { 64, 64, 64, 4, 4 }; - -class LMMS_EXPORT SampleBuffer : public QObject, public sharedObject -{ - Q_OBJECT - MM_OPERATORS public: - enum class LoopMode { - Off = 0, - On, - PingPong - }; - class LMMS_EXPORT handleState - { - MM_OPERATORS - public: - handleState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR); - virtual ~handleState(); + using value_type = sampleFrame; + using reference = sampleFrame&; + using const_reference = const sampleFrame&; + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + using difference_type = std::vector::difference_type; + using size_type = std::vector::size_type; + using reverse_iterator = std::vector::reverse_iterator; + using const_reverse_iterator = std::vector::const_reverse_iterator; - const f_cnt_t frameIndex() const - { - return m_frameIndex; - } + SampleBuffer() = default; + explicit SampleBuffer(const QString& audioFile); + SampleBuffer(const QString& base64, int sampleRate); + SampleBuffer(std::vector data, int sampleRate); + SampleBuffer( + const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); - void setFrameIndex(f_cnt_t index) - { - m_frameIndex = index; - } + friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept; + auto toBase64() const -> QString; - bool isBackwards() const - { - return m_isBackwards; - } + auto audioFile() const -> const QString& { return m_audioFile; } + auto sampleRate() const -> sample_rate_t { return m_sampleRate; } - void setBackwards(bool backwards) - { - m_isBackwards = backwards; - } + auto begin() -> iterator { return m_data.begin(); } + auto end() -> iterator { return m_data.end(); } - int interpolationMode() const - { - return m_interpolationMode; - } + auto begin() const -> const_iterator { return m_data.begin(); } + auto end() const -> const_iterator { return m_data.end(); } + auto cbegin() const -> const_iterator { return m_data.cbegin(); } + auto cend() const -> const_iterator { return m_data.cend(); } - private: - f_cnt_t m_frameIndex; - const bool m_varyingPitch; - bool m_isBackwards; - SRC_STATE * m_resamplingData; - int m_interpolationMode; + auto rbegin() -> reverse_iterator { return m_data.rbegin(); } + auto rend() -> reverse_iterator { return m_data.rend(); } - friend class SampleBuffer; + auto rbegin() const -> const_reverse_iterator { return m_data.rbegin(); } + auto rend() const -> const_reverse_iterator { return m_data.rend(); } - } ; + auto crbegin() const -> const_reverse_iterator { return m_data.crbegin(); } + auto crend() const -> const_reverse_iterator { return m_data.crend(); } + auto data() const -> const sampleFrame* { return m_data.data(); } + auto size() const -> size_type { return m_data.size(); } + auto empty() const -> bool { return m_data.empty(); } - SampleBuffer(); - // constructor which either loads sample _audio_file or decodes - // base64-data out of string - SampleBuffer(const QString & audioFile, bool isBase64Data = false); - SampleBuffer(const sampleFrame * data, const f_cnt_t frames); - explicit SampleBuffer(const f_cnt_t frames); - SampleBuffer(const SampleBuffer & orig); - - friend void swap(SampleBuffer & first, SampleBuffer & second) noexcept; - SampleBuffer& operator= (const SampleBuffer that); - - ~SampleBuffer() override; - - bool play( - sampleFrame * ab, - handleState * state, - const fpp_t frames, - const float freq, - const LoopMode loopMode = LoopMode::Off - ); - - void visualize( - QPainter & p, - const QRect & dr, - const QRect & clip, - f_cnt_t fromFrame = 0, - f_cnt_t toFrame = 0 - ); - inline void visualize( - QPainter & p, - const QRect & dr, - f_cnt_t fromFrame = 0, - f_cnt_t toFrame = 0 - ) - { - visualize(p, dr, dr, fromFrame, toFrame); - } - - inline const QString & audioFile() const - { - return m_audioFile; - } - - inline f_cnt_t startFrame() const - { - return m_startFrame; - } - - inline f_cnt_t endFrame() const - { - return m_endFrame; - } - - inline f_cnt_t loopStartFrame() const - { - return m_loopStartFrame; - } - - inline f_cnt_t loopEndFrame() const - { - return m_loopEndFrame; - } - - void setLoopStartFrame(f_cnt_t start) - { - m_loopStartFrame = start; - } - - void setLoopEndFrame(f_cnt_t end) - { - m_loopEndFrame = end; - } - - void setAllPointFrames( - f_cnt_t start, - f_cnt_t end, - f_cnt_t loopStart, - f_cnt_t loopEnd - ) - { - m_startFrame = start; - m_endFrame = end; - m_loopStartFrame = loopStart; - m_loopEndFrame = loopEnd; - } - - inline f_cnt_t frames() const - { - return m_frames; - } - - inline float amplification() const - { - return m_amplification; - } - - inline bool reversed() const - { - return m_reversed; - } - - inline float frequency() const - { - return m_frequency; - } - - sample_rate_t sampleRate() const - { - return m_sampleRate; - } - - int sampleLength() const - { - return double(m_endFrame - m_startFrame) / m_sampleRate * 1000; - } - - inline void setFrequency(float freq) - { - m_frequency = freq; - } - - inline void setSampleRate(sample_rate_t rate) - { - m_sampleRate = rate; - } - - inline const sampleFrame * data() const - { - return m_data; - } - - QString openAudioFile() const; - QString openAndSetAudioFile(); - QString openAndSetWaveformFile(); - - QString & toBase64(QString & dst) const; - - - // protect calls from the GUI to this function with dataReadLock() and - // dataUnlock() - SampleBuffer * resample(const sample_rate_t srcSR, const sample_rate_t dstSR); - - void normalizeSampleRate(const sample_rate_t srcSR, bool keepSettings = false); - - // protect calls from the GUI to this function with dataReadLock() and - // dataUnlock(), out of loops for efficiency - inline sample_t userWaveSample(const float sample) const - { - f_cnt_t frames = m_frames; - sampleFrame * data = m_data; - const float frame = sample * frames; - f_cnt_t f1 = static_cast(frame) % frames; - if (f1 < 0) - { - f1 += frames; - } - return linearInterpolate(data[f1][0], data[(f1 + 1) % frames][0], fraction(frame)); - } - - void dataReadLock() - { - m_varLock.lockForRead(); - } - - void dataUnlock() - { - m_varLock.unlock(); - } - - - std::unique_ptr m_userAntiAliasWaveTable; - - -public slots: - void setAudioFile(const QString & audioFile); - void loadFromBase64(const QString & data); - void setStartFrame(const lmms::f_cnt_t s); - void setEndFrame(const lmms::f_cnt_t e); - void setAmplification(float a); - void setReversed(bool on); - void sampleRateChanged(); + static auto emptyBuffer() -> std::shared_ptr; private: - static sample_rate_t audioEngineSampleRate(); - - void update(bool keepSettings = false); - - void convertIntToFloat(int_sample_t * & ibuf, f_cnt_t frames, int channels); - void directFloatWrite(sample_t * & fbuf, f_cnt_t frames, int channels); - - f_cnt_t decodeSampleSF( - QString fileName, - sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate - ); -#ifdef LMMS_HAVE_OGGVORBIS - f_cnt_t decodeSampleOGGVorbis( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate - ); -#endif - f_cnt_t decodeSampleDS( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate - ); - + std::vector m_data; QString m_audioFile; - sampleFrame * m_origData; - f_cnt_t m_origFrames; - sampleFrame * m_data; - mutable QReadWriteLock m_varLock; - f_cnt_t m_frames; - f_cnt_t m_startFrame; - f_cnt_t m_endFrame; - f_cnt_t m_loopStartFrame; - f_cnt_t m_loopEndFrame; - float m_amplification; - bool m_reversed; - float m_frequency; - sample_rate_t m_sampleRate; - - sampleFrame * getSampleFragment( - f_cnt_t index, - f_cnt_t frames, - LoopMode loopMode, - sampleFrame * * tmp, - bool * backwards, - f_cnt_t loopStart, - f_cnt_t loopEnd, - f_cnt_t end - ) const; - - f_cnt_t getLoopedIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const; - f_cnt_t getPingPongIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const; - - -signals: - void sampleUpdated(); - -} ; + sample_rate_t m_sampleRate = Engine::audioEngine()->processingSampleRate(); +}; } // namespace lmms diff --git a/include/SampleClip.h b/include/SampleClip.h index 5246787bd..da11996b1 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -25,7 +25,9 @@ #ifndef LMMS_SAMPLE_CLIP_H #define LMMS_SAMPLE_CLIP_H +#include #include "Clip.h" +#include "Sample.h" namespace lmms { @@ -45,14 +47,15 @@ class SampleClip : public Clip Q_OBJECT mapPropertyFromModel(bool,isRecord,setRecord,m_recordModel); public: - SampleClip( Track * _track ); + SampleClip(Track* track, Sample sample, bool isPlaying); + SampleClip(Track* track); SampleClip( const SampleClip& orig ); ~SampleClip() override; SampleClip& operator=( const SampleClip& that ) = delete; void changeLength( const TimePos & _length ) override; - const QString & sampleFile() const; + const QString& sampleFile() const; void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; void loadSettings( const QDomElement & _this ) override; @@ -61,9 +64,9 @@ public: return "sampleclip"; } - SampleBuffer* sampleBuffer() + Sample& sample() { - return m_sampleBuffer; + return m_sample; } TimePos sampleLength() const; @@ -74,10 +77,10 @@ public: bool isPlaying() const; void setIsPlaying(bool isPlaying); + void setSampleBuffer(std::shared_ptr sb); public slots: - void setSampleBuffer( lmms::SampleBuffer* sb ); - void setSampleFile( const QString & sf ); + void setSampleFile(const QString& sf); void updateLength(); void toggleRecord(); void playbackPositionChanged(); @@ -85,7 +88,7 @@ public slots: private: - SampleBuffer* m_sampleBuffer; + Sample m_sample; BoolModel m_recordModel; bool m_isPlaying; diff --git a/include/SampleDecoder.h b/include/SampleDecoder.h new file mode 100644 index 000000000..d7ce076dd --- /dev/null +++ b/include/SampleDecoder.h @@ -0,0 +1,57 @@ +/* + * SampleDecoder.h - Decodes audio files in various formats + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SAMPLE_DECODER_H +#define LMMS_SAMPLE_DECODER_H + +#include +#include +#include +#include +#include + +#include "lmms_basics.h" + +namespace lmms { +class SampleDecoder +{ +public: + struct Result + { + std::vector data; + int sampleRate; + }; + + struct AudioType + { + std::string name; + std::string extension; + }; + + static auto decode(const QString& audioFile) -> std::optional; + static auto supportedAudioTypes() -> const std::vector&; +}; +} // namespace lmms + +#endif // LMMS_SAMPLE_DECODER_H diff --git a/include/SampleLoader.h b/include/SampleLoader.h new file mode 100644 index 000000000..7dbdbdc33 --- /dev/null +++ b/include/SampleLoader.h @@ -0,0 +1,48 @@ +/* + * SampleLoader.h - Load audio and waveform files + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SAMPLE_LOADER_H +#define LMMS_GUI_SAMPLE_LOADER_H + +#include +#include + +#include "SampleBuffer.h" +#include "lmms_export.h" + +namespace lmms::gui { +class LMMS_EXPORT SampleLoader +{ +public: + static QString openAudioFile(const QString& previousFile = ""); + static QString openWaveformFile(const QString& previousFile = ""); + static std::shared_ptr createBufferFromFile(const QString& filePath); + static std::shared_ptr createBufferFromBase64( + const QString& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); +private: + static void displayError(const QString& message); +}; +} // namespace lmms::gui + +#endif // LMMS_GUI_SAMPLE_LOADER_H diff --git a/include/SamplePlayHandle.h b/include/SamplePlayHandle.h index 31b4f0bd5..280010b06 100644 --- a/include/SamplePlayHandle.h +++ b/include/SamplePlayHandle.h @@ -26,6 +26,7 @@ #ifndef LMMS_SAMPLE_PLAY_HANDLE_H #define LMMS_SAMPLE_PLAY_HANDLE_H +#include "Sample.h" #include "SampleBuffer.h" #include "AutomatableModel.h" #include "PlayHandle.h" @@ -43,7 +44,7 @@ class AudioPort; class LMMS_EXPORT SamplePlayHandle : public PlayHandle { public: - SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort = true ); + SamplePlayHandle(Sample* sample, bool ownAudioPort = true); SamplePlayHandle( const QString& sampleFile ); SamplePlayHandle( SampleClip* clip ); ~SamplePlayHandle() override; @@ -81,11 +82,11 @@ public: private: - SampleBuffer * m_sampleBuffer; + Sample* m_sample; bool m_doneMayReturnTrue; f_cnt_t m_frame; - SampleBuffer::handleState m_state; + Sample::PlaybackState m_state; const bool m_ownAudioPort; diff --git a/include/SampleRecordHandle.h b/include/SampleRecordHandle.h index de1ca19ba..df2d7c772 100644 --- a/include/SampleRecordHandle.h +++ b/include/SampleRecordHandle.h @@ -27,6 +27,7 @@ #include #include +#include #include "PlayHandle.h" #include "TimePos.h" @@ -53,7 +54,7 @@ public: bool isFromTrack( const Track * _track ) const override; f_cnt_t framesRecorded() const; - void createSampleBuffer( SampleBuffer * * _sample_buf ); + std::shared_ptr createSampleBuffer(); private: diff --git a/include/SampleWaveform.h b/include/SampleWaveform.h new file mode 100644 index 000000000..692c1f9cb --- /dev/null +++ b/include/SampleWaveform.h @@ -0,0 +1,41 @@ +/* + * SampleWaveform.h + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SAMPLE_WAVEFORM_H +#define LMMS_GUI_SAMPLE_WAVEFORM_H + +#include + +#include "Sample.h" +#include "lmms_export.h" + +namespace lmms::gui { +class LMMS_EXPORT SampleWaveform +{ +public: + static void visualize(const Sample& sample, QPainter& p, const QRect& dr, int fromFrame = 0, int toFrame = 0); +}; +} // namespace lmms::gui + +#endif // LMMS_GUI_SAMPLE_WAVEFORM_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 459ff566c..ce3c9c07b 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -29,7 +29,6 @@ #include #include #include - #include #include "AudioEngine.h" @@ -42,6 +41,8 @@ #include "NotePlayHandle.h" #include "PathUtil.h" #include "PixmapButton.h" +#include "SampleLoader.h" +#include "SampleWaveform.h" #include "Song.h" #include "StringPairDrag.h" #include "Clipboard.h" @@ -83,7 +84,6 @@ Plugin::Descriptor PLUGIN_EXPORT audiofileprocessor_plugin_descriptor = AudioFileProcessor::AudioFileProcessor( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &audiofileprocessor_plugin_descriptor ), - m_sampleBuffer(), m_ampModel( 100, 0, 500, 1, this, tr( "Amplify" ) ), m_startPointModel( 0, 0, 1, 0.0000001f, this, tr( "Start of sample" ) ), m_endPointModel( 1, 0, 1, 0.0000001f, this, tr( "End of sample" ) ), @@ -131,18 +131,18 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, // played. if( m_stutterModel.value() == true && _n->frequency() < 20.0 ) { - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; return; } if( !_n->m_pluginData ) { - if( m_stutterModel.value() == true && m_nextPlayStartPoint >= m_sampleBuffer.endFrame() ) + if (m_stutterModel.value() == true && m_nextPlayStartPoint >= m_sample.endFrame()) { // Restart playing the note if in stutter mode, not in loop mode, // and we're at the end of the sample. - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; } // set interpolation mode for libsamplerate @@ -159,25 +159,25 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, srcmode = SRC_SINC_MEDIUM_QUALITY; break; } - _n->m_pluginData = new handleState( _n->hasDetuningInfo(), srcmode ); - ((handleState *)_n->m_pluginData)->setFrameIndex( m_nextPlayStartPoint ); - ((handleState *)_n->m_pluginData)->setBackwards( m_nextPlayBackwards ); + _n->m_pluginData = new Sample::PlaybackState(_n->hasDetuningInfo(), srcmode); + static_cast(_n->m_pluginData)->setFrameIndex(m_nextPlayStartPoint); + static_cast(_n->m_pluginData)->setBackwards(m_nextPlayBackwards); // debug code -/* qDebug( "frames %d", m_sampleBuffer.frames() ); - qDebug( "startframe %d", m_sampleBuffer.startFrame() ); +/* qDebug( "frames %d", m_sample->frames() ); + qDebug( "startframe %d", m_sample->startFrame() ); qDebug( "nextPlayStartPoint %d", m_nextPlayStartPoint );*/ } if( ! _n->isFinished() ) { - if( m_sampleBuffer.play( _working_buffer + offset, - (handleState *)_n->m_pluginData, + if (m_sample.play(_working_buffer + offset, + static_cast(_n->m_pluginData), frames, _n->frequency(), - static_cast( m_loopModel.value() ) ) ) + static_cast(m_loopModel.value()))) { applyRelease( _working_buffer, _n ); - emit isPlaying( ((handleState *)_n->m_pluginData)->frameIndex() ); + emit isPlaying(static_cast(_n->m_pluginData)->frameIndex()); } else { @@ -191,8 +191,8 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, } if( m_stutterModel.value() == true ) { - m_nextPlayStartPoint = ((handleState *)_n->m_pluginData)->frameIndex(); - m_nextPlayBackwards = ((handleState *)_n->m_pluginData)->isBackwards(); + m_nextPlayStartPoint = static_cast(_n->m_pluginData)->frameIndex(); + m_nextPlayBackwards = static_cast(_n->m_pluginData)->backwards(); } } @@ -201,7 +201,7 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, void AudioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) { - delete (handleState *)_n->m_pluginData; + delete static_cast(_n->m_pluginData); } @@ -209,11 +209,10 @@ void AudioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) void AudioFileProcessor::saveSettings(QDomDocument& doc, QDomElement& elem) { - elem.setAttribute("src", m_sampleBuffer.audioFile()); - if (m_sampleBuffer.audioFile().isEmpty()) + elem.setAttribute("src", m_sample.sampleFile()); + if (m_sample.sampleFile().isEmpty()) { - QString s; - elem.setAttribute("sampledata", m_sampleBuffer.toBase64(s)); + elem.setAttribute("sampledata", m_sample.toBase64()); } m_reverseModel.saveSettings(doc, elem, "reversed"); m_loopModel.saveSettings(doc, elem, "looped"); @@ -230,20 +229,17 @@ void AudioFileProcessor::saveSettings(QDomDocument& doc, QDomElement& elem) void AudioFileProcessor::loadSettings(const QDomElement& elem) { - if (!elem.attribute("src").isEmpty()) + if (auto srcFile = elem.attribute("src"); !srcFile.isEmpty()) { - setAudioFile(elem.attribute("src"), false); - - QString absolutePath = PathUtil::toAbsolute(m_sampleBuffer.audioFile()); - if (!QFileInfo(absolutePath).exists()) + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { - QString message = tr("Sample not found: %1").arg(m_sampleBuffer.audioFile()); - Engine::getSong()->collectError(message); + setAudioFile(srcFile, false); } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), srcFile)); } } - else if (!elem.attribute("sampledata").isEmpty()) + else if (auto sampleData = elem.attribute("sampledata"); !sampleData.isEmpty()) { - m_sampleBuffer.loadFromBase64(elem.attribute("srcdata")); + m_sample = Sample(gui::SampleLoader::createBufferFromBase64(sampleData)); } m_loopModel.loadSettings(elem, "looped"); @@ -274,6 +270,7 @@ void AudioFileProcessor::loadSettings(const QDomElement& elem) } pointChanged(); + emit sampleUpdated(); } @@ -298,7 +295,7 @@ QString AudioFileProcessor::nodeName() const auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> int { // If we can play indefinitely, use the default beat note duration - if (static_cast(m_loopModel.value()) != SampleBuffer::LoopMode::Off) { return 0; } + if (static_cast(m_loopModel.value()) != Sample::Loop::Off) { return 0; } // Otherwise, use the remaining sample duration const auto baseFreq = instrumentTrack()->baseFreq(); @@ -306,10 +303,10 @@ auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> int * Engine::audioEngine()->processingSampleRate() / Engine::audioEngine()->baseSampleRate(); - const auto startFrame = m_nextPlayStartPoint >= m_sampleBuffer.endFrame() - ? m_sampleBuffer.startFrame() + const auto startFrame = m_nextPlayStartPoint >= m_sample.endFrame() + ? m_sample.startFrame() : m_nextPlayStartPoint; - const auto duration = m_sampleBuffer.endFrame() - startFrame; + const auto duration = m_sample.endFrame() - startFrame; return static_cast(std::floor(duration * freqFactor)); } @@ -322,25 +319,22 @@ gui::PluginView* AudioFileProcessor::instantiateView( QWidget * _parent ) return new gui::AudioFileProcessorView( this, _parent ); } - - - -void AudioFileProcessor::setAudioFile( const QString & _audio_file, - bool _rename ) +void AudioFileProcessor::setAudioFile(const QString& _audio_file, bool _rename) { // is current channel-name equal to previous-filename?? if( _rename && ( instrumentTrack()->name() == - QFileInfo( m_sampleBuffer.audioFile() ).fileName() || - m_sampleBuffer.audioFile().isEmpty() ) ) + QFileInfo(m_sample.sampleFile()).fileName() || + m_sample.sampleFile().isEmpty())) { // then set it to new one instrumentTrack()->setName( PathUtil::cleanName( _audio_file ) ); } // else we don't touch the track-name, because the user named it self - m_sampleBuffer.setAudioFile( _audio_file ); + m_sample = Sample(gui::SampleLoader::createBufferFromFile(_audio_file)); loopPointChanged(); + emit sampleUpdated(); } @@ -348,9 +342,10 @@ void AudioFileProcessor::setAudioFile( const QString & _audio_file, void AudioFileProcessor::reverseModelChanged() { - m_sampleBuffer.setReversed( m_reverseModel.value() ); - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_sample.setReversed(m_reverseModel.value()); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; + emit sampleUpdated(); } @@ -358,13 +353,14 @@ void AudioFileProcessor::reverseModelChanged() void AudioFileProcessor::ampModelChanged() { - m_sampleBuffer.setAmplification( m_ampModel.value() / 100.0f ); + m_sample.setAmplification(m_ampModel.value() / 100.0f); + emit sampleUpdated(); } void AudioFileProcessor::stutterModelChanged() { - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; } @@ -433,14 +429,14 @@ void AudioFileProcessor::loopPointChanged() void AudioFileProcessor::pointChanged() { - const auto f_start = static_cast(m_startPointModel.value() * m_sampleBuffer.frames()); - const auto f_end = static_cast(m_endPointModel.value() * m_sampleBuffer.frames()); - const auto f_loop = static_cast(m_loopPointModel.value() * m_sampleBuffer.frames()); + const auto f_start = static_cast(m_startPointModel.value() * m_sample.sampleSize()); + const auto f_end = static_cast(m_endPointModel.value() * m_sample.sampleSize()); + const auto f_loop = static_cast(m_loopPointModel.value() * m_sample.sampleSize()); m_nextPlayStartPoint = f_start; m_nextPlayBackwards = false; - m_sampleBuffer.setAllPointFrames( f_start, f_end, f_loop, f_end ); + m_sample.setAllPointFrames(f_start, f_end, f_loop, f_end); emit dataChanged(); } @@ -601,7 +597,7 @@ void AudioFileProcessorView::newWaveView() delete m_waveView; m_waveView = 0; } - m_waveView = new AudioFileProcessorWaveView( this, 245, 75, castModel()->m_sampleBuffer ); + m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->m_sample); m_waveView->move( 2, 172 ); m_waveView->setKnobs( dynamic_cast( m_startKnob ), @@ -648,7 +644,8 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) auto a = castModel(); QString file_name = ""; - int idx = a->m_sampleBuffer.audioFile().length(); + + int idx = a->m_sample.sampleFile().length(); p.setFont( pointSize<8>( font() ) ); @@ -659,7 +656,7 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) while( idx > 0 && fm.size( Qt::TextSingleLine, file_name + "..." ).width() < 210 ) { - file_name = a->m_sampleBuffer.audioFile()[--idx] + file_name; + file_name = a->m_sample.sampleFile()[--idx] + file_name; } if( idx > 0 ) @@ -687,7 +684,7 @@ void AudioFileProcessorView::sampleUpdated() void AudioFileProcessorView::openAudioFile() { - QString af = castModel()->m_sampleBuffer.openAudioFile(); + QString af = SampleLoader::openAudioFile(); if (af.isEmpty()) { return; } castModel()->setAudioFile(af); @@ -701,8 +698,7 @@ void AudioFileProcessorView::openAudioFile() void AudioFileProcessorView::modelChanged() { auto a = castModel(); - connect( &a->m_sampleBuffer, SIGNAL( sampleUpdated() ), - this, SLOT( sampleUpdated() ) ); + connect(a, &AudioFileProcessor::sampleUpdated, this, &AudioFileProcessorView::sampleUpdated); m_ampKnob->setModel( &a->m_ampModel ); m_startKnob->setModel( &a->m_startPointModel ); m_endKnob->setModel( &a->m_endPointModel ); @@ -719,20 +715,20 @@ void AudioFileProcessorView::modelChanged() void AudioFileProcessorWaveView::updateSampleRange() { - if( m_sampleBuffer.frames() > 1 ) + if (m_sample->sampleSize() > 1) { - const f_cnt_t marging = ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * 0.1; - m_from = qMax( 0, m_sampleBuffer.startFrame() - marging ); - m_to = qMin( m_sampleBuffer.endFrame() + marging, m_sampleBuffer.frames() ); + const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; + m_from = qMax(0, m_sample->startFrame() - marging); + m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); } } -AudioFileProcessorWaveView::AudioFileProcessorWaveView( QWidget * _parent, int _w, int _h, SampleBuffer& buf ) : +AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf) : QWidget( _parent ), - m_sampleBuffer( buf ), + m_sample(buf), m_graph( QPixmap( _w - 2 * s_padding, _h - 2 * s_padding ) ), m_from( 0 ), - m_to( m_sampleBuffer.frames() ), + m_to(m_sample->sampleSize()), m_last_from( 0 ), m_last_to( 0 ), m_last_amp( 0 ), @@ -880,11 +876,11 @@ void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) const QRect graph_rect( s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding ); const f_cnt_t frames = m_to - m_from; - m_startFrameX = graph_rect.x() + ( m_sampleBuffer.startFrame() - m_from ) * + m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * double( graph_rect.width() ) / frames; - m_endFrameX = graph_rect.x() + ( m_sampleBuffer.endFrame() - m_from ) * + m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * double( graph_rect.width() ) / frames; - m_loopFrameX = graph_rect.x() + ( m_sampleBuffer.loopStartFrame() - m_from ) * + m_loopFrameX = graph_rect.x() + (m_sample->loopStartFrame() - m_from) * double( graph_rect.width() ) / frames; const int played_width_px = ( m_framesPlayed - m_from ) * double( graph_rect.width() ) / frames; @@ -959,7 +955,7 @@ void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) p.setFont( pointSize<8>( font() ) ); QString length_text; - const int length = m_sampleBuffer.sampleLength(); + const int length = m_sample->sampleDuration().count(); if( length > 20000 ) { @@ -988,42 +984,37 @@ void AudioFileProcessorWaveView::updateGraph() { if( m_to == 1 ) { - m_to = m_sampleBuffer.frames() * 0.7; + m_to = m_sample->sampleSize() * 0.7; slideSamplePointToFrames( Point::End, m_to * 0.7 ); } - if( m_from > m_sampleBuffer.startFrame() ) + if (m_from > m_sample->startFrame()) { - m_from = m_sampleBuffer.startFrame(); + m_from = m_sample->startFrame(); } - if( m_to < m_sampleBuffer.endFrame() ) + if (m_to < m_sample->endFrame()) { - m_to = m_sampleBuffer.endFrame(); + m_to = m_sample->endFrame(); } - if( m_sampleBuffer.reversed() != m_reversed ) + if (m_sample->reversed() != m_reversed) { reverse(); } - else if( m_last_from == m_from && m_last_to == m_to && m_sampleBuffer.amplification() == m_last_amp ) + else if (m_last_from == m_from && m_last_to == m_to && m_sample->amplification() == m_last_amp) { return; } m_last_from = m_from; m_last_to = m_to; - m_last_amp = m_sampleBuffer.amplification(); + m_last_amp = m_sample->amplification(); m_graph.fill( Qt::transparent ); QPainter p( &m_graph ); p.setPen( QColor( 255, 255, 255 ) ); - - m_sampleBuffer.visualize( - p, - QRect( 0, 0, m_graph.width(), m_graph.height() ), - m_from, m_to - ); + SampleWaveform::visualize(*m_sample, p, QRect(0, 0, m_graph.width(), m_graph.height()), m_from, m_to); } @@ -1031,9 +1022,9 @@ void AudioFileProcessorWaveView::updateGraph() void AudioFileProcessorWaveView::zoom( const bool _out ) { - const f_cnt_t start = m_sampleBuffer.startFrame(); - const f_cnt_t end = m_sampleBuffer.endFrame(); - const f_cnt_t frames = m_sampleBuffer.frames(); + const f_cnt_t start = m_sample->startFrame(); + const f_cnt_t end = m_sample->endFrame(); + const f_cnt_t frames = m_sample->sampleSize(); const f_cnt_t d_from = start - m_from; const f_cnt_t d_to = m_to - end; @@ -1066,7 +1057,7 @@ void AudioFileProcessorWaveView::zoom( const bool _out ) ); } - if( double( new_to - new_from ) / m_sampleBuffer.sampleRate() > 0.05 ) + if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) { m_from = new_from; m_to = new_to; @@ -1085,8 +1076,8 @@ void AudioFileProcessorWaveView::slide( int _px ) step = -step; } - f_cnt_t step_from = qBound( 0, m_from + step, m_sampleBuffer.frames() ) - m_from; - f_cnt_t step_to = qBound( m_from + 1, m_to + step, m_sampleBuffer.frames() ) - m_to; + f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; + f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; step = qAbs( step_from ) < qAbs( step_to ) ? step_from : step_to; @@ -1147,7 +1138,7 @@ void AudioFileProcessorWaveView::slideSamplePointByFrames( Point _point, f_cnt_t } else { - const double v = static_cast( _frames ) / m_sampleBuffer.frames(); + const double v = static_cast(_frames) / m_sample->sampleSize(); if( _slide_to ) { a_knob->slideTo( v ); @@ -1164,11 +1155,11 @@ void AudioFileProcessorWaveView::slideSamplePointByFrames( Point _point, f_cnt_t void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) { - if( m_sampleBuffer.frames() <= 1 ) + if (m_sample->sampleSize() <= 1) { return; } - const double v = static_cast( _frames ) / m_sampleBuffer.frames(); + const double v = static_cast( _frames ) / m_sample->sampleSize(); // update knobs in the right order // to avoid them clamping each other if (v < 0) @@ -1191,14 +1182,14 @@ void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) void AudioFileProcessorWaveView::reverse() { slideSampleByFrames( - m_sampleBuffer.frames() - - m_sampleBuffer.endFrame() - - m_sampleBuffer.startFrame() + m_sample->sampleSize() + - m_sample->endFrame() + - m_sample->startFrame() ); const f_cnt_t from = m_from; - m_from = m_sampleBuffer.frames() - m_to; - m_to = m_sampleBuffer.frames() - from; + m_from = m_sample->sampleSize() - m_to; + m_to = m_sample->sampleSize() - from; m_reversed = ! m_reversed; } @@ -1240,8 +1231,7 @@ void AudioFileProcessorWaveView::knob::slideTo( double _v, bool _check_bound ) float AudioFileProcessorWaveView::knob::getValue( const QPoint & _p ) { const double dec_fact = ! m_waveView ? 1 : - double( m_waveView->m_to - m_waveView->m_from ) - / m_waveView->m_sampleBuffer.frames(); + static_cast(m_waveView->m_to - m_waveView->m_from) / m_waveView->m_sample->sampleSize(); const float inc = Knob::getValue( _p ) * dec_fact; return inc; @@ -1262,12 +1252,12 @@ bool AudioFileProcessorWaveView::knob::checkBound( double _v ) const return false; const double d1 = qAbs( m_relatedKnob->model()->value() - model()->value() ) - * ( m_waveView->m_sampleBuffer.frames() ) - / m_waveView->m_sampleBuffer.sampleRate(); + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); const double d2 = qAbs( m_relatedKnob->model()->value() - _v ) - * ( m_waveView->m_sampleBuffer.frames() ) - / m_waveView->m_sampleBuffer.sampleRate(); + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); return d1 < d2 || d2 > 0.005; } diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 4a5f21cc0..80a40c56f 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -31,6 +31,7 @@ #include "ComboBoxModel.h" #include "Instrument.h" #include "InstrumentView.h" +#include "Sample.h" #include "SampleBuffer.h" #include "Knob.h" @@ -78,8 +79,7 @@ public: public slots: - void setAudioFile( const QString & _audio_file, bool _rename = true ); - + void setAudioFile(const QString& _audio_file, bool _rename = true); private slots: void reverseModelChanged(); @@ -93,12 +93,10 @@ private slots: signals: void isPlaying( lmms::f_cnt_t _current_frame ); - + void sampleUpdated(); private: - using handleState = SampleBuffer::handleState; - - SampleBuffer m_sampleBuffer; + Sample m_sample; FloatModel m_ampModel; FloatModel m_startPointModel; @@ -246,7 +244,7 @@ private: SampleLoop } ; - SampleBuffer& m_sampleBuffer; + Sample* m_sample; QPixmap m_graph; f_cnt_t m_from; f_cnt_t m_to; @@ -266,8 +264,10 @@ private: f_cnt_t m_framesPlayed; bool m_animation; + friend class AudioFileProcessorView; + public: - AudioFileProcessorWaveView( QWidget * _parent, int _w, int _h, SampleBuffer& buf ); + AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf); void setKnobs(knob *_start, knob *_end, knob *_loop ); diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 0713d3100..2d67f0ddf 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -46,7 +46,7 @@ #include "Knob.h" #include "NotePlayHandle.h" #include "PathUtil.h" -#include "SampleBuffer.h" +#include "Sample.h" #include "Song.h" #include "PatchesDialog.h" @@ -437,7 +437,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) if (sample.region->PitchTrack == true) { freq_factor *= sample.freqFactor; } // We need a bit of margin so we don't get glitching - samples = frames / freq_factor + MARGIN[m_interpolation]; + samples = frames / freq_factor + Sample::s_interpolationMargins[m_interpolation]; } // Load this note's data diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index 24c54d66b..e525498a5 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -153,8 +153,8 @@ void PatmanInstrument::playNote( NotePlayHandle * _n, float play_freq = hdata->tuned ? _n->frequency() : hdata->sample->frequency(); - if( hdata->sample->play( _working_buffer + offset, hdata->state, frames, - play_freq, m_loopedModel.value() ? SampleBuffer::LoopMode::On : SampleBuffer::LoopMode::Off ) ) + if (hdata->sample->play(_working_buffer + offset, hdata->state, frames, + play_freq, m_loopedModel.value() ? Sample::Loop::On : Sample::Loop::Off)) { applyRelease( _working_buffer, _n ); } @@ -170,7 +170,6 @@ void PatmanInstrument::playNote( NotePlayHandle * _n, void PatmanInstrument::deleteNotePluginData( NotePlayHandle * _n ) { auto hdata = (handle_data*)_n->m_pluginData; - sharedObject::unref( hdata->sample ); delete hdata->state; delete hdata; } @@ -356,9 +355,8 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( } } - auto psample = new SampleBuffer(data, frames); - psample->setFrequency( root_freq / 1000.0f ); - psample->setSampleRate( sample_rate ); + auto psample = std::make_shared(data, frames, sample_rate); + psample->setFrequency(root_freq / 1000.0f); if( modes & MODES_LOOPING ) { @@ -366,7 +364,7 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( psample->setLoopEndFrame( loop_end ); } - m_patchSamples.push_back( psample ); + m_patchSamples.push_back(psample); delete[] wave_samples; delete[] data; @@ -382,7 +380,6 @@ void PatmanInstrument::unloadCurrentPatch() { while( !m_patchSamples.empty() ) { - sharedObject::unref( m_patchSamples.back() ); m_patchSamples.pop_back(); } } @@ -395,7 +392,7 @@ void PatmanInstrument::selectSample( NotePlayHandle * _n ) const float freq = _n->frequency(); float min_dist = HUGE_VALF; - SampleBuffer* sample = nullptr; + std::shared_ptr sample = nullptr; for (const auto& patchSample : m_patchSamples) { @@ -412,15 +409,8 @@ void PatmanInstrument::selectSample( NotePlayHandle * _n ) auto hdata = new handle_data; hdata->tuned = m_tunedModel.value(); - if( sample ) - { - hdata->sample = sharedObject::ref( sample ); - } - else - { - hdata->sample = new SampleBuffer( nullptr, 0 ); - } - hdata->state = new SampleBuffer::handleState( _n->hasDetuningInfo() ); + hdata->sample = sample ? sample : std::make_shared(); + hdata->state = new Sample::PlaybackState(_n->hasDetuningInfo()); _n->m_pluginData = hdata; } diff --git a/plugins/Patman/Patman.h b/plugins/Patman/Patman.h index 3a15db5f3..8d2c8c657 100644 --- a/plugins/Patman/Patman.h +++ b/plugins/Patman/Patman.h @@ -28,6 +28,7 @@ #include "Instrument.h" #include "InstrumentView.h" +#include "Sample.h" #include "SampleBuffer.h" #include "AutomatableModel.h" #include "MemoryManager.h" @@ -87,13 +88,13 @@ private: struct handle_data { MM_OPERATORS - SampleBuffer::handleState* state; + Sample::PlaybackState* state; bool tuned; - SampleBuffer* sample; + std::shared_ptr sample; }; QString m_patchFile; - QVector m_patchSamples; + QVector> m_patchSamples; BoolModel m_loopedModel; BoolModel m_tunedModel; diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 2918265ce..c9e75df60 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -31,6 +31,7 @@ #include "Engine.h" #include "InstrumentTrack.h" #include "PathUtil.h" +#include "SampleLoader.h" #include "Song.h" #include "embed.h" #include "lmms_constants.h" @@ -76,7 +77,7 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { - if (m_originalSample.frames() <= 1) { return; } + if (m_originalSample.sampleSize() <= 1) { return; } int noteIndex = handle->key() - m_parentTrack->baseNote(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); @@ -115,24 +116,24 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteLeft > 0) { - int noteFrame = noteDone * m_originalSample.frames(); + int noteFrame = noteDone * m_originalSample.sampleSize(); SRC_STATE* resampleState = playbackState->resamplingState(); SRC_DATA resampleData; resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); resampleData.data_out = (workingBuffer + offset)->data(); - resampleData.input_frames = noteLeft * m_originalSample.frames(); + resampleData.input_frames = noteLeft * m_originalSample.sampleSize(); resampleData.output_frames = frames; resampleData.src_ratio = speedRatio; src_process(resampleState, &resampleData); - float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); + float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.sampleSize(); playbackState->setNoteDone(nextNoteDone); // exponential fade out, applyRelease() not used since it extends the note length int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate(); - int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio; + int noteFramesLeft = noteLeft * m_originalSample.sampleSize() * speedRatio; for (int i = 0; i < frames; i++) { float fadeValue = static_cast(noteFramesLeft - i) / fadeOutFrames; @@ -160,7 +161,7 @@ void SlicerT::deleteNotePluginData(NotePlayHandle* handle) // http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf void SlicerT::findSlices() { - if (m_originalSample.frames() <= 1) { return; } + if (m_originalSample.sampleSize() <= 1) { return; } m_slicePoints = {}; const int windowSize = 512; @@ -170,8 +171,8 @@ void SlicerT::findSlices() int minDist = sampleRate * minBeatLength; float maxMag = -1; - std::vector singleChannel(m_originalSample.frames(), 0); - for (int i = 0; i < m_originalSample.frames(); i++) + std::vector singleChannel(m_originalSample.sampleSize(), 0); + for (int i = 0; i < m_originalSample.sampleSize(); i++) { singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; maxMag = std::max(maxMag, singleChannel[i]); @@ -232,7 +233,7 @@ void SlicerT::findSlices() spectralFlux = 1E-10; // again for no divison by zero } - m_slicePoints.push_back(m_originalSample.frames()); + m_slicePoints.push_back(m_originalSample.sampleSize()); for (float& sliceValue : m_slicePoints) { @@ -255,7 +256,7 @@ void SlicerT::findSlices() for (float& sliceIndex : m_slicePoints) { - sliceIndex /= m_originalSample.frames(); + sliceIndex /= m_originalSample.sampleSize(); } m_slicePoints[0] = 0; @@ -268,10 +269,10 @@ void SlicerT::findSlices() // and lies in the 100 - 200 bpm range void SlicerT::findBPM() { - if (m_originalSample.frames() <= 1) { return; } + if (m_originalSample.sampleSize() <= 1) { return; } float sampleRate = m_originalSample.sampleRate(); - float totalFrames = m_originalSample.frames(); + float totalFrames = m_originalSample.sampleSize(); float sampleLength = totalFrames / sampleRate; float bpmEstimate = 240.0f / sampleLength; @@ -295,7 +296,7 @@ std::vector SlicerT::getMidi() std::vector outputNotes; float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); - float outFrames = m_originalSample.frames() * speedRatio; + float outFrames = m_originalSample.sampleSize() * speedRatio; float framesPerTick = Engine::framesPerTick(); float totalTicks = outFrames / framesPerTick; @@ -320,7 +321,7 @@ std::vector SlicerT::getMidi() void SlicerT::updateFile(QString file) { - m_originalSample.setAudioFile(file); + if (auto buffer = gui::SampleLoader::createBufferFromFile(file)) { m_originalSample = Sample(std::move(buffer)); } findBPM(); findSlices(); @@ -336,11 +337,10 @@ void SlicerT::updateSlices() void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) { element.setAttribute("version", "1"); - element.setAttribute("src", m_originalSample.audioFile()); - if (m_originalSample.audioFile().isEmpty()) + element.setAttribute("src", m_originalSample.sampleFile()); + if (m_originalSample.sampleFile().isEmpty()) { - QString s; - element.setAttribute("sampledata", m_originalSample.toBase64(s)); + element.setAttribute("sampledata", m_originalSample.toBase64()); } element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); @@ -357,20 +357,23 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) void SlicerT::loadSettings(const QDomElement& element) { - if (!element.attribute("src").isEmpty()) + if (auto srcFile = element.attribute("src"); !srcFile.isEmpty()) { - m_originalSample.setAudioFile(element.attribute("src")); - - QString absolutePath = PathUtil::toAbsolute(m_originalSample.audioFile()); - if (!QFileInfo(absolutePath).exists()) + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { - QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile()); + auto buffer = gui::SampleLoader::createBufferFromFile(srcFile); + m_originalSample = Sample(std::move(buffer)); + } + else + { + QString message = tr("Sample not found: %1").arg(srcFile); Engine::getSong()->collectError(message); } } - else if (!element.attribute("sampledata").isEmpty()) + else if (auto sampleData = element.attribute("sampledata"); !sampleData.isEmpty()) { - m_originalSample.loadFromBase64(element.attribute("srcdata")); + auto buffer = gui::SampleLoader::createBufferFromBase64(sampleData); + m_originalSample = Sample(std::move(buffer)); } if (!element.attribute("totalSlices").isEmpty()) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 8671eecd1..010985dfc 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -33,6 +33,7 @@ #include "Instrument.h" #include "InstrumentView.h" #include "Note.h" +#include "Sample.h" #include "SampleBuffer.h" #include "SlicerTView.h" #include "lmms_basics.h" @@ -95,7 +96,7 @@ private: ComboBoxModel m_sliceSnap; BoolModel m_enableSync; - SampleBuffer m_originalSample; + Sample m_originalSample; std::vector m_slicePoints; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 833d4b434..bbdb53ccb 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -31,6 +31,7 @@ #include "DataFile.h" #include "Engine.h" #include "InstrumentTrack.h" +#include "SampleLoader.h" #include "SlicerT.h" #include "Song.h" #include "StringPairDrag.h" @@ -108,7 +109,7 @@ Knob* SlicerTView::createStyledKnob() void SlicerTView::exportMidi() { using namespace Clipboard; - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } DataFile dataFile(DataFile::Type::ClipboardData); QDomElement noteList = dataFile.createElement("note-list"); @@ -129,7 +130,7 @@ void SlicerTView::exportMidi() void SlicerTView::openFiles() { - QString audioFile = m_slicerTParent->m_originalSample.openAudioFile(); + const auto audioFile = SampleLoader::openAudioFile(); if (audioFile.isEmpty()) { return; } m_slicerTParent->updateFile(audioFile); } diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 6685f4f8c..66747036a 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -26,6 +26,7 @@ #include +#include "SampleWaveform.h" #include "SlicerT.h" #include "SlicerTView.h" #include "embed.h" @@ -84,12 +85,13 @@ SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instr void SlicerTWaveform::drawSeekerWaveform() { m_seekerWaveform.fill(s_waveformBgColor); - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_seekerWaveform); brush.setPen(s_waveformColor); - m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), - 0, m_slicerTParent->m_originalSample.frames()); + SampleWaveform::visualize(m_slicerTParent->m_originalSample, brush, + QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, + m_slicerTParent->m_originalSample.sampleSize()); // increase brightness in inner color QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); @@ -100,7 +102,7 @@ void SlicerTWaveform::drawSeekerWaveform() void SlicerTWaveform::drawSeeker() { m_seeker.fill(s_emptyColor); - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_seeker); brush.setPen(s_sliceColor); @@ -134,16 +136,17 @@ void SlicerTWaveform::drawSeeker() void SlicerTWaveform::drawEditorWaveform() { m_editorWaveform.fill(s_emptyColor); - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_editorWaveform); - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.sampleSize(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.sampleSize(); brush.setPen(s_waveformColor); float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; - m_slicerTParent->m_originalSample.visualize( - brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); + + SampleWaveform::visualize(m_slicerTParent->m_originalSample, brush, + QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); // increase brightness in inner color QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); @@ -157,7 +160,7 @@ void SlicerTWaveform::drawEditor() QPainter brush(&m_sliceEditor); // No sample loaded - if (m_slicerTParent->m_originalSample.frames() <= 1) + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { brush.setPen(s_playHighlightColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); @@ -306,7 +309,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) drawEditorWaveform(); break; case Qt::MouseButton::LeftButton: - if (m_slicerTParent->m_originalSample.frames() <= 1) { static_cast(parent())->openFiles(); } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { static_cast(parent())->openFiles(); } // update seeker middle for correct movement m_seekerMiddle = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; break; diff --git a/plugins/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index 5b8f6e8ad..d5f2e905f 100644 --- a/plugins/TripleOscillator/TripleOscillator.cpp +++ b/plugins/TripleOscillator/TripleOscillator.cpp @@ -23,8 +23,8 @@ */ - #include +#include #include "TripleOscillator.h" #include "AudioEngine.h" @@ -35,9 +35,11 @@ #include "Knob.h" #include "NotePlayHandle.h" #include "Oscillator.h" +#include "PathUtil.h" #include "PixmapButton.h" #include "SampleBuffer.h" - +#include "SampleLoader.h" +#include "Song.h" #include "embed.h" #include "plugin_export.h" @@ -133,22 +135,13 @@ OscillatorObject::OscillatorObject( Model * _parent, int _idx ) : } - - - -OscillatorObject::~OscillatorObject() -{ - sharedObject::unref( m_sampleBuffer ); -} - - - - void OscillatorObject::oscUserDefWaveDblClick() { - QString af = m_sampleBuffer->openAndSetWaveformFile(); + auto af = gui::SampleLoader::openWaveformFile(); if( af != "" ) { + m_sampleBuffer = gui::SampleLoader::createBufferFromFile(af); + m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_sampleBuffer.get()); // TODO: //m_usrWaveBtn->setToolTip(m_sampleBuffer->audioFile()); } @@ -289,8 +282,16 @@ void TripleOscillator::loadSettings( const QDomElement & _this ) "modalgo" + QString::number( i+1 ) ); m_osc[i]->m_useWaveTableModel.loadSettings( _this, "useWaveTable" + QString::number (i+1 ) ); - m_osc[i]->m_sampleBuffer->setAudioFile( _this.attribute( - "userwavefile" + is ) ); + + if (auto userWaveFile = _this.attribute("userwavefile" + is); !userWaveFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) + { + m_osc[i]->m_sampleBuffer = gui::SampleLoader::createBufferFromFile(userWaveFile); + m_osc[i]->m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_osc[i]->m_sampleBuffer.get()); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } + } } } @@ -360,7 +361,8 @@ void TripleOscillator::playNote( NotePlayHandle * _n, oscs_l[i]->setUserWave( m_osc[i]->m_sampleBuffer ); oscs_r[i]->setUserWave( m_osc[i]->m_sampleBuffer ); - + oscs_l[i]->setUserAntiAliasWaveTable(m_osc[i]->m_userAntiAliasWaveTable); + oscs_r[i]->setUserAntiAliasWaveTable(m_osc[i]->m_userAntiAliasWaveTable); } _n->m_pluginData = new oscPtr; diff --git a/plugins/TripleOscillator/TripleOscillator.h b/plugins/TripleOscillator/TripleOscillator.h index f3290153b..c0258b544 100644 --- a/plugins/TripleOscillator/TripleOscillator.h +++ b/plugins/TripleOscillator/TripleOscillator.h @@ -26,9 +26,13 @@ #ifndef _TRIPLE_OSCILLATOR_H #define _TRIPLE_OSCILLATOR_H +#include + #include "Instrument.h" #include "InstrumentView.h" #include "AutomatableModel.h" +#include "OscillatorConstants.h" +#include "SampleBuffer.h" namespace lmms { @@ -57,9 +61,6 @@ class OscillatorObject : public Model Q_OBJECT public: OscillatorObject( Model * _parent, int _idx ); - ~OscillatorObject() override; - - private: FloatModel m_volumeModel; FloatModel m_panModel; @@ -71,7 +72,8 @@ private: IntModel m_waveShapeModel; IntModel m_modulationAlgoModel; BoolModel m_useWaveTableModel; - SampleBuffer* m_sampleBuffer; + std::shared_ptr m_sampleBuffer = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userAntiAliasWaveTable; float m_volumeLeft; float m_volumeRight; diff --git a/src/core/AudioResampler.cpp b/src/core/AudioResampler.cpp new file mode 100644 index 000000000..5f6b6a239 --- /dev/null +++ b/src/core/AudioResampler.cpp @@ -0,0 +1,64 @@ +/* + * AudioResampler.cpp - wrapper for libsamplerate + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "AudioResampler.h" + +#include +#include +#include + +namespace lmms { + +AudioResampler::AudioResampler(int interpolationMode, int channels) + : m_interpolationMode(interpolationMode) + , m_channels(channels) + , m_state(src_new(interpolationMode, channels, &m_error)) +{ + if (!m_state) + { + const auto errorMessage = std::string{src_strerror(m_error)}; + const auto fullMessage = std::string{"Failed to create an AudioResampler: "} + errorMessage; + throw std::runtime_error{fullMessage}; + } +} + +AudioResampler::~AudioResampler() +{ + src_delete(m_state); +} + +auto AudioResampler::resample(const float* in, long inputFrames, float* out, long outputFrames, double ratio) + -> ProcessResult +{ + auto data = SRC_DATA{}; + data.data_in = in; + data.input_frames = inputFrames; + data.data_out = out; + data.output_frames = outputFrames; + data.src_ratio = ratio; + data.end_of_input = 0; + return {src_process(m_state, &data), data.input_frames_used, data.output_frames_gen}; +} + +} // namespace lmms diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c2dc0bf78..26d458f9e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -4,6 +4,7 @@ set(LMMS_SRCS core/AudioEngine.cpp core/AudioEngineProfiler.cpp core/AudioEngineWorkerThread.cpp + core/AudioResampler.cpp core/AutomatableModel.cpp core/AutomationClip.cpp core/AutomationNode.cpp @@ -65,8 +66,10 @@ set(LMMS_SRCS core/RemotePlugin.cpp core/RenderManager.cpp core/RingBuffer.cpp + core/Sample.cpp core/SampleBuffer.cpp core/SampleClip.cpp + core/SampleDecoder.cpp core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp core/Scale.cpp diff --git a/src/core/EnvelopeAndLfoParameters.cpp b/src/core/EnvelopeAndLfoParameters.cpp index 0a9673c8e..611700c51 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -22,13 +22,17 @@ * */ -#include - #include "EnvelopeAndLfoParameters.h" + +#include +#include + #include "AudioEngine.h" #include "Engine.h" #include "Oscillator.h" - +#include "PathUtil.h" +#include "SampleLoader.h" +#include "Song.h" namespace lmms { @@ -118,7 +122,7 @@ EnvelopeAndLfoParameters::EnvelopeAndLfoParameters( m_controlEnvAmountModel( false, this, tr( "Modulate env amount" ) ), m_lfoFrame( 0 ), m_lfoAmountIsZero( false ), - m_lfoShapeData( nullptr ) + m_lfoShapeData(nullptr) { m_amountModel.setCenterValue( 0 ); m_lfoAmountModel.setCenterValue( 0 ); @@ -221,7 +225,7 @@ inline sample_t EnvelopeAndLfoParameters::lfoShapeSample( fpp_t _frame_offset ) shape_sample = Oscillator::sawSample( phase ); break; case LfoShape::UserDefinedWave: - shape_sample = m_userWave.userWaveSample( phase ); + shape_sample = Oscillator::userWaveSample(m_userWave.get(), phase); break; case LfoShape::RandomWave: if( frame == 0 ) @@ -354,7 +358,7 @@ void EnvelopeAndLfoParameters::saveSettings( QDomDocument & _doc, m_lfoAmountModel.saveSettings( _doc, _parent, "lamt" ); m_x100Model.saveSettings( _doc, _parent, "x100" ); m_controlEnvAmountModel.saveSettings( _doc, _parent, "ctlenvamt" ); - _parent.setAttribute( "userwavefile", m_userWave.audioFile() ); + _parent.setAttribute("userwavefile", m_userWave->audioFile()); } @@ -386,7 +390,14 @@ void EnvelopeAndLfoParameters::loadSettings( const QDomElement & _this ) m_sustainModel.setValue( 1.0 - m_sustainModel.value() ); } - m_userWave.setAudioFile( _this.attribute( "userwavefile" ) ); + if (const auto userWaveFile = _this.attribute("userwavefile"); !userWaveFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) + { + m_userWave = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } + } updateSampleVars(); } diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 88f64803c..152e0ad8b 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -23,13 +23,15 @@ * */ -#include - - #include "LfoController.h" -#include "AudioEngine.h" -#include "Song.h" +#include +#include + +#include "AudioEngine.h" +#include "PathUtil.h" +#include "SampleLoader.h" +#include "Song.h" namespace lmms { @@ -48,7 +50,7 @@ LfoController::LfoController( Model * _parent ) : m_phaseOffset( 0 ), m_currentPhase( 0 ), m_sampleFunction( &Oscillator::sinSample ), - m_userDefSampleBuffer( new SampleBuffer ) + m_userDefSampleBuffer(std::make_shared()) { setSampleExact( true ); connect( &m_waveModel, SIGNAL(dataChanged()), @@ -74,7 +76,6 @@ LfoController::LfoController( Model * _parent ) : LfoController::~LfoController() { - sharedObject::unref( m_userDefSampleBuffer ); m_baseModel.disconnect( this ); m_speedModel.disconnect( this ); m_amountModel.disconnect( this ); @@ -122,7 +123,7 @@ void LfoController::updateValueBuffer() } case Oscillator::WaveShape::UserDefined: { - currentSample = m_userDefSampleBuffer->userWaveSample(phase); + currentSample = Oscillator::userWaveSample(m_userDefSampleBuffer.get(), phase); break; } default: @@ -222,7 +223,7 @@ void LfoController::saveSettings( QDomDocument & _doc, QDomElement & _this ) m_phaseModel.saveSettings( _doc, _this, "phase" ); m_waveModel.saveSettings( _doc, _this, "wave" ); m_multiplierModel.saveSettings( _doc, _this, "multiplier" ); - _this.setAttribute( "userwavefile" , m_userDefSampleBuffer->audioFile() ); + _this.setAttribute("userwavefile", m_userDefSampleBuffer->audioFile()); } @@ -237,7 +238,15 @@ void LfoController::loadSettings( const QDomElement & _this ) m_phaseModel.loadSettings( _this, "phase" ); m_waveModel.loadSettings( _this, "wave" ); m_multiplierModel.loadSettings( _this, "multiplier" ); - m_userDefSampleBuffer->setAudioFile( _this.attribute("userwavefile" ) ); + + if (const auto userWaveFile = _this.attribute("userwavefile"); !userWaveFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) + { + m_userDefSampleBuffer = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } + } updateSampleFunction(); } diff --git a/src/core/Oscillator.cpp b/src/core/Oscillator.cpp index 06033b63e..0330fad58 100644 --- a/src/core/Oscillator.cpp +++ b/src/core/Oscillator.cpp @@ -182,19 +182,23 @@ void Oscillator::generateFromFFT(int bands, sample_t* table) normalize(s_sampleBuffer.data(), table, OscillatorConstants::WAVETABLE_LENGTH, 2*OscillatorConstants::WAVETABLE_LENGTH + 1); } -void Oscillator::generateAntiAliasUserWaveTable(SampleBuffer *sampleBuffer) +std::unique_ptr Oscillator::generateAntiAliasUserWaveTable(const SampleBuffer* sampleBuffer) { - if (sampleBuffer->m_userAntiAliasWaveTable == nullptr) {return;} - + auto userAntiAliasWaveTable = std::make_unique(); for (int i = 0; i < OscillatorConstants::WAVE_TABLES_PER_WAVEFORM_COUNT; ++i) { - for (int i = 0; i < OscillatorConstants::WAVETABLE_LENGTH; ++i) + // TODO: This loop seems to be doing the same thing for each iteration of the outer loop, + // and could probably be moved out of it + for (int j = 0; j < OscillatorConstants::WAVETABLE_LENGTH; ++j) { - s_sampleBuffer[i] = sampleBuffer->userWaveSample((float)i / (float)OscillatorConstants::WAVETABLE_LENGTH); + s_sampleBuffer[j] = Oscillator::userWaveSample( + sampleBuffer, static_cast(j) / OscillatorConstants::WAVETABLE_LENGTH); } fftwf_execute(s_fftPlan); - Oscillator::generateFromFFT(OscillatorConstants::MAX_FREQ / freqFromWaveTableBand(i), (*(sampleBuffer->m_userAntiAliasWaveTable))[i].data()); + Oscillator::generateFromFFT(OscillatorConstants::MAX_FREQ / freqFromWaveTableBand(i), (*userAntiAliasWaveTable)[i].data()); } + + return userAntiAliasWaveTable; } @@ -807,13 +811,13 @@ template<> inline sample_t Oscillator::getSample( const float _sample ) { - if (m_useWaveTable && !m_isModulator) + if (m_useWaveTable && m_userAntiAliasWaveTable && !m_isModulator) { - return wtSample(m_userWave->m_userAntiAliasWaveTable, _sample); + return wtSample(m_userAntiAliasWaveTable.get(), _sample); } else { - return userWaveSample(_sample); + return userWaveSample(m_userWave.get(), _sample); } } diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp new file mode 100644 index 000000000..cdb12851f --- /dev/null +++ b/src/core/Sample.cpp @@ -0,0 +1,230 @@ +/* + * Sample.cpp - State for container-class SampleBuffer + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Sample.h" + +#include +#include + +namespace lmms { + +Sample::Sample(const QString& audioFile) + : m_buffer(std::make_shared(audioFile)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const QByteArray& base64, int sampleRate) + : m_buffer(std::make_shared(base64, sampleRate)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const sampleFrame* data, int numFrames, int sampleRate) + : m_buffer(std::make_shared(data, numFrames, sampleRate)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(std::shared_ptr buffer) + : m_buffer(buffer) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const Sample& other) + : m_buffer(other.m_buffer) + , m_startFrame(other.startFrame()) + , m_endFrame(other.endFrame()) + , m_loopStartFrame(other.loopStartFrame()) + , m_loopEndFrame(other.loopEndFrame()) + , m_amplification(other.amplification()) + , m_frequency(other.frequency()) + , m_reversed(other.reversed()) +{ +} + +Sample::Sample(Sample&& other) + : m_buffer(std::move(other.m_buffer)) + , m_startFrame(other.startFrame()) + , m_endFrame(other.endFrame()) + , m_loopStartFrame(other.loopStartFrame()) + , m_loopEndFrame(other.loopEndFrame()) + , m_amplification(other.amplification()) + , m_frequency(other.frequency()) + , m_reversed(other.reversed()) +{ +} + +auto Sample::operator=(const Sample& other) -> Sample& +{ + m_buffer = other.m_buffer; + m_startFrame = other.startFrame(); + m_endFrame = other.endFrame(); + m_loopStartFrame = other.loopStartFrame(); + m_loopEndFrame = other.loopEndFrame(); + m_amplification = other.amplification(); + m_frequency = other.frequency(); + m_reversed = other.reversed(); + + return *this; +} + +auto Sample::operator=(Sample&& other) -> Sample& +{ + m_buffer = std::move(other.m_buffer); + m_startFrame = other.startFrame(); + m_endFrame = other.endFrame(); + m_loopStartFrame = other.loopStartFrame(); + m_loopEndFrame = other.loopEndFrame(); + m_amplification = other.amplification(); + m_frequency = other.frequency(); + m_reversed = other.reversed(); + + return *this; +} + +bool Sample::play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency, Loop loopMode) +{ + if (numFrames <= 0 || desiredFrequency <= 0) { return false; } + + auto resampleRatio = static_cast(Engine::audioEngine()->processingSampleRate()) / m_buffer->sampleRate(); + resampleRatio *= frequency() / desiredFrequency; + + auto playBuffer = std::vector(numFrames / resampleRatio); + if (!typeInfo::isEqual(resampleRatio, 1.0f)) + { + playBuffer.resize(playBuffer.size() + s_interpolationMargins[state->resampler().interpolationMode()]); + } + + const auto start = startFrame(); + const auto end = endFrame(); + const auto loopStart = loopStartFrame(); + const auto loopEnd = loopEndFrame(); + + switch (loopMode) + { + case Loop::Off: + state->m_frameIndex = std::clamp(state->m_frameIndex, start, end); + if (state->m_frameIndex == end) { return false; } + break; + case Loop::On: + state->m_frameIndex = std::clamp(state->m_frameIndex, start, loopEnd); + if (state->m_frameIndex == loopEnd) { state->m_frameIndex = loopStart; } + break; + case Loop::PingPong: + state->m_frameIndex = std::clamp(state->m_frameIndex, start, loopEnd); + if (state->m_frameIndex == loopEnd) + { + state->m_frameIndex = loopEnd - 1; + state->m_backwards = true; + } + else if (state->m_frameIndex <= loopStart && state->m_backwards) + { + state->m_frameIndex = loopStart; + state->m_backwards = false; + } + break; + } + + playSampleRange(state, playBuffer.data(), playBuffer.size()); + + const auto result + = state->resampler().resample(&playBuffer[0][0], playBuffer.size(), &dst[0][0], numFrames, resampleRatio); + if (result.error != 0) { return false; } + + state->m_frameIndex += (state->m_backwards ? -1 : 1) * result.inputFramesUsed; + amplifySampleRange(dst, result.outputFramesGenerated); + + return true; +} + +auto Sample::sampleDuration() const -> std::chrono::milliseconds +{ + const auto numFrames = endFrame() - startFrame(); + const auto duration = numFrames / static_cast(m_buffer->sampleRate()) * 1000; + return std::chrono::milliseconds{static_cast(duration)}; +} + +void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, int loopEndFrame) +{ + setStartFrame(startFrame); + setEndFrame(endFrame); + setLoopStartFrame(loopStartFrame); + setLoopEndFrame(loopEndFrame); +} + +void Sample::playSampleRange(PlaybackState* state, sampleFrame* dst, size_t numFrames) const +{ + auto framesToCopy = 0; + if (state->m_backwards) + { + framesToCopy = std::min(state->m_frameIndex - startFrame(), numFrames); + copyBufferBackward(dst, state->m_frameIndex, framesToCopy); + } + else + { + framesToCopy = std::min(endFrame() - state->m_frameIndex, numFrames); + copyBufferForward(dst, state->m_frameIndex, framesToCopy); + } + + if (framesToCopy < numFrames) { std::fill_n(dst + framesToCopy, numFrames - framesToCopy, sampleFrame{0, 0}); } +} + +void Sample::copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const +{ + reversed() ? std::copy_n(m_buffer->rbegin() + initialPosition, advanceAmount, dst) + : std::copy_n(m_buffer->begin() + initialPosition, advanceAmount, dst); +} + +void Sample::copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const +{ + reversed() ? std::reverse_copy( + m_buffer->rbegin() + initialPosition - advanceAmount, m_buffer->rbegin() + initialPosition, dst) + : std::reverse_copy( + m_buffer->begin() + initialPosition - advanceAmount, m_buffer->begin() + initialPosition, dst); +} + +void Sample::amplifySampleRange(sampleFrame* src, int numFrames) const +{ + const auto amplification = m_amplification.load(std::memory_order_relaxed); + for (int i = 0; i < numFrames; ++i) + { + src[i][0] *= amplification; + src[i][1] *= amplification; + } +} +} // namespace lmms diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 2a0076a28..550a9d7bc 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -23,1612 +23,74 @@ */ #include "SampleBuffer.h" -#include "Oscillator.h" +#include -#include - -#include -#include -#include -#include - - -#include - -#define OV_EXCLUDE_STATIC_CALLBACKS -#ifdef LMMS_HAVE_OGGVORBIS -#include -#endif - -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H -#include -#endif - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H -#include -#endif - - -#include "AudioEngine.h" -#include "base64.h" -#include "ConfigManager.h" -#include "DrumSynth.h" -#include "endian_handling.h" -#include "Engine.h" -#include "GuiApplication.h" -#include "Note.h" #include "PathUtil.h" +#include "SampleDecoder.h" +#include "lmms_basics.h" -#include "FileDialog.h" +namespace lmms { -namespace lmms +SampleBuffer::SampleBuffer(const sampleFrame* data, int numFrames, int sampleRate) + : m_data(data, data + numFrames) + , m_sampleRate(sampleRate) { - -SampleBuffer::SampleBuffer() : - m_userAntiAliasWaveTable(nullptr), - m_audioFile(""), - m_origData(nullptr), - m_origFrames(0), - m_data(nullptr), - m_frames(0), - m_startFrame(0), - m_endFrame(0), - m_loopStartFrame(0), - m_loopEndFrame(0), - m_amplification(1.0f), - m_reversed(false), - m_frequency(DefaultBaseFreq), - m_sampleRate(audioEngineSampleRate()) -{ - - connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(sampleRateChanged())); - update(); } - - -SampleBuffer::SampleBuffer(const QString & audioFile, bool isBase64Data) - : SampleBuffer() +SampleBuffer::SampleBuffer(const QString& audioFile) { - if (isBase64Data) + if (audioFile.isEmpty()) { throw std::runtime_error{"Failure loading audio file: Audio file path is empty."}; } + const auto absolutePath = PathUtil::toAbsolute(audioFile); + + if (auto decodedResult = SampleDecoder::decode(absolutePath)) { - loadFromBase64(audioFile); - } - else - { - m_audioFile = audioFile; - update(); + auto& [data, sampleRate] = *decodedResult; + m_data = std::move(data); + m_sampleRate = sampleRate; + m_audioFile = PathUtil::toShortestRelative(audioFile); + return; } + + throw std::runtime_error{ + "Failed to decode audio file: Either the audio codec is unsupported, or the file is corrupted."}; } - - - -SampleBuffer::SampleBuffer(const sampleFrame * data, const f_cnt_t frames) - : SampleBuffer() +SampleBuffer::SampleBuffer(const QString& base64, int sampleRate) + : m_sampleRate(sampleRate) { - if (frames > 0) - { - m_origData = MM_ALLOC( frames); - memcpy(m_origData, data, frames * BYTES_PER_FRAME); - m_origFrames = frames; - update(); - } + // TODO: Replace with non-Qt equivalent + const auto bytes = QByteArray::fromBase64(base64.toUtf8()); + m_data.resize(bytes.size() / sizeof(sampleFrame)); + std::memcpy(reinterpret_cast(m_data.data()), bytes, m_data.size() * sizeof(sampleFrame)); } - - - -SampleBuffer::SampleBuffer(const f_cnt_t frames) - : SampleBuffer() +SampleBuffer::SampleBuffer(std::vector data, int sampleRate) + : m_data(std::move(data)) + , m_sampleRate(sampleRate) { - if (frames > 0) - { - m_origData = MM_ALLOC( frames); - memset(m_origData, 0, frames * BYTES_PER_FRAME); - m_origFrames = frames; - update(); - } } - - - -SampleBuffer::SampleBuffer(const SampleBuffer& orig) -{ - orig.m_varLock.lockForRead(); - - m_audioFile = orig.m_audioFile; - m_origFrames = orig.m_origFrames; - m_origData = (m_origFrames > 0) ? MM_ALLOC( m_origFrames) : nullptr; - m_frames = orig.m_frames; - m_data = (m_frames > 0) ? MM_ALLOC( m_frames) : nullptr; - m_startFrame = orig.m_startFrame; - m_endFrame = orig.m_endFrame; - m_loopStartFrame = orig.m_loopStartFrame; - m_loopEndFrame = orig.m_loopEndFrame; - m_amplification = orig.m_amplification; - m_reversed = orig.m_reversed; - m_frequency = orig.m_frequency; - m_sampleRate = orig.m_sampleRate; - - //Deep copy m_origData and m_data from original - const auto origFrameBytes = m_origFrames * BYTES_PER_FRAME; - const auto frameBytes = m_frames * BYTES_PER_FRAME; - if (orig.m_origData != nullptr && origFrameBytes > 0) - { memcpy(m_origData, orig.m_origData, origFrameBytes); } - if (orig.m_data != nullptr && frameBytes > 0) - { memcpy(m_data, orig.m_data, frameBytes); } - - orig.m_varLock.unlock(); -} - - - - void swap(SampleBuffer& first, SampleBuffer& second) noexcept { using std::swap; - - // Lock both buffers for writing, with address as lock ordering - if (&first == &second) { return; } - else if (&first > &second) - { - first.m_varLock.lockForWrite(); - second.m_varLock.lockForWrite(); - } - else - { - second.m_varLock.lockForWrite(); - first.m_varLock.lockForWrite(); - } - - first.m_audioFile.swap(second.m_audioFile); - swap(first.m_origData, second.m_origData); swap(first.m_data, second.m_data); - swap(first.m_origFrames, second.m_origFrames); - swap(first.m_frames, second.m_frames); - swap(first.m_startFrame, second.m_startFrame); - swap(first.m_endFrame, second.m_endFrame); - swap(first.m_loopStartFrame, second.m_loopStartFrame); - swap(first.m_loopEndFrame, second.m_loopEndFrame); - swap(first.m_amplification, second.m_amplification); - swap(first.m_frequency, second.m_frequency); - swap(first.m_reversed, second.m_reversed); + swap(first.m_audioFile, second.m_audioFile); swap(first.m_sampleRate, second.m_sampleRate); - - // Unlock again - first.m_varLock.unlock(); - second.m_varLock.unlock(); } - - - -SampleBuffer& SampleBuffer::operator=(SampleBuffer that) +QString SampleBuffer::toBase64() const { - swap(*this, that); - return *this; + // TODO: Replace with non-Qt equivalent + const auto data = reinterpret_cast(m_data.data()); + const auto size = static_cast(m_data.size() * sizeof(sampleFrame)); + const auto byteArray = QByteArray{data, size}; + return byteArray.toBase64(); } - - - -SampleBuffer::~SampleBuffer() +auto SampleBuffer::emptyBuffer() -> std::shared_ptr { - MM_FREE(m_origData); - MM_FREE(m_data); -} - - - -void SampleBuffer::sampleRateChanged() -{ - update(true); -} - -sample_rate_t SampleBuffer::audioEngineSampleRate() -{ - return Engine::audioEngine()->processingSampleRate(); -} - - -void SampleBuffer::update(bool keepSettings) -{ - const bool lock = (m_data != nullptr); - if (lock) - { - Engine::audioEngine()->requestChangeInModel(); - m_varLock.lockForWrite(); - MM_FREE(m_data); - } - - // File size and sample length limits - const int fileSizeMax = 300; // MB - const int sampleLengthMax = 90; // Minutes - - enum class FileLoadError - { - None, - ReadPermissionDenied, - TooLarge, - Invalid - }; - FileLoadError fileLoadError = FileLoadError::None; - - if (m_audioFile.isEmpty() && m_origData != nullptr && m_origFrames > 0) - { - // TODO: reverse- and amplification-property is not covered - // by following code... - m_data = MM_ALLOC( m_origFrames); - memcpy(m_data, m_origData, m_origFrames * BYTES_PER_FRAME); - if (keepSettings == false) - { - m_frames = m_origFrames; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = m_frames; - } - } - else if (!m_audioFile.isEmpty()) - { - QString file = PathUtil::toAbsolute(m_audioFile); - int_sample_t * buf = nullptr; - sample_t * fbuf = nullptr; - ch_cnt_t channels = DEFAULT_CHANNELS; - sample_rate_t samplerate = audioEngineSampleRate(); - m_frames = 0; - - const QFileInfo fileInfo(file); - if (!fileInfo.isReadable()) - { - fileLoadError = FileLoadError::ReadPermissionDenied; - } - else if (fileInfo.size() > fileSizeMax * 1024 * 1024) - { - fileLoadError = FileLoadError::TooLarge; - } - else - { - // Use QFile to handle unicode file names on Windows - QFile f(file); - SNDFILE * sndFile = nullptr; - SF_INFO sfInfo; - sfInfo.format = 0; - - if (f.open(QIODevice::ReadOnly) && (sndFile = sf_open_fd(f.handle(), SFM_READ, &sfInfo, false))) - { - f_cnt_t frames = sfInfo.frames; - int rate = sfInfo.samplerate; - if (frames / rate > sampleLengthMax * 60) - { - fileLoadError = FileLoadError::TooLarge; - } - sf_close(sndFile); - } - f.close(); - } - - if (fileLoadError == FileLoadError::None) - { -#ifdef LMMS_HAVE_OGGVORBIS - // workaround for a bug in libsndfile or our libsndfile decoder - // causing some OGG files to be distorted -> try with OGG Vorbis - // decoder first if filename extension matches "ogg" - if (m_frames == 0 && fileInfo.suffix() == "ogg") - { - m_frames = decodeSampleOGGVorbis(file, buf, channels, samplerate); - } -#endif - if (m_frames == 0) - { - m_frames = decodeSampleSF(file, fbuf, channels, samplerate); - } -#ifdef LMMS_HAVE_OGGVORBIS - if (m_frames == 0) - { - m_frames = decodeSampleOGGVorbis(file, buf, channels, samplerate); - } -#endif - if (m_frames == 0) - { - m_frames = decodeSampleDS(file, buf, channels, samplerate); - } - - if (m_frames == 0) - { - fileLoadError = FileLoadError::Invalid; - } - } - - if (m_frames == 0 || fileLoadError != FileLoadError::None) // if still no frames, bail - { - // sample couldn't be decoded, create buffer containing - // one sample-frame - m_data = MM_ALLOC( 1); - memset(m_data, 0, sizeof(*m_data)); - m_frames = 1; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = 1; - } - else // otherwise normalize sample rate - { - normalizeSampleRate(samplerate, keepSettings); - } - } - else - { - // neither an audio-file nor a buffer to copy from, so create - // buffer containing one sample-frame - m_data = MM_ALLOC( 1); - memset(m_data, 0, sizeof(*m_data)); - m_frames = 1; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = 1; - } - - if (lock) - { - m_varLock.unlock(); - Engine::audioEngine()->doneChangeInModel(); - } - - emit sampleUpdated(); - - // allocate space for anti-aliased wave table - if (m_userAntiAliasWaveTable == nullptr) - { - m_userAntiAliasWaveTable = std::make_unique(); - } - Oscillator::generateAntiAliasUserWaveTable(this); - - if (fileLoadError != FileLoadError::None) - { - QString title = tr("Fail to open file"); - QString message; - - switch (fileLoadError) - { - case FileLoadError::None: - // present just to avoid a compiler warning - break; - - case FileLoadError::ReadPermissionDenied: - message = tr("Read permission denied"); - break; - - case FileLoadError::TooLarge: - message = tr("Audio files are limited to %1 MB " - "in size and %2 minutes of playing time" - ).arg(fileSizeMax).arg(sampleLengthMax); - break; - - case FileLoadError::Invalid: - message = tr("Invalid audio file"); - break; - } - - if (gui::getGUI() != nullptr) - { - QMessageBox::information(nullptr, title, message, QMessageBox::Ok); - } - else - { - fprintf(stderr, "%s\n", message.toUtf8().constData()); - } - } -} - - -void SampleBuffer::convertIntToFloat( - int_sample_t * & ibuf, - f_cnt_t frames, - int channels -) -{ - // following code transforms int-samples into float-samples and does amplifying & reversing - const float fac = 1 / OUTPUT_SAMPLE_MULTIPLIER; - m_data = MM_ALLOC( frames); - const int ch = (channels > 1) ? 1 : 0; - - // if reversing is on, we also reverse when scaling - bool isReversed = m_reversed; - int idx = isReversed ? (frames - 1) * channels : 0; - for (f_cnt_t frame = 0; frame < frames; ++frame) - { - m_data[frame][0] = ibuf[idx+0] * fac; - m_data[frame][1] = ibuf[idx+ch] * fac; - idx += isReversed ? -channels : channels; - } - - delete[] ibuf; -} - -void SampleBuffer::directFloatWrite( - sample_t * & fbuf, - f_cnt_t frames, - int channels -) -{ - - m_data = MM_ALLOC( frames); - const int ch = (channels > 1) ? 1 : 0; - - // if reversing is on, we also reverse when scaling - bool isReversed = m_reversed; - int idx = isReversed ? (frames - 1) * channels : 0; - for (f_cnt_t frame = 0; frame < frames; ++frame) - { - m_data[frame][0] = fbuf[idx+0]; - m_data[frame][1] = fbuf[idx+ch]; - idx += isReversed ? -channels : channels; - } - - delete[] fbuf; -} - - -void SampleBuffer::normalizeSampleRate(const sample_rate_t srcSR, bool keepSettings) -{ - const sample_rate_t oldRate = m_sampleRate; - // do samplerate-conversion to our default-samplerate - if (srcSR != audioEngineSampleRate()) - { - SampleBuffer * resampled = resample(srcSR, audioEngineSampleRate()); - - m_sampleRate = audioEngineSampleRate(); - MM_FREE(m_data); - m_frames = resampled->frames(); - m_data = MM_ALLOC( m_frames); - memcpy(m_data, resampled->data(), m_frames * sizeof(sampleFrame)); - delete resampled; - } - - if (keepSettings == false) - { - // update frame-variables - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = m_frames; - } - else if (oldRate != audioEngineSampleRate()) - { - auto oldRateToNewRateRatio = static_cast(audioEngineSampleRate()) / oldRate; - - m_startFrame = std::clamp(f_cnt_t(m_startFrame * oldRateToNewRateRatio), 0, m_frames); - m_endFrame = std::clamp(f_cnt_t(m_endFrame * oldRateToNewRateRatio), m_startFrame, m_frames); - m_loopStartFrame = std::clamp(f_cnt_t(m_loopStartFrame * oldRateToNewRateRatio), 0, m_frames); - m_loopEndFrame = std::clamp(f_cnt_t(m_loopEndFrame * oldRateToNewRateRatio), m_loopStartFrame, m_frames); - m_sampleRate = audioEngineSampleRate(); - } -} - - - - -f_cnt_t SampleBuffer::decodeSampleSF( - QString fileName, - sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate -) -{ - SNDFILE * sndFile; - SF_INFO sfInfo; - sfInfo.format = 0; - f_cnt_t frames = 0; - sf_count_t sfFramesRead; - - - // Use QFile to handle unicode file names on Windows - QFile f(fileName); - if (f.open(QIODevice::ReadOnly) && (sndFile = sf_open_fd(f.handle(), SFM_READ, &sfInfo, false))) - { - frames = sfInfo.frames; - - buf = new sample_t[sfInfo.channels * frames]; - sfFramesRead = sf_read_float(sndFile, buf, sfInfo.channels * frames); - - if (sfFramesRead < sfInfo.channels * frames) - { -#ifdef DEBUG_LMMS - qDebug("SampleBuffer::decodeSampleSF(): could not read" - " sample %s: %s", fileName, sf_strerror(nullptr)); -#endif - } - channels = sfInfo.channels; - samplerate = sfInfo.samplerate; - - sf_close(sndFile); - } - else - { -#ifdef DEBUG_LMMS - qDebug("SampleBuffer::decodeSampleSF(): could not load " - "sample %s: %s", fileName, sf_strerror(nullptr)); -#endif - } - f.close(); - - //write down either directly or convert i->f depending on file type - - if (frames > 0 && buf != nullptr) - { - directFloatWrite(buf, frames, channels); - } - - return frames; -} - - - - -#ifdef LMMS_HAVE_OGGVORBIS - -// callback-functions for reading ogg-file - -size_t qfileReadCallback(void * ptr, size_t size, size_t n, void * udata ) -{ - return static_cast(udata)->read((char*) ptr, size * n); -} - - - - -int qfileSeekCallback(void * udata, ogg_int64_t offset, int whence) -{ - auto f = static_cast(udata); - - if (whence == SEEK_CUR) - { - f->seek(f->pos() + offset); - } - else if (whence == SEEK_END) - { - f->seek(f->size() + offset); - } - else - { - f->seek(offset); - } - return 0; -} - - - - -int qfileCloseCallback(void * udata) -{ - delete static_cast(udata); - return 0; -} - - - - -long qfileTellCallback(void * udata) -{ - return static_cast(udata)->pos(); -} - - - - -f_cnt_t SampleBuffer::decodeSampleOGGVorbis( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate -) -{ - static ov_callbacks callbacks = - { - qfileReadCallback, - qfileSeekCallback, - qfileCloseCallback, - qfileTellCallback - } ; - - OggVorbis_File vf; - - f_cnt_t frames = 0; - - auto f = new QFile(fileName); - if (f->open(QFile::ReadOnly) == false) - { - delete f; - return 0; - } - - int err = ov_open_callbacks(f, &vf, nullptr, 0, callbacks); - - if (err < 0) - { - switch (err) - { - case OV_EREAD: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " media read error\n"); - break; - case OV_ENOTVORBIS: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " not an Ogg Vorbis file\n"); - break; - case OV_EVERSION: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " vorbis version mismatch\n"); - break; - case OV_EBADHEADER: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " invalid Vorbis bitstream header\n"); - break; - case OV_EFAULT: - printf("SampleBuffer::decodeSampleOgg(): " - "internal logic fault\n"); - break; - } - delete f; - return 0; - } - - ov_pcm_seek(&vf, 0); - - channels = ov_info(&vf, -1)->channels; - samplerate = ov_info(&vf, -1)->rate; - - ogg_int64_t total = ov_pcm_total(&vf, -1); - - buf = new int_sample_t[total * channels]; - int bitstream = 0; - long bytesRead = 0; - - do - { - bytesRead = ov_read(&vf, - (char *) &buf[frames * channels], - (total - frames) * channels * BYTES_PER_INT_SAMPLE, - isLittleEndian() ? 0 : 1, - BYTES_PER_INT_SAMPLE, - 1, - &bitstream - ); - - if (bytesRead < 0) - { - break; - } - frames += bytesRead / (channels * BYTES_PER_INT_SAMPLE); - } - while (bytesRead != 0 && bitstream == 0); - - ov_clear(&vf); - - // if buffer isn't empty, convert it to float and write it down - if (frames > 0 && buf != nullptr) - { - convertIntToFloat(buf, frames, channels); - } - - return frames; -} -#endif // LMMS_HAVE_OGGVORBIS - - - - -f_cnt_t SampleBuffer::decodeSampleDS( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate -) -{ - DrumSynth ds; - f_cnt_t frames = ds.GetDSFileSamples(fileName, buf, channels, samplerate); - - if (frames > 0 && buf != nullptr) - { - convertIntToFloat(buf, frames, channels); - } - - return frames; - -} - - - - -bool SampleBuffer::play( - sampleFrame * ab, - handleState * state, - const fpp_t frames, - const float freq, - const LoopMode loopMode -) -{ - f_cnt_t startFrame = m_startFrame; - f_cnt_t endFrame = m_endFrame; - f_cnt_t loopStartFrame = m_loopStartFrame; - f_cnt_t loopEndFrame = m_loopEndFrame; - - if (endFrame == 0 || frames == 0) - { - return false; - } - - // variable for determining if we should currently be playing backwards in a ping-pong loop - bool isBackwards = state->isBackwards(); - - // The SampleBuffer can play a given sample with increased or decreased pitch. However, only - // samples that contain a tone that matches the default base note frequency of 440 Hz will - // produce the exact requested pitch in [Hz]. - const double freqFactor = (double) freq / (double) m_frequency * - m_sampleRate / Engine::audioEngine()->processingSampleRate(); - - // calculate how many frames we have in requested pitch - const auto totalFramesForCurrentPitch = static_cast((endFrame - startFrame) / freqFactor); - - if (totalFramesForCurrentPitch == 0) - { - return false; - } - - - // this holds the index of the first frame to play - f_cnt_t playFrame = std::max(state->m_frameIndex, startFrame); - - if (loopMode == LoopMode::Off) - { - if (playFrame >= endFrame || (endFrame - playFrame) / freqFactor == 0) - { - // the sample is done being played - return false; - } - } - else if (loopMode == LoopMode::On) - { - playFrame = getLoopedIndex(playFrame, loopStartFrame, loopEndFrame); - } - else - { - playFrame = getPingPongIndex(playFrame, loopStartFrame, loopEndFrame); - } - - f_cnt_t fragmentSize = (f_cnt_t)(frames * freqFactor) + MARGIN[state->interpolationMode()]; - - sampleFrame * tmp = nullptr; - - // check whether we have to change pitch... - if (freqFactor != 1.0 || state->m_varyingPitch) - { - SRC_DATA srcData; - // Generate output - srcData.data_in = - getSampleFragment(playFrame, fragmentSize, loopMode, &tmp, &isBackwards, - loopStartFrame, loopEndFrame, endFrame )->data(); - srcData.data_out = ab->data(); - srcData.input_frames = fragmentSize; - srcData.output_frames = frames; - srcData.src_ratio = 1.0 / freqFactor; - srcData.end_of_input = 0; - int error = src_process(state->m_resamplingData, &srcData); - if (error) - { - printf("SampleBuffer: error while resampling: %s\n", - src_strerror(error)); - } - if (srcData.output_frames_gen > frames) - { - printf("SampleBuffer: not enough frames: %ld / %d\n", - srcData.output_frames_gen, frames); - } - // Advance - switch (loopMode) - { - case LoopMode::Off: - playFrame += srcData.input_frames_used; - break; - case LoopMode::On: - playFrame += srcData.input_frames_used; - playFrame = getLoopedIndex(playFrame, loopStartFrame, loopEndFrame); - break; - case LoopMode::PingPong: - { - f_cnt_t left = srcData.input_frames_used; - if (state->isBackwards()) - { - playFrame -= srcData.input_frames_used; - if (playFrame < loopStartFrame) - { - left -= (loopStartFrame - playFrame); - playFrame = loopStartFrame; - } - else left = 0; - } - playFrame += left; - playFrame = getPingPongIndex(playFrame, loopStartFrame, loopEndFrame); - break; - } - } - } - else - { - // we don't have to pitch, so we just copy the sample-data - // as is into pitched-copy-buffer - - // Generate output - memcpy(ab, - getSampleFragment(playFrame, frames, loopMode, &tmp, &isBackwards, - loopStartFrame, loopEndFrame, endFrame), - frames * BYTES_PER_FRAME); - // Advance - switch (loopMode) - { - case LoopMode::Off: - playFrame += frames; - break; - case LoopMode::On: - playFrame += frames; - playFrame = getLoopedIndex(playFrame, loopStartFrame, loopEndFrame); - break; - case LoopMode::PingPong: - { - f_cnt_t left = frames; - if (state->isBackwards()) - { - playFrame -= frames; - if (playFrame < loopStartFrame) - { - left -= (loopStartFrame - playFrame); - playFrame = loopStartFrame; - } - else left = 0; - } - playFrame += left; - playFrame = getPingPongIndex(playFrame, loopStartFrame, loopEndFrame); - break; - } - } - } - - if (tmp != nullptr) - { - MM_FREE(tmp); - } - - state->setBackwards(isBackwards); - state->setFrameIndex(playFrame); - - for (fpp_t i = 0; i < frames; ++i) - { - ab[i][0] *= m_amplification; - ab[i][1] *= m_amplification; - } - - return true; -} - - - - -sampleFrame * SampleBuffer::getSampleFragment( - f_cnt_t index, - f_cnt_t frames, - LoopMode loopMode, - sampleFrame * * tmp, - bool * backwards, - f_cnt_t loopStart, - f_cnt_t loopEnd, - f_cnt_t end -) const -{ - if (loopMode == LoopMode::Off) - { - if (index + frames <= end) - { - return m_data + index; - } - } - else if (loopMode == LoopMode::On) - { - if (index + frames <= loopEnd) - { - return m_data + index; - } - } - else - { - if (!*backwards && index + frames < loopEnd) - { - return m_data + index; - } - } - - *tmp = MM_ALLOC( frames); - - if (loopMode == LoopMode::Off) - { - f_cnt_t available = end - index; - memcpy(*tmp, m_data + index, available * BYTES_PER_FRAME); - memset(*tmp + available, 0, (frames - available) * BYTES_PER_FRAME); - } - else if (loopMode == LoopMode::On) - { - f_cnt_t copied = std::min(frames, loopEnd - index); - memcpy(*tmp, m_data + index, copied * BYTES_PER_FRAME); - f_cnt_t loopFrames = loopEnd - loopStart; - while (copied < frames) - { - f_cnt_t todo = std::min(frames - copied, loopFrames); - memcpy(*tmp + copied, m_data + loopStart, todo * BYTES_PER_FRAME); - copied += todo; - } - } - else - { - f_cnt_t pos = index; - bool currentBackwards = pos < loopStart - ? false - : *backwards; - f_cnt_t copied = 0; - - - if (currentBackwards) - { - copied = std::min(frames, pos - loopStart); - for (int i = 0; i < copied; i++) - { - (*tmp)[i][0] = m_data[pos - i][0]; - (*tmp)[i][1] = m_data[pos - i][1]; - } - pos -= copied; - if (pos == loopStart) { currentBackwards = false; } - } - else - { - copied = std::min(frames, loopEnd - pos); - memcpy(*tmp, m_data + pos, copied * BYTES_PER_FRAME); - pos += copied; - if (pos == loopEnd) { currentBackwards = true; } - } - - while (copied < frames) - { - if (currentBackwards) - { - f_cnt_t todo = std::min(frames - copied, pos - loopStart); - for (int i = 0; i < todo; i++) - { - (*tmp)[copied + i][0] = m_data[pos - i][0]; - (*tmp)[copied + i][1] = m_data[pos - i][1]; - } - pos -= todo; - copied += todo; - if (pos <= loopStart) { currentBackwards = false; } - } - else - { - f_cnt_t todo = std::min(frames - copied, loopEnd - pos); - memcpy(*tmp + copied, m_data + pos, todo * BYTES_PER_FRAME); - pos += todo; - copied += todo; - if (pos >= loopEnd) { currentBackwards = true; } - } - } - *backwards = currentBackwards; - } - - return *tmp; -} - - - - -f_cnt_t SampleBuffer::getLoopedIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const -{ - if (index < endf) - { - return index; - } - return startf + (index - startf) % (endf - startf); -} - - -f_cnt_t SampleBuffer::getPingPongIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const -{ - if (index < endf) - { - return index; - } - const f_cnt_t loopLen = endf - startf; - const f_cnt_t loopPos = (index - endf) % (loopLen * 2); - - return (loopPos < loopLen) - ? endf - loopPos - : startf + (loopPos - loopLen); -} - - -/* @brief Draws a sample buffer on the QRect given in the range [fromFrame, toFrame) - * @param QPainter p: Painter object for the painting operations - * @param QRect dr: QRect where the buffer will be drawn in - * @param QRect clip: QRect used for clipping - * @param f_cnt_t fromFrame: First frame of the range - * @param f_cnt_t toFrame: Last frame of the range non-inclusive - */ -void SampleBuffer::visualize( - QPainter & p, - const QRect & dr, - const QRect & clip, - f_cnt_t fromFrame, - f_cnt_t toFrame -) -{ - if (m_frames == 0) { return; } - - const bool focusOnRange = toFrame <= m_frames && 0 <= fromFrame && fromFrame < toFrame; - //TODO: If the clip QRect is not being used we should remove it - //p.setClipRect(clip); - const int w = dr.width(); - const int h = dr.height(); - - const int yb = h / 2 + dr.y(); - const float ySpace = h * 0.5f; - const int nbFrames = focusOnRange ? toFrame - fromFrame : m_frames; - - const double fpp = std::max(1., static_cast(nbFrames) / w); - // There are 2 possibilities: Either nbFrames is bigger than - // the width, so we will have width points, or nbFrames is - // smaller than the width (fpp = 1) and we will have nbFrames - // points - const int totalPoints = nbFrames > w - ? w - : nbFrames; - std::vector fEdgeMax(totalPoints); - std::vector fEdgeMin(totalPoints); - std::vector fRmsMax(totalPoints); - std::vector fRmsMin(totalPoints); - int curPixel = 0; - const int xb = dr.x(); - const int first = focusOnRange ? fromFrame : 0; - const int last = focusOnRange ? toFrame - 1 : m_frames - 1; - // When the number of frames isn't perfectly divisible by the - // width, the remaining frames don't fit the last pixel and are - // past the visible area. lastVisibleFrame is the index number of - // the last visible frame. - const int visibleFrames = (fpp * w); - const int lastVisibleFrame = focusOnRange - ? fromFrame + visibleFrames - 1 - : visibleFrames - 1; - - for (double frame = first; frame <= last && frame <= lastVisibleFrame; frame += fpp) - { - float maxData = -1; - float minData = 1; - - auto rmsData = std::array{}; - - // Find maximum and minimum samples within range - for (int i = 0; i < fpp && frame + i <= last; ++i) - { - for (int j = 0; j < 2; ++j) - { - auto curData = m_data[static_cast(frame) + i][j]; - - if (curData > maxData) { maxData = curData; } - if (curData < minData) { minData = curData; } - - rmsData[j] += curData * curData; - } - } - - const float trueRmsData = (rmsData[0] + rmsData[1]) / 2 / fpp; - const float sqrtRmsData = sqrt(trueRmsData); - const float maxRmsData = std::clamp(sqrtRmsData, minData, maxData); - const float minRmsData = std::clamp(-sqrtRmsData, minData, maxData); - - // If nbFrames >= w, we can use curPixel to calculate X - // but if nbFrames < w, we need to calculate it proportionally - // to the total number of points - auto x = nbFrames >= w - ? xb + curPixel - : xb + ((static_cast(curPixel) / nbFrames) * w); - // Partial Y calculation - auto py = ySpace * m_amplification; - fEdgeMax[curPixel] = QPointF(x, (yb - (maxData * py))); - fEdgeMin[curPixel] = QPointF(x, (yb - (minData * py))); - fRmsMax[curPixel] = QPointF(x, (yb - (maxRmsData * py))); - fRmsMin[curPixel] = QPointF(x, (yb - (minRmsData * py))); - ++curPixel; - } - - for (int i = 0; i < totalPoints; ++i) - { - p.drawLine(fEdgeMax[i], fEdgeMin[i]); - } - - p.setPen(p.pen().color().lighter(123)); - - for (int i = 0; i < totalPoints; ++i) - { - p.drawLine(fRmsMax[i], fRmsMin[i]); - } -} - - - - -QString SampleBuffer::openAudioFile() const -{ - gui::FileDialog ofd(nullptr, tr("Open audio file")); - - QString dir; - if (!m_audioFile.isEmpty()) - { - QString f = m_audioFile; - if (QFileInfo(f).isRelative()) - { - f = ConfigManager::inst()->userSamplesDir() + f; - if (QFileInfo(f).exists() == false) - { - f = ConfigManager::inst()->factorySamplesDir() + - m_audioFile; - } - } - dir = QFileInfo(f).absolutePath(); - } - else - { - dir = ConfigManager::inst()->userSamplesDir(); - } - // change dir to position of previously opened file - ofd.setDirectory(dir); - ofd.setFileMode(gui::FileDialog::ExistingFiles); - - // set filters - QStringList types; - types << tr("All Audio-Files (*.wav *.ogg " -#ifdef LMMS_HAVE_SNDFILE_MP3 - "*.mp3 " -#endif - "*.ds *.flac *.spx *.voc " - "*.aif *.aiff *.au *.raw)") - << tr("Wave-Files (*.wav)") - << tr("OGG-Files (*.ogg)") -#ifdef LMMS_HAVE_SNDFILE_MP3 - << tr("MP3-Files (*.mp3)") -#endif - << tr("DrumSynth-Files (*.ds)") - << tr("FLAC-Files (*.flac)") - << tr("SPEEX-Files (*.spx)") - //<< tr("MIDI-Files (*.mid)") - << tr("VOC-Files (*.voc)") - << tr("AIFF-Files (*.aif *.aiff)") - << tr("AU-Files (*.au)") - << tr("RAW-Files (*.raw)") - //<< tr("MOD-Files (*.mod)") - ; - ofd.setNameFilters(types); - if (!m_audioFile.isEmpty()) - { - // select previously opened file - ofd.selectFile(QFileInfo(m_audioFile).fileName()); - } - - if (ofd.exec () == QDialog::Accepted) - { - if (ofd.selectedFiles().isEmpty()) - { - return QString(); - } - return PathUtil::toShortestRelative(ofd.selectedFiles()[0]); - } - - return QString(); -} - - - - -QString SampleBuffer::openAndSetAudioFile() -{ - QString fileName = this->openAudioFile(); - - if(!fileName.isEmpty()) - { - this->setAudioFile(fileName); - } - - return fileName; -} - - -QString SampleBuffer::openAndSetWaveformFile() -{ - if (m_audioFile.isEmpty()) - { - m_audioFile = ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac"; - } - - QString fileName = this->openAudioFile(); - - if (!fileName.isEmpty()) - { - this->setAudioFile(fileName); - } - else - { - m_audioFile = ""; - } - - return fileName; -} - - - -#undef LMMS_HAVE_FLAC_STREAM_ENCODER_H /* not yet... */ -#undef LMMS_HAVE_FLAC_STREAM_DECODER_H - -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H -FLAC__StreamEncoderWriteStatus flacStreamEncoderWriteCallback( - const FLAC__StreamEncoder * /*encoder*/, - const FLAC__byte buffer[], - unsigned int /*samples*/, - unsigned int bytes, - unsigned int /*currentFrame*/, - void * clientData -) -{ -/* if (bytes == 0) - { - return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; - }*/ - return (static_cast(clientData)->write( - (const char *) buffer, bytes) == (int) bytes) - ? FLAC__STREAM_ENCODER_WRITE_STATUS_OK - : FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; -} - - -void flacStreamEncoderMetadataCallback( - const FLAC__StreamEncoder *, - const FLAC__StreamMetadata * metadata, - void * clientData -) -{ - QBuffer * b = static_cast(clientData); - b->seek(0); - b->write((const char *) metadata, sizeof(*metadata)); -} - -#endif // LMMS_HAVE_FLAC_STREAM_ENCODER_H - - - -QString & SampleBuffer::toBase64(QString & dst) const -{ -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H - const f_cnt_t FRAMES_PER_BUF = 1152; - - FLAC__StreamEncoder * flacEnc = FLAC__stream_encoder_new(); - FLAC__stream_encoder_set_channels(flacEnc, DEFAULT_CHANNELS); - FLAC__stream_encoder_set_blocksize(flacEnc, FRAMES_PER_BUF); -/* FLAC__stream_encoder_set_do_exhaustive_model_search(flacEnc, true); - FLAC__stream_encoder_set_do_mid_side_stereo(flacEnc, true);*/ - FLAC__stream_encoder_set_sample_rate(flacEnc, - Engine::audioEngine()->sampleRate()); - - QBuffer baWriter; - baWriter.open(QBuffer::WriteOnly); - - FLAC__stream_encoder_set_write_callback(flacEnc, - flacStreamEncoderWriteCallback); - FLAC__stream_encoder_set_metadata_callback(flacEnc, - flacStreamEncoderMetadataCallback); - FLAC__stream_encoder_set_client_data(flacEnc, &baWriter); - - if (FLAC__stream_encoder_init(flacEnc) != FLAC__STREAM_ENCODER_OK) - { - printf("Error within FLAC__stream_encoder_init()!\n"); - } - - f_cnt_t frameCnt = 0; - - while (frameCnt < m_frames) - { - f_cnt_t remaining = std::min(FRAMES_PER_BUF, m_frames - frameCnt); - FLAC__int32 buf[FRAMES_PER_BUF * DEFAULT_CHANNELS]; - for (f_cnt_t f = 0; f < remaining; ++f) - { - for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) - { - buf[f*DEFAULT_CHANNELS+ch] = (FLAC__int32)( - AudioEngine::clip(m_data[f+frameCnt][ch]) * - OUTPUT_SAMPLE_MULTIPLIER); - } - } - FLAC__stream_encoder_process_interleaved(flacEnc, buf, remaining); - frameCnt += remaining; - } - FLAC__stream_encoder_finish(flacEnc); - FLAC__stream_encoder_delete(flacEnc); - printf("%d %d\n", frameCnt, (int)baWriter.size()); - baWriter.close(); - - base64::encode(baWriter.buffer().data(), baWriter.buffer().size(), dst); - -#else // LMMS_HAVE_FLAC_STREAM_ENCODER_H - - base64::encode((const char *) m_data, - m_frames * sizeof(sampleFrame), dst); - -#endif // LMMS_HAVE_FLAC_STREAM_ENCODER_H - - return dst; -} - - - - -SampleBuffer * SampleBuffer::resample(const sample_rate_t srcSR, const sample_rate_t dstSR ) -{ - sampleFrame * data = m_data; - const f_cnt_t frames = m_frames; - const auto dstFrames = static_cast((frames / (float)srcSR) * (float)dstSR); - auto dstSB = new SampleBuffer(dstFrames); - sampleFrame * dstBuf = dstSB->m_origData; - - // yeah, libsamplerate, let's rock with sinc-interpolation! - int error; - SRC_STATE * state; - if ((state = src_new(SRC_SINC_MEDIUM_QUALITY, DEFAULT_CHANNELS, &error)) != nullptr) - { - SRC_DATA srcData; - srcData.end_of_input = 1; - srcData.data_in = data->data(); - srcData.data_out = dstBuf->data(); - srcData.input_frames = frames; - srcData.output_frames = dstFrames; - srcData.src_ratio = (double) dstSR / srcSR; - if ((error = src_process(state, &srcData))) - { - printf("SampleBuffer: error while resampling: %s\n", src_strerror(error)); - } - src_delete(state); - } - else - { - printf("Error: src_new() failed in SampleBuffer.cpp!\n"); - } - dstSB->update(); - return dstSB; -} - - - - -void SampleBuffer::setAudioFile(const QString & audioFile) -{ - m_audioFile = PathUtil::toShortestRelative(audioFile); - update(); -} - - - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H - -struct flacStreamDecoderClientData -{ - QBuffer * readBuffer; - QBuffer * writeBuffer; -} ; - - - -FLAC__StreamDecoderReadStatus flacStreamDecoderReadCallback( - const FLAC__StreamDecoder * /*decoder*/, - FLAC__byte * buffer, - unsigned int * bytes, - void * clientData -) -{ - int res = static_cast( - clientData)->readBuffer->read((char *) buffer, *bytes); - - if (res > 0) - { - *bytes = res; - return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; - } - - *bytes = 0; - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; -} - - - - -FLAC__StreamDecoderWriteStatus flacStreamDecoderWriteCallback( - const FLAC__StreamDecoder * /*decoder*/, - const FLAC__Frame * frame, - const FLAC__int32 * const buffer[], - void * clientData -) -{ - if (frame->header.channels != 2) - { - printf("channels != 2 in flacStreamDecoderWriteCallback()\n"); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - - if (frame->header.bits_per_sample != 16) - { - printf("bits_per_sample != 16 in flacStreamDecoderWriteCallback()\n"); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - - const f_cnt_t numberOfFrames = frame->header.blocksize; - for (f_cnt_t f = 0; f < numberOfFrames; ++f) - { - sampleFrame sframe = { buffer[0][f] / OUTPUT_SAMPLE_MULTIPLIER, - buffer[1][f] / OUTPUT_SAMPLE_MULTIPLIER - } ; - static_cast( - clientData )->writeBuffer->write( - (const char *) sframe, sizeof(sframe)); - } - return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; -} - - -void flacStreamDecoderMetadataCallback( - const FLAC__StreamDecoder *, - const FLAC__StreamMetadata *, - void * /*clientData*/ -) -{ - printf("stream decoder metadata callback\n"); -/* QBuffer * b = static_cast(clientData); - b->seek(0); - b->write((const char *) metadata, sizeof(*metadata));*/ -} - - -void flacStreamDecoderErrorCallback( - const FLAC__StreamDecoder *, - FLAC__StreamDecoderErrorStatus status, - void * /*clientData*/ -) -{ - printf("error callback! %d\n", status); - // what to do now?? -} - -#endif // LMMS_HAVE_FLAC_STREAM_DECODER_H - - -void SampleBuffer::loadFromBase64(const QString & data) -{ - char * dst = nullptr; - int dsize = 0; - base64::decode(data, &dst, &dsize); - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H - - QByteArray origData = QByteArray::fromRawData(dst, dsize); - QBuffer baReader(&origData); - baReader.open(QBuffer::ReadOnly); - - QBuffer baWriter; - baWriter.open(QBuffer::WriteOnly); - - flacStreamDecoderClientData cdata = { &baReader, &baWriter } ; - - FLAC__StreamDecoder * flacDec = FLAC__stream_decoder_new(); - - FLAC__stream_decoder_set_read_callback(flacDec, - flacStreamDecoderReadCallback); - FLAC__stream_decoder_set_write_callback(flacDec, - flacStreamDecoderWriteCallback); - FLAC__stream_decoder_set_error_callback(flacDec, - flacStreamDecoderErrorCallback); - FLAC__stream_decoder_set_metadata_callback(flacDec, - flacStreamDecoderMetadataCallback); - FLAC__stream_decoder_set_client_data(flacDec, &cdata); - - FLAC__stream_decoder_init(flacDec); - - FLAC__stream_decoder_process_until_end_of_stream(flacDec); - - FLAC__stream_decoder_finish(flacDec); - FLAC__stream_decoder_delete(flacDec); - - baReader.close(); - - origData = baWriter.buffer(); - printf("%d\n", (int) origData.size()); - - m_origFrames = origData.size() / sizeof(sampleFrame); - MM_FREE(m_origData); - m_origData = MM_ALLOC( m_origFrames); - memcpy(m_origData, origData.data(), origData.size()); - -#else /* LMMS_HAVE_FLAC_STREAM_DECODER_H */ - - m_origFrames = dsize / sizeof(sampleFrame); - MM_FREE(m_origData); - m_origData = MM_ALLOC( m_origFrames); - memcpy(m_origData, dst, dsize); - -#endif // LMMS_HAVE_FLAC_STREAM_DECODER_H - - delete[] dst; - - m_audioFile = QString(); - update(); -} - - - - -void SampleBuffer::setStartFrame(const f_cnt_t s) -{ - m_startFrame = s; -} - - - - -void SampleBuffer::setEndFrame(const f_cnt_t e) -{ - m_endFrame = e; -} - - - - -void SampleBuffer::setAmplification(float a) -{ - m_amplification = a; - emit sampleUpdated(); -} - - - - -void SampleBuffer::setReversed(bool on) -{ - Engine::audioEngine()->requestChangeInModel(); - m_varLock.lockForWrite(); - if (m_reversed != on) { std::reverse(m_data, m_data + m_frames); } - m_reversed = on; - m_varLock.unlock(); - Engine::audioEngine()->doneChangeInModel(); - emit sampleUpdated(); -} - - - - - -SampleBuffer::handleState::handleState(bool varyingPitch, int interpolationMode) : - m_frameIndex(0), - m_varyingPitch(varyingPitch), - m_isBackwards(false) -{ - int error; - m_interpolationMode = interpolationMode; - - if ((m_resamplingData = src_new(interpolationMode, DEFAULT_CHANNELS, &error)) == nullptr) - { - qDebug("Error: src_new() failed in SampleBuffer.cpp!\n"); - } -} - - - - -SampleBuffer::handleState::~handleState() -{ - src_delete(m_resamplingData); + static auto s_buffer = std::make_shared(); + return s_buffer; } } // namespace lmms diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index ba8420a41..42d4f6441 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -25,21 +25,22 @@ #include "SampleClip.h" #include +#include +#include "PathUtil.h" #include "SampleBuffer.h" #include "SampleClipView.h" +#include "SampleLoader.h" #include "SampleTrack.h" #include "TimeLineWidget.h" - namespace lmms { - -SampleClip::SampleClip( Track * _track ) : - Clip( _track ), - m_sampleBuffer( new SampleBuffer ), - m_isPlaying( false ) +SampleClip::SampleClip(Track* _track, Sample sample, bool isPlaying) + : Clip(_track) + , m_sample(std::move(sample)) + , m_isPlaying(false) { saveJournallingState( false ); setSampleFile( "" ); @@ -81,14 +82,14 @@ SampleClip::SampleClip( Track * _track ) : updateTrackClips(); } -SampleClip::SampleClip(const SampleClip& orig) : - SampleClip(orig.getTrack()) +SampleClip::SampleClip(Track* track) + : SampleClip(track, Sample(), false) +{ +} + +SampleClip::SampleClip(const SampleClip& orig) : + SampleClip(orig.getTrack(), orig.m_sample, orig.m_isPlaying) { - // TODO: This creates a new SampleBuffer for the new Clip, eating up memory - // & eventually causing performance issues. Letting tracks share buffers - // when they're identical would fix this, but isn't possible right now. - *m_sampleBuffer = *orig.m_sampleBuffer; - m_isPlaying = orig.m_isPlaying; } @@ -101,9 +102,6 @@ SampleClip::~SampleClip() { sampletrack->updateClips(); } - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); } @@ -117,33 +115,30 @@ void SampleClip::changeLength( const TimePos & _length ) -const QString & SampleClip::sampleFile() const +const QString& SampleClip::sampleFile() const { - return m_sampleBuffer->audioFile(); + return m_sample.sampleFile(); } - - -void SampleClip::setSampleBuffer( SampleBuffer* sb ) +void SampleClip::setSampleBuffer(std::shared_ptr sb) { - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); - m_sampleBuffer = sb; + { + const auto guard = Engine::audioEngine()->requestChangesGuard(); + m_sample = Sample(std::move(sb)); + } updateLength(); emit sampleChanged(); } - - -void SampleClip::setSampleFile(const QString & sf) +void SampleClip::setSampleFile(const QString& sf) { int length = 0; if (!sf.isEmpty()) { - m_sampleBuffer->setAudioFile(sf); + //Otherwise set it to the sample's length + m_sample = Sample(gui::SampleLoader::createBufferFromFile(sf)); length = sampleLength(); } @@ -222,7 +217,7 @@ void SampleClip::updateLength() TimePos SampleClip::sampleLength() const { - return (int)( m_sampleBuffer->frames() / Engine::framesPerTick() ); + return static_cast(m_sample.sampleSize() / Engine::framesPerTick(m_sample.sampleRate())); } @@ -230,7 +225,7 @@ TimePos SampleClip::sampleLength() const void SampleClip::setSampleStartFrame(f_cnt_t startFrame) { - m_sampleBuffer->setStartFrame( startFrame ); + m_sample.setStartFrame(startFrame); } @@ -238,7 +233,7 @@ void SampleClip::setSampleStartFrame(f_cnt_t startFrame) void SampleClip::setSamplePlayLength(f_cnt_t length) { - m_sampleBuffer->setEndFrame( length ); + m_sample.setEndFrame(length); } @@ -261,15 +256,15 @@ void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) if( sampleFile() == "" ) { QString s; - _this.setAttribute( "data", m_sampleBuffer->toBase64( s ) ); + _this.setAttribute("data", m_sample.toBase64()); } - _this.setAttribute( "sample_rate", m_sampleBuffer->sampleRate()); + _this.setAttribute( "sample_rate", m_sample.sampleRate()); if (const auto& c = color()) { _this.setAttribute("color", c->name()); } - if (m_sampleBuffer->reversed()) + if (m_sample.reversed()) { _this.setAttribute("reversed", "true"); } @@ -285,14 +280,23 @@ void SampleClip::loadSettings( const QDomElement & _this ) { movePosition( _this.attribute( "pos" ).toInt() ); } - setSampleFile( _this.attribute( "src" ) ); + + if (const auto srcFile = _this.attribute("src"); !srcFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) + { + setSampleFile(srcFile); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), srcFile)); } + } + if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) { - m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) ); - if (_this.hasAttribute("sample_rate")) - { - m_sampleBuffer->setSampleRate(_this.attribute("sample_rate").toInt()); - } + auto sampleRate = _this.hasAttribute("sample_rate") ? _this.attribute("sample_rate").toInt() : + Engine::audioEngine()->processingSampleRate(); + + auto buffer = gui::SampleLoader::createBufferFromBase64(_this.attribute("data"), sampleRate); + m_sample = Sample(std::move(buffer)); } changeLength( _this.attribute( "len" ).toInt() ); setMuted( _this.attribute( "muted" ).toInt() ); @@ -305,7 +309,7 @@ void SampleClip::loadSettings( const QDomElement & _this ) if(_this.hasAttribute("reversed")) { - m_sampleBuffer->setReversed(true); + m_sample.setReversed(true); emit wasReversed(); // tell SampleClipView to update the view } } diff --git a/src/core/SampleDecoder.cpp b/src/core/SampleDecoder.cpp new file mode 100644 index 000000000..4db0d438a --- /dev/null +++ b/src/core/SampleDecoder.cpp @@ -0,0 +1,184 @@ +#include "SampleDecoder.h" + +#include +#include +#include +#include +#include + +#ifdef LMMS_HAVE_OGGVORBIS +#include +#endif + +#include "AudioEngine.h" +#include "DrumSynth.h" +#include "Engine.h" +#include "lmms_basics.h" + +namespace lmms { + +namespace { + +using Decoder = std::optional(*)(const QString&); + +auto decodeSampleSF(const QString& audioFile) -> std::optional; +auto decodeSampleDS(const QString& audioFile) -> std::optional; +#ifdef LMMS_HAVE_OGGVORBIS +auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional; +#endif + +static constexpr std::array decoders = {&decodeSampleSF, +#ifdef LMMS_HAVE_OGGVORBIS + &decodeSampleOggVorbis, +#endif + &decodeSampleDS}; + +auto decodeSampleSF(const QString& audioFile) -> std::optional +{ + SNDFILE* sndFile = nullptr; + auto sfInfo = SF_INFO{}; + + // Use QFile to handle unicode file names on Windows + auto file = QFile{audioFile}; + if (!file.open(QIODevice::ReadOnly)) { return std::nullopt; } + + sndFile = sf_open_fd(file.handle(), SFM_READ, &sfInfo, false); + if (sf_error(sndFile) != 0) { return std::nullopt; } + + auto buf = std::vector(sfInfo.channels * sfInfo.frames); + sf_read_float(sndFile, buf.data(), buf.size()); + + sf_close(sndFile); + file.close(); + + auto result = std::vector(sfInfo.frames); + for (int i = 0; i < static_cast(result.size()); ++i) + { + if (sfInfo.channels == 1) + { + // Upmix from mono to stereo + result[i] = {buf[i], buf[i]}; + } + else if (sfInfo.channels > 1) + { + // TODO: Add support for higher number of channels (i.e., 5.1 channel systems) + // The current behavior assumes stereo in all cases excluding mono. + // This may not be the expected behavior, given some audio files with a higher number of channels. + result[i] = {buf[i * sfInfo.channels], buf[i * sfInfo.channels + 1]}; + } + } + + return SampleDecoder::Result{std::move(result), static_cast(sfInfo.samplerate)}; +} + +auto decodeSampleDS(const QString& audioFile) -> std::optional +{ + // Populated by DrumSynth::GetDSFileSamples + int_sample_t* dataPtr = nullptr; + + auto ds = DrumSynth{}; + const auto engineRate = Engine::audioEngine()->processingSampleRate(); + const auto frames = ds.GetDSFileSamples(audioFile, dataPtr, DEFAULT_CHANNELS, engineRate); + const auto data = std::unique_ptr{dataPtr}; // NOLINT, we have to use a C-style array here + + if (frames <= 0 || !data) { return std::nullopt; } + + auto result = std::vector(frames); + src_short_to_float_array(data.get(), &result[0][0], frames * DEFAULT_CHANNELS); + + return SampleDecoder::Result{std::move(result), static_cast(engineRate)}; +} + +#ifdef LMMS_HAVE_OGGVORBIS +auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional +{ + auto vorbisFile = OggVorbis_File{}; + const auto openError = ov_fopen(audioFile.toLocal8Bit(), &vorbisFile); + + if (openError != 0) { return std::nullopt; } + + const auto vorbisInfo = ov_info(&vorbisFile, -1); + const auto numChannels = vorbisInfo->channels; + const auto sampleRate = vorbisInfo->rate; + const auto numSamples = ov_pcm_total(&vorbisFile, -1); + + auto buffer = std::vector(numSamples); + auto output = static_cast(nullptr); + + auto totalSamplesRead = 0; + while (true) + { + auto samplesRead = ov_read_float(&vorbisFile, &output, numSamples, 0); + + if (samplesRead < 0) { return std::nullopt; } + else if (samplesRead == 0) { break; } + + std::copy_n(*output, samplesRead, buffer.begin() + totalSamplesRead); + totalSamplesRead += samplesRead; + } + + ov_clear(&vorbisFile); + auto result = std::vector(numSamples / numChannels); + for (int i = 0; i < buffer.size(); ++i) + { + if (numChannels == 1) { result[i] = {buffer[i], buffer[i]}; } + else if (numChannels > 1) { result[i] = {buffer[i * numChannels], buffer[i * numChannels + 1]}; } + } + + return SampleDecoder::Result{std::move(result), static_cast(sampleRate)}; +} +#endif // LMMS_HAVE_OGGVORBIS +} // namespace + +auto SampleDecoder::supportedAudioTypes() -> const std::vector& +{ + static const auto s_audioTypes = [] + { + auto types = std::vector(); + + // Add DrumSynth by default since that support comes from us + types.push_back(AudioType{"DrumSynth", "ds"}); + + auto sfFormatInfo = SF_FORMAT_INFO{}; + auto simpleTypeCount = 0; + sf_command(nullptr, SFC_GET_SIMPLE_FORMAT_COUNT, &simpleTypeCount, sizeof(int)); + + // TODO: Ideally, this code should be iterating over the major formats, but some important extensions such as *.ogg + // are not included. This is planned for future versions of sndfile. + for (int simple = 0; simple < simpleTypeCount; ++simple) + { + sfFormatInfo.format = simple; + sf_command(nullptr, SFC_GET_SIMPLE_FORMAT, &sfFormatInfo, sizeof(sfFormatInfo)); + + auto it = std::find_if(types.begin(), types.end(), + [&](const AudioType& type) { return sfFormatInfo.extension == type.extension; }); + if (it != types.end()) { continue; } + + auto name = std::string{sfFormatInfo.extension}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char ch) { return std::toupper(ch); }); + + types.push_back(AudioType{std::move(name), sfFormatInfo.extension}); + + return types; + } + + std::sort(types.begin(), types.end(), + [&](const AudioType& a, const AudioType& b) { return a.name < b.name; }); + return types; + }(); + return s_audioTypes; +} + +auto SampleDecoder::decode(const QString& audioFile) -> std::optional +{ + auto result = std::optional{}; + for (const auto& decoder : decoders) + { + result = decoder(audioFile); + if (result) { break; } + } + + return result; +} + +} // namespace lmms diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp index ea27146cb..61ded132a 100644 --- a/src/core/SamplePlayHandle.cpp +++ b/src/core/SamplePlayHandle.cpp @@ -35,9 +35,9 @@ namespace lmms { -SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort ) : +SamplePlayHandle::SamplePlayHandle(Sample* sample, bool ownAudioPort) : PlayHandle( Type::SamplePlayHandle ), - m_sampleBuffer( sharedObject::ref( sampleBuffer ) ), + m_sample(sample), m_doneMayReturnTrue( true ), m_frame( 0 ), m_ownAudioPort( ownAudioPort ), @@ -56,16 +56,15 @@ SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPo SamplePlayHandle::SamplePlayHandle( const QString& sampleFile ) : - SamplePlayHandle( new SampleBuffer( sampleFile ) , true) + SamplePlayHandle(new Sample(sampleFile), true) { - sharedObject::unref( m_sampleBuffer ); } SamplePlayHandle::SamplePlayHandle( SampleClip* clip ) : - SamplePlayHandle( clip->sampleBuffer() , false) + SamplePlayHandle(&clip->sample(), false) { m_track = clip->getTrack(); setAudioPort( ( (SampleTrack *)clip->getTrack() )->audioPort() ); @@ -76,10 +75,10 @@ SamplePlayHandle::SamplePlayHandle( SampleClip* clip ) : SamplePlayHandle::~SamplePlayHandle() { - sharedObject::unref( m_sampleBuffer ); if( m_ownAudioPort ) { delete audioPort(); + delete m_sample; } } @@ -115,7 +114,7 @@ void SamplePlayHandle::play( sampleFrame * buffer ) m_volumeModel->value() / DefaultVolume } };*/ // SamplePlayHandle always plays the sample at its original pitch; // it is used only for previews, SampleTracks and the metronome. - if (!m_sampleBuffer->play(workingBuffer, &m_state, frames, DefaultBaseFreq)) + if (!m_sample->play(workingBuffer, &m_state, frames, DefaultBaseFreq)) { memset(workingBuffer, 0, frames * sizeof(sampleFrame)); } @@ -145,8 +144,8 @@ bool SamplePlayHandle::isFromTrack( const Track * _track ) const f_cnt_t SamplePlayHandle::totalFrames() const { - return ( m_sampleBuffer->endFrame() - m_sampleBuffer->startFrame() ) * - ( Engine::audioEngine()->processingSampleRate() / m_sampleBuffer->sampleRate() ); + return (m_sample->endFrame() - m_sample->startFrame()) * + (static_cast(Engine::audioEngine()->processingSampleRate()) / m_sample->sampleRate()); } diff --git a/src/core/SampleRecordHandle.cpp b/src/core/SampleRecordHandle.cpp index 10e970b8f..6857efa83 100644 --- a/src/core/SampleRecordHandle.cpp +++ b/src/core/SampleRecordHandle.cpp @@ -51,13 +51,8 @@ SampleRecordHandle::SampleRecordHandle( SampleClip* clip ) : SampleRecordHandle::~SampleRecordHandle() { - if( !m_buffers.empty() ) - { - SampleBuffer* sb; - createSampleBuffer( &sb ); - m_clip->setSampleBuffer( sb ); - } - + if (!m_buffers.empty()) { m_clip->setSampleBuffer(createSampleBuffer()); } + while( !m_buffers.empty() ) { delete[] m_buffers.front().first; @@ -111,28 +106,22 @@ f_cnt_t SampleRecordHandle::framesRecorded() const -void SampleRecordHandle::createSampleBuffer( SampleBuffer** sampleBuf ) +std::shared_ptr SampleRecordHandle::createSampleBuffer() { const f_cnt_t frames = framesRecorded(); // create buffer to store all recorded buffers in - auto data = new sampleFrame[frames]; - // make sure buffer is cleaned up properly at the end... - sampleFrame * data_ptr = data; - - - assert( data != nullptr ); + auto bigBuffer = std::vector(frames); // now copy all buffers into big buffer - for( bufferList::const_iterator it = m_buffers.begin(); it != m_buffers.end(); ++it ) + auto framesCopied = 0; + for (const auto& [buf, numFrames] : m_buffers) { - memcpy( data_ptr, ( *it ).first, ( *it ).second * - sizeof( sampleFrame ) ); - data_ptr += ( *it ).second; + std::copy_n(buf, numFrames, bigBuffer.begin() + framesCopied); + framesCopied += numFrames; } + // create according sample-buffer out of big buffer - *sampleBuf = new SampleBuffer( data, frames ); - ( *sampleBuf)->setSampleRate( Engine::audioEngine()->inputSampleRate() ); - delete[] data; + return std::make_shared(std::move(bigBuffer), Engine::audioEngine()->inputSampleRate()); } diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 7a664a11e..13c1d6988 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -283,10 +283,9 @@ void Track::loadSettings( const QDomElement & element ) return; } - while( !m_clips.empty() ) { - delete m_clips.front(); -// m_clips.erase( m_clips.begin() ); + auto guard = Engine::audioEngine()->requestChangesGuard(); + deleteClips(); } QDomNode node = element.firstChild(); diff --git a/src/core/audio/AudioSampleRecorder.cpp b/src/core/audio/AudioSampleRecorder.cpp index f60248c50..b5bbf5a8f 100644 --- a/src/core/audio/AudioSampleRecorder.cpp +++ b/src/core/audio/AudioSampleRecorder.cpp @@ -67,32 +67,22 @@ f_cnt_t AudioSampleRecorder::framesRecorded() const return frames; } - - - -void AudioSampleRecorder::createSampleBuffer( SampleBuffer** sampleBuf ) +std::shared_ptr AudioSampleRecorder::createSampleBuffer() { const f_cnt_t frames = framesRecorded(); // create buffer to store all recorded buffers in - auto data = new sampleFrame[frames]; - // make sure buffer is cleaned up properly at the end... - sampleFrame * data_ptr = data; - - - assert( data != nullptr ); + auto bigBuffer = std::vector(frames); // now copy all buffers into big buffer - for( BufferList::ConstIterator it = m_buffers.begin(); - it != m_buffers.end(); ++it ) + auto framesCopied = 0; + for (const auto& [buf, numFrames] : m_buffers) { - memcpy( data_ptr, ( *it ).first, ( *it ).second * - sizeof( sampleFrame ) ); - data_ptr += ( *it ).second; + std::copy_n(buf, numFrames, bigBuffer.begin() + framesCopied); + framesCopied += numFrames; } + // create according sample-buffer out of big buffer - *sampleBuf = new SampleBuffer( data, frames ); - ( *sampleBuf )->setSampleRate( sampleRate() ); - delete[] data; + return std::make_shared(std::move(bigBuffer), sampleRate()); } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index e050d14bd..ac010f4f4 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -34,7 +34,9 @@ SET(LMMS_SRCS gui/PluginBrowser.cpp gui/ProjectNotes.cpp gui/RowTableView.cpp + gui/SampleLoader.cpp gui/SampleTrackWindow.cpp + gui/SampleWaveform.cpp gui/SendButtonIndicator.cpp gui/SideBar.cpp gui/SideBarWidget.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 32f29988b..89201cb04 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -57,7 +57,9 @@ #include "PatternStore.h" #include "PluginFactory.h" #include "PresetPreviewPlayHandle.h" +#include "Sample.h" #include "SampleClip.h" +#include "SampleLoader.h" #include "SamplePlayHandle.h" #include "SampleTrack.h" #include "Song.h" @@ -715,9 +717,12 @@ void FileBrowserTreeWidget::previewFileItem(FileItem* file) embed::getIconPixmap("sample_file", 24, 24), 0); // TODO: this can be removed once we do this outside the event thread qApp->processEvents(QEventLoop::ExcludeUserInputEvents); - auto s = new SamplePlayHandle(fileName); - s->setDoneMayReturnTrue(false); - newPPH = s; + if (auto buffer = SampleLoader::createBufferFromFile(fileName)) + { + auto s = new SamplePlayHandle(new lmms::Sample{std::move(buffer)}); + s->setDoneMayReturnTrue(false); + newPPH = s; + } delete tf; } else if ( diff --git a/src/gui/LfoControllerDialog.cpp b/src/gui/LfoControllerDialog.cpp index 77362b169..559ac1336 100644 --- a/src/gui/LfoControllerDialog.cpp +++ b/src/gui/LfoControllerDialog.cpp @@ -31,6 +31,7 @@ #include "Knob.h" #include "TempoSyncKnob.h" #include "PixmapButton.h" +#include "SampleLoader.h" namespace lmms::gui { @@ -210,14 +211,14 @@ LfoControllerDialog::~LfoControllerDialog() void LfoControllerDialog::askUserDefWave() { - SampleBuffer * sampleBuffer = dynamic_cast(this->model())-> - m_userDefSampleBuffer; - QString fileName = sampleBuffer->openAndSetWaveformFile(); - if( fileName.isEmpty() == false ) - { - // TODO: - m_userWaveBtn->setToolTip(sampleBuffer->audioFile()); - } + const auto fileName = SampleLoader::openWaveformFile(); + if (fileName.isEmpty()) { return; } + + auto lfoModel = dynamic_cast(model()); + auto& buffer = lfoModel->m_userDefSampleBuffer; + buffer = SampleLoader::createBufferFromFile(fileName); + + m_userWaveBtn->setToolTip(buffer->audioFile()); } diff --git a/src/gui/SampleLoader.cpp b/src/gui/SampleLoader.cpp new file mode 100644 index 000000000..f2340852d --- /dev/null +++ b/src/gui/SampleLoader.cpp @@ -0,0 +1,126 @@ +/* + * SampleLoader.cpp - Static functions that open audio files + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SampleLoader.h" + +#include +#include +#include + +#include "ConfigManager.h" +#include "FileDialog.h" +#include "GuiApplication.h" +#include "PathUtil.h" +#include "SampleDecoder.h" +#include "Song.h" + +namespace lmms::gui { +QString SampleLoader::openAudioFile(const QString& previousFile) +{ + auto openFileDialog = FileDialog(nullptr, QObject::tr("Open audio file")); + auto dir = !previousFile.isEmpty() ? PathUtil::toAbsolute(previousFile) : ConfigManager::inst()->userSamplesDir(); + + // change dir to position of previously opened file + openFileDialog.setDirectory(dir); + openFileDialog.setFileMode(FileDialog::ExistingFiles); + + // set filters + auto fileTypes = QStringList{}; + auto allFileTypes = QStringList{}; + auto nameFilters = QStringList{}; + const auto& supportedAudioTypes = SampleDecoder::supportedAudioTypes(); + + for (const auto& audioType : supportedAudioTypes) + { + const auto name = QString::fromStdString(audioType.name); + const auto extension = QString::fromStdString(audioType.extension); + const auto displayExtension = QString{"*.%1"}.arg(extension); + fileTypes.append(QString{"%1 (%2)"}.arg(FileDialog::tr("%1 files").arg(name), displayExtension)); + allFileTypes.append(displayExtension); + } + + nameFilters.append(QString{"%1 (%2)"}.arg(FileDialog::tr("All audio files"), allFileTypes.join(" "))); + nameFilters.append(fileTypes); + nameFilters.append(QString("%1 (*)").arg(FileDialog::tr("Other files"))); + + openFileDialog.setNameFilters(nameFilters); + + if (!previousFile.isEmpty()) + { + // select previously opened file + openFileDialog.selectFile(QFileInfo{previousFile}.fileName()); + } + + if (openFileDialog.exec() == QDialog::Accepted) + { + if (openFileDialog.selectedFiles().isEmpty()) { return ""; } + + return PathUtil::toShortestRelative(openFileDialog.selectedFiles()[0]); + } + + return ""; +} + +QString SampleLoader::openWaveformFile(const QString& previousFile) +{ + return openAudioFile( + previousFile.isEmpty() ? ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac" : previousFile); +} + +std::shared_ptr SampleLoader::createBufferFromFile(const QString& filePath) +{ + if (filePath.isEmpty()) { return SampleBuffer::emptyBuffer(); } + + try + { + return std::make_shared(filePath); + } + catch (const std::runtime_error& error) + { + if (getGUI()) { displayError(QString::fromStdString(error.what())); } + return SampleBuffer::emptyBuffer(); + } +} + +std::shared_ptr SampleLoader::createBufferFromBase64(const QString& base64, int sampleRate) +{ + if (base64.isEmpty()) { return SampleBuffer::emptyBuffer(); } + + try + { + return std::make_shared(base64, sampleRate); + } + catch (const std::runtime_error& error) + { + if (getGUI()) { displayError(QString::fromStdString(error.what())); } + return SampleBuffer::emptyBuffer(); + } +} + +void SampleLoader::displayError(const QString& message) +{ + QMessageBox::critical(nullptr, QObject::tr("Error loading sample"), message); +} + +} // namespace lmms::gui diff --git a/src/gui/SampleWaveform.cpp b/src/gui/SampleWaveform.cpp new file mode 100644 index 000000000..5d3afdee3 --- /dev/null +++ b/src/gui/SampleWaveform.cpp @@ -0,0 +1,94 @@ +/* + * SampleWaveform.cpp + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SampleWaveform.h" + +namespace lmms::gui { + +void SampleWaveform::visualize(const Sample& sample, QPainter& p, const QRect& dr, int fromFrame, int toFrame) +{ + if (sample.sampleSize() == 0) { return; } + + const auto x = dr.x(); + const auto height = dr.height(); + const auto width = dr.width(); + const auto centerY = dr.center().y(); + + const auto halfHeight = height / 2; + const auto buffer = sample.data() + fromFrame; + + const auto color = p.pen().color(); + const auto rmsColor = color.lighter(123); + + auto numFrames = toFrame - fromFrame; + if (numFrames == 0) { numFrames = sample.sampleSize(); } + + const auto framesPerPixel = std::max(1, numFrames / width); + + constexpr auto maxFramesPerPixel = 512; + const auto resolution = std::max(1, framesPerPixel / maxFramesPerPixel); + const auto framesPerResolution = framesPerPixel / resolution; + + const auto numPixels = std::min(numFrames, width); + auto min = std::vector(numPixels, 1); + auto max = std::vector(numPixels, -1); + auto squared = std::vector(numPixels); + + const auto maxFrames = numPixels * framesPerPixel; + for (int i = 0; i < maxFrames; i += resolution) + { + const auto pixelIndex = i / framesPerPixel; + const auto value = std::accumulate(buffer[i].begin(), buffer[i].end(), 0.0f) / buffer[i].size(); + if (value > max[pixelIndex]) { max[pixelIndex] = value; } + if (value < min[pixelIndex]) { min[pixelIndex] = value; } + squared[pixelIndex] += value * value; + } + + const auto amplification = sample.amplification(); + const auto reversed = sample.reversed(); + + for (int i = 0; i < numPixels; i++) + { + const auto lineY1 = centerY - max[i] * halfHeight * amplification; + const auto lineY2 = centerY - min[i] * halfHeight * amplification; + + auto lineX = i + x; + if (reversed) { lineX = width - lineX; } + + p.drawLine(lineX, lineY1, lineX, lineY2); + + const auto rms = std::sqrt(squared[i] / framesPerResolution); + const auto maxRMS = std::clamp(rms, min[i], max[i]); + const auto minRMS = std::clamp(-rms, min[i], max[i]); + + const auto rmsLineY1 = centerY - maxRMS * halfHeight * amplification; + const auto rmsLineY2 = centerY - minRMS * halfHeight * amplification; + + p.setPen(rmsColor); + p.drawLine(lineX, rmsLineY1, lineX, rmsLineY2); + p.setPen(color); + } +} + +} // namespace lmms::gui diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index b2ad5c99c..5c8a12b91 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -294,6 +294,17 @@ void ClipView::remove() // delete ourself close(); + + if (m_clip->getTrack()) + { + auto guard = Engine::audioEngine()->requestChangesGuard(); + m_clip->getTrack()->removeClip(m_clip); + } + + // TODO: Clip::~Clip should not be responsible for removing the Clip from the Track. + // One would expect that a call to Track::removeClip would already do that for you, as well + // as actually deleting the Clip with the deleteLater function. That being said, it shouldn't + // be possible to make a Clip without a Track (i.e., Clip::getTrack is never nullptr). m_clip->deleteLater(); } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index 81bbd271d..8f3163385 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -32,8 +32,9 @@ #include "AutomationEditor.h" #include "embed.h" #include "PathUtil.h" -#include "SampleBuffer.h" #include "SampleClip.h" +#include "SampleLoader.h" +#include "SampleWaveform.h" #include "Song.h" #include "StringPairDrag.h" @@ -62,9 +63,11 @@ void SampleClipView::updateSample() update(); // set tooltip to filename so that user can see what sample this // sample-clip contains - setToolTip(m_clip->m_sampleBuffer->audioFile() != "" ? - PathUtil::toAbsolute(m_clip->m_sampleBuffer->audioFile()) : - tr( "Double-click to open sample" ) ); + setToolTip( + !m_clip->m_sample.sampleFile().isEmpty() + ? PathUtil::toAbsolute(m_clip->m_sample.sampleFile()) + : tr("Double-click to open sample") + ); } @@ -120,8 +123,7 @@ void SampleClipView::dropEvent( QDropEvent * _de ) } else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) { - m_clip->m_sampleBuffer->loadFromBase64( - StringPairDrag::decodeValue( _de ) ); + m_clip->setSampleBuffer(SampleLoader::createBufferFromBase64(StringPairDrag::decodeValue(_de))); m_clip->updateLength(); update(); _de->accept(); @@ -179,12 +181,12 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { - QString af = m_clip->m_sampleBuffer->openAudioFile(); + QString af = SampleLoader::openAudioFile(); if ( af.isEmpty() ) {} //Don't do anything if no file is loaded - else if ( af == m_clip->m_sampleBuffer->audioFile() ) + else if (af == m_clip->m_sample.sampleFile()) { //Instead of reloading the existing file, just reset the size - int length = (int) ( m_clip->m_sampleBuffer->frames() / Engine::framesPerTick() ); + int length = static_cast(m_clip->m_sample.sampleSize() / Engine::framesPerTick()); m_clip->changeLength(length); } else @@ -267,9 +269,9 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) float offset = m_clip->startTimeOffset() / ticksPerBar * pixelsPerBar(); QRect r = QRect( offset, spacing, qMax( static_cast( m_clip->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing ); - m_clip->m_sampleBuffer->visualize( p, r, pe->rect() ); + SampleWaveform::visualize(m_clip->m_sample, p, r); - QString name = PathUtil::cleanName(m_clip->m_sampleBuffer->audioFile()); + QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); paintTextLabel(name, p); // disable antialiasing for borders, since its not needed @@ -322,7 +324,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) void SampleClipView::reverseSample() { - m_clip->sampleBuffer()->setReversed(!m_clip->sampleBuffer()->reversed()); + m_clip->m_sample.setReversed(!m_clip->m_sample.reversed()); Engine::getSong()->setModified(); update(); } diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index e7153bfa3..90881c7ef 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -39,6 +39,7 @@ #include #include "SampleClip.h" +#include "SampleWaveform.h" #ifndef __USE_XOPEN #define __USE_XOPEN @@ -1235,9 +1236,9 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } // draw ghost sample - if (m_ghostSample != nullptr && m_ghostSample->sampleBuffer()->frames() > 1 && m_renderSample) + if (m_ghostSample != nullptr && m_ghostSample->sample().sampleSize() > 1 && m_renderSample) { - int sampleFrames = m_ghostSample->sampleBuffer()->frames(); + int sampleFrames = m_ghostSample->sample().sampleSize(); int length = static_cast(sampleFrames) / Engine::framesPerTick(); int editorHeight = grid_bottom - TOP_MARGIN; @@ -1247,7 +1248,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN; p.setPen(m_ghostSampleColor); - m_ghostSample->sampleBuffer()->visualize(p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); + SampleWaveform::visualize(m_ghostSample->sample(), p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); } // draw ghost notes diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index 2a1928326..c3bf53b39 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -28,6 +28,7 @@ #include "EnvelopeAndLfoView.h" #include "EnvelopeAndLfoParameters.h" +#include "SampleLoader.h" #include "embed.h" #include "Engine.h" #include "gui_templates.h" @@ -306,8 +307,7 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) QString value = StringPairDrag::decodeValue( _de ); if( type == "samplefile" ) { - m_params->m_userWave.setAudioFile( - StringPairDrag::decodeValue( _de ) ); + m_params->m_userWave = SampleLoader::createBufferFromFile(value); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -316,9 +316,10 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) { DataFile dataFile( value.toUtf8() ); - m_params->m_userWave.setAudioFile( dataFile.content(). + auto file = dataFile.content(). firstChildElement().firstChildElement(). - firstChildElement().attribute( "src" ) ); + firstChildElement().attribute("src"); + m_params->m_userWave = SampleLoader::createBufferFromFile(file); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -428,8 +429,6 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) osc_frames *= 100.0f; } - // userWaveSample() may be used, called out of loop for efficiency - m_params->m_userWave.dataReadLock(); float old_y = 0; for( int x = 0; x <= LFO_GRAPH_W; ++x ) { @@ -465,8 +464,7 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) val = m_randomGraph; break; case EnvelopeAndLfoParameters::LfoShape::UserDefinedWave: - val = m_params->m_userWave. - userWaveSample( phase ); + val = Oscillator::userWaveSample(m_params->m_userWave.get(), phase); break; } if( static_cast( cur_sample ) <= @@ -481,7 +479,6 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) graph_y_base + cur_y ) ); old_y = cur_y; } - m_params->m_userWave.dataUnlock(); p.setPen( QColor( 201, 201, 225 ) ); int ms_per_osc = static_cast( SECS_PER_LFO_OSCILLATION * @@ -499,7 +496,7 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() if( static_cast(m_params->m_lfoWaveModel.value()) == EnvelopeAndLfoParameters::LfoShape::UserDefinedWave ) { - if( m_params->m_userWave.frames() <= 1 ) + if (m_params->m_userWave->size() <= 1) { TextFloat::displayMessage( tr( "Hint" ), tr( "Drag and drop a sample into this window." ), diff --git a/src/gui/widgets/Graph.cpp b/src/gui/widgets/Graph.cpp index 9972209a8..922b98668 100644 --- a/src/gui/widgets/Graph.cpp +++ b/src/gui/widgets/Graph.cpp @@ -26,6 +26,7 @@ #include #include "Graph.h" +#include "SampleLoader.h" #include "StringPairDrag.h" #include "SampleBuffer.h" #include "Oscillator.h" @@ -588,21 +589,16 @@ void graphModel::setWaveToNoise() QString graphModel::setWaveToUser() { - auto sampleBuffer = new SampleBuffer; - QString fileName = sampleBuffer->openAndSetWaveformFile(); + QString fileName = gui::SampleLoader::openWaveformFile(); if( fileName.isEmpty() == false ) { - sampleBuffer->dataReadLock(); + auto sampleBuffer = gui::SampleLoader::createBufferFromFile(fileName); for( int i = 0; i < length(); i++ ) { - m_samples[i] = sampleBuffer->userWaveSample( - i / static_cast( length() ) ); + m_samples[i] = Oscillator::userWaveSample(sampleBuffer.get(), i / static_cast(length())); } - sampleBuffer->dataUnlock(); } - sharedObject::unref( sampleBuffer ); - emit samplesChanged( 0, length() - 1 ); return fileName; }; diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index 876cb307f..130502856 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -108,10 +108,10 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, { if( sClip->isPlaying() == false && _start >= (sClip->startPosition() + sClip->startTimeOffset()) ) { - auto bufferFramesPerTick = Engine::framesPerTick (sClip->sampleBuffer ()->sampleRate ()); + auto bufferFramesPerTick = Engine::framesPerTick(sClip->sample().sampleRate()); f_cnt_t sampleStart = bufferFramesPerTick * ( _start - sClip->startPosition() - sClip->startTimeOffset() ); f_cnt_t clipFrameLength = bufferFramesPerTick * ( sClip->endPosition() - sClip->startPosition() - sClip->startTimeOffset() ); - f_cnt_t sampleBufferLength = sClip->sampleBuffer()->frames(); + f_cnt_t sampleBufferLength = sClip->sample().sampleSize(); //if the Clip smaller than the sample length we play only until Clip end //else we play the sample to the end but nothing more f_cnt_t samplePlayLength = clipFrameLength > sampleBufferLength ? sampleBufferLength : clipFrameLength; From 4eba656bd3f6df978623d8cfb0d9271ed98f6112 Mon Sep 17 00:00:00 2001 From: Spekular Date: Mon, 25 Dec 2023 17:26:35 +0100 Subject: [PATCH 054/191] New loop marker shortcuts, attempt 2 (#6382) Co-authored-by: Dominic Clark --- data/themes/classic/cursor_select_left.png | Bin 0 -> 206 bytes data/themes/classic/cursor_select_right.png | Bin 0 -> 193 bytes data/themes/classic/style.css | 12 +- data/themes/default/cursor_select_left.png | Bin 0 -> 206 bytes data/themes/default/cursor_select_right.png | Bin 0 -> 193 bytes data/themes/default/style.css | 11 +- include/SetupDialog.h | 3 + include/TimeLineWidget.h | 114 ++++++--- src/gui/editors/SongEditor.cpp | 2 +- src/gui/editors/TimeLineWidget.cpp | 262 +++++++++++++------- src/gui/modals/SetupDialog.cpp | 21 ++ 11 files changed, 298 insertions(+), 127 deletions(-) create mode 100644 data/themes/classic/cursor_select_left.png create mode 100644 data/themes/classic/cursor_select_right.png create mode 100644 data/themes/default/cursor_select_left.png create mode 100644 data/themes/default/cursor_select_right.png diff --git a/data/themes/classic/cursor_select_left.png b/data/themes/classic/cursor_select_left.png new file mode 100644 index 0000000000000000000000000000000000000000..eaa80e0bbe3ac8e5ded5e721e82a9dd1a8b8b338 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V6vx+V@QPi+v$#62NZZ%%>VwMe>H+X%*!@+ z?->UH#}g^Gswz{%;-5%M$1W(mFQ!zLzxsgbxnvt53E}mB<~f+nIeqG*z5@dzivXOs p$5P}y^R$!lvI6;>1s;*b3=DjSL74G){)!Z!V3?!lvI6;>1s;*b3=DjSL74G){)!Z!V6vx+V@QPi+v$#62NZZ%%>VwMe>H+X%*!@+ z?->UH#}g^Gswz{%;-5%M$1W(mFQ!zLzxsgbxnvt53E}mB<~f+nIeqG*z5@dzivXOs p$5P}y^R$!lvI6;>1s;*b3=DjSL74G){)!Z!V3? + +#include +#include #include #include "Song.h" @@ -57,10 +61,15 @@ public: Q_PROPERTY( QColor inactiveLoopColor READ getInactiveLoopColor WRITE setInactiveLoopColor ) Q_PROPERTY( QBrush inactiveLoopBrush READ getInactiveLoopBrush WRITE setInactiveLoopBrush ) Q_PROPERTY( QColor inactiveLoopInnerColor READ getInactiveLoopInnerColor WRITE setInactiveLoopInnerColor ) + Q_PROPERTY(QColor inactiveLoopHandleColor MEMBER m_inactiveLoopHandleColor) Q_PROPERTY( QColor activeLoopColor READ getActiveLoopColor WRITE setActiveLoopColor ) Q_PROPERTY( QBrush activeLoopBrush READ getActiveLoopBrush WRITE setActiveLoopBrush ) Q_PROPERTY( QColor activeLoopInnerColor READ getActiveLoopInnerColor WRITE setActiveLoopInnerColor ) + Q_PROPERTY(QColor activeLoopHandleColor MEMBER m_activeLoopHandleColor) Q_PROPERTY( int loopRectangleVerticalPadding READ getLoopRectangleVerticalPadding WRITE setLoopRectangleVerticalPadding ) + Q_PROPERTY(int loopHandleWidth MEMBER m_loopHandleWidth) + Q_PROPERTY(QSize mouseHotspotSelLeft READ mouseHotspotSelLeft WRITE setMouseHotspotSelLeft) + Q_PROPERTY(QSize mouseHotspotSelRight READ mouseHotspotSelRight WRITE setMouseHotspotSelRight) enum class AutoScrollState { @@ -99,6 +108,28 @@ public: inline int const & getLoopRectangleVerticalPadding() const { return m_loopRectangleVerticalPadding; } inline void setLoopRectangleVerticalPadding(int const & loopRectangleVerticalPadding) { m_loopRectangleVerticalPadding = loopRectangleVerticalPadding; } + auto mouseHotspotSelLeft() const -> QSize + { + const auto point = m_cursorSelectLeft.hotSpot(); + return QSize{point.x(), point.y()}; + } + + void setMouseHotspotSelLeft(const QSize& s) + { + m_cursorSelectLeft = QCursor{m_cursorSelectLeft.pixmap(), s.width(), s.height()}; + } + + auto mouseHotspotSelRight() const -> QSize + { + const auto point = m_cursorSelectRight.hotSpot(); + return QSize{point.x(), point.y()}; + } + + void setMouseHotspotSelRight(const QSize& s) + { + m_cursorSelectRight = QCursor{m_cursorSelectRight.pixmap(), s.width(), s.height()}; + } + inline Song::PlayPos & pos() { return( m_pos ); @@ -143,51 +174,64 @@ protected: void mousePressEvent( QMouseEvent * _me ) override; void mouseMoveEvent( QMouseEvent * _me ) override; void mouseReleaseEvent( QMouseEvent * _me ) override; - + void contextMenuEvent(QContextMenuEvent* event) override; private: - QPixmap m_posMarkerPixmap = embed::getIconPixmap("playpos_marker"); - - QColor m_inactiveLoopColor; - QBrush m_inactiveLoopBrush; - QColor m_inactiveLoopInnerColor; - - QColor m_activeLoopColor; - QBrush m_activeLoopBrush; - QColor m_activeLoopInnerColor; - - int m_loopRectangleVerticalPadding; - - QColor m_barLineColor; - QColor m_barNumberColor; - - AutoScrollState m_autoScroll; - - bool m_changedPosition; - - int m_xOffset; - int m_posMarkerX; - float m_ppb; - float m_snapSize; - Song::PlayPos & m_pos; - Timeline* m_timeline; - const TimePos & m_begin; - const Song::PlayMode m_mode; - - TextFloat * m_hint; - int m_initalXSelect; - - enum class Action { NoAction, MovePositionMarker, MoveLoopBegin, MoveLoopEnd, + MoveLoop, SelectSongClip, - } m_action; + }; - int m_moveXOff; + auto getClickedTime(int xPosition) const -> TimePos; + auto getLoopAction(QMouseEvent* event) const -> Action; + auto actionCursor(Action action) const -> QCursor; + + QPixmap m_posMarkerPixmap = embed::getIconPixmap("playpos_marker"); + + QColor m_inactiveLoopColor = QColor{52, 63, 53, 64}; + QBrush m_inactiveLoopBrush = QColor{255, 255, 255, 32}; + QColor m_inactiveLoopInnerColor = QColor{255, 255, 255, 32}; + QColor m_inactiveLoopHandleColor = QColor{255, 255, 255, 32}; + + QColor m_activeLoopColor = QColor{52, 63, 53, 255}; + QBrush m_activeLoopBrush = QColor{55, 141, 89}; + QColor m_activeLoopInnerColor = QColor{74, 155, 100, 255}; + QColor m_activeLoopHandleColor = QColor{74, 155, 100, 255}; + + int m_loopRectangleVerticalPadding = 1; + int m_loopHandleWidth = 5; + + QColor m_barLineColor = QColor{192, 192, 192}; + QColor m_barNumberColor = m_barLineColor.darker(120); + + QCursor m_cursorSelectLeft = QCursor{embed::getIconPixmap("cursor_select_left"), 0, 16}; + QCursor m_cursorSelectRight = QCursor{embed::getIconPixmap("cursor_select_right"), 32, 16}; + + AutoScrollState m_autoScroll = AutoScrollState::Enabled; + + // Width of the unused region on the widget's left (above track labels or piano) + int m_xOffset; + float m_ppb; + float m_snapSize = 1.f; + Song::PlayPos & m_pos; + Timeline* m_timeline; + // Leftmost position visible in parent editor + const TimePos & m_begin; + const Song::PlayMode m_mode; + // When in MoveLoop mode we need the initial positions. Storing only the latest + // position allows for unquantized drag but fails when toggling quantization. + std::array m_oldLoopPos; + TimePos m_dragStartPos; + + TextFloat* m_hint = nullptr; + int m_initalXSelect; + + Action m_action = Action::NoAction; }; } // namespace lmms::gui diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 69a41a764..400c03aa9 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -806,7 +806,7 @@ void SongEditor::updatePosition( const TimePos & t ) m_scrollBack = false; } - const int x = m_timeLine->markerX(t) + 8; + const int x = m_timeLine->markerX(t); if( x >= trackOpWidth + widgetWidth -1 ) { m_positionLine->show(); diff --git a/src/gui/editors/TimeLineWidget.cpp b/src/gui/editors/TimeLineWidget.cpp index f77361a91..7657e2916 100644 --- a/src/gui/editors/TimeLineWidget.cpp +++ b/src/gui/editors/TimeLineWidget.cpp @@ -27,11 +27,14 @@ #include #include +#include +#include #include #include #include #include +#include "ConfigManager.h" #include "embed.h" #include "GuiApplication.h" #include "NStateButton.h" @@ -47,35 +50,17 @@ namespace TimeLineWidget::TimeLineWidget(const int xoff, const int yoff, const float ppb, Song::PlayPos& pos, Timeline& timeline, const TimePos& begin, Song::PlayMode mode, QWidget* parent) : - QWidget( parent ), - m_inactiveLoopColor( 52, 63, 53, 64 ), - m_inactiveLoopBrush( QColor( 255, 255, 255, 32 ) ), - m_inactiveLoopInnerColor( 255, 255, 255, 32 ), - m_activeLoopColor( 52, 63, 53, 255 ), - m_activeLoopBrush( QColor( 55, 141, 89 ) ), - m_activeLoopInnerColor( 74, 155, 100, 255 ), - m_loopRectangleVerticalPadding( 1 ), - m_barLineColor( 192, 192, 192 ), - m_barNumberColor( m_barLineColor.darker( 120 ) ), - m_autoScroll( AutoScrollState::Enabled ), - m_changedPosition( true ), - m_xOffset( xoff ), - m_posMarkerX( 0 ), - m_ppb( ppb ), - m_snapSize( 1.0 ), - m_pos( pos ), + QWidget{parent}, + m_xOffset{xoff}, + m_ppb{ppb}, + m_pos{pos}, m_timeline{&timeline}, - m_begin( begin ), - m_mode( mode ), - m_hint( nullptr ), - m_action( Action::NoAction ), - m_moveXOff( 0 ) + m_begin{begin}, + m_mode{mode} { setAttribute( Qt::WA_OpaquePaintEvent, true ); move( 0, yoff ); - m_xOffset -= m_posMarkerPixmap.width() / 2; - setMouseTracking(true); auto updateTimer = new QTimer(this); @@ -98,12 +83,8 @@ TimeLineWidget::~TimeLineWidget() void TimeLineWidget::setXOffset(const int x) { m_xOffset = x; - m_xOffset -= m_posMarkerPixmap.width() / 2; } - - - void TimeLineWidget::addToolButtons( QToolBar * _tool_bar ) { auto autoScroll = new NStateButton(_tool_bar); @@ -151,15 +132,8 @@ void TimeLineWidget::addToolButtons( QToolBar * _tool_bar ) void TimeLineWidget::updatePosition() { - const int new_x = markerX( m_pos ); - - if( new_x != m_posMarkerX ) - { - m_posMarkerX = new_x; - m_changedPosition = true; - emit positionChanged( m_pos ); - update(); - } + emit positionChanged(m_pos); + update(); } void TimeLineWidget::toggleAutoScroll( int _n ) @@ -175,19 +149,18 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) p.fillRect( 0, 0, width(), height(), p.background() ); // Clip so that we only draw everything starting from the offset - const int leftMargin = m_xOffset + m_posMarkerPixmap.width() / 2; - p.setClipRect(leftMargin, 0, width() - leftMargin, height() ); + p.setClipRect(m_xOffset, 0, width() - m_xOffset, height()); - // Draw the loop rectangle - int const & loopRectMargin = getLoopRectangleVerticalPadding(); + // Variables for the loop rectangle + int const loopRectMargin = getLoopRectangleVerticalPadding(); int const loopRectHeight = this->height() - 2 * loopRectMargin; - int const loopStart = markerX(m_timeline->loopBegin()) + 8; - int const loopEndR = markerX(m_timeline->loopEnd()) + 9; + int const loopStart = markerX(m_timeline->loopBegin()); + int const loopEndR = markerX(m_timeline->loopEnd()); int const loopRectWidth = loopEndR - loopStart; bool const loopPointsActive = m_timeline->loopEnabled(); - // Draw the main rectangle (inner fill only) + // Draw the main loop rectangle (inner fill only) QRect outerRectangle( loopStart, loopRectMargin, loopRectWidth - 1, loopRectHeight - 1 ); p.fillRect( outerRectangle, loopPointsActive ? getActiveLoopBrush() : getInactiveLoopBrush()); @@ -203,8 +176,7 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) QColor const & barNumberColor = getBarNumberColor(); bar_t barNumber = m_begin.getBar(); - int const x = m_xOffset + m_posMarkerPixmap.width() / 2 - - ((static_cast(m_begin * m_ppb) / TimePos::ticksPerBar()) % static_cast(m_ppb)); + int const x = m_xOffset - ((static_cast(m_begin * m_ppb) / TimePos::ticksPerBar()) % static_cast(m_ppb)); // Double the interval between bar numbers until they are far enough appart int barLabelInterval = 1; @@ -225,83 +197,139 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) } } - // Draw the main rectangle (outer border) + // Draw the loop rectangle's outer outline p.setPen( loopPointsActive ? getActiveLoopColor() : getInactiveLoopColor() ); p.setBrush( Qt::NoBrush ); p.drawRect( outerRectangle ); - // Draw the inner border outline (no fill) + // Draw the loop rectangle's inner outline QRect innerRectangle = outerRectangle.adjusted( 1, 1, -1, -1 ); p.setPen( loopPointsActive ? getActiveLoopInnerColor() : getInactiveLoopInnerColor() ); p.setBrush( Qt::NoBrush ); p.drawRect( innerRectangle ); + + // Draw loop handles if necessary + const auto handleMode = ConfigManager::inst()->value("app", "loopmarkermode") == "handles"; + if (handleMode && underMouse() && QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) + { + const auto handleWidth = std::min(m_loopHandleWidth, loopRectWidth / 2 - 1); + const auto leftHandle = QRectF(loopStart - .5, loopRectMargin - .5, handleWidth, loopRectHeight); + const auto rightHandle = QRectF(loopEndR - handleWidth - .5, loopRectMargin - .5, handleWidth, loopRectHeight); + const auto color = loopPointsActive ? m_activeLoopHandleColor : m_inactiveLoopHandleColor; + + p.fillRect(leftHandle, color); + p.fillRect(rightHandle, color); + } // Only draw the position marker if the position line is in view - if (m_posMarkerX >= m_xOffset && m_posMarkerX < width() - m_posMarkerPixmap.width() / 2) + if (markerX(m_pos) >= m_xOffset && markerX(m_pos) < width() - m_posMarkerPixmap.width() / 2) { // Let the position marker extrude to the left p.setClipping(false); p.setOpacity(0.6); - p.drawPixmap(m_posMarkerX, height() - m_posMarkerPixmap.height(), m_posMarkerPixmap); + p.drawPixmap(markerX(m_pos) - (m_posMarkerPixmap.width() / 2), + height() - m_posMarkerPixmap.height(), m_posMarkerPixmap); } } - - - -void TimeLineWidget::mousePressEvent( QMouseEvent* event ) +auto TimeLineWidget::getClickedTime(const int xPosition) const -> TimePos { - if( event->x() < m_xOffset ) + // How far into the timeline we clicked, measuring pixels from the leftmost part of the editor + const auto pixelDelta = std::max(xPosition - m_xOffset, 0); + return m_begin + static_cast(pixelDelta * TimePos::ticksPerBar() / m_ppb); +} + +auto TimeLineWidget::getLoopAction(QMouseEvent* event) const -> TimeLineWidget::Action +{ + const auto mode = ConfigManager::inst()->value("app", "loopmarkermode"); + const auto xPos = event->x(); + const auto button = event->button(); + + if (mode == "handles") { - return; + // Loop start and end pos, or closest edge of screen if loop extends off it + const auto leftMost = std::max(markerX(m_timeline->loopBegin()), m_xOffset); + const auto rightMost = std::min(markerX(m_timeline->loopEnd()), width()); + // Distance from click to handle, positive aimed towards center of loop + const auto deltaLeft = xPos - leftMost; + const auto deltaRight = rightMost - xPos; + + if (deltaLeft < 0 || deltaRight < 0) { return Action::NoAction; } // Clicked outside loop + else if (deltaLeft <= m_loopHandleWidth && deltaLeft < deltaRight) { return Action::MoveLoopBegin; } + else if (deltaRight <= m_loopHandleWidth) { return Action::MoveLoopEnd; } + else { return Action::MoveLoop; } } - if( event->button() == Qt::LeftButton && !(event->modifiers() & Qt::ShiftModifier) ) + else if (mode == "closest") { - m_action = Action::MovePositionMarker; - if (event->x() - m_xOffset < m_posMarkerPixmap.width()) + const TimePos loopMid = (m_timeline->loopBegin() + m_timeline->loopEnd()) / 2; + return getClickedTime(xPos) < loopMid ? Action::MoveLoopBegin : Action::MoveLoopEnd; + } + else // Default to dual-button mode + { + if (button == Qt::LeftButton) { return Action::MoveLoopBegin; } + else if (button == Qt::RightButton) { return Action::MoveLoopEnd; } + return Action::NoAction; + } +} + +auto TimeLineWidget::actionCursor(Action action) const -> QCursor +{ + switch (action) { + case Action::MoveLoop: return Qt::SizeHorCursor; + case Action::MoveLoopBegin: return m_cursorSelectLeft; + case Action::MoveLoopEnd: return m_cursorSelectRight; + // Fall back to normal cursor if no action or action cursor not specified + default: return Qt::ArrowCursor; + } +} + +void TimeLineWidget::mousePressEvent(QMouseEvent* event) +{ + if (event->x() < m_xOffset) { return; } + + const auto shift = event->modifiers() & Qt::ShiftModifier; + const auto ctrl = event->modifiers() & Qt::ControlModifier; + + if (shift) // loop marker manipulation + { + m_action = getLoopAction(event); + setCursor(actionCursor(m_action)); + + if (m_action == Action::MoveLoop) { - m_moveXOff = event->x() - m_xOffset; - } - else - { - m_moveXOff = m_posMarkerPixmap.width() / 2; + m_dragStartPos = getClickedTime(event->x()); + m_oldLoopPos = {m_timeline->loopBegin(), m_timeline->loopEnd()}; } } - else if( event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier) ) + else if (event->button() == Qt::LeftButton && ctrl) // selection { m_action = Action::SelectSongClip; m_initalXSelect = event->x(); } - else if( event->button() == Qt::RightButton ) + else if (event->button() == Qt::LeftButton && !ctrl) // move playhead { - m_moveXOff = m_posMarkerPixmap.width() / 2; - - const auto cursorXOffset = std::max(event->x() - m_xOffset - m_moveXOff, 0); - const TimePos timeAtCursor = m_begin + static_cast(cursorXOffset * TimePos::ticksPerBar() / m_ppb); - const TimePos loopMid = (m_timeline->loopBegin() + m_timeline->loopEnd()) / 2; - - m_action = timeAtCursor < loopMid ? Action::MoveLoopBegin : Action::MoveLoopEnd; + m_action = Action::MovePositionMarker; } - if( m_action == Action::MoveLoopBegin || m_action == Action::MoveLoopEnd ) + if (m_action == Action::MoveLoopBegin || m_action == Action::MoveLoopEnd) { delete m_hint; - m_hint = TextFloat::displayMessage( tr( "Hint" ), - tr( "Press <%1> to disable magnetic loop points." ).arg(UI_CTRL_KEY), - embed::getIconPixmap( "hint" ), 0 ); + m_hint = TextFloat::displayMessage(tr("Hint"), + tr("Press <%1> to disable magnetic loop points.").arg(UI_CTRL_KEY), + embed::getIconPixmap("hint"), 0); } - mouseMoveEvent( event ); + + setContextMenuPolicy(m_action == Action::NoAction ? Qt::DefaultContextMenu : Qt::PreventContextMenu); + + mouseMoveEvent(event); } - - - void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) { parentWidget()->update(); // essential for widgets that this timeline had taken their mouse move event from. - const auto cursorXOffset = std::max(event->x() - m_xOffset - m_moveXOff, 0); - TimePos timeAtCursor = m_begin + static_cast(cursorXOffset * TimePos::ticksPerBar() / m_ppb); + auto timeAtCursor = getClickedTime(event->x()); + const auto control = event->modifiers() & Qt::ControlModifier; switch( m_action ) { @@ -324,7 +352,6 @@ void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) const auto otherPoint = m_action == Action::MoveLoopBegin ? m_timeline->loopEnd() : m_timeline->loopBegin(); - const bool control = event->modifiers() & Qt::ControlModifier; if (control) { // no ctrl-press-hint when having ctrl pressed @@ -349,18 +376,35 @@ void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) update(); break; } - case Action::SelectSongClip: + case Action::MoveLoop: + { + const TimePos dragDelta = timeAtCursor - m_dragStartPos; + auto loopPos = m_oldLoopPos; + for (auto& point : loopPos) + { + point += dragDelta; + if (!control) { point = point.quantize(m_snapSize); } + } + m_timeline->setLoopPoints(loopPos[0], loopPos[1]); + break; + } + case Action::SelectSongClip: emit regionSelectedFromPixels( m_initalXSelect , event->x() ); - break; + break; default: break; } + + if (event->buttons() == Qt::NoButton) + { + setCursor(QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier) + ? actionCursor(getLoopAction(event)) + : Qt::ArrowCursor + ); + } } - - - void TimeLineWidget::mouseReleaseEvent( QMouseEvent* event ) { delete m_hint; @@ -369,5 +413,45 @@ void TimeLineWidget::mouseReleaseEvent( QMouseEvent* event ) m_action = Action::NoAction; } +void TimeLineWidget::contextMenuEvent(QContextMenuEvent* event) +{ + if (event->x() < m_xOffset) { return; } + + auto menu = QMenu{}; + + menu.addAction(tr("Set loop begin here"), [this, event] { + auto begin = getClickedTime(event->x()); + const auto end = m_timeline->loopEnd(); + if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { begin = begin.quantize(m_snapSize); } + if (begin == end) { m_timeline->setLoopEnd(end + m_snapSize * TimePos::ticksPerBar()); } + m_timeline->setLoopBegin(begin); + update(); + }); + menu.addAction(tr("Set loop end here"), [this, event] { + const auto begin = m_timeline->loopBegin(); + auto end = getClickedTime(event->x()); + if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { end = end.quantize(m_snapSize); } + if (begin == end) { m_timeline->setLoopBegin(begin - m_snapSize * TimePos::ticksPerBar()); } + m_timeline->setLoopEnd(end); + update(); + }); + + menu.addSeparator(); + + const auto loopMenu = menu.addMenu(tr("Loop edit mode (hold shift)")); + const auto loopMode = ConfigManager::inst()->value("app", "loopmarkermode", "dual"); + const auto addLoopModeAction = [loopMenu, &loopMode](QString text, QString mode) { + const auto action = loopMenu->addAction(text, [mode] { + ConfigManager::inst()->setValue("app", "loopmarkermode", mode); + }); + action->setCheckable(true); + if (loopMode == mode) { action->setChecked(true); } + }; + addLoopModeAction(tr("Dual-button"), "dual"); + addLoopModeAction(tr("Grab closest"), "closest"); + addLoopModeAction(tr("Handles"), "handles"); + + menu.exec(event->globalPos()); +} } // namespace lmms::gui diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 209422563..fffa94c82 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -120,6 +120,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : "app", "disablebackup").toInt()), m_openLastProject(ConfigManager::inst()->value( "app", "openlastproject").toInt()), + m_loopMarkerMode{ConfigManager::inst()->value("app", "loopmarkermode", "dual")}, m_lang(ConfigManager::inst()->value( "app", "language")), m_saveInterval( ConfigManager::inst()->value( @@ -255,6 +256,19 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : addCheckBox(tr("Show warning when deleting a mixer channel that is in use"), guiGroupBox, guiGroupLayout, m_mixerChannelDeletionWarning, SLOT(toggleMixerChannelDeletionWarning(bool)), false); + m_loopMarkerComboBox = new QComboBox{guiGroupBox}; + + m_loopMarkerComboBox->addItem(tr("Dual-button"), "dual"); + m_loopMarkerComboBox->addItem(tr("Grab closest"), "closest"); + m_loopMarkerComboBox->addItem(tr("Handles"), "handles"); + + m_loopMarkerComboBox->setCurrentIndex(m_loopMarkerComboBox->findData(m_loopMarkerMode)); + connect(m_loopMarkerComboBox, qOverload(&QComboBox::currentIndexChanged), + this, &SetupDialog::loopMarkerModeChanged); + + guiGroupLayout->addWidget(new QLabel{tr("Loop edit mode"), guiGroupBox}); + guiGroupLayout->addWidget(m_loopMarkerComboBox); + generalControlsLayout->addWidget(guiGroupBox); generalControlsLayout->addSpacing(10); @@ -922,6 +936,7 @@ void SetupDialog::accept() QString::number(!m_disableBackup)); ConfigManager::inst()->setValue("app", "openlastproject", QString::number(m_openLastProject)); + ConfigManager::inst()->setValue("app", "loopmarkermode", m_loopMarkerMode); ConfigManager::inst()->setValue("app", "language", m_lang); ConfigManager::inst()->setValue("ui", "saveinterval", QString::number(m_saveInterval)); @@ -1061,6 +1076,12 @@ void SetupDialog::toggleOpenLastProject(bool enabled) } +void SetupDialog::loopMarkerModeChanged() +{ + m_loopMarkerMode = m_loopMarkerComboBox->currentData().toString(); +} + + void SetupDialog::setLanguage(int lang) { m_lang = m_languages[lang]; From 4e63f60652337faef678b10001f5c7742713b43b Mon Sep 17 00:00:00 2001 From: saker Date: Mon, 25 Dec 2023 13:14:19 -0500 Subject: [PATCH 055/191] Add missing copyright in SampleDecoder.cpp (#7030) --- src/core/SampleDecoder.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/core/SampleDecoder.cpp b/src/core/SampleDecoder.cpp index 4db0d438a..853543d85 100644 --- a/src/core/SampleDecoder.cpp +++ b/src/core/SampleDecoder.cpp @@ -1,3 +1,27 @@ +/* + * SampleDecoder.cpp - Decodes audio files in various formats + * + * Copyright (c) 2023 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + #include "SampleDecoder.h" #include From 6b21dc7896eca36f647fc802b07e7c976e5ac24c Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 30 Dec 2023 11:55:28 -0500 Subject: [PATCH 056/191] Use Qt layouts for mixer channels (#6591) Use Qt layouts for the mixer channels. These changes will enable several other improvements, like for example making the mixer and faders resizable, adding peak indicators, etc. This is a squash commit which consists of the following individual commits: * Remove extra transparency in send/receive arrows The extra transparency was conflicting with the positioning of the arrows in the layout * Begin reimplementing MixerChannelView MixerChannelView is now a combination of the MixerLine with the previous MixerChannelView * Adjust SendButtonIndicator to use MixerChannelView * Remove MixerLine - Move MixerChannelView into src/gui * Remove MixerView::MixerChannelView * Remove header of MixerLine * Change MixerView.h to use MixerChannelView Change MixerView.h to use MixerChannelView rather than MixerLine Also do some cleanup, such as removing an unused forward declaration of QButtonGroup * Create EffectRackView + Set height of sizeHint() using MIXER_CHANNEL_HEIGHT (287) * Remove include of MixerLine - Include MixerChannelView * Phase 1: Adjust MixerView to use new MixerChannelView * Move children wigets into header file * Phase 2: Adjust MixerView to use new MixerChannelView * Phase 3: Adjust MixerView to use new MixerChannelView * Phase 4: Adjust MixerView to use new MixerChannelView * Phase 5: Adjust MixerView to use new MixerChannelView * Phase 5: Adjust MixerView to use new MixerChannelView * Remove places where MixerChannelView is being deleted Before, MixerChannelView was not inherited by QWidget, meaning it could not have a parent and had to be deleted when necessary. Since the MixerView owns the new MixerChannelView, this is no longer necessary. * Replace MixerLine with MixerChannelView - Include MixerChannelView in MixerView * Replace setCurrentMixerLine calls with setCurrentMixerChannel around codebase * Add event handlers in MixerChannelView * Implement MixerChannelView::eventFilter * Update theme styles to use MixerChannelView * Add QColor properties from style - Set the Qt::WA_StyledBackground attribute on * Add effect rack to rack layout when adding channel * Set size for MixerChannelView - Change nullptr to this for certain widgets - Some custom widgets may expect there to be a parent - Add spacing in channel layout - Increase size of mixer channel * Retain size when widgets are hidden * Implement paintEvent - Rename states in SendReceiveState * Implement send/receive arrow toggling - Make maxTextHeight constexpr in elideName - Remove background changing on mouse press (is now handled in paintEvent) * Implement renaming mixer channels * Implement color functions * Implement channel moving/removing functions * Do some cleanup Not sure if that connection with the mute model was needed, but removing it did not seem to introduce any issues. * Include cassert * Replace references to MixerLine with MixerChannelView * Reduce height + Make m_renameLineEdit transparent + Retain size when LCD is hidden + Remove stretch after renameLineEdit in layout * Remove trailing whitespace * Make m_renameLineEdit read only + Transpose m_renameLineEditView rectangle (with 5px offset) * Set spacing in channel layout back to 0 * Remove sizeHint override and constant size * Use sizeHint for mixerChannelSize + Leave auto fill background to false in MixerChannelView + Only set width for EffectRackView * Set margins to 4 on all sides in MixerChannelView * Move solo and mute closer to each other Move the solo and mute buttons closer to each other in the mixer channels. Technically this is accomplished by putting them into their own layout with minimal margins and spacing. * Fixes for CodeFactor * Code review changes Mostly whitespace and formatting changes: remove tabs, remove spaces in parameter lists, remove underscores from parameter names. Some lines have been shortened by introducing intermediate variables, e.g. in `MixerChannelView`. `MixerView` has many changes but only related to whitespace. Spaces have been introduced for if and for statements. Whitespace at round braces has been removed everywhere in the implementation file even if a line was not touched by the intial changes. Remove duplicate forward declaration of `MixerChannelView`. * Adjust parameter order in MixerChannelView's constructor Make the parent `QWidget` the first parameter as it is a Qt convention. The default parameter had to be removed due to this. * Move styling of rename line edit into style sheets Move the style of the `QGraphicsView` for the rename line edit from the code into the style sheets of the default and classic theme. * More code review changes Fix spaces between types and references/pointers, e.g. use `const QBrush& c` instead of `const QBrush & c`. Remove underscores from parameter names. Remove spaces near parentheses. Replace tabs with spaces. Introduce intermediate variable to resolve "hanging" + operator. Replace the connection for the periodic fader updates with one that uses function pointers instead of `SIGNAL` and `SLOT`. --------- Co-authored-by: Michael Gregorius --- data/locale/ar.ts | 26 +- data/locale/bs.ts | 22 +- data/locale/ca.ts | 26 +- data/locale/cs.ts | 26 +- data/locale/de.ts | 26 +- data/locale/el.ts | 26 +- data/locale/en.ts | 26 +- data/locale/eo.ts | 26 +- data/locale/es.ts | 26 +- data/locale/eu.ts | 26 +- data/locale/fa.ts | 26 +- data/locale/fr.ts | 26 +- data/locale/gl.ts | 26 +- data/locale/he.ts | 26 +- data/locale/hi_IN.ts | 26 +- data/locale/hu_HU.ts | 26 +- data/locale/id.ts | 26 +- data/locale/it.ts | 26 +- data/locale/ja.ts | 26 +- data/locale/ka.ts | 26 +- data/locale/ko.ts | 26 +- data/locale/ms_MY.ts | 26 +- data/locale/nb.ts | 26 +- data/locale/nl.ts | 26 +- data/locale/oc.ts | 26 +- data/locale/pl.ts | 26 +- data/locale/pt.ts | 26 +- data/locale/ro.ts | 26 +- data/locale/ru.ts | 26 +- data/locale/sl.ts | 26 +- data/locale/sr.ts | 4 +- data/locale/sv.ts | 26 +- data/locale/tr.ts | 26 +- data/locale/uk.ts | 26 +- data/locale/zh_CN.ts | 26 +- data/locale/zh_TW.ts | 26 +- data/themes/classic/style.css | 7 +- data/themes/default/receive_bg_arrow.png | Bin 277 -> 4824 bytes data/themes/default/send_bg_arrow.png | Bin 279 -> 4810 bytes data/themes/default/style.css | 7 +- include/InstrumentTrackView.h | 4 +- include/InstrumentTrackWindow.h | 4 +- ...eLcdSpinBox.h => MixerChannelLcdSpinBox.h} | 14 +- include/MixerChannelView.h | 131 +++++ include/MixerLine.h | 118 ----- include/MixerView.h | 62 +-- include/SampleTrackView.h | 4 +- include/SampleTrackWindow.h | 4 +- include/SendButtonIndicator.h | 16 +- src/gui/CMakeLists.txt | 4 +- src/gui/MixerChannelView.cpp | 447 +++++++++++++++++ src/gui/MixerLine.cpp | 448 ------------------ src/gui/MixerView.cpp | 325 +++++-------- src/gui/SampleTrackWindow.cpp | 6 +- src/gui/SendButtonIndicator.cpp | 37 +- src/gui/instrument/InstrumentTrackWindow.cpp | 4 +- src/gui/tracks/InstrumentTrackView.cpp | 4 +- src/gui/tracks/SampleTrackView.cpp | 6 +- ...SpinBox.cpp => MixerChannelLcdSpinBox.cpp} | 12 +- 59 files changed, 1249 insertions(+), 1325 deletions(-) rename include/{MixerLineLcdSpinBox.h => MixerChannelLcdSpinBox.h} (74%) create mode 100644 include/MixerChannelView.h delete mode 100644 include/MixerLine.h create mode 100644 src/gui/MixerChannelView.cpp delete mode 100644 src/gui/MixerLine.cpp rename src/gui/widgets/{MixerLineLcdSpinBox.cpp => MixerChannelLcdSpinBox.cpp} (82%) diff --git a/data/locale/ar.ts b/data/locale/ar.ts index 0d44c22bf..4aca06e51 100644 --- a/data/locale/ar.ts +++ b/data/locale/ar.ts @@ -5334,62 +5334,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New Mixer Channel diff --git a/data/locale/bs.ts b/data/locale/bs.ts index 7abf0baf1..8050af8c2 100644 --- a/data/locale/bs.ts +++ b/data/locale/bs.ts @@ -2698,14 +2698,14 @@ Please make sure you have write-permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + The Mixer channel receives input from one or more instrument tracks. It in turn can be routed to multiple other mixer channels. LMMS automatically takes care of preventing infinite loops for you and doesn't allow making a connection that would result in an infinite loop. @@ -2716,27 +2716,27 @@ You can remove and move mixer channels in the context menu, which is accessed by - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels @@ -2789,12 +2789,12 @@ You can remove and move mixer channels in the context menu, which is accessed by - + Rename mixer channel - + Enter the new name for this mixer channel @@ -9752,7 +9752,7 @@ Please make sure you have read-permission to the file and the directory containi - MixerLineLcdSpinBox + MixerChannelLcdSpinBox Assign to: diff --git a/data/locale/ca.ts b/data/locale/ca.ts index 0e27c39db..2b06d0754 100644 --- a/data/locale/ca.ts +++ b/data/locale/ca.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/cs.ts b/data/locale/cs.ts index 022f55459..16982e4ae 100644 --- a/data/locale/cs.ts +++ b/data/locale/cs.ts @@ -5334,62 +5334,62 @@ Ověřte si prosím, zda máte povolen zápis do souboru a do složky, ve které - MixerLine + MixerChannelView - + Channel send amount Množství odeslaného kanálu - + Move &left PÅ™esunout do&leva - + Move &right PÅ™esun dop&rava - + Rename &channel PÅ™ejmenovat &kanál - + R&emove channel PÅ™&esunout kanál - + Remove &unused channels Odstranit nepo&užívané kanály - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: PÅ™iÅ™adit k: - + New mixer Channel Nový efektový kanál diff --git a/data/locale/de.ts b/data/locale/de.ts index 7817857fd..d5d0625c2 100644 --- a/data/locale/de.ts +++ b/data/locale/de.ts @@ -5334,62 +5334,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount Kanal Sendemenge - + Move &left Nach &links verschieben - + Move &right Nach &rechts verschieben - + Rename &channel &Kanal umbenennen - + R&emove channel Kanal &Entfernen - + Remove &unused channels Entferne &unbenutzte Kanäle - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Weise hinzu: - + New mixer Channel Neuer FX-Kanal diff --git a/data/locale/el.ts b/data/locale/el.ts index 07e61778f..130d5c63e 100644 --- a/data/locale/el.ts +++ b/data/locale/el.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/en.ts b/data/locale/en.ts index 15c3ab1f0..246311921 100644 --- a/data/locale/en.ts +++ b/data/locale/en.ts @@ -5335,62 +5335,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/eo.ts b/data/locale/eo.ts index 0dd9c405f..88e92cda8 100644 --- a/data/locale/eo.ts +++ b/data/locale/eo.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/es.ts b/data/locale/es.ts index 3953ddc11..26c743784 100644 --- a/data/locale/es.ts +++ b/data/locale/es.ts @@ -5334,62 +5334,62 @@ Asegúrate de tener permisos de escritura tanto del archivo como del directorio - MixerLine + MixerChannelView - + Channel send amount Cantidad de envío del canal - + Move &left Mover a la Izquierda (&L) - + Move &right Mover a la Derecha (&R) - + Rename &channel Renombrar &Canal - + R&emove channel Borrar canal (&E) - + Remove &unused channels Quitar los canales que no esten en &uso - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Asignar a: - + New mixer Channel Nuevo Canal FX diff --git a/data/locale/eu.ts b/data/locale/eu.ts index fe6495c0a..7e815a261 100644 --- a/data/locale/eu.ts +++ b/data/locale/eu.ts @@ -5614,62 +5614,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/fa.ts b/data/locale/fa.ts index b376a8424..b167716fd 100644 --- a/data/locale/fa.ts +++ b/data/locale/fa.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/fr.ts b/data/locale/fr.ts index 4862f4263..0386fb4c6 100644 --- a/data/locale/fr.ts +++ b/data/locale/fr.ts @@ -5618,62 +5618,62 @@ Veuillez vous assurez que vous avez les droits d'écriture sur le fichier e - MixerLine + MixerChannelView - + Channel send amount Quantité de signal envoyé du canal - + Move &left Déplacer à &gauche - + Move &right Déplacer à &droite - + Rename &channel &Renommer le canal - + R&emove channel &Supprimer le canal - + Remove &unused channels Supprimer les canaux &inutilisés - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Assigner à : - + New mixer Channel Nouveau canal d'effet diff --git a/data/locale/gl.ts b/data/locale/gl.ts index a1a9e6bf1..01cd54322 100644 --- a/data/locale/gl.ts +++ b/data/locale/gl.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/he.ts b/data/locale/he.ts index fef0caa91..2699b7e1a 100644 --- a/data/locale/he.ts +++ b/data/locale/he.ts @@ -5334,62 +5334,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/hi_IN.ts b/data/locale/hi_IN.ts index 82cf364e3..5b2eeebf5 100644 --- a/data/locale/hi_IN.ts +++ b/data/locale/hi_IN.ts @@ -5335,62 +5335,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/hu_HU.ts b/data/locale/hu_HU.ts index a0f1e4d45..88ef6a431 100644 --- a/data/locale/hu_HU.ts +++ b/data/locale/hu_HU.ts @@ -5339,62 +5339,62 @@ EllenÅ‘rizd, hogy rendelkezel-e a szükséges engedélyekkel és próbáld újra - MixerLine + MixerChannelView - + Channel send amount - + Move &left Mozgatás &balra - + Move &right Mozgatás &jobbra - + Rename &channel Csatorna át&nevezése - + R&emove channel Csatorna &eltávolítása - + Remove &unused channels &Nem használt csatornák eltávolítása - + Set channel color Szín módosítása - + Remove channel color Szín eltávolítása - + Pick random channel color Véletlenszerű szín - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Hozzárendelés: - + New mixer Channel Új csatorna diff --git a/data/locale/id.ts b/data/locale/id.ts index c504740e9..4adeb9b22 100644 --- a/data/locale/id.ts +++ b/data/locale/id.ts @@ -5335,62 +5335,62 @@ Pastikan Anda memiliki izin menulis ke file dan direktori yang berisi berkas ter - MixerLine + MixerChannelView - + Channel send amount Jumlah kirim saluran - + Move &left Pindah ke &kiri - + Move &right Pindah ke &kanan - + Rename &channel Ganti nama &saluran - + R&emove channel H&apus saluran - + Remove &unused channels Hapus &saluran yang tak terpakai - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel Saluran FX Baru diff --git a/data/locale/it.ts b/data/locale/it.ts index d5a68e6e7..104a3bdbc 100644 --- a/data/locale/it.ts +++ b/data/locale/it.ts @@ -5339,62 +5339,62 @@ Si prega di controllare i permessi di scrittura sul file e la cartella che lo co - MixerLine + MixerChannelView - + Channel send amount Quantità di segnale inviata dal canale - + Move &left Sposta a &sinistra - + Move &right Sposta a $destra - + Rename &channel Rinomina &canale - + R&emove channel R&imuovi canale - + Remove &unused channels Rimuovi canali in&utilizzati - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Assegna a: - + New mixer Channel Nuovo canale FX diff --git a/data/locale/ja.ts b/data/locale/ja.ts index 14b38c698..84c5c8a6a 100644 --- a/data/locale/ja.ts +++ b/data/locale/ja.ts @@ -5335,62 +5335,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left 一ã¤å·¦ã¸ (&l) - + Move &right 一ã¤å³ã¸ (&r) - + Rename &channel ãƒãƒ£ãƒ³ãƒãƒ«åを変更 (&c) - + R&emove channel ãƒãƒ£ãƒ³ãƒãƒ«ã‚’削除 (&e) - + Remove &unused channels 使用ã—ã¦ã„ãªã„ãƒãƒ£ãƒ³ãƒãƒ«ã‚’削除 (&u) - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/ka.ts b/data/locale/ka.ts index 51eededf2..bd7890457 100644 --- a/data/locale/ka.ts +++ b/data/locale/ka.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/ko.ts b/data/locale/ko.ts index 43b99e7f4..036c73231 100644 --- a/data/locale/ko.ts +++ b/data/locale/ko.ts @@ -5337,62 +5337,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left 왼쪽으로 ì´ë™(&L) - + Move &right 오른쪽으로 ì´ë™(&R) - + Rename &channel ì±„ë„ ì´ë¦„ 바꾸기(&C) - + R&emove channel ì±„ë„ ì œê±°(&R) - + Remove &unused channels 사용하지 않는 ì±„ë„ ì œê±°(&U) - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: ì±„ë„ í• ë‹¹: - + New mixer Channel 새 FX ì±„ë„ diff --git a/data/locale/ms_MY.ts b/data/locale/ms_MY.ts index ff3478421..dc3561ea2 100644 --- a/data/locale/ms_MY.ts +++ b/data/locale/ms_MY.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/nb.ts b/data/locale/nb.ts index 659344d64..adfc6d856 100644 --- a/data/locale/nb.ts +++ b/data/locale/nb.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/nl.ts b/data/locale/nl.ts index 7ff3e8735..8d1304135 100644 --- a/data/locale/nl.ts +++ b/data/locale/nl.ts @@ -5335,62 +5335,62 @@ Zorg ervoor dat u schrijfbevoegdheid heeft voor het bestand en voor de map die h - MixerLine + MixerChannelView - + Channel send amount Hoeveelheid kanaal-send - + Move &left &Links verplaatsen - + Move &right &Rechts verplaatsen - + Rename &channel &Kanaal hernoemen - + R&emove channel Kanaal v&erwijderen - + Remove &unused channels Ongebr&uikte kanalen verwijderen - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Toewijzen aan: - + New mixer Channel Nieuw FX-kanaal diff --git a/data/locale/oc.ts b/data/locale/oc.ts index 045eaf3ad..be804cfca 100644 --- a/data/locale/oc.ts +++ b/data/locale/oc.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/pl.ts b/data/locale/pl.ts index ff36a8dac..3a12f0815 100644 --- a/data/locale/pl.ts +++ b/data/locale/pl.ts @@ -5619,62 +5619,62 @@ Upewnij siÄ™, że masz uprawnienia do zapisu do pliku i katalogu zawierajÄ…cego - MixerLine + MixerChannelView - + Channel send amount Ilość wysyÅ‚ania kanaÅ‚u - + Move &left PrzesuÅ„ w &lewo - + Move &right PrzesuÅ„ w p&rawo - + Rename &channel ZmieÅ„ nazwÄ™ &kanaÅ‚u - + R&emove channel UsuÅ„ k&anaÅ‚ - + Remove &unused channels &UsuÅ„ nieużywane kanaÅ‚y - + Set channel color Ustaw kolor kanaÅ‚u - + Remove channel color UsuÅ„ kolor kanaÅ‚u - + Pick random channel color Ustaw losowy kolor kanaÅ‚u - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Przypisz do: - + New mixer Channel Nowy kanaÅ‚ efektów diff --git a/data/locale/pt.ts b/data/locale/pt.ts index f8cfe7618..d88d0fa24 100644 --- a/data/locale/pt.ts +++ b/data/locale/pt.ts @@ -5336,62 +5336,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount Quantidade de envio de canal - + Move &left - + Move &right - + Rename &channel Renomear canal - + R&emove channel Remover canal - + Remove &unused channels Remover canais não utilizados - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Atribuir a: - + New mixer Channel Novo Canal FX diff --git a/data/locale/ro.ts b/data/locale/ro.ts index 58abbba99..4823ca57e 100644 --- a/data/locale/ro.ts +++ b/data/locale/ro.ts @@ -5334,62 +5334,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/ru.ts b/data/locale/ru.ts index 73b7e06ad..dee2b8482 100644 --- a/data/locale/ru.ts +++ b/data/locale/ru.ts @@ -5348,62 +5348,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount Величина отправки канала - + Move &left Подвинуть в&лево - + Move &right Подвинуть в&право - + Rename &channel Пере&именовать канал - + R&emove channel &Удалить канал - + Remove &unused channels Удалить &неиÑпользуемые каналы - + Set channel color УÑтановить цвет канала - + Remove channel color Удалить цвет канала - + Pick random channel color Выбрать Ñлучайный цвет канала - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Ðазначить на: - + New mixer Channel Ðовый канал ЭФ diff --git a/data/locale/sl.ts b/data/locale/sl.ts index e7bfbc308..1aa67d54d 100644 --- a/data/locale/sl.ts +++ b/data/locale/sl.ts @@ -5333,62 +5333,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount - + Move &left - + Move &right - + Rename &channel - + R&emove channel - + Remove &unused channels - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: - + New mixer Channel diff --git a/data/locale/sr.ts b/data/locale/sr.ts index 183936bc7..557890f35 100644 --- a/data/locale/sr.ts +++ b/data/locale/sr.ts @@ -2178,7 +2178,7 @@ Please make sure you have write-permission to the file and the directory contain - MixerLine + MixerChannelView Channel send amount @@ -7752,7 +7752,7 @@ Please make sure you have read-permission to the file and the directory containi - MixerLineLcdSpinBox + MixerChannelLcdSpinBox Assign to: diff --git a/data/locale/sv.ts b/data/locale/sv.ts index f5d4e0fb4..b40306cac 100644 --- a/data/locale/sv.ts +++ b/data/locale/sv.ts @@ -5617,62 +5617,62 @@ Se till att du har skrivbehörighet till filen och mappen som innehÃ¥ller filen - MixerLine + MixerChannelView - + Channel send amount Kanalsändningsbelopp - + Move &left Flytta &vänster - + Move &right Flytta &höger - + Rename &channel Byt namn pÃ¥ &kanal - + R&emove channel T&a bort kanal - + Remove &unused channels Ta bort &oanvända kanaler - + Set channel color Ställ in kanalfärg - + Remove channel color Ta bort kanalfärg - + Pick random channel color Välj slumpmässig kanalfärg - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Tilldela till: - + New mixer Channel Ny FX-kanal diff --git a/data/locale/tr.ts b/data/locale/tr.ts index b899337a5..bd469667f 100644 --- a/data/locale/tr.ts +++ b/data/locale/tr.ts @@ -5619,62 +5619,62 @@ Lütfen dosyaya ve dosyayı içeren dizine yazma izniniz olduÄŸundan emin olun v - MixerLine + MixerChannelView - + Channel send amount Kanal gönderme miktarı - + Move &left Sol&a taşı - + Move &right &SaÄŸa taşı - + Rename &channel &Kanalı yeniden adlandır - + R&emove channel Kanalı k&aldır - + Remove &unused channels &Kullanılmayan kanalları kaldırın - + Set channel color Kanal rengini ayarla - + Remove channel color Kanal rengini kaldır - + Pick random channel color Rastgele kanal rengi seçin - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Ata: - + New mixer Channel Yeni FX Kanalı diff --git a/data/locale/uk.ts b/data/locale/uk.ts index 9fb6389c9..245c433a1 100644 --- a/data/locale/uk.ts +++ b/data/locale/uk.ts @@ -5334,62 +5334,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount Величина відправки каналу - + Move &left Рухати вліво &L - + Move &right Рухати вправо &R - + Rename &channel Перейменувати канал &C - + R&emove channel Видалити канал &e - + Remove &unused channels Видалити канали Ñкі &не викориÑтовуютьÑÑ - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: Призначити до: - + New mixer Channel Ðовий ефект каналу diff --git a/data/locale/zh_CN.ts b/data/locale/zh_CN.ts index 9b783b963..57c080341 100644 --- a/data/locale/zh_CN.ts +++ b/data/locale/zh_CN.ts @@ -5343,62 +5343,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount 通é“å‘é€çš„æ•°é‡ - + Move &left å‘左移(&L) - + Move &right å‘å³ç§»(&R) - + Rename &channel é‡å‘½å通é“(&C) - + R&emove channel 删除通é“(&E) - + Remove &unused channels 移除所有未用通é“(&U) - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: 分é…ç»™: - + New mixer Channel æ–°çš„æ•ˆæžœé€šé“ diff --git a/data/locale/zh_TW.ts b/data/locale/zh_TW.ts index a3a727edb..9348dfb32 100644 --- a/data/locale/zh_TW.ts +++ b/data/locale/zh_TW.ts @@ -5334,62 +5334,62 @@ Please make sure you have write permission to the file and the directory contain - MixerLine + MixerChannelView - + Channel send amount 通é“發é€çš„æ•¸é‡ - + Move &left å‘左移(&L) - + Move &right å‘å³ç§»(&R) - + Rename &channel é‡å‘½å通é“(&C) - + R&emove channel 刪除通é“(&E) - + Remove &unused channels 移除所有未用通é“(&U) - + Set channel color - + Remove channel color - + Pick random channel color - MixerLineLcdSpinBox + MixerChannelLcdSpinBox - + Assign to: 分é…給: - + New mixer Channel æ–°çš„æ•ˆæžœé€šé“ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 9eeb41993..3ef397651 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -632,7 +632,7 @@ lmms--gui--ControllerRackView QPushButton { font-size: 10px; } -lmms--gui--MixerLine { +lmms--gui--MixerChannelView { background: #5b6571; color: #e0e0e0; qproperty-backgroundActive: qlineargradient(spread:reflect, x1:0, y1:0, x2:1, y2:0, @@ -643,6 +643,11 @@ lmms--gui--MixerLine { qproperty-strokeInnerInactive: rgba( 255, 255, 255, 50 ); } +lmms--gui--MixerChannelView QGraphicsView { + background: transparent; + border-style: none; +} + /* persistent peak markers for fx peak meters */ lmms--gui--Fader { qproperty-peakGreen: rgb( 74, 253, 133); diff --git a/data/themes/default/receive_bg_arrow.png b/data/themes/default/receive_bg_arrow.png index d4961540ad081a9c2a9b55bf28cca0529c6014f7..368a1bf15a9396285b9da153139940539746d6a2 100644 GIT binary patch literal 4824 zcmeHKc~Dd577vTE2-QcS2ug_&tAI&vR+0-*fgp6@t1(_N#L@yoCxkz&V+OV!L&D76ooHAbVRjAC6&ivL_A52 z5iz}7N+9U(zSy-WH`>!?Q}=PwB2MlUyI(rui?4rn>fJl_iQ9?ZX7{~Ac2;FUmNWXT z?smKz&~9*@gK9PucexkghiaxX9PxMOLza_W;?G8wL^nLrpQnm#tO{4g@D%UwaoTKG z791*yu}Ql3`9M7Bs^b1Zn-^JVa!pUaVvfqcy2V^!_84x?Id!jPW@BIO+V>6@&)W*(=Vr7V~apTwL1ee(eb*W~)U*i)dL-<@4z&uiwN^FE7pu48Ijq<=S=?W`r#k z`xW1w*Z3m+m<6v;wm*99x>zgR?m66f+^H$2$+qNX_NJ9(Ju>kFXZwvUBd$Wq7HS^Bpc-pe-8m{=BAwOUI=C?_uioRRbck54TXBX3}ehvNMT&!MK zHMP%UN_|xGDe;8L-LexMwQgR~#i^EAE~2kZE;V)2%dIV+X1=;ea!M`D+x%mH_pEIb zk{>R;_UyCV0R>@3b+ffBXaS)yXosCkZ5-dS&`g;6Y;KOHyER#cC9lCfA>37KU#TM6YSS z_;?wtd!nD8{It2E`4{zpdvUN?`;e9r1AUb8evtrG~ zAVI)|h|9%y>douB`Lh{>M^mh76r=@yeP|-*%C+sSXd+r$D%`Xk-jlGmW&xC61NYQY zUIyT~Z(~~1u2?AF#nf%w{F^{?zr3QLxqDjc7bl;6apTaJY4s`x^-1MxTx`1$c3MO^Z zmWb>w4J3pg5a|}@KfeE3Hc*nZ#l&PYr`G27SM9|7_A|CO-qw&j>pXh7z2}1+-M;Jm z`C;$0?t))T|7~9%nw%fywkyNsI1YL4aqhK`=QrWk9Z!9EZ2vh<>`v#{w(XVB{+7sH zo`vTuni1)gIek*G-}>kOsTzFnIl}Rm*;u4?gPrw+2k$d4xm(z@n^wfOx31{sq!8%LW1Y5YwPmJ*``WhdHr@g4+jB*% zO4pL9f~v$AMXykUVMQSL%-5?CNi2pF#h6U4WVBdx}9HB6=I zbUKQTK~ZUBR2rAdr9yNnolXW8WNnfXNAzT+cBTPh2!oGlB^tRJm#dUS112I?CEz>~ z3FwI<@hQ{-!6>{^JIn&$gQ`c=R2l`MDiqYQE3|k(A^;f<=ucN@MPQ~=mta~|f<}S` zBw|W@=2(cpV1aNH!H|+nu23730_1(<43&)9sS`ADMjKQ@#o{mp0M&x{Xdl_*a_I-M zK8nqtF{U#%5a4bU|0C;>+>OS-N+5vwDoKLDX)vEhGQuU3<`}48r4~n&63hSvz$tQo!-cpQ%Zn~0(@>O2W};Gr zj4-4$GK0>>xKfl$L+Ro%5aAj*s7fSmY*Yp)6hJ{-u0+ZNfnW#+1<^=2WCUY~$zEJ9 zj80>-#dN8c5z4R+u&*$fN1{`p4-#P~ZLi$;c6Ff_(q zh03K#e`RfG9-`0CmIukTz<-iaG}KWMSp3k_&|{q3*h)mAu_<6gGL(WANyJcNoB(S` zC0UIqWf+(q!{s_6m;XsAaM+-?#cU>-CSgHjCdNX@Tq$UCNW!4e#VCu7K|`#LqH9%B zT!&~dUm4&La0SZK$Q5ypaR=Q$jzzZ`Gi(naj7$UP9|)rkCrmZWjFF6esDI$Z$7nES z$bfZ2GBCWrEToPO!(q-qynp9scrX4=7ewN?lTYGzoUU=YK8b-(G9It4ak@T7o-C(OM`%q z^o@3ANllYqJ2>{HI(c^W_||s)bH?DtNr@`&U8*ScYHwlwiHD84f}^$G`3IIHw-rr= a>|?%4OE;&Nbfy7(LU2GBzszq{+W!Ef0va;_ literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^vOsLX!3HE5KYi{3QY^(zo*^7SP{WbZ0pxQQctjQh z)n5l;MkkHg6+l7B64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1ShdAc}; zM6|xWa+mXvfk@lKB|CiF3tIUeimZEKvG+~r!u8^E3!8%ct~P!9tI+aij*WnaR=_sD z%{w);c6o2nT^h6KJo|}l$`^{gjNHJ?CTD!)Pxu94gHJZeYwAU8nJ;sHn5X$o zcV2kp_lM`Zv$p+MKJQGl<(f6#@h@$D?~DsX1sC@_uIDYTtur`Nl?ZelgQu&X%Q~lo FCIC;uXG#D7 diff --git a/data/themes/default/send_bg_arrow.png b/data/themes/default/send_bg_arrow.png index 05a8c7366b4fb0349cc6dfddb68edaef26a77aa0..311505b6850b90a8138c72a8e1e2c8920b383873 100644 GIT binary patch literal 4810 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 PnkI8EW-qY&5Dq7w`MG9~LoS1njjx zc?aWXzB$JC?=0_PGhhgA3ZCa}+qZ!6%K_!@MSq2zWXfCmCS>^l-N)eR>gTe~DWM4f DG7)b5 diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 7963f51a4..3a8f411de 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -674,7 +674,7 @@ lmms--gui--ControllerRackView QPushButton { font-size: 10px; } -lmms--gui--MixerLine { +lmms--gui--MixerChannelView { background: #14161A; color: #d1d8e4; qproperty-backgroundActive: #3B424A; @@ -684,6 +684,11 @@ lmms--gui--MixerLine { qproperty-strokeInnerInactive: #0C0D0F; } +lmms--gui--MixerChannelView QGraphicsView { + background: transparent; + border-style: none; +} + /* persistent peak markers for fx peak meters */ lmms--gui--Fader { qproperty-peakGreen: #0ad45c; diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index d7d5fb83a..e89a576e9 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -25,7 +25,7 @@ #ifndef LMMS_GUI_INSTRUMENT_TRACK_VIEW_H #define LMMS_GUI_INSTRUMENT_TRACK_VIEW_H -#include "MixerLineLcdSpinBox.h" +#include "MixerChannelLcdSpinBox.h" #include "TrackView.h" #include "InstrumentTrack.h" @@ -99,7 +99,7 @@ private: // widgets in track-settings-widget TrackLabelButton * m_tlb; - MixerLineLcdSpinBox* m_mixerChannelNumber; + MixerChannelLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/InstrumentTrackWindow.h b/include/InstrumentTrackWindow.h index 971c63899..48a352cbd 100644 --- a/include/InstrumentTrackWindow.h +++ b/include/InstrumentTrackWindow.h @@ -43,7 +43,7 @@ namespace gui { class EffectRackView; -class MixerLineLcdSpinBox; +class MixerChannelLcdSpinBox; class InstrumentFunctionArpeggioView; class InstrumentFunctionNoteStackingView; class InstrumentMidiIOView; @@ -142,7 +142,7 @@ private: QLabel * m_pitchLabel; LcdSpinBox* m_pitchRangeSpinBox; QLabel * m_pitchRangeLabel; - MixerLineLcdSpinBox * m_mixerChannelNumber; + MixerChannelLcdSpinBox * m_mixerChannelNumber; diff --git a/include/MixerLineLcdSpinBox.h b/include/MixerChannelLcdSpinBox.h similarity index 74% rename from include/MixerLineLcdSpinBox.h rename to include/MixerChannelLcdSpinBox.h index 1ae2813f2..0abd9f100 100644 --- a/include/MixerLineLcdSpinBox.h +++ b/include/MixerChannelLcdSpinBox.h @@ -1,5 +1,5 @@ /* - * MixerLineLcdSpinBox.h - a specialization of LcdSpnBox for setting mixer channels + * MixerChannelLcdSpinBox.h - a specialization of LcdSpnBox for setting mixer channels * * Copyright (c) 2004-2014 Tobias Doerffel * @@ -22,8 +22,8 @@ * */ -#ifndef LMMS_GUI_MIXER_LINE_LCD_SPIN_BOX_H -#define LMMS_GUI_MIXER_LINE_LCD_SPIN_BOX_H +#ifndef LMMS_GUI_MIXER_CHANNEL_LCD_SPIN_BOX_H +#define LMMS_GUI_MIXER_CHANNEL_LCD_SPIN_BOX_H #include "LcdSpinBox.h" @@ -34,14 +34,14 @@ namespace lmms::gui class TrackView; -class MixerLineLcdSpinBox : public LcdSpinBox +class MixerChannelLcdSpinBox : public LcdSpinBox { Q_OBJECT public: - MixerLineLcdSpinBox(int numDigits, QWidget * parent, const QString& name, TrackView * tv = nullptr) : + MixerChannelLcdSpinBox(int numDigits, QWidget * parent, const QString& name, TrackView * tv = nullptr) : LcdSpinBox(numDigits, parent, name), m_tv(tv) {} - ~MixerLineLcdSpinBox() override = default; + ~MixerChannelLcdSpinBox() override = default; void setTrackView(TrackView * tv); @@ -56,4 +56,4 @@ private: } // namespace lmms::gui -#endif // LMMS_GUI_MIXER_LINE_LCD_SPIN_BOX_H +#endif // LMMS_GUI_MIXER_CHANNEL_LCD_SPIN_BOX_H diff --git a/include/MixerChannelView.h b/include/MixerChannelView.h new file mode 100644 index 000000000..8d2306f91 --- /dev/null +++ b/include/MixerChannelView.h @@ -0,0 +1,131 @@ +/* + * MixerChannelView.h - the mixer channel view + * + * Copyright (c) 2022 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef MIXER_CHANNEL_VIEW_H +#define MIXER_CHANNEL_VIEW_H + +#include "EffectRackView.h" +#include "Fader.h" +#include "Knob.h" +#include "LcdWidget.h" +#include "PixmapButton.h" +#include "SendButtonIndicator.h" + +#include +#include +#include +#include +#include + +namespace lmms::gui +{ + constexpr int MIXER_CHANNEL_INNER_BORDER_SIZE = 3; + constexpr int MIXER_CHANNEL_OUTER_BORDER_SIZE = 1; + + class MixerChannelView : public QWidget + { + Q_OBJECT + Q_PROPERTY(QBrush backgroundActive READ backgroundActive WRITE setBackgroundActive) + Q_PROPERTY(QColor strokeOuterActive READ strokeOuterActive WRITE setStrokeOuterActive) + Q_PROPERTY(QColor strokeOuterInactive READ strokeOuterInactive WRITE setStrokeOuterInactive) + Q_PROPERTY(QColor strokeInnerActive READ strokeInnerActive WRITE setStrokeInnerActive) + Q_PROPERTY(QColor strokeInnerInactive READ strokeInnerInactive WRITE setStrokeInnerInactive) + public: + enum class SendReceiveState + { + None, SendToThis, ReceiveFromThis + }; + + MixerChannelView(QWidget* parent, MixerView* mixerView, int channelIndex); + void paintEvent(QPaintEvent* event) override; + void contextMenuEvent(QContextMenuEvent*) override; + void mousePressEvent(QMouseEvent*) override; + void mouseDoubleClickEvent(QMouseEvent*) override; + bool eventFilter(QObject* dist, QEvent* event) override; + + int channelIndex() const; + void setChannelIndex(int index); + + SendReceiveState sendReceiveState() const; + void setSendReceiveState(const SendReceiveState& state); + + QBrush backgroundActive() const; + void setBackgroundActive(const QBrush& c); + + QColor strokeOuterActive() const; + void setStrokeOuterActive(const QColor& c); + + QColor strokeOuterInactive() const; + void setStrokeOuterInactive(const QColor& c); + + QColor strokeInnerActive() const; + void setStrokeInnerActive(const QColor& c); + + QColor strokeInnerInactive() const; + void setStrokeInnerInactive(const QColor& c); + + public slots: + void renameChannel(); + void resetColor(); + void selectColor(); + void randomizeColor(); + + private slots: + void renameFinished(); + void removeChannel(); + void removeUnusedChannels(); + void moveChannelLeft(); + void moveChannelRight(); + + private: + QString elideName(const QString& name); + + private: + SendButtonIndicator* m_sendButton; + Knob* m_sendKnob; + LcdWidget* m_channelNumberLcd; + QLineEdit* m_renameLineEdit; + QGraphicsView* m_renameLineEditView; + QLabel* m_sendArrow; + QLabel* m_receiveArrow; + PixmapButton* m_muteButton; + PixmapButton* m_soloButton; + Fader* m_fader; + EffectRackView* m_effectRackView; + MixerView* m_mixerView; + SendReceiveState m_sendReceiveState = SendReceiveState::None; + int m_channelIndex = 0; + bool m_inRename = false; + + QBrush m_backgroundActive; + QColor m_strokeOuterActive; + QColor m_strokeOuterInactive; + QColor m_strokeInnerActive; + QColor m_strokeInnerInactive; + + friend class MixerView; + }; +} // namespace lmms::gui + +#endif \ No newline at end of file diff --git a/include/MixerLine.h b/include/MixerLine.h deleted file mode 100644 index 655b30ec3..000000000 --- a/include/MixerLine.h +++ /dev/null @@ -1,118 +0,0 @@ -/* - * MixerLine.h - Mixer line widget - * - * Copyright (c) 2009 Andrew Kelley - * Copyright (c) 2014 Tobias Doerffel - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#ifndef LMMS_GUI_MIXER_LINE_H -#define LMMS_GUI_MIXER_LINE_H - -#include - -class QGraphicsView; -class QLineEdit; - -namespace lmms::gui -{ - - -class Knob; -class LcdWidget; -class MixerView; -class SendButtonIndicator; - -class MixerLine : public QWidget -{ - Q_OBJECT -public: - Q_PROPERTY( QBrush backgroundActive READ backgroundActive WRITE setBackgroundActive ) - Q_PROPERTY( QColor strokeOuterActive READ strokeOuterActive WRITE setStrokeOuterActive ) - Q_PROPERTY( QColor strokeOuterInactive READ strokeOuterInactive WRITE setStrokeOuterInactive ) - Q_PROPERTY( QColor strokeInnerActive READ strokeInnerActive WRITE setStrokeInnerActive ) - Q_PROPERTY( QColor strokeInnerInactive READ strokeInnerInactive WRITE setStrokeInnerInactive ) - MixerLine( QWidget * _parent, MixerView * _mv, int _channelIndex); - ~MixerLine() override; - - void paintEvent( QPaintEvent * ) override; - void mousePressEvent( QMouseEvent * ) override; - void mouseDoubleClickEvent( QMouseEvent * ) override; - void contextMenuEvent( QContextMenuEvent * ) override; - - inline int channelIndex() { return m_channelIndex; } - void setChannelIndex(int index); - - Knob * m_sendKnob; - SendButtonIndicator * m_sendBtn; - - QBrush backgroundActive() const; - void setBackgroundActive( const QBrush & c ); - - QColor strokeOuterActive() const; - void setStrokeOuterActive( const QColor & c ); - - QColor strokeOuterInactive() const; - void setStrokeOuterInactive( const QColor & c ); - - QColor strokeInnerActive() const; - void setStrokeInnerActive( const QColor & c ); - - QColor strokeInnerInactive() const; - void setStrokeInnerInactive( const QColor & c ); - - static const int MixerLineHeight; - - bool eventFilter (QObject *dist, QEvent *event) override; - -private: - void drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isActive, bool sendToThis, bool receiveFromThis ); - QString elideName( const QString & name ); - - MixerView * m_mv; - LcdWidget* m_lcd; - int m_channelIndex; - QBrush m_backgroundActive; - QColor m_strokeOuterActive; - QColor m_strokeOuterInactive; - QColor m_strokeInnerActive; - QColor m_strokeInnerInactive; - bool m_inRename; - QLineEdit * m_renameLineEdit; - QGraphicsView * m_view; - -public slots: - void renameChannel(); - void resetColor(); - void selectColor(); - void randomizeColor(); - -private slots: - void renameFinished(); - void removeChannel(); - void removeUnusedChannels(); - void moveChannelLeft(); - void moveChannelRight(); -}; - - -} // namespace lmms::gui - -#endif // LMMS_GUI_MIXER_LINE_H diff --git a/include/MixerView.h b/include/MixerView.h index 2bb5ed417..a47786481 100644 --- a/include/MixerView.h +++ b/include/MixerView.h @@ -2,7 +2,7 @@ * MixerView.h - effect-mixer-view for LMMS * * Copyright (c) 2008-2014 Tobias Doerffel - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -30,6 +30,7 @@ #include #include +#include "MixerChannelView.h" #include "ModelView.h" #include "Engine.h" #include "Fader.h" @@ -37,61 +38,38 @@ #include "embed.h" #include "EffectRackView.h" -class QButtonGroup; - - namespace lmms::gui { - -class MixerLine; - class LMMS_EXPORT MixerView : public QWidget, public ModelView, public SerializingObjectHook { Q_OBJECT public: - class MixerChannelView - { - public: - MixerChannelView(QWidget * _parent, MixerView * _mv, int _chIndex ); - - void setChannelIndex( int index ); - - MixerLine * m_mixerLine; - PixmapButton * m_muteBtn; - PixmapButton * m_soloBtn; - Fader * m_fader; - EffectRackView * m_rackView; - }; - - MixerView(); - ~MixerView() override; + void keyPressEvent(QKeyEvent* e) override; - void keyPressEvent(QKeyEvent * e) override; + void saveSettings(QDomDocument& doc, QDomElement& domElement) override; + void loadSettings(const QDomElement& domElement) override; - void saveSettings( QDomDocument & _doc, QDomElement & _this ) override; - void loadSettings( const QDomElement & _this ) override; - - inline MixerLine * currentMixerLine() + inline MixerChannelView* currentMixerChannel() { - return m_currentMixerLine; + return m_currentMixerChannel; } - inline MixerChannelView * channelView(int index) + inline MixerChannelView* channelView(int index) { return m_mixerChannelViews[index]; } - void setCurrentMixerLine( MixerLine * _line ); - void setCurrentMixerLine( int _line ); + void setCurrentMixerChannel(MixerChannelView* channel); + void setCurrentMixerChannel(int channel); void clear(); // display the send button and knob correctly - void updateMixerLine(int index); + void updateMixerChannel(int index); // notify the view that a mixer channel was deleted void deleteChannel(int index); @@ -115,22 +93,22 @@ public slots: int addNewChannel(); protected: - void closeEvent( QCloseEvent * _ce ) override; - + void closeEvent(QCloseEvent* ce) override; + private slots: void updateFaders(); void toggledSolo(); private: - QVector m_mixerChannelViews; + QVector m_mixerChannelViews; - MixerLine * m_currentMixerLine; + MixerChannelView* m_currentMixerChannel; - QScrollArea * channelArea; - QHBoxLayout * chLayout; - QWidget * m_channelAreaWidget; - QStackedLayout * m_racksLayout; - QWidget * m_racksWidget; + QScrollArea* channelArea; + QHBoxLayout* chLayout; + QWidget* m_channelAreaWidget; + QStackedLayout* m_racksLayout; + QWidget* m_racksWidget; void updateMaxChannelSelector(); diff --git a/include/SampleTrackView.h b/include/SampleTrackView.h index 3ccb97aea..2f94bfb56 100644 --- a/include/SampleTrackView.h +++ b/include/SampleTrackView.h @@ -26,7 +26,7 @@ #define LMMS_GUI_SAMPLE_TRACK_VIEW_H -#include "MixerLineLcdSpinBox.h" +#include "MixerChannelLcdSpinBox.h" #include "TrackView.h" namespace lmms @@ -91,7 +91,7 @@ private slots: private: SampleTrackWindow * m_window; - MixerLineLcdSpinBox* m_mixerChannelNumber; + MixerChannelLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/SampleTrackWindow.h b/include/SampleTrackWindow.h index c2a722d53..4d535bfe5 100644 --- a/include/SampleTrackWindow.h +++ b/include/SampleTrackWindow.h @@ -38,7 +38,7 @@ namespace lmms::gui class EffectRackView; class Knob; -class MixerLineLcdSpinBox; +class MixerChannelLcdSpinBox; class SampleTrackView; @@ -90,7 +90,7 @@ private: QLineEdit * m_nameLineEdit; Knob * m_volumeKnob; Knob * m_panningKnob; - MixerLineLcdSpinBox * m_mixerChannelNumber; + MixerChannelLcdSpinBox * m_mixerChannelNumber; EffectRackView * m_effectRack; } ; diff --git a/include/SendButtonIndicator.h b/include/SendButtonIndicator.h index 86f38318f..9e9417926 100644 --- a/include/SendButtonIndicator.h +++ b/include/SendButtonIndicator.h @@ -37,27 +37,25 @@ class FloatModel; namespace gui { -class MixerLine; +class MixerChannelView; class MixerView; - -class SendButtonIndicator : public QLabel +class SendButtonIndicator : public QLabel { public: - SendButtonIndicator( QWidget * _parent, MixerLine * _owner, - MixerView * _mv); + SendButtonIndicator(QWidget* parent, MixerChannelView* owner, MixerView* mv); - void mousePressEvent( QMouseEvent * e ) override; + void mousePressEvent(QMouseEvent* e) override; void updateLightStatus(); private: - MixerLine * m_parent; - MixerView * m_mv; + MixerChannelView* m_parent; + MixerView* m_mv; QPixmap m_qpmOff = embed::getIconPixmap("mixer_send_off", 29, 20); QPixmap m_qpmOn = embed::getIconPixmap("mixer_send_on", 29, 20); - FloatModel * getSendModel(); + FloatModel* getSendModel(); }; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index ac010f4f4..001c92c79 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -27,7 +27,7 @@ SET(LMMS_SRCS gui/MicrotunerConfig.cpp gui/MidiCCRackView.cpp gui/MidiSetupWidget.cpp - gui/MixerLine.cpp + gui/MixerChannelView.cpp gui/MixerView.cpp gui/ModelView.cpp gui/PeakControllerDialog.cpp @@ -113,7 +113,7 @@ SET(LMMS_SRCS gui/widgets/LedCheckBox.cpp gui/widgets/LeftRightNav.cpp gui/widgets/MeterDialog.cpp - gui/widgets/MixerLineLcdSpinBox.cpp + gui/widgets/MixerChannelLcdSpinBox.cpp gui/widgets/NStateButton.cpp gui/widgets/Oscilloscope.cpp gui/widgets/PixmapButton.cpp diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp new file mode 100644 index 000000000..0f8ccedea --- /dev/null +++ b/src/gui/MixerChannelView.cpp @@ -0,0 +1,447 @@ +/* + * MixerChannelView.h - the mixer channel view + * + * Copyright (c) 2022 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "CaptionMenu.h" +#include "ColorChooser.h" +#include "GuiApplication.h" +#include "Mixer.h" +#include "MixerChannelView.h" +#include "MixerView.h" +#include "Song.h" + +#include "gui_templates.h" +#include "lmms_math.h" + +#include +#include +#include +#include +#include + +#include + +namespace lmms::gui +{ + MixerChannelView::MixerChannelView(QWidget* parent, MixerView* mixerView, int channelIndex) : + QWidget(parent), + m_mixerView(mixerView), + m_channelIndex(channelIndex) + { + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); + + auto retainSizeWhenHidden = [](QWidget* widget) + { + auto sizePolicy = widget->sizePolicy(); + sizePolicy.setRetainSizeWhenHidden(true); + widget->setSizePolicy(sizePolicy); + }; + + m_sendButton = new SendButtonIndicator{this, this, mixerView}; + retainSizeWhenHidden(m_sendButton); + + m_sendKnob = new Knob{KnobType::Bright26, this, tr("Channel send amount")}; + retainSizeWhenHidden(m_sendKnob); + + m_channelNumberLcd = new LcdWidget{2, this}; + m_channelNumberLcd->setValue(channelIndex); + retainSizeWhenHidden(m_channelNumberLcd); + + const auto mixerChannel = Engine::mixer()->mixerChannel(channelIndex); + const auto mixerName = mixerChannel->m_name; + setToolTip(mixerName); + + m_renameLineEdit = new QLineEdit{mixerName, nullptr}; + m_renameLineEdit->setFixedWidth(65); + m_renameLineEdit->setFont(pointSizeF(font(), 7.5f)); + m_renameLineEdit->setReadOnly(true); + m_renameLineEdit->installEventFilter(this); + + auto renameLineEditScene = new QGraphicsScene{}; + m_renameLineEditView = new QGraphicsView{}; + m_renameLineEditView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_renameLineEditView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_renameLineEditView->setAttribute(Qt::WA_TransparentForMouseEvents, true); + m_renameLineEditView->setScene(renameLineEditScene); + + auto renameLineEditProxy = renameLineEditScene->addWidget(m_renameLineEdit); + renameLineEditProxy->setRotation(-90); + m_renameLineEditView->setFixedSize(m_renameLineEdit->height() + 5, m_renameLineEdit->width() + 5); + + m_sendArrow = new QLabel{}; + m_sendArrow->setPixmap(embed::getIconPixmap("send_bg_arrow")); + retainSizeWhenHidden(m_sendArrow); + m_sendArrow->setVisible(m_sendReceiveState == SendReceiveState::SendToThis); + + m_receiveArrow = new QLabel{}; + m_receiveArrow->setPixmap(embed::getIconPixmap("receive_bg_arrow")); + retainSizeWhenHidden(m_receiveArrow); + m_receiveArrow->setVisible(m_sendReceiveState == SendReceiveState::ReceiveFromThis); + + m_muteButton = new PixmapButton(this, tr("Mute")); + m_muteButton->setModel(&mixerChannel->m_muteModel); + m_muteButton->setActiveGraphic(embed::getIconPixmap("led_off")); + m_muteButton->setInactiveGraphic(embed::getIconPixmap("led_green")); + m_muteButton->setCheckable(true); + m_muteButton->setToolTip(tr("Mute this channel")); + + m_soloButton = new PixmapButton(this, tr("Solo")); + m_soloButton->setModel(&mixerChannel->m_soloModel); + m_soloButton->setActiveGraphic(embed::getIconPixmap("led_red")); + m_soloButton->setInactiveGraphic(embed::getIconPixmap("led_off")); + m_soloButton->setCheckable(true); + m_soloButton->setToolTip(tr("Solo this channel")); + connect(&mixerChannel->m_soloModel, &BoolModel::dataChanged, mixerView, &MixerView::toggledSolo, Qt::DirectConnection); + + QVBoxLayout* soloMuteLayout = new QVBoxLayout(); + soloMuteLayout->setContentsMargins(0, 0, 0, 0); + soloMuteLayout->setSpacing(0); + soloMuteLayout->addWidget(m_soloButton, 0, Qt::AlignHCenter); + soloMuteLayout->addWidget(m_muteButton, 0, Qt::AlignHCenter); + + m_fader = new Fader{&mixerChannel->m_volumeModel, tr("Fader %1").arg(channelIndex), this}; + m_fader->setLevelsDisplayedInDBFS(); + m_fader->setMinPeak(dbfsToAmp(-42)); + m_fader->setMaxPeak(dbfsToAmp(9)); + + m_effectRackView = new EffectRackView{&mixerChannel->m_fxChain, mixerView->m_racksWidget}; + m_effectRackView->setFixedWidth(EffectRackView::DEFAULT_WIDTH); + + auto mainLayout = new QVBoxLayout{this}; + mainLayout->setContentsMargins(4, 4, 4, 4); + mainLayout->addWidget(m_receiveArrow, 0, Qt::AlignHCenter); + mainLayout->addWidget(m_sendButton, 0, Qt::AlignHCenter); + mainLayout->addWidget(m_sendKnob, 0, Qt::AlignHCenter); + mainLayout->addWidget(m_sendArrow, 0, Qt::AlignHCenter); + mainLayout->addWidget(m_channelNumberLcd, 0, Qt::AlignHCenter); + mainLayout->addStretch(); + mainLayout->addWidget(m_renameLineEditView, 0, Qt::AlignHCenter); + mainLayout->addLayout(soloMuteLayout, 0); + mainLayout->addWidget(m_fader, 0, Qt::AlignHCenter); + + connect(m_renameLineEdit, &QLineEdit::editingFinished, this, &MixerChannelView::renameFinished); + } + + void MixerChannelView::contextMenuEvent(QContextMenuEvent*) + { + auto contextMenu = new CaptionMenu(Engine::mixer()->mixerChannel(m_channelIndex)->m_name, this); + + if (m_channelIndex != 0) // no move-options in master + { + contextMenu->addAction(tr("Move &left"), this, &MixerChannelView::moveChannelLeft); + contextMenu->addAction(tr("Move &right"), this, &MixerChannelView::moveChannelRight); + } + + contextMenu->addAction(tr("Rename &channel"), this, &MixerChannelView::renameChannel); + contextMenu->addSeparator(); + + if (m_channelIndex != 0) // no remove-option in master + { + contextMenu->addAction(embed::getIconPixmap("cancel"), tr("R&emove channel"), this, &MixerChannelView::removeChannel); + contextMenu->addSeparator(); + } + + contextMenu->addAction(embed::getIconPixmap("cancel"), tr("Remove &unused channels"), this, &MixerChannelView::removeUnusedChannels); + contextMenu->addSeparator(); + + auto colorMenu = QMenu{tr("Color"), this}; + colorMenu.setIcon(embed::getIconPixmap("colorize")); + colorMenu.addAction(tr("Change"), this, &MixerChannelView::selectColor); + colorMenu.addAction(tr("Reset"), this, &MixerChannelView::resetColor); + colorMenu.addAction(tr("Pick random"), this, &MixerChannelView::randomizeColor); + contextMenu->addMenu(&colorMenu); + + contextMenu->exec(QCursor::pos()); + delete contextMenu; + } + + void MixerChannelView::paintEvent(QPaintEvent* event) + { + auto * mixer = Engine::mixer(); + const auto channel = mixer->mixerChannel(m_channelIndex); + const bool muted = channel->m_muteModel.value(); + const auto name = channel->m_name; + const auto elidedName = elideName(name); + const auto * mixerChannelView = m_mixerView->currentMixerChannel(); + const auto isActive = mixerChannelView == this; + + if (!m_inRename && m_renameLineEdit->text() != elidedName) + { + m_renameLineEdit->setText(elidedName); + } + + const auto width = rect().width(); + const auto height = rect().height(); + auto painter = QPainter{this}; + + if (channel->color().has_value() && !muted) + { + painter.fillRect(rect(), channel->color()->darker(isActive ? 120 : 150)); + } + else + { + painter.fillRect(rect(), isActive ? backgroundActive().color() : painter.background().color()); + } + + // inner border + painter.setPen(isActive ? strokeInnerActive() : strokeInnerInactive()); + painter.drawRect(1, 1, width - MIXER_CHANNEL_INNER_BORDER_SIZE, height - MIXER_CHANNEL_INNER_BORDER_SIZE); + + // outer border + painter.setPen(isActive ? strokeOuterActive() : strokeOuterInactive()); + painter.drawRect(0, 0, width - MIXER_CHANNEL_OUTER_BORDER_SIZE, height - MIXER_CHANNEL_OUTER_BORDER_SIZE); + + const auto & currentMixerChannelIndex = mixerChannelView->m_channelIndex; + const auto sendToThis = mixer->channelSendModel(currentMixerChannelIndex, m_channelIndex) != nullptr; + const auto receiveFromThis = mixer->channelSendModel(m_channelIndex, currentMixerChannelIndex) != nullptr; + const auto sendReceiveStateNone = !sendToThis && !receiveFromThis; + + // Only one or none of them can be on + assert(sendToThis ^ receiveFromThis || sendReceiveStateNone); + + m_sendArrow->setVisible(sendToThis); + m_receiveArrow->setVisible(receiveFromThis); + + if (sendReceiveStateNone) + { + setSendReceiveState(SendReceiveState::None); + } + else + { + setSendReceiveState(sendToThis ? SendReceiveState::SendToThis : SendReceiveState::ReceiveFromThis); + } + + QWidget::paintEvent(event); + } + + void MixerChannelView::mousePressEvent(QMouseEvent*) + { + if (m_mixerView->currentMixerChannel() != this) + { + m_mixerView->setCurrentMixerChannel(this); + } + } + + void MixerChannelView::mouseDoubleClickEvent(QMouseEvent*) + { + renameChannel(); + } + + bool MixerChannelView::eventFilter(QObject* dist, QEvent* event) + { + // If we are in a rename, capture the enter/return events and handle them + if (event->type() == QEvent::KeyPress) + { + auto keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) + { + if (m_inRename) + { + renameFinished(); + event->accept(); // Stop the event from propagating + return true; + } + } + } + return false; + } + + int MixerChannelView::channelIndex() const + { + return m_channelIndex; + } + + void MixerChannelView::setChannelIndex(int index) + { + MixerChannel* mixerChannel = Engine::mixer()->mixerChannel(index); + m_fader->setModel(&mixerChannel->m_volumeModel); + m_muteButton->setModel(&mixerChannel->m_muteModel); + m_soloButton->setModel(&mixerChannel->m_soloModel); + m_effectRackView->setModel(&mixerChannel->m_fxChain); + m_channelIndex = index; + } + + MixerChannelView::SendReceiveState MixerChannelView::sendReceiveState() const + { + return m_sendReceiveState; + } + + void MixerChannelView::setSendReceiveState(const SendReceiveState& state) + { + m_sendReceiveState = state; + m_sendArrow->setVisible(state == SendReceiveState::SendToThis); + m_receiveArrow->setVisible(state == SendReceiveState::ReceiveFromThis); + } + + QBrush MixerChannelView::backgroundActive() const + { + return m_backgroundActive; + } + + void MixerChannelView::setBackgroundActive(const QBrush& c) + { + m_backgroundActive = c; + } + + QColor MixerChannelView::strokeOuterActive() const + { + return m_strokeOuterActive; + } + + void MixerChannelView::setStrokeOuterActive(const QColor& c) + { + m_strokeOuterActive = c; + } + + QColor MixerChannelView::strokeOuterInactive() const + { + return m_strokeOuterInactive; + } + + void MixerChannelView::setStrokeOuterInactive(const QColor& c) + { + m_strokeOuterInactive = c; + } + + QColor MixerChannelView::strokeInnerActive() const + { + return m_strokeInnerActive; + } + + void MixerChannelView::setStrokeInnerActive(const QColor& c) + { + m_strokeInnerActive = c; + } + + QColor MixerChannelView::strokeInnerInactive() const + { + return m_strokeInnerInactive; + } + + void MixerChannelView::setStrokeInnerInactive(const QColor& c) + { + m_strokeInnerInactive = c; + } + + void MixerChannelView::renameChannel() + { + m_inRename = true; + setToolTip(""); + m_renameLineEdit->setReadOnly(false); + + m_channelNumberLcd->hide(); + m_renameLineEdit->setFixedWidth(m_renameLineEdit->width()); + m_renameLineEdit->setText(Engine::mixer()->mixerChannel(m_channelIndex)->m_name); + + m_renameLineEditView->setFocus(); + m_renameLineEdit->selectAll(); + m_renameLineEdit->setFocus(); + } + + void MixerChannelView::renameFinished() + { + m_inRename = false; + + m_renameLineEdit->deselect(); + m_renameLineEdit->setReadOnly(true); + m_renameLineEdit->setFixedWidth(m_renameLineEdit->width()); + m_channelNumberLcd->show(); + + auto newName = m_renameLineEdit->text(); + setFocus(); + + const auto mixerChannel = Engine::mixer()->mixerChannel(m_channelIndex); + if (!newName.isEmpty() && mixerChannel->m_name != newName) + { + mixerChannel->m_name = newName; + m_renameLineEdit->setText(elideName(newName)); + Engine::getSong()->setModified(); + } + + setToolTip(mixerChannel->m_name); + } + + void MixerChannelView::resetColor() + { + Engine::mixer()->mixerChannel(m_channelIndex)->setColor(std::nullopt); + Engine::getSong()->setModified(); + update(); + } + + void MixerChannelView::selectColor() + { + const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + + const auto initialColor = channel->color().value_or(backgroundActive().color()); + const auto * colorChooser = ColorChooser{this}.withPalette(ColorChooser::Palette::Mixer); + const auto newColor = colorChooser->getColor(initialColor); + + if (!newColor.isValid()) { return; } + + channel->setColor(newColor); + + Engine::getSong()->setModified(); + update(); + } + + void MixerChannelView::randomizeColor() + { + auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + channel->setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]); + Engine::getSong()->setModified(); + update(); + } + + void MixerChannelView::removeChannel() + { + auto mix = getGUI()->mixerView(); + mix->deleteChannel(m_channelIndex); + } + + void MixerChannelView::removeUnusedChannels() + { + auto mix = getGUI()->mixerView(); + mix->deleteUnusedChannels(); + } + + void MixerChannelView::moveChannelLeft() + { + auto mix = getGUI()->mixerView(); + mix->moveChannelLeft(m_channelIndex); + } + + void MixerChannelView::moveChannelRight() + { + auto mix = getGUI()->mixerView(); + mix->moveChannelRight(m_channelIndex); + } + + QString MixerChannelView::elideName(const QString& name) + { + const auto maxTextHeight = m_renameLineEdit->width(); + const auto metrics = QFontMetrics{m_renameLineEdit->font()}; + const auto elidedName = metrics.elidedText(name, Qt::ElideRight, maxTextHeight); + return elidedName; + } + +} // namespace lmms::gui \ No newline at end of file diff --git a/src/gui/MixerLine.cpp b/src/gui/MixerLine.cpp deleted file mode 100644 index 182e131d3..000000000 --- a/src/gui/MixerLine.cpp +++ /dev/null @@ -1,448 +0,0 @@ -/* - * MixerLine.cpp - Mixer line widget - * - * Copyright (c) 2009 Andrew Kelley - * Copyright (c) 2014 Tobias Doerffel - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#include "MixerLine.h" - -#include - -#include -#include -#include -#include - -#include "CaptionMenu.h" -#include "ColorChooser.h" -#include "embed.h" -#include "Knob.h" -#include "LcdWidget.h" -#include "Mixer.h" -#include "MixerView.h" -#include "gui_templates.h" -#include "GuiApplication.h" -#include "SendButtonIndicator.h" -#include "Song.h" - -namespace lmms::gui -{ - - -bool MixerLine::eventFilter( QObject *dist, QEvent *event ) -{ - // If we are in a rename, capture the enter/return events and handle them - if ( event->type() == QEvent::KeyPress ) - { - auto keyEvent = static_cast(event); - if( keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return ) - { - if( m_inRename ) - { - renameFinished(); - event->accept(); // Stop the event from propagating - return true; - } - } - } - return false; -} - -const int MixerLine::MixerLineHeight = 287; - -MixerLine::MixerLine( QWidget * _parent, MixerView * _mv, int _channelIndex ) : - QWidget( _parent ), - m_mv( _mv ), - m_channelIndex( _channelIndex ), - m_backgroundActive( Qt::SolidPattern ), - m_strokeOuterActive( 0, 0, 0 ), - m_strokeOuterInactive( 0, 0, 0 ), - m_strokeInnerActive( 0, 0, 0 ), - m_strokeInnerInactive( 0, 0, 0 ), - m_inRename( false ) -{ - setFixedSize( 33, MixerLineHeight ); - setAttribute( Qt::WA_OpaquePaintEvent, true ); - setCursor( QCursor( embed::getIconPixmap( "hand" ), 3, 3 ) ); - - // mixer sends knob - m_sendKnob = new Knob( KnobType::Bright26, this, tr( "Channel send amount" ) ); - m_sendKnob->move( 3, 22 ); - m_sendKnob->setVisible( false ); - - // send button indicator - m_sendBtn = new SendButtonIndicator( this, this, m_mv ); - m_sendBtn->move( 2, 2 ); - - // channel number - m_lcd = new LcdWidget( 2, this ); - m_lcd->setValue( m_channelIndex ); - m_lcd->move( 4, 58 ); - m_lcd->setMarginWidth( 1 ); - - QString name = Engine::mixer()->mixerChannel( m_channelIndex )->m_name; - setToolTip( name ); - - m_renameLineEdit = new QLineEdit(); - m_renameLineEdit->setText( name ); - m_renameLineEdit->setFixedWidth( 65 ); - m_renameLineEdit->setFont( pointSizeF( font(), 7.5f ) ); - m_renameLineEdit->setReadOnly( true ); - m_renameLineEdit->installEventFilter( this ); - - auto scene = new QGraphicsScene(); - scene->setSceneRect( 0, 0, 33, MixerLineHeight ); - - m_view = new QGraphicsView( this ); - m_view->setStyleSheet( "border-style: none; background: transparent;" ); - m_view->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); - m_view->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); - m_view->setAttribute( Qt::WA_TransparentForMouseEvents, true ); - m_view->setScene( scene ); - - QGraphicsProxyWidget * proxyWidget = scene->addWidget( m_renameLineEdit ); - proxyWidget->setRotation( -90 ); - proxyWidget->setPos( 8, 145 ); - - connect( m_renameLineEdit, SIGNAL(editingFinished()), this, SLOT(renameFinished())); - connect( &Engine::mixer()->mixerChannel( m_channelIndex )->m_muteModel, SIGNAL(dataChanged()), this, SLOT(update())); -} - - - - -MixerLine::~MixerLine() -{ - delete m_sendKnob; - delete m_sendBtn; - delete m_lcd; -} - - - - -void MixerLine::setChannelIndex( int index ) -{ - m_channelIndex = index; - m_lcd->setValue( m_channelIndex ); - m_lcd->update(); -} - - - -void MixerLine::drawMixerLine( QPainter* p, const MixerLine *mixerLine, bool isActive, bool sendToThis, bool receiveFromThis ) -{ - auto channel = Engine::mixer()->mixerChannel( m_channelIndex ); - bool muted = channel->m_muteModel.value(); - QString name = channel->m_name; - QString elidedName = elideName( name ); - if( !m_inRename && m_renameLineEdit->text() != elidedName ) - { - m_renameLineEdit->setText( elidedName ); - } - - int width = mixerLine->rect().width(); - int height = mixerLine->rect().height(); - - if (channel->color().has_value() && !muted) - { - p->fillRect(mixerLine->rect(), channel->color()->darker(isActive ? 120 : 150)); - } - else - { - p->fillRect( mixerLine->rect(), - isActive ? mixerLine->backgroundActive().color() : p->background().color() ); - } - - // inner border - p->setPen( isActive ? mixerLine->strokeInnerActive() : mixerLine->strokeInnerInactive() ); - p->drawRect( 1, 1, width-3, height-3 ); - - // outer border - p->setPen( isActive ? mixerLine->strokeOuterActive() : mixerLine->strokeOuterInactive() ); - p->drawRect( 0, 0, width-1, height-1 ); - - // draw the mixer send background - - static auto s_sendBgArrow = embed::getIconPixmap("send_bg_arrow", 29, 56); - static auto s_receiveBgArrow = embed::getIconPixmap("receive_bg_arrow", 29, 56); - p->drawPixmap(2, 0, 29, 56, sendToThis ? s_sendBgArrow : s_receiveBgArrow); -} - - - - -QString MixerLine::elideName( const QString & name ) -{ - const int maxTextHeight = 60; - QFontMetrics metrics( m_renameLineEdit->font() ); - QString elidedName = metrics.elidedText( name, Qt::ElideRight, maxTextHeight ); - return elidedName; -} - - - - -void MixerLine::paintEvent( QPaintEvent * ) -{ - bool sendToThis = Engine::mixer()->channelSendModel( m_mv->currentMixerLine()->m_channelIndex, m_channelIndex ) != nullptr; - bool receiveFromThis = Engine::mixer()->channelSendModel( m_channelIndex, m_mv->currentMixerLine()->m_channelIndex ) != nullptr; - QPainter painter; - painter.begin( this ); - drawMixerLine( &painter, this, m_mv->currentMixerLine() == this, sendToThis, receiveFromThis ); - painter.end(); -} - - - - -void MixerLine::mousePressEvent( QMouseEvent * ) -{ - m_mv->setCurrentMixerLine( this ); -} - - - - -void MixerLine::mouseDoubleClickEvent( QMouseEvent * ) -{ - renameChannel(); -} - - - - -void MixerLine::contextMenuEvent( QContextMenuEvent * ) -{ - QPointer contextMenu = new CaptionMenu( Engine::mixer()->mixerChannel( m_channelIndex )->m_name, this ); - if( m_channelIndex != 0 ) // no move-options in master - { - contextMenu->addAction( tr( "Move &left" ), this, SLOT(moveChannelLeft())); - contextMenu->addAction( tr( "Move &right" ), this, SLOT(moveChannelRight())); - } - contextMenu->addAction( tr( "Rename &channel" ), this, SLOT(renameChannel())); - contextMenu->addSeparator(); - - if( m_channelIndex != 0 ) // no remove-option in master - { - contextMenu->addAction( embed::getIconPixmap( "cancel" ), tr( "R&emove channel" ), this, SLOT(removeChannel())); - contextMenu->addSeparator(); - } - contextMenu->addAction( embed::getIconPixmap( "cancel" ), tr( "Remove &unused channels" ), this, SLOT(removeUnusedChannels())); - contextMenu->addSeparator(); - - QMenu colorMenu(tr("Color"), this); - colorMenu.setIcon(embed::getIconPixmap("colorize")); - colorMenu.addAction(tr("Change"), this, SLOT(selectColor())); - colorMenu.addAction(tr("Reset"), this, SLOT(resetColor())); - colorMenu.addAction(tr("Pick random"), this, SLOT(randomizeColor())); - contextMenu->addMenu(&colorMenu); - - contextMenu->exec( QCursor::pos() ); - delete contextMenu; -} - - - - -void MixerLine::renameChannel() -{ - m_inRename = true; - setToolTip( "" ); - m_renameLineEdit->setReadOnly( false ); - m_lcd->hide(); - m_renameLineEdit->setFixedWidth( 135 ); - m_renameLineEdit->setText( Engine::mixer()->mixerChannel( m_channelIndex )->m_name ); - m_view->setFocus(); - m_renameLineEdit->selectAll(); - m_renameLineEdit->setFocus(); -} - - - - -void MixerLine::renameFinished() -{ - m_inRename = false; - m_renameLineEdit->deselect(); - m_renameLineEdit->setReadOnly( true ); - m_renameLineEdit->setFixedWidth( 65 ); - m_lcd->show(); - QString newName = m_renameLineEdit->text(); - setFocus(); - if( !newName.isEmpty() && Engine::mixer()->mixerChannel( m_channelIndex )->m_name != newName ) - { - Engine::mixer()->mixerChannel( m_channelIndex )->m_name = newName; - m_renameLineEdit->setText( elideName( newName ) ); - Engine::getSong()->setModified(); - } - QString name = Engine::mixer()->mixerChannel( m_channelIndex )->m_name; - setToolTip( name ); -} - - - - -void MixerLine::removeChannel() -{ - MixerView * mix = getGUI()->mixerView(); - mix->deleteChannel( m_channelIndex ); -} - - - - -void MixerLine::removeUnusedChannels() -{ - MixerView * mix = getGUI()->mixerView(); - mix->deleteUnusedChannels(); -} - - - - -void MixerLine::moveChannelLeft() -{ - MixerView * mix = getGUI()->mixerView(); - mix->moveChannelLeft( m_channelIndex ); -} - - - - -void MixerLine::moveChannelRight() -{ - MixerView * mix = getGUI()->mixerView(); - mix->moveChannelRight( m_channelIndex ); -} - - - - -QBrush MixerLine::backgroundActive() const -{ - return m_backgroundActive; -} - - - - -void MixerLine::setBackgroundActive( const QBrush & c ) -{ - m_backgroundActive = c; -} - - - - -QColor MixerLine::strokeOuterActive() const -{ - return m_strokeOuterActive; -} - - - - -void MixerLine::setStrokeOuterActive( const QColor & c ) -{ - m_strokeOuterActive = c; -} - - - - -QColor MixerLine::strokeOuterInactive() const -{ - return m_strokeOuterInactive; -} - - - - -void MixerLine::setStrokeOuterInactive( const QColor & c ) -{ - m_strokeOuterInactive = c; -} - - - - -QColor MixerLine::strokeInnerActive() const -{ - return m_strokeInnerActive; -} - - - - -void MixerLine::setStrokeInnerActive( const QColor & c ) -{ - m_strokeInnerActive = c; -} - - - - -QColor MixerLine::strokeInnerInactive() const -{ - return m_strokeInnerInactive; -} - - - - -void MixerLine::setStrokeInnerInactive( const QColor & c ) -{ - m_strokeInnerInactive = c; -} - -// Ask user for a color, and set it as the mixer line color -void MixerLine::selectColor() -{ - const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); - const auto newColor = ColorChooser{this} - .withPalette(ColorChooser::Palette::Mixer) - ->getColor(channel->color().value_or(backgroundActive().color())); - if (!newColor.isValid()) { return; } - channel->setColor(newColor); - Engine::getSong()->setModified(); - update(); -} - -// Disable the usage of color on this mixer line -void MixerLine::resetColor() -{ - Engine::mixer()->mixerChannel(m_channelIndex)->setColor(std::nullopt); - Engine::getSong()->setModified(); - update(); -} - -// Pick a random color from the mixer palette and set it as our color -void MixerLine::randomizeColor() -{ - const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); - channel->setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[std::rand() % 48]); - Engine::getSong()->setModified(); - update(); -} - -} // namespace lmms::gui diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 018e72c2b..f20b60a38 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -33,9 +33,9 @@ #include "lmms_math.h" +#include "MixerChannelView.h" #include "MixerView.h" #include "Knob.h" -#include "MixerLine.h" #include "Mixer.h" #include "GuiApplication.h" #include "MainWindow.h" @@ -54,7 +54,7 @@ namespace lmms::gui MixerView::MixerView() : QWidget(), - ModelView( nullptr, this ), + ModelView(nullptr, this), SerializingObjectHook() { #if QT_VERSION < 0x50C00 @@ -68,54 +68,54 @@ MixerView::MixerView() : #endif Mixer * m = Engine::mixer(); - m->setHook( this ); + m->setHook(this); //QPalette pal = palette(); - //pal.setColor( QPalette::Window, QColor( 72, 76, 88 ) ); - //setPalette( pal ); + //pal.setColor(QPalette::Window, QColor(72, 76, 88)); + //setPalette(pal); - setAutoFillBackground( true ); - setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed ); + setAutoFillBackground(true); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - setWindowTitle( tr( "Mixer" ) ); - setWindowIcon( embed::getIconPixmap( "mixer" ) ); + setWindowTitle(tr("Mixer")); + setWindowIcon(embed::getIconPixmap("mixer")); // main-layout - auto ml = new QHBoxLayout; + auto ml = new QHBoxLayout{this}; // Set margins - ml->setContentsMargins( 0, 4, 0, 0 ); + ml->setContentsMargins(0, 4, 0, 0); // Channel area m_channelAreaWidget = new QWidget; - chLayout = new QHBoxLayout( m_channelAreaWidget ); - chLayout->setSizeConstraint( QLayout::SetMinimumSize ); - chLayout->setSpacing( 0 ); + chLayout = new QHBoxLayout(m_channelAreaWidget); + chLayout->setSizeConstraint(QLayout::SetMinimumSize); + chLayout->setSpacing(0); chLayout->setContentsMargins(0, 0, 0, 0); m_channelAreaWidget->setLayout(chLayout); // create rack layout before creating the first channel m_racksWidget = new QWidget; - m_racksLayout = new QStackedLayout( m_racksWidget ); - m_racksLayout->setContentsMargins( 0, 0, 0, 0 ); - m_racksWidget->setLayout( m_racksLayout ); + m_racksLayout = new QStackedLayout(m_racksWidget); + m_racksLayout->setContentsMargins(0, 0, 0, 0); + m_racksWidget->setLayout(m_racksLayout); // add master channel - m_mixerChannelViews.resize( m->numChannels() ); - m_mixerChannelViews[0] = new MixerChannelView( this, this, 0 ); + m_mixerChannelViews.resize(m->numChannels()); + m_mixerChannelViews[0] = new MixerChannelView(this, this, 0); - m_racksLayout->addWidget( m_mixerChannelViews[0]->m_rackView ); + m_racksLayout->addWidget(m_mixerChannelViews[0]->m_effectRackView); MixerChannelView * masterView = m_mixerChannelViews[0]; - ml->addWidget( masterView->m_mixerLine, 0, Qt::AlignTop ); + ml->addWidget(masterView, 0, Qt::AlignTop); - QSize mixerLineSize = masterView->m_mixerLine->size(); + auto mixerChannelSize = masterView->sizeHint(); // add mixer channels - for( int i = 1; i < m_mixerChannelViews.size(); ++i ) + for (int i = 1; i < m_mixerChannelViews.size(); ++i) { m_mixerChannelViews[i] = new MixerChannelView(m_channelAreaWidget, this, i); - chLayout->addWidget( m_mixerChannelViews[i]->m_mixerLine ); + chLayout->addWidget(m_mixerChannelViews[i]); } // add the scrolling section to the main layout @@ -123,68 +123,63 @@ MixerView::MixerView() : class ChannelArea : public QScrollArea { public: - ChannelArea( QWidget * parent, MixerView * mv ) : - QScrollArea( parent ), m_mv( mv ) {} + ChannelArea(QWidget* parent, MixerView* mv) : + QScrollArea(parent), m_mv(mv) {} ~ChannelArea() override = default; - void keyPressEvent( QKeyEvent * e ) override + void keyPressEvent(QKeyEvent* e) override { - m_mv->keyPressEvent( e ); + m_mv->keyPressEvent(e); } private: - MixerView * m_mv; + MixerView* m_mv; }; - channelArea = new ChannelArea( this, this ); - channelArea->setWidget( m_channelAreaWidget ); - channelArea->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); - channelArea->setFrameStyle( QFrame::NoFrame ); - channelArea->setMinimumWidth( mixerLineSize.width() * 6 ); - channelArea->setFixedHeight( mixerLineSize.height() + - style()->pixelMetric( QStyle::PM_ScrollBarExtent ) ); - ml->addWidget( channelArea, 1, Qt::AlignTop ); + channelArea = new ChannelArea(this, this); + channelArea->setWidget(m_channelAreaWidget); + channelArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + channelArea->setFrameStyle(QFrame::NoFrame); + channelArea->setMinimumWidth(mixerChannelSize.width() * 6); + + int const scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent); + channelArea->setFixedHeight(mixerChannelSize.height() + scrollBarExtent); + + ml->addWidget(channelArea, 1, Qt::AlignTop); // show the add new mixer channel button auto newChannelBtn = new QPushButton(embed::getIconPixmap("new_channel"), QString(), this); - newChannelBtn->setObjectName( "newChannelBtn" ); - newChannelBtn->setFixedSize( mixerLineSize ); - connect( newChannelBtn, SIGNAL(clicked()), this, SLOT(addNewChannel())); - ml->addWidget( newChannelBtn, 0, Qt::AlignTop ); + newChannelBtn->setObjectName("newChannelBtn"); + newChannelBtn->setFixedSize(mixerChannelSize); + connect(newChannelBtn, SIGNAL(clicked()), this, SLOT(addNewChannel())); + ml->addWidget(newChannelBtn, 0, Qt::AlignTop); // add the stacked layout for the effect racks of mixer channels - ml->addWidget( m_racksWidget, 0, Qt::AlignTop | Qt::AlignRight ); + m_racksWidget->setFixedHeight(mixerChannelSize.height()); + ml->addWidget(m_racksWidget); - setCurrentMixerLine( m_mixerChannelViews[0]->m_mixerLine ); + setCurrentMixerChannel(m_mixerChannelViews[0]); - setLayout( ml ); updateGeometry(); - // timer for updating faders - connect( getGUI()->mainWindow(), SIGNAL(periodicUpdate()), - this, SLOT(updateFaders())); + auto* mainWindow = getGUI()->mainWindow(); + // timer for updating faders + connect(mainWindow, &MainWindow::periodicUpdate, this, &MixerView::updateFaders); // add ourself to workspace - QMdiSubWindow * subWin = getGUI()->mainWindow()->addWindowedWidget( this ); + QMdiSubWindow* subWin = mainWindow->addWindowedWidget(this); Qt::WindowFlags flags = subWin->windowFlags(); flags &= ~Qt::WindowMaximizeButtonHint; - subWin->setWindowFlags( flags ); - layout()->setSizeConstraint( QLayout::SetMinimumSize ); - subWin->layout()->setSizeConstraint( QLayout::SetMinAndMaxSize ); + subWin->setWindowFlags(flags); + layout()->setSizeConstraint(QLayout::SetMinimumSize); + subWin->layout()->setSizeConstraint(QLayout::SetMinAndMaxSize); - parentWidget()->setAttribute( Qt::WA_DeleteOnClose, false ); - parentWidget()->move( 5, 310 ); + parentWidget()->setAttribute(Qt::WA_DeleteOnClose, false); + parentWidget()->move(5, 310); // we want to receive dataChanged-signals in order to update - setModel( m ); + setModel(m); } -MixerView::~MixerView() -{ - for (auto mixerChannelView : m_mixerChannelViews) - { - delete mixerChannelView; - } -} @@ -194,12 +189,11 @@ int MixerView::addNewChannel() Mixer * mix = Engine::mixer(); int newChannelIndex = mix->createChannel(); - m_mixerChannelViews.push_back(new MixerChannelView(m_channelAreaWidget, this, - newChannelIndex)); - chLayout->addWidget( m_mixerChannelViews[newChannelIndex]->m_mixerLine ); - m_racksLayout->addWidget( m_mixerChannelViews[newChannelIndex]->m_rackView ); + m_mixerChannelViews.push_back(new MixerChannelView(m_channelAreaWidget, this, newChannelIndex)); + chLayout->addWidget(m_mixerChannelViews[newChannelIndex]); + m_racksLayout->addWidget(m_mixerChannelViews[newChannelIndex]->m_effectRackView); - updateMixerLine(newChannelIndex); + updateMixerChannel(newChannelIndex); updateMaxChannelSelector(); @@ -210,35 +204,29 @@ int MixerView::addNewChannel() void MixerView::refreshDisplay() { // delete all views and re-add them - for( int i = 1; iremoveWidget(m_mixerChannelViews[i]->m_mixerLine); - m_racksLayout->removeWidget( m_mixerChannelViews[i]->m_rackView ); - delete m_mixerChannelViews[i]->m_fader; - delete m_mixerChannelViews[i]->m_muteBtn; - delete m_mixerChannelViews[i]->m_soloBtn; - delete m_mixerChannelViews[i]->m_mixerLine; - delete m_mixerChannelViews[i]->m_rackView; - delete m_mixerChannelViews[i]; + chLayout->removeWidget(m_mixerChannelViews[i]); + m_racksLayout->removeWidget(m_mixerChannelViews[i]->m_effectRackView); } m_channelAreaWidget->adjustSize(); // re-add the views m_mixerChannelViews.resize(Engine::mixer()->numChannels()); - for( int i = 1; i < m_mixerChannelViews.size(); ++i ) + for (int i = 1; i < m_mixerChannelViews.size(); ++i) { m_mixerChannelViews[i] = new MixerChannelView(m_channelAreaWidget, this, i); - chLayout->addWidget(m_mixerChannelViews[i]->m_mixerLine); - m_racksLayout->addWidget( m_mixerChannelViews[i]->m_rackView ); + chLayout->addWidget(m_mixerChannelViews[i]); + m_racksLayout->addWidget(m_mixerChannelViews[i]->m_effectRackView); } - // set selected mixer line to 0 - setCurrentMixerLine( 0 ); + // set selected mixer channel to 0 + setCurrentMixerChannel(0); // update all mixer lines - for( int i = 0; i < m_mixerChannelViews.size(); ++i ) + for (int i = 0; i < m_mixerChannelViews.size(); ++i) { - updateMixerLine( i ); + updateMixerChannel(i); } updateMaxChannelSelector(); @@ -272,74 +260,21 @@ void MixerView::updateMaxChannelSelector() } -void MixerView::saveSettings( QDomDocument & _doc, QDomElement & _this ) +void MixerView::saveSettings(QDomDocument& doc, QDomElement& domElement) { - MainWindow::saveWidgetState( this, _this ); + MainWindow::saveWidgetState(this, domElement); } -void MixerView::loadSettings( const QDomElement & _this ) +void MixerView::loadSettings(const QDomElement& domElement) { - MainWindow::restoreWidgetState( this, _this ); + MainWindow::restoreWidgetState(this, domElement); } -MixerView::MixerChannelView::MixerChannelView(QWidget * _parent, MixerView * _mv, - int channelIndex ) -{ - m_mixerLine = new MixerLine(_parent, _mv, channelIndex); - MixerChannel *mixerChannel = Engine::mixer()->mixerChannel(channelIndex); - - m_fader = new Fader( &mixerChannel->m_volumeModel, - tr( "Fader %1" ).arg( channelIndex ), m_mixerLine ); - m_fader->setLevelsDisplayedInDBFS(); - m_fader->setMinPeak(dbfsToAmp(-42)); - m_fader->setMaxPeak(dbfsToAmp(9)); - - m_fader->move( 16-m_fader->width()/2, - m_mixerLine->height()- - m_fader->height()-5 ); - - m_muteBtn = new PixmapButton( m_mixerLine, tr( "Mute" ) ); - m_muteBtn->setModel( &mixerChannel->m_muteModel ); - m_muteBtn->setActiveGraphic( - embed::getIconPixmap( "led_off" ) ); - m_muteBtn->setInactiveGraphic( - embed::getIconPixmap( "led_green" ) ); - m_muteBtn->setCheckable( true ); - m_muteBtn->move( 9, m_fader->y()-11); - m_muteBtn->setToolTip(tr("Mute this channel")); - - m_soloBtn = new PixmapButton( m_mixerLine, tr( "Solo" ) ); - m_soloBtn->setModel( &mixerChannel->m_soloModel ); - m_soloBtn->setActiveGraphic( - embed::getIconPixmap( "led_red" ) ); - m_soloBtn->setInactiveGraphic( - embed::getIconPixmap( "led_off" ) ); - m_soloBtn->setCheckable( true ); - m_soloBtn->move( 9, m_fader->y()-21); - connect(&mixerChannel->m_soloModel, SIGNAL(dataChanged()), - _mv, SLOT ( toggledSolo() ), Qt::DirectConnection ); - m_soloBtn->setToolTip(tr("Solo this channel")); - - // Create EffectRack for the channel - m_rackView = new EffectRackView( &mixerChannel->m_fxChain, _mv->m_racksWidget ); - m_rackView->setFixedSize( EffectRackView::DEFAULT_WIDTH, MixerLine::MixerLineHeight ); -} - - -void MixerView::MixerChannelView::setChannelIndex( int index ) -{ - MixerChannel* mixerChannel = Engine::mixer()->mixerChannel( index ); - - m_fader->setModel( &mixerChannel->m_volumeModel ); - m_muteBtn->setModel( &mixerChannel->m_muteModel ); - m_soloBtn->setModel( &mixerChannel->m_soloModel ); - m_rackView->setModel( &mixerChannel->m_fxChain ); -} void MixerView::toggledSolo() @@ -349,31 +284,31 @@ void MixerView::toggledSolo() -void MixerView::setCurrentMixerLine( MixerLine * _line ) +void MixerView::setCurrentMixerChannel(MixerChannelView* channel) { // select - m_currentMixerLine = _line; - m_racksLayout->setCurrentWidget( m_mixerChannelViews[ _line->channelIndex() ]->m_rackView ); + m_currentMixerChannel = channel; + m_racksLayout->setCurrentWidget(m_mixerChannelViews[channel->channelIndex()]->m_effectRackView); // set up send knob - for(int i = 0; i < m_mixerChannelViews.size(); ++i) + for (int i = 0; i < m_mixerChannelViews.size(); ++i) { - updateMixerLine(i); + updateMixerChannel(i); } } -void MixerView::updateMixerLine(int index) +void MixerView::updateMixerChannel(int index) { Mixer * mix = Engine::mixer(); // does current channel send to this channel? - int selIndex = m_currentMixerLine->channelIndex(); - MixerLine * thisLine = m_mixerChannelViews[index]->m_mixerLine; - thisLine->setToolTip( Engine::mixer()->mixerChannel( index )->m_name ); + int selIndex = m_currentMixerChannel->channelIndex(); + auto thisLine = m_mixerChannelViews[index]; + thisLine->setToolTip(Engine::mixer()->mixerChannel(index)->m_name); FloatModel * sendModel = mix->channelSendModel(selIndex, index); - if( sendModel == nullptr ) + if (sendModel == nullptr) { // does not send, hide send knob thisLine->m_sendKnob->setVisible(false); @@ -386,8 +321,8 @@ void MixerView::updateMixerLine(int index) } // disable the send button if it would cause an infinite loop - thisLine->m_sendBtn->setVisible(! mix->isInfiniteLoop(selIndex, index)); - thisLine->m_sendBtn->updateLightStatus(); + thisLine->m_sendButton->setVisible(!mix->isInfiniteLoop(selIndex, index)); + thisLine->m_sendButton->updateLightStatus(); thisLine->update(); } @@ -395,7 +330,7 @@ void MixerView::updateMixerLine(int index) void MixerView::deleteChannel(int index) { // can't delete master - if( index == 0 ) return; + if (index == 0) return; // if there is no user confirmation, do nothing if (!confirmRemoval(index)) @@ -404,7 +339,7 @@ void MixerView::deleteChannel(int index) } // remember selected line - int selLine = m_currentMixerLine->channelIndex(); + int selLine = m_currentMixerChannel->channelIndex(); // in case the deleted channel is soloed or the remaining // channels will be left in a muted state @@ -413,23 +348,17 @@ void MixerView::deleteChannel(int index) // delete the real channel Engine::mixer()->deleteChannel(index); - // delete the view - chLayout->removeWidget(m_mixerChannelViews[index]->m_mixerLine); - m_racksLayout->removeWidget(m_mixerChannelViews[index]->m_rackView); - delete m_mixerChannelViews[index]->m_fader; - delete m_mixerChannelViews[index]->m_muteBtn; - delete m_mixerChannelViews[index]->m_soloBtn; - // delete mixerLine later to prevent a crash when deleting from context menu - m_mixerChannelViews[index]->m_mixerLine->hide(); - m_mixerChannelViews[index]->m_mixerLine->deleteLater(); - delete m_mixerChannelViews[index]->m_rackView; - delete m_mixerChannelViews[index]; + chLayout->removeWidget(m_mixerChannelViews[index]); + m_racksLayout->removeWidget(m_mixerChannelViews[index]); + // delete MixerChannelView later to prevent a crash when deleting from context menu + m_mixerChannelViews[index]->hide(); + m_mixerChannelViews[index]->deleteLater(); m_channelAreaWidget->adjustSize(); // make sure every channel knows what index it is for (int i = index + 1; i < m_mixerChannelViews.size(); ++i) { - m_mixerChannelViews[i]->m_mixerLine->setChannelIndex(i - 1); + m_mixerChannelViews[i]->setChannelIndex(i - 1); } m_mixerChannelViews.remove(index); @@ -438,7 +367,7 @@ void MixerView::deleteChannel(int index) { selLine = m_mixerChannelViews.size() - 1; } - setCurrentMixerLine(selLine); + setCurrentMixerChannel(selLine); updateMaxChannelSelector(); } @@ -503,39 +432,39 @@ void MixerView::deleteUnusedChannels() void MixerView::moveChannelLeft(int index, int focusIndex) { // can't move master or first channel left or last channel right - if( index <= 1 || index >= m_mixerChannelViews.size() ) return; + if (index <= 1 || index >= m_mixerChannelViews.size()) return; Mixer *m = Engine::mixer(); // Move instruments channels - m->moveChannelLeft( index ); + m->moveChannelLeft(index); // Update widgets models - m_mixerChannelViews[index]->setChannelIndex( index ); - m_mixerChannelViews[index - 1]->setChannelIndex( index - 1 ); + m_mixerChannelViews[index]->setChannelIndex(index); + m_mixerChannelViews[index - 1]->setChannelIndex(index - 1); // Focus on new position - setCurrentMixerLine( focusIndex ); + setCurrentMixerChannel(focusIndex); } void MixerView::moveChannelLeft(int index) { - moveChannelLeft( index, index - 1 ); + moveChannelLeft(index, index - 1); } void MixerView::moveChannelRight(int index) { - moveChannelLeft( index + 1, index + 1 ); + moveChannelLeft(index + 1, index + 1); } void MixerView::renameChannel(int index) { - m_mixerChannelViews[index]->m_mixerLine->renameChannel(); + m_mixerChannelViews[index]->renameChannel(); } @@ -545,32 +474,32 @@ void MixerView::keyPressEvent(QKeyEvent * e) switch(e->key()) { case Qt::Key_Delete: - deleteChannel(m_currentMixerLine->channelIndex()); + deleteChannel(m_currentMixerChannel->channelIndex()); break; case Qt::Key_Left: - if( e->modifiers() & Qt::AltModifier ) + if (e->modifiers() & Qt::AltModifier) { - moveChannelLeft( m_currentMixerLine->channelIndex() ); + moveChannelLeft(m_currentMixerChannel->channelIndex()); } else { // select channel to the left - setCurrentMixerLine( m_currentMixerLine->channelIndex()-1 ); + setCurrentMixerChannel(m_currentMixerChannel->channelIndex() - 1); } break; case Qt::Key_Right: - if( e->modifiers() & Qt::AltModifier ) + if (e->modifiers() & Qt::AltModifier) { - moveChannelRight( m_currentMixerLine->channelIndex() ); + moveChannelRight(m_currentMixerChannel->channelIndex()); } else { // select channel to the right - setCurrentMixerLine( m_currentMixerLine->channelIndex()+1 ); + setCurrentMixerChannel(m_currentMixerChannel->channelIndex() + 1); } break; case Qt::Key_Insert: - if ( e->modifiers() & Qt::ShiftModifier ) + if (e->modifiers() & Qt::ShiftModifier) { addNewChannel(); } @@ -578,16 +507,16 @@ void MixerView::keyPressEvent(QKeyEvent * e) case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_F2: - renameChannel( m_currentMixerLine->channelIndex() ); + renameChannel(m_currentMixerChannel->channelIndex()); break; } } -void MixerView::closeEvent( QCloseEvent * _ce ) +void MixerView::closeEvent(QCloseEvent * ce) { - if( parentWidget() ) + if (parentWidget()) { parentWidget()->hide(); } @@ -595,16 +524,16 @@ void MixerView::closeEvent( QCloseEvent * _ce ) { hide(); } - _ce->ignore(); + ce->ignore(); } -void MixerView::setCurrentMixerLine( int _line ) +void MixerView::setCurrentMixerChannel(int channel) { - if( _line >= 0 && _line < m_mixerChannelViews.size() ) + if (channel >= 0 && channel < m_mixerChannelViews.size()) { - setCurrentMixerLine( m_mixerChannelViews[_line]->m_mixerLine ); + setCurrentMixerChannel(m_mixerChannelViews[channel]); } } @@ -628,31 +557,31 @@ void MixerView::updateFaders() m->mixerChannel(0)->m_peakLeft *= Engine::audioEngine()->masterGain(); m->mixerChannel(0)->m_peakRight *= Engine::audioEngine()->masterGain(); - for( int i = 0; i < m_mixerChannelViews.size(); ++i ) + for (int i = 0; i < m_mixerChannelViews.size(); ++i) { const float opl = m_mixerChannelViews[i]->m_fader->getPeak_L(); const float opr = m_mixerChannelViews[i]->m_fader->getPeak_R(); const float fallOff = 1.25; - if( m->mixerChannel(i)->m_peakLeft >= opl/fallOff ) + if (m->mixerChannel(i)->m_peakLeft >= opl/fallOff) { - m_mixerChannelViews[i]->m_fader->setPeak_L( m->mixerChannel(i)->m_peakLeft ); + m_mixerChannelViews[i]->m_fader->setPeak_L(m->mixerChannel(i)->m_peakLeft); // Set to -1 so later we'll know if this value has been refreshed yet. m->mixerChannel(i)->m_peakLeft = -1; } - else if( m->mixerChannel(i)->m_peakLeft != -1 ) + else if (m->mixerChannel(i)->m_peakLeft != -1) { - m_mixerChannelViews[i]->m_fader->setPeak_L( opl/fallOff ); + m_mixerChannelViews[i]->m_fader->setPeak_L(opl/fallOff); } - if( m->mixerChannel(i)->m_peakRight >= opr/fallOff ) + if (m->mixerChannel(i)->m_peakRight >= opr/fallOff) { - m_mixerChannelViews[i]->m_fader->setPeak_R( m->mixerChannel(i)->m_peakRight ); + m_mixerChannelViews[i]->m_fader->setPeak_R(m->mixerChannel(i)->m_peakRight); // Set to -1 so later we'll know if this value has been refreshed yet. m->mixerChannel(i)->m_peakRight = -1; } - else if( m->mixerChannel(i)->m_peakRight != -1 ) + else if (m->mixerChannel(i)->m_peakRight != -1) { - m_mixerChannelViews[i]->m_fader->setPeak_R( opr/fallOff ); + m_mixerChannelViews[i]->m_fader->setPeak_R(opr/fallOff); } } } diff --git a/src/gui/SampleTrackWindow.cpp b/src/gui/SampleTrackWindow.cpp index f6d7f9ea1..c0dd8e04e 100644 --- a/src/gui/SampleTrackWindow.cpp +++ b/src/gui/SampleTrackWindow.cpp @@ -21,7 +21,7 @@ * Boston, MA 02110-1301 USA. * */ - + #include "SampleTrackWindow.h" #include @@ -37,7 +37,7 @@ #include "GuiApplication.h" #include "Knob.h" #include "MainWindow.h" -#include "MixerLineLcdSpinBox.h" +#include "MixerChannelLcdSpinBox.h" #include "SampleTrackView.h" #include "Song.h" #include "SubWindow.h" @@ -133,7 +133,7 @@ SampleTrackWindow::SampleTrackWindow(SampleTrackView * tv) : // setup spinbox for selecting Mixer-channel - m_mixerChannelNumber = new MixerLineLcdSpinBox(2, nullptr, tr("Mixer channel"), m_stv); + m_mixerChannelNumber = new MixerChannelLcdSpinBox(2, nullptr, tr("Mixer channel"), m_stv); basicControlsLayout->addWidget(m_mixerChannelNumber, 0, 3); basicControlsLayout->setAlignment(m_mixerChannelNumber, widgetAlignment); diff --git a/src/gui/SendButtonIndicator.cpp b/src/gui/SendButtonIndicator.cpp index d6f8a8327..4fb20cc31 100644 --- a/src/gui/SendButtonIndicator.cpp +++ b/src/gui/SendButtonIndicator.cpp @@ -2,51 +2,48 @@ #include "embed.h" #include "Mixer.h" -#include "MixerLine.h" +#include "MixerChannelView.h" #include "MixerView.h" namespace lmms::gui { -SendButtonIndicator:: SendButtonIndicator( QWidget * _parent, MixerLine * _owner, - MixerView * _mv) : - QLabel( _parent ), - m_parent( _owner ), - m_mv( _mv ) +SendButtonIndicator:: SendButtonIndicator(QWidget* parent, MixerChannelView* owner, MixerView* mv) : + QLabel(parent), + m_parent(owner), + m_mv(mv) { - - // don't do any initializing yet, because the MixerView and MixerLine + // don't do any initializing yet, because the MixerView and MixerChannelView // that were passed to this constructor are not done with their constructors // yet. setPixmap(m_qpmOff); } -void SendButtonIndicator::mousePressEvent( QMouseEvent * e ) +void SendButtonIndicator::mousePressEvent(QMouseEvent* e) { - Mixer * mix = Engine::mixer(); - int from = m_mv->currentMixerLine()->channelIndex(); + Mixer* mix = Engine::mixer(); + int from = m_mv->currentMixerChannel()->channelIndex(); int to = m_parent->channelIndex(); - FloatModel * sendModel = mix->channelSendModel(from, to); - if( sendModel == nullptr ) + FloatModel* sendModel = mix->channelSendModel(from, to); + if (sendModel == nullptr) { // not sending. create a mixer send. - mix->createChannelSend( from, to ); + mix->createChannelSend(from, to); } else { // sending. delete the mixer send. - mix->deleteChannelSend( from, to ); + mix->deleteChannelSend(from, to); } - m_mv->updateMixerLine(m_parent->channelIndex()); + m_mv->updateMixerChannel(m_parent->channelIndex()); updateLightStatus(); } -FloatModel * SendButtonIndicator::getSendModel() +FloatModel* SendButtonIndicator::getSendModel() { - Mixer * mix = Engine::mixer(); - return mix->channelSendModel( - m_mv->currentMixerLine()->channelIndex(), m_parent->channelIndex()); + Mixer* mix = Engine::mixer(); + return mix->channelSendModel(m_mv->currentMixerChannel()->channelIndex(), m_parent->channelIndex()); } void SendButtonIndicator::updateLightStatus() diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index 28cd8c6c8..fa9be2a50 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -42,7 +42,7 @@ #include "FileBrowser.h" #include "FileDialog.h" #include "GroupBox.h" -#include "MixerLineLcdSpinBox.h" +#include "MixerChannelLcdSpinBox.h" #include "GuiApplication.h" #include "gui_templates.h" #include "Instrument.h" @@ -206,7 +206,7 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : // setup spinbox for selecting Mixer-channel - m_mixerChannelNumber = new MixerLineLcdSpinBox( 2, nullptr, tr( "Mixer channel" ), m_itv ); + m_mixerChannelNumber = new MixerChannelLcdSpinBox(2, nullptr, tr("Mixer channel"), m_itv); basicControlsLayout->addWidget( m_mixerChannelNumber, 0, 6 ); basicControlsLayout->setAlignment( m_mixerChannelNumber, widgetAlignment ); diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 8087af423..12b6227ca 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -74,7 +74,7 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV connect(ConfigManager::inst(), SIGNAL(valueChanged(QString,QString,QString)), this, SLOT(handleConfigChange(QString,QString,QString))); - m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber = new MixerChannelLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); m_mixerChannelNumber->show(); m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), @@ -240,7 +240,7 @@ void InstrumentTrackView::assignMixerLine(int channelIndex) { model()->mixerChannelModel()->setValue( channelIndex ); - getGUI()->mixerView()->setCurrentMixerLine( channelIndex ); + getGUI()->mixerView()->setCurrentMixerChannel(channelIndex); } diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index ddb68ee99..45a695d11 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -21,7 +21,7 @@ * Boston, MA 02110-1301 USA. * */ - + #include "SampleTrackView.h" #include @@ -58,7 +58,7 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : m_tlb->setIcon(embed::getIconPixmap("sample_track")); m_tlb->show(); - m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber = new MixerChannelLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); m_mixerChannelNumber->show(); m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), @@ -241,7 +241,7 @@ void SampleTrackView::assignMixerLine(int channelIndex) { model()->mixerChannelModel()->setValue(channelIndex); - getGUI()->mixerView()->setCurrentMixerLine(channelIndex); + getGUI()->mixerView()->setCurrentMixerChannel(channelIndex); } diff --git a/src/gui/widgets/MixerLineLcdSpinBox.cpp b/src/gui/widgets/MixerChannelLcdSpinBox.cpp similarity index 82% rename from src/gui/widgets/MixerLineLcdSpinBox.cpp rename to src/gui/widgets/MixerChannelLcdSpinBox.cpp index 06eb823c0..8a67394de 100644 --- a/src/gui/widgets/MixerLineLcdSpinBox.cpp +++ b/src/gui/widgets/MixerChannelLcdSpinBox.cpp @@ -1,5 +1,5 @@ /* - * MixerLineLcdSpinBox.cpp - a specialization of LcdSpnBox for setting mixer channels + * MixerChannelLcdSpinBox.cpp - a specialization of LcdSpnBox for setting mixer channels * * Copyright (c) 2004-2014 Tobias Doerffel * @@ -22,7 +22,7 @@ * */ -#include "MixerLineLcdSpinBox.h" +#include "MixerChannelLcdSpinBox.h" #include "CaptionMenu.h" #include "MixerView.h" @@ -33,14 +33,14 @@ namespace lmms::gui { -void MixerLineLcdSpinBox::setTrackView(TrackView * tv) +void MixerChannelLcdSpinBox::setTrackView(TrackView * tv) { m_tv = tv; } -void MixerLineLcdSpinBox::mouseDoubleClickEvent(QMouseEvent* event) +void MixerChannelLcdSpinBox::mouseDoubleClickEvent(QMouseEvent* event) { - getGUI()->mixerView()->setCurrentMixerLine(model()->value()); + getGUI()->mixerView()->setCurrentMixerChannel(model()->value()); getGUI()->mixerView()->parentWidget()->show(); getGUI()->mixerView()->show();// show Mixer window @@ -48,7 +48,7 @@ void MixerLineLcdSpinBox::mouseDoubleClickEvent(QMouseEvent* event) //engine::getMixerView()->raise(); } -void MixerLineLcdSpinBox::contextMenuEvent(QContextMenuEvent* event) +void MixerChannelLcdSpinBox::contextMenuEvent(QContextMenuEvent* event) { // for the case, the user clicked right while pressing left mouse- // button, the context-menu appears while mouse-cursor is still hidden From bf4e57da19abf617afcf80e85a7a310e2b72848c Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 31 Dec 2023 11:49:15 +0100 Subject: [PATCH 057/191] Fix minimum size of LADSPA dialogs (#6982) (#7019) Remove the code which computes a minimum height for the LADSPA dialogs. It was intended to make sure that no scrollbar is shown in most cases. However, doing so came at the cost that the computed height was the minimum height as well. Therefore the dialogs took a lot of space on low-res displays and could not be made smaller. After the removal the behavior is still sane. Small dialogs are shown in full and dialogs which are larger, e.g. "Calf Equalizer 12 Band LADSPA", seem to be sized around half the height of the workspace and show scrollbars. --- .../LadspaEffect/LadspaMatrixControlDialog.cpp | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp index 88810cee6..4c9cd50ac 100644 --- a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp @@ -41,9 +41,6 @@ #include "LadspaControlView.h" #include "LedCheckBox.h" -#include "GuiApplication.h" -#include "MainWindow.h" - namespace lmms::gui { @@ -206,20 +203,6 @@ void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls // From: https://forum.qt.io/topic/13374/solved-qscrollarea-vertical-scroll-only/4 m_scrollArea->setMinimumWidth(matrixWidget->minimumSizeHint().width() + m_scrollArea->verticalScrollBar()->width()); - - // Make sure that the widget is shown without a scrollbar whenever possible - // If the widget fits on the workspace we use the height of the widget as the minimum size of the scroll area. - // This will ensure that the scrollbar is not shown initially (and never will be). - // If the widget is larger than the workspace then we want it to mostly cover the workspace. - // - // This is somewhat ugly but I have no idea how to control the initial size of the scroll area otherwise - auto const workspaceSize = getGUI()->mainWindow()->workspace()->viewport()->size(); - // Make sure that we always account a minumum height for the workspace, i.e. that we never compute - // something close to 0 if the LMMS window is very small - int workspaceHeight = qMax(200, static_cast(workspaceSize.height() * 0.9)); - int minOfWidgetAndWorkspace = qMin(matrixWidget->minimumSizeHint().height(), workspaceHeight); - m_scrollArea->setMinimumHeight(minOfWidgetAndWorkspace); - if (getChannelCount() > 1 && m_stereoLink != nullptr) { m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); From 36cb0ed7ca13208e96fb640d47866b48889573d3 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 31 Dec 2023 11:55:59 +0100 Subject: [PATCH 058/191] Extend TabWidget's style sheet options # Extend TabWidget's style sheet options Extend the `TabWidget` class so that the text color of the selected tab can be set in the style sheet. Adjust the paint method to make use of the new property. Adjust the default style sheet as follows: * Background color of the selected tab is the green of the knobs * Text color of the selected tab is full on white Adjust the classic style sheet in such a way that nothing changes, i.e. the text colors of the selected tab and the other ones are the same. # Code review style changes Completely adjust the code style of TabWidget: * Pointer/reference close to type * Remove underscores from parameter names * Remove spaces from parentheses * Add space after if and for statements # Remove repeated iterator dereferences Remove repeated iterator dereferences by introducing variables with speaking names. Fixes #6730. --- data/themes/classic/style.css | 1 + data/themes/default/style.css | 3 +- include/TabWidget.h | 50 ++++----- src/gui/widgets/TabWidget.cpp | 184 +++++++++++++++++++--------------- 4 files changed, 133 insertions(+), 105 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 3ef397651..f61d4ba58 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -189,6 +189,7 @@ lmms--gui--TabWidget { qproperty-tabText: rgba(255, 255, 255, 180); qproperty-tabTitleText: #fff; qproperty-tabSelected: #61666b; + qproperty-tabTextSelected: rgba(255, 255, 255, 180); qproperty-tabBackground: #3c434b; qproperty-tabBorder: #3c434b; } diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 3a8f411de..e1f0cf395 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -219,7 +219,8 @@ lmms--gui--TabWidget { background-color: #262b30; qproperty-tabText: rgba(255, 255, 255, 180); qproperty-tabTitleText: #fff; - qproperty-tabSelected: #323940; + qproperty-tabSelected: #1c4933; + qproperty-tabTextSelected: rgba(255, 255, 255, 255); qproperty-tabBackground: #181b1f; qproperty-tabBorder: #181b1f; } diff --git a/include/TabWidget.h b/include/TabWidget.h index d28537df8..52e59d577 100644 --- a/include/TabWidget.h +++ b/include/TabWidget.h @@ -40,53 +40,56 @@ class TabWidget : public QWidget public: //! @param resizable If true, the widget resizes to fit the size of all tabs //! If false, all child widget will be cut down to the TabWidget's size - TabWidget( const QString & _caption, QWidget * _parent, - bool usePixmap = false, bool resizable = false ); + TabWidget(const QString& caption, QWidget* parent, + bool usePixmap = false, bool resizable = false); ~TabWidget() override = default; - void addTab( QWidget * w, const QString & name, const char *pixmap = nullptr, int idx = -1 ); + void addTab(QWidget* w, const QString& name, const char* pixmap = nullptr, int idx = -1); - void setActiveTab( int idx ); + void setActiveTab(int idx); - int findTabAtPos( const QPoint *pos ); + int findTabAtPos(const QPoint* pos); inline int activeTab() const { - return( m_activeTab ); + return(m_activeTab); } // Themeability - Q_PROPERTY( QColor tabText READ tabText WRITE setTabText) - Q_PROPERTY( QColor tabTitleText READ tabTitleText WRITE setTabTitleText) - Q_PROPERTY( QColor tabSelected READ tabSelected WRITE setTabSelected) - Q_PROPERTY( QColor tabBackground READ tabBackground WRITE setTabBackground) - Q_PROPERTY( QColor tabBorder READ tabBorder WRITE setTabBorder) + Q_PROPERTY(QColor tabText READ tabText WRITE setTabText) + Q_PROPERTY(QColor tabTitleText READ tabTitleText WRITE setTabTitleText) + Q_PROPERTY(QColor tabSelected READ tabSelected WRITE setTabSelected) + Q_PROPERTY(QColor tabTextSelected READ tabTextSelected WRITE setTabTextSelected) + Q_PROPERTY(QColor tabBackground READ tabBackground WRITE setTabBackground) + Q_PROPERTY(QColor tabBorder READ tabBorder WRITE setTabBorder) QColor tabText() const; - void setTabText( const QColor & c ); + void setTabText(const QColor & c); QColor tabTitleText() const; - void setTabTitleText( const QColor & c ); + void setTabTitleText(const QColor & c); QColor tabSelected() const; - void setTabSelected( const QColor & c ); + void setTabSelected(const QColor & c); + QColor tabTextSelected() const; + void setTabTextSelected(const QColor & c); QColor tabBackground() const; - void setTabBackground( const QColor & c ); + void setTabBackground(const QColor & c); QColor tabBorder() const; - void setTabBorder( const QColor & c ); + void setTabBorder(const QColor & c); protected: - bool event( QEvent * event ) override; - void mousePressEvent( QMouseEvent * _me ) override; - void paintEvent( QPaintEvent * _pe ) override; - void resizeEvent( QResizeEvent * _re ) override; - void wheelEvent( QWheelEvent * _we ) override; + bool event(QEvent* event) override; + void mousePressEvent(QMouseEvent* me) override; + void paintEvent(QPaintEvent* pe) override; + void resizeEvent(QResizeEvent* re) override; + void wheelEvent(QWheelEvent* we) override; QSize minimumSizeHint() const override; QSize sizeHint() const override; private: struct widgetDesc { - QWidget * w; // ptr to widget - const char * pixmap; // artwork for the widget + QWidget* w; // ptr to widget + const char* pixmap; // artwork for the widget QString name; // name for widget int nwidth; // width of name when painting (only valid for text tab) } ; @@ -104,6 +107,7 @@ private: QColor m_tabText; // The color of the tabs' text. QColor m_tabTitleText; // The color of the TabWidget's title text. QColor m_tabSelected; // The highlighting color for the selected tab. + QColor m_tabTextSelected;// The text color for the selected tab. QColor m_tabBackground; // The TabWidget's background color. QColor m_tabBorder; // The TabWidget's borders color. } ; diff --git a/src/gui/widgets/TabWidget.cpp b/src/gui/widgets/TabWidget.cpp index 5ab56fee7..27671933e 100644 --- a/src/gui/widgets/TabWidget.cpp +++ b/src/gui/widgets/TabWidget.cpp @@ -38,18 +38,19 @@ namespace lmms::gui { -TabWidget::TabWidget(const QString & caption, QWidget * parent, bool usePixmap, +TabWidget::TabWidget(const QString& caption, QWidget* parent, bool usePixmap, bool resizable) : - QWidget( parent ), - m_resizable( resizable ), - m_activeTab( 0 ), - m_caption( caption ), - m_usePixmap( usePixmap ), - m_tabText( 0, 0, 0 ), - m_tabTitleText( 0, 0, 0 ), - m_tabSelected( 0, 0, 0 ), - m_tabBackground( 0, 0, 0 ), - m_tabBorder( 0, 0, 0 ) + QWidget(parent), + m_resizable(resizable), + m_activeTab(0), + m_caption(caption), + m_usePixmap(usePixmap), + m_tabText(0, 0, 0), + m_tabTitleText(0, 0, 0), + m_tabSelected(0, 0, 0), + m_tabTextSelected(0, 0, 0), + m_tabBackground(0, 0, 0), + m_tabBorder(0, 0, 0) { // Create taller tabbar when it's to display artwork tabs @@ -57,24 +58,24 @@ TabWidget::TabWidget(const QString & caption, QWidget * parent, bool usePixmap, m_tabheight = caption.isEmpty() ? m_tabbarHeight - 3 : m_tabbarHeight - 4; - setFont( pointSize<8>( font() ) ); + setFont(pointSize<8>(font())); - setAutoFillBackground( true ); - QColor bg_color = QApplication::palette().color( QPalette::Active, QPalette::Window ). darker( 132 ); + setAutoFillBackground(true); + QColor bg_color = QApplication::palette().color(QPalette::Active, QPalette::Window).darker(132); QPalette pal = palette(); - pal.setColor( QPalette::Window, bg_color ); - setPalette( pal ); + pal.setColor(QPalette::Window, bg_color); + setPalette(pal); } -void TabWidget::addTab( QWidget * w, const QString & name, const char *pixmap, int idx ) +void TabWidget::addTab(QWidget* w, const QString& name, const char* pixmap, int idx) { - setFont( pointSize<8>( font() ) ); + setFont(pointSize<8>(font())); // Append tab when position is not given - if( idx < 0/* || m_widgets.contains( idx ) == true*/ ) + if (idx < 0/* || m_widgets.contains(idx) == true*/) { - while( m_widgets.contains( ++idx ) == true ) + while(m_widgets.contains(++idx) == true) { } } @@ -83,19 +84,19 @@ void TabWidget::addTab( QWidget * w, const QString & name, const char *pixmap, i int tab_width = horizontalAdvance(fontMetrics(), name) + 10; // Register new tab - widgetDesc d = { w, pixmap, name, tab_width }; + widgetDesc d = {w, pixmap, name, tab_width}; m_widgets[idx] = d; // Position tab's window if (!m_resizable) { - w->setFixedSize( width() - 4, height() - m_tabbarHeight ); + w->setFixedSize(width() - 4, height() - m_tabbarHeight); } - w->move( 2, m_tabbarHeight - 1 ); + w->move(2, m_tabbarHeight - 1); w->hide(); // Show tab's window if it's active - if( m_widgets.contains( m_activeTab ) ) + if (m_widgets.contains(m_activeTab)) { // make sure new tab doesn't overlap current widget m_widgets[m_activeTab].w->show(); @@ -106,15 +107,15 @@ void TabWidget::addTab( QWidget * w, const QString & name, const char *pixmap, i -void TabWidget::setActiveTab( int idx ) +void TabWidget::setActiveTab(int idx) { - if( m_widgets.contains( idx ) ) + if (m_widgets.contains(idx)) { int old_active = m_activeTab; m_activeTab = idx; m_widgets[m_activeTab].w->raise(); m_widgets[m_activeTab].w->show(); - if( old_active != idx && m_widgets.contains( old_active ) ) + if (old_active != idx && m_widgets.contains(old_active)) { m_widgets[old_active].w->hide(); } @@ -124,42 +125,44 @@ void TabWidget::setActiveTab( int idx ) // Return the index of the tab at position "pos" -int TabWidget::findTabAtPos( const QPoint *pos ) +int TabWidget::findTabAtPos(const QPoint* pos) { - if( pos->y() > 1 && pos->y() < m_tabbarHeight - 1 ) + if (pos->y() > 1 && pos->y() < m_tabbarHeight - 1) { int cx = ((m_caption == "") ? 4 : 14) + horizontalAdvance(fontMetrics(), m_caption); - for( widgetStack::iterator it = m_widgets.begin(); it != m_widgets.end(); ++it ) + for (widgetStack::iterator it = m_widgets.begin(); it != m_widgets.end(); ++it) { - if( pos->x() >= cx && pos->x() <= cx + ( *it ).nwidth ) + int const currentWidgetWidth = it->nwidth; + + if (pos->x() >= cx && pos->x() <= cx + currentWidgetWidth) { - return( it.key() ); + return(it.key()); } - cx += ( *it ).nwidth; + cx += currentWidgetWidth; } } // Haven't found any tab at position "pos" - return( -1 ); + return(-1); } // Overload the QWidget::event handler to display tooltips (from https://doc.qt.io/qt-4.8/qt-widgets-tooltips-example.html) -bool TabWidget::event(QEvent *event) +bool TabWidget::event(QEvent* event) { - if ( event->type() == QEvent::ToolTip ) + if (event->type() == QEvent::ToolTip) { auto helpEvent = static_cast(event); - int idx = findTabAtPos( & helpEvent->pos() ); + int idx = findTabAtPos(& helpEvent->pos()); - if ( idx != -1 ) + if (idx != -1) { // Display tab's tooltip - QToolTip::showText( helpEvent->globalPos(), m_widgets[idx].name ); + QToolTip::showText(helpEvent->globalPos(), m_widgets[idx].name); } else { @@ -177,17 +180,17 @@ bool TabWidget::event(QEvent *event) // Activate tab when clicked -void TabWidget::mousePressEvent( QMouseEvent * me ) +void TabWidget::mousePressEvent(QMouseEvent* me) { // Find index of tab that has been clicked QPoint pos = me->pos(); - int idx = findTabAtPos( &pos ); + int idx = findTabAtPos(&pos); // When found, activate tab that has been clicked - if ( idx != -1 ) + if (idx != -1) { - setActiveTab( idx ); + setActiveTab(idx); update(); return; } @@ -196,7 +199,7 @@ void TabWidget::mousePressEvent( QMouseEvent * me ) -void TabWidget::resizeEvent( QResizeEvent * ) +void TabWidget::resizeEvent(QResizeEvent*) { if (!m_resizable) { @@ -210,28 +213,28 @@ void TabWidget::resizeEvent( QResizeEvent * ) -void TabWidget::paintEvent( QPaintEvent * pe ) +void TabWidget::paintEvent(QPaintEvent* pe) { - QPainter p( this ); - p.setFont( pointSize<7>( font() ) ); + QPainter p(this); + p.setFont(pointSize<7>(font())); // Draw background QBrush bg_color = p.background(); - p.fillRect( 0, 0, width() - 1, height() - 1, bg_color ); + p.fillRect(0, 0, width() - 1, height() - 1, bg_color); // Draw external borders - p.setPen( tabBorder() ); - p.drawRect( 0, 0, width() - 1, height() - 1 ); + p.setPen(tabBorder()); + p.drawRect(0, 0, width() - 1, height() - 1); // Draw tabs' bar background - p.fillRect( 1, 1, width() - 2, m_tabheight + 2, tabBackground() ); + p.fillRect(1, 1, width() - 2, m_tabheight + 2, tabBackground()); // Draw title, if any - if( ! m_caption.isEmpty() ) + if (!m_caption.isEmpty()) { - p.setFont( pointSize<8>( p.font() ) ); - p.setPen( tabTitleText() ); - p.drawText( 5, 11, m_caption ); + p.setFont(pointSize<8>(p.font())); + p.setPen(tabTitleText()); + p.drawText(5, 11, m_caption); } // Calculate the tabs' x (tabs are painted next to the caption) @@ -241,47 +244,54 @@ void TabWidget::paintEvent( QPaintEvent * pe ) widgetStack::iterator first = m_widgets.begin(); widgetStack::iterator last = m_widgets.end(); int tab_width = width(); - if ( first != last ) + if (first != last) { - tab_width = ( width() - tab_x_offset ) / std::distance( first, last ); + tab_width = (width() - tab_x_offset) / std::distance(first, last); } // Draw all tabs - p.setPen( tabText() ); - for( widgetStack::iterator it = first ; it != last ; ++it ) + p.setPen(tabText()); + for (widgetStack::iterator it = first ; it != last ; ++it) { + auto & currentWidgetDesc = *it; + // Draw a text tab or a artwork tab. - if( m_usePixmap ) + if (m_usePixmap) { // Fixes tab's width, because original size is only correct for text tabs - ( *it ).nwidth = tab_width; + currentWidgetDesc.nwidth = tab_width; // Get artwork - QPixmap artwork( embed::getIconPixmap( ( *it ).pixmap ) ); + QPixmap artwork(embed::getIconPixmap(currentWidgetDesc.pixmap)); // Highlight active tab - if( it.key() == m_activeTab ) + if (it.key() == m_activeTab) { - p.fillRect( tab_x_offset, 0, ( *it ).nwidth, m_tabbarHeight - 1, tabSelected() ); + p.fillRect(tab_x_offset, 0, currentWidgetDesc.nwidth, m_tabbarHeight - 1, tabSelected()); } // Draw artwork - p.drawPixmap(tab_x_offset + ( ( *it ).nwidth - artwork.width() ) / 2, 1, artwork ); + p.drawPixmap(tab_x_offset + (currentWidgetDesc.nwidth - artwork.width()) / 2, 1, artwork); } else { // Highlight tab when active - if( it.key() == m_activeTab ) + if (it.key() == m_activeTab) { - p.fillRect( tab_x_offset, 2, ( *it ).nwidth - 6, m_tabbarHeight - 4, tabSelected() ); + p.fillRect(tab_x_offset, 2, currentWidgetDesc.nwidth - 6, m_tabbarHeight - 4, tabSelected()); + p.setPen(tabTextSelected()); + p.drawText(tab_x_offset + 3, m_tabheight + 1, currentWidgetDesc.name); + } + else + { + // Draw text + p.setPen(tabText()); + p.drawText(tab_x_offset + 3, m_tabheight + 1, currentWidgetDesc.name); } - - // Draw text - p.drawText( tab_x_offset + 3, m_tabheight + 1, ( *it ).name ); } // Next tab's horizontal position - tab_x_offset += ( *it ).nwidth; + tab_x_offset += currentWidgetDesc.nwidth; } } @@ -289,9 +299,9 @@ void TabWidget::paintEvent( QPaintEvent * pe ) // Switch between tabs with mouse wheel -void TabWidget::wheelEvent( QWheelEvent * we ) +void TabWidget::wheelEvent(QWheelEvent* we) { - if(position(we).y() > m_tabheight) + if (position(we).y() > m_tabheight) { return; } @@ -299,15 +309,15 @@ void TabWidget::wheelEvent( QWheelEvent * we ) we->accept(); int dir = (we->angleDelta().y() < 0) ? 1 : -1; int tab = m_activeTab; - while( tab > -1 && static_cast( tab ) < m_widgets.count() ) + while(tab > -1 && static_cast(tab) < m_widgets.count()) { tab += dir; - if( m_widgets.contains( tab ) ) + if (m_widgets.contains(tab)) { break; } } - setActiveTab( tab ); + setActiveTab(tab); } @@ -363,7 +373,7 @@ QColor TabWidget::tabTitleText() const } // Set the color to be used to draw a TabWidget's title text (if any) -void TabWidget::setTabTitleText( const QColor & c ) +void TabWidget::setTabTitleText(const QColor& c) { m_tabTitleText = c; } @@ -375,7 +385,7 @@ QColor TabWidget::tabText() const } // Set the color to be used to draw a TabWidget's text (if any) -void TabWidget::setTabText( const QColor & c ) +void TabWidget::setTabText(const QColor& c) { m_tabText = c; } @@ -387,11 +397,23 @@ QColor TabWidget::tabSelected() const } // Set the color to be used to highlight a TabWidget'selected tab (if any) -void TabWidget::setTabSelected( const QColor & c ) +void TabWidget::setTabSelected(const QColor& c) { m_tabSelected = c; } +// Return the text color of the selected tab +QColor TabWidget::tabTextSelected() const +{ + return m_tabTextSelected; +} + +// Set the text color of the selected tab +void TabWidget::setTabTextSelected(const QColor& c) +{ + m_tabTextSelected = c; +} + // Return the color to be used for the TabWidget's background QColor TabWidget::tabBackground() const { @@ -399,7 +421,7 @@ QColor TabWidget::tabBackground() const } // Set the color to be used for the TabWidget's background -void TabWidget::setTabBackground( const QColor & c ) +void TabWidget::setTabBackground(const QColor& c) { m_tabBackground = c; } @@ -411,7 +433,7 @@ QColor TabWidget::tabBorder() const } // Set the color to be used for the TabWidget's borders -void TabWidget::setTabBorder( const QColor & c ) +void TabWidget::setTabBorder(const QColor& c) { m_tabBorder = c; } From 1868fa170a0580191b155f7d0586dd42da40e8fc Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 31 Dec 2023 15:52:24 +0100 Subject: [PATCH 059/191] Fix equalizer peak updates (#7038) Fix the peak update of the LMMS equalizer which accidentally used the same sample twice. Simplify the `gain` method by removing repeated deferences which made the code hard to read. Use `std::max` to update the peak values to the maximum of the buffer. --- plugins/Eq/EqEffect.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/Eq/EqEffect.h b/plugins/Eq/EqEffect.h index 2d54c42a2..9b23b51b5 100644 --- a/plugins/Eq/EqEffect.h +++ b/plugins/Eq/EqEffect.h @@ -28,6 +28,9 @@ #include "EqControls.h" #include "EqFilter.h" +#include + + namespace lmms { @@ -42,23 +45,20 @@ public: { return &m_eqControls; } - inline void gain( sampleFrame * buf, const fpp_t frames, float scale, sampleFrame * peak ) + inline void gain( sampleFrame * buf, const fpp_t frames, float scale, sampleFrame * peak ) { peak[0][0] = 0.0f; peak[0][1] = 0.0f; for( fpp_t f = 0; f < frames; ++f ) { - buf[f][0] *= scale; - buf[f][1] *= scale; + auto & sf = buf[f]; - if( fabs( buf[f][0] ) > peak[0][0] ) - { - peak[0][0] = fabs( buf[f][0] ); - } - if( fabs( buf[f][1] ) > peak[0][1] ) - { - peak[0][1] = fabs( buf[f][0] ); - } + // Apply gain to sample frame + sf[0] *= scale; + sf[1] *= scale; + // Update peaks + peak[0][0] = std::max(peak[0][0], (float)fabs(sf[0])); + peak[0][1] = std::max(peak[0][1], (float)fabs(sf[1])); } } From f79695e4f86b5fdeac335d282604693efe37a02b Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 31 Dec 2023 18:05:46 -0500 Subject: [PATCH 060/191] Fix regressions from #6610 (#7040) Fixes crashes when using decodeSampleOggVorbis, as well as missing supported audio formats --- src/core/SampleDecoder.cpp | 60 ++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/core/SampleDecoder.cpp b/src/core/SampleDecoder.cpp index 853543d85..94f8e387b 100644 --- a/src/core/SampleDecoder.cpp +++ b/src/core/SampleDecoder.cpp @@ -43,7 +43,7 @@ namespace lmms { namespace { -using Decoder = std::optional(*)(const QString&); +using Decoder = std::optional (*)(const QString&); auto decodeSampleSF(const QString& audioFile) -> std::optional; auto decodeSampleDS(const QString& audioFile) -> std::optional; @@ -62,7 +62,7 @@ auto decodeSampleSF(const QString& audioFile) -> std::optional std::optional std::optional { - auto vorbisFile = OggVorbis_File{}; - const auto openError = ov_fopen(audioFile.toLocal8Bit(), &vorbisFile); + static auto s_read = [](void* buffer, size_t size, size_t count, void* stream) -> size_t { + auto file = static_cast(stream); + return file->read(static_cast(buffer), size * count); + }; - if (openError != 0) { return std::nullopt; } + static auto s_seek = [](void* stream, ogg_int64_t offset, int whence) -> int { + auto file = static_cast(stream); + if (whence == SEEK_SET) { file->seek(offset); } + else if (whence == SEEK_CUR) { file->seek(file->pos() + offset); } + else if (whence == SEEK_END) { file->seek(file->size() + offset); } + else { return -1; } + return 0; + }; + + static auto s_close = [](void* stream) -> int { + auto file = static_cast(stream); + file->close(); + return 0; + }; + + static auto s_tell = [](void* stream) -> long { + auto file = static_cast(stream); + return file->pos(); + }; + + static ov_callbacks s_callbacks = {s_read, s_seek, s_close, s_tell}; + + // TODO: Remove use of QFile + auto file = QFile{audioFile}; + if (!file.open(QIODevice::ReadOnly)) { return std::nullopt; } + + auto vorbisFile = OggVorbis_File{}; + if (ov_open_callbacks(&file, &vorbisFile, nullptr, 0, s_callbacks) < 0) { return std::nullopt; } const auto vorbisInfo = ov_info(&vorbisFile, -1); + if (vorbisInfo == nullptr) { return std::nullopt; } + const auto numChannels = vorbisInfo->channels; const auto sampleRate = vorbisInfo->rate; const auto numSamples = ov_pcm_total(&vorbisFile, -1); + if (numSamples < 0) { return std::nullopt; } auto buffer = std::vector(numSamples); auto output = static_cast(nullptr); @@ -141,14 +173,14 @@ auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional(numSamples / numChannels); - for (int i = 0; i < buffer.size(); ++i) + auto result = std::vector(totalSamplesRead / numChannels); + for (int i = 0; i < result.size(); ++i) { if (numChannels == 1) { result[i] = {buffer[i], buffer[i]}; } else if (numChannels > 1) { result[i] = {buffer[i * numChannels], buffer[i * numChannels + 1]}; } } + ov_clear(&vorbisFile); return SampleDecoder::Result{std::move(result), static_cast(sampleRate)}; } #endif // LMMS_HAVE_OGGVORBIS @@ -156,8 +188,7 @@ auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional const std::vector& { - static const auto s_audioTypes = [] - { + static const auto s_audioTypes = [] { auto types = std::vector(); // Add DrumSynth by default since that support comes from us @@ -167,8 +198,8 @@ auto SampleDecoder::supportedAudioTypes() -> const std::vector& auto simpleTypeCount = 0; sf_command(nullptr, SFC_GET_SIMPLE_FORMAT_COUNT, &simpleTypeCount, sizeof(int)); - // TODO: Ideally, this code should be iterating over the major formats, but some important extensions such as *.ogg - // are not included. This is planned for future versions of sndfile. + // TODO: Ideally, this code should be iterating over the major formats, but some important extensions such as + // *.ogg are not included. This is planned for future versions of sndfile. for (int simple = 0; simple < simpleTypeCount; ++simple) { sfFormatInfo.format = simple; @@ -182,12 +213,9 @@ auto SampleDecoder::supportedAudioTypes() -> const std::vector& std::transform(name.begin(), name.end(), name.begin(), [](unsigned char ch) { return std::toupper(ch); }); types.push_back(AudioType{std::move(name), sfFormatInfo.extension}); - - return types; } - std::sort(types.begin(), types.end(), - [&](const AudioType& a, const AudioType& b) { return a.name < b.name; }); + std::sort(types.begin(), types.end(), [&](const AudioType& a, const AudioType& b) { return a.name < b.name; }); return types; }(); return s_audioTypes; From 4049cc29d3fa29a922d1917005a70f52a2e7ea3a Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 31 Dec 2023 21:52:56 -0500 Subject: [PATCH 061/191] Fix `Fader` pixmaps (#7041) --- include/Fader.h | 6 +++--- src/gui/widgets/Fader.cpp | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/include/Fader.h b/include/Fader.h index c44d976a7..20132f71d 100644 --- a/include/Fader.h +++ b/include/Fader.h @@ -152,9 +152,9 @@ private: QElapsedTimer m_lastPeakTimer_L; QElapsedTimer m_lastPeakTimer_R; - QPixmap m_back = embed::getIconPixmap("fader_background"); - QPixmap m_leds = embed::getIconPixmap("fader_leds"); - QPixmap m_knob = embed::getIconPixmap("fader_knob"); + QPixmap m_back; + QPixmap m_leds; + QPixmap m_knob; bool m_levelsDisplayedInDBFS; diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index 9ddbc74e1..6dbd9fbc3 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -70,6 +70,9 @@ Fader::Fader( FloatModel * _model, const QString & _name, QWidget * _parent ) : m_persistentPeak_R( 0.0 ), m_fMinPeak( 0.01f ), m_fMaxPeak( 1.1 ), + m_back(embed::getIconPixmap("fader_background")), + m_leds(embed::getIconPixmap("fader_leds")), + m_knob(embed::getIconPixmap("fader_knob")), m_levelsDisplayedInDBFS(false), m_moveStartPoint( -1 ), m_startValue( 0 ), @@ -97,6 +100,9 @@ Fader::Fader( FloatModel * model, const QString & name, QWidget * parent, QPixma m_persistentPeak_R( 0.0 ), m_fMinPeak( 0.01f ), m_fMaxPeak( 1.1 ), + m_back(*back), + m_leds(*leds), + m_knob(*knob), m_levelsDisplayedInDBFS(false), m_moveStartPoint( -1 ), m_startValue( 0 ), From 8eba258bdad8e940db9ae84f6362d1749d8bb425 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Sun, 31 Dec 2023 19:53:52 -0800 Subject: [PATCH 062/191] Accept Editor close event (#7035) --- src/gui/editors/Editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editors/Editor.cpp b/src/gui/editors/Editor.cpp index 7091c094b..a61c2cd60 100644 --- a/src/gui/editors/Editor.cpp +++ b/src/gui/editors/Editor.cpp @@ -148,7 +148,7 @@ void Editor::closeEvent( QCloseEvent * _ce ) { hide(); } - _ce->ignore(); + _ce->accept(); } DropToolBar::DropToolBar(QWidget* parent) : QToolBar(parent) From c2f2b7e0d762961a19050bcbd636edad34f67c19 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 3 Jan 2024 17:01:28 +0100 Subject: [PATCH 063/191] Handle divisions by 0 in Lb302 and Monstro (#7021) * Handle divisions by 0 in Lb302 Handle potential division by 0 which in turn lead to floating point exceptions in Lb302. The division can occur in the `process` method if it is called with the member `vco_inc` still initialized to 0. This seems to happen then the Lb302 processes with no notes actually being played. The offending call is `BandLimitedWave::pdToLen(vco_inc)` which is now prevented when the increment is 0. In the latter case a sample of 0 is produced. This works because in all other cases the Lb302 instance should process a note which in turn sets the increments to something different from 0. * Fix FPEs in Monstro Fix some floating point exceptions in `MonstroSynth::renderOutput` that occurred due to calling `BandLimitedWave::pdToLen` with a value of 0. This happened when one of the phase deltas was set to 0, i.e. `pd_l` or `pd_r`. These cases are now checked and produce silence in the corresponding components if the phase delta is set to 0. * Fix uninitialized variables Initialize the local variables `len_l` and `len_r` to 0. This fixes compiler warnings a la "error: 'len_r' may be used uninitialized in this function" which are treated as errors on the build server. * Adjust whitespace of touched code --- plugins/Lb302/Lb302.cpp | 11 +++--- plugins/Monstro/Monstro.cpp | 72 ++++++++++++++++++++++++++++--------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index ee49442d5..44583cbc5 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -585,20 +585,23 @@ int Lb302Synth::process(sampleFrame *outbuf, const int size) vco_k = 0.5 * Oscillator::noiseSample( vco_c ); break; + // The next cases all use the BandLimitedWave class which uses the oscillator increment `vco_inc` to compute samples. + // If that oscillator increment is 0 we return a 0 sample because calling BandLimitedWave::pdToLen(0) leads to a + // division by 0 which in turn leads to floating point exceptions. case VcoShape::BLSawtooth: - vco_k = BandLimitedWave::oscillate( vco_c + 0.5f, BandLimitedWave::pdToLen( vco_inc ), BandLimitedWave::Waveform::BLSaw ) * 0.5f; + vco_k = vco_inc == 0. ? 0. : BandLimitedWave::oscillate(vco_c + 0.5f, BandLimitedWave::pdToLen(vco_inc), BandLimitedWave::Waveform::BLSaw) * 0.5f; break; case VcoShape::BLSquare: - vco_k = BandLimitedWave::oscillate( vco_c + 0.5f, BandLimitedWave::pdToLen( vco_inc ), BandLimitedWave::Waveform::BLSquare ) * 0.5f; + vco_k = vco_inc == 0. ? 0. : BandLimitedWave::oscillate(vco_c + 0.5f, BandLimitedWave::pdToLen(vco_inc), BandLimitedWave::Waveform::BLSquare) * 0.5f; break; case VcoShape::BLTriangle: - vco_k = BandLimitedWave::oscillate( vco_c + 0.5f, BandLimitedWave::pdToLen( vco_inc ), BandLimitedWave::Waveform::BLTriangle ) * 0.5f; + vco_k = vco_inc == 0. ? 0. : BandLimitedWave::oscillate(vco_c + 0.5f, BandLimitedWave::pdToLen(vco_inc), BandLimitedWave::Waveform::BLTriangle) * 0.5f; break; case VcoShape::BLMoog: - vco_k = BandLimitedWave::oscillate( vco_c + 0.5f, BandLimitedWave::pdToLen( vco_inc ), BandLimitedWave::Waveform::BLMoog ); + vco_k = vco_inc == 0. ? 0. : BandLimitedWave::oscillate(vco_c + 0.5f, BandLimitedWave::pdToLen(vco_inc), BandLimitedWave::Waveform::BLMoog); break; } diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index 2201e4ed9..9563f756d 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -320,8 +320,8 @@ void MonstroSynth::renderOutput( fpp_t _frames, sampleFrame * _buf ) float rightph; float pd_l; float pd_r; - float len_l; - float len_r; + float len_l(0.); + float len_r(0.); // osc1 vars float o1l_f; @@ -503,12 +503,27 @@ void MonstroSynth::renderOutput( fpp_t _frames, sampleFrame * _buf ) if( pd_r > 0.5 ) pd_r = 1.0 - pd_r; // multi-wave DC Oscillator - len_l = BandLimitedWave::pdToLen( pd_l ); - len_r = BandLimitedWave::pdToLen( pd_r ); - if( m_counter2l > 0 ) { len_l /= m_counter2l; m_counter2l--; } - if( m_counter2r > 0 ) { len_r /= m_counter2r; m_counter2r--; } - sample_t O2L = oscillate( o2w, leftph, len_l ); - sample_t O2R = oscillate( o2w, rightph, len_r ); + sample_t O2L = 0.; + if (pd_l != 0.) + { + len_l = BandLimitedWave::pdToLen(pd_l); + if (m_counter2l > 0) + { + len_l /= m_counter2l; m_counter2l--; + } + O2L = oscillate(o2w, leftph, len_l); + } + + sample_t O2R = 0.; + if (len_r != 0.) + { + len_r = BandLimitedWave::pdToLen(pd_r); + if (m_counter2r > 0) + { + len_r /= m_counter2r; m_counter2r--; + } + O2R = oscillate(o2w, rightph, len_r); + } // modulate volume O2L *= o2lv; @@ -568,17 +583,40 @@ void MonstroSynth::renderOutput( fpp_t _frames, sampleFrame * _buf ) if( pd_r > 0.5 ) pd_r = 1.0 - pd_r; // multi-wave DC Oscillator - len_l = BandLimitedWave::pdToLen( pd_l ); - len_r = BandLimitedWave::pdToLen( pd_r ); - if( m_counter3l > 0 ) { len_l /= m_counter3l; m_counter3l--; } - if( m_counter3r > 0 ) { len_r /= m_counter3r; m_counter3r--; } - // sub-osc 1 - sample_t O3AL = oscillate( o3w1, leftph, len_l ); - sample_t O3AR = oscillate( o3w1, rightph, len_r ); + sample_t O3AL = 0.; + sample_t O3AR = 0.; // multi-wave DC Oscillator, sub-osc 2 - sample_t O3BL = oscillate( o3w2, leftph, len_l ); - sample_t O3BR = oscillate( o3w2, rightph, len_r ); + sample_t O3BL = 0.; + sample_t O3BR = 0.; + + if (pd_l != 0.) + { + len_l = BandLimitedWave::pdToLen(pd_l); + if (m_counter3l > 0) + { + len_l /= m_counter3l; m_counter3l--; + } + // sub-osc 1 + O3AL = oscillate(o3w1, leftph, len_l); + + // multi-wave DC Oscillator, sub-osc 2 + O3BL = oscillate(o3w2, leftph, len_l); + } + + if (pd_r != 0.) + { + len_r = BandLimitedWave::pdToLen(pd_r); + if (m_counter3r > 0) + { + len_r /= m_counter3r; m_counter3r--; + } + // sub-osc 1 + O3AR = oscillate(o3w1, rightph, len_r); + + // multi-wave DC Oscillator, sub-osc 2 + O3BR = oscillate(o3w2, rightph, len_r); + } // calc and modulate sub sub = o3sub; From 56e7f7de50c5eda47c688c7ce92f48a09d04fec8 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:31:22 -0800 Subject: [PATCH 064/191] Add setSampleRate to BasicFilters and update LOMM allpass sample rate (#7048) --- include/BasicFilters.h | 10 ++++++++++ plugins/LOMM/LOMM.cpp | 11 +---------- plugins/LOMM/LOMM.h | 2 -- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/include/BasicFilters.h b/include/BasicFilters.h index 9351cbafb..b994aecc6 100644 --- a/include/BasicFilters.h +++ b/include/BasicFilters.h @@ -328,6 +328,16 @@ public: } } + inline void setSampleRate(const sample_rate_t sampleRate) + { + m_sampleRate = sampleRate; + m_sampleRatio = 1.f / m_sampleRate; + if (m_subFilter != nullptr) + { + m_subFilter->setSampleRate(m_sampleRate); + } + } + inline sample_t update( sample_t _in0, ch_cnt_t _chnl ) { sample_t out; diff --git a/plugins/LOMM/LOMM.cpp b/plugins/LOMM/LOMM.cpp index 482a59b7e..a0bd556ef 100644 --- a/plugins/LOMM/LOMM.cpp +++ b/plugins/LOMM/LOMM.cpp @@ -81,6 +81,7 @@ void LOMMEffect::changeSampleRate() m_lp2.setSampleRate(m_sampleRate); m_hp1.setSampleRate(m_sampleRate); m_hp2.setSampleRate(m_sampleRate); + m_ap.setSampleRate(m_sampleRate); m_coeffPrecalc = -2.2f / (m_sampleRate * 0.001f); m_needsUpdate = true; @@ -98,16 +99,6 @@ void LOMMEffect::changeSampleRate() } } -void LOMMEffect::clearFilterHistories() -{ - m_lp1.clearHistory(); - m_lp2.clearHistory(); - m_hp1.clearHistory(); - m_hp2.clearHistory(); -} - - - bool LOMMEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) { diff --git a/plugins/LOMM/LOMM.h b/plugins/LOMM/LOMM.h index c1a5aef70..196d0a09d 100644 --- a/plugins/LOMM/LOMM.h +++ b/plugins/LOMM/LOMM.h @@ -52,8 +52,6 @@ public: return &m_lommControls; } - void clearFilterHistories(); - inline float msToCoeff(float ms) { return (ms == 0) ? 0 : exp(m_coeffPrecalc / ms); From 1eff322c2a3cc238a414312026bdb9d6ebfbe3b0 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 7 Jan 2024 10:06:34 +0100 Subject: [PATCH 065/191] Fix crash on subsequent song loads (#7051) Fix a crash that occurs when songs are loaded after each other. The crash only occurs if the "participating" songs have at least one other channel besides the master channel. The crash occurred because the `MixerChannelViews` were not really deleted in `MixerView::refreshDisplay`. As a consequence the parent child relationship was still given between the `MixerView` and the `MixerChannelView`. When a song was loaded the event queue was evaluated which in turn resulted in the method `Fader::knobPosY` being called on a `Fader` which did not have a model anymore. Fixes #7046. --- src/gui/MixerView.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index f20b60a38..a28ac7976 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -206,8 +206,11 @@ void MixerView::refreshDisplay() // delete all views and re-add them for (int i = 1; iremoveWidget(m_mixerChannelViews[i]); - m_racksLayout->removeWidget(m_mixerChannelViews[i]->m_effectRackView); + auto * mixerChannelView = m_mixerChannelViews[i]; + chLayout->removeWidget(mixerChannelView); + m_racksLayout->removeWidget(mixerChannelView->m_effectRackView); + + delete mixerChannelView; } m_channelAreaWidget->adjustSize(); From 18d34431b352f49d0312d6509aef0a4bc2dd12be Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Mon, 8 Jan 2024 06:50:55 +0530 Subject: [PATCH 066/191] Run without terminal for non-debug Windows builds (#7022) * set win32 flag * added mingw too to win32 flag + dom's suggestions * revert unnecessary changes i made. * simplify msys linker flag condition * removed extra whitespace * whitespace change 2 --- src/CMakeLists.txt | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c074ea2ef..d55a725dd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -210,22 +210,16 @@ ENDFOREACH() set_target_properties(lmms PROPERTIES ENABLE_EXPORTS ON + WIN32_EXECUTABLE $> ) -IF(LMMS_BUILD_WIN32) - IF(NOT MSVC) - SET_PROPERTY(TARGET lmms - APPEND_STRING PROPERTY LINK_FLAGS " -mwindows" - ) - ENDIF() - IF(LMMS_BUILD_MSYS) - # ENABLE_EXPORTS property has no effect in some MSYS2 configurations. - # Add the linker flag manually to create liblmms.dll.a import library - SET_PROPERTY(TARGET lmms - APPEND_STRING PROPERTY LINK_FLAGS " -Wl,--out-implib,liblmms.dll.a" - ) - ENDIF() -ELSE() +IF(LMMS_BUILD_MSYS) + # ENABLE_EXPORTS property has no effect in some MSYS2 configurations. + # Add the linker flag manually to create liblmms.dll.a import library + SET_PROPERTY(TARGET lmms + APPEND_STRING PROPERTY LINK_FLAGS " -Wl,--out-implib,liblmms.dll.a" + ) +ELSEIF(NOT WIN32) if(CMAKE_INSTALL_MANDIR) SET(INSTALL_MANDIR ${CMAKE_INSTALL_MANDIR}) ELSE(CMAKE_INSTALL_MANDIR) From d945ac1cbef225369216a34d1b9d99a576848a06 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 8 Jan 2024 15:44:58 +0100 Subject: [PATCH 067/191] Fix scaling of PixmapButton in layouts (#7053) --- include/PixmapButton.h | 2 ++ src/gui/widgets/PixmapButton.cpp | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/include/PixmapButton.h b/include/PixmapButton.h index e8f546dc0..734bd11ae 100644 --- a/include/PixmapButton.h +++ b/include/PixmapButton.h @@ -56,6 +56,8 @@ protected: void mouseReleaseEvent( QMouseEvent * _me ) override; void mouseDoubleClickEvent( QMouseEvent * _me ) override; +private: + bool isActive() const; private: QPixmap m_activePixmap; diff --git a/src/gui/widgets/PixmapButton.cpp b/src/gui/widgets/PixmapButton.cpp index 13c09c52e..069acad56 100644 --- a/src/gui/widgets/PixmapButton.cpp +++ b/src/gui/widgets/PixmapButton.cpp @@ -50,20 +50,15 @@ PixmapButton::PixmapButton( QWidget * _parent, const QString & _name ) : -void PixmapButton::paintEvent( QPaintEvent * ) +void PixmapButton::paintEvent(QPaintEvent*) { - QPainter p( this ); + QPainter p(this); - if( ( model() != nullptr && model()->value() ) || m_pressed ) + QPixmap* pixmapToDraw = isActive() ? &m_activePixmap : &m_inactivePixmap; + + if (!pixmapToDraw->isNull()) { - if( !m_activePixmap.isNull() ) - { - p.drawPixmap( 0, 0, m_activePixmap ); - } - } - else if( !m_inactivePixmap.isNull() ) - { - p.drawPixmap( 0, 0, m_inactivePixmap ); + p.drawPixmap(0, 0, *pixmapToDraw); } } @@ -129,15 +124,20 @@ void PixmapButton::setInactiveGraphic( const QPixmap & _pm, bool _update ) QSize PixmapButton::sizeHint() const { - if( ( model() != nullptr && model()->value() ) || m_pressed ) + if (isActive()) { - return m_activePixmap.size() / devicePixelRatio(); + return m_activePixmap.size(); } else { - return m_inactivePixmap.size() / devicePixelRatio(); + return m_inactivePixmap.size(); } } +bool PixmapButton::isActive() const +{ + return (model() != nullptr && model()->value()) || m_pressed; +} + } // namespace lmms::gui From 85399c12c266a36fdf0777254a3c39c85c4d3877 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 13 Jan 2024 22:03:03 +0100 Subject: [PATCH 068/191] Abstraction in MixerChannelView (#7057) Reduce code repetition in `MixerChannelView` by introducing methods that: * Retrieve the `MixerChannel` that's associated with the view * Check if the associated channel is the master channel This abstracts some functionality and also reduces direct usage of the variable `m_channelIndex`. --- include/MixerChannelView.h | 7 +++++++ src/gui/MixerChannelView.cpp | 29 +++++++++++++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/include/MixerChannelView.h b/include/MixerChannelView.h index 8d2306f91..1710623d7 100644 --- a/include/MixerChannelView.h +++ b/include/MixerChannelView.h @@ -38,6 +38,11 @@ #include #include +namespace lmms +{ + class MixerChannel; +} + namespace lmms::gui { constexpr int MIXER_CHANNEL_INNER_BORDER_SIZE = 3; @@ -100,6 +105,8 @@ namespace lmms::gui private: QString elideName(const QString& name); + MixerChannel* mixerChannel() const; + auto isMasterChannel() const -> bool { return m_channelIndex == 0; } private: SendButtonIndicator* m_sendButton; diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index 0f8ccedea..9ea266238 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -144,9 +144,9 @@ namespace lmms::gui void MixerChannelView::contextMenuEvent(QContextMenuEvent*) { - auto contextMenu = new CaptionMenu(Engine::mixer()->mixerChannel(m_channelIndex)->m_name, this); + auto contextMenu = new CaptionMenu(mixerChannel()->m_name, this); - if (m_channelIndex != 0) // no move-options in master + if (!isMasterChannel()) // no move-options in master { contextMenu->addAction(tr("Move &left"), this, &MixerChannelView::moveChannelLeft); contextMenu->addAction(tr("Move &right"), this, &MixerChannelView::moveChannelRight); @@ -155,7 +155,7 @@ namespace lmms::gui contextMenu->addAction(tr("Rename &channel"), this, &MixerChannelView::renameChannel); contextMenu->addSeparator(); - if (m_channelIndex != 0) // no remove-option in master + if (!isMasterChannel()) // no remove-option in master { contextMenu->addAction(embed::getIconPixmap("cancel"), tr("R&emove channel"), this, &MixerChannelView::removeChannel); contextMenu->addSeparator(); @@ -178,7 +178,7 @@ namespace lmms::gui void MixerChannelView::paintEvent(QPaintEvent* event) { auto * mixer = Engine::mixer(); - const auto channel = mixer->mixerChannel(m_channelIndex); + const auto channel = mixerChannel(); const bool muted = channel->m_muteModel.value(); const auto name = channel->m_name; const auto elidedName = elideName(name); @@ -351,7 +351,7 @@ namespace lmms::gui m_channelNumberLcd->hide(); m_renameLineEdit->setFixedWidth(m_renameLineEdit->width()); - m_renameLineEdit->setText(Engine::mixer()->mixerChannel(m_channelIndex)->m_name); + m_renameLineEdit->setText(mixerChannel()->m_name); m_renameLineEditView->setFocus(); m_renameLineEdit->selectAll(); @@ -370,27 +370,27 @@ namespace lmms::gui auto newName = m_renameLineEdit->text(); setFocus(); - const auto mixerChannel = Engine::mixer()->mixerChannel(m_channelIndex); - if (!newName.isEmpty() && mixerChannel->m_name != newName) + const auto mc = mixerChannel(); + if (!newName.isEmpty() && mc->m_name != newName) { - mixerChannel->m_name = newName; + mc->m_name = newName; m_renameLineEdit->setText(elideName(newName)); Engine::getSong()->setModified(); } - setToolTip(mixerChannel->m_name); + setToolTip(mc->m_name); } void MixerChannelView::resetColor() { - Engine::mixer()->mixerChannel(m_channelIndex)->setColor(std::nullopt); + mixerChannel()->setColor(std::nullopt); Engine::getSong()->setModified(); update(); } void MixerChannelView::selectColor() { - const auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + const auto channel = mixerChannel(); const auto initialColor = channel->color().value_or(backgroundActive().color()); const auto * colorChooser = ColorChooser{this}.withPalette(ColorChooser::Palette::Mixer); @@ -406,7 +406,7 @@ namespace lmms::gui void MixerChannelView::randomizeColor() { - auto channel = Engine::mixer()->mixerChannel(m_channelIndex); + auto channel = mixerChannel(); channel->setColor(ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]); Engine::getSong()->setModified(); update(); @@ -444,4 +444,9 @@ namespace lmms::gui return elidedName; } + MixerChannel* MixerChannelView::mixerChannel() const + { + return Engine::mixer()->mixerChannel(m_channelIndex); + } + } // namespace lmms::gui \ No newline at end of file From b67c53ad29359b721b4cc17f3a83a1bbc7eebebe Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 13 Jan 2024 21:05:18 -0500 Subject: [PATCH 069/191] Fix sliding of waveform when drawing sample in reverse (#7063) --- include/Sample.h | 2 +- include/SampleWaveform.h | 10 +++- .../AudioFileProcessor/AudioFileProcessor.cpp | 12 ++-- plugins/SlicerT/SlicerTWaveform.cpp | 17 +++--- src/gui/SampleWaveform.cpp | 55 ++++++++----------- src/gui/clips/SampleClipView.cpp | 5 +- src/gui/editors/AutomationEditor.cpp | 7 ++- 7 files changed, 62 insertions(+), 46 deletions(-) diff --git a/include/Sample.h b/include/Sample.h index 2ccb78b19..102aaf2d5 100644 --- a/include/Sample.h +++ b/include/Sample.h @@ -96,7 +96,7 @@ public: auto sampleDuration() const -> std::chrono::milliseconds; auto sampleFile() const -> const QString& { return m_buffer->audioFile(); } auto sampleRate() const -> int { return m_buffer->sampleRate(); } - auto sampleSize() const -> int { return m_buffer->size(); } + auto sampleSize() const -> size_t { return m_buffer->size(); } auto toBase64() const -> QString { return m_buffer->toBase64(); } diff --git a/include/SampleWaveform.h b/include/SampleWaveform.h index 692c1f9cb..0185e0e98 100644 --- a/include/SampleWaveform.h +++ b/include/SampleWaveform.h @@ -34,7 +34,15 @@ namespace lmms::gui { class LMMS_EXPORT SampleWaveform { public: - static void visualize(const Sample& sample, QPainter& p, const QRect& dr, int fromFrame = 0, int toFrame = 0); + struct Parameters + { + const sampleFrame* buffer; + size_t size; + float amplification; + bool reversed; + }; + + static void visualize(Parameters parameters, QPainter& painter, const QRect& rect); }; } // namespace lmms::gui diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index ce3c9c07b..86b922fc4 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -719,7 +719,7 @@ void AudioFileProcessorWaveView::updateSampleRange() { const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; m_from = qMax(0, m_sample->startFrame() - marging); - m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); + m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); } } @@ -1014,7 +1014,11 @@ void AudioFileProcessorWaveView::updateGraph() m_graph.fill( Qt::transparent ); QPainter p( &m_graph ); p.setPen( QColor( 255, 255, 255 ) ); - SampleWaveform::visualize(*m_sample, p, QRect(0, 0, m_graph.width(), m_graph.height()), m_from, m_to); + + const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; + const auto waveform = SampleWaveform::Parameters{ + m_sample->data() + m_from, static_cast(m_to - m_from), m_sample->amplification(), m_sample->reversed()}; + SampleWaveform::visualize(waveform, p, rect); } @@ -1076,8 +1080,8 @@ void AudioFileProcessorWaveView::slide( int _px ) step = -step; } - f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; - f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; + f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; + f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; step = qAbs( step_from ) < qAbs( step_to ) ? step_from : step_to; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 66747036a..3793ed2f1 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -89,9 +89,10 @@ void SlicerTWaveform::drawSeekerWaveform() QPainter brush(&m_seekerWaveform); brush.setPen(s_waveformColor); - SampleWaveform::visualize(m_slicerTParent->m_originalSample, brush, - QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, - m_slicerTParent->m_originalSample.sampleSize()); + const auto& sample = m_slicerTParent->m_originalSample; + const auto waveform = SampleWaveform::Parameters{sample.data(), sample.sampleSize(), sample.amplification(), sample.reversed()}; + const auto rect = QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()); + SampleWaveform::visualize(waveform, brush, rect); // increase brightness in inner color QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); @@ -139,14 +140,16 @@ void SlicerTWaveform::drawEditorWaveform() if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_editorWaveform); - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.sampleSize(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.sampleSize(); + size_t startFrame = m_seekerStart * m_slicerTParent->m_originalSample.sampleSize(); + size_t endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.sampleSize(); brush.setPen(s_waveformColor); float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; - SampleWaveform::visualize(m_slicerTParent->m_originalSample, brush, - QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); + const auto& sample = m_slicerTParent->m_originalSample; + const auto waveform = SampleWaveform::Parameters{sample.data() + startFrame, endFrame - startFrame, sample.amplification(), sample.reversed()}; + const auto rect = QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight); + SampleWaveform::visualize(waveform, brush, rect); // increase brightness in inner color QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); diff --git a/src/gui/SampleWaveform.cpp b/src/gui/SampleWaveform.cpp index 5d3afdee3..68b09b482 100644 --- a/src/gui/SampleWaveform.cpp +++ b/src/gui/SampleWaveform.cpp @@ -26,31 +26,25 @@ namespace lmms::gui { -void SampleWaveform::visualize(const Sample& sample, QPainter& p, const QRect& dr, int fromFrame, int toFrame) +void SampleWaveform::visualize(Parameters parameters, QPainter& painter, const QRect& rect) { - if (sample.sampleSize() == 0) { return; } - - const auto x = dr.x(); - const auto height = dr.height(); - const auto width = dr.width(); - const auto centerY = dr.center().y(); + const auto x = rect.x(); + const auto height = rect.height(); + const auto width = rect.width(); + const auto centerY = rect.center().y(); const auto halfHeight = height / 2; - const auto buffer = sample.data() + fromFrame; - const auto color = p.pen().color(); + const auto color = painter.pen().color(); const auto rmsColor = color.lighter(123); - auto numFrames = toFrame - fromFrame; - if (numFrames == 0) { numFrames = sample.sampleSize(); } - - const auto framesPerPixel = std::max(1, numFrames / width); + const auto framesPerPixel = std::max(1, parameters.size / width); constexpr auto maxFramesPerPixel = 512; - const auto resolution = std::max(1, framesPerPixel / maxFramesPerPixel); + const auto resolution = std::max(1, framesPerPixel / maxFramesPerPixel); const auto framesPerResolution = framesPerPixel / resolution; - const auto numPixels = std::min(numFrames, width); + const auto numPixels = std::min(parameters.size, width); auto min = std::vector(numPixels, 1); auto max = std::vector(numPixels, -1); auto squared = std::vector(numPixels); @@ -59,35 +53,34 @@ void SampleWaveform::visualize(const Sample& sample, QPainter& p, const QRect& d for (int i = 0; i < maxFrames; i += resolution) { const auto pixelIndex = i / framesPerPixel; - const auto value = std::accumulate(buffer[i].begin(), buffer[i].end(), 0.0f) / buffer[i].size(); + const auto frameIndex = !parameters.reversed ? i : maxFrames - i; + + const auto& frame = parameters.buffer[frameIndex]; + const auto value = std::accumulate(frame.begin(), frame.end(), 0.0f) / frame.size(); + if (value > max[pixelIndex]) { max[pixelIndex] = value; } if (value < min[pixelIndex]) { min[pixelIndex] = value; } + squared[pixelIndex] += value * value; } - const auto amplification = sample.amplification(); - const auto reversed = sample.reversed(); - for (int i = 0; i < numPixels; i++) { - const auto lineY1 = centerY - max[i] * halfHeight * amplification; - const auto lineY2 = centerY - min[i] * halfHeight * amplification; - - auto lineX = i + x; - if (reversed) { lineX = width - lineX; } - - p.drawLine(lineX, lineY1, lineX, lineY2); + const auto lineY1 = centerY - max[i] * halfHeight * parameters.amplification; + const auto lineY2 = centerY - min[i] * halfHeight * parameters.amplification; + const auto lineX = i + x; + painter.drawLine(lineX, lineY1, lineX, lineY2); const auto rms = std::sqrt(squared[i] / framesPerResolution); const auto maxRMS = std::clamp(rms, min[i], max[i]); const auto minRMS = std::clamp(-rms, min[i], max[i]); - const auto rmsLineY1 = centerY - maxRMS * halfHeight * amplification; - const auto rmsLineY2 = centerY - minRMS * halfHeight * amplification; + const auto rmsLineY1 = centerY - maxRMS * halfHeight * parameters.amplification; + const auto rmsLineY2 = centerY - minRMS * halfHeight * parameters.amplification; - p.setPen(rmsColor); - p.drawLine(lineX, rmsLineY1, lineX, rmsLineY2); - p.setPen(color); + painter.setPen(rmsColor); + painter.drawLine(lineX, rmsLineY1, lineX, rmsLineY2); + painter.setPen(color); } } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index 8f3163385..ef46325e4 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -269,7 +269,10 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) float offset = m_clip->startTimeOffset() / ticksPerBar * pixelsPerBar(); QRect r = QRect( offset, spacing, qMax( static_cast( m_clip->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing ); - SampleWaveform::visualize(m_clip->m_sample, p, r); + + const auto& sample = m_clip->m_sample; + const auto waveform = SampleWaveform::Parameters{sample.data(), sample.sampleSize(), sample.amplification(), sample.reversed()}; + SampleWaveform::visualize(waveform, p, r); QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); paintTextLabel(name, p); diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 90881c7ef..c84725f44 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1248,7 +1248,12 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN; p.setPen(m_ghostSampleColor); - SampleWaveform::visualize(m_ghostSample->sample(), p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); + + const auto& sample = m_ghostSample->sample(); + const auto waveform = SampleWaveform::Parameters{ + sample.data(), sample.sampleSize(), sample.amplification(), sample.reversed()}; + const auto rect = QRect(startPos, yOffset, sampleWidth, sampleHeight); + SampleWaveform::visualize(waveform, p, rect); } // draw ghost notes From af90aff84c96672af1f0cbbef6debae9043d0f13 Mon Sep 17 00:00:00 2001 From: zynskeywolf Date: Sun, 14 Jan 2024 03:08:41 +0100 Subject: [PATCH 070/191] Some minor UI fixes (#7044) * fix ladspa browser description (unwanted line breaks), add missing labels to instruments' controls * some more ui fixes * translation context + style fixup --- plugins/BitInvader/BitInvader.cpp | 4 +- plugins/Flanger/FlangerControls.cpp | 4 +- plugins/LadspaBrowser/LadspaDescription.cpp | 55 +++++-------------- plugins/Nes/Nes.cpp | 34 ++++++------ plugins/Organic/Organic.cpp | 2 +- plugins/SpectrumAnalyzer/SaControlsDialog.cpp | 8 +-- plugins/Stk/Mallets/Mallets.cpp | 2 +- src/gui/editors/AutomationEditor.cpp | 4 +- 8 files changed, 42 insertions(+), 71 deletions(-) diff --git a/plugins/BitInvader/BitInvader.cpp b/plugins/BitInvader/BitInvader.cpp index 4ea73dc71..4685478ff 100644 --- a/plugins/BitInvader/BitInvader.cpp +++ b/plugins/BitInvader/BitInvader.cpp @@ -153,8 +153,8 @@ BitInvader::BitInvader( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &bitinvader_plugin_descriptor ), m_sampleLength(wavetableSize, 4, wavetableSize, 1, this, tr("Sample length")), m_graph(-1.0f, 1.0f, wavetableSize, this), - m_interpolation( false, this ), - m_normalize( false, this ) + m_interpolation(false, this, tr("Interpolation")), + m_normalize(false, this, tr("Normalize")) { m_graph.setWaveToSine(); lengthChanged(); diff --git a/plugins/Flanger/FlangerControls.cpp b/plugins/Flanger/FlangerControls.cpp index 7402216ee..5550cdfb7 100644 --- a/plugins/Flanger/FlangerControls.cpp +++ b/plugins/Flanger/FlangerControls.cpp @@ -38,9 +38,9 @@ FlangerControls::FlangerControls( FlangerEffect *effect ) : m_effect ( effect ), m_delayTimeModel(0.001, 0.0001, 0.050, 0.0001, this, tr( "Delay samples" ) ), m_lfoFrequencyModel( 0.25, 0.01, 60, 0.0001, 60000.0, this, tr( "LFO frequency" ) ), - m_lfoAmountModel( 0.0, 0.0, 0.0025, 0.0001, this, tr( "Seconds" ) ), + m_lfoAmountModel( 0.0, 0.0, 0.0025, 0.0001, this, tr( "Amount" ) ), m_lfoPhaseModel( 90.0, 0.0, 360.0, 0.0001, this, tr( "Stereo phase" ) ), - m_feedbackModel( 0.0, -1.0, 1.0, 0.0001, this, tr( "Regen" ) ), + m_feedbackModel( 0.0, -1.0, 1.0, 0.0001, this, tr( "Feedback" ) ), m_whiteNoiseAmountModel( 0.0, 0.0, 0.05, 0.0001, this, tr( "Noise" ) ), m_invertFeedbackModel ( false, this, tr( "Invert" ) ) diff --git a/plugins/LadspaBrowser/LadspaDescription.cpp b/plugins/LadspaBrowser/LadspaDescription.cpp index fbcd6c25d..7b1ede1c3 100644 --- a/plugins/LadspaBrowser/LadspaDescription.cpp +++ b/plugins/LadspaBrowser/LadspaDescription.cpp @@ -125,68 +125,39 @@ void LadspaDescription::update( const ladspa_key_t & _key ) Ladspa2LMMS * manager = Engine::getLADSPAManager(); auto name = new QLabel(description); - name->setText( QWidget::tr( "Name: " ) + manager->getName( _key ) ); + name->setText(tr("Name: ") + manager->getName(_key)); layout->addWidget( name ); - auto maker = new QWidget(description); - auto makerLayout = new QHBoxLayout(maker); - makerLayout->setContentsMargins(0, 0, 0, 0); - makerLayout->setSpacing( 0 ); + auto maker = new QLabel(description); + maker->setText(tr("Maker: ") + manager->getMaker(_key)); layout->addWidget( maker ); - auto maker_label = new QLabel(maker); - maker_label->setText( QWidget::tr( "Maker: " ) ); - maker_label->setAlignment( Qt::AlignTop ); - auto maker_content = new QLabel(maker); - maker_content->setText( manager->getMaker( _key ) ); - maker_content->setWordWrap( true ); - makerLayout->addWidget( maker_label ); - makerLayout->addWidget( maker_content, 1 ); - - auto copyright = new QWidget(description); - auto copyrightLayout = new QHBoxLayout(copyright); - copyrightLayout->setContentsMargins(0, 0, 0, 0); - copyrightLayout->setSpacing( 0 ); + auto copyright = new QLabel(description); + copyright->setText(tr("Copyright: ") + manager->getCopyright(_key)); layout->addWidget( copyright ); - auto copyright_label = new QLabel(copyright); - copyright_label->setText( QWidget::tr( "Copyright: " ) ); - copyright_label->setAlignment( Qt::AlignTop ); - - auto copyright_content = new QLabel(copyright); - copyright_content->setText( manager->getCopyright( _key ) ); - copyright_content->setWordWrap( true ); - copyrightLayout->addWidget( copyright_label ); - copyrightLayout->addWidget( copyright_content, 1 ); - auto requiresRealTime = new QLabel(description); - requiresRealTime->setText( QWidget::tr( "Requires Real Time: " ) + - ( manager->hasRealTimeDependency( _key ) ? - QWidget::tr( "Yes" ) : - QWidget::tr( "No" ) ) ); + requiresRealTime->setText(tr("Requires Real Time: ") + + (manager->hasRealTimeDependency(_key) ? tr("Yes") : tr("No"))); layout->addWidget( requiresRealTime ); auto realTimeCapable = new QLabel(description); - realTimeCapable->setText( QWidget::tr( "Real Time Capable: " ) + - ( manager->isRealTimeCapable( _key ) ? - QWidget::tr( "Yes" ) : - QWidget::tr( "No" ) ) ); + realTimeCapable->setText(tr("Real Time Capable: ") + + (manager->isRealTimeCapable(_key) ? tr("Yes") : tr("No"))); layout->addWidget( realTimeCapable ); auto inplaceBroken = new QLabel(description); - inplaceBroken->setText( QWidget::tr( "In Place Broken: " ) + - ( manager->isInplaceBroken( _key ) ? - QWidget::tr( "Yes" ) : - QWidget::tr( "No" ) ) ); + inplaceBroken->setText(tr("In Place Broken: ") + + (manager->isInplaceBroken(_key) ? tr("Yes") : tr("No"))); layout->addWidget( inplaceBroken ); auto channelsIn = new QLabel(description); - channelsIn->setText( QWidget::tr( "Channels In: " ) + QString::number( + channelsIn->setText(tr("Channels In: ") + QString::number( manager->getDescription( _key )->inputChannels ) ); layout->addWidget( channelsIn ); auto channelsOut = new QLabel(description); - channelsOut->setText( QWidget::tr( "Channels Out: " ) + QString::number( + channelsOut->setText(tr("Channels Out: ") + QString::number( manager->getDescription( _key )->outputChannels ) ); layout->addWidget( channelsOut ); } diff --git a/plugins/Nes/Nes.cpp b/plugins/Nes/Nes.cpp index 2c0907a19..df88c4942 100644 --- a/plugins/Nes/Nes.cpp +++ b/plugins/Nes/Nes.cpp @@ -482,53 +482,53 @@ void NesObject::updatePitch() NesInstrument::NesInstrument( InstrumentTrack * instrumentTrack ) : Instrument( instrumentTrack, &nes_plugin_descriptor ), - m_ch1Enabled( true, this ), + m_ch1Enabled(true, this, tr("Channel 1 enable")), m_ch1Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 1 coarse detune" ) ), m_ch1Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 1 volume" ) ), - m_ch1EnvEnabled( false, this ), - m_ch1EnvLooped( false, this ), + m_ch1EnvEnabled(false, this, tr("Channel 1 envelope enable")), + m_ch1EnvLooped(false, this, tr("Channel 1 envelope loop")), m_ch1EnvLen( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 1 envelope length" ) ), m_ch1DutyCycle( 0, 0, 3, this, tr( "Channel 1 duty cycle" ) ), - m_ch1SweepEnabled( false, this ), + m_ch1SweepEnabled(false, this, tr("Channel 1 sweep enable")), m_ch1SweepAmt( 0.f, -7.f, 7.f, 1.f, this, tr( "Channel 1 sweep amount" ) ), m_ch1SweepRate( 0.f, 0.f, 7.f, 1.f, this, tr( "Channel 1 sweep rate" ) ), - m_ch2Enabled( true, this ), - m_ch2Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 2 Coarse detune" ) ), - m_ch2Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 2 Volume" ) ), + m_ch2Enabled(true, this, tr("Channel 2 enable")), + m_ch2Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 2 coarse detune" ) ), + m_ch2Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 2 volume" ) ), - m_ch2EnvEnabled( false, this ), - m_ch2EnvLooped( false, this ), + m_ch2EnvEnabled(false, this, tr("Channel 2 envelope enable")), + m_ch2EnvLooped(false, this, tr("Channel 2 envelope loop")), m_ch2EnvLen( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 2 envelope length" ) ), m_ch2DutyCycle( 2, 0, 3, this, tr( "Channel 2 duty cycle" ) ), - m_ch2SweepEnabled( false, this ), + m_ch2SweepEnabled(false, this, tr("Channel 2 sweep enable")), m_ch2SweepAmt( 0.f, -7.f, 7.f, 1.f, this, tr( "Channel 2 sweep amount" ) ), m_ch2SweepRate( 0.f, 0.f, 7.f, 1.f, this, tr( "Channel 2 sweep rate" ) ), //channel 3 - m_ch3Enabled( true, this ), + m_ch3Enabled(true, this, tr("Channel 3 enable")), m_ch3Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 3 coarse detune" ) ), m_ch3Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 3 volume" ) ), //channel 4 - m_ch4Enabled( false, this ), + m_ch4Enabled(false, this, tr("Channel 4 enable")), m_ch4Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 4 volume" ) ), - m_ch4EnvEnabled( false, this ), - m_ch4EnvLooped( false, this ), + m_ch4EnvEnabled(false, this, tr("Channel 4 envelope enable")), + m_ch4EnvLooped(false, this, tr("Channel 4 envelope loop")), m_ch4EnvLen( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 4 envelope length" ) ), - m_ch4NoiseMode( false, this ), - m_ch4NoiseFreqMode( false, this ), + m_ch4NoiseMode(false, this, tr("Channel 4 noise mode")), + m_ch4NoiseFreqMode(false, this, tr("Channel 4 frequency mode")), m_ch4NoiseFreq( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 4 noise frequency" ) ), m_ch4Sweep( 0.f, -7.f, 7.f, 1.f, this, tr( "Channel 4 noise frequency sweep" ) ), - m_ch4NoiseQuantize( true, this ), + m_ch4NoiseQuantize(true, this, tr("Channel 4 quantize")), //master m_masterVol( 1.0f, 0.0f, 2.0f, 0.01f, this, tr( "Master volume" ) ), diff --git a/plugins/Organic/Organic.cpp b/plugins/Organic/Organic.cpp index 761010922..2dba63629 100644 --- a/plugins/Organic/Organic.cpp +++ b/plugins/Organic/Organic.cpp @@ -563,7 +563,7 @@ OscillatorObject::OscillatorObject( Model * _parent, int _index ) : m_panModel( DefaultPanning, PanningLeft, PanningRight, 1.0f, this, tr( "Osc %1 panning" ).arg( _index + 1 ) ), m_detuneModel( 0.0f, -1200.0f, 1200.0f, 1.0f, - this, tr( "Osc %1 fine detuning left" ).arg( _index + 1 ) ) + this, tr( "Osc %1 stereo detuning" ).arg( _index + 1 ) ) { } diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index d36d8a3ee..2b0ca4fec 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -119,28 +119,28 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) auto waterfallButton = new LedCheckBox(tr("Waterfall"), this); waterfallButton->setToolTip(tr("Display real-time spectrogram")); waterfallButton->setCheckable(true); - waterfallButton->setMinimumSize(70, 12); + waterfallButton->setMinimumSize(100, 12); waterfallButton->setModel(&controls->m_waterfallModel); config_layout->addWidget(waterfallButton, 0, 1); auto smoothButton = new LedCheckBox(tr("Averaging"), this); smoothButton->setToolTip(tr("Enable exponential moving average")); smoothButton->setCheckable(true); - smoothButton->setMinimumSize(70, 12); + smoothButton->setMinimumSize(100, 12); smoothButton->setModel(&controls->m_smoothModel); config_layout->addWidget(smoothButton, 1, 1); auto stereoButton = new LedCheckBox(tr("Stereo"), this); stereoButton->setToolTip(tr("Display stereo channels separately")); stereoButton->setCheckable(true); - stereoButton->setMinimumSize(70, 12); + stereoButton->setMinimumSize(100, 12); stereoButton->setModel(&controls->m_stereoModel); config_layout->addWidget(stereoButton, 2, 1); auto peakHoldButton = new LedCheckBox(tr("Peak hold"), this); peakHoldButton->setToolTip(tr("Display envelope of peak values")); peakHoldButton->setCheckable(true); - peakHoldButton->setMinimumSize(70, 12); + peakHoldButton->setMinimumSize(100, 12); peakHoldButton->setModel(&controls->m_peakHoldModel); config_layout->addWidget(peakHoldButton, 3, 1); diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index dd3b09494..c67814b5f 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -85,7 +85,7 @@ MalletsInstrument::MalletsInstrument( InstrumentTrack * _instrument_track ): // TODO: m_vibratoModel m_velocityModel(64.0f, 0.1f, 128.0f, 0.1f, this, tr( "Speed" )), m_strikeModel( true, this, tr( "Bowed" ) ), - m_presetsModel(this), + m_presetsModel(this, tr("Instrument")), m_spreadModel(0, 0, 255, 1, this, tr( "Spread" )), m_randomModel(0.0f, 0.0f, 1.0f, 0.01f, this, tr("Randomness")), m_versionModel( MALLETS_PRESET_VERSION, 0, MALLETS_PRESET_VERSION, this, "" ), diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index c84725f44..8fd892597 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1429,8 +1429,8 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) p.drawText( VALUES_WIDTH + 20, TOP_MARGIN + 40, width() - VALUES_WIDTH - 20 - SCROLLBAR_SIZE, grid_height - 40, Qt::TextWordWrap, - tr( "Please open an automation clip with " - "the context menu of a control!" ) ); + tr( "Please open an automation clip by " + "double-clicking on it!" ) ); } // TODO: Get this out of paint event From b53f2b4f5472ae05643a6d11611b20cbaf35c389 Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 13 Jan 2024 22:12:24 -0500 Subject: [PATCH 071/191] Fix AFP playback indicator not shown when dragging and dropping samples (#7064) The issue occurred because `AudioFileProcessorWaveView::updateSampleRange` was not being called in `AudioFileProcessorWaveView::dropEvent`. --- .../AudioFileProcessor/AudioFileProcessor.cpp | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 86b922fc4..f50a6df68 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -611,29 +611,26 @@ void AudioFileProcessorView::newWaveView() void AudioFileProcessorView::dropEvent( QDropEvent * _de ) { - QString type = StringPairDrag::decodeKey( _de ); - QString value = StringPairDrag::decodeValue( _de ); - if( type == "samplefile" ) + const auto type = StringPairDrag::decodeKey(_de); + const auto value = StringPairDrag::decodeValue(_de); + + if (type == "samplefile") { castModel()->setAudioFile(value); } + else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) { - castModel()->setAudioFile( value ); - _de->accept(); - newWaveView(); - return; + DataFile dataFile(value.toUtf8()); + castModel()->setAudioFile(dataFile.content().firstChild().toElement().attribute("src")); } - else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) + else { - DataFile dataFile( value.toUtf8() ); - castModel()->setAudioFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); - _de->accept(); + _de->ignore(); return; } - _de->ignore(); + m_waveView->updateSampleRange(); + Engine::getSong()->setModified(); + _de->accept(); } - - - void AudioFileProcessorView::paintEvent( QPaintEvent * ) { QPainter p( this ); From abe2c92bedbc5e7a16ea4858e94694fc247d9552 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 14 Jan 2024 17:17:13 +0100 Subject: [PATCH 072/191] Resizable mixer window (#7037) Make the mixer window resizable and allow the effects rack to grow. This enables the users a better overview of the effects used. --- src/gui/MixerView.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index a28ac7976..a6ee2989c 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -75,7 +75,6 @@ MixerView::MixerView() : //setPalette(pal); setAutoFillBackground(true); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setWindowTitle(tr("Mixer")); setWindowIcon(embed::getIconPixmap("mixer")); @@ -153,7 +152,6 @@ MixerView::MixerView() : // add the stacked layout for the effect racks of mixer channels - m_racksWidget->setFixedHeight(mixerChannelSize.height()); ml->addWidget(m_racksWidget); setCurrentMixerChannel(m_mixerChannelViews[0]); From 5fdf61135785e6388d0eb42edb38efa9cf1fb903 Mon Sep 17 00:00:00 2001 From: saker Date: Mon, 15 Jan 2024 02:34:46 -0500 Subject: [PATCH 073/191] Fix memory leaks within tests (#7001) Caused by not calling `Engine::destroy`, as well as creating tracks/clips on the heap and not freeing the memory at the end of the test. Instead of creating tracks/clips on the heap, they are now made on the stack, similar to how the surrounding tests create its objects. --- tests/main.cpp | 1 + tests/src/tracks/AutomationTrackTest.cpp | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/main.cpp b/tests/main.cpp index c1a5b5a10..b95f211d4 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -19,5 +19,6 @@ int main(int argc, char* argv[]) if (QTest::qExec(suite, argc, argv) != 0) { ++failed; } } qDebug() << "<<" << failed << "out of"<(Track::create(Track::Type::Instrument, song)); + InstrumentTrack instrumentTrack(song); - MidiClip* midiClip = dynamic_cast(instrumentTrack->createClip(0)); - midiClip->changeLength(TimePos(4, 0)); - Note* note = midiClip->addNote(Note(TimePos(4, 0)), false); + MidiClip midiClip(&instrumentTrack); + midiClip.changeLength(TimePos(4, 0)); + Note* note = midiClip.addNote(Note(TimePos(4, 0)), false); note->createDetuning(); DetuningHelper* dh = note->detuning(); @@ -175,10 +174,11 @@ private slots: auto song = Engine::getSong(); auto patternStore = Engine::patternStore(); PatternTrack patternTrack(song); - Track* automationTrack = Track::create(Track::Type::Automation, patternStore); + AutomationTrack automationTrack(patternStore); + automationTrack.createClipsForPattern(patternStore->numOfPatterns() - 1); - QVERIFY(automationTrack->numOfClips()); - AutomationClip* c1 = dynamic_cast(automationTrack->getClip(0)); + QVERIFY(automationTrack.numOfClips()); + auto c1 = dynamic_cast(automationTrack.getClip(0)); QVERIFY(c1); FloatModel model; From 629ba0362b8f668ef0d3cf4ae85fe78a6d4ce40f Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 15 Jan 2024 08:37:11 +0100 Subject: [PATCH 074/191] Fix mixer channel updates on solo/mute (#7055) * Fix mixer channel updates on solo/mute Fix the update of the mixer channels whenever a channel is soloed or muted. The solution is rather "brutal" as it updates all mixer channel views when one of the models changes. Introduce private helper method `MixerView::updateAllMixerChannels`. It calls the `update` method on each `MixerChannelView` so that they can get repainted. Call `updateAllMixerChannels` at the end of the existing method `toggledSolo`. Introduce a new method `MixerView::toggledMute` which is called whenever the mute model of a channel changes. It also updates all mixer channels. Fixes #7054. * Improve mixer channel update for mute events Improve the mixer channel update for mute events by not delegating to `MixerView` like for the solo case. Instead the `MixerChannelView` now has its own `toggledMute` slot which simply updates the widget on changes to the mute model. Also fix `MixerChannelView::setChannelIndex` by disconnecting from the signals of the previous mixer channel and connecting to the signals for the new one. Remove `toggledMute` from `MixerView`. The solo implementation is kept as is because it also triggers changes to the core model. So the chain seems to be: * Solo button clicked in mixer channel view * Button changes solo model * Solo model signals to mixer view which was connected via the mixer channel view * Mixer view performs changes in the other core models * All mixer channels are updated. Are better chain would first update the core models and then update the GUI from the changes: * Solo button clicked in mixer channel view * Button changes solo model * Solo model signals to core mixer, i.e. not a view! * Mixer view performs changes in the other core models * Changed models emit signal to GUI elements * Revert "Improve mixer channel update for mute events" This reverts commit ede65963ea1d6131944679a131641c18b97bce0b. * Add comment After the revert done with commit the code is more consistent again but not in a good way. Hence a comment is added which indicates that an inprovement is needed. * Abstract mixer retrieval Abstract the retrieval of the mixer behind the new method `getMixer`. This is done in preparation for some dependency injection so that the `MixerView` does not have to ask the `Engine` for the mixer but gets it injected, a.k.a. the "Hollywood principle": "Don't call us, we'll call you." It's called `getMixer` and not just mixer because it allows for locale variables to be called `mixer` without confusing it with the method. * Let MixerView connect directly to models Let the `MixerView` connect directly to the solo and mute models it is connected with. Remove the connections that are made in `MixerChannelView` which acted as a proxy which only complicated things. Add `connectToSoloAndMute` which connects the `MixerView` to the solo and mute models of a given channel. Call it whenever a new channel is created. Add `disconnectFromSoloAndMute` which disconnects the `MixerView` from the solo and mute models of a given channel. Call it when a channel is deleted. * Code cleanup Cleanup code related to the creation of the master channel view. * Inject the Mixer into the MixerView Inject the `Mixer` into the `MixerView` via the constructor. This makes it more explicit that the `Mixer` is the model of the view. It also implements the "Dependency Inversion Principle" in that the `MIxerView` does not have to ask the `Engine` for the `Mixer` anymore. The current changes should be safe in that the `Mixer` instance is static and does not change. * Fix connections on song load Disconnect and reconnect in `MixerView::refreshDisplay` which is called when a song is loaded. --- include/MixerView.h | 18 +++++++- src/gui/GuiApplication.cpp | 2 +- src/gui/MixerChannelView.cpp | 3 +- src/gui/MixerView.cpp | 88 ++++++++++++++++++++++++++++-------- 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/include/MixerView.h b/include/MixerView.h index a47786481..81287bc54 100644 --- a/include/MixerView.h +++ b/include/MixerView.h @@ -38,6 +38,11 @@ #include "embed.h" #include "EffectRackView.h" +namespace lmms +{ + class Mixer; +} + namespace lmms::gui { class LMMS_EXPORT MixerView : public QWidget, public ModelView, @@ -45,7 +50,7 @@ class LMMS_EXPORT MixerView : public QWidget, public ModelView, { Q_OBJECT public: - MixerView(); + MixerView(Mixer* mixer); void keyPressEvent(QKeyEvent* e) override; void saveSettings(QDomDocument& doc, QDomElement& domElement) override; @@ -97,7 +102,17 @@ protected: private slots: void updateFaders(); + // TODO This should be improved. Currently the solo and mute models are connected via + // the MixerChannelView's constructor with the MixerView. It would already be an improvement + // if the MixerView connected itself to each new MixerChannel that it creates/handles. void toggledSolo(); + void toggledMute(); + +private: + Mixer* getMixer() const; + void updateAllMixerChannels(); + void connectToSoloAndMute(int channelIndex); + void disconnectFromSoloAndMute(int channelIndex); private: QVector m_mixerChannelViews; @@ -109,6 +124,7 @@ private: QWidget* m_channelAreaWidget; QStackedLayout* m_racksLayout; QWidget* m_racksWidget; + Mixer* m_mixer; void updateMaxChannelSelector(); diff --git a/src/gui/GuiApplication.cpp b/src/gui/GuiApplication.cpp index 3370cbc6e..5c4bdd19a 100644 --- a/src/gui/GuiApplication.cpp +++ b/src/gui/GuiApplication.cpp @@ -148,7 +148,7 @@ GuiApplication::GuiApplication() connect(m_songEditor, SIGNAL(destroyed(QObject*)), this, SLOT(childDestroyed(QObject*))); displayInitProgress(tr("Preparing mixer")); - m_mixerView = new MixerView; + m_mixerView = new MixerView(Engine::mixer()); connect(m_mixerView, SIGNAL(destroyed(QObject*)), this, SLOT(childDestroyed(QObject*))); displayInitProgress(tr("Preparing controller rack")); diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index 9ea266238..928255806 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -110,8 +110,7 @@ namespace lmms::gui m_soloButton->setActiveGraphic(embed::getIconPixmap("led_red")); m_soloButton->setInactiveGraphic(embed::getIconPixmap("led_off")); m_soloButton->setCheckable(true); - m_soloButton->setToolTip(tr("Solo this channel")); - connect(&mixerChannel->m_soloModel, &BoolModel::dataChanged, mixerView, &MixerView::toggledSolo, Qt::DirectConnection); + m_soloButton->setToolTip(tr("Solo this channel")); QVBoxLayout* soloMuteLayout = new QVBoxLayout(); soloMuteLayout->setContentsMargins(0, 0, 0, 0); diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index a6ee2989c..93b4e1299 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -52,10 +52,11 @@ namespace lmms::gui { -MixerView::MixerView() : +MixerView::MixerView(Mixer* mixer) : QWidget(), ModelView(nullptr, this), - SerializingObjectHook() + SerializingObjectHook(), + m_mixer(mixer) { #if QT_VERSION < 0x50C00 // Workaround for a bug in Qt versions below 5.12, @@ -67,8 +68,7 @@ MixerView::MixerView() : using ::operator|; #endif - Mixer * m = Engine::mixer(); - m->setHook(this); + mixer->setHook(this); //QPalette pal = palette(); //pal.setColor(QPalette::Window, QColor(72, 76, 88)); @@ -100,12 +100,13 @@ MixerView::MixerView() : m_racksWidget->setLayout(m_racksLayout); // add master channel - m_mixerChannelViews.resize(m->numChannels()); - m_mixerChannelViews[0] = new MixerChannelView(this, this, 0); + m_mixerChannelViews.resize(mixer->numChannels()); + MixerChannelView * masterView = new MixerChannelView(this, this, 0); + connectToSoloAndMute(0); + m_mixerChannelViews[0] = masterView; m_racksLayout->addWidget(m_mixerChannelViews[0]->m_effectRackView); - MixerChannelView * masterView = m_mixerChannelViews[0]; ml->addWidget(masterView, 0, Qt::AlignTop); auto mixerChannelSize = masterView->sizeHint(); @@ -114,6 +115,7 @@ MixerView::MixerView() : for (int i = 1; i < m_mixerChannelViews.size(); ++i) { m_mixerChannelViews[i] = new MixerChannelView(m_channelAreaWidget, this, i); + connectToSoloAndMute(i); chLayout->addWidget(m_mixerChannelViews[i]); } @@ -175,7 +177,7 @@ MixerView::MixerView() : parentWidget()->move(5, 310); // we want to receive dataChanged-signals in order to update - setModel(m); + setModel(mixer); } @@ -184,10 +186,11 @@ MixerView::MixerView() : int MixerView::addNewChannel() { // add new mixer channel and redraw the form. - Mixer * mix = Engine::mixer(); + Mixer * mix = getMixer(); int newChannelIndex = mix->createChannel(); m_mixerChannelViews.push_back(new MixerChannelView(m_channelAreaWidget, this, newChannelIndex)); + connectToSoloAndMute(newChannelIndex); chLayout->addWidget(m_mixerChannelViews[newChannelIndex]); m_racksLayout->addWidget(m_mixerChannelViews[newChannelIndex]->m_effectRackView); @@ -204,6 +207,9 @@ void MixerView::refreshDisplay() // delete all views and re-add them for (int i = 1; iremoveWidget(mixerChannelView); m_racksLayout->removeWidget(mixerChannelView->m_effectRackView); @@ -213,10 +219,12 @@ void MixerView::refreshDisplay() m_channelAreaWidget->adjustSize(); // re-add the views - m_mixerChannelViews.resize(Engine::mixer()->numChannels()); + m_mixerChannelViews.resize(getMixer()->numChannels()); for (int i = 1; i < m_mixerChannelViews.size(); ++i) { m_mixerChannelViews[i] = new MixerChannelView(m_channelAreaWidget, this, i); + connectToSoloAndMute(i); + chLayout->addWidget(m_mixerChannelViews[i]); m_racksLayout->addWidget(m_mixerChannelViews[i]->m_effectRackView); } @@ -280,10 +288,46 @@ void MixerView::loadSettings(const QDomElement& domElement) void MixerView::toggledSolo() { - Engine::mixer()->toggledSolo(); + getMixer()->toggledSolo(); + + updateAllMixerChannels(); } +void MixerView::toggledMute() +{ + updateAllMixerChannels(); +} + +Mixer* MixerView::getMixer() const +{ + return m_mixer; +} + +void MixerView::updateAllMixerChannels() +{ + for (int i = 0; i < m_mixerChannelViews.size(); ++i) + { + m_mixerChannelViews[i]->update(); + } +} + +void MixerView::connectToSoloAndMute(int channelIndex) +{ + auto * mixerChannel = getMixer()->mixerChannel(channelIndex); + + connect(&mixerChannel->m_muteModel, &BoolModel::dataChanged, this, &MixerView::toggledMute, Qt::DirectConnection); + connect(&mixerChannel->m_soloModel, &BoolModel::dataChanged, this, &MixerView::toggledSolo, Qt::DirectConnection); +} + +void MixerView::disconnectFromSoloAndMute(int channelIndex) +{ + auto * mixerChannel = getMixer()->mixerChannel(channelIndex); + + disconnect(&mixerChannel->m_muteModel, &BoolModel::dataChanged, this, &MixerView::toggledMute); + disconnect(&mixerChannel->m_soloModel, &BoolModel::dataChanged, this, &MixerView::toggledSolo); +} + void MixerView::setCurrentMixerChannel(MixerChannelView* channel) { @@ -301,12 +345,12 @@ void MixerView::setCurrentMixerChannel(MixerChannelView* channel) void MixerView::updateMixerChannel(int index) { - Mixer * mix = Engine::mixer(); + Mixer * mix = getMixer(); // does current channel send to this channel? int selIndex = m_currentMixerChannel->channelIndex(); auto thisLine = m_mixerChannelViews[index]; - thisLine->setToolTip(Engine::mixer()->mixerChannel(index)->m_name); + thisLine->setToolTip(getMixer()->mixerChannel(index)->m_name); FloatModel * sendModel = mix->channelSendModel(selIndex, index); if (sendModel == nullptr) @@ -339,15 +383,19 @@ void MixerView::deleteChannel(int index) return; } + // Disconnect from the solo/mute models of the channel we are about to delete + disconnectFromSoloAndMute(index); + // remember selected line int selLine = m_currentMixerChannel->channelIndex(); + Mixer* mixer = getMixer(); // in case the deleted channel is soloed or the remaining // channels will be left in a muted state - Engine::mixer()->clearChannel(index); + mixer->clearChannel(index); // delete the real channel - Engine::mixer()->deleteChannel(index); + mixer->deleteChannel(index); chLayout->removeWidget(m_mixerChannelViews[index]); m_racksLayout->removeWidget(m_mixerChannelViews[index]); @@ -379,7 +427,7 @@ bool MixerView::confirmRemoval(int index) bool needConfirm = ConfigManager::inst()->value("ui", "mixerchanneldeletionwarning", "1").toInt(); if (!needConfirm) { return true; } - Mixer* mix = Engine::mixer(); + Mixer* mix = getMixer(); if (!mix->isChannelInUse(index)) { @@ -416,7 +464,7 @@ bool MixerView::confirmRemoval(int index) void MixerView::deleteUnusedChannels() { - Mixer* mix = Engine::mixer(); + Mixer* mix = getMixer(); // Check all channels except master, delete those with no incoming sends for (int i = m_mixerChannelViews.size() - 1; i > 0; --i) @@ -435,7 +483,7 @@ void MixerView::moveChannelLeft(int index, int focusIndex) // can't move master or first channel left or last channel right if (index <= 1 || index >= m_mixerChannelViews.size()) return; - Mixer *m = Engine::mixer(); + Mixer *m = getMixer(); // Move instruments channels m->moveChannelLeft(index); @@ -542,7 +590,7 @@ void MixerView::setCurrentMixerChannel(int channel) void MixerView::clear() { - Engine::mixer()->clear(); + getMixer()->clear(); refreshDisplay(); } @@ -552,7 +600,7 @@ void MixerView::clear() void MixerView::updateFaders() { - Mixer * m = Engine::mixer(); + Mixer * m = getMixer(); // apply master gain m->mixerChannel(0)->m_peakLeft *= Engine::audioEngine()->masterGain(); From 6c4d458599fab991fa332e9b42aba647a2925755 Mon Sep 17 00:00:00 2001 From: saker Date: Mon, 15 Jan 2024 17:19:33 -0500 Subject: [PATCH 075/191] Migrate to CTest (#7062) --- .github/workflows/build.yml | 20 +++++----- include/lmms_math.h | 7 ++-- tests/CMakeLists.txt | 49 ++++++++++++------------ tests/QTestSuite.cpp | 19 --------- tests/QTestSuite.h | 21 ---------- tests/main.cpp | 24 ------------ tests/src/core/ArrayVectorTest.cpp | 10 ++--- tests/src/core/AutomatableModelTest.cpp | 23 +++++++++-- tests/src/core/MathTest.cpp | 11 +++--- tests/src/core/ProjectVersionTest.cpp | 9 +++-- tests/src/core/RelativePathsTest.cpp | 13 ++++--- tests/src/tracks/AutomationTrackTest.cpp | 15 ++++++-- 12 files changed, 92 insertions(+), 129 deletions(-) delete mode 100644 tests/QTestSuite.cpp delete mode 100644 tests/QTestSuite.h delete mode 100644 tests/main.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fe1ec7bf..b08c3ba20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,10 +45,10 @@ jobs: cmake .. $CMAKE_OPTS -DCMAKE_INSTALL_PREFIX=./install - name: Build run: cmake --build build - - name: Build tests - run: cmake --build build --target tests - name: Run tests - run: build/tests/tests + run: | + cd build/tests + ctest --output-on-failure -j2 - name: Package run: | cmake --build build --target install @@ -123,10 +123,10 @@ jobs: -DUSE_WERROR=OFF - name: Build run: cmake --build build - - name: Build tests - run: cmake --build build --target tests - name: Run tests - run: build/tests/tests + run: | + cd build/tests + ctest --output-on-failure -j3 - name: Package run: | cmake --build build --target install @@ -194,8 +194,6 @@ jobs: ../cmake/build_win${{ matrix.arch }}.sh - name: Build run: cmake --build build - - name: Build tests - run: cmake --build build --target tests - name: Package run: cmake --build build --target package - name: Upload artifacts @@ -286,8 +284,10 @@ jobs: ${{ steps.cache-deps.outputs.cache-hit == 'true' && 'NO' || 'YES' }} - name: Build run: cmake --build build - - name: Build tests - run: cmake --build build --target tests + - name: Run tests + run: | + cd build/tests + ctest --output-on-failure -j2 - name: Package run: cmake --build build --target package - name: Upload artifacts diff --git a/include/lmms_math.h b/include/lmms_math.h index ea0a75581..f6455d693 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -25,12 +25,13 @@ #ifndef LMMS_MATH_H #define LMMS_MATH_H +#include +#include +#include #include + #include "lmms_constants.h" #include "lmmsconfig.h" -#include - -#include namespace lmms { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ddf9e2962..9a609922c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,34 +1,33 @@ -INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}") -INCLUDE_DIRECTORIES("${CMAKE_CURRENT_BINARY_DIR}") -INCLUDE_DIRECTORIES("${CMAKE_SOURCE_DIR}/include") -INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}") -INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}/src") +include(CTest) -SET(CMAKE_CXX_STANDARD 17) - -SET(CMAKE_AUTOMOC ON) - -# FIXME: remove this once we export include directories for LMMS -IF(LMMS_BUILD_APPLE) -INCLUDE_DIRECTORIES("/usr/local/include") -ENDIF() - -ADD_EXECUTABLE(tests - EXCLUDE_FROM_ALL - main.cpp - QTestSuite.cpp - $ +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(LMMS_TESTS src/core/ArrayVectorTest.cpp src/core/AutomatableModelTest.cpp src/core/MathTest.cpp src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp - src/tracks/AutomationTrackTest.cpp ) -TARGET_COMPILE_DEFINITIONS(tests - PRIVATE $ -) -TARGET_LINK_LIBRARIES(tests ${QT_LIBRARIES} ${QT_QTTEST_LIBRARY}) -TARGET_LINK_LIBRARIES(tests ${LMMS_REQUIRED_LIBS}) + +foreach(LMMS_TEST_SRC IN LISTS LMMS_TESTS) + # TODO CMake 3.20: Use cmake_path + get_filename_component(LMMS_TEST_NAME ${LMMS_TEST_SRC} NAME_WE) + + add_executable(${LMMS_TEST_NAME} $ ${LMMS_TEST_SRC}) + add_test(NAME ${LMMS_TEST_NAME} COMMAND ${LMMS_TEST_NAME}) + + # TODO CMake 3.12: Propagate usage requirements by linking to lmmsobjs + target_include_directories(${LMMS_TEST_NAME} PRIVATE $) + + target_link_libraries(${LMMS_TEST_NAME} PRIVATE + ${LMMS_REQUIRED_LIBS} + ${QT_LIBRARIES} + ${QT_QTTEST_LIBRARY} + ) + + target_compile_features(${LMMS_TEST_NAME} PRIVATE cxx_std_17) + target_compile_definitions(${LMMS_TEST_NAME} PRIVATE $) +endforeach() diff --git a/tests/QTestSuite.cpp b/tests/QTestSuite.cpp deleted file mode 100644 index a5a49fd20..000000000 --- a/tests/QTestSuite.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "QTestSuite.h" - -QList QTestSuite::m_suites; - -QTestSuite::QTestSuite(QObject *parent) : QObject(parent) -{ - m_suites << this; -} - -QTestSuite::~QTestSuite() -{ - m_suites.removeAll(this); -} - -QList QTestSuite::suites() -{ - return m_suites; -} - diff --git a/tests/QTestSuite.h b/tests/QTestSuite.h deleted file mode 100644 index 6cd27f5aa..000000000 --- a/tests/QTestSuite.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef QTESTSUITE_H -#define QTESTSUITE_H - -#include -#include -#include - -class QTestSuite : public QObject -{ - Q_OBJECT -public: - explicit QTestSuite(QObject *parent = 0); - ~QTestSuite() override; - - static QList suites(); - -private: - static QList m_suites; -}; - -#endif // QTESTSUITE_H diff --git a/tests/main.cpp b/tests/main.cpp deleted file mode 100644 index b95f211d4..000000000 --- a/tests/main.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "QTestSuite.h" - -#include - -#include - -#include "Engine.h" - -int main(int argc, char* argv[]) -{ - new QCoreApplication(argc, argv); - lmms::Engine::init(true); - - int numsuites = QTestSuite::suites().size(); - qDebug() << ">> Will run" << numsuites << "test suites"; - int failed = 0; - for (QTestSuite*& suite : QTestSuite::suites()) - { - if (QTest::qExec(suite, argc, argv) != 0) { ++failed; } - } - qDebug() << "<<" << failed << "out of"< +#include #include #include -#include "QTestSuite.h" - using lmms::ArrayVector; struct ShouldNotConstruct @@ -59,10 +59,9 @@ struct DestructorCheck bool* destructed; }; -class ArrayVectorTest : QTestSuite +class ArrayVectorTest : public QObject { Q_OBJECT - private slots: void defaultConstructorTest() { @@ -826,6 +825,7 @@ private slots: QVERIFY(!(e != v)); QVERIFY(g != v); } -} ArrayVectorTests; +}; +QTEST_GUILESS_MAIN(ArrayVectorTest) #include "ArrayVectorTest.moc" diff --git a/tests/src/core/AutomatableModelTest.cpp b/tests/src/core/AutomatableModelTest.cpp index 78b9069b5..6e8a28116 100644 --- a/tests/src/core/AutomatableModelTest.cpp +++ b/tests/src/core/AutomatableModelTest.cpp @@ -22,15 +22,16 @@ * */ -#include "QTestSuite.h" +#include #include "AutomatableModel.h" #include "ComboBoxModel.h" +#include "Engine.h" -class AutomatableModelTest : QTestSuite +class AutomatableModelTest : public QObject { Q_OBJECT - +public: bool m1Changed, m2Changed; void resetChanged() { m1Changed = m2Changed = false; } @@ -41,6 +42,19 @@ private slots: // helper slots private slots: // tests //! Test that upcast and exact casts work, //! but no downcast or any other casts + + void initTestCase() + { + using namespace lmms; + Engine::init(true); + } + + void cleanupTestCase() + { + using namespace lmms; + Engine::destroy(); + } + void CastTests() { using namespace lmms; @@ -100,6 +114,7 @@ private slots: // tests QVERIFY(m2.value()); QVERIFY(!m3.value()); } -} AutomatableModelTests; +}; +QTEST_GUILESS_MAIN(AutomatableModelTest) #include "AutomatableModelTest.moc" diff --git a/tests/src/core/MathTest.cpp b/tests/src/core/MathTest.cpp index 2b6404cfd..00694c44f 100644 --- a/tests/src/core/MathTest.cpp +++ b/tests/src/core/MathTest.cpp @@ -22,13 +22,13 @@ * */ -#include "QTestSuite.h" +#include +#include +#include #include "lmms_math.h" -#include - -class MathTest : QTestSuite +class MathTest : public QObject { Q_OBJECT private slots: @@ -48,6 +48,7 @@ private slots: QCOMPARE(numDigitsAsInt(900000000), 9); QCOMPARE(numDigitsAsInt(-900000000), 10); } -} MathTests; +}; +QTEST_GUILESS_MAIN(MathTest) #include "MathTest.moc" diff --git a/tests/src/core/ProjectVersionTest.cpp b/tests/src/core/ProjectVersionTest.cpp index 387d90056..03b689541 100644 --- a/tests/src/core/ProjectVersionTest.cpp +++ b/tests/src/core/ProjectVersionTest.cpp @@ -22,11 +22,11 @@ * */ -#include "QTestSuite.h" - #include "ProjectVersion.h" -class ProjectVersionTest : QTestSuite +#include + +class ProjectVersionTest : public QObject { Q_OBJECT private slots: @@ -75,6 +75,7 @@ private slots: //An identifier of the form "-x" is non-numeric, not negative QVERIFY(ProjectVersion("1.0.0-alpha.-1") > "1.0.0-alpha.1"); } -} ProjectVersionTests; +}; +QTEST_GUILESS_MAIN(ProjectVersionTest) #include "ProjectVersionTest.moc" diff --git a/tests/src/core/RelativePathsTest.cpp b/tests/src/core/RelativePathsTest.cpp index 3b5d023d0..089ab2e8a 100644 --- a/tests/src/core/RelativePathsTest.cpp +++ b/tests/src/core/RelativePathsTest.cpp @@ -22,15 +22,15 @@ * */ -#include "QTestSuite.h" +#include +#include +#include #include "ConfigManager.h" -#include "SampleBuffer.h" #include "PathUtil.h" +#include "SampleBuffer.h" -#include - -class RelativePathsTest : QTestSuite +class RelativePathsTest : public QObject { Q_OBJECT private slots: @@ -66,6 +66,7 @@ private slots: QCOMPARE(PathUtil::toAbsolute(""), empty); QCOMPARE(PathUtil::toShortestRelative(""), empty); } -} RelativePathTests; +}; +QTEST_GUILESS_MAIN(RelativePathsTest) #include "RelativePathsTest.moc" diff --git a/tests/src/tracks/AutomationTrackTest.cpp b/tests/src/tracks/AutomationTrackTest.cpp index f01508075..b4f6effd9 100644 --- a/tests/src/tracks/AutomationTrackTest.cpp +++ b/tests/src/tracks/AutomationTrackTest.cpp @@ -22,7 +22,7 @@ * */ -#include "QTestSuite.h" +#include #include "QCoreApplication" @@ -39,12 +39,20 @@ #include "Engine.h" #include "Song.h" -class AutomationTrackTest : QTestSuite +class AutomationTrackTest : public QObject { Q_OBJECT private slots: void initTestCase() { + using namespace lmms; + Engine::init(true); + } + + void cleanupTestCase() + { + using namespace lmms; + Engine::destroy(); } void testClipLinear() @@ -232,6 +240,7 @@ private slots: QCOMPARE(song->automatedValuesAt(0)[&model], 50.0f); } -} AutomationTrackTest; +}; +QTEST_GUILESS_MAIN(AutomationTrackTest) #include "AutomationTrackTest.moc" From 3343496c007ce34130e7c466e65d3df2a6ff35d8 Mon Sep 17 00:00:00 2001 From: szeli1 <143485814+szeli1@users.noreply.github.com> Date: Mon, 15 Jan 2024 23:20:59 +0100 Subject: [PATCH 076/191] Compressor plugin hideable controls (#7008) --- .../Compressor/CompressorControlDialog.cpp | 73 +++++++++++++++++++ plugins/Compressor/CompressorControlDialog.h | 4 + 2 files changed, 77 insertions(+) diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 2d04b690e..ab81c84ec 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -694,6 +694,79 @@ void CompressorControlDialog::drawGraph() } +void CompressorControlDialog::mouseDoubleClickEvent(QMouseEvent* event) +{ + setGuiVisibility(!m_guiVisibility); +} + + +void CompressorControlDialog::setGuiVisibility(bool isVisible) +{ + if (!isVisible) + { + m_rmsKnob->setVisible(isVisible); + m_rmsEnabledLabel->setVisible(isVisible); + + m_lookaheadLengthKnob->setVisible(isVisible); + m_lookaheadEnabledLabel->setVisible(isVisible); + + m_blendKnob->setVisible(isVisible); + m_blendEnabledLabel->setVisible(isVisible); + + m_ratioKnob->setVisible(isVisible); + m_ratioEnabledLabel->setVisible(isVisible); + } + else + { + m_rmsKnob->setVisible(!m_controls->m_peakmodeModel.value()); + m_rmsEnabledLabel->setVisible(!m_controls->m_peakmodeModel.value()); + + m_blendKnob->setVisible(m_controls->m_stereoLinkModel.value() == 4); + m_blendEnabledLabel->setVisible(m_controls->m_stereoLinkModel.value() == 4); + + m_lookaheadLengthKnob->setVisible(m_controls->m_lookaheadModel.value()); + m_lookaheadEnabledLabel->setVisible(m_controls->m_lookaheadModel.value()); + + m_ratioKnob->setVisible(!m_controls->m_limiterModel.value()); + m_ratioEnabledLabel->setVisible(!m_controls->m_limiterModel.value()); + } + m_controlsBoxLabel->setVisible(isVisible); + m_thresholdKnob->setVisible(isVisible); + m_attackKnob->setVisible(isVisible); + m_releaseKnob->setVisible(isVisible); + m_kneeKnob->setVisible(isVisible); + m_rangeKnob->setVisible(isVisible); + m_holdKnob->setVisible(isVisible); + m_inBalanceKnob->setVisible(isVisible); + m_outBalanceKnob->setVisible(isVisible); + m_stereoBalanceKnob->setVisible(isVisible); + m_tiltKnob->setVisible(isVisible); + m_tiltFreqKnob->setVisible(isVisible); + m_mixKnob->setVisible(isVisible); + m_autoAttackKnob->setVisible(isVisible); + m_autoReleaseKnob->setVisible(isVisible); + m_outFader->setVisible(isVisible); + m_inFader->setVisible(isVisible); + rmsButton->setVisible(isVisible); + peakButton->setVisible(isVisible); + rmsPeakGroup->setVisible(isVisible); + leftRightButton->setVisible(isVisible); + midSideButton->setVisible(isVisible); + compressButton->setVisible(isVisible); + limitButton->setVisible(isVisible); + unlinkedButton->setVisible(isVisible); + maximumButton->setVisible(isVisible); + averageButton->setVisible(isVisible); + minimumButton->setVisible(isVisible); + blendButton->setVisible(isVisible); + autoMakeupButton->setVisible(isVisible); + auditionButton->setVisible(isVisible); + feedbackButton->setVisible(isVisible); + lookaheadButton->setVisible(isVisible); + m_guiVisibility = isVisible; +} + + void CompressorControlDialog::resetCompressorView() { m_windowSizeX = size().width(); diff --git a/plugins/Compressor/CompressorControlDialog.h b/plugins/Compressor/CompressorControlDialog.h index 1324d7e26..a61482ad8 100755 --- a/plugins/Compressor/CompressorControlDialog.h +++ b/plugins/Compressor/CompressorControlDialog.h @@ -108,6 +108,8 @@ private: void drawKneePixmap2(); void drawMiscPixmap(); void drawGraph(); + void mouseDoubleClickEvent(QMouseEvent* event) override; + void setGuiVisibility(bool isVisible); QPainter m_p; @@ -214,6 +216,8 @@ private: PixmapButton * feedbackButton; PixmapButton * lookaheadButton; + bool m_guiVisibility = true; + QElapsedTimer m_timeElapsed; int m_timeSinceLastUpdate = 0; From 1e6a902dd32e9bf4dbf57e17913bdd67acecfaad Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 17 Jan 2024 16:53:32 +0100 Subject: [PATCH 077/191] Fix most of the track cloning for MIDI ports (#7066) Fixes most of the problem that the MIDI port information is not cloned when a track is cloned. The bug was caused because cloning is implemented via serialization and deserialization of the Track. The previous code only serialized the MIDI port if `Engine::getSong()->isSavingProject()` is true. However, when we are cloning the statement will be `false` because we are not saving the song. Therefore the MIDI port's state was not saved and the clone was initialized with the default values. The fix is to serialize the MIDI port in the following cases: * We are not in song saving mode, i.e. we are in the state that this issue is about, e.g. cloning. * We save a song and the MIDI connections are not discarded. Using boolean algebra these conditions can be simplified as seen in the changed statement. --- src/tracks/InstrumentTrack.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 4b00e0d79..cdd360e70 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -859,9 +859,11 @@ void InstrumentTrack::saveTrackSpecificSettings( QDomDocument& doc, QDomElement m_noteStacking.saveState( doc, thisElement ); m_arpeggio.saveState( doc, thisElement ); - // Don't save midi port info if the user chose to. - if (Engine::getSong()->isSavingProject() - && !Engine::getSong()->getSaveOptions().discardMIDIConnections.value()) + // Save the midi port info if we are not in song saving mode, e.g. in + // track cloning mode or if we are in song saving mode and the user + // has chosen to discard the MIDI connections. + if (!Engine::getSong()->isSavingProject() || + !Engine::getSong()->getSaveOptions().discardMIDIConnections.value()) { // Don't save auto assigned midi device connection bool hasAuto = m_hasAutoMidiDev; From 8e8f68552ff1bf4811c31df841db6f5af4d4da01 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 21 Jan 2024 12:25:00 +0100 Subject: [PATCH 078/191] Disentangle and clean the classes of the Audio File Processor (#7069) ## Extract views Extract the classes `AudioFileProcessorWaveView` and `AudioFileProcessorView` into their own files. Reformat the new classes by removing unnecessary whitespace and underscores. Add spaces to if-statements. Cleanup the includes. ## Remove friend relationship Remove the friend relationship between `AudioFileProcessor` and `AudioFileProcessorView`. Introduce getters for entities that the view is interested in. --- .../AudioFileProcessor/AudioFileProcessor.cpp | 852 +----------------- .../AudioFileProcessor/AudioFileProcessor.h | 216 +---- .../AudioFileProcessorView.cpp | 286 ++++++ .../AudioFileProcessorView.h | 85 ++ .../AudioFileProcessorWaveView.cpp | 540 +++++++++++ .../AudioFileProcessorWaveView.h | 181 ++++ plugins/AudioFileProcessor/CMakeLists.txt | 2 +- 7 files changed, 1110 insertions(+), 1052 deletions(-) create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorView.cpp create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorView.h create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorWaveView.h diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index f50a6df68..7cc5ee3fa 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -23,37 +23,21 @@ */ #include "AudioFileProcessor.h" +#include "AudioFileProcessorView.h" -#include - -#include -#include -#include -#include - -#include "AudioEngine.h" -#include "ComboBox.h" -#include "ConfigManager.h" -#include "DataFile.h" -#include "Engine.h" -#include "gui_templates.h" #include "InstrumentTrack.h" -#include "NotePlayHandle.h" #include "PathUtil.h" -#include "PixmapButton.h" #include "SampleLoader.h" -#include "SampleWaveform.h" #include "Song.h" -#include "StringPairDrag.h" -#include "Clipboard.h" -#include "embed.h" #include "plugin_export.h" +#include + + namespace lmms { - extern "C" { @@ -441,834 +425,6 @@ void AudioFileProcessor::pointChanged() } - - -namespace gui -{ - - - - -AudioFileProcessorView::AudioFileProcessorView( Instrument * _instrument, - QWidget * _parent ) : - InstrumentViewFixedSize( _instrument, _parent ) -{ - m_openAudioFileButton = new PixmapButton( this ); - m_openAudioFileButton->setCursor( QCursor( Qt::PointingHandCursor ) ); - m_openAudioFileButton->move( 227, 72 ); - m_openAudioFileButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "select_file" ) ); - m_openAudioFileButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "select_file" ) ); - connect( m_openAudioFileButton, SIGNAL( clicked() ), - this, SLOT( openAudioFile() ) ); - m_openAudioFileButton->setToolTip(tr("Open sample")); - - m_reverseButton = new PixmapButton( this ); - m_reverseButton->setCheckable( true ); - m_reverseButton->move( 164, 105 ); - m_reverseButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "reverse_on" ) ); - m_reverseButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "reverse_off" ) ); - m_reverseButton->setToolTip(tr("Reverse sample")); - -// loop button group - - auto m_loopOffButton = new PixmapButton(this); - m_loopOffButton->setCheckable( true ); - m_loopOffButton->move( 190, 105 ); - m_loopOffButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_off_on" ) ); - m_loopOffButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_off_off" ) ); - m_loopOffButton->setToolTip(tr("Disable loop")); - - auto m_loopOnButton = new PixmapButton(this); - m_loopOnButton->setCheckable( true ); - m_loopOnButton->move( 190, 124 ); - m_loopOnButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_on_on" ) ); - m_loopOnButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_on_off" ) ); - m_loopOnButton->setToolTip(tr("Enable loop")); - - auto m_loopPingPongButton = new PixmapButton(this); - m_loopPingPongButton->setCheckable( true ); - m_loopPingPongButton->move( 216, 124 ); - m_loopPingPongButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_pingpong_on" ) ); - m_loopPingPongButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_pingpong_off" ) ); - m_loopPingPongButton->setToolTip(tr("Enable ping-pong loop")); - - m_loopGroup = new automatableButtonGroup( this ); - m_loopGroup->addButton( m_loopOffButton ); - m_loopGroup->addButton( m_loopOnButton ); - m_loopGroup->addButton( m_loopPingPongButton ); - - m_stutterButton = new PixmapButton( this ); - m_stutterButton->setCheckable( true ); - m_stutterButton->move( 164, 124 ); - m_stutterButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "stutter_on" ) ); - m_stutterButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "stutter_off" ) ); - m_stutterButton->setToolTip( - tr( "Continue sample playback across notes" ) ); - - m_ampKnob = new Knob( KnobType::Bright26, this ); - m_ampKnob->setVolumeKnob( true ); - m_ampKnob->move( 5, 108 ); - m_ampKnob->setHintText( tr( "Amplify:" ), "%" ); - - m_startKnob = new AudioFileProcessorWaveView::knob( this ); - m_startKnob->move( 45, 108 ); - m_startKnob->setHintText( tr( "Start point:" ), "" ); - - m_endKnob = new AudioFileProcessorWaveView::knob( this ); - m_endKnob->move( 125, 108 ); - m_endKnob->setHintText( tr( "End point:" ), "" ); - - m_loopKnob = new AudioFileProcessorWaveView::knob( this ); - m_loopKnob->move( 85, 108 ); - m_loopKnob->setHintText( tr( "Loopback point:" ), "" ); - -// interpolation selector - m_interpBox = new ComboBox( this ); - m_interpBox->setGeometry( 142, 62, 82, ComboBox::DEFAULT_HEIGHT ); - m_interpBox->setFont( pointSize<8>( m_interpBox->font() ) ); - -// wavegraph - m_waveView = 0; - newWaveView(); - - connect( castModel(), SIGNAL( isPlaying( lmms::f_cnt_t ) ), - m_waveView, SLOT( isPlaying( lmms::f_cnt_t ) ) ); - - qRegisterMetaType( "lmms::f_cnt_t" ); - - setAcceptDrops( true ); -} - - - - - - - - -void AudioFileProcessorView::dragEnterEvent( QDragEnterEvent * _dee ) -{ - // For mimeType() and MimeType enum class - using namespace Clipboard; - - if( _dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) - { - QString txt = _dee->mimeData()->data( - mimeType( MimeType::StringPair ) ); - if( txt.section( ':', 0, 0 ) == QString( "clip_%1" ).arg( - static_cast(Track::Type::Sample) ) ) - { - _dee->acceptProposedAction(); - } - else if( txt.section( ':', 0, 0 ) == "samplefile" ) - { - _dee->acceptProposedAction(); - } - else - { - _dee->ignore(); - } - } - else - { - _dee->ignore(); - } -} - - - - -void AudioFileProcessorView::newWaveView() -{ - if ( m_waveView ) - { - delete m_waveView; - m_waveView = 0; - } - m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->m_sample); - m_waveView->move( 2, 172 ); - m_waveView->setKnobs( - dynamic_cast( m_startKnob ), - dynamic_cast( m_endKnob ), - dynamic_cast( m_loopKnob ) ); - m_waveView->show(); -} - - - - -void AudioFileProcessorView::dropEvent( QDropEvent * _de ) -{ - const auto type = StringPairDrag::decodeKey(_de); - const auto value = StringPairDrag::decodeValue(_de); - - if (type == "samplefile") { castModel()->setAudioFile(value); } - else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) - { - DataFile dataFile(value.toUtf8()); - castModel()->setAudioFile(dataFile.content().firstChild().toElement().attribute("src")); - } - else - { - _de->ignore(); - return; - } - - m_waveView->updateSampleRange(); - Engine::getSong()->setModified(); - _de->accept(); -} - -void AudioFileProcessorView::paintEvent( QPaintEvent * ) -{ - QPainter p( this ); - - static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); - p.drawPixmap(0, 0, s_artwork); - - auto a = castModel(); - - QString file_name = ""; - - int idx = a->m_sample.sampleFile().length(); - - p.setFont( pointSize<8>( font() ) ); - - QFontMetrics fm( p.font() ); - - // simple algorithm for creating a text from the filename that - // matches in the white rectangle - while( idx > 0 && - fm.size( Qt::TextSingleLine, file_name + "..." ).width() < 210 ) - { - file_name = a->m_sample.sampleFile()[--idx] + file_name; - } - - if( idx > 0 ) - { - file_name = "..." + file_name; - } - - p.setPen( QColor( 255, 255, 255 ) ); - p.drawText( 8, 99, file_name ); -} - - - - -void AudioFileProcessorView::sampleUpdated() -{ - m_waveView->updateSampleRange(); - m_waveView->update(); - update(); -} - - - - - -void AudioFileProcessorView::openAudioFile() -{ - QString af = SampleLoader::openAudioFile(); - if (af.isEmpty()) { return; } - - castModel()->setAudioFile(af); - Engine::getSong()->setModified(); - m_waveView->updateSampleRange(); -} - - - - -void AudioFileProcessorView::modelChanged() -{ - auto a = castModel(); - connect(a, &AudioFileProcessor::sampleUpdated, this, &AudioFileProcessorView::sampleUpdated); - m_ampKnob->setModel( &a->m_ampModel ); - m_startKnob->setModel( &a->m_startPointModel ); - m_endKnob->setModel( &a->m_endPointModel ); - m_loopKnob->setModel( &a->m_loopPointModel ); - m_reverseButton->setModel( &a->m_reverseModel ); - m_loopGroup->setModel( &a->m_loopModel ); - m_stutterButton->setModel( &a->m_stutterModel ); - m_interpBox->setModel( &a->m_interpolationModel ); - sampleUpdated(); -} - - - - -void AudioFileProcessorWaveView::updateSampleRange() -{ - if (m_sample->sampleSize() > 1) - { - const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; - m_from = qMax(0, m_sample->startFrame() - marging); - m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); - } -} - -AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf) : - QWidget( _parent ), - m_sample(buf), - m_graph( QPixmap( _w - 2 * s_padding, _h - 2 * s_padding ) ), - m_from( 0 ), - m_to(m_sample->sampleSize()), - m_last_from( 0 ), - m_last_to( 0 ), - m_last_amp( 0 ), - m_startKnob( 0 ), - m_endKnob( 0 ), - m_loopKnob( 0 ), - m_isDragging( false ), - m_reversed( false ), - m_framesPlayed( 0 ), - m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()) -{ - setFixedSize( _w, _h ); - setMouseTracking( true ); - - updateSampleRange(); - - m_graph.fill( Qt::transparent ); - update(); - updateCursor(); -} - - - - -void AudioFileProcessorWaveView::isPlaying( f_cnt_t _current_frame ) -{ - m_framesPlayed = _current_frame; - update(); -} - - - - -void AudioFileProcessorWaveView::enterEvent( QEvent * _e ) -{ - updateCursor(); -} - - - - -void AudioFileProcessorWaveView::leaveEvent( QEvent * _e ) -{ - updateCursor(); -} - - - - -void AudioFileProcessorWaveView::mousePressEvent( QMouseEvent * _me ) -{ - m_isDragging = true; - m_draggingLastPoint = _me->pos(); - - const int x = _me->x(); - - const int start_dist = qAbs( m_startFrameX - x ); - const int end_dist = qAbs( m_endFrameX - x ); - const int loop_dist = qAbs( m_loopFrameX - x ); - - DraggingType dt = DraggingType::SampleLoop; int md = loop_dist; - if( start_dist < loop_dist ) { dt = DraggingType::SampleStart; md = start_dist; } - else if( end_dist < loop_dist ) { dt = DraggingType::SampleEnd; md = end_dist; } - - if( md < 4 ) - { - m_draggingType = dt; - } - else - { - m_draggingType = DraggingType::Wave; - updateCursor(_me); - } -} - - - - -void AudioFileProcessorWaveView::mouseReleaseEvent( QMouseEvent * _me ) -{ - m_isDragging = false; - if( m_draggingType == DraggingType::Wave ) - { - updateCursor(_me); - } -} - - - - -void AudioFileProcessorWaveView::mouseMoveEvent( QMouseEvent * _me ) -{ - if( ! m_isDragging ) - { - updateCursor(_me); - return; - } - - const int step = _me->x() - m_draggingLastPoint.x(); - switch( m_draggingType ) - { - case DraggingType::SampleStart: - slideSamplePointByPx( Point::Start, step ); - break; - case DraggingType::SampleEnd: - slideSamplePointByPx( Point::End, step ); - break; - case DraggingType::SampleLoop: - slideSamplePointByPx( Point::Loop, step ); - break; - case DraggingType::Wave: - default: - if( qAbs( _me->y() - m_draggingLastPoint.y() ) - < 2 * qAbs( _me->x() - m_draggingLastPoint.x() ) ) - { - slide( step ); - } - else - { - zoom( _me->y() < m_draggingLastPoint.y() ); - } - } - - m_draggingLastPoint = _me->pos(); - update(); -} - - - - -void AudioFileProcessorWaveView::wheelEvent( QWheelEvent * _we ) -{ - zoom( _we->angleDelta().y() > 0 ); - update(); -} - - - - -void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) -{ - QPainter p( this ); - - p.drawPixmap( s_padding, s_padding, m_graph ); - - const QRect graph_rect( s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding ); - const f_cnt_t frames = m_to - m_from; - m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * - double( graph_rect.width() ) / frames; - m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * - double( graph_rect.width() ) / frames; - m_loopFrameX = graph_rect.x() + (m_sample->loopStartFrame() - m_from) * - double( graph_rect.width() ) / frames; - const int played_width_px = ( m_framesPlayed - m_from ) * - double( graph_rect.width() ) / frames; - - // loop point line - p.setPen( QColor( 0x7F, 0xFF, 0xFF ) ); //TODO: put into a qproperty - p.drawLine( m_loopFrameX, graph_rect.y(), - m_loopFrameX, - graph_rect.height() + graph_rect.y() ); - - // start/end lines - p.setPen( QColor( 0xFF, 0xFF, 0xFF ) ); //TODO: put into a qproperty - p.drawLine( m_startFrameX, graph_rect.y(), - m_startFrameX, - graph_rect.height() + graph_rect.y() ); - p.drawLine( m_endFrameX, graph_rect.y(), - m_endFrameX, - graph_rect.height() + graph_rect.y() ); - - - if( m_endFrameX - m_startFrameX > 2 ) - { - p.fillRect( - m_startFrameX + 1, - graph_rect.y(), - m_endFrameX - m_startFrameX - 1, - graph_rect.height() + graph_rect.y(), - QColor( 95, 175, 255, 50 ) //TODO: put into a qproperty - ); - if( m_endFrameX - m_loopFrameX > 2 ) - p.fillRect( - m_loopFrameX + 1, - graph_rect.y(), - m_endFrameX - m_loopFrameX - 1, - graph_rect.height() + graph_rect.y(), - QColor( 95, 205, 255, 65 ) //TODO: put into a qproperty - ); - - if( m_framesPlayed && m_animation) - { - QLinearGradient g( m_startFrameX, 0, played_width_px, 0 ); - const QColor c( 0, 120, 255, 180 ); //TODO: put into a qproperty - g.setColorAt( 0, Qt::transparent ); - g.setColorAt( 0.8, c ); - g.setColorAt( 1, c ); - p.fillRect( - m_startFrameX + 1, - graph_rect.y(), - played_width_px - ( m_startFrameX + 1 ), - graph_rect.height() + graph_rect.y(), - g - ); - p.setPen( QColor( 255, 255, 255 ) ); //TODO: put into a qproperty - p.drawLine( - played_width_px, - graph_rect.y(), - played_width_px, - graph_rect.height() + graph_rect.y() - ); - m_framesPlayed = 0; - } - } - - QLinearGradient g( 0, 0, width() * 0.7, 0 ); - const QColor c( 16, 111, 170, 180 ); - g.setColorAt( 0, c ); - g.setColorAt( 0.4, c ); - g.setColorAt( 1, Qt::transparent ); - p.fillRect( s_padding, s_padding, m_graph.width(), 14, g ); - - p.setPen( QColor( 255, 255, 255 ) ); - p.setFont( pointSize<8>( font() ) ); - - QString length_text; - const int length = m_sample->sampleDuration().count(); - - if( length > 20000 ) - { - length_text = QString::number( length / 1000 ) + "s"; - } - else if( length > 2000 ) - { - length_text = QString::number( ( length / 100 ) / 10.0 ) + "s"; - } - else - { - length_text = QString::number( length ) + "ms"; - } - - p.drawText( - s_padding + 2, - s_padding + 10, - tr( "Sample length:" ) + " " + length_text - ); -} - - - - -void AudioFileProcessorWaveView::updateGraph() -{ - if( m_to == 1 ) - { - m_to = m_sample->sampleSize() * 0.7; - slideSamplePointToFrames( Point::End, m_to * 0.7 ); - } - - if (m_from > m_sample->startFrame()) - { - m_from = m_sample->startFrame(); - } - - if (m_to < m_sample->endFrame()) - { - m_to = m_sample->endFrame(); - } - - if (m_sample->reversed() != m_reversed) - { - reverse(); - } - else if (m_last_from == m_from && m_last_to == m_to && m_sample->amplification() == m_last_amp) - { - return; - } - - m_last_from = m_from; - m_last_to = m_to; - m_last_amp = m_sample->amplification(); - - m_graph.fill( Qt::transparent ); - QPainter p( &m_graph ); - p.setPen( QColor( 255, 255, 255 ) ); - - const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; - const auto waveform = SampleWaveform::Parameters{ - m_sample->data() + m_from, static_cast(m_to - m_from), m_sample->amplification(), m_sample->reversed()}; - SampleWaveform::visualize(waveform, p, rect); -} - - - - -void AudioFileProcessorWaveView::zoom( const bool _out ) -{ - const f_cnt_t start = m_sample->startFrame(); - const f_cnt_t end = m_sample->endFrame(); - const f_cnt_t frames = m_sample->sampleSize(); - const f_cnt_t d_from = start - m_from; - const f_cnt_t d_to = m_to - end; - - const f_cnt_t step = qMax( 1, qMax( d_from, d_to ) / 10 ); - const f_cnt_t step_from = ( _out ? - step : step ); - const f_cnt_t step_to = ( _out ? step : - step ); - - const double comp_ratio = double( qMin( d_from, d_to ) ) - / qMax( 1, qMax( d_from, d_to ) ); - - f_cnt_t new_from; - f_cnt_t new_to; - - if( ( _out && d_from < d_to ) || ( ! _out && d_to < d_from ) ) - { - new_from = qBound( 0, m_from + step_from, start ); - new_to = qBound( - end, - m_to + f_cnt_t( step_to * ( new_from == m_from ? 1 : comp_ratio ) ), - frames - ); - } - else - { - new_to = qBound( end, m_to + step_to, frames ); - new_from = qBound( - 0, - m_from + f_cnt_t( step_from * ( new_to == m_to ? 1 : comp_ratio ) ), - start - ); - } - - if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) - { - m_from = new_from; - m_to = new_to; - } -} - - - - -void AudioFileProcessorWaveView::slide( int _px ) -{ - const double fact = qAbs( double( _px ) / width() ); - f_cnt_t step = ( m_to - m_from ) * fact; - if( _px > 0 ) - { - step = -step; - } - - f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; - f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; - - step = qAbs( step_from ) < qAbs( step_to ) ? step_from : step_to; - - m_from += step; - m_to += step; - slideSampleByFrames( step ); -} - - - - -void AudioFileProcessorWaveView::setKnobs( knob * _start, knob * _end, knob * _loop ) -{ - m_startKnob = _start; - m_endKnob = _end; - m_loopKnob = _loop; - - m_startKnob->setWaveView( this ); - m_startKnob->setRelatedKnob( m_endKnob ); - - m_endKnob->setWaveView( this ); - m_endKnob->setRelatedKnob( m_startKnob ); - - m_loopKnob->setWaveView( this ); -} - - - - -void AudioFileProcessorWaveView::slideSamplePointByPx( Point _point, int _px ) -{ - slideSamplePointByFrames( - _point, - f_cnt_t( ( double( _px ) / width() ) * ( m_to - m_from ) ) - ); -} - - - - -void AudioFileProcessorWaveView::slideSamplePointByFrames( Point _point, f_cnt_t _frames, bool _slide_to ) -{ - knob * a_knob = m_startKnob; - switch( _point ) - { - case Point::End: - a_knob = m_endKnob; - break; - case Point::Loop: - a_knob = m_loopKnob; - break; - case Point::Start: - break; - } - if( a_knob == nullptr ) - { - return; - } - else - { - const double v = static_cast(_frames) / m_sample->sampleSize(); - if( _slide_to ) - { - a_knob->slideTo( v ); - } - else - { - a_knob->slideBy( v ); - } - } -} - - - - -void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) -{ - if (m_sample->sampleSize() <= 1) - { - return; - } - const double v = static_cast( _frames ) / m_sample->sampleSize(); - // update knobs in the right order - // to avoid them clamping each other - if (v < 0) - { - m_startKnob->slideBy(v, false); - m_loopKnob->slideBy(v, false); - m_endKnob->slideBy(v, false); - } - else - { - m_endKnob->slideBy(v, false); - m_loopKnob->slideBy(v, false); - m_startKnob->slideBy(v, false); - } -} - - - - -void AudioFileProcessorWaveView::reverse() -{ - slideSampleByFrames( - m_sample->sampleSize() - - m_sample->endFrame() - - m_sample->startFrame() - ); - - const f_cnt_t from = m_from; - m_from = m_sample->sampleSize() - m_to; - m_to = m_sample->sampleSize() - from; - - m_reversed = ! m_reversed; -} - - - -void AudioFileProcessorWaveView::updateCursor( QMouseEvent * _me ) -{ - bool const waveIsDragged = m_isDragging && (m_draggingType == DraggingType::Wave); - bool const pointerCloseToStartEndOrLoop = (_me != nullptr ) && - ( isCloseTo( _me->x(), m_startFrameX ) || - isCloseTo( _me->x(), m_endFrameX ) || - isCloseTo( _me->x(), m_loopFrameX ) ); - - if( !m_isDragging && pointerCloseToStartEndOrLoop) - setCursor(Qt::SizeHorCursor); - else if( waveIsDragged ) - setCursor(Qt::ClosedHandCursor); - else - setCursor(Qt::OpenHandCursor); -} - - - - -void AudioFileProcessorWaveView::knob::slideTo( double _v, bool _check_bound ) -{ - if( _check_bound && ! checkBound( _v ) ) - { - return; - } - model()->setValue( _v ); - emit sliderMoved( model()->value() ); -} - - - - -float AudioFileProcessorWaveView::knob::getValue( const QPoint & _p ) -{ - const double dec_fact = ! m_waveView ? 1 : - static_cast(m_waveView->m_to - m_waveView->m_from) / m_waveView->m_sample->sampleSize(); - const float inc = Knob::getValue( _p ) * dec_fact; - - return inc; -} - - - - -bool AudioFileProcessorWaveView::knob::checkBound( double _v ) const -{ - if( ! m_relatedKnob || ! m_waveView ) - { - return true; - } - - if( ( m_relatedKnob->model()->value() - _v > 0 ) != - ( m_relatedKnob->model()->value() - model()->value() >= 0 ) ) - return false; - - const double d1 = qAbs( m_relatedKnob->model()->value() - model()->value() ) - * (m_waveView->m_sample->sampleSize()) - / m_waveView->m_sample->sampleRate(); - - const double d2 = qAbs( m_relatedKnob->model()->value() - _v ) - * (m_waveView->m_sample->sampleSize()) - / m_waveView->m_sample->sampleRate(); - - return d1 < d2 || d2 > 0.005; -} - - -} // namespace gui - - - - extern "C" { diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 80a40c56f..7ade1ec4f 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -26,31 +26,17 @@ #ifndef LMMS_AUDIO_FILE_PROCESSOR_H #define LMMS_AUDIO_FILE_PROCESSOR_H -#include +#include "AutomatableModel.h" #include "ComboBoxModel.h" + #include "Instrument.h" -#include "InstrumentView.h" #include "Sample.h" -#include "SampleBuffer.h" -#include "Knob.h" namespace lmms { -namespace gui -{ -class automatableButtonGroup; -class PluginView; -class InstrumentViewFixedSize; -class Knob; -class PixmapButton; -class ComboBox; -class AudioFileProcessorView; -} - - class AudioFileProcessor : public Instrument { Q_OBJECT @@ -77,6 +63,17 @@ public: gui::PluginView* instantiateView( QWidget * _parent ) override; + Sample const & sample() const { return m_sample; } + + FloatModel & ampModel() { return m_ampModel; } + FloatModel & startPointModel() { return m_startPointModel; } + FloatModel & endPointModel() { return m_endPointModel; } + FloatModel & loopPointModel() { return m_loopPointModel; } + BoolModel & reverseModel() { return m_reverseModel; } + IntModel & loopModel() { return m_loopModel; } + BoolModel & stutterModel() { return m_stutterModel; } + ComboBoxModel & interpolationModel() { return m_interpolationModel; } + public slots: void setAudioFile(const QString& _audio_file, bool _rename = true); @@ -109,195 +106,8 @@ private: f_cnt_t m_nextPlayStartPoint; bool m_nextPlayBackwards; - - friend class gui::AudioFileProcessorView; - } ; - -namespace gui -{ - -class AudioFileProcessorWaveView; - - -class AudioFileProcessorView : public gui::InstrumentViewFixedSize -{ - Q_OBJECT -public: - AudioFileProcessorView( Instrument * _instrument, QWidget * _parent ); - virtual ~AudioFileProcessorView() = default; - - void newWaveView(); -protected slots: - void sampleUpdated(); - void openAudioFile(); - - -protected: - virtual void dragEnterEvent( QDragEnterEvent * _dee ); - virtual void dropEvent( QDropEvent * _de ); - virtual void paintEvent( QPaintEvent * ); - - -private: - virtual void modelChanged(); - - - AudioFileProcessorWaveView * m_waveView; - Knob * m_ampKnob; - Knob * m_startKnob; - Knob * m_endKnob; - Knob * m_loopKnob; - - gui::PixmapButton * m_openAudioFileButton; - PixmapButton * m_reverseButton; - automatableButtonGroup * m_loopGroup; - PixmapButton * m_stutterButton; - ComboBox * m_interpBox; - -} ; - - - -class AudioFileProcessorWaveView : public QWidget -{ - Q_OBJECT -protected: - virtual void enterEvent( QEvent * _e ); - virtual void leaveEvent( QEvent * _e ); - virtual void mousePressEvent( QMouseEvent * _me ); - virtual void mouseReleaseEvent( QMouseEvent * _me ); - virtual void mouseMoveEvent( QMouseEvent * _me ); - virtual void wheelEvent( QWheelEvent * _we ); - virtual void paintEvent( QPaintEvent * _pe ); - - -public: - enum class Point - { - Start, - End, - Loop - } ; - - class knob : public Knob - { - const AudioFileProcessorWaveView * m_waveView; - const Knob * m_relatedKnob; - - - public: - knob( QWidget * _parent ) : - Knob( KnobType::Bright26, _parent ), - m_waveView( 0 ), - m_relatedKnob( 0 ) - { - setFixedSize( 37, 47 ); - } - - void setWaveView( const AudioFileProcessorWaveView * _wv ) - { - m_waveView = _wv; - } - - void setRelatedKnob( const Knob * _knob ) - { - m_relatedKnob = _knob; - } - - void slideBy( double _v, bool _check_bound = true ) - { - slideTo( model()->value() + _v, _check_bound ); - } - - void slideTo( double _v, bool _check_bound = true ); - - - protected: - float getValue( const QPoint & _p ); - - - private: - bool checkBound( double _v ) const; - } ; - - -public slots: - void update() - { - updateGraph(); - QWidget::update(); - } - - void isPlaying( lmms::f_cnt_t _current_frame ); - - -private: - static const int s_padding = 2; - - enum class DraggingType - { - Wave, - SampleStart, - SampleEnd, - SampleLoop - } ; - - Sample* m_sample; - QPixmap m_graph; - f_cnt_t m_from; - f_cnt_t m_to; - f_cnt_t m_last_from; - f_cnt_t m_last_to; - float m_last_amp; - knob * m_startKnob; - knob * m_endKnob; - knob * m_loopKnob; - f_cnt_t m_startFrameX; - f_cnt_t m_endFrameX; - f_cnt_t m_loopFrameX; - bool m_isDragging; - QPoint m_draggingLastPoint; - DraggingType m_draggingType; - bool m_reversed; - f_cnt_t m_framesPlayed; - bool m_animation; - - friend class AudioFileProcessorView; - -public: - AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf); - void setKnobs(knob *_start, knob *_end, knob *_loop ); - - - void updateSampleRange(); -private: - void zoom( const bool _out = false ); - void slide( int _px ); - void slideSamplePointByPx( Point _point, int _px ); - void slideSamplePointByFrames( Point _point, f_cnt_t _frames, bool _slide_to = false ); - void slideSampleByFrames( f_cnt_t _frames ); - - void slideSamplePointToFrames( Point _point, f_cnt_t _frames ) - { - slideSamplePointByFrames( _point, _frames, true ); - } - - void updateGraph(); - void reverse(); - void updateCursor( QMouseEvent * _me = nullptr ); - - static bool isCloseTo( int _a, int _b ) - { - return qAbs( _a - _b ) < 4; - } - -} ; - - -} // namespace gui - } // namespace lmms #endif // LMMS_AUDIO_FILE_PROCESSOR_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp new file mode 100644 index 000000000..43882222f --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp @@ -0,0 +1,286 @@ +/* + * AudioFileProcessor.cpp - instrument for using audio files + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "AudioFileProcessorView.h" + +#include "AudioFileProcessor.h" +#include "AudioFileProcessorWaveView.h" + +#include + +#include "ComboBox.h" +#include "DataFile.h" +#include "gui_templates.h" +#include "PixmapButton.h" +#include "SampleLoader.h" +#include "Song.h" +#include "StringPairDrag.h" +#include "Track.h" +#include "Clipboard.h" + + +namespace lmms +{ + +namespace gui +{ + +AudioFileProcessorView::AudioFileProcessorView(Instrument* instrument, + QWidget* parent) : + InstrumentViewFixedSize(instrument, parent) +{ + m_openAudioFileButton = new PixmapButton(this); + m_openAudioFileButton->setCursor(QCursor(Qt::PointingHandCursor)); + m_openAudioFileButton->move(227, 72); + m_openAudioFileButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "select_file")); + m_openAudioFileButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "select_file")); + connect(m_openAudioFileButton, SIGNAL(clicked()), + this, SLOT(openAudioFile())); + m_openAudioFileButton->setToolTip(tr("Open sample")); + + m_reverseButton = new PixmapButton(this); + m_reverseButton->setCheckable(true); + m_reverseButton->move(164, 105); + m_reverseButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "reverse_on")); + m_reverseButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "reverse_off")); + m_reverseButton->setToolTip(tr("Reverse sample")); + +// loop button group + + auto m_loopOffButton = new PixmapButton(this); + m_loopOffButton->setCheckable(true); + m_loopOffButton->move(190, 105); + m_loopOffButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_off_on")); + m_loopOffButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_off_off")); + m_loopOffButton->setToolTip(tr("Disable loop")); + + auto m_loopOnButton = new PixmapButton(this); + m_loopOnButton->setCheckable(true); + m_loopOnButton->move(190, 124); + m_loopOnButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_on_on")); + m_loopOnButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_on_off")); + m_loopOnButton->setToolTip(tr("Enable loop")); + + auto m_loopPingPongButton = new PixmapButton(this); + m_loopPingPongButton->setCheckable(true); + m_loopPingPongButton->move(216, 124); + m_loopPingPongButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_pingpong_on")); + m_loopPingPongButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_pingpong_off")); + m_loopPingPongButton->setToolTip(tr("Enable ping-pong loop")); + + m_loopGroup = new automatableButtonGroup(this); + m_loopGroup->addButton(m_loopOffButton); + m_loopGroup->addButton(m_loopOnButton); + m_loopGroup->addButton(m_loopPingPongButton); + + m_stutterButton = new PixmapButton(this); + m_stutterButton->setCheckable(true); + m_stutterButton->move(164, 124); + m_stutterButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "stutter_on")); + m_stutterButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "stutter_off")); + m_stutterButton->setToolTip( + tr("Continue sample playback across notes")); + + m_ampKnob = new Knob(KnobType::Bright26, this); + m_ampKnob->setVolumeKnob(true); + m_ampKnob->move(5, 108); + m_ampKnob->setHintText(tr("Amplify:"), "%"); + + m_startKnob = new AudioFileProcessorWaveView::knob(this); + m_startKnob->move(45, 108); + m_startKnob->setHintText(tr("Start point:"), ""); + + m_endKnob = new AudioFileProcessorWaveView::knob(this); + m_endKnob->move(125, 108); + m_endKnob->setHintText(tr("End point:"), ""); + + m_loopKnob = new AudioFileProcessorWaveView::knob(this); + m_loopKnob->move(85, 108); + m_loopKnob->setHintText(tr("Loopback point:"), ""); + +// interpolation selector + m_interpBox = new ComboBox(this); + m_interpBox->setGeometry(142, 62, 82, ComboBox::DEFAULT_HEIGHT); + m_interpBox->setFont(pointSize<8>(m_interpBox->font())); + +// wavegraph + m_waveView = 0; + newWaveView(); + + connect(castModel(), SIGNAL(isPlaying(lmms::f_cnt_t)), + m_waveView, SLOT(isPlaying(lmms::f_cnt_t))); + + qRegisterMetaType("lmms::f_cnt_t"); + + setAcceptDrops(true); +} + +void AudioFileProcessorView::dragEnterEvent(QDragEnterEvent* dee) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + if (dee->mimeData()->hasFormat(mimeType(MimeType::StringPair))) + { + QString txt = dee->mimeData()->data( + mimeType(MimeType::StringPair)); + if (txt.section(':', 0, 0) == QString("clip_%1").arg( + static_cast(Track::Type::Sample))) + { + dee->acceptProposedAction(); + } + else if (txt.section(':', 0, 0) == "samplefile") + { + dee->acceptProposedAction(); + } + else + { + dee->ignore(); + } + } + else + { + dee->ignore(); + } +} + +void AudioFileProcessorView::newWaveView() +{ + if (m_waveView) + { + delete m_waveView; + m_waveView = 0; + } + m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->sample()); + m_waveView->move(2, 172); + m_waveView->setKnobs( + dynamic_cast(m_startKnob), + dynamic_cast(m_endKnob), + dynamic_cast(m_loopKnob)); + m_waveView->show(); +} + +void AudioFileProcessorView::dropEvent(QDropEvent* de) +{ + const auto type = StringPairDrag::decodeKey(de); + const auto value = StringPairDrag::decodeValue(de); + + if (type == "samplefile") { castModel()->setAudioFile(value); } + else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) + { + DataFile dataFile(value.toUtf8()); + castModel()->setAudioFile(dataFile.content().firstChild().toElement().attribute("src")); + } + else + { + de->ignore(); + return; + } + + m_waveView->updateSampleRange(); + Engine::getSong()->setModified(); + de->accept(); +} + +void AudioFileProcessorView::paintEvent(QPaintEvent*) +{ + QPainter p(this); + + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + p.drawPixmap(0, 0, s_artwork); + + auto a = castModel(); + + QString file_name = ""; + + int idx = a->sample().sampleFile().length(); + + p.setFont(pointSize<8>(font())); + + QFontMetrics fm(p.font()); + + // simple algorithm for creating a text from the filename that + // matches in the white rectangle + while(idx > 0 && + fm.size(Qt::TextSingleLine, file_name + "...").width() < 210) + { + file_name = a->sample().sampleFile()[--idx] + file_name; + } + + if (idx > 0) + { + file_name = "..." + file_name; + } + + p.setPen(QColor(255, 255, 255)); + p.drawText(8, 99, file_name); +} + +void AudioFileProcessorView::sampleUpdated() +{ + m_waveView->updateSampleRange(); + m_waveView->update(); + update(); +} + +void AudioFileProcessorView::openAudioFile() +{ + QString af = SampleLoader::openAudioFile(); + if (af.isEmpty()) { return; } + + castModel()->setAudioFile(af); + Engine::getSong()->setModified(); + m_waveView->updateSampleRange(); +} + +void AudioFileProcessorView::modelChanged() +{ + auto a = castModel(); + connect(a, &AudioFileProcessor::sampleUpdated, this, &AudioFileProcessorView::sampleUpdated); + m_ampKnob->setModel(&a->ampModel()); + m_startKnob->setModel(&a->startPointModel()); + m_endKnob->setModel(&a->endPointModel()); + m_loopKnob->setModel(&a->loopPointModel()); + m_reverseButton->setModel(&a->reverseModel()); + m_loopGroup->setModel(&a->loopModel()); + m_stutterButton->setModel(&a->stutterModel()); + m_interpBox->setModel(&a->interpolationModel()); + sampleUpdated(); +} + +} // namespace gui + +} // namespace lmms diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.h b/plugins/AudioFileProcessor/AudioFileProcessorView.h new file mode 100644 index 000000000..039eaab2c --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.h @@ -0,0 +1,85 @@ +/* + * AudioFileProcessorView.h - View of the AFP + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_AUDIO_FILE_PROCESSOR_VIEW_H +#define LMMS_AUDIO_FILE_PROCESSOR_VIEW_H + +#include "InstrumentView.h" + + +namespace lmms +{ + +namespace gui +{ + +class automatableButtonGroup; +class Knob; +class PixmapButton; +class ComboBox; +class AudioFileProcessorWaveView; + + +class AudioFileProcessorView : public gui::InstrumentViewFixedSize +{ + Q_OBJECT +public: + AudioFileProcessorView(Instrument* instrument, QWidget* parent); + virtual ~AudioFileProcessorView() = default; + + void newWaveView(); + +protected slots: + void sampleUpdated(); + void openAudioFile(); + +protected: + virtual void dragEnterEvent(QDragEnterEvent* dee); + virtual void dropEvent(QDropEvent* de); + virtual void paintEvent(QPaintEvent*); + + // Private methods +private: + virtual void modelChanged(); + + // Private members +private: + AudioFileProcessorWaveView* m_waveView; + Knob* m_ampKnob; + Knob* m_startKnob; + Knob* m_endKnob; + Knob* m_loopKnob; + + gui::PixmapButton* m_openAudioFileButton; + PixmapButton* m_reverseButton; + automatableButtonGroup* m_loopGroup; + PixmapButton* m_stutterButton; + ComboBox* m_interpBox; +} ; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_AUDIO_FILE_PROCESSOR_VIEW_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp new file mode 100644 index 000000000..51a4d7ccb --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -0,0 +1,540 @@ +/* + * AudioFileProcessorWaveView.cpp - Wave renderer of the AFP + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "AudioFileProcessorWaveView.h" + +#include "ConfigManager.h" +#include "gui_templates.h" +#include "SampleWaveform.h" + +#include +#include + + +namespace lmms +{ + +namespace gui +{ + +void AudioFileProcessorWaveView::updateSampleRange() +{ + if (m_sample->sampleSize() > 1) + { + const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; + m_from = qMax(0, m_sample->startFrame() - marging); + m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); + } +} + +AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * parent, int w, int h, Sample const * buf) : + QWidget(parent), + m_sample(buf), + m_graph(QPixmap(w - 2 * s_padding, h - 2 * s_padding)), + m_from(0), + m_to(m_sample->sampleSize()), + m_last_from(0), + m_last_to(0), + m_last_amp(0), + m_startKnob(0), + m_endKnob(0), + m_loopKnob(0), + m_isDragging(false), + m_reversed(false), + m_framesPlayed(0), + m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()) +{ + setFixedSize(w, h); + setMouseTracking(true); + + updateSampleRange(); + + m_graph.fill(Qt::transparent); + update(); + updateCursor(); +} + +void AudioFileProcessorWaveView::isPlaying(f_cnt_t current_frame) +{ + m_framesPlayed = current_frame; + update(); +} + +void AudioFileProcessorWaveView::enterEvent(QEvent * e) +{ + updateCursor(); +} + +void AudioFileProcessorWaveView::leaveEvent(QEvent * e) +{ + updateCursor(); +} + +void AudioFileProcessorWaveView::mousePressEvent(QMouseEvent * me) +{ + m_isDragging = true; + m_draggingLastPoint = me->pos(); + + const int x = me->x(); + + const int start_dist = qAbs(m_startFrameX - x); + const int end_dist = qAbs(m_endFrameX - x); + const int loop_dist = qAbs(m_loopFrameX - x); + + DraggingType dt = DraggingType::SampleLoop; int md = loop_dist; + if (start_dist < loop_dist) { dt = DraggingType::SampleStart; md = start_dist; } + else if (end_dist < loop_dist) { dt = DraggingType::SampleEnd; md = end_dist; } + + if (md < 4) + { + m_draggingType = dt; + } + else + { + m_draggingType = DraggingType::Wave; + updateCursor(me); + } +} + +void AudioFileProcessorWaveView::mouseReleaseEvent(QMouseEvent * me) +{ + m_isDragging = false; + if (m_draggingType == DraggingType::Wave) + { + updateCursor(me); + } +} + +void AudioFileProcessorWaveView::mouseMoveEvent(QMouseEvent * me) +{ + if (! m_isDragging) + { + updateCursor(me); + return; + } + + const int step = me->x() - m_draggingLastPoint.x(); + switch(m_draggingType) + { + case DraggingType::SampleStart: + slideSamplePointByPx(Point::Start, step); + break; + case DraggingType::SampleEnd: + slideSamplePointByPx(Point::End, step); + break; + case DraggingType::SampleLoop: + slideSamplePointByPx(Point::Loop, step); + break; + case DraggingType::Wave: + default: + if (qAbs(me->y() - m_draggingLastPoint.y()) + < 2 * qAbs(me->x() - m_draggingLastPoint.x())) + { + slide(step); + } + else + { + zoom(me->y() < m_draggingLastPoint.y()); + } + } + + m_draggingLastPoint = me->pos(); + update(); +} + +void AudioFileProcessorWaveView::wheelEvent(QWheelEvent * we) +{ + zoom(we->angleDelta().y() > 0); + update(); +} + +void AudioFileProcessorWaveView::paintEvent(QPaintEvent * pe) +{ + QPainter p(this); + + p.drawPixmap(s_padding, s_padding, m_graph); + + const QRect graph_rect(s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding); + const f_cnt_t frames = m_to - m_from; + m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * + double(graph_rect.width()) / frames; + m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * + double(graph_rect.width()) / frames; + m_loopFrameX = graph_rect.x() + (m_sample->loopStartFrame() - m_from) * + double(graph_rect.width()) / frames; + const int played_width_px = (m_framesPlayed - m_from) * + double(graph_rect.width()) / frames; + + // loop point line + p.setPen(QColor(0x7F, 0xFF, 0xFF)); //TODO: put into a qproperty + p.drawLine(m_loopFrameX, graph_rect.y(), + m_loopFrameX, + graph_rect.height() + graph_rect.y()); + + // start/end lines + p.setPen(QColor(0xFF, 0xFF, 0xFF)); //TODO: put into a qproperty + p.drawLine(m_startFrameX, graph_rect.y(), + m_startFrameX, + graph_rect.height() + graph_rect.y()); + p.drawLine(m_endFrameX, graph_rect.y(), + m_endFrameX, + graph_rect.height() + graph_rect.y()); + + + if (m_endFrameX - m_startFrameX > 2) + { + p.fillRect( + m_startFrameX + 1, + graph_rect.y(), + m_endFrameX - m_startFrameX - 1, + graph_rect.height() + graph_rect.y(), + QColor(95, 175, 255, 50) //TODO: put into a qproperty + ); + if (m_endFrameX - m_loopFrameX > 2) + p.fillRect( + m_loopFrameX + 1, + graph_rect.y(), + m_endFrameX - m_loopFrameX - 1, + graph_rect.height() + graph_rect.y(), + QColor(95, 205, 255, 65) //TODO: put into a qproperty + ); + + if (m_framesPlayed && m_animation) + { + QLinearGradient g(m_startFrameX, 0, played_width_px, 0); + const QColor c(0, 120, 255, 180); //TODO: put into a qproperty + g.setColorAt(0, Qt::transparent); + g.setColorAt(0.8, c); + g.setColorAt(1, c); + p.fillRect( + m_startFrameX + 1, + graph_rect.y(), + played_width_px - (m_startFrameX + 1), + graph_rect.height() + graph_rect.y(), + g + ); + p.setPen(QColor(255, 255, 255)); //TODO: put into a qproperty + p.drawLine( + played_width_px, + graph_rect.y(), + played_width_px, + graph_rect.height() + graph_rect.y() + ); + m_framesPlayed = 0; + } + } + + QLinearGradient g(0, 0, width() * 0.7, 0); + const QColor c(16, 111, 170, 180); + g.setColorAt(0, c); + g.setColorAt(0.4, c); + g.setColorAt(1, Qt::transparent); + p.fillRect(s_padding, s_padding, m_graph.width(), 14, g); + + p.setPen(QColor(255, 255, 255)); + p.setFont(pointSize<8>(font())); + + QString length_text; + const int length = m_sample->sampleDuration().count(); + + if (length > 20000) + { + length_text = QString::number(length / 1000) + "s"; + } + else if (length > 2000) + { + length_text = QString::number((length / 100) / 10.0) + "s"; + } + else + { + length_text = QString::number(length) + "ms"; + } + + p.drawText( + s_padding + 2, + s_padding + 10, + tr("Sample length:") + " " + length_text + ); +} + +void AudioFileProcessorWaveView::updateGraph() +{ + if (m_to == 1) + { + m_to = m_sample->sampleSize() * 0.7; + slideSamplePointToFrames(Point::End, m_to * 0.7); + } + + if (m_from > m_sample->startFrame()) + { + m_from = m_sample->startFrame(); + } + + if (m_to < m_sample->endFrame()) + { + m_to = m_sample->endFrame(); + } + + if (m_sample->reversed() != m_reversed) + { + reverse(); + } + else if (m_last_from == m_from && m_last_to == m_to && m_sample->amplification() == m_last_amp) + { + return; + } + + m_last_from = m_from; + m_last_to = m_to; + m_last_amp = m_sample->amplification(); + + m_graph.fill(Qt::transparent); + QPainter p(&m_graph); + p.setPen(QColor(255, 255, 255)); + + const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; + const auto waveform = SampleWaveform::Parameters{ + m_sample->data() + m_from, static_cast(m_to - m_from), m_sample->amplification(), m_sample->reversed()}; + SampleWaveform::visualize(waveform, p, rect); +} + +void AudioFileProcessorWaveView::zoom(const bool out) +{ + const f_cnt_t start = m_sample->startFrame(); + const f_cnt_t end = m_sample->endFrame(); + const f_cnt_t frames = m_sample->sampleSize(); + const f_cnt_t d_from = start - m_from; + const f_cnt_t d_to = m_to - end; + + const f_cnt_t step = qMax(1, qMax(d_from, d_to) / 10); + const f_cnt_t step_from = (out ? - step : step); + const f_cnt_t step_to = (out ? step : - step); + + const double comp_ratio = double(qMin(d_from, d_to)) + / qMax(1, qMax(d_from, d_to)); + + f_cnt_t new_from; + f_cnt_t new_to; + + if ((out && d_from < d_to) || (! out && d_to < d_from)) + { + new_from = qBound(0, m_from + step_from, start); + new_to = qBound( + end, + m_to + f_cnt_t(step_to * (new_from == m_from ? 1 : comp_ratio)), + frames + ); + } + else + { + new_to = qBound(end, m_to + step_to, frames); + new_from = qBound( + 0, + m_from + f_cnt_t(step_from * (new_to == m_to ? 1 : comp_ratio)), + start + ); + } + + if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) + { + m_from = new_from; + m_to = new_to; + } +} + +void AudioFileProcessorWaveView::slide(int px) +{ + const double fact = qAbs(double(px) / width()); + f_cnt_t step = (m_to - m_from) * fact; + if (px > 0) + { + step = -step; + } + + f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; + f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; + + step = qAbs(step_from) < qAbs(step_to) ? step_from : step_to; + + m_from += step; + m_to += step; + slideSampleByFrames(step); +} + +void AudioFileProcessorWaveView::setKnobs(knob * start, knob * end, knob * loop) +{ + m_startKnob = start; + m_endKnob = end; + m_loopKnob = loop; + + m_startKnob->setWaveView(this); + m_startKnob->setRelatedKnob(m_endKnob); + + m_endKnob->setWaveView(this); + m_endKnob->setRelatedKnob(m_startKnob); + + m_loopKnob->setWaveView(this); +} + +void AudioFileProcessorWaveView::slideSamplePointByPx(Point point, int px) +{ + slideSamplePointByFrames( + point, + f_cnt_t((double(px) / width()) * (m_to - m_from)) + ); +} + +void AudioFileProcessorWaveView::slideSamplePointByFrames(Point point, f_cnt_t frames, bool slide_to) +{ + knob * a_knob = m_startKnob; + switch(point) + { + case Point::End: + a_knob = m_endKnob; + break; + case Point::Loop: + a_knob = m_loopKnob; + break; + case Point::Start: + break; + } + if (a_knob == nullptr) + { + return; + } + else + { + const double v = static_cast(frames) / m_sample->sampleSize(); + if (slide_to) + { + a_knob->slideTo(v); + } + else + { + a_knob->slideBy(v); + } + } +} + + + + +void AudioFileProcessorWaveView::slideSampleByFrames(f_cnt_t frames) +{ + if (m_sample->sampleSize() <= 1) + { + return; + } + const double v = static_cast(frames) / m_sample->sampleSize(); + // update knobs in the right order + // to avoid them clamping each other + if (v < 0) + { + m_startKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_endKnob->slideBy(v, false); + } + else + { + m_endKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_startKnob->slideBy(v, false); + } +} + +void AudioFileProcessorWaveView::reverse() +{ + slideSampleByFrames( + m_sample->sampleSize() + - m_sample->endFrame() + - m_sample->startFrame() + ); + + const f_cnt_t from = m_from; + m_from = m_sample->sampleSize() - m_to; + m_to = m_sample->sampleSize() - from; + + m_reversed = ! m_reversed; +} + +void AudioFileProcessorWaveView::updateCursor(QMouseEvent * me) +{ + bool const waveIsDragged = m_isDragging && (m_draggingType == DraggingType::Wave); + bool const pointerCloseToStartEndOrLoop = (me != nullptr) && + (isCloseTo(me->x(), m_startFrameX) || + isCloseTo(me->x(), m_endFrameX) || + isCloseTo(me->x(), m_loopFrameX)); + + if (!m_isDragging && pointerCloseToStartEndOrLoop) + setCursor(Qt::SizeHorCursor); + else if (waveIsDragged) + setCursor(Qt::ClosedHandCursor); + else + setCursor(Qt::OpenHandCursor); +} + +void AudioFileProcessorWaveView::knob::slideTo(double v, bool check_bound) +{ + if (check_bound && ! checkBound(v)) + { + return; + } + model()->setValue(v); + emit sliderMoved(model()->value()); +} + +float AudioFileProcessorWaveView::knob::getValue(const QPoint & p) +{ + const double dec_fact = ! m_waveView ? 1 : + static_cast(m_waveView->m_to - m_waveView->m_from) / m_waveView->m_sample->sampleSize(); + const float inc = Knob::getValue(p) * dec_fact; + + return inc; +} + +bool AudioFileProcessorWaveView::knob::checkBound(double v) const +{ + if (! m_relatedKnob || ! m_waveView) + { + return true; + } + + if ((m_relatedKnob->model()->value() - v > 0) != + (m_relatedKnob->model()->value() - model()->value() >= 0)) + return false; + + const double d1 = qAbs(m_relatedKnob->model()->value() - model()->value()) + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); + + const double d2 = qAbs(m_relatedKnob->model()->value() - v) + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); + + return d1 < d2 || d2 > 0.005; +} + +} // namespace gui + +} // namespace lmms diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h new file mode 100644 index 000000000..83a159725 --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h @@ -0,0 +1,181 @@ +/* + * AudioFileProcessorWaveView.h - Wave renderer of the AFP + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_AUDIO_FILE_PROCESSOR_WAVE_VIEW_H +#define LMMS_AUDIO_FILE_PROCESSOR_WAVE_VIEW_H + + +#include "Knob.h" + + +namespace lmms +{ + +class Sample; + +namespace gui +{ + +class AudioFileProcessorView; + +class AudioFileProcessorWaveView : public QWidget +{ + Q_OBJECT +protected: + virtual void enterEvent(QEvent* e); + virtual void leaveEvent(QEvent* e); + virtual void mousePressEvent(QMouseEvent* me); + virtual void mouseReleaseEvent(QMouseEvent* me); + virtual void mouseMoveEvent(QMouseEvent* me); + virtual void wheelEvent(QWheelEvent* we); + virtual void paintEvent(QPaintEvent* pe); + + +public: + enum class Point + { + Start, + End, + Loop + } ; + + class knob : public Knob + { + const AudioFileProcessorWaveView* m_waveView; + const Knob* m_relatedKnob; + + + public: + knob(QWidget* parent) : + Knob(KnobType::Bright26, parent), + m_waveView(0), + m_relatedKnob(0) + { + setFixedSize(37, 47); + } + + void setWaveView(const AudioFileProcessorWaveView* wv) + { + m_waveView = wv; + } + + void setRelatedKnob(const Knob* knob) + { + m_relatedKnob = knob; + } + + void slideBy(double v, bool check_bound = true) + { + slideTo(model()->value() + v, check_bound); + } + + void slideTo(double v, bool check_bound = true); + + + protected: + float getValue(const QPoint & p); + + + private: + bool checkBound(double v) const; + } ; + + +public slots: + void update() + { + updateGraph(); + QWidget::update(); + } + + void isPlaying(lmms::f_cnt_t current_frame); + + +private: + static const int s_padding = 2; + + enum class DraggingType + { + Wave, + SampleStart, + SampleEnd, + SampleLoop + } ; + + Sample const* m_sample; + QPixmap m_graph; + f_cnt_t m_from; + f_cnt_t m_to; + f_cnt_t m_last_from; + f_cnt_t m_last_to; + float m_last_amp; + knob* m_startKnob; + knob* m_endKnob; + knob* m_loopKnob; + f_cnt_t m_startFrameX; + f_cnt_t m_endFrameX; + f_cnt_t m_loopFrameX; + bool m_isDragging; + QPoint m_draggingLastPoint; + DraggingType m_draggingType; + bool m_reversed; + f_cnt_t m_framesPlayed; + bool m_animation; + + friend class AudioFileProcessorView; + +public: + AudioFileProcessorWaveView(QWidget* parent, int w, int h, Sample const* buf); + void setKnobs(knob* start, knob* end, knob* loop); + + + void updateSampleRange(); +private: + void zoom(const bool out = false); + void slide(int px); + void slideSamplePointByPx(Point point, int px); + void slideSamplePointByFrames(Point point, f_cnt_t frames, bool slide_to = false); + void slideSampleByFrames(f_cnt_t frames); + + void slideSamplePointToFrames(Point point, f_cnt_t frames) + { + slideSamplePointByFrames(point, frames, true); + } + + void updateGraph(); + void reverse(); + void updateCursor(QMouseEvent* me = nullptr); + + static bool isCloseTo(int a, int b) + { + return qAbs(a - b) < 4; + } + +} ; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_AUDIO_FILE_PROCESSOR_WAVE_VIEW_H diff --git a/plugins/AudioFileProcessor/CMakeLists.txt b/plugins/AudioFileProcessor/CMakeLists.txt index 055fad791..a67532112 100644 --- a/plugins/AudioFileProcessor/CMakeLists.txt +++ b/plugins/AudioFileProcessor/CMakeLists.txt @@ -1,3 +1,3 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(audiofileprocessor AudioFileProcessor.cpp AudioFileProcessor.h MOCFILES AudioFileProcessor.h EMBEDDED_RESOURCES *.png) +BUILD_PLUGIN(audiofileprocessor AudioFileProcessor.cpp AudioFileProcessor.h AudioFileProcessorView.cpp AudioFileProcessorView.h AudioFileProcessorWaveView.cpp AudioFileProcessorWaveView.h MOCFILES AudioFileProcessor.h AudioFileProcessorView.h AudioFileProcessorWaveView.h EMBEDDED_RESOURCES *.png) From ffcf8c261dcf8f4fb10d951d1d32571b28cafa01 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 24 Jan 2024 18:12:42 +0100 Subject: [PATCH 079/191] Add private setters for "from" and "to" (#7071) * Add private setters for "from" and "to" Add private setters for the "from" and "to" values in `AudioFileProcessorWaveView`. When being used the setters will ensure that the bounds are respected. Also add a `range` method because this computation was done repeatedly throughout the code. Fixes #7068 but masks some of the original problems with the code that computes out-of-bounds values for "from" and "to" like the `slide` method. Problematic code can still be found by temporarily adding the following assertions to the setters: * `assert (to <= m_sample->sampleSize());` in `setTo` * `assert (from >= 0);` in `setFrom` * Remove superfluous calls to qMax and qMin --- .../AudioFileProcessorWaveView.cpp | 47 +++++++++++++------ .../AudioFileProcessorWaveView.h | 3 ++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index 51a4d7ccb..f130ca41c 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -31,6 +31,8 @@ #include #include +#include + namespace lmms { @@ -43,11 +45,26 @@ void AudioFileProcessorWaveView::updateSampleRange() if (m_sample->sampleSize() > 1) { const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; - m_from = qMax(0, m_sample->startFrame() - marging); - m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); + setFrom(m_sample->startFrame() - marging); + setTo(m_sample->endFrame() + marging); } } +void AudioFileProcessorWaveView::setTo(f_cnt_t to) +{ + m_to = std::min(to, static_cast(m_sample->sampleSize())); +} + +void AudioFileProcessorWaveView::setFrom(f_cnt_t from) +{ + m_from = std::max(from, 0); +} + +f_cnt_t AudioFileProcessorWaveView::range() const +{ + return m_to - m_from; +} + AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * parent, int w, int h, Sample const * buf) : QWidget(parent), m_sample(buf), @@ -176,7 +193,7 @@ void AudioFileProcessorWaveView::paintEvent(QPaintEvent * pe) p.drawPixmap(s_padding, s_padding, m_graph); const QRect graph_rect(s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding); - const f_cnt_t frames = m_to - m_from; + const f_cnt_t frames = range(); m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * double(graph_rect.width()) / frames; m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * @@ -282,18 +299,18 @@ void AudioFileProcessorWaveView::updateGraph() { if (m_to == 1) { - m_to = m_sample->sampleSize() * 0.7; + setTo(m_sample->sampleSize() * 0.7); slideSamplePointToFrames(Point::End, m_to * 0.7); } if (m_from > m_sample->startFrame()) { - m_from = m_sample->startFrame(); + setFrom(m_sample->startFrame()); } if (m_to < m_sample->endFrame()) { - m_to = m_sample->endFrame(); + setTo(m_sample->endFrame()); } if (m_sample->reversed() != m_reversed) @@ -315,7 +332,7 @@ void AudioFileProcessorWaveView::updateGraph() const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; const auto waveform = SampleWaveform::Parameters{ - m_sample->data() + m_from, static_cast(m_to - m_from), m_sample->amplification(), m_sample->reversed()}; + m_sample->data() + m_from, static_cast(range()), m_sample->amplification(), m_sample->reversed()}; SampleWaveform::visualize(waveform, p, rect); } @@ -358,15 +375,15 @@ void AudioFileProcessorWaveView::zoom(const bool out) if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) { - m_from = new_from; - m_to = new_to; + setFrom(new_from); + setTo(new_to); } } void AudioFileProcessorWaveView::slide(int px) { const double fact = qAbs(double(px) / width()); - f_cnt_t step = (m_to - m_from) * fact; + f_cnt_t step = range() * fact; if (px > 0) { step = -step; @@ -377,8 +394,8 @@ void AudioFileProcessorWaveView::slide(int px) step = qAbs(step_from) < qAbs(step_to) ? step_from : step_to; - m_from += step; - m_to += step; + setFrom(m_from + step); + setTo(m_to + step); slideSampleByFrames(step); } @@ -401,7 +418,7 @@ void AudioFileProcessorWaveView::slideSamplePointByPx(Point point, int px) { slideSamplePointByFrames( point, - f_cnt_t((double(px) / width()) * (m_to - m_from)) + f_cnt_t((double(px) / width()) * range()) ); } @@ -472,8 +489,8 @@ void AudioFileProcessorWaveView::reverse() ); const f_cnt_t from = m_from; - m_from = m_sample->sampleSize() - m_to; - m_to = m_sample->sampleSize() - from; + setFrom(m_sample->sampleSize() - m_to); + setTo(m_sample->sampleSize() - from); m_reversed = ! m_reversed; } diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h index 83a159725..713064580 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h @@ -152,6 +152,9 @@ public: void updateSampleRange(); private: + void setTo(f_cnt_t to); + void setFrom(f_cnt_t from); + f_cnt_t range() const; void zoom(const bool out = false); void slide(int px); void slideSamplePointByPx(Point point, int px); From 339644a4188bcbfe07edee2a55a85c9776fce434 Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Tue, 30 Jan 2024 09:57:07 -0600 Subject: [PATCH 080/191] Sf2Player missing nullptr initialization (#7084) --- plugins/Sf2Player/PatchesDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sf2Player/PatchesDialog.cpp b/plugins/Sf2Player/PatchesDialog.cpp index c3ffe2d29..8a32a10ce 100644 --- a/plugins/Sf2Player/PatchesDialog.cpp +++ b/plugins/Sf2Player/PatchesDialog.cpp @@ -328,7 +328,7 @@ void PatchesDialog::bankChanged () fluid_preset_t preset; fluid_preset_t *pCurPreset = &preset; #else - fluid_preset_t *pCurPreset; + fluid_preset_t *pCurPreset = nullptr; #endif while ((pCurPreset = fluid_sfont_iteration_next_wrapper(pSoundFont, pCurPreset))) { int iBank = fluid_preset_get_banknum(pCurPreset); From a8e427fcb8d3d846ba79c8d74d73ef09ecb63c2d Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Wed, 31 Jan 2024 21:44:12 +0000 Subject: [PATCH 081/191] Update URL of tap-plugins submodule (#7087) --- .github/workflows/build.yml | 3 ++- .gitmodules | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b08c3ba20..40ba209e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -171,7 +171,8 @@ jobs: run: | add-apt-repository ppa:git-core/ppa apt-get update - apt-get --yes install git + apt-get --yes install apt-transport-https ca-certificates git + update-ca-certificates git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Check out uses: actions/checkout@v3 diff --git a/.gitmodules b/.gitmodules index fa6980ac5..4a6a0a3e3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -24,7 +24,7 @@ url = https://github.com/swh/ladspa [submodule "plugins/LadspaEffect/tap/tap-plugins"] path = plugins/LadspaEffect/tap/tap-plugins - url = https://github.com/tomszilagyi/tap-plugins + url = https://git.hq.sig7.se/tap-plugins.git [submodule "src/3rdparty/weakjack/weakjack"] path = src/3rdparty/weakjack/weakjack url = https://github.com/x42/weakjack.git From b44411382cdaccf591a02e64b18fd1ce6ce91d5f Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Fri, 2 Feb 2024 08:56:21 +0000 Subject: [PATCH 082/191] Fix vcpkg dependencies (#7088) --- vcpkg.json | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/vcpkg.json b/vcpkg.json index 48a3e3c28..1f6440934 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,14 +1,13 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "name": "lmms", "dependencies": [ { "name": "fftw3", "default-features": false, "features": [ "sse", - "sse2", - "avx", - "avx2" + "sse2" ] }, { @@ -64,10 +63,7 @@ }, { "name": "sdl2", - "default-features": false, - "features": [ - "base" - ] + "default-features": false }, { "name": "zlib", From c2052151abb91376d4dea320e35b744240082e5a Mon Sep 17 00:00:00 2001 From: Monospace-V <76674645+Monospace-V@users.noreply.github.com> Date: Sat, 3 Feb 2024 21:40:52 +0530 Subject: [PATCH 083/191] Center effects W/D knob at 0 (#7077) Set the center value of the W/D amount knob as zero specifically in Effect.cpp --- src/core/Effect.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/Effect.cpp b/src/core/Effect.cpp index 151eaf13e..51b701d9b 100644 --- a/src/core/Effect.cpp +++ b/src/core/Effect.cpp @@ -52,9 +52,11 @@ Effect::Effect( const Plugin::Descriptor * _desc, m_autoQuitModel( 1.0f, 1.0f, 8000.0f, 100.0f, 1.0f, this, tr( "Decay" ) ), m_autoQuitDisabled( false ) { + m_wetDryModel.setCenterValue(0); + m_srcState[0] = m_srcState[1] = nullptr; reinitSRC(); - + if( ConfigManager::inst()->value( "ui", "disableautoquit").toInt() ) { m_autoQuitDisabled = true; From dd53bec311b18c1900f27e21a1fbe59040169f06 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 10 Feb 2024 14:27:50 +0100 Subject: [PATCH 084/191] Hide the LED button for "Custom Base Velocity" (#7067) Hide the LED button for "Custom Base Velocity" as it did not have any other effect besides disabling the spinbox in the GUI. It did not affect any model which could be evaluated elsewhere. ## Technical details Add functionality to show/hide the LED button to `GroupBox`. There's also a corresponding getter called `ledButtonShown`. The latter is evaluated in the following situations: * Mouse clicks: if the LED button is hidden then the model is not toggled. * Paining: The X position of the caption changes depending on whether the LED button is shown or not. At a certain point the class `GroupBox` should be replaced by something that's implemented with layouts and which is used wherever it's possible. --- include/GroupBox.h | 15 +++++++++++++++ src/gui/instrument/InstrumentMidiIOView.cpp | 5 +---- src/gui/widgets/GroupBox.cpp | 18 +++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/include/GroupBox.h b/include/GroupBox.h index 6a8f424f9..fdeb31c4d 100644 --- a/include/GroupBox.h +++ b/include/GroupBox.h @@ -50,6 +50,21 @@ public: return m_led; } + /** + * @brief Returns whether the LED button is shown or not + * + * @return true LED button is shown + * @return false LED button is hidden + */ + bool ledButtonShown() const; + + /** + * @brief Sets if the LED check box is shown or not + * + * @param value Set to true to show the LED check box or to false to hide it. + */ + void setLedButtonShown(bool value); + int titleBarHeight() const { return m_titleBarHeight; diff --git a/src/gui/instrument/InstrumentMidiIOView.cpp b/src/gui/instrument/InstrumentMidiIOView.cpp index fd9d6fc54..e321d061e 100644 --- a/src/gui/instrument/InstrumentMidiIOView.cpp +++ b/src/gui/instrument/InstrumentMidiIOView.cpp @@ -145,6 +145,7 @@ InstrumentMidiIOView::InstrumentMidiIOView( QWidget* parent ) : } auto baseVelocityGroupBox = new GroupBox(tr("CUSTOM BASE VELOCITY")); + baseVelocityGroupBox->setLedButtonShown(false); layout->addWidget( baseVelocityGroupBox ); auto baseVelocityLayout = new QVBoxLayout(baseVelocityGroupBox); @@ -160,12 +161,8 @@ InstrumentMidiIOView::InstrumentMidiIOView( QWidget* parent ) : m_baseVelocitySpinBox = new LcdSpinBox( 3, baseVelocityGroupBox ); m_baseVelocitySpinBox->setLabel( tr( "BASE VELOCITY" ) ); - m_baseVelocitySpinBox->setEnabled( false ); baseVelocityLayout->addWidget( m_baseVelocitySpinBox ); - connect( baseVelocityGroupBox->ledButton(), SIGNAL(toggled(bool)), - m_baseVelocitySpinBox, SLOT(setEnabled(bool))); - layout->addStretch(); } diff --git a/src/gui/widgets/GroupBox.cpp b/src/gui/widgets/GroupBox.cpp index b5187de25..e3e71a812 100644 --- a/src/gui/widgets/GroupBox.cpp +++ b/src/gui/widgets/GroupBox.cpp @@ -72,13 +72,23 @@ void GroupBox::modelChanged() } +bool GroupBox::ledButtonShown() const +{ + return m_led->isVisible(); +} + + +void GroupBox::setLedButtonShown(bool value) +{ + m_led->setVisible(value); +} void GroupBox::mousePressEvent( QMouseEvent * _me ) { - if( _me->y() > 1 && _me->y() < 13 && _me->button() == Qt::LeftButton ) + if (ledButtonShown() && _me->y() > 1 && _me->y() < 13 && _me->button() == Qt::LeftButton) { - model()->setValue( !model()->value() ); + model()->setValue(!model()->value()); } } @@ -102,7 +112,9 @@ void GroupBox::paintEvent( QPaintEvent * pe ) // draw text p.setPen( palette().color( QPalette::Active, QPalette::Text ) ); p.setFont( pointSize<8>( font() ) ); - p.drawText( 22, m_titleBarHeight, m_caption ); + + int const captionX = ledButtonShown() ? 22 : 6; + p.drawText(captionX, m_titleBarHeight, m_caption); } From 2d185dfe02f722b77034a45b72132b819afe871f Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Sat, 10 Feb 2024 21:02:58 -0600 Subject: [PATCH 085/191] Fix deletion of mixer channels when calling MixerView::clear (#7081) * Fix deletion of mixer channels when calling MixerView::clear * clear channel 0 + delete from back to front --- src/gui/MixerView.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 93b4e1299..224ea2c85 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -590,7 +590,8 @@ void MixerView::setCurrentMixerChannel(int channel) void MixerView::clear() { - getMixer()->clear(); + for (auto i = m_mixerChannelViews.size() - 1; i > 0; --i) { deleteChannel(i); } + getMixer()->clearChannel(0); refreshDisplay(); } From 074dd0bd13cab69a0378856267fee2c0d6c44e6f Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Mon, 12 Feb 2024 00:00:12 +0530 Subject: [PATCH 086/191] Add `WANT_OSS` as a compile time option (#7099) --- CMakeLists.txt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 858849abd..e45db03e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ INCLUDE(VersionInfo) INCLUDE(DetectMachine) OPTION(WANT_ALSA "Include ALSA (Advanced Linux Sound Architecture) support" ON) +OPTION(WANT_OSS "Include Open Sound System support" ON) OPTION(WANT_CALF "Include CALF LADSPA plugins" ON) OPTION(WANT_CAPS "Include C* Audio Plugin Suite (LADSPA plugins)" ON) OPTION(WANT_CARLA "Include Carla plugin" ON) @@ -109,9 +110,11 @@ IF(LMMS_BUILD_APPLE) LINK_DIRECTORIES("${APPLE_PREFIX}/lib") SET(WANT_SOUNDIO OFF) SET(WANT_ALSA OFF) + SET(WANT_OSS OFF) SET(WANT_PULSEAUDIO OFF) SET(WANT_VST OFF) SET(STATUS_ALSA "") + SET(STATUS_OSS "") SET(STATUS_PULSEAUDIO "") SET(STATUS_APPLEMIDI "OK") ELSE(LMMS_BUILD_APPLE) @@ -121,6 +124,7 @@ ENDIF(LMMS_BUILD_APPLE) IF(LMMS_BUILD_WIN32) SET(WANT_ALSA OFF) + SET(WANT_OSS OFF) SET(WANT_PULSEAUDIO OFF) SET(WANT_SNDIO OFF) SET(WANT_SOUNDIO OFF) @@ -128,6 +132,7 @@ IF(LMMS_BUILD_WIN32) SET(BUNDLE_QT_TRANSLATIONS ON) SET(LMMS_HAVE_WINMM TRUE) SET(STATUS_ALSA "") + SET(STATUS_OSS "") SET(STATUS_PULSEAUDIO "") SET(STATUS_SOUNDIO "") SET(STATUS_SNDIO "") @@ -464,13 +469,13 @@ IF(WANT_OGGVORBIS) ENDIF(WANT_OGGVORBIS) -# check whether to enable OSS-support -IF(LMMS_HAVE_SOUNDCARD_H OR LMMS_HAVE_SYS_SOUNDCARD_H) +# check for OSS +IF(WANT_OSS AND (LMMS_HAVE_SOUNDCARD_H OR LMMS_HAVE_SYS_SOUNDCARD_H)) SET(LMMS_HAVE_OSS TRUE) SET(STATUS_OSS "OK") -ELSE(LMMS_HAVE_SOUNDCARD_H OR LMMS_HAVE_SYS_SOUNDCARD_H) +ELSEIF(WANT_OSS) SET(STATUS_OSS "") -ENDIF(LMMS_HAVE_SOUNDCARD_H OR LMMS_HAVE_SYS_SOUNDCARD_H) +ENDIF() # check for ALSA From c87ff4194f79ca8791a1da714bc0ba7554311861 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Fri, 16 Feb 2024 18:56:30 +0000 Subject: [PATCH 087/191] Use GitHub mirror of tap-plugins (#7110) --- .github/workflows/build.yml | 3 +-- .gitmodules | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40ba209e3..b08c3ba20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -171,8 +171,7 @@ jobs: run: | add-apt-repository ppa:git-core/ppa apt-get update - apt-get --yes install apt-transport-https ca-certificates git - update-ca-certificates + apt-get --yes install git git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Check out uses: actions/checkout@v3 diff --git a/.gitmodules b/.gitmodules index 4a6a0a3e3..c85f7e5d8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -24,7 +24,7 @@ url = https://github.com/swh/ladspa [submodule "plugins/LadspaEffect/tap/tap-plugins"] path = plugins/LadspaEffect/tap/tap-plugins - url = https://git.hq.sig7.se/tap-plugins.git + url = https://github.com/lmms/tap-plugins [submodule "src/3rdparty/weakjack/weakjack"] path = src/3rdparty/weakjack/weakjack url = https://github.com/x42/weakjack.git From a81ad74e3ae6c4ac053ef26670f96aba640d96c4 Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 16 Feb 2024 16:31:44 -0500 Subject: [PATCH 088/191] Fix playback within `Sample` (#7100) This revisits two main aspects of playback from `Sample`: copying frames into a temporary playback buffer before resampling, and advancing the state's frame index. Both operations were improperly done, causing distorted audio to be heard when resampling from within the `Sample::play` function. To fix this, playback into the temporary playback buffer is now done using the `playRaw` function, which copies the frame one by one in a loop, moving through the buffer correctly as determined by the loop mode. In addition, advancement of the playback index is done using the `advance` function, which advances the index by the given amount and handles an out of bounds index for the loop modes as necessary using the modulo operator. --- include/Sample.h | 21 ++--- include/SampleBuffer.h | 2 +- src/core/Sample.cpp | 179 +++++++++++++++++++++----------------- src/core/SampleBuffer.cpp | 2 +- 4 files changed, 108 insertions(+), 96 deletions(-) diff --git a/include/Sample.h b/include/Sample.h index 102aaf2d5..86fba1ddc 100644 --- a/include/Sample.h +++ b/include/Sample.h @@ -33,9 +33,6 @@ #include "SampleBuffer.h" #include "lmms_export.h" -class QPainter; -class QRect; - namespace lmms { class LMMS_EXPORT Sample { @@ -63,17 +60,17 @@ public: } auto resampler() -> AudioResampler& { return m_resampler; } - auto frameIndex() const -> f_cnt_t { return m_frameIndex; } + auto frameIndex() const -> int { return m_frameIndex; } auto varyingPitch() const -> bool { return m_varyingPitch; } auto backwards() const -> bool { return m_backwards; } - void setFrameIndex(f_cnt_t frameIndex) { m_frameIndex = frameIndex; } + void setFrameIndex(int frameIndex) { m_frameIndex = frameIndex; } void setVaryingPitch(bool varyingPitch) { m_varyingPitch = varyingPitch; } void setBackwards(bool backwards) { m_backwards = backwards; } private: AudioResampler m_resampler; - f_cnt_t m_frameIndex = 0; + int m_frameIndex = 0; bool m_varyingPitch = false; bool m_backwards = false; friend class Sample; @@ -81,7 +78,7 @@ public: Sample() = default; Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); - Sample(const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const sampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); Sample(const Sample& other); Sample(Sample&& other); explicit Sample(const QString& audioFile); @@ -90,8 +87,8 @@ public: auto operator=(const Sample&) -> Sample&; auto operator=(Sample&&) -> Sample&; - auto play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency = DefaultBaseFreq, - Loop loopMode = Loop::Off) -> bool; + auto play(sampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency = DefaultBaseFreq, + Loop loopMode = Loop::Off) const -> bool; auto sampleDuration() const -> std::chrono::milliseconds; auto sampleFile() const -> const QString& { return m_buffer->audioFile(); } @@ -120,10 +117,8 @@ public: void setReversed(bool reversed) { m_reversed.store(reversed, std::memory_order_relaxed); } private: - void playSampleRange(PlaybackState* state, sampleFrame* dst, size_t numFrames) const; - void amplifySampleRange(sampleFrame* src, int numFrames) const; - void copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const; - void copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const; + void playRaw(sampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const; + void advance(PlaybackState* state, size_t advanceAmount, Loop loopMode) const; private: std::shared_ptr m_buffer = SampleBuffer::emptyBuffer(); diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 0db8aa4d3..4089eb446 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -56,7 +56,7 @@ public: SampleBuffer(const QString& base64, int sampleRate); SampleBuffer(std::vector data, int sampleRate); SampleBuffer( - const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + const sampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept; auto toBase64() const -> QString; diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp index cdb12851f..333ee624d 100644 --- a/src/core/Sample.cpp +++ b/src/core/Sample.cpp @@ -24,8 +24,7 @@ #include "Sample.h" -#include -#include +#include namespace lmms { @@ -47,7 +46,7 @@ Sample::Sample(const QByteArray& base64, int sampleRate) { } -Sample::Sample(const sampleFrame* data, int numFrames, int sampleRate) +Sample::Sample(const sampleFrame* data, size_t numFrames, int sampleRate) : m_buffer(std::make_shared(data, numFrames, sampleRate)) , m_startFrame(0) , m_endFrame(m_buffer->size()) @@ -117,57 +116,39 @@ auto Sample::operator=(Sample&& other) -> Sample& return *this; } -bool Sample::play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency, Loop loopMode) +bool Sample::play(sampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency, Loop loopMode) const { - if (numFrames <= 0 || desiredFrequency <= 0) { return false; } + assert(numFrames > 0); + assert(desiredFrequency > 0); - auto resampleRatio = static_cast(Engine::audioEngine()->processingSampleRate()) / m_buffer->sampleRate(); - resampleRatio *= frequency() / desiredFrequency; + const auto pastBounds = state->m_frameIndex >= m_endFrame || (state->m_frameIndex < 0 && state->m_backwards); + if (loopMode == Loop::Off && pastBounds) { return false; } - auto playBuffer = std::vector(numFrames / resampleRatio); - if (!typeInfo::isEqual(resampleRatio, 1.0f)) - { - playBuffer.resize(playBuffer.size() + s_interpolationMargins[state->resampler().interpolationMode()]); - } + const auto outputSampleRate = Engine::audioEngine()->processingSampleRate() * m_frequency / desiredFrequency; + const auto inputSampleRate = m_buffer->sampleRate(); + const auto resampleRatio = outputSampleRate / inputSampleRate; + const auto marginSize = s_interpolationMargins[state->resampler().interpolationMode()]; - const auto start = startFrame(); - const auto end = endFrame(); - const auto loopStart = loopStartFrame(); - const auto loopEnd = loopEndFrame(); + state->m_frameIndex = std::max(m_startFrame, state->m_frameIndex); - switch (loopMode) - { - case Loop::Off: - state->m_frameIndex = std::clamp(state->m_frameIndex, start, end); - if (state->m_frameIndex == end) { return false; } - break; - case Loop::On: - state->m_frameIndex = std::clamp(state->m_frameIndex, start, loopEnd); - if (state->m_frameIndex == loopEnd) { state->m_frameIndex = loopStart; } - break; - case Loop::PingPong: - state->m_frameIndex = std::clamp(state->m_frameIndex, start, loopEnd); - if (state->m_frameIndex == loopEnd) - { - state->m_frameIndex = loopEnd - 1; - state->m_backwards = true; - } - else if (state->m_frameIndex <= loopStart && state->m_backwards) - { - state->m_frameIndex = loopStart; - state->m_backwards = false; - } - break; - } + auto playBuffer = std::vector(numFrames / resampleRatio + marginSize); + playRaw(playBuffer.data(), playBuffer.size(), state, loopMode); - playSampleRange(state, playBuffer.data(), playBuffer.size()); - - const auto result + const auto resampleResult = state->resampler().resample(&playBuffer[0][0], playBuffer.size(), &dst[0][0], numFrames, resampleRatio); - if (result.error != 0) { return false; } + advance(state, resampleResult.inputFramesUsed, loopMode); - state->m_frameIndex += (state->m_backwards ? -1 : 1) * result.inputFramesUsed; - amplifySampleRange(dst, result.outputFramesGenerated); + const auto outputFrames = resampleResult.outputFramesGenerated; + if (outputFrames < numFrames) { std::fill_n(dst + outputFrames, numFrames - outputFrames, sampleFrame{}); } + + if (!typeInfo::isEqual(m_amplification, 1.0f)) + { + for (int i = 0; i < numFrames; ++i) + { + dst[i][0] *= m_amplification; + dst[i][1] *= m_amplification; + } + } return true; } @@ -187,44 +168,80 @@ void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, setLoopEndFrame(loopEndFrame); } -void Sample::playSampleRange(PlaybackState* state, sampleFrame* dst, size_t numFrames) const +void Sample::playRaw(sampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const { - auto framesToCopy = 0; - if (state->m_backwards) + auto index = state->m_frameIndex; + auto backwards = state->m_backwards; + + for (size_t i = 0; i < numFrames; ++i) { - framesToCopy = std::min(state->m_frameIndex - startFrame(), numFrames); - copyBufferBackward(dst, state->m_frameIndex, framesToCopy); - } - else - { - framesToCopy = std::min(endFrame() - state->m_frameIndex, numFrames); - copyBufferForward(dst, state->m_frameIndex, framesToCopy); - } + switch (loopMode) + { + case Loop::Off: + if (index < 0 || index > m_endFrame) { return; } + break; + case Loop::On: + if (index < m_loopStartFrame && backwards) { index = m_loopEndFrame - 1; } + else if (index >= m_loopEndFrame) { index = m_loopStartFrame; } + break; + case Loop::PingPong: + if (index < m_loopStartFrame && backwards) + { + index = m_loopStartFrame; + backwards = false; + } + else if (index >= m_loopEndFrame) + { + index = m_loopEndFrame - 1; + backwards = true; + } + break; + default: + break; + } - if (framesToCopy < numFrames) { std::fill_n(dst + framesToCopy, numFrames - framesToCopy, sampleFrame{0, 0}); } -} - -void Sample::copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const -{ - reversed() ? std::copy_n(m_buffer->rbegin() + initialPosition, advanceAmount, dst) - : std::copy_n(m_buffer->begin() + initialPosition, advanceAmount, dst); -} - -void Sample::copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const -{ - reversed() ? std::reverse_copy( - m_buffer->rbegin() + initialPosition - advanceAmount, m_buffer->rbegin() + initialPosition, dst) - : std::reverse_copy( - m_buffer->begin() + initialPosition - advanceAmount, m_buffer->begin() + initialPosition, dst); -} - -void Sample::amplifySampleRange(sampleFrame* src, int numFrames) const -{ - const auto amplification = m_amplification.load(std::memory_order_relaxed); - for (int i = 0; i < numFrames; ++i) - { - src[i][0] *= amplification; - src[i][1] *= amplification; + dst[i] = m_buffer->data()[m_reversed ? m_buffer->size() - index - 1 : index]; + backwards ? --index : ++index; } } + +void Sample::advance(PlaybackState* state, size_t advanceAmount, Loop loopMode) const +{ + state->m_frameIndex += (state->m_backwards ? -1 : 1) * advanceAmount; + if (loopMode == Loop::Off) { return; } + + const auto distanceFromLoopStart = std::abs(state->m_frameIndex - m_loopStartFrame); + const auto distanceFromLoopEnd = std::abs(state->m_frameIndex - m_loopEndFrame); + const auto loopSize = m_loopEndFrame - m_loopStartFrame; + if (loopSize == 0) { return; } + + switch (loopMode) + { + case Loop::On: + if (state->m_frameIndex < m_loopStartFrame && state->m_backwards) + { + state->m_frameIndex = m_loopEndFrame - 1 - distanceFromLoopStart % loopSize; + } + else if (state->m_frameIndex >= m_loopEndFrame) + { + state->m_frameIndex = m_loopStartFrame + distanceFromLoopEnd % loopSize; + } + break; + case Loop::PingPong: + if (state->m_frameIndex < m_loopStartFrame && state->m_backwards) + { + state->m_frameIndex = m_loopStartFrame + distanceFromLoopStart % loopSize; + state->m_backwards = false; + } + else if (state->m_frameIndex >= m_loopEndFrame) + { + state->m_frameIndex = m_loopEndFrame - 1 - distanceFromLoopEnd % loopSize; + state->m_backwards = true; + } + break; + default: + break; + } +} + } // namespace lmms diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 550a9d7bc..6483dd522 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -31,7 +31,7 @@ namespace lmms { -SampleBuffer::SampleBuffer(const sampleFrame* data, int numFrames, int sampleRate) +SampleBuffer::SampleBuffer(const sampleFrame* data, size_t numFrames, int sampleRate) : m_data(data, data + numFrames) , m_sampleRate(sampleRate) { From 99120f567d54b95c2d1a8e786f5d3a3f9713f715 Mon Sep 17 00:00:00 2001 From: Pascal <81458575+khoidauminh@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:48:17 +0700 Subject: [PATCH 089/191] Prevent out of bound read in `Sample::playRaw` (#7113) --- src/core/Sample.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp index 333ee624d..a07b100bf 100644 --- a/src/core/Sample.cpp +++ b/src/core/Sample.cpp @@ -178,7 +178,7 @@ void Sample::playRaw(sampleFrame* dst, size_t numFrames, const PlaybackState* st switch (loopMode) { case Loop::Off: - if (index < 0 || index > m_endFrame) { return; } + if (index < 0 || index >= m_endFrame) { return; } break; case Loop::On: if (index < m_loopStartFrame && backwards) { index = m_loopEndFrame - 1; } From 34ab5ff7300b7cd5ffc0895dc21e2172e70c3540 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:56:45 +0100 Subject: [PATCH 090/191] Fixes #6626: Throw if Lv2 object CTORs fail (#6951) On plugin instantiation failure, `Lv2Proc::m_valid` was being set to false. However, `Lv2Proc::run` did not evaluate `m_valid` and still called `lilv_instance_run`, which caused undefined behavior, including crashes. This bug fixes this by not even create such zombie classes, and instead `throw`s right away. The throws are caught in `lmms_plugin_main`, as suggested in the PR discussion and as the VST3 approach. --- include/Lv2ControlBase.h | 4 --- include/Lv2Proc.h | 4 --- plugins/Lv2Effect/Lv2Effect.cpp | 10 +++++--- plugins/Lv2Effect/Lv2Effect.h | 2 -- plugins/Lv2Effect/Lv2FxControls.cpp | 7 ++---- plugins/Lv2Instrument/Lv2Instrument.cpp | 33 +++++++++++-------------- plugins/Lv2Instrument/Lv2Instrument.h | 2 -- src/core/lv2/Lv2ControlBase.cpp | 28 ++++++--------------- src/core/lv2/Lv2Proc.cpp | 16 +++++------- 9 files changed, 37 insertions(+), 69 deletions(-) diff --git a/include/Lv2ControlBase.h b/include/Lv2ControlBase.h index 2d44f0ecf..9bfb40f87 100644 --- a/include/Lv2ControlBase.h +++ b/include/Lv2ControlBase.h @@ -102,9 +102,6 @@ protected: Lv2ControlBase& operator=(const Lv2ControlBase&) = delete; - //! Must be checked after ctor or reload - bool isValid() const { return m_valid; } - /* overrides */ @@ -149,7 +146,6 @@ private: //! fulfill LMMS' requirement of having stereo input and output std::vector> m_procs; - bool m_valid = true; bool m_hasGUI = false; unsigned m_channelsPerProc; diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 1259aeede..65bd90698 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -78,8 +78,6 @@ public: ~Lv2Proc() override; void reload(); void onSampleRateChanged(); - //! Must be checked after ctor or reload - bool isValid() const { return m_valid; } /* port access @@ -173,8 +171,6 @@ protected: void shutdownPlugin(); private: - bool m_valid = true; - const LilvPlugin* m_plugin; LilvInstance* m_instance = nullptr; Lv2Features m_features; diff --git a/plugins/Lv2Effect/Lv2Effect.cpp b/plugins/Lv2Effect/Lv2Effect.cpp index 9c21b3f2a..eef6305cc 100644 --- a/plugins/Lv2Effect/Lv2Effect.cpp +++ b/plugins/Lv2Effect/Lv2Effect.cpp @@ -24,6 +24,7 @@ #include "Lv2Effect.h" +#include #include "Lv2SubPluginFeatures.h" @@ -109,9 +110,12 @@ extern "C" PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *_parent, void *_data) { using KeyType = Plugin::Descriptor::SubPluginFeatures::Key; - auto eff = new Lv2Effect(_parent, static_cast(_data)); - if (!eff->isValid()) { delete eff; eff = nullptr; } - return eff; + try { + return new Lv2Effect(_parent, static_cast(_data)); + } catch (const std::runtime_error& e) { + qCritical() << e.what(); + return nullptr; + } } } diff --git a/plugins/Lv2Effect/Lv2Effect.h b/plugins/Lv2Effect/Lv2Effect.h index 3bcded355..a28182132 100644 --- a/plugins/Lv2Effect/Lv2Effect.h +++ b/plugins/Lv2Effect/Lv2Effect.h @@ -41,8 +41,6 @@ public: initialization */ Lv2Effect(Model* parent, const Descriptor::SubPluginFeatures::Key* _key); - //! Must be checked after ctor or reload - bool isValid() const { return m_controls.isValid(); } bool processAudioBuffer( sampleFrame* buf, const fpp_t frames ) override; EffectControls* controls() override { return &m_controls; } diff --git a/plugins/Lv2Effect/Lv2FxControls.cpp b/plugins/Lv2Effect/Lv2FxControls.cpp index 3ec7dbe23..72c387ba7 100644 --- a/plugins/Lv2Effect/Lv2FxControls.cpp +++ b/plugins/Lv2Effect/Lv2FxControls.cpp @@ -38,11 +38,8 @@ Lv2FxControls::Lv2FxControls(class Lv2Effect *effect, const QString& uri) : EffectControls(effect), Lv2ControlBase(this, uri) { - if (isValid()) - { - connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, - this, [this](){onSampleRateChanged();}); - } + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, + this, &Lv2FxControls::onSampleRateChanged); } diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 841b8a89a..316829327 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -76,19 +76,16 @@ Lv2Instrument::Lv2Instrument(InstrumentTrack *instrumentTrackArg, Instrument(instrumentTrackArg, &lv2instrument_plugin_descriptor, key), Lv2ControlBase(this, key->attributes["uri"]) { - if (Lv2ControlBase::isValid()) - { - clearRunningNotes(); + clearRunningNotes(); - connect(instrumentTrack()->pitchRangeModel(), SIGNAL(dataChanged()), - this, SLOT(updatePitchRange()), Qt::DirectConnection); - connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, - this, [this](){onSampleRateChanged();}); + connect(instrumentTrack()->pitchRangeModel(), SIGNAL(dataChanged()), + this, SLOT(updatePitchRange()), Qt::DirectConnection); + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, + this, &Lv2Instrument::onSampleRateChanged); - // now we need a play-handle which cares for calling play() - auto iph = new InstrumentPlayHandle(this, instrumentTrackArg); - Engine::audioEngine()->addPlayHandle(iph); - } + // now we need a play-handle which cares for calling play() + auto iph = new InstrumentPlayHandle(this, instrumentTrackArg); + Engine::audioEngine()->addPlayHandle(iph); } @@ -134,11 +131,6 @@ void Lv2Instrument::onSampleRateChanged() -bool Lv2Instrument::isValid() const { return Lv2ControlBase::isValid(); } - - - - void Lv2Instrument::saveSettings(QDomDocument &doc, QDomElement &that) { Lv2ControlBase::saveSettings(doc, that); @@ -321,9 +313,12 @@ extern "C" PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *_parent, void *_data) { using KeyType = Plugin::Descriptor::SubPluginFeatures::Key; - auto ins = new Lv2Instrument(static_cast(_parent), static_cast(_data)); - if (!ins->isValid()) { delete ins; ins = nullptr; } - return ins; + try { + return new Lv2Instrument(static_cast(_parent), static_cast(_data)); + } catch (const std::runtime_error& e) { + qCritical() << e.what(); + return nullptr; + } } } diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h index 5e255e0df..de41dc958 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.h +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -61,8 +61,6 @@ public: ~Lv2Instrument() override; void reload(); void onSampleRateChanged(); - //! Must be checked after ctor or reload - bool isValid() const; /* load/save diff --git a/src/core/lv2/Lv2ControlBase.cpp b/src/core/lv2/Lv2ControlBase.cpp index 64cdc51fd..5741866e9 100644 --- a/src/core/lv2/Lv2ControlBase.cpp +++ b/src/core/lv2/Lv2ControlBase.cpp @@ -59,7 +59,7 @@ Lv2ControlBase::Lv2ControlBase(Model* that, const QString &uri) : else { qCritical() << "No Lv2 plugin found for URI" << uri; - m_valid = false; + throw std::runtime_error("No Lv2 plugin found for given URI"); } } @@ -77,26 +77,14 @@ void Lv2ControlBase::init(Model* meAsModel) while (channelsLeft > 0) { std::unique_ptr newOne = std::make_unique(m_plugin, meAsModel); - if (newOne->isValid()) - { - channelsLeft -= std::max( - 1 + static_cast(newOne->inPorts().m_right), - 1 + static_cast(newOne->outPorts().m_right)); - Q_ASSERT(channelsLeft >= 0); - m_procs.push_back(std::move(newOne)); - } - else - { - qCritical() << "Failed instantiating LV2 processor"; - m_valid = false; - channelsLeft = 0; - } - } - if (m_valid) - { - m_channelsPerProc = DEFAULT_CHANNELS / m_procs.size(); - linkAllModels(); + channelsLeft -= std::max( + 1 + static_cast(newOne->inPorts().m_right), + 1 + static_cast(newOne->outPorts().m_right)); + Q_ASSERT(channelsLeft >= 0); + m_procs.push_back(std::move(newOne)); } + m_channelsPerProc = DEFAULT_CHANNELS / m_procs.size(); + linkAllModels(); } diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 158196fdb..77177a1c0 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -467,7 +467,7 @@ void Lv2Proc::initPlugin() << "(URI:" << lilv_node_as_uri(lilv_plugin_get_uri(m_plugin)) << ")"; - m_valid = false; + throw std::runtime_error("Failed to create Lv2 processor"); } } @@ -476,16 +476,12 @@ void Lv2Proc::initPlugin() void Lv2Proc::shutdownPlugin() { - if (m_valid) - { - lilv_instance_deactivate(m_instance); - lilv_instance_free(m_instance); - m_instance = nullptr; + lilv_instance_deactivate(m_instance); + lilv_instance_free(m_instance); + m_instance = nullptr; - m_features.clear(); - m_options.clear(); - } - m_valid = true; + m_features.clear(); + m_options.clear(); } From a81666a9f55420828a12abf787db693222da885f Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 18 Feb 2024 21:22:22 +0100 Subject: [PATCH 091/191] Track resizing behavior (#7114) * Simplify TrackLabelButton Remove the dependency to `Instrument` and `InstrumentTrack` from `TrackLabelButton`. The icon of an `InstrumentTrackView` is now determined in the class `InstrumentTrackView` itself using the new static method `determinePixmap`. This enables the removal of the overridden `paintEvent` method from `TrackLabelButton`. It was also attempted to keep a non-static member function version and to use the `InstrumentTrackView`'s model with a cast. However, this did not work because 'setModel' is executed as the very last step in the constructor, i.e. the model is not already set when `determinePixmap` is called. Pulling it to the top is too risky right now. * Add helper method isInCompactMode Add the helper method `isInCompactMode` which knows how to use the `ConfigManager` to determine if the option for compact track buttons is enabled. This removes duplicate code with intricate knowledge of the keys under which compact mode is stored. * Set song as modified when track name changes Extend `Track::setName` to set the song as modified whenever the track name changes. This moves the update into a core class and enables the removal of the dependencies to `Engine` and `Song` from the GUI class `TrackLabelButton`. Also add a check if the name is really changed and only perform the actions if that's the case. To make this work the implementation of `setName` had to be moved from the header into the implementation file. * Keep instrument and sample content at top on resize Keep the content of the instrument and sample track at the top of the widget if the widget is resized. This is also fixes a bug where the `TrackLabelButton` does not react anymore when the size is increased. Technically the layout with the actual widgets is put into another vertical layout with a spacer that consumes all remaining space at the bottom. * Vertical track resizing via mouse wheel Enable to vertically resize tracks using the mouse wheel. Scrolling the mouse wheel over the track view with the control key pressed will increase/decrease the height by one pixel per wheel event. Pressing the shift key will increase/decrease in steps of five pixels. Extract code that can be shared between the existing and the new way to resize the track into the private helper method `resizeToHeight`. * Render beat pattern step buttons at the top Render the step buttons of beat patterns at the top instead of at the bottom so that they stay aligned with the other elements to the left of them (buttons, knobs, etc). Set the y offset to 4 so that the step buttons are vertically aligned with the other elements. The previous calculation lead to a minimum offset of 6 which always made the step buttons look misaligned. Introduce the new static variable `BeatStepButtonOffset` which ensures that the rendering and the evaluation of mouse clicks do not go out of sync. --- include/InstrumentTrackView.h | 2 ++ include/Track.h | 6 +--- include/TrackLabelButton.h | 3 +- include/TrackView.h | 3 ++ src/core/Track.cpp | 15 +++++++++ src/gui/clips/MidiClipView.cpp | 5 +-- src/gui/tracks/InstrumentTrackView.cpp | 32 ++++++++++++++++-- src/gui/tracks/SampleTrackView.cpp | 6 +++- src/gui/tracks/TrackLabelButton.cpp | 46 +++++--------------------- src/gui/tracks/TrackView.cpp | 28 ++++++++++++++-- 10 files changed, 94 insertions(+), 52 deletions(-) diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index e89a576e9..c7d524b36 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -93,6 +93,8 @@ private slots: void handleConfigChange(QString cls, QString attr, QString value); +private: + static QPixmap determinePixmap(InstrumentTrack* instrumentTrack); private: InstrumentTrackWindow * m_window; diff --git a/include/Track.h b/include/Track.h index 248d56b04..1c161984f 100644 --- a/include/Track.h +++ b/include/Track.h @@ -201,11 +201,7 @@ public: BoolModel* getMutedModel(); public slots: - virtual void setName( const QString & newName ) - { - m_name = newName; - emit nameChanged(); - } + virtual void setName(const QString& newName); void setMutedBeforeSolo(const bool muted) { diff --git a/include/TrackLabelButton.h b/include/TrackLabelButton.h index 0d1c6e163..e19fc6be9 100644 --- a/include/TrackLabelButton.h +++ b/include/TrackLabelButton.h @@ -55,9 +55,10 @@ protected: void mousePressEvent( QMouseEvent * _me ) override; void mouseDoubleClickEvent( QMouseEvent * _me ) override; void mouseReleaseEvent( QMouseEvent * _me ) override; - void paintEvent( QPaintEvent * _pe ) override; void resizeEvent( QResizeEvent * _re ) override; +private: + bool isInCompactMode() const; private: TrackView * m_trackView; diff --git a/include/TrackView.h b/include/TrackView.h index f697d9ea8..b2654202b 100644 --- a/include/TrackView.h +++ b/include/TrackView.h @@ -134,9 +134,12 @@ protected: void mousePressEvent( QMouseEvent * me ) override; void mouseMoveEvent( QMouseEvent * me ) override; void mouseReleaseEvent( QMouseEvent * me ) override; + void wheelEvent(QWheelEvent* we) override; void paintEvent( QPaintEvent * pe ) override; void resizeEvent( QResizeEvent * re ) override; +private: + void resizeToHeight(int height); private: enum class Action diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 13c1d6988..6c4ba465e 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -637,4 +637,19 @@ BoolModel *Track::getMutedModel() return &m_mutedModel; } +void Track::setName(const QString& newName) +{ + if (m_name != newName) + { + m_name = newName; + + if (auto song = Engine::getSong()) + { + song->setModified(); + } + + emit nameChanged(); + } +} + } // namespace lmms diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index a9ee1cb40..0a6fece31 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -46,6 +46,7 @@ namespace lmms::gui { +constexpr int BeatStepButtonOffset = 4; MidiClipView::MidiClipView( MidiClip* clip, TrackView* parent ) : ClipView( clip, parent ), @@ -246,7 +247,7 @@ void MidiClipView::mousePressEvent( QMouseEvent * _me ) { bool displayPattern = fixedClips() || (pixelsPerBar() >= 96 && m_legacySEPattern); if (_me->button() == Qt::LeftButton && m_clip->m_clipType == MidiClip::Type::BeatClip && displayPattern - && _me->y() > height() - m_stepBtnOff.height()) + && _me->y() > BeatStepButtonOffset && _me->y() < BeatStepButtonOffset + m_stepBtnOff.height()) // when mouse button is pressed in pattern mode @@ -478,7 +479,7 @@ void MidiClipView::paintEvent( QPaintEvent * ) // figure out x and y coordinates for step graphic const int x = BORDER_WIDTH + static_cast(it * w / steps); - const int y = height() - m_stepBtnOff.height() - 1; + const int y = BeatStepButtonOffset; if (n) { diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 12b6227ca..788991ed0 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -40,6 +40,7 @@ #include "Mixer.h" #include "MixerView.h" #include "GuiApplication.h" +#include "Instrument.h" #include "InstrumentTrack.h" #include "InstrumentTrackWindow.h" #include "MainWindow.h" @@ -62,7 +63,7 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_tlb = new TrackLabelButton( this, getTrackSettingsWidget() ); m_tlb->setCheckable( true ); - m_tlb->setIcon( embed::getIconPixmap( "instrument_track" ) ); + m_tlb->setIcon(determinePixmap(_it)); m_tlb->show(); connect( m_tlb, SIGNAL(toggled(bool)), @@ -142,7 +143,9 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); - auto layout = new QHBoxLayout(getTrackSettingsWidget()); + auto masterLayout = new QVBoxLayout(getTrackSettingsWidget()); + masterLayout->setContentsMargins(0, 1, 0, 0); + auto layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(m_tlb); @@ -150,6 +153,8 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV layout->addWidget(m_activityIndicator); layout->addWidget(m_volumeKnob); layout->addWidget(m_panningKnob); + masterLayout->addLayout(layout); + masterLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); connect( m_activityIndicator, SIGNAL(pressed()), this, SLOT(activityIndicatorPressed())); @@ -393,4 +398,27 @@ QMenu * InstrumentTrackView::createMixerMenu(QString title, QString newMixerLabe } +QPixmap InstrumentTrackView::determinePixmap(InstrumentTrack* instrumentTrack) +{ + if (instrumentTrack) + { + Instrument* instrument = instrumentTrack->instrument(); + + if (instrument && instrument->descriptor()) + { + const PixmapLoader* pl = instrument->key().isValid() + ? instrument->key().logo() + : instrument->descriptor()->logo; + + if (pl) + { + return pl->pixmap(); + } + } + } + + return embed::getIconPixmap("instrument_track"); +} + + } // namespace lmms::gui diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 45a695d11..8475f7fa9 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -86,7 +86,9 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); - auto layout = new QHBoxLayout(getTrackSettingsWidget()); + auto masterLayout = new QVBoxLayout(getTrackSettingsWidget()); + masterLayout->setContentsMargins(0, 1, 0, 0); + auto layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(m_tlb); @@ -94,6 +96,8 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : layout->addWidget(m_activityIndicator); layout->addWidget(m_volumeKnob); layout->addWidget(m_panningKnob); + masterLayout->addLayout(layout); + masterLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); connect(_t, SIGNAL(playingChanged()), this, SLOT(updateIndicator())); diff --git a/src/gui/tracks/TrackLabelButton.cpp b/src/gui/tracks/TrackLabelButton.cpp index 2a50a4aa2..087edba3d 100644 --- a/src/gui/tracks/TrackLabelButton.cpp +++ b/src/gui/tracks/TrackLabelButton.cpp @@ -30,13 +30,10 @@ #include "ConfigManager.h" #include "embed.h" -#include "Engine.h" -#include "Instrument.h" -#include "InstrumentTrack.h" #include "RenameDialog.h" -#include "Song.h" #include "TrackRenameLineEdit.h" #include "TrackView.h" +#include "Track.h" namespace lmms::gui { @@ -53,7 +50,7 @@ TrackLabelButton::TrackLabelButton( TrackView * _tv, QWidget * _parent ) : m_renameLineEdit = new TrackRenameLineEdit( this ); m_renameLineEdit->hide(); - if( ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt() ) + if (isInCompactMode()) { setFixedSize( 32, 29 ); } @@ -77,7 +74,7 @@ TrackLabelButton::TrackLabelButton( TrackView * _tv, QWidget * _parent ) : void TrackLabelButton::rename() { - if( ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt() ) + if (isInCompactMode()) { QString txt = m_trackView->getTrack()->name(); RenameDialog renameDlg( txt ); @@ -85,7 +82,6 @@ void TrackLabelButton::rename() if( txt != text() ) { m_trackView->getTrack()->setName( txt ); - Engine::getSong()->setModified(); } } else @@ -103,7 +99,7 @@ void TrackLabelButton::rename() void TrackLabelButton::renameFinished() { - if( !( ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt() ) ) + if (!isInCompactMode()) { m_renameLineEdit->clearFocus(); m_renameLineEdit->hide(); @@ -113,7 +109,6 @@ void TrackLabelButton::renameFinished() { setText( elideName( m_renameLineEdit->text() ) ); m_trackView->getTrack()->setName( m_renameLineEdit->text() ); - Engine::getSong()->setModified(); } } } @@ -185,35 +180,6 @@ void TrackLabelButton::mouseReleaseEvent( QMouseEvent *_me ) -void TrackLabelButton::paintEvent( QPaintEvent * _pe ) -{ - if( m_trackView->getTrack()->type() == Track::Type::Instrument ) - { - auto it = dynamic_cast(m_trackView->getTrack()); - const PixmapLoader * pl; - auto get_logo = [](InstrumentTrack* it) -> const PixmapLoader* - { - return it->instrument()->key().isValid() - ? it->instrument()->key().logo() - : it->instrument()->descriptor()->logo; - }; - if( it && it->instrument() && - it->instrument()->descriptor() && - ( pl = get_logo(it) ) ) - { - if( pl->pixmapName() != m_iconName ) - { - m_iconName = pl->pixmapName(); - setIcon( pl->pixmap() ); - } - } - } - QToolButton::paintEvent( _pe ); -} - - - - void TrackLabelButton::resizeEvent(QResizeEvent *_re) { setText( elideName( m_trackView->getTrack()->displayName() ) ); @@ -237,5 +203,9 @@ QString TrackLabelButton::elideName( const QString &name ) return elidedName; } +bool TrackLabelButton::isInCompactMode() const +{ + return ConfigManager::inst()->value("ui", "compacttrackbuttons").toInt(); +} } // namespace lmms::gui diff --git a/src/gui/tracks/TrackView.cpp b/src/gui/tracks/TrackView.cpp index 426be7e36..08ad01c9c 100644 --- a/src/gui/tracks/TrackView.cpp +++ b/src/gui/tracks/TrackView.cpp @@ -364,9 +364,7 @@ void TrackView::mouseMoveEvent( QMouseEvent * me ) } else if( m_action == Action::Resize ) { - setFixedHeight( qMax( me->y(), MINIMAL_TRACK_HEIGHT ) ); - m_trackContainerView->realignTracks(); - m_track->setHeight( height() ); + resizeToHeight(me->y()); } if( height() < DEFAULT_TRACK_HEIGHT ) @@ -393,6 +391,22 @@ void TrackView::mouseReleaseEvent( QMouseEvent * me ) QWidget::mouseReleaseEvent( me ); } +void TrackView::wheelEvent(QWheelEvent* we) +{ + we->accept(); + + const int deltaY = we->angleDelta().y(); + int const direction = deltaY < 0 ? -1 : 1; + + auto const modKeys = we->modifiers(); + int stepSize = modKeys == Qt::ControlModifier ? 1 : modKeys == Qt::ShiftModifier ? 5 : 0; + + if (stepSize != 0) + { + resizeToHeight(height() + stepSize * direction); + } +} + @@ -445,4 +459,12 @@ void TrackView::setIndicatorMute(FadeButton* indicator, bool muted) } +void TrackView::resizeToHeight(int h) +{ + setFixedHeight(qMax(h, MINIMAL_TRACK_HEIGHT)); + m_trackContainerView->realignTracks(); + m_track->setHeight(height()); +} + + } // namespace lmms::gui From 876a36cebf0c89fe0102d2da8b6928a438309299 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Sun, 18 Feb 2024 21:14:37 +0000 Subject: [PATCH 092/191] Remove message location check from check-strings test (#7112) * Ignore message location in check-strings test * Support running test verification script on Windows --- tests/scripted/check-strings | 13 +++---------- tests/scripted/verify | 24 ++++++------------------ 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/tests/scripted/check-strings b/tests/scripted/check-strings index 5504cc588..fe15d908d 100755 --- a/tests/scripted/check-strings +++ b/tests/scripted/check-strings @@ -71,19 +71,12 @@ for p in re.findall(r'\[submodule "([^"]+)"\]\s*$', Path('.gitmodules').read_tex caption('locale') -filenames = set() classes_found = set() for cur_file in Path('data/locale').glob('*.ts'): tree = ElementTree.parse(str(cur_file)) root = tree.getroot() - for location in root.findall('./context/message/location'): - filenames.add(location.attrib['filename']) for location in root.findall('./context/name'): classes_found.add(location.text) -for f in sorted(filenames): - # The files sometimes are relative to data/local and sometimes to the git tree's root... - if not Path(f).is_file() and not Path(f'data/locale/{f}').is_file(): - error('data/locale', f'Source file does not exist: {f}') for c in sorted(classes_found): if is_our_class(c) and '::' not in c and c not in classes: error('data/locale', f'Class does not exist in source code: {c}') @@ -119,14 +112,14 @@ for theme in sorted([d for d in Path('data/themes').iterdir() if d.is_dir()]): if str(c.value).startswith(GUI_NAMESPACE_PREFIX): classes_in_sheet.add(unscope_classname(stylesheet, c.value)) else: - error(str(stylesheet), f"Namespace prefix missing from class {c.value}") + error(stylesheet.as_posix(), f"Namespace prefix missing from class {c.value}") class_found = True # After whitespace or comma comes a new class elif c.type == 'whitespace' or (c.type == 'literal' and c.value == ','): class_found = False missing_classes = classes_in_sheet - classes for class_in_sheet in sorted(missing_classes): - error(str(stylesheet), f'Class does not exist in source code: {class_in_sheet}') + error(stylesheet.as_posix(), f'Class does not exist in source code: {class_in_sheet}') caption('patches (checks only plugins/)') @@ -145,7 +138,7 @@ for cur_file in sorted(Path('.').glob('*/patches/*.patch')): if mpath.parent == Path('plugins/LadspaEffect/swh/ladspa/'): mpath = mpath.with_suffix('.xml') if not mpath.is_file(): - error(str(cur_file), f'Source file does not exist: {str(mpath)}') + error(cur_file.as_posix(), f'Source file does not exist: {mpath.as_posix()}') caption('debian docs (only one string)') diff --git a/tests/scripted/verify b/tests/scripted/verify index 3f6d60103..57c33dc8e 100755 --- a/tests/scripted/verify +++ b/tests/scripted/verify @@ -4,6 +4,7 @@ import subprocess from pathlib import Path import tempfile import os +import sys def set_git_config(): @@ -17,7 +18,7 @@ def set_git_config(): def create_file(filename: str, file_content: str): """Create a file in the current directory and adds it to git""" Path(filename).parent.mkdir(parents=True, exist_ok=True) - with open(filename, "w") as textfile: + with open(filename, "w", encoding='utf-8') as textfile: print(file_content, file=textfile) subprocess.run(['git', 'add', filename], check=True) @@ -64,7 +65,10 @@ class ScriptTest(): def run(self, expected_returncode: int = 1): # default: something goes wrong ("to the safe side") """Run the script, check the exit code and store the result""" - self.result = subprocess.run([str(self.scriptpath)], capture_output=True, text=True) + command = [str(self.scriptpath)] + if os.name == 'nt': + command.insert(0, sys.executable) + self.result = subprocess.run(command, capture_output=True, text=True) print('--->8--- Script output BEGIN --->8---') print(self.result.stdout) print('--->8--- Script output END --->8---') @@ -102,22 +106,6 @@ with tempfile.TemporaryDirectory() as tmpdir: test.run(0) # exitcode 0 - no errors expected test.expect('0 errors') - with ScriptTest(check_strings) as test: - create_file('data/locale/fr.ts', - '\n' - ' \n' - ' TestClass\n' - ' \n' - ' \n' - ' About LMMS\n' - ' À propos de LMMS\n' - ' \n' - '\n' - '\n') - test.run() - test.expect('Error: data/locale: Source file does not exist: ../../src/core/non-existent.cpp') - test.expect('1 errors') - with ScriptTest(check_strings) as test: create_file('data/locale/fr.ts', '\n' From 360254fd81cda14cbcec3fff3dff5456005ebfc8 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Mon, 19 Feb 2024 09:03:25 -0800 Subject: [PATCH 093/191] Rewrite EffectSelectDialog to add effect groups and delete Qt Designer UI file (#7024) --- include/EffectSelectDialog.h | 80 +++++-- src/gui/CMakeLists.txt | 1 - src/gui/modals/EffectSelectDialog.cpp | 290 +++++++++++++++----------- src/gui/modals/EffectSelectDialog.ui | 109 ---------- 4 files changed, 227 insertions(+), 253 deletions(-) delete mode 100644 src/gui/modals/EffectSelectDialog.ui diff --git a/include/EffectSelectDialog.h b/include/EffectSelectDialog.h index 493b07774..db8e60f0d 100644 --- a/include/EffectSelectDialog.h +++ b/include/EffectSelectDialog.h @@ -2,6 +2,7 @@ * EffectSelectDialog.h - dialog to choose effect plugin * * Copyright (c) 2006-2009 Tobias Doerffel + * Copyright (c) 2023 Lost Robot * * This file is part of LMMS - https://lmms.io * @@ -25,49 +26,86 @@ #ifndef LMMS_GUI_EFFECT_SELECT_DIALOG_H #define LMMS_GUI_EFFECT_SELECT_DIALOG_H -#include -#include -#include - #include "Effect.h" - -namespace Ui { class EffectSelectDialog; } +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace lmms::gui { +class DualColumnFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + DualColumnFilterProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) + { + } + + void setEffectTypeFilter(const QString& filter) + { + m_effectTypeFilter = filter; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override + { + QModelIndex nameIndex = sourceModel()->index(source_row, 0, source_parent); + QModelIndex typeIndex = sourceModel()->index(source_row, 1, source_parent); + + QString name = sourceModel()->data(nameIndex, Qt::DisplayRole).toString(); + QString type = sourceModel()->data(typeIndex, Qt::DisplayRole).toString(); + + QRegExp nameRegExp(filterRegExp()); + nameRegExp.setCaseSensitivity(Qt::CaseInsensitive); + + bool nameFilterPassed = nameRegExp.indexIn(name) != -1; + bool typeFilterPassed = type.contains(m_effectTypeFilter, Qt::CaseInsensitive); + + return nameFilterPassed && typeFilterPassed; + } + +private: + QString m_effectTypeFilter; +}; + class EffectSelectDialog : public QDialog { Q_OBJECT public: - EffectSelectDialog( QWidget * _parent ); - ~EffectSelectDialog() override; - - Effect * instantiateSelectedPlugin( EffectChain * _parent ); + EffectSelectDialog(QWidget* parent); + Effect* instantiateSelectedPlugin(EffectChain* parent); protected slots: void acceptSelection(); - void rowChanged( const QModelIndex &, const QModelIndex & ); - void sortAgain(); + void rowChanged(const QModelIndex&, const QModelIndex&); void updateSelection(); - + + bool eventFilter(QObject* obj, QEvent* event) override; private: - Ui::EffectSelectDialog * ui; - EffectKeyList m_effectKeys; EffectKey m_currentSelection; QStandardItemModel m_sourceModel; - QSortFilterProxyModel m_model; - QWidget * m_descriptionWidget; - -} ; - + DualColumnFilterProxyModel m_model; + QWidget* m_descriptionWidget; + QTableView* m_pluginList; + QScrollArea* m_scrollArea; + QLineEdit* m_filterEdit; +}; } // namespace lmms::gui -#endif // LMMS_GUI_EFFECT_SELECT_DIALOG_H +#endif diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 001c92c79..e2342b465 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -132,7 +132,6 @@ SET(LMMS_SRCS set(LMMS_UIS ${LMMS_UIS} gui/modals/about_dialog.ui - gui/modals/EffectSelectDialog.ui gui/modals/export_project.ui PARENT_SCOPE diff --git a/src/gui/modals/EffectSelectDialog.cpp b/src/gui/modals/EffectSelectDialog.cpp index 993052fab..4e6427e81 100644 --- a/src/gui/modals/EffectSelectDialog.cpp +++ b/src/gui/modals/EffectSelectDialog.cpp @@ -2,6 +2,7 @@ * EffectSelectDialog.cpp - dialog to choose effect plugin * * Copyright (c) 2006-2009 Tobias Doerffel + * Copyright (c) 2023 Lost Robot * * This file is part of LMMS - https://lmms.io * @@ -23,63 +24,62 @@ */ #include "EffectSelectDialog.h" - -#include "ui_EffectSelectDialog.h" - #include "DummyEffect.h" #include "EffectChain.h" #include "embed.h" #include "PluginFactory.h" +#include +#include +#include #include +#include +#include +#include namespace lmms::gui { - -EffectSelectDialog::EffectSelectDialog( QWidget * _parent ) : - QDialog( _parent ), - ui( new Ui::EffectSelectDialog ), +EffectSelectDialog::EffectSelectDialog(QWidget* parent) : + QDialog(parent), + m_effectKeys(), + m_currentSelection(), m_sourceModel(), m_model(), - m_descriptionWidget( nullptr ) + m_descriptionWidget(nullptr), + m_pluginList(new QTableView(this)), + m_scrollArea(new QScrollArea(this)) { - ui->setupUi( this ); - - setWindowIcon( embed::getIconPixmap( "setup_audio" ) ); - - // query effects + setWindowTitle(tr("Add effect")); + resize(640, 480); + + setWindowIcon(embed::getIconPixmap("setup_audio")); + // Query effects EffectKeyList subPluginEffectKeys; - - for (const Plugin::Descriptor* desc: getPluginFactory()->descriptors(Plugin::Type::Effect)) + for (const auto desc : getPluginFactory()->descriptors(Plugin::Type::Effect)) { - if( desc->subPluginFeatures ) + if (desc->subPluginFeatures) { - desc->subPluginFeatures->listSubPluginKeys( - desc, - subPluginEffectKeys ); + desc->subPluginFeatures->listSubPluginKeys(desc, subPluginEffectKeys); } else { - m_effectKeys << EffectKey( desc, desc->name ); - + m_effectKeys << EffectKey(desc, desc->name); } } - m_effectKeys += subPluginEffectKeys; - // and fill our source model - m_sourceModel.setHorizontalHeaderItem( 0, new QStandardItem( tr( "Name" ) ) ); - m_sourceModel.setHorizontalHeaderItem( 1, new QStandardItem( tr( "Type" ) ) ); + // Fill the source model + m_sourceModel.setHorizontalHeaderItem(0, new QStandardItem(tr("Name"))); + m_sourceModel.setHorizontalHeaderItem(1, new QStandardItem(tr("Type"))); int row = 0; - for( EffectKeyList::ConstIterator it = m_effectKeys.begin(); - it != m_effectKeys.end(); ++it ) + for (EffectKeyList::ConstIterator it = m_effectKeys.begin(); it != m_effectKeys.end(); ++it) { QString name; QString type; - if( it->desc->subPluginFeatures ) + if (it->desc->subPluginFeatures) { name = it->displayName(); type = it->desc->displayName; @@ -89,114 +89,148 @@ EffectSelectDialog::EffectSelectDialog( QWidget * _parent ) : name = it->desc->displayName; type = "LMMS"; } - m_sourceModel.setItem( row, 0, new QStandardItem( name ) ); - m_sourceModel.setItem( row, 1, new QStandardItem( type ) ); + m_sourceModel.setItem(row, 0, new QStandardItem(name)); + m_sourceModel.setItem(row, 1, new QStandardItem(type)); ++row; } - // setup filtering - m_model.setSourceModel( &m_sourceModel ); - m_model.setFilterCaseSensitivity( Qt::CaseInsensitive ); + // Setup filtering + m_model.setSourceModel(&m_sourceModel); + m_model.setFilterCaseSensitivity(Qt::CaseInsensitive); - ui->filterEdit->setPlaceholderText(tr("Search")); - ui->filterEdit->setClearButtonEnabled(true); - ui->filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); + QHBoxLayout* mainLayout = new QHBoxLayout(this); - connect(ui->filterEdit, &QLineEdit::textChanged, &m_model, &QSortFilterProxyModel::setFilterFixedString); - connect(ui->filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::updateSelection); - connect(ui->filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::sortAgain); + QVBoxLayout* leftSectionLayout = new QVBoxLayout(); - ui->pluginList->setModel( &m_model ); + QStringList buttonLabels = { tr("All"), "LMMS", "LADSPA", "LV2", "VST" }; + QStringList buttonSearchString = { "", "LMMS", "LADSPA", "LV2", "VST" }; + + for (int i = 0; i < buttonLabels.size(); ++i) + { + const QString& label = buttonLabels[i]; + const QString& searchString = buttonSearchString[i]; + + QPushButton* button = new QPushButton(label, this); + button->setFixedSize(50, 50); + button->setFocusPolicy(Qt::NoFocus); + leftSectionLayout->addWidget(button); + + connect(button, &QPushButton::clicked, this, [this, searchString] { + m_model.setEffectTypeFilter(searchString); + updateSelection(); + }); + } + + leftSectionLayout->addStretch();// Add stretch to the button layout to push buttons to the top + mainLayout->addLayout(leftSectionLayout); + + m_filterEdit = new QLineEdit(this); + connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &text) { + m_model.setFilterRegExp(QRegExp(text, Qt::CaseInsensitive)); + }); + connect(m_filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::updateSelection); + m_filterEdit->setFocus(); + m_filterEdit->setFocusPolicy(Qt::StrongFocus); + m_filterEdit->setPlaceholderText(tr("Search")); + m_filterEdit->setClearButtonEnabled(true); + m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); + + m_pluginList->setModel(&m_model); + m_pluginList->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_pluginList->setSelectionBehavior(QAbstractItemView::SelectRows); + m_pluginList->setSelectionMode(QAbstractItemView::SingleSelection); + m_pluginList->setSortingEnabled(true); + m_pluginList->sortByColumn(0, Qt::AscendingOrder); // Initial sort by column 0 (Name) + m_pluginList->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_pluginList->verticalHeader()->hide(); + m_pluginList->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_pluginList->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + m_pluginList->setFocusPolicy(Qt::NoFocus); + + // Scroll Area + m_scrollArea->setWidgetResizable(true); + QWidget* scrollAreaWidgetContents = new QWidget(m_scrollArea); + scrollAreaWidgetContents->setObjectName("scrollAreaWidgetContents"); + m_scrollArea->setWidget(scrollAreaWidgetContents); + m_scrollArea->setMaximumHeight(180); + m_scrollArea->setFocusPolicy(Qt::NoFocus); + + // Button Box + QDialogButtonBox* buttonBox = new QDialogButtonBox(Qt::Horizontal, this); + buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + buttonBox->setFocusPolicy(Qt::NoFocus); + connect(buttonBox, &QDialogButtonBox::accepted, this, &EffectSelectDialog::acceptSelection); + connect(buttonBox, &QDialogButtonBox::rejected, this, &EffectSelectDialog::reject); + + QVBoxLayout* rightSectionLayout = new QVBoxLayout(); + rightSectionLayout->addWidget(m_filterEdit); + rightSectionLayout->addWidget(m_pluginList); + rightSectionLayout->addWidget(m_scrollArea); + rightSectionLayout->addWidget(buttonBox); + mainLayout->addLayout(rightSectionLayout); + + setLayout(mainLayout); - // setup selection model auto selectionModel = new QItemSelectionModel(&m_model); - ui->pluginList->setSelectionModel( selectionModel ); - connect( selectionModel, SIGNAL( currentRowChanged( const QModelIndex&, - const QModelIndex & ) ), - SLOT( rowChanged( const QModelIndex &, const QModelIndex& ) ) ); - connect( ui->pluginList, SIGNAL( doubleClicked( const QModelIndex& ) ), - SLOT(acceptSelection())); + m_pluginList->setSelectionModel(selectionModel); + connect(selectionModel, &QItemSelectionModel::currentRowChanged, + this, &EffectSelectDialog::rowChanged); - // try to accept current selection when pressing "OK" - connect( ui->buttonBox, SIGNAL(accepted()), - this, SLOT(acceptSelection())); - - ui->filterEdit->setClearButtonEnabled( true ); - ui->pluginList->verticalHeader()->setSectionResizeMode( - QHeaderView::ResizeToContents ); - ui->pluginList->verticalHeader()->hide(); - ui->pluginList->horizontalHeader()->setSectionResizeMode( 0, - QHeaderView::Stretch ); - ui->pluginList->horizontalHeader()->setSectionResizeMode( 1, - QHeaderView::ResizeToContents ); - ui->pluginList->sortByColumn( 0, Qt::AscendingOrder ); + connect(m_pluginList, &QTableView::doubleClicked, + this, &EffectSelectDialog::acceptSelection); + + setModal(true); + installEventFilter(this); updateSelection(); show(); } - - -EffectSelectDialog::~EffectSelectDialog() -{ - delete ui; -} - - - - -Effect * EffectSelectDialog::instantiateSelectedPlugin( EffectChain * _parent ) +Effect* EffectSelectDialog::instantiateSelectedPlugin(EffectChain* parent) { Effect* result = nullptr; - if(!m_currentSelection.name.isEmpty() && m_currentSelection.desc) + if (!m_currentSelection.name.isEmpty() && m_currentSelection.desc) { - result = Effect::instantiate(m_currentSelection.desc->name, - _parent, &m_currentSelection); + result = Effect::instantiate(m_currentSelection.desc->name, parent, &m_currentSelection); } - if(!result) + if (!result) { - result = new DummyEffect(_parent, QDomElement()); + result = new DummyEffect(parent, QDomElement()); } return result; } - - - void EffectSelectDialog::acceptSelection() { - if( m_currentSelection.isValid() ) + if (m_currentSelection.isValid()) { accept(); } } - - - -void EffectSelectDialog::rowChanged( const QModelIndex & _idx, - const QModelIndex & ) +void EffectSelectDialog::rowChanged(const QModelIndex& idx, const QModelIndex&) { delete m_descriptionWidget; m_descriptionWidget = nullptr; - if( m_model.mapToSource( _idx ).row() < 0 ) + if (m_model.mapToSource(idx).row() < 0) { - // invalidate current selection + // Invalidate current selection m_currentSelection = Plugin::Descriptor::SubPluginFeatures::Key(); } else { - m_currentSelection = m_effectKeys[m_model.mapToSource( _idx ).row()]; + m_currentSelection = m_effectKeys[m_model.mapToSource(idx).row()]; } - if( m_currentSelection.desc ) + if (m_currentSelection.desc) { m_descriptionWidget = new QWidget; auto hbox = new QHBoxLayout(m_descriptionWidget); + hbox->setAlignment(Qt::AlignTop); - Plugin::Descriptor const & descriptor = *( m_currentSelection.desc ); + Plugin::Descriptor const& descriptor = *(m_currentSelection.desc); const PixmapLoader* pixLoa = m_currentSelection.logo(); if (pixLoa) @@ -204,76 +238,88 @@ void EffectSelectDialog::rowChanged( const QModelIndex & _idx, auto logoLabel = new QLabel(m_descriptionWidget); logoLabel->setPixmap(pixLoa->pixmap()); logoLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + logoLabel->setMaximumSize(64, 64); - hbox->addWidget( logoLabel ); - hbox->setAlignment( logoLabel, Qt::AlignTop); + hbox->addWidget(logoLabel); } auto textualInfoWidget = new QWidget(m_descriptionWidget); - hbox->addWidget(textualInfoWidget); auto textWidgetLayout = new QVBoxLayout(textualInfoWidget); textWidgetLayout->setContentsMargins(4, 4, 4, 4); - textWidgetLayout->setSpacing( 0 ); + textWidgetLayout->setSpacing(8); - if ( m_currentSelection.desc->subPluginFeatures ) + if (m_currentSelection.desc->subPluginFeatures) { auto subWidget = new QWidget(textualInfoWidget); auto subLayout = new QVBoxLayout(subWidget); - subLayout->setContentsMargins(4, 4, 4, 4); - subLayout->setSpacing( 0 ); + subLayout->setContentsMargins(0, 0, 0, 0); + subLayout->setSpacing(8); + m_currentSelection.desc->subPluginFeatures-> - fillDescriptionWidget( subWidget, &m_currentSelection ); - for( QWidget * w : subWidget->findChildren() ) + fillDescriptionWidget(subWidget, &m_currentSelection); + for (QWidget* w : subWidget->findChildren()) { - if( w->parent() == subWidget ) + if (w->parent() == subWidget) { - subLayout->addWidget( w ); + subLayout->addWidget(w); + subLayout->setAlignment(w, QFlags(Qt::AlignTop | Qt::AlignLeft)); } } - textWidgetLayout->addWidget(subWidget); } else { auto label = new QLabel(m_descriptionWidget); QString labelText = "

" + tr("Name") + ": " + QString::fromUtf8(descriptor.displayName) + "

"; - labelText += "

" + tr("Description") + ": " + qApp->translate( "PluginBrowser", descriptor.description ) + "

"; + labelText += "

" + tr("Description") + ": " + qApp->translate("PluginBrowser", descriptor.description) + "

"; labelText += "

" + tr("Author") + ": " + QString::fromUtf8(descriptor.author) + "

"; label->setText(labelText); + label->setWordWrap(true); + textWidgetLayout->addWidget(label); } - ui->scrollArea->setWidget( m_descriptionWidget ); + m_scrollArea->setWidget(m_descriptionWidget); m_descriptionWidget->show(); } } - - - -void EffectSelectDialog::sortAgain() -{ - ui->pluginList->setSortingEnabled( ui->pluginList->isSortingEnabled() ); -} - - - - void EffectSelectDialog::updateSelection() { - // no valid selection anymore due to changed filter? - if( ui->pluginList->selectionModel()->selection().size() <= 0 ) + // No valid selection anymore due to changed filter? + if (m_pluginList->selectionModel()->selection().size() <= 0) { - // then select our first item - ui->pluginList->selectionModel()->select( m_model.index( 0, 0 ), - QItemSelectionModel::ClearAndSelect - | QItemSelectionModel::Rows ); - rowChanged( m_model.index( 0, 0 ), QModelIndex() ); + // Then select our first item + m_pluginList->selectionModel()-> + select(m_model.index(0, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + rowChanged(m_model.index(0, 0), QModelIndex()); } } +bool EffectSelectDialog::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == this && event->type() == QEvent::KeyPress) + { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down) + { + QItemSelectionModel* selectionModel = m_pluginList->selectionModel(); + int currentRow = selectionModel->currentIndex().row(); + int newRow = (keyEvent->key() == Qt::Key_Up) ? currentRow - 1 : currentRow + 1; + int rowCount = m_pluginList->model()->rowCount(); + newRow = qBound(0, newRow, rowCount - 1); + + selectionModel->setCurrentIndex(m_pluginList->model()->index(newRow, 0), + QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + m_pluginList->scrollTo(m_pluginList->model()->index(newRow, 0)); + return true; + } + } + + return QDialog::eventFilter(obj, event); +} } // namespace lmms::gui diff --git a/src/gui/modals/EffectSelectDialog.ui b/src/gui/modals/EffectSelectDialog.ui deleted file mode 100644 index b0433e66c..000000000 --- a/src/gui/modals/EffectSelectDialog.ui +++ /dev/null @@ -1,109 +0,0 @@ - - - EffectSelectDialog - - - - 0 - 0 - 585 - 550 - - - - Add effect - - - true - - - - 10 - - - - - - - - - 500 - 250 - - - - QAbstractItemView::NoEditTriggers - - - Qt::ScrollBarAlwaysOff - - - QAbstractItemView::SelectRows - - - QAbstractItemView::SingleSelection - - - false - - - true - - - - - - - QFrame::NoFrame - - - - - 0 - 0 - 497 - 109 - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - rejected() - EffectSelectDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - - - RowTableView - QTableView -
RowTableView.h
-
-
-
From cbaf2f09194adc22e6a9fa14f307f2e51edaaf9f Mon Sep 17 00:00:00 2001 From: Monospace-V <76674645+Monospace-V@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:10:56 +0530 Subject: [PATCH 094/191] Update src/common/ to coding conventions (#7082) * basic whitespace removal on RemoteBasePlugin.cpp (src/common/RemoteBasePlugin.cpp) * Additional RPB. (src/common/RemotePluginBase.cpp). * Additional RemotePluginBase.cpp cleaning (src/common/SharedMemory.cpp) * something more --- src/common/RemotePluginBase.cpp | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/common/RemotePluginBase.cpp b/src/common/RemotePluginBase.cpp index a4739a63c..97ae8ac9e 100644 --- a/src/common/RemotePluginBase.cpp +++ b/src/common/RemotePluginBase.cpp @@ -34,23 +34,23 @@ namespace lmms #ifdef SYNC_WITH_SHM_FIFO -RemotePluginBase::RemotePluginBase( shmFifo * _in, shmFifo * _out ) : - m_in( _in ), - m_out( _out ) +RemotePluginBase::RemotePluginBase(shmFifo * _in, shmFifo * _out) : + m_in(_in), + m_out(_out) #else RemotePluginBase::RemotePluginBase() : - m_socket( -1 ), - m_invalid( false ) + m_socket(-1), + m_invalid(false) #endif { #ifdef LMMS_HAVE_LOCALE_H // make sure, we're using common ways to print/scan // floats to/from strings (',' vs. '.' for decimal point etc.) - setlocale( LC_NUMERIC, "C" ); + setlocale(LC_NUMERIC, "C"); #endif #ifndef SYNC_WITH_SHM_FIFO - pthread_mutex_init( &m_receiveMutex, nullptr ); - pthread_mutex_init( &m_sendMutex, nullptr ); + pthread_mutex_init(&m_receiveMutex, nullptr); + pthread_mutex_init(&m_sendMutex, nullptr); #endif } @@ -63,39 +63,39 @@ RemotePluginBase::~RemotePluginBase() delete m_in; delete m_out; #else - pthread_mutex_destroy( &m_receiveMutex ); - pthread_mutex_destroy( &m_sendMutex ); + pthread_mutex_destroy(&m_receiveMutex); + pthread_mutex_destroy(&m_sendMutex); #endif } -int RemotePluginBase::sendMessage( const message & _m ) +int RemotePluginBase::sendMessage(const message & _m) { #ifdef SYNC_WITH_SHM_FIFO m_out->lock(); - m_out->writeInt( _m.id ); - m_out->writeInt( _m.data.size() ); + m_out->writeInt(_m.id); + m_out->writeInt(_m.data.size()); int j = 8; - for( unsigned int i = 0; i < _m.data.size(); ++i ) + for (unsigned int i = 0; i < _m.data.size(); ++i) { - m_out->writeString( _m.data[i] ); + m_out->writeString(_m.data[i]); j += 4 + _m.data[i].size(); } m_out->unlock(); m_out->messageSent(); #else - pthread_mutex_lock( &m_sendMutex ); - writeInt( _m.id ); - writeInt( _m.data.size() ); + pthread_mutex_lock(&m_sendMutex); + writeInt(_m.id); + writeInt(_m.data.size()); int j = 8; for (const auto& str : _m.data) { writeString(str); j += 4 + str.size(); } - pthread_mutex_unlock( &m_sendMutex ); + pthread_mutex_unlock(&m_sendMutex); #endif return j; @@ -112,21 +112,21 @@ RemotePluginBase::message RemotePluginBase::receiveMessage() message m; m.id = m_in->readInt(); const int s = m_in->readInt(); - for( int i = 0; i < s; ++i ) + for (int i = 0; i < s; ++i) { - m.data.push_back( m_in->readString() ); + m.data.push_back(m_in->readString()); } m_in->unlock(); #else - pthread_mutex_lock( &m_receiveMutex ); + pthread_mutex_lock(&m_receiveMutex); message m; m.id = readInt(); const int s = readInt(); - for( int i = 0; i < s; ++i ) + for (int i = 0; i < s; ++i) { - m.data.push_back( readString() ); + m.data.push_back(readString()); } - pthread_mutex_unlock( &m_receiveMutex ); + pthread_mutex_unlock(&m_receiveMutex); #endif return m; } @@ -136,10 +136,10 @@ RemotePluginBase::message RemotePluginBase::receiveMessage() RemotePluginBase::message RemotePluginBase::waitForMessage( const message & _wm, - bool _busy_waiting ) + bool _busy_waiting) { #ifndef BUILD_REMOTE_PLUGIN_CLIENT - if( _busy_waiting ) + if (_busy_waiting) { // No point processing events outside of the main thread _busy_waiting = QThread::currentThread() == @@ -148,41 +148,41 @@ RemotePluginBase::message RemotePluginBase::waitForMessage( struct WaitDepthCounter { - WaitDepthCounter( int & depth, bool busy ) : - m_depth( depth ), - m_busy( busy ) + WaitDepthCounter(int & depth, bool busy) : + m_depth(depth), + m_busy(busy) { - if( m_busy ) { ++m_depth; } + if (m_busy) { ++m_depth; } } ~WaitDepthCounter() { - if( m_busy ) { --m_depth; } + if (m_busy) { --m_depth; } } int & m_depth; bool m_busy; }; - WaitDepthCounter wdc( waitDepthCounter(), _busy_waiting ); + WaitDepthCounter wdc(waitDepthCounter(), _busy_waiting); #endif - while( !isInvalid() ) + while (!isInvalid()) { #ifndef BUILD_REMOTE_PLUGIN_CLIENT - if( _busy_waiting && !messagesLeft() ) + if (_busy_waiting && !messagesLeft()) { QCoreApplication::processEvents( - QEventLoop::ExcludeUserInputEvents, 50 ); + QEventLoop::ExcludeUserInputEvents, 50); continue; } #endif message m = receiveMessage(); - processMessage( m ); - if( m.id == _wm.id ) + processMessage(m); + if (m.id == _wm.id) { return m; } - else if( m.id == IdUndefined ) + else if (m.id == IdUndefined) { return m; } From 7318af0fe9469b47ccf00d943105212a776705ff Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Fri, 23 Feb 2024 14:57:41 -0600 Subject: [PATCH 095/191] Fix invalidated iterator when removing notes in Piano Roll (#7080) * Fix invalidated iterator when removing notes in Piano Roll * fixup - typo * Add MidiClip::removeNote(iterator) function * Use iterator version of remoteNote * Fix parameter name * Change variable name again --- include/MidiClip.h | 3 ++- src/gui/editors/PianoRoll.cpp | 2 +- src/tracks/MidiClip.cpp | 27 +++++++++++++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/include/MidiClip.h b/include/MidiClip.h index c2287bd00..b3ed0d84a 100644 --- a/include/MidiClip.h +++ b/include/MidiClip.h @@ -63,7 +63,8 @@ public: // note management Note * addNote( const Note & _new_note, const bool _quant_pos = true ); - void removeNote( Note * _note_to_del ); + NoteVector::const_iterator removeNote(NoteVector::const_iterator it); + NoteVector::const_iterator removeNote(Note* note); Note * noteAtStep( int _step ); diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 8f2d964ff..ff8832325 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -2646,7 +2646,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) ) { // delete this note - m_midiClip->removeNote( note ); + it = m_midiClip->removeNote(it); Engine::getSong()->setModified(); } else diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index b5e764b17..dfee9a5e6 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -208,16 +208,30 @@ Note * MidiClip::addNote( const Note & _new_note, const bool _quant_pos ) -void MidiClip::removeNote( Note * _note_to_del ) +NoteVector::const_iterator MidiClip::removeNote(NoteVector::const_iterator it) +{ + instrumentTrack()->lock(); + delete *it; + auto new_it = m_notes.erase(it); + instrumentTrack()->unlock(); + + checkType(); + updateLength(); + + emit dataChanged(); + return new_it; +} + +NoteVector::const_iterator MidiClip::removeNote(Note* note) { instrumentTrack()->lock(); - m_notes.erase(std::remove_if(m_notes.begin(), m_notes.end(), [&](Note* note) + auto it = std::find(m_notes.begin(), m_notes.end(), note); + if (it != m_notes.end()) { - auto shouldRemove = note == _note_to_del; - if (shouldRemove) { delete note; } - return shouldRemove; - }), m_notes.end()); + delete *it; + it = m_notes.erase(it); + } instrumentTrack()->unlock(); @@ -225,6 +239,7 @@ void MidiClip::removeNote( Note * _note_to_del ) updateLength(); emit dataChanged(); + return it; } From d4ae82283b5b2e918391bc5d30a7557c18aba563 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 24 Feb 2024 11:03:38 +0100 Subject: [PATCH 096/191] Reenable song editor zooming (#7118) Reenable zooming in the song editor by * only resizing the track if the alt modifier is pressed and * only accepting the wheel even if we in fact resize the track. By doing so all other wheel events bubble up so that zooming and scrolling of the song editor can be handled by parent components. The code that handles the retrieval of the delta value had to be adjusted as well. The reason is that pressing the alt key messes with the way that deltas are reported in the wheel event. The wheel event is interpreted as a horizontal scroll when alt is pressed. --- src/gui/tracks/TrackView.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gui/tracks/TrackView.cpp b/src/gui/tracks/TrackView.cpp index 08ad01c9c..e23236021 100644 --- a/src/gui/tracks/TrackView.cpp +++ b/src/gui/tracks/TrackView.cpp @@ -393,17 +393,18 @@ void TrackView::mouseReleaseEvent( QMouseEvent * me ) void TrackView::wheelEvent(QWheelEvent* we) { - we->accept(); - - const int deltaY = we->angleDelta().y(); + // Note: we add the values because one of them will be 0. If the alt modifier + // is pressed x is non-zero and otherwise y. + const int deltaY = we->angleDelta().x() + we->angleDelta().y(); int const direction = deltaY < 0 ? -1 : 1; auto const modKeys = we->modifiers(); - int stepSize = modKeys == Qt::ControlModifier ? 1 : modKeys == Qt::ShiftModifier ? 5 : 0; + int stepSize = modKeys == (Qt::ControlModifier | Qt::AltModifier) ? 1 : modKeys == (Qt::ShiftModifier | Qt::AltModifier) ? 5 : 0; if (stepSize != 0) { resizeToHeight(height() + stepSize * direction); + we->accept(); } } From 03d067b10574a0e61d38036f56f0d7c2564808f5 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 24 Feb 2024 11:05:54 +0100 Subject: [PATCH 097/191] Fix crash in Audio File Processor (#7124) * Fix crash in Audio File Processor Fix a crash in the Audio File Processor that occurs when an Audio File Processor with a reversed sample is loaded from a save file and then the plugin window is opened. The problem was caused by a call to `AudioFileProcessorWaveView::slideSampleByFrames` during the execution of constructor of `AudioFileProcessorWaveView`. In that situation the three `knob` members were all `nullptr` because for some reason there was an explicit setter for them which was only called after construction. This is fixed by passing and setting the knobs in the constructor. Another question is if it's not a problem in the first place that the knobs are given to the `AudioFileProcessorWaveView` instead of their underlying models. --- .../AudioFileProcessorView.cpp | 6 +-- .../AudioFileProcessorWaveView.cpp | 37 +++++++++---------- .../AudioFileProcessorWaveView.h | 6 ++- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp index 43882222f..d16b1d019 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp @@ -184,12 +184,12 @@ void AudioFileProcessorView::newWaveView() delete m_waveView; m_waveView = 0; } - m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->sample()); - m_waveView->move(2, 172); - m_waveView->setKnobs( + m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->sample(), dynamic_cast(m_startKnob), dynamic_cast(m_endKnob), dynamic_cast(m_loopKnob)); + m_waveView->move(2, 172); + m_waveView->show(); } diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index f130ca41c..507c4e7c0 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -65,7 +65,8 @@ f_cnt_t AudioFileProcessorWaveView::range() const return m_to - m_from; } -AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * parent, int w, int h, Sample const * buf) : +AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget* parent, int w, int h, Sample const* buf, + knob* start, knob* end, knob* loop) : QWidget(parent), m_sample(buf), m_graph(QPixmap(w - 2 * s_padding, h - 2 * s_padding)), @@ -74,9 +75,9 @@ AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * parent, int w, m_last_from(0), m_last_to(0), m_last_amp(0), - m_startKnob(0), - m_endKnob(0), - m_loopKnob(0), + m_startKnob(start), + m_endKnob(end), + m_loopKnob(loop), m_isDragging(false), m_reversed(false), m_framesPlayed(0), @@ -85,6 +86,8 @@ AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * parent, int w, setFixedSize(w, h); setMouseTracking(true); + configureKnobRelationsAndWaveViews(); + updateSampleRange(); m_graph.fill(Qt::transparent); @@ -399,21 +402,6 @@ void AudioFileProcessorWaveView::slide(int px) slideSampleByFrames(step); } -void AudioFileProcessorWaveView::setKnobs(knob * start, knob * end, knob * loop) -{ - m_startKnob = start; - m_endKnob = end; - m_loopKnob = loop; - - m_startKnob->setWaveView(this); - m_startKnob->setRelatedKnob(m_endKnob); - - m_endKnob->setWaveView(this); - m_endKnob->setRelatedKnob(m_startKnob); - - m_loopKnob->setWaveView(this); -} - void AudioFileProcessorWaveView::slideSamplePointByPx(Point point, int px) { slideSamplePointByFrames( @@ -511,6 +499,17 @@ void AudioFileProcessorWaveView::updateCursor(QMouseEvent * me) setCursor(Qt::OpenHandCursor); } +void AudioFileProcessorWaveView::configureKnobRelationsAndWaveViews() +{ + m_startKnob->setWaveView(this); + m_startKnob->setRelatedKnob(m_endKnob); + + m_endKnob->setWaveView(this); + m_endKnob->setRelatedKnob(m_startKnob); + + m_loopKnob->setWaveView(this); +} + void AudioFileProcessorWaveView::knob::slideTo(double v, bool check_bound) { if (check_bound && ! checkBound(v)) diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h index 713064580..f40b69d12 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h @@ -146,8 +146,8 @@ private: friend class AudioFileProcessorView; public: - AudioFileProcessorWaveView(QWidget* parent, int w, int h, Sample const* buf); - void setKnobs(knob* start, knob* end, knob* loop); + AudioFileProcessorWaveView(QWidget* parent, int w, int h, Sample const* buf, + knob* start, knob* end, knob* loop); void updateSampleRange(); @@ -170,6 +170,8 @@ private: void reverse(); void updateCursor(QMouseEvent* me = nullptr); + void configureKnobRelationsAndWaveViews(); + static bool isCloseTo(int a, int b) { return qAbs(a - b) < 4; From c991a85eefe2a314ad8f27b7bc1f1b134af46c93 Mon Sep 17 00:00:00 2001 From: TechnoPorg <69441745+TechnoPorg@users.noreply.github.com> Date: Sun, 25 Feb 2024 11:49:56 -0700 Subject: [PATCH 098/191] Remove `MemoryManager` (#7128) Removes `MemoryManager` and the use of rpmalloc in favor of the `new` and `delete` operators found in C++. --------- Co-authored-by: Veratil --- .gitmodules | 3 - include/AudioPort.h | 2 - include/AutomatableModel.h | 2 - include/BasicFilters.h | 5 - include/Clip.h | 1 - include/DataFile.h | 4 +- include/Delay.h | 33 +++--- include/DetuningHelper.h | 2 - include/Effect.h | 2 - include/Instrument.h | 2 - include/InstrumentTrack.h | 1 - include/MemoryManager.h | 111 -------------------- include/MidiEventProcessor.h | 2 - include/NotePlayHandle.h | 4 - include/Plugin.h | 2 - include/RingBuffer.h | 3 +- include/Track.h | 1 - include/ValueBuffer.h | 2 - plugins/BitInvader/BitInvader.h | 2 - plugins/Bitcrush/Bitcrush.cpp | 4 +- plugins/CrossoverEQ/CrossoverEQ.cpp | 12 +-- plugins/FreeBoy/GbApuWrapper.h | 2 - plugins/GigPlayer/GigPlayer.h | 2 - plugins/Kicker/KickerOsc.h | 2 - plugins/LadspaEffect/LadspaEffect.cpp | 13 ++- plugins/Monstro/Monstro.h | 1 - plugins/MultitapEcho/MultitapEcho.cpp | 4 +- plugins/Nes/Nes.h | 2 - plugins/Organic/Organic.h | 3 - plugins/Patman/Patman.h | 2 - plugins/Sf2Player/Sf2Player.h | 1 - plugins/Sfxr/Sfxr.h | 2 - plugins/Sid/SidInstrument.h | 1 - plugins/TripleOscillator/TripleOscillator.h | 3 - plugins/Vibed/Vibed.cpp | 2 - plugins/Watsyn/Watsyn.h | 2 - plugins/Xpressive/ExprSynth.h | 2 - src/3rdparty/CMakeLists.txt | 1 - src/3rdparty/rpmalloc/CMakeLists.txt | 49 --------- src/3rdparty/rpmalloc/rpmalloc | 1 - src/CMakeLists.txt | 1 - src/core/AudioEngineWorkerThread.cpp | 2 - src/core/BufferManager.cpp | 5 +- src/core/CMakeLists.txt | 1 - src/core/MemoryManager.cpp | 84 --------------- src/core/NotePlayHandle.cpp | 12 +-- src/core/ProjectRenderer.cpp | 1 - 47 files changed, 42 insertions(+), 359 deletions(-) delete mode 100644 include/MemoryManager.h delete mode 100644 src/3rdparty/rpmalloc/CMakeLists.txt delete mode 160000 src/3rdparty/rpmalloc/rpmalloc delete mode 100644 src/core/MemoryManager.cpp diff --git a/.gitmodules b/.gitmodules index c85f7e5d8..82f3e464c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "src/3rdparty/qt5-x11embed"] path = src/3rdparty/qt5-x11embed url = https://github.com/Lukas-W/qt5-x11embed.git -[submodule "src/3rdparty/rpmalloc/rpmalloc"] - path = src/3rdparty/rpmalloc/rpmalloc - url = https://github.com/mjansson/rpmalloc.git [submodule "plugins/ZynAddSubFx/zynaddsubfx"] path = plugins/ZynAddSubFx/zynaddsubfx url = https://github.com/lmms/zynaddsubfx.git diff --git a/include/AudioPort.h b/include/AudioPort.h index d9803d205..9e3ce2bd6 100644 --- a/include/AudioPort.h +++ b/include/AudioPort.h @@ -29,7 +29,6 @@ #include #include -#include "MemoryManager.h" #include "PlayHandle.h" namespace lmms @@ -41,7 +40,6 @@ class BoolModel; class AudioPort : public ThreadableJob { - MM_OPERATORS public: AudioPort( const QString & _name, bool _has_effect_chain = true, FloatModel * volumeModel = nullptr, FloatModel * panningModel = nullptr, diff --git a/include/AutomatableModel.h b/include/AutomatableModel.h index 2264a592e..87adfdc6e 100644 --- a/include/AutomatableModel.h +++ b/include/AutomatableModel.h @@ -33,7 +33,6 @@ #include "Model.h" #include "TimePos.h" #include "ValueBuffer.h" -#include "MemoryManager.h" #include "ModelVisitor.h" @@ -77,7 +76,6 @@ class ControllerConnection; class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject { Q_OBJECT - MM_OPERATORS public: using AutoModelVector = std::vector; diff --git a/include/BasicFilters.h b/include/BasicFilters.h index b994aecc6..4cde320a6 100644 --- a/include/BasicFilters.h +++ b/include/BasicFilters.h @@ -40,7 +40,6 @@ #include "lmms_basics.h" #include "lmms_constants.h" #include "interpolation.h" -#include "MemoryManager.h" namespace lmms { @@ -50,7 +49,6 @@ template class BasicFilters; template class LinkwitzRiley { - MM_OPERATORS public: LinkwitzRiley( float sampleRate ) { @@ -145,7 +143,6 @@ using StereoLinkwitzRiley = LinkwitzRiley<2>; template class BiQuad { - MM_OPERATORS public: BiQuad() { @@ -188,7 +185,6 @@ using StereoBiQuad = BiQuad<2>; template class OnePole { - MM_OPERATORS public: OnePole() { @@ -222,7 +218,6 @@ using StereoOnePole = OnePole<2>; template class BasicFilters { - MM_OPERATORS public: enum class FilterType { diff --git a/include/Clip.h b/include/Clip.h index 0b540ccfb..a520ad4e4 100644 --- a/include/Clip.h +++ b/include/Clip.h @@ -50,7 +50,6 @@ class TrackView; class LMMS_EXPORT Clip : public Model, public JournallingObject { Q_OBJECT - MM_OPERATORS mapPropertyFromModel(bool,isMuted,setMuted,m_mutedModel); mapPropertyFromModel(bool,isSolo,setSolo,m_soloModel); public: diff --git a/include/DataFile.h b/include/DataFile.h index 3f1706229..ce5d4edf4 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -28,9 +28,9 @@ #include #include +#include #include "lmms_export.h" -#include "MemoryManager.h" class QTextStream; @@ -42,7 +42,6 @@ class ProjectVersion; class LMMS_EXPORT DataFile : public QDomDocument { - MM_OPERATORS using UpgradeMethod = void(DataFile::*)(); @@ -149,7 +148,6 @@ private: QDomElement m_head; Type m_type; unsigned int m_fileVersion; - } ; diff --git a/include/Delay.h b/include/Delay.h index daa871baf..71fbe1b00 100644 --- a/include/Delay.h +++ b/include/Delay.h @@ -29,7 +29,6 @@ #include "lmms_basics.h" #include "lmms_math.h" #include "interpolation.h" -#include "MemoryManager.h" namespace lmms { @@ -74,20 +73,20 @@ public: m_delay( 0 ), m_fraction( 0.0 ) { - m_buffer = MM_ALLOC(maxDelay ); + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } virtual ~CombFeedback() { - MM_FREE( m_buffer ); + delete[] m_buffer; } inline void setMaxDelay( int maxDelay ) { if( maxDelay > m_size ) { - MM_FREE( m_buffer ); - m_buffer = MM_ALLOC( maxDelay ); + delete[] m_buffer; + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } m_size = maxDelay; @@ -145,20 +144,20 @@ class CombFeedfwd m_delay( 0 ), m_fraction( 0.0 ) { - m_buffer = MM_ALLOC( maxDelay ); + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } virtual ~CombFeedfwd() { - MM_FREE( m_buffer ); + delete[] m_buffer; } inline void setMaxDelay( int maxDelay ) { if( maxDelay > m_size ) { - MM_FREE( m_buffer ); - m_buffer = MM_ALLOC( maxDelay ); + delete[] m_buffer; + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } m_size = maxDelay; @@ -216,20 +215,20 @@ class CombFeedbackDualtap m_delay( 0 ), m_fraction( 0.0 ) { - m_buffer = MM_ALLOC( maxDelay ); + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } virtual ~CombFeedbackDualtap() { - MM_FREE( m_buffer ); + delete[] m_buffer; } inline void setMaxDelay( int maxDelay ) { if( maxDelay > m_size ) { - MM_FREE( m_buffer ); - m_buffer = MM_ALLOC( maxDelay ); + delete[] m_buffer; + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } m_size = maxDelay; @@ -297,20 +296,20 @@ public: m_delay( 0 ), m_fraction( 0.0 ) { - m_buffer = MM_ALLOC( maxDelay ); + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } virtual ~AllpassDelay() { - MM_FREE( m_buffer ); + delete[] m_buffer; } inline void setMaxDelay( int maxDelay ) { if( maxDelay > m_size ) { - MM_FREE( m_buffer ); - m_buffer = MM_ALLOC( maxDelay ); + delete[] m_buffer; + m_buffer = new frame[maxDelay]; memset( m_buffer, 0, sizeof( frame ) * maxDelay ); } m_size = maxDelay; diff --git a/include/DetuningHelper.h b/include/DetuningHelper.h index e5d5f5712..da8eb5983 100644 --- a/include/DetuningHelper.h +++ b/include/DetuningHelper.h @@ -27,7 +27,6 @@ #define LMMS_DETUNING_HELPER_H #include "InlineAutomation.h" -#include "MemoryManager.h" namespace lmms { @@ -35,7 +34,6 @@ namespace lmms class DetuningHelper : public InlineAutomation { Q_OBJECT - MM_OPERATORS public: DetuningHelper() : InlineAutomation() diff --git a/include/Effect.h b/include/Effect.h index f2fb6e80f..8b2ff81f0 100644 --- a/include/Effect.h +++ b/include/Effect.h @@ -31,7 +31,6 @@ #include "AudioEngine.h" #include "AutomatableModel.h" #include "TempoSyncKnobModel.h" -#include "MemoryManager.h" namespace lmms { @@ -49,7 +48,6 @@ class EffectView; class LMMS_EXPORT Effect : public Plugin { - MM_OPERATORS Q_OBJECT public: Effect( const Plugin::Descriptor * _desc, diff --git a/include/Instrument.h b/include/Instrument.h index f23e0b401..243bdba61 100644 --- a/include/Instrument.h +++ b/include/Instrument.h @@ -31,7 +31,6 @@ #include "Flags.h" #include "lmms_export.h" #include "lmms_basics.h" -#include "MemoryManager.h" #include "Plugin.h" #include "TimePos.h" @@ -47,7 +46,6 @@ class Track; class LMMS_EXPORT Instrument : public Plugin { - MM_OPERATORS public: enum class Flag { diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index 5efafe0c7..3d84df597 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -60,7 +60,6 @@ class MidiCCRackView; class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor { Q_OBJECT - MM_OPERATORS mapPropertyFromModel(int,getVolume,setVolume,m_volumeModel); public: InstrumentTrack( TrackContainer* tc ); diff --git a/include/MemoryManager.h b/include/MemoryManager.h deleted file mode 100644 index fa2fe8110..000000000 --- a/include/MemoryManager.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * MemoryManager.h - * - * Copyright (c) 2017 Lukas W - * Copyright (c) 2014 Vesa Kivimäki - * Copyright (c) 2007-2014 Tobias Doerffel - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#ifndef LMMS_MEMORY_MANAGER_H -#define LMMS_MEMORY_MANAGER_H - -#include -#include - -#include "lmms_export.h" - -namespace lmms -{ - - -class LMMS_EXPORT MemoryManager -{ -public: - struct ThreadGuard - { - ThreadGuard(); - ~ThreadGuard(); - }; - - static void * alloc( size_t size ); - static void free( void * ptr ); -}; - -template -struct MmAllocator -{ - using value_type = T; - template struct rebind { - using other = MmAllocator; - }; - - T* allocate( std::size_t n ) - { - return reinterpret_cast( MemoryManager::alloc( sizeof(T) * n ) ); - } - - void deallocate( T* p, std::size_t ) - { - MemoryManager::free( p ); - } - - using vector = std::vector>; -}; - - -#define MM_OPERATORS \ -public: \ -static void * operator new ( size_t size ) \ -{ \ - return MemoryManager::alloc( size ); \ -} \ -static void * operator new[] ( size_t size ) \ -{ \ - return MemoryManager::alloc( size ); \ -} \ -static void operator delete ( void * ptr ) \ -{ \ - MemoryManager::free( ptr ); \ -} \ -static void operator delete[] ( void * ptr ) \ -{ \ - MemoryManager::free( ptr ); \ -} - -// for use in cases where overriding new/delete isn't a possibility -template -T* MM_ALLOC(size_t count) -{ - return reinterpret_cast( - MemoryManager::alloc(sizeof(T) * count)); -} - -// and just for symmetry... -template -void MM_FREE(T* ptr) -{ - MemoryManager::free(ptr); -} - - -} // namespace lmms - -#endif // LMMS_MEMORY_MANAGER_H diff --git a/include/MidiEventProcessor.h b/include/MidiEventProcessor.h index 1c45b3e3f..0fcb9610e 100644 --- a/include/MidiEventProcessor.h +++ b/include/MidiEventProcessor.h @@ -26,7 +26,6 @@ #define LMMS_MIDI_EVENT_PROCESSOR_H #include "MidiEvent.h" -#include "MemoryManager.h" #include "TimePos.h" namespace lmms @@ -35,7 +34,6 @@ namespace lmms // all classes being able to process MIDI-events should inherit from this class MidiEventProcessor { - MM_OPERATORS public: MidiEventProcessor() = default; diff --git a/include/NotePlayHandle.h b/include/NotePlayHandle.h index 7105d6672..f70268132 100644 --- a/include/NotePlayHandle.h +++ b/include/NotePlayHandle.h @@ -32,7 +32,6 @@ #include "Note.h" #include "PlayHandle.h" #include "Track.h" -#include "MemoryManager.h" class QReadWriteLock; @@ -47,7 +46,6 @@ using ConstNotePlayHandleList = QList; class LMMS_EXPORT NotePlayHandle : public PlayHandle, public Note { - MM_OPERATORS public: void * m_pluginData; std::unique_ptr> m_filter; @@ -273,7 +271,6 @@ public: private: class BaseDetuning { - MM_OPERATORS public: BaseDetuning( DetuningHelper* detuning ); @@ -341,7 +338,6 @@ const int NPH_CACHE_INCREMENT = 16; class NotePlayHandleManager { - MM_OPERATORS public: static void init(); static NotePlayHandle * acquire( InstrumentTrack* instrumentTrack, diff --git a/include/Plugin.h b/include/Plugin.h index 439dd95ad..100e9f658 100644 --- a/include/Plugin.h +++ b/include/Plugin.h @@ -30,7 +30,6 @@ #include "JournallingObject.h" #include "Model.h" -#include "MemoryManager.h" class QWidget; @@ -71,7 +70,6 @@ class PluginView; */ class LMMS_EXPORT Plugin : public Model, public JournallingObject { - MM_OPERATORS Q_OBJECT public: enum class Type diff --git a/include/RingBuffer.h b/include/RingBuffer.h index 90a607a13..98f726475 100644 --- a/include/RingBuffer.h +++ b/include/RingBuffer.h @@ -29,7 +29,7 @@ #include #include #include "lmms_basics.h" -#include "MemoryManager.h" +#include "lmms_export.h" namespace lmms @@ -41,7 +41,6 @@ namespace lmms class LMMS_EXPORT RingBuffer : public QObject { Q_OBJECT - MM_OPERATORS public: /** \brief Constructs a ringbuffer of specified size, will not care about samplerate changes * \param size The size of the buffer in frames. The actual size will be size + period size diff --git a/include/Track.h b/include/Track.h index 1c161984f..b801bb182 100644 --- a/include/Track.h +++ b/include/Track.h @@ -67,7 +67,6 @@ char const *const FILENAME_FILTER = "[\\0000-\x1f\"*/:<>?\\\\|\x7f]"; class LMMS_EXPORT Track : public Model, public JournallingObject { Q_OBJECT - MM_OPERATORS mapPropertyFromModel(bool,isMuted,setMuted,m_mutedModel); mapPropertyFromModel(bool,isSolo,setSolo,m_soloModel); public: diff --git a/include/ValueBuffer.h b/include/ValueBuffer.h index 683d17fb1..33d93cde0 100644 --- a/include/ValueBuffer.h +++ b/include/ValueBuffer.h @@ -28,7 +28,6 @@ #include -#include "MemoryManager.h" #include "lmms_export.h" namespace lmms @@ -37,7 +36,6 @@ namespace lmms class LMMS_EXPORT ValueBuffer : public std::vector { - MM_OPERATORS public: ValueBuffer() = default; ValueBuffer(int length); diff --git a/plugins/BitInvader/BitInvader.h b/plugins/BitInvader/BitInvader.h index a08640e99..6dce9db83 100644 --- a/plugins/BitInvader/BitInvader.h +++ b/plugins/BitInvader/BitInvader.h @@ -31,7 +31,6 @@ #include "Instrument.h" #include "InstrumentView.h" #include "Graph.h" -#include "MemoryManager.h" namespace lmms { @@ -48,7 +47,6 @@ class PixmapButton; class BSynth { - MM_OPERATORS public: BSynth( float * sample, NotePlayHandle * _nph, bool _interpolation, float factor, diff --git a/plugins/Bitcrush/Bitcrush.cpp b/plugins/Bitcrush/Bitcrush.cpp index 8d29186b5..df4a8605d 100644 --- a/plugins/Bitcrush/Bitcrush.cpp +++ b/plugins/Bitcrush/Bitcrush.cpp @@ -62,7 +62,7 @@ BitcrushEffect::BitcrushEffect( Model * parent, const Descriptor::SubPluginFeatu m_sampleRate( Engine::audioEngine()->processingSampleRate() ), m_filter( m_sampleRate ) { - m_buffer = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() * OS_RATE ); + m_buffer = new sampleFrame[Engine::audioEngine()->framesPerPeriod() * OS_RATE]; m_filter.setLowpass( m_sampleRate * ( CUTOFF_RATIO * OS_RATIO ) ); m_needsUpdate = true; @@ -77,7 +77,7 @@ BitcrushEffect::BitcrushEffect( Model * parent, const Descriptor::SubPluginFeatu BitcrushEffect::~BitcrushEffect() { - MM_FREE( m_buffer ); + delete[] m_buffer; } diff --git a/plugins/CrossoverEQ/CrossoverEQ.cpp b/plugins/CrossoverEQ/CrossoverEQ.cpp index c4334677c..4dca94a4c 100644 --- a/plugins/CrossoverEQ/CrossoverEQ.cpp +++ b/plugins/CrossoverEQ/CrossoverEQ.cpp @@ -64,16 +64,16 @@ CrossoverEQEffect::CrossoverEQEffect( Model* parent, const Descriptor::SubPlugin m_hp4( m_sampleRate ), m_needsUpdate( true ) { - m_tmp1 = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); - m_tmp2 = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); - m_work = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); + m_tmp2 = new sampleFrame[Engine::audioEngine()->framesPerPeriod()]; + m_tmp1 = new sampleFrame[Engine::audioEngine()->framesPerPeriod()]; + m_work = new sampleFrame[Engine::audioEngine()->framesPerPeriod()]; } CrossoverEQEffect::~CrossoverEQEffect() { - MM_FREE( m_tmp1 ); - MM_FREE( m_tmp2 ); - MM_FREE( m_work ); + delete[] m_tmp1; + delete[] m_tmp2; + delete[] m_work; } void CrossoverEQEffect::sampleRateChanged() diff --git a/plugins/FreeBoy/GbApuWrapper.h b/plugins/FreeBoy/GbApuWrapper.h index 493a28731..3b95869d5 100644 --- a/plugins/FreeBoy/GbApuWrapper.h +++ b/plugins/FreeBoy/GbApuWrapper.h @@ -26,7 +26,6 @@ #include "Gb_Apu.h" #include "Multi_Buffer.h" -#include "MemoryManager.h" namespace lmms { @@ -34,7 +33,6 @@ namespace lmms class GbApuWrapper : private Gb_Apu { - MM_OPERATORS public: GbApuWrapper() = default; ~GbApuWrapper() = default; diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index e5039f109..986018654 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -38,7 +38,6 @@ #include "Knob.h" #include "LcdSpinBox.h" #include "LedCheckBox.h" -#include "MemoryManager.h" #include "gig.h" @@ -236,7 +235,6 @@ public: class GigInstrument : public Instrument { Q_OBJECT - MM_OPERATORS mapPropertyFromModel( int, getBank, setBank, m_bankNum ); mapPropertyFromModel( int, getPatch, setPatch, m_patchNum ); diff --git a/plugins/Kicker/KickerOsc.h b/plugins/Kicker/KickerOsc.h index 1accb50a4..69436c5fc 100644 --- a/plugins/Kicker/KickerOsc.h +++ b/plugins/Kicker/KickerOsc.h @@ -31,7 +31,6 @@ #include "lmms_math.h" #include "interpolation.h" -#include "MemoryManager.h" namespace lmms { @@ -40,7 +39,6 @@ namespace lmms template class KickerOsc { - MM_OPERATORS public: KickerOsc( const FX & fx, const float start, const float end, const float noise, const float offset, const float slope, const float env, const float diststart, const float distend, const float length ) : diff --git a/plugins/LadspaEffect/LadspaEffect.cpp b/plugins/LadspaEffect/LadspaEffect.cpp index cc754a829..837bd554c 100644 --- a/plugins/LadspaEffect/LadspaEffect.cpp +++ b/plugins/LadspaEffect/LadspaEffect.cpp @@ -36,7 +36,6 @@ #include "LadspaControl.h" #include "LadspaSubPluginFeatures.h" #include "AutomationClip.h" -#include "MemoryManager.h" #include "ValueBuffer.h" #include "Song.h" @@ -326,7 +325,7 @@ void LadspaEffect::pluginInstantiation() manager->isPortInput( m_key, port ) ) { p->rate = BufferRate::ChannelIn; - p->buffer = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); + p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()]; inbuf[ inputch ] = p->buffer; inputch++; } @@ -341,24 +340,24 @@ void LadspaEffect::pluginInstantiation() } else { - p->buffer = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); + p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()]; m_inPlaceBroken = true; } } else if( manager->isPortInput( m_key, port ) ) { p->rate = BufferRate::AudioRateInput; - p->buffer = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); + p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()]; } else { p->rate = BufferRate::AudioRateOutput; - p->buffer = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); + p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()]; } } else { - p->buffer = MM_ALLOC( 1 ); + p->buffer = new LADSPA_Data[1]; if( manager->isPortInput( m_key, port ) ) { @@ -557,7 +556,7 @@ void LadspaEffect::pluginDestruction() port_desc_t * pp = m_ports.at( proc ).at( port ); if( m_inPlaceBroken || pp->rate != BufferRate::ChannelOut ) { - if( pp->buffer) MM_FREE( pp->buffer ); + if( pp->buffer) delete[] pp->buffer; } delete pp; } diff --git a/plugins/Monstro/Monstro.h b/plugins/Monstro/Monstro.h index 21efedaf3..919409b2d 100644 --- a/plugins/Monstro/Monstro.h +++ b/plugins/Monstro/Monstro.h @@ -173,7 +173,6 @@ class ComboBox; class MonstroSynth { - MM_OPERATORS public: MonstroSynth( MonstroInstrument * _i, NotePlayHandle * _nph ); virtual ~MonstroSynth() = default; diff --git a/plugins/MultitapEcho/MultitapEcho.cpp b/plugins/MultitapEcho/MultitapEcho.cpp index 4f5e9fdf8..ff3ca828a 100644 --- a/plugins/MultitapEcho/MultitapEcho.cpp +++ b/plugins/MultitapEcho/MultitapEcho.cpp @@ -58,7 +58,7 @@ MultitapEchoEffect::MultitapEchoEffect( Model* parent, const Descriptor::SubPlug m_sampleRate( Engine::audioEngine()->processingSampleRate() ), m_sampleRatio( 1.0f / m_sampleRate ) { - m_work = MM_ALLOC( Engine::audioEngine()->framesPerPeriod() ); + m_work = new sampleFrame[Engine::audioEngine()->framesPerPeriod()]; m_buffer.reset(); m_stages = static_cast( m_controls.m_stages.value() ); updateFilters( 0, 19 ); @@ -67,7 +67,7 @@ MultitapEchoEffect::MultitapEchoEffect( Model* parent, const Descriptor::SubPlug MultitapEchoEffect::~MultitapEchoEffect() { - MM_FREE( m_work ); + delete[] m_work; } diff --git a/plugins/Nes/Nes.h b/plugins/Nes/Nes.h index b4102f31d..a05b3a2f8 100644 --- a/plugins/Nes/Nes.h +++ b/plugins/Nes/Nes.h @@ -31,7 +31,6 @@ #include "InstrumentView.h" #include "AutomatableModel.h" #include "PixmapButton.h" -#include "MemoryManager.h" #define makeknob( name, x, y, hint, unit, oname ) \ @@ -92,7 +91,6 @@ class NesInstrumentView; class NesObject { - MM_OPERATORS public: NesObject( NesInstrument * nes, const sample_rate_t samplerate, NotePlayHandle * nph ); virtual ~NesObject() = default; diff --git a/plugins/Organic/Organic.h b/plugins/Organic/Organic.h index a46b7882f..e50550e5e 100644 --- a/plugins/Organic/Organic.h +++ b/plugins/Organic/Organic.h @@ -84,7 +84,6 @@ const float CENT = 1.0f / 1200.0f; class OscillatorObject : public Model { Q_OBJECT - MM_OPERATORS private: int m_numOscillators; IntModel m_waveShape; @@ -159,7 +158,6 @@ private: struct oscPtr { - MM_OPERATORS Oscillator * oscLeft; Oscillator * oscRight; float phaseOffsetLeft[NUM_OSCILLATORS]; @@ -196,7 +194,6 @@ private: struct OscillatorKnobs { - MM_OPERATORS OscillatorKnobs( Knob * h, Knob * v, diff --git a/plugins/Patman/Patman.h b/plugins/Patman/Patman.h index 8d2c8c657..486524522 100644 --- a/plugins/Patman/Patman.h +++ b/plugins/Patman/Patman.h @@ -31,7 +31,6 @@ #include "Sample.h" #include "SampleBuffer.h" #include "AutomatableModel.h" -#include "MemoryManager.h" namespace lmms { @@ -87,7 +86,6 @@ public slots: private: struct handle_data { - MM_OPERATORS Sample::PlaybackState* state; bool tuned; std::shared_ptr sample; diff --git a/plugins/Sf2Player/Sf2Player.h b/plugins/Sf2Player/Sf2Player.h index 17ddf5500..1af370e05 100644 --- a/plugins/Sf2Player/Sf2Player.h +++ b/plugins/Sf2Player/Sf2Player.h @@ -34,7 +34,6 @@ #include "Instrument.h" #include "InstrumentView.h" #include "LcdSpinBox.h" -#include "MemoryManager.h" class QLabel; diff --git a/plugins/Sfxr/Sfxr.h b/plugins/Sfxr/Sfxr.h index edec0ba6f..8af8984c9 100644 --- a/plugins/Sfxr/Sfxr.h +++ b/plugins/Sfxr/Sfxr.h @@ -31,7 +31,6 @@ #include "AutomatableModel.h" #include "Instrument.h" #include "InstrumentView.h" -#include "MemoryManager.h" namespace lmms { @@ -78,7 +77,6 @@ class SfxrInstrumentView; class SfxrSynth { - MM_OPERATORS public: SfxrSynth( const SfxrInstrument * s ); virtual ~SfxrSynth() = default; diff --git a/plugins/Sid/SidInstrument.h b/plugins/Sid/SidInstrument.h index 53efa8942..1a133b58b 100644 --- a/plugins/Sid/SidInstrument.h +++ b/plugins/Sid/SidInstrument.h @@ -48,7 +48,6 @@ class PixmapButton; class VoiceObject : public Model { Q_OBJECT - MM_OPERATORS public: enum class WaveForm { Square = 0, diff --git a/plugins/TripleOscillator/TripleOscillator.h b/plugins/TripleOscillator/TripleOscillator.h index c0258b544..4b6d97835 100644 --- a/plugins/TripleOscillator/TripleOscillator.h +++ b/plugins/TripleOscillator/TripleOscillator.h @@ -57,7 +57,6 @@ const int NUM_OF_OSCILLATORS = 3; class OscillatorObject : public Model { - MM_OPERATORS Q_OBJECT public: OscillatorObject( Model * _parent, int _idx ); @@ -139,7 +138,6 @@ private: struct oscPtr { - MM_OPERATORS Oscillator * oscLeft; Oscillator * oscRight; } ; @@ -170,7 +168,6 @@ private: struct OscillatorKnobs { - MM_OPERATORS OscillatorKnobs( Knob * v, Knob * p, Knob * c, diff --git a/plugins/Vibed/Vibed.cpp b/plugins/Vibed/Vibed.cpp index ad6a3942a..ddf9097a5 100644 --- a/plugins/Vibed/Vibed.cpp +++ b/plugins/Vibed/Vibed.cpp @@ -33,7 +33,6 @@ #include "InstrumentTrack.h" #include "NotePlayHandle.h" #include "VibratingString.h" -#include "MemoryManager.h" #include "base64.h" #include "CaptionMenu.h" #include "volume.h" @@ -67,7 +66,6 @@ Plugin::Descriptor PLUGIN_EXPORT vibedstrings_plugin_descriptor = class Vibed::StringContainer { - MM_OPERATORS public: StringContainer(float pitch, sample_rate_t sampleRate, int bufferLength) : m_pitch(pitch), m_sampleRate(sampleRate), m_bufferLength(bufferLength) {} diff --git a/plugins/Watsyn/Watsyn.h b/plugins/Watsyn/Watsyn.h index 3a736e162..d238edbde 100644 --- a/plugins/Watsyn/Watsyn.h +++ b/plugins/Watsyn/Watsyn.h @@ -32,7 +32,6 @@ #include "AutomatableModel.h" #include "TempoSyncKnob.h" #include -#include "MemoryManager.h" namespace lmms { @@ -88,7 +87,6 @@ class WatsynView; class WatsynObject { - MM_OPERATORS public: WatsynObject( float * _A1wave, float * _A2wave, float * _B1wave, float * _B2wave, diff --git a/plugins/Xpressive/ExprSynth.h b/plugins/Xpressive/ExprSynth.h index f338b78fc..5d664c85e 100644 --- a/plugins/Xpressive/ExprSynth.h +++ b/plugins/Xpressive/ExprSynth.h @@ -30,7 +30,6 @@ #include #include "AutomatableModel.h" #include "Graph.h" -#include "MemoryManager.h" namespace lmms { @@ -102,7 +101,6 @@ public: class ExprSynth { - MM_OPERATORS public: ExprSynth(const WaveSample* gW1, const WaveSample* gW2, const WaveSample* gW3, ExprFront* exprO1, ExprFront* exprO2, NotePlayHandle* nph, const sample_rate_t sample_rate, const FloatModel* pan1, const FloatModel* pan2, float rel_trans); diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index a95332a07..f1578a970 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -4,7 +4,6 @@ IF(LMMS_BUILD_LINUX AND WANT_VST) ENDIF() ADD_SUBDIRECTORY(hiir) -ADD_SUBDIRECTORY(rpmalloc) ADD_SUBDIRECTORY(weakjack) if(MINGW) diff --git a/src/3rdparty/rpmalloc/CMakeLists.txt b/src/3rdparty/rpmalloc/CMakeLists.txt deleted file mode 100644 index 047c32678..000000000 --- a/src/3rdparty/rpmalloc/CMakeLists.txt +++ /dev/null @@ -1,49 +0,0 @@ -add_library(rpmalloc STATIC - rpmalloc/rpmalloc/rpmalloc.c - rpmalloc/rpmalloc/rpmalloc.h -) - -target_include_directories(rpmalloc PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/rpmalloc/rpmalloc -) - -set_property(TARGET rpmalloc PROPERTY C_STANDARD 11) - -IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(rpmalloc - PRIVATE -Wno-unused-variable - ) -endif() - -if (NOT LMMS_BUILD_WIN32) - target_compile_definitions(rpmalloc - PRIVATE -D_GNU_SOURCE - ) -endif() - -if(MINGW) - target_compile_definitions(rpmalloc - PRIVATE -D_WIN32_WINNT=0x600 - ) -endif() - -if (CMAKE_BUILD_TYPE STREQUAL "Debug") - # rpmalloc uses GCC builtin "__builtin_umull_overflow" with ENABLE_VALIDATE_ARGS, - # which is only available starting with GCC 5 - if (CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION VERSION_LESS 5) - set(ENABLE_VALIDATE_ARGS OFF) - else () - set(ENABLE_VALIDATE_ARGS ON) - endif() - target_compile_definitions(rpmalloc - PRIVATE -DENABLE_ASSERTS=1 -DENABLE_VALIDATE_ARGS=${ENABLE_VALIDATE_ARGS} - ) -endif() - -option(LMMS_ENABLE_MALLOC_STATS "Enables statistics for rpmalloc" OFF) - -if (LMMS_ENABLE_MALLOC_STATS) - target_compile_definitions(rpmalloc - PRIVATE -DENABLE_STATISTICS=1 - ) -endif() diff --git a/src/3rdparty/rpmalloc/rpmalloc b/src/3rdparty/rpmalloc/rpmalloc deleted file mode 160000 index 80daac0d5..000000000 --- a/src/3rdparty/rpmalloc/rpmalloc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 80daac0d539ab2a8edfd5ca24b1e0c77a4974bbb diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d55a725dd..d71b34c59 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -186,7 +186,6 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${SUIL_LIBRARIES} ${LILV_LIBRARIES} ${FFTW3F_LIBRARIES} - rpmalloc SampleRate::samplerate SndFile::sndfile ${EXTRA_LIBRARIES} diff --git a/src/core/AudioEngineWorkerThread.cpp b/src/core/AudioEngineWorkerThread.cpp index 528841c71..ae459c5e4 100644 --- a/src/core/AudioEngineWorkerThread.cpp +++ b/src/core/AudioEngineWorkerThread.cpp @@ -30,7 +30,6 @@ #include "denormals.h" #include "AudioEngine.h" -#include "MemoryManager.h" #include "ThreadableJob.h" #if __SSE__ @@ -167,7 +166,6 @@ void AudioEngineWorkerThread::startAndWaitForJobs() void AudioEngineWorkerThread::run() { - MemoryManager::ThreadGuard mmThreadGuard; Q_UNUSED(mmThreadGuard); disable_denormals(); QMutex m; diff --git a/src/core/BufferManager.cpp b/src/core/BufferManager.cpp index ff35e6a19..2362be85a 100644 --- a/src/core/BufferManager.cpp +++ b/src/core/BufferManager.cpp @@ -28,7 +28,6 @@ #include -#include "MemoryManager.h" namespace lmms { @@ -43,7 +42,7 @@ void BufferManager::init( fpp_t fpp ) sampleFrame * BufferManager::acquire() { - return MM_ALLOC( s_framesPerPeriod ); + return new sampleFrame[s_framesPerPeriod]; } void BufferManager::clear( sampleFrame *ab, const f_cnt_t frames, const f_cnt_t offset ) @@ -62,7 +61,7 @@ void BufferManager::clear( surroundSampleFrame * ab, const f_cnt_t frames, void BufferManager::release( sampleFrame * buf ) { - MM_FREE( buf ); + delete[] buf; } } // namespace lmms diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 26d458f9e..1a4871fc7 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -39,7 +39,6 @@ set(LMMS_SRCS core/LinkedModelGroups.cpp core/LocklessAllocator.cpp core/MemoryHelper.cpp - core/MemoryManager.cpp core/MeterModel.cpp core/MicroTimer.cpp core/Microtuner.cpp diff --git a/src/core/MemoryManager.cpp b/src/core/MemoryManager.cpp deleted file mode 100644 index bd3168f14..000000000 --- a/src/core/MemoryManager.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * MemoryManager.cpp - * - * Copyright (c) 2017 Lukas W - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - - -#include "MemoryManager.h" - -#include -#include "rpmalloc.h" - -namespace lmms -{ - - -/// Global static object handling rpmalloc intializing and finalizing -struct MemoryManagerGlobalGuard { - MemoryManagerGlobalGuard() { - rpmalloc_initialize(); - } - ~MemoryManagerGlobalGuard() { - rpmalloc_finalize(); - } -} static mm_global_guard; - - -namespace { -static thread_local size_t thread_guard_depth; -} - -MemoryManager::ThreadGuard::ThreadGuard() -{ - if (thread_guard_depth++ == 0) { - rpmalloc_thread_initialize(); - } -} - -MemoryManager::ThreadGuard::~ThreadGuard() -{ - if (--thread_guard_depth == 0) { - rpmalloc_thread_finalize(true); - } -} - -static thread_local MemoryManager::ThreadGuard local_mm_thread_guard{}; - -void* MemoryManager::alloc(size_t size) -{ - // Reference local thread guard to ensure it is initialized. - // Compilers may optimize the instance away otherwise. - Q_UNUSED(&local_mm_thread_guard); - Q_ASSERT_X(rpmalloc_is_thread_initialized(), "MemoryManager::alloc", "Thread not initialized"); - return rpmalloc(size); -} - - -void MemoryManager::free(void * ptr) -{ - Q_UNUSED(&local_mm_thread_guard); - Q_ASSERT_X(rpmalloc_is_thread_initialized(), "MemoryManager::free", "Thread not initialized"); - return rpfree(ptr); -} - - -} // namespace lmms diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index 712b64e89..2c1c21931 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -610,9 +610,9 @@ int NotePlayHandleManager::s_size; void NotePlayHandleManager::init() { - s_available = MM_ALLOC( INITIAL_NPH_CACHE ); + s_available = new NotePlayHandle*[INITIAL_NPH_CACHE]; - auto n = MM_ALLOC(INITIAL_NPH_CACHE); + auto n = static_cast(std::malloc(sizeof(NotePlayHandle) * INITIAL_NPH_CACHE)); for( int i=0; i < INITIAL_NPH_CACHE; ++i ) { @@ -655,11 +655,11 @@ void NotePlayHandleManager::release( NotePlayHandle * nph ) void NotePlayHandleManager::extend( int c ) { s_size += c; - auto tmp = MM_ALLOC(s_size); - MM_FREE( s_available ); + auto tmp = new NotePlayHandle*[s_size]; + delete[] s_available; s_available = tmp; - auto n = MM_ALLOC(c); + auto n = static_cast(std::malloc(sizeof(NotePlayHandle) * c)); for( int i=0; i < c; ++i ) { @@ -670,7 +670,7 @@ void NotePlayHandleManager::extend( int c ) void NotePlayHandleManager::free() { - MM_FREE(s_available); + delete[] s_available; } diff --git a/src/core/ProjectRenderer.cpp b/src/core/ProjectRenderer.cpp index 3f101330a..3d83515f2 100644 --- a/src/core/ProjectRenderer.cpp +++ b/src/core/ProjectRenderer.cpp @@ -159,7 +159,6 @@ void ProjectRenderer::startProcessing() void ProjectRenderer::run() { - MemoryManager::ThreadGuard mmThreadGuard; Q_UNUSED(mmThreadGuard); #if 0 #if defined(LMMS_BUILD_LINUX) || defined(LMMS_BUILD_FREEBSD) #ifdef LMMS_HAVE_SCHED_H From 3ae13ae45e82bc0ad2d526ca91940e800db70800 Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 10 Mar 2024 23:06:46 -0400 Subject: [PATCH 099/191] Apply master gain outside audio devices (#7135) --- include/AudioDevice.h | 7 +------ include/AudioFileFlac.h | 4 +--- include/AudioFileMP3.h | 4 +--- include/AudioFileOgg.h | 4 +--- include/AudioFileWave.h | 4 +--- include/AudioSampleRecorder.h | 4 +--- include/MixHelpers.h | 2 ++ src/core/AudioEngine.cpp | 3 +++ src/core/MixHelpers.cpp | 9 +++++++++ src/core/audio/AudioAlsa.cpp | 5 +---- src/core/audio/AudioDevice.cpp | 17 +++++------------ src/core/audio/AudioFileFlac.cpp | 6 +++--- src/core/audio/AudioFileMP3.cpp | 9 +++------ src/core/audio/AudioFileOgg.cpp | 11 +++-------- src/core/audio/AudioFileWave.cpp | 13 +++---------- src/core/audio/AudioJack.cpp | 3 +-- src/core/audio/AudioOss.cpp | 2 +- src/core/audio/AudioPortAudio.cpp | 6 +----- src/core/audio/AudioPulseAudio.cpp | 5 +---- src/core/audio/AudioSampleRecorder.cpp | 6 +----- src/core/audio/AudioSdl.cpp | 12 +----------- src/core/audio/AudioSndio.cpp | 3 +-- src/core/audio/AudioSoundIo.cpp | 4 +--- src/gui/MixerView.cpp | 4 ---- src/gui/widgets/Oscilloscope.cpp | 6 +++--- 25 files changed, 49 insertions(+), 104 deletions(-) diff --git a/include/AudioDevice.h b/include/AudioDevice.h index d1a9617cd..c6ee46efc 100644 --- a/include/AudioDevice.h +++ b/include/AudioDevice.h @@ -96,11 +96,7 @@ public: protected: // subclasses can re-implement this for being used in conjunction with // processNextBuffer() - virtual void writeBuffer( const surroundSampleFrame * /* _buf*/, - const fpp_t /*_frames*/, - const float /*_master_gain*/ ) - { - } + virtual void writeBuffer(const surroundSampleFrame* /* _buf*/, const fpp_t /*_frames*/) {} // called by according driver for fetching new sound-data fpp_t getNextBuffer( surroundSampleFrame * _ab ); @@ -109,7 +105,6 @@ protected: // returns num of bytes in outbuf int convertToS16( const surroundSampleFrame * _ab, const fpp_t _frames, - const float _master_gain, int_sample_t * _output_buffer, const bool _convert_endian = false ); diff --git a/include/AudioFileFlac.h b/include/AudioFileFlac.h index 944e30478..9432f4231 100644 --- a/include/AudioFileFlac.h +++ b/include/AudioFileFlac.h @@ -65,9 +65,7 @@ private: SF_INFO m_sfinfo; SNDFILE* m_sf; - void writeBuffer(surroundSampleFrame const* _ab, - fpp_t const frames, - float master_gain) override; + void writeBuffer(surroundSampleFrame const* _ab, fpp_t const frames) override; bool startEncoding(); void finishEncoding(); diff --git a/include/AudioFileMP3.h b/include/AudioFileMP3.h index 4289ad211..013c93a3e 100644 --- a/include/AudioFileMP3.h +++ b/include/AudioFileMP3.h @@ -58,9 +58,7 @@ public: } protected: - void writeBuffer( const surroundSampleFrame * /* _buf*/, - const fpp_t /*_frames*/, - const float /*_master_gain*/ ) override; + void writeBuffer(const surroundSampleFrame* /* _buf*/, const fpp_t /*_frames*/) override; private: void flushRemainingBuffers(); diff --git a/include/AudioFileOgg.h b/include/AudioFileOgg.h index 77be8ca1c..fc3ce25b4 100644 --- a/include/AudioFileOgg.h +++ b/include/AudioFileOgg.h @@ -58,9 +58,7 @@ public: private: - void writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, - const float _master_gain ) override; + void writeBuffer(const surroundSampleFrame* _ab, const fpp_t _frames) override; bool startEncoding(); void finishEncoding(); diff --git a/include/AudioFileWave.h b/include/AudioFileWave.h index c186aaaa7..22b124f93 100644 --- a/include/AudioFileWave.h +++ b/include/AudioFileWave.h @@ -56,9 +56,7 @@ public: private: - void writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, - float _master_gain ) override; + void writeBuffer(const surroundSampleFrame* _ab, const fpp_t _frames) override; bool startEncoding(); void finishEncoding(); diff --git a/include/AudioSampleRecorder.h b/include/AudioSampleRecorder.h index d481cc16c..a3e776881 100644 --- a/include/AudioSampleRecorder.h +++ b/include/AudioSampleRecorder.h @@ -48,9 +48,7 @@ public: std::shared_ptr createSampleBuffer(); private: - void writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, - const float _master_gain ) override; + void writeBuffer(const surroundSampleFrame* _ab, const fpp_t _frames) override; using BufferList = QList>; BufferList m_buffers; diff --git a/include/MixHelpers.h b/include/MixHelpers.h index 6458c65fc..dde17dd02 100644 --- a/include/MixHelpers.h +++ b/include/MixHelpers.h @@ -45,6 +45,8 @@ bool sanitize( sampleFrame * src, int frames ); /*! \brief Add samples from src to dst */ void add( sampleFrame* dst, const sampleFrame* src, int frames ); +/*! \brief Multiply samples from `dst` by `coeff` */ +void multiply(sampleFrame* dst, float coeff, int frames); /*! \brief Add samples from src multiplied by coeffSrc to dst */ void addMultiplied( sampleFrame* dst, const sampleFrame* src, float coeffSrc, int frames ); diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 47b42e11b..04e3c7c7c 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -24,6 +24,7 @@ #include "AudioEngine.h" +#include "MixHelpers.h" #include "denormals.h" #include "lmmsconfig.h" @@ -433,6 +434,8 @@ void AudioEngine::renderStageMix() Mixer *mixer = Engine::mixer(); mixer->masterMix(m_outputBufferWrite); + MixHelpers::multiply(m_outputBufferWrite, m_masterGain, m_framesPerPeriod); + emit nextAudioBuffer(m_outputBufferRead); // and trigger LFOs diff --git a/src/core/MixHelpers.cpp b/src/core/MixHelpers.cpp index bc55419e9..209640b70 100644 --- a/src/core/MixHelpers.cpp +++ b/src/core/MixHelpers.cpp @@ -178,6 +178,15 @@ struct AddSwappedMultipliedOp const float m_coeff; }; +void multiply(sampleFrame* dst, float coeff, int frames) +{ + for (int i = 0; i < frames; ++i) + { + dst[i][0] *= coeff; + dst[i][1] *= coeff; + } +} + void addSwappedMultiplied( sampleFrame* dst, const sampleFrame* src, float coeffSrc, int frames ) { run<>( dst, src, frames, AddSwappedMultipliedOp(coeffSrc) ); diff --git a/src/core/audio/AudioAlsa.cpp b/src/core/audio/AudioAlsa.cpp index 6e17ad0fe..201a967a3 100644 --- a/src/core/audio/AudioAlsa.cpp +++ b/src/core/audio/AudioAlsa.cpp @@ -323,10 +323,7 @@ void AudioAlsa::run() } outbuf_size = frames * channels(); - convertToS16( temp, frames, - audioEngine()->masterGain(), - outbuf, - m_convertEndian ); + convertToS16(temp, frames, outbuf, m_convertEndian); } int min_len = std::min(len, outbuf_size - outbuf_pos); memcpy( ptr, outbuf + outbuf_pos, diff --git a/src/core/audio/AudioDevice.cpp b/src/core/audio/AudioDevice.cpp index 5fb58a1b0..58ba3932e 100644 --- a/src/core/audio/AudioDevice.cpp +++ b/src/core/audio/AudioDevice.cpp @@ -66,10 +66,7 @@ AudioDevice::~AudioDevice() void AudioDevice::processNextBuffer() { const fpp_t frames = getNextBuffer( m_buffer ); - if( frames ) - { - writeBuffer( m_buffer, frames, audioEngine()->masterGain() ); - } + if (frames) { writeBuffer(m_buffer, frames); } else { m_inProcess = false; @@ -211,7 +208,6 @@ fpp_t AudioDevice::resample( const surroundSampleFrame * _src, int AudioDevice::convertToS16( const surroundSampleFrame * _ab, const fpp_t _frames, - const float _master_gain, int_sample_t * _output_buffer, const bool _convert_endian ) { @@ -222,8 +218,8 @@ int AudioDevice::convertToS16( const surroundSampleFrame * _ab, { for( ch_cnt_t chnl = 0; chnl < channels(); ++chnl ) { - temp = static_cast( AudioEngine::clip( _ab[frame][chnl] * _master_gain ) * OUTPUT_SAMPLE_MULTIPLIER ); - + temp = static_cast(AudioEngine::clip(_ab[frame][chnl]) * OUTPUT_SAMPLE_MULTIPLIER); + ( _output_buffer + frame * channels() )[chnl] = ( temp & 0x00ff ) << 8 | ( temp & 0xff00 ) >> 8; @@ -236,11 +232,8 @@ int AudioDevice::convertToS16( const surroundSampleFrame * _ab, { for( ch_cnt_t chnl = 0; chnl < channels(); ++chnl ) { - ( _output_buffer + frame * channels() )[chnl] = - static_cast( - AudioEngine::clip( _ab[frame][chnl] * - _master_gain ) * - OUTPUT_SAMPLE_MULTIPLIER ); + (_output_buffer + frame * channels())[chnl] + = static_cast(AudioEngine::clip(_ab[frame][chnl]) * OUTPUT_SAMPLE_MULTIPLIER); } } } diff --git a/src/core/audio/AudioFileFlac.cpp b/src/core/audio/AudioFileFlac.cpp index af71003d1..097fbdd89 100644 --- a/src/core/audio/AudioFileFlac.cpp +++ b/src/core/audio/AudioFileFlac.cpp @@ -89,7 +89,7 @@ bool AudioFileFlac::startEncoding() return true; } -void AudioFileFlac::writeBuffer(surroundSampleFrame const* _ab, fpp_t const frames, float master_gain) +void AudioFileFlac::writeBuffer(surroundSampleFrame const* _ab, fpp_t const frames) { OutputSettings::BitDepth depth = getOutputSettings().getBitDepth(); float clipvalue = std::nextafterf( -1.0f, 0.0f ); @@ -104,7 +104,7 @@ void AudioFileFlac::writeBuffer(surroundSampleFrame const* _ab, fpp_t const fram // Clip the negative side to just above -1.0 in order to prevent it from changing sign // Upstream issue: https://github.com/erikd/libsndfile/issues/309 // When this commit is reverted libsndfile-1.0.29 must be made a requirement for FLAC - buf[frame*channels() + channel] = std::max(clipvalue, _ab[frame][channel] * master_gain); + buf[frame*channels() + channel] = std::max(clipvalue, _ab[frame][channel]); } } sf_writef_float(m_sf, static_cast(buf.data()), frames); @@ -112,7 +112,7 @@ void AudioFileFlac::writeBuffer(surroundSampleFrame const* _ab, fpp_t const fram else // integer PCM encoding { auto buf = std::vector(frames * channels()); - convertToS16(_ab, frames, master_gain, buf.data(), !isLittleEndian()); + convertToS16(_ab, frames, buf.data(), !isLittleEndian()); sf_writef_short(m_sf, static_cast(buf.data()), frames); } diff --git a/src/core/audio/AudioFileMP3.cpp b/src/core/audio/AudioFileMP3.cpp index ef0677152..2141fabfc 100644 --- a/src/core/audio/AudioFileMP3.cpp +++ b/src/core/audio/AudioFileMP3.cpp @@ -53,21 +53,18 @@ AudioFileMP3::~AudioFileMP3() tearDownEncoder(); } -void AudioFileMP3::writeBuffer( const surroundSampleFrame * _buf, - const fpp_t _frames, - const float _master_gain ) +void AudioFileMP3::writeBuffer(const surroundSampleFrame* _buf, const fpp_t _frames) { if (_frames < 1) { return; } - // TODO Why isn't the gain applied by the driver but inside the device? std::vector interleavedDataBuffer(_frames * 2); for (fpp_t i = 0; i < _frames; ++i) { - interleavedDataBuffer[2*i] = _buf[i][0] * _master_gain; - interleavedDataBuffer[2*i + 1] = _buf[i][1] * _master_gain; + interleavedDataBuffer[2*i] = _buf[i][0]; + interleavedDataBuffer[2*i + 1] = _buf[i][1]; } size_t minimumBufferSize = 1.25 * _frames + 7200; diff --git a/src/core/audio/AudioFileOgg.cpp b/src/core/audio/AudioFileOgg.cpp index d61e27da8..9d5f0c809 100644 --- a/src/core/audio/AudioFileOgg.cpp +++ b/src/core/audio/AudioFileOgg.cpp @@ -185,12 +185,7 @@ bool AudioFileOgg::startEncoding() return true; } - - - -void AudioFileOgg::writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, - const float _master_gain ) +void AudioFileOgg::writeBuffer(const surroundSampleFrame* _ab, const fpp_t _frames) { int eos = 0; @@ -201,7 +196,7 @@ void AudioFileOgg::writeBuffer( const surroundSampleFrame * _ab, { for( ch_cnt_t chnl = 0; chnl < channels(); ++chnl ) { - buffer[chnl][frame] = _ab[frame][chnl] * _master_gain; + buffer[chnl][frame] = _ab[frame][chnl]; } } @@ -258,7 +253,7 @@ void AudioFileOgg::finishEncoding() if( m_ok ) { // just for flushing buffers... - writeBuffer( nullptr, 0, 0.0f ); + writeBuffer(nullptr, 0); // clean up ogg_stream_clear( &m_os ); diff --git a/src/core/audio/AudioFileWave.cpp b/src/core/audio/AudioFileWave.cpp index 9c51437ff..612b98982 100644 --- a/src/core/audio/AudioFileWave.cpp +++ b/src/core/audio/AudioFileWave.cpp @@ -93,12 +93,7 @@ bool AudioFileWave::startEncoding() return true; } - - - -void AudioFileWave::writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, - const float _master_gain ) +void AudioFileWave::writeBuffer(const surroundSampleFrame* _ab, const fpp_t _frames) { OutputSettings::BitDepth bitDepth = getOutputSettings().getBitDepth(); @@ -109,8 +104,7 @@ void AudioFileWave::writeBuffer( const surroundSampleFrame * _ab, { for( ch_cnt_t chnl = 0; chnl < channels(); ++chnl ) { - buf[frame*channels()+chnl] = _ab[frame][chnl] * - _master_gain; + buf[frame * channels() + chnl] = _ab[frame][chnl]; } } sf_writef_float( m_sf, buf, _frames ); @@ -119,8 +113,7 @@ void AudioFileWave::writeBuffer( const surroundSampleFrame * _ab, else { auto buf = new int_sample_t[_frames * channels()]; - convertToS16( _ab, _frames, _master_gain, buf, - !isLittleEndian() ); + convertToS16(_ab, _frames, buf, !isLittleEndian()); sf_writef_short( m_sf, buf, _frames ); delete[] buf; diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index a4fd2c095..61d7814ed 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -344,13 +344,12 @@ int AudioJack::processCallback(jack_nframes_t nframes) while (done < nframes && !m_stopped) { jack_nframes_t todo = std::min(nframes - done, m_framesToDoInCurBuf - m_framesDoneInCurBuf); - const float gain = audioEngine()->masterGain(); for (int c = 0; c < channels(); ++c) { jack_default_audio_sample_t* o = m_tempOutBufs[c]; for (jack_nframes_t frame = 0; frame < todo; ++frame) { - o[done + frame] = m_outBuf[m_framesDoneInCurBuf + frame][c] * gain; + o[done + frame] = m_outBuf[m_framesDoneInCurBuf + frame][c]; } } done += todo; diff --git a/src/core/audio/AudioOss.cpp b/src/core/audio/AudioOss.cpp index 8fedd3b2b..bd6d46c95 100644 --- a/src/core/audio/AudioOss.cpp +++ b/src/core/audio/AudioOss.cpp @@ -303,7 +303,7 @@ void AudioOss::run() break; } - int bytes = convertToS16( temp, frames, audioEngine()->masterGain(), outbuf, m_convertEndian ); + int bytes = convertToS16(temp, frames, outbuf, m_convertEndian); if( write( m_audioFD, outbuf, bytes ) != bytes ) { break; diff --git a/src/core/audio/AudioPortAudio.cpp b/src/core/audio/AudioPortAudio.cpp index 3684a79a8..f303545d2 100644 --- a/src/core/audio/AudioPortAudio.cpp +++ b/src/core/audio/AudioPortAudio.cpp @@ -298,15 +298,11 @@ int AudioPortAudio::process_callback( const int min_len = std::min(static_cast(_framesPerBuffer), m_outBufSize - m_outBufPos); - float master_gain = audioEngine()->masterGain(); - for( fpp_t frame = 0; frame < min_len; ++frame ) { for( ch_cnt_t chnl = 0; chnl < channels(); ++chnl ) { - ( _outputBuffer + frame * channels() )[chnl] = - AudioEngine::clip( m_outBuf[frame][chnl] * - master_gain ); + (_outputBuffer + frame * channels())[chnl] = AudioEngine::clip(m_outBuf[frame][chnl]); } } diff --git a/src/core/audio/AudioPulseAudio.cpp b/src/core/audio/AudioPulseAudio.cpp index 3ca8764cc..a0c5ccaf9 100644 --- a/src/core/audio/AudioPulseAudio.cpp +++ b/src/core/audio/AudioPulseAudio.cpp @@ -278,10 +278,7 @@ void AudioPulseAudio::streamWriteCallback( pa_stream *s, size_t length ) m_quit = true; break; } - int bytes = convertToS16( temp, frames, - audioEngine()->masterGain(), - pcmbuf, - m_convertEndian ); + int bytes = convertToS16(temp, frames, pcmbuf, m_convertEndian); if( bytes > 0 ) { pa_stream_write( m_s, pcmbuf, bytes, nullptr, 0, diff --git a/src/core/audio/AudioSampleRecorder.cpp b/src/core/audio/AudioSampleRecorder.cpp index b5bbf5a8f..c9448b89e 100644 --- a/src/core/audio/AudioSampleRecorder.cpp +++ b/src/core/audio/AudioSampleRecorder.cpp @@ -85,11 +85,7 @@ std::shared_ptr AudioSampleRecorder::createSampleBuffer() return std::make_shared(std::move(bigBuffer), sampleRate()); } - - - -void AudioSampleRecorder::writeBuffer( const surroundSampleFrame * _ab, - const fpp_t _frames, const float ) +void AudioSampleRecorder::writeBuffer(const surroundSampleFrame* _ab, const fpp_t _frames) { auto buf = new sampleFrame[_frames]; for( fpp_t frame = 0; frame < _frames; ++frame ) diff --git a/src/core/audio/AudioSdl.cpp b/src/core/audio/AudioSdl.cpp index 12aa97d63..679912c50 100644 --- a/src/core/audio/AudioSdl.cpp +++ b/src/core/audio/AudioSdl.cpp @@ -261,13 +261,6 @@ void AudioSdl::sdlAudioCallback( Uint8 * _buf, int _len ) m_currentBufferFramesCount - m_currentBufferFramePos); - const float gain = audioEngine()->masterGain(); - for (uint f = 0; f < min_frames_count; f++) - { - (m_outBuf + m_currentBufferFramePos)[f][0] *= gain; - (m_outBuf + m_currentBufferFramePos)[f][1] *= gain; - } - memcpy( _buf, m_outBuf + m_currentBufferFramePos, min_frames_count*sizeof(sampleFrame) ); _buf += min_frames_count*sizeof(sampleFrame); _len -= min_frames_count*sizeof(sampleFrame); @@ -291,10 +284,7 @@ void AudioSdl::sdlAudioCallback( Uint8 * _buf, int _len ) m_convertedBufSize = frames * channels() * sizeof( int_sample_t ); - convertToS16( m_outBuf, frames, - audioEngine()->masterGain(), - (int_sample_t *)m_convertedBuf, - m_outConvertEndian ); + convertToS16(m_outBuf, frames, reinterpret_cast(m_convertedBuf), m_outConvertEndian); } const int min_len = std::min(_len, m_convertedBufSize - m_convertedBufPos); diff --git a/src/core/audio/AudioSndio.cpp b/src/core/audio/AudioSndio.cpp index bb9b249f8..d934dfb9c 100644 --- a/src/core/audio/AudioSndio.cpp +++ b/src/core/audio/AudioSndio.cpp @@ -167,8 +167,7 @@ void AudioSndio::run() break; } - uint bytes = convertToS16( temp, frames, - audioEngine()->masterGain(), outbuf, m_convertEndian ); + uint bytes = convertToS16(temp, frames, outbuf, m_convertEndian); if( sio_write( m_hdl, outbuf, bytes ) != bytes ) { break; diff --git a/src/core/audio/AudioSoundIo.cpp b/src/core/audio/AudioSoundIo.cpp index 36a1929df..6e8a03e38 100644 --- a/src/core/audio/AudioSoundIo.cpp +++ b/src/core/audio/AudioSoundIo.cpp @@ -286,8 +286,6 @@ void AudioSoundIo::writeCallback(int frameCountMin, int frameCountMax) int bytesPerSample = m_outstream->bytes_per_sample; int err; - const float gain = audioEngine()->masterGain(); - int framesLeft = frameCountMax; while (framesLeft > 0) @@ -328,7 +326,7 @@ void AudioSoundIo::writeCallback(int frameCountMin, int frameCountMax) for (int channel = 0; channel < layout->channel_count; channel += 1) { - float sample = gain * m_outBuf[m_outBufFrameIndex][channel]; + float sample = m_outBuf[m_outBufFrameIndex][channel]; memcpy(areas[channel].ptr, &sample, bytesPerSample); areas[channel].ptr += areas[channel].step; } diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 224ea2c85..8b2ecdc56 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -603,10 +603,6 @@ void MixerView::updateFaders() { Mixer * m = getMixer(); - // apply master gain - m->mixerChannel(0)->m_peakLeft *= Engine::audioEngine()->masterGain(); - m->mixerChannel(0)->m_peakRight *= Engine::audioEngine()->masterGain(); - for (int i = 0; i < m_mixerChannelViews.size(); ++i) { const float opl = m_mixerChannelViews[i]->m_fader->getPeak_L(); diff --git a/src/gui/widgets/Oscilloscope.cpp b/src/gui/widgets/Oscilloscope.cpp index bec45c162..a689f53f3 100644 --- a/src/gui/widgets/Oscilloscope.cpp +++ b/src/gui/widgets/Oscilloscope.cpp @@ -143,14 +143,14 @@ void Oscilloscope::paintEvent( QPaintEvent * ) { AudioEngine const * audioEngine = Engine::audioEngine(); - float master_output = audioEngine->masterGain(); + float masterOutput = audioEngine->masterGain(); const fpp_t frames = audioEngine->framesPerPeriod(); AudioEngine::StereoSample peakValues = audioEngine->getPeakValues(m_buffer, frames); const float max_level = qMax( peakValues.left, peakValues.right ); // Set the color of the line according to the maximum level - float const maxLevelWithAppliedMasterGain = max_level * master_output; + float const maxLevelWithAppliedMasterGain = max_level * masterOutput; p.setPen(QPen(determineLineColor(maxLevelWithAppliedMasterGain), 0.7)); p.setRenderHint( QPainter::Antialiasing ); @@ -158,7 +158,7 @@ void Oscilloscope::paintEvent( QPaintEvent * ) // now draw all that stuff int w = width() - 4; const qreal xd = static_cast(w) / frames; - const qreal half_h = -( height() - 6 ) / 3.0 * static_cast(master_output) - 1; + const qreal half_h = -(height() - 6) / 3.0 * static_cast(masterOutput) - 1; int x_base = 2; const qreal y_base = height() / 2 - 0.5; From 1be2a206619e86f6bd08a56457413d7335def449 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 11 Mar 2024 20:38:20 +0100 Subject: [PATCH 100/191] Fix a NaN in EqSpectrumView (#7140) Fix a `NaN` in `EqSpectrumView` that's caused by `m_peakSum` not being initialzed. --- plugins/Eq/EqSpectrumView.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Eq/EqSpectrumView.cpp b/plugins/Eq/EqSpectrumView.cpp index aa556490b..e75248755 100644 --- a/plugins/Eq/EqSpectrumView.cpp +++ b/plugins/Eq/EqSpectrumView.cpp @@ -186,6 +186,7 @@ namespace gui EqSpectrumView::EqSpectrumView(EqAnalyser *b, QWidget *_parent) : QWidget( _parent ), m_analyser( b ), + m_peakSum(0.), m_periodicalUpdate( false ) { setFixedSize( 450, 200 ); From 227fc47a97d51f4d1b68b0943ac25ade2f456834 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 11 Mar 2024 22:38:45 +0100 Subject: [PATCH 101/191] Fix NaNs in basic filters and Equalizer (#7141) Fix several NaNs in the context of the basic filters and the Equalizer. The coefficients of the `BiQuad` were not initialized which seems to have led to NaNs down the line. Fix a division by zero in `EqEffect::peakBand` and `EqSpectrumView::paintEvent` if the energy is zero. The condition `m_peakSum <= 0` was removed from `EqSpectrumView::paintEvent` because the value is initialized to 0 down the line and later only potentially positive values are added. --- include/BasicFilters.h | 7 ++++++- plugins/Eq/EqEffect.cpp | 5 ++++- plugins/Eq/EqSpectrumView.cpp | 9 +++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/include/BasicFilters.h b/include/BasicFilters.h index 4cde320a6..215c32dfb 100644 --- a/include/BasicFilters.h +++ b/include/BasicFilters.h @@ -144,7 +144,12 @@ template class BiQuad { public: - BiQuad() + BiQuad() : + m_a1(0.), + m_a2(0.), + m_b0(0.), + m_b1(0.), + m_b2(0.) { clearHistory(); } diff --git a/plugins/Eq/EqEffect.cpp b/plugins/Eq/EqEffect.cpp index 8a7954144..31be4d0f5 100644 --- a/plugins/Eq/EqEffect.cpp +++ b/plugins/Eq/EqEffect.cpp @@ -289,6 +289,9 @@ bool EqEffect::processAudioBuffer( sampleFrame *buf, const fpp_t frames ) float EqEffect::peakBand( float minF, float maxF, EqAnalyser *fft, int sr ) { + auto const fftEnergy = fft->getEnergy(); + if (fftEnergy == 0.) { return 0.; } + float peak = -60; float *b = fft->m_bands; float h = 0; @@ -296,7 +299,7 @@ float EqEffect::peakBand( float minF, float maxF, EqAnalyser *fft, int sr ) { if( bandToFreq( x ,sr) >= minF && bandToFreq( x,sr ) <= maxF ) { - h = 20 * ( log10( *b / fft->getEnergy() ) ); + h = 20. * log10(*b / fftEnergy); peak = h > peak ? h : peak; } } diff --git a/plugins/Eq/EqSpectrumView.cpp b/plugins/Eq/EqSpectrumView.cpp index e75248755..35eb90dc0 100644 --- a/plugins/Eq/EqSpectrumView.cpp +++ b/plugins/Eq/EqSpectrumView.cpp @@ -208,10 +208,10 @@ EqSpectrumView::EqSpectrumView(EqAnalyser *b, QWidget *_parent) : void EqSpectrumView::paintEvent(QPaintEvent *event) { - const float energy = m_analyser->getEnergy(); - if( energy <= 0 && m_peakSum <= 0 ) + const float energy = m_analyser->getEnergy(); + if (energy <= 0.) { - //dont draw anything + // If there is no energy in the signal we don't need to draw anything return; } @@ -238,7 +238,8 @@ void EqSpectrumView::paintEvent(QPaintEvent *event) const float fallOff = 1.07; for( int x = 0; x < MAX_BANDS; ++x, ++bands ) { - peak = ( fh * 2.0 / 3.0 * ( 20 * ( log10( *bands / energy ) ) - LOWER_Y ) / ( - LOWER_Y ) ); + peak = *bands != 0. ? (fh * 2.0 / 3.0 * (20. * log10(*bands / energy) - LOWER_Y) / (-LOWER_Y)) : 0.; + if( peak < 0 ) { peak = 0; From 238974253d0fc8137939a8983fc57e7bac0b6752 Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Tue, 12 Mar 2024 14:56:49 -0400 Subject: [PATCH 102/191] Update game-music-emu submodule --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 82f3e464c..1c43fd1b3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,7 @@ url = https://github.com/lmms/zynaddsubfx.git [submodule "plugins/FreeBoy/game-music-emu"] path = plugins/FreeBoy/game-music-emu - url = https://bitbucket.org/mpyne/game-music-emu.git + url = https://github.com/libgme/game-music-emu.git [submodule "plugins/OpulenZ/adplug"] path = plugins/OpulenZ/adplug url = https://github.com/adplug/adplug.git From 37073af49475a5574db28f2dc3fd31d7939c2556 Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Tue, 12 Mar 2024 15:45:34 -0400 Subject: [PATCH 103/191] Update to latest commit --- plugins/FreeBoy/game-music-emu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/FreeBoy/game-music-emu b/plugins/FreeBoy/game-music-emu index 21a064ea6..6b676192d 160000 --- a/plugins/FreeBoy/game-music-emu +++ b/plugins/FreeBoy/game-music-emu @@ -1 +1 @@ -Subproject commit 21a064ea66a5cdf71910e207c4756095c266814f +Subproject commit 6b676192d98302e698ac78fe3c00833eae6a74e5 From 04ecf733951d2aba5bc8dad08c50f189979c8737 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 13 Mar 2024 20:00:58 +0100 Subject: [PATCH 104/191] Fix missing icons for some instruments (#7132) Revert some of the changes made in commit 88e0e94dcdb. The underlying idea was that the `InstrumentTrackView` should be responsible for assigning the icon that's shown by its `TrackLabelButton`. However, this does not work because in some cases the `InstrumentTrack` that's passed into `InstrumentTrackView::determinePixmap` does not have an `Instrument` assigned. This in turn seems to be caused due to initalizations that are running in parallel in different threads. Here are the steps to reproduce the threading problem (line numbers refer to commit 360254f): 1. Set a break point in line 1054 of `InstrumentTrack`, i.e. the line in `InstrumentTrack::loadInstrument` where `m_instrument` is being assigned to. 2. Set a break point in `InstrumentTrackView::determinePixmap`, e.g. inside of the first if statement. 3. Drop an instance of "Sf2 Player" onto the Song Editor. 4. The first break point in `InstrumentTrack` is hit in a thread called "lmms::Instrumen" (shown like that in my debugger). Try to step over it. 5. The second break point in `InstrumentTrackView` now gets hit before the code is stepped over. This time we are in the thread called "lmms". I guess this is the GUI main thread. 6. Continue execution. If you now switch to the application then the icon is shown. I guess the debugger is halted long enough in the main thread so that the InstrumentTrack gets an instrument assigned in another thread. If you delete/disable the break point in `InstrumentTrack::determinePixmap` and follow the coarse steps above then the icon is not shown because the track has no instrument. The current fix still delegates to the `InstrumentTrackView` to determine the pixmap in hopes that one day there will be a better solution where the parent component can be fully responsible for its child component. Fixes #7116. --- include/InstrumentTrackView.h | 2 ++ include/TrackLabelButton.h | 1 + src/gui/tracks/InstrumentTrackView.cpp | 5 +++++ src/gui/tracks/TrackLabelButton.cpp | 11 +++++++++++ 4 files changed, 19 insertions(+) diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index c7d524b36..cfde89bde 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -71,6 +71,8 @@ public: // Create a menu for assigning/creating channels for this track QMenu * createMixerMenu( QString title, QString newMixerLabel ) override; + QPixmap determinePixmap(); + protected: void modelChanged() override; diff --git a/include/TrackLabelButton.h b/include/TrackLabelButton.h index e19fc6be9..1d3620d12 100644 --- a/include/TrackLabelButton.h +++ b/include/TrackLabelButton.h @@ -55,6 +55,7 @@ protected: void mousePressEvent( QMouseEvent * _me ) override; void mouseDoubleClickEvent( QMouseEvent * _me ) override; void mouseReleaseEvent( QMouseEvent * _me ) override; + void paintEvent(QPaintEvent* pe) override; void resizeEvent( QResizeEvent * _re ) override; private: diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 788991ed0..c812999fd 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -397,6 +397,11 @@ QMenu * InstrumentTrackView::createMixerMenu(QString title, QString newMixerLabe return mixerMenu; } +QPixmap InstrumentTrackView::determinePixmap() +{ + return determinePixmap(dynamic_cast(getTrack())); +} + QPixmap InstrumentTrackView::determinePixmap(InstrumentTrack* instrumentTrack) { diff --git a/src/gui/tracks/TrackLabelButton.cpp b/src/gui/tracks/TrackLabelButton.cpp index 087edba3d..c164b780e 100644 --- a/src/gui/tracks/TrackLabelButton.cpp +++ b/src/gui/tracks/TrackLabelButton.cpp @@ -30,6 +30,7 @@ #include "ConfigManager.h" #include "embed.h" +#include "InstrumentTrackView.h" #include "RenameDialog.h" #include "TrackRenameLineEdit.h" #include "TrackView.h" @@ -178,6 +179,16 @@ void TrackLabelButton::mouseReleaseEvent( QMouseEvent *_me ) } +void TrackLabelButton::paintEvent(QPaintEvent* pe) +{ + InstrumentTrackView* instrumentTrackView = dynamic_cast(m_trackView); + if (instrumentTrackView) + { + setIcon(instrumentTrackView->determinePixmap()); + } + + QToolButton::paintEvent(pe); +} void TrackLabelButton::resizeEvent(QResizeEvent *_re) From b9ebc24e13893ed83e87eac3647b52408e7cedca Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 15 Mar 2024 21:40:15 -0400 Subject: [PATCH 105/191] Check if `m_peakSum` is less than or equal to 0 again (#7146) --- plugins/Eq/EqSpectrumView.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/Eq/EqSpectrumView.cpp b/plugins/Eq/EqSpectrumView.cpp index 35eb90dc0..540450883 100644 --- a/plugins/Eq/EqSpectrumView.cpp +++ b/plugins/Eq/EqSpectrumView.cpp @@ -209,11 +209,7 @@ EqSpectrumView::EqSpectrumView(EqAnalyser *b, QWidget *_parent) : void EqSpectrumView::paintEvent(QPaintEvent *event) { const float energy = m_analyser->getEnergy(); - if (energy <= 0.) - { - // If there is no energy in the signal we don't need to draw anything - return; - } + if (energy <= 0. && m_peakSum <= 0) { return; } const int fh = height(); const int LOWER_Y = -36; // dB From 4120a04d0be6c7c00dd9610da7a2ec9d023c55ce Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Sat, 16 Mar 2024 11:04:01 +0530 Subject: [PATCH 106/191] Replace QRegExp with QRegularExpression (#7133) * replace QRegExp with QRegularExpression (find n replace) * follow up to fix errors * removed rpmalloc * Fix compilation for qt5, to be fixed when qt6 fully supported. Co-authored-by: Kevin Zander * Added QtGlobal header for version finding fix * Use the other syntax to try fix compilation. * Check for 5.12 instead. * Fix the header * Attempt at fixing it further. * Use version checks properly in header file * Use QT_VERSION_CHECK macro in sources * Apply suggestions from messmerd's review Co-authored-by: Dalton Messmer --------- Co-authored-by: Kevin Zander Co-authored-by: Dalton Messmer --- include/AutomatableModel.h | 3 ++- include/EffectSelectDialog.h | 17 +++++++++++++---- include/LadspaBase.h | 4 +++- plugins/CarlaBase/Carla.h | 5 +++-- .../LadspaEffect/LadspaSubPluginFeatures.cpp | 3 +-- plugins/ZynAddSubFx/ZynAddSubFx.cpp | 2 +- src/core/AutomatableModel.cpp | 4 ++-- src/core/DataFile.cpp | 6 +++--- src/core/RenderManager.cpp | 3 ++- src/gui/MicrotunerConfig.cpp | 14 +++++++------- src/gui/instrument/InstrumentTrackWindow.cpp | 2 +- src/gui/modals/EffectSelectDialog.cpp | 6 ++++++ src/gui/modals/VersionedSaveDialog.cpp | 4 ++-- 13 files changed, 46 insertions(+), 27 deletions(-) diff --git a/include/AutomatableModel.h b/include/AutomatableModel.h index 87adfdc6e..15285e17a 100644 --- a/include/AutomatableModel.h +++ b/include/AutomatableModel.h @@ -25,9 +25,10 @@ #ifndef LMMS_AUTOMATABLE_MODEL_H #define LMMS_AUTOMATABLE_MODEL_H +#include #include #include -#include +#include #include "JournallingObject.h" #include "Model.h" diff --git a/include/EffectSelectDialog.h b/include/EffectSelectDialog.h index db8e60f0d..53e8dbe7e 100644 --- a/include/EffectSelectDialog.h +++ b/include/EffectSelectDialog.h @@ -33,7 +33,7 @@ #include #include #include -#include +#include #include #include #include @@ -65,10 +65,19 @@ protected: QString name = sourceModel()->data(nameIndex, Qt::DisplayRole).toString(); QString type = sourceModel()->data(typeIndex, Qt::DisplayRole).toString(); - QRegExp nameRegExp(filterRegExp()); - nameRegExp.setCaseSensitivity(Qt::CaseInsensitive); + // TODO: cleanup once we drop Qt5 support +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) + QRegularExpression nameRegularExpression(filterRegularExpression()); + nameRegularExpression.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + + bool nameFilterPassed = nameRegularExpression.match(name).capturedStart() != -1; +#else + QRegExp nameRegularExpression(filterRegExp()); + nameRegularExpression.setCaseSensitivity(Qt::CaseInsensitive); + + bool nameFilterPassed = nameRegularExpression.indexIn(name) != -1; +#endif - bool nameFilterPassed = nameRegExp.indexIn(name) != -1; bool typeFilterPassed = type.contains(m_effectTypeFilter, Qt::CaseInsensitive); return nameFilterPassed && typeFilterPassed; diff --git a/include/LadspaBase.h b/include/LadspaBase.h index 6569c5a30..0a2b067d4 100644 --- a/include/LadspaBase.h +++ b/include/LadspaBase.h @@ -26,6 +26,8 @@ #ifndef LMMS_LADSPA_BASE_H #define LMMS_LADSPA_BASE_H +#include + #include "LadspaManager.h" #include "Plugin.h" @@ -75,7 +77,7 @@ inline Plugin::Descriptor::SubPluginFeatures::Key ladspaKeyToSubPluginKey( { Plugin::Descriptor::SubPluginFeatures::Key::AttributeMap m; QString file = _key.first; - m["file"] = file.remove( QRegExp( "\\.so$" ) ).remove( QRegExp( "\\.dll$" ) ); + m["file"] = file.remove(QRegularExpression("\\.so$")).remove(QRegularExpression("\\.dll$")); m["plugin"] = _key.second; return Plugin::Descriptor::SubPluginFeatures::Key( _desc, _name, m ); } diff --git a/plugins/CarlaBase/Carla.h b/plugins/CarlaBase/Carla.h index e04444f91..3d0e424a2 100644 --- a/plugins/CarlaBase/Carla.h +++ b/plugins/CarlaBase/Carla.h @@ -33,6 +33,7 @@ #include #include #include +#include // carla/source/includes #include "carlabase_export.h" @@ -89,8 +90,8 @@ public: // From AutomatableModel.h, it's private there. inline static bool mustQuoteName(const QString &name) { - QRegExp reg("^[A-Za-z0-9._-]+$"); - return !reg.exactMatch(name); + QRegularExpression reg("^[A-Za-z0-9._-]+$"); + return !reg.match(name).hasMatch(); } inline void loadSettings(const QDomElement& element, const QString& name = QString("value")) override diff --git a/plugins/LadspaEffect/LadspaSubPluginFeatures.cpp b/plugins/LadspaEffect/LadspaSubPluginFeatures.cpp index 46a211f9f..fc4667152 100644 --- a/plugins/LadspaEffect/LadspaSubPluginFeatures.cpp +++ b/plugins/LadspaEffect/LadspaSubPluginFeatures.cpp @@ -171,8 +171,7 @@ ladspa_key_t LadspaSubPluginFeatures::subPluginKeyToLadspaKey( const Key * _key ) { QString file = _key->attributes["file"]; - return( ladspa_key_t( file.remove( QRegExp( "\\.so$" ) ). - remove( QRegExp( "\\.dll$" ) ) + + return(ladspa_key_t(file.remove(QRegularExpression("\\.so$")).remove(QRegularExpression("\\.dll$")) + #ifdef LMMS_BUILD_WIN32 ".dll" #else diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index be38bcb79..01fa6400b 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -311,7 +311,7 @@ void ZynAddSubFxInstrument::loadFile( const QString & _file ) m_pluginMutex.unlock(); } - instrumentTrack()->setName( QFileInfo( _file ).baseName().replace( QRegExp( "^[0-9]{4}-" ), QString() ) ); + instrumentTrack()->setName(QFileInfo(_file).baseName().replace(QRegularExpression("^[0-9]{4}-"), QString())); m_modifiedControllers.clear(); diff --git a/src/core/AutomatableModel.cpp b/src/core/AutomatableModel.cpp index e46a864f8..c701f28e3 100644 --- a/src/core/AutomatableModel.cpp +++ b/src/core/AutomatableModel.cpp @@ -97,8 +97,8 @@ bool AutomatableModel::isAutomated() const bool AutomatableModel::mustQuoteName(const QString& name) { - QRegExp reg("^[A-Za-z0-9._-]+$"); - return !reg.exactMatch(name); + QRegularExpression reg("^[A-Za-z0-9._-]+$"); + return !reg.match(name).hasMatch(); } void AutomatableModel::saveSettings( QDomDocument& doc, QDomElement& element, const QString& name ) diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index a520e6bc5..3c6309db8 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include "base64.h" @@ -973,8 +974,7 @@ void DataFile::upgrade_0_4_0_20080622() { QDomElement el = list.item( i ).toElement(); QString s = el.attribute( "name" ); - s.replace( QRegExp( "^Beat/Baseline " ), - "Beat/Bassline " ); + s.replace(QRegularExpression("^Beat/Baseline "), "Beat/Bassline"); el.setAttribute( "name", s ); } } @@ -1109,7 +1109,7 @@ void DataFile::upgrade_1_1_91() { QDomElement el = list.item( i ).toElement(); QString s = el.attribute( "src" ); - s.replace( QRegExp("/samples/bassloopes/"), "/samples/bassloops/" ); + s.replace(QRegularExpression("/samples/bassloopes/"), "/samples/bassloops/"); el.setAttribute( "src", s ); } diff --git a/src/core/RenderManager.cpp b/src/core/RenderManager.cpp index 9f6192039..d375b95ee 100644 --- a/src/core/RenderManager.cpp +++ b/src/core/RenderManager.cpp @@ -23,6 +23,7 @@ */ #include +#include #include "RenderManager.h" @@ -182,7 +183,7 @@ QString RenderManager::pathForTrack(const Track *track, int num) { QString extension = ProjectRenderer::getFileExtensionFromFormat( m_format ); QString name = track->name(); - name = name.remove(QRegExp(FILENAME_FILTER)); + name = name.remove(QRegularExpression(FILENAME_FILTER)); name = QString( "%1_%2%3" ).arg( num ).arg( name ).arg( extension ); return QDir(m_outputPath).filePath(name); } diff --git a/src/gui/MicrotunerConfig.cpp b/src/gui/MicrotunerConfig.cpp index 4156b9e79..6bb8415bd 100644 --- a/src/gui/MicrotunerConfig.cpp +++ b/src/gui/MicrotunerConfig.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include #include "ComboBox.h" @@ -342,7 +342,7 @@ bool MicrotunerConfig::validateScaleForm() { if (line.isEmpty()) {continue;} if (line[0] == '!') {continue;} // comment - QString firstSection = line.section(QRegExp("\\s+|/"), 0, 0, QString::SectionSkipEmpty); + QString firstSection = line.section(QRegularExpression("\\s+|/"), 0, 0, QString::SectionSkipEmpty); if (firstSection.contains('.')) // cent mode { bool ok = true; @@ -357,7 +357,7 @@ bool MicrotunerConfig::validateScaleForm() if (!ok) {fail(tr("Numerator of an interval defined as a ratio cannot be converted to a number")); return false;} if (line.contains('/')) { - den = line.split('/').at(1).section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty).toInt(&ok); + den = line.split('/').at(1).section(QRegularExpression("\\s+"), 0, 0, QString::SectionSkipEmpty).toInt(&ok); } if (!ok) {fail(tr("Denominator of an interval defined as a ratio cannot be converted to a number")); return false;} if (num * den < 0) {fail(tr("Interval defined as a ratio cannot be negative")); return false;} @@ -390,7 +390,7 @@ bool MicrotunerConfig::validateKeymapForm() { if (line.isEmpty()) {continue;} if (line[0] == '!') {continue;} // comment - QString firstSection = line.section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty); + QString firstSection = line.section(QRegularExpression("\\s+"), 0, 0, QString::SectionSkipEmpty); if (firstSection == "x") {continue;} // not mapped // otherwise must contain a number bool ok = true; @@ -424,7 +424,7 @@ bool MicrotunerConfig::applyScale() { if (line.isEmpty()) {continue;} if (line[0] == '!') {continue;} // comment - QString firstSection = line.section(QRegExp("\\s+|/"), 0, 0, QString::SectionSkipEmpty); + QString firstSection = line.section(QRegularExpression("\\s+|/"), 0, 0, QString::SectionSkipEmpty); if (firstSection.contains('.')) // cent mode { newIntervals.emplace_back(firstSection.toFloat()); @@ -435,7 +435,7 @@ bool MicrotunerConfig::applyScale() num = firstSection.toInt(); if (line.contains('/')) { - den = line.split('/').at(1).section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty).toInt(); + den = line.split('/').at(1).section(QRegularExpression("\\s+"), 0, 0, QString::SectionSkipEmpty).toInt(); } newIntervals.emplace_back(num, den); } @@ -470,7 +470,7 @@ bool MicrotunerConfig::applyKeymap() { if (line.isEmpty()) {continue;} if (line[0] == '!') {continue;} // comment - QString firstSection = line.section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty); + QString firstSection = line.section(QRegularExpression("\\s+"), 0, 0, QString::SectionSkipEmpty); if (firstSection == "x") { newMap.push_back(-1); // not mapped diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index fa9be2a50..a726dd5b9 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -420,7 +420,7 @@ void InstrumentTrackWindow::saveSettingsBtnClicked() sfd.setDirectory(presetRoot + m_track->instrumentName()); sfd.setFileMode( FileDialog::AnyFile ); QString fname = m_track->name(); - sfd.selectFile(fname.remove(QRegExp(FILENAME_FILTER))); + sfd.selectFile(fname.remove(QRegularExpression(FILENAME_FILTER))); sfd.setDefaultSuffix( "xpf"); if( sfd.exec() == QDialog::Accepted && diff --git a/src/gui/modals/EffectSelectDialog.cpp b/src/gui/modals/EffectSelectDialog.cpp index 4e6427e81..65976059f 100644 --- a/src/gui/modals/EffectSelectDialog.cpp +++ b/src/gui/modals/EffectSelectDialog.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -126,7 +127,12 @@ EffectSelectDialog::EffectSelectDialog(QWidget* parent) : m_filterEdit = new QLineEdit(this); connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &text) { +// TODO: Cleanup when we don't support Qt5 anymore +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) + m_model.setFilterRegularExpression(QRegularExpression(text, QRegularExpression::CaseInsensitiveOption)); +#else m_model.setFilterRegExp(QRegExp(text, Qt::CaseInsensitive)); +#endif }); connect(m_filterEdit, &QLineEdit::textChanged, this, &EffectSelectDialog::updateSelection); m_filterEdit->setFocus(); diff --git a/src/gui/modals/VersionedSaveDialog.cpp b/src/gui/modals/VersionedSaveDialog.cpp index 0c61df9f2..c8e1c6821 100644 --- a/src/gui/modals/VersionedSaveDialog.cpp +++ b/src/gui/modals/VersionedSaveDialog.cpp @@ -89,9 +89,9 @@ VersionedSaveDialog::VersionedSaveDialog( QWidget *parent, bool VersionedSaveDialog::changeFileNameVersion(QString &fileName, bool increment ) { - static QRegExp regexp( "[- ]\\d+(\\.\\w+)?$" ); + static QRegularExpression regex( "[- ]\\d+(\\.\\w+)?$" ); - int idx = regexp.indexIn( fileName ); + int idx = regex.match(fileName).capturedStart(); // For file names without extension (no ".mmpz") int insertIndex = fileName.lastIndexOf( '.' ); if ( insertIndex < idx+1 ) From af7431be994bdbb6e0a1edbc1ec6261bc47dddd3 Mon Sep 17 00:00:00 2001 From: TechnoPorg <69441745+TechnoPorg@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:53:22 -0600 Subject: [PATCH 107/191] Add GitHub issue forms for bug reports and feature requests (#7102) --- .github/ISSUE_TEMPLATE/bug_report.md | 36 --------- .github/ISSUE_TEMPLATE/bug_report.yml | 90 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 18 ----- .github/ISSUE_TEMPLATE/feature_request.yml | 31 ++++++++ 4 files changed, 121 insertions(+), 54 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index fcc875601..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -# Please search the issue tracker for existing bug reports before submitting your own. Delete this line to confirm no similar report has been posted yet. - -### Bug Summary - -#### Steps to reproduce - -#### Expected behavior - -#### Actual behavior - -#### Screenshot - -#### Affected LMMS versions - - - -#### Logs -
- Click to expand -
-
-
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..4cd1464d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,90 @@ +# yamllint disable-file rule:line-length +name: Bug Report +description: File a bug report to help us improve +labels: + - bug +body: + - type: input + id: system-information + attributes: + label: System Information + description: | + - The operating system you use to run LMMS. + - When relevant, also include your hardware information. + placeholder: ex. Fedora Linux 39, KDE Plasma 5.27.10 - 13th Gen Intel® Core™ i9-13950HX, 32GB RAM + validations: + required: true + - type: input + id: affected-version + attributes: + label: LMMS Version(s) + description: | + - The version of LMMS affected by the bug. + - Can be an official version number, nightly release identifier, or commit hash. + - The version number can be found under the Help > About menu. + placeholder: ex. 1.2.2, 1.3.0-alpha.1.518+gdd53bec31, 2d185df + validations: + required: true + - type: input + id: working-version + attributes: + label: Most Recent Working Version + description: | + - If there is a previous version of LMMS that did not exhibit the bug, include it here. + placeholder: ex. 1.2.2, 1.3.0-alpha.1.518+gdd53bec31, 2d185df + validations: + required: false + - type: textarea + id: bug-summary + attributes: + label: Bug Summary + description: Briefly describe the bug. + validations: + required: true + - type: textarea + id: expected-behaviour + attributes: + label: Expected Behaviour + description: Describe what should have happened. + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps To Reproduce + description: | + - Describe the minimum set of steps required to reproduce this bug. + - If you included a minimum reproducible project below, you can describe here how it should be used. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: | + - Copy and paste any relevant log output here. + value: | +
+ Click to expand +
+          
+        
+
+ validations: + required: false + - type: textarea + id: supporting-files + attributes: + label: Screenshots / Minimum Reproducible Project + description: | + - Upload any screenshots showing the bug in action. + - If possible, also include a .mmp/.mmpz project containing the simplest possible setup needed to reproduce the bug. + + ***Note:** To upload a project file to GitHub, it will need to be placed in a .zip archive.* + - type: checkboxes + id: search-for-existing + attributes: + label: Please search the issue tracker for existing bug reports before submitting your own. + options: + - label: I have searched all existing issues and confirmed that this is not a duplicate. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index f9a0ae192..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -# Please search the issue tracker for existing feature requests before submitting your own. Delete this line to confirm no similar request has been posted yet. - -### Enhancement Summary - -#### Justification - -#### Mockup - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..1f11b4eb3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,31 @@ +# yamllint disable-file rule:line-length +name: Feature Request +description: Suggest an idea for the project +labels: + - "enhancement" +body: + - type: textarea + id: enhancement-summary + attributes: + label: Enhancement Summary + description: | + - Briefly describe the enhancement. + - Explain why you believe the proposed enhancement to be a good idea, and (if applicable) how it helps overcome a limitation of LMMS you are currently facing. + validations: + required: true + - type: textarea + id: mockup + attributes: + label: Implementation Details / Mockup + description: | + - Explain how you believe this enhancement should be implemented. + - If your proposal encompasses changes to the user interface, include diagrams displaying your intent. + validations: + required: true + - type: checkboxes + id: search-for-existing + attributes: + label: Please search the issue tracker for existing feature requests before submitting your own. + options: + - label: I have searched all existing issues and confirmed that this is not a duplicate. + required: true From 3f5ac806e9c74048da6e2b0276ddc3581fdc575a Mon Sep 17 00:00:00 2001 From: Alexander Medvedev <71594357+Snowiiii@users.noreply.github.com> Date: Sun, 17 Mar 2024 00:53:40 +0100 Subject: [PATCH 108/191] Update jack2 (#7147) --- src/3rdparty/jack2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/3rdparty/jack2 b/src/3rdparty/jack2 index db76dd6bb..ac334fabf 160000 --- a/src/3rdparty/jack2 +++ b/src/3rdparty/jack2 @@ -1 +1 @@ -Subproject commit db76dd6bb879a0a24d73ec41cc2e6a21bca8ee08 +Subproject commit ac334fabfb56989e9115ee6e2a77c1f6162d14fb From 3e19d1335f478504c22ed68af9c96fccd6a6774b Mon Sep 17 00:00:00 2001 From: Alexander Medvedev <71594357+Snowiiii@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:43:41 +0100 Subject: [PATCH 109/191] Update carla (#7149) --- plugins/CarlaBase/carla | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/CarlaBase/carla b/plugins/CarlaBase/carla index 4ac8ff2ef..66afe24a0 160000 --- a/plugins/CarlaBase/carla +++ b/plugins/CarlaBase/carla @@ -1 +1 @@ -Subproject commit 4ac8ff2ef412d4ab190d2e285e318b1f339af4ae +Subproject commit 66afe24a08790732cc17d81d4b846a1e0cfa0118 From 9ff63a5f5a49f445ee53afcb055adff447638b59 Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 24 Mar 2024 18:05:15 -0400 Subject: [PATCH 110/191] Set mixer channel LCD when its index changes (#7160) --- src/gui/MixerChannelView.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index 928255806..e7d4fc5b3 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -277,6 +277,7 @@ namespace lmms::gui m_muteButton->setModel(&mixerChannel->m_muteModel); m_soloButton->setModel(&mixerChannel->m_soloModel); m_effectRackView->setModel(&mixerChannel->m_fxChain); + m_channelNumberLcd->setValue(index); m_channelIndex = index; } From c2fdb8cfd20957bc4f6a9078b647574e24f72257 Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Mon, 25 Mar 2024 18:22:37 -0500 Subject: [PATCH 111/191] Move confirmation to remove mixer channels outside `MixerView::deleteChannel` #7154) Moves the confirmation to remove mixer channels outside of `MixerView::deleteChannel` and into `MixerChannelView::removeChannel`. This is so that the confirmation only applies when the user uses the context menu to remove a channel, which is important since no confirmation should apply when, for example, the mixer view is cleared with calls to `MixerView::clear`. --- include/MixerChannelView.h | 1 + include/MixerView.h | 1 - src/gui/MixerChannelView.cpp | 39 ++++++++++++++++++++++++++++ src/gui/MixerView.cpp | 49 ------------------------------------ 4 files changed, 40 insertions(+), 50 deletions(-) diff --git a/include/MixerChannelView.h b/include/MixerChannelView.h index 1710623d7..cbaf0ccc6 100644 --- a/include/MixerChannelView.h +++ b/include/MixerChannelView.h @@ -104,6 +104,7 @@ namespace lmms::gui void moveChannelRight(); private: + bool confirmRemoval(int index); QString elideName(const QString& name); MixerChannel* mixerChannel() const; auto isMasterChannel() const -> bool { return m_channelIndex == 0; } diff --git a/include/MixerView.h b/include/MixerView.h index 81287bc54..89315f93a 100644 --- a/include/MixerView.h +++ b/include/MixerView.h @@ -78,7 +78,6 @@ public: // notify the view that a mixer channel was deleted void deleteChannel(int index); - bool confirmRemoval(int index); // delete all unused channels void deleteUnusedChannels(); diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index e7d4fc5b3..bd518093b 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -29,6 +29,7 @@ #include "MixerChannelView.h" #include "MixerView.h" #include "Song.h" +#include "ConfigManager.h" #include "gui_templates.h" #include "lmms_math.h" @@ -38,6 +39,8 @@ #include #include #include +#include +#include #include @@ -412,8 +415,44 @@ namespace lmms::gui update(); } + bool MixerChannelView::confirmRemoval(int index) + { + // if config variable is set to false, there is no need for user confirmation + bool needConfirm = ConfigManager::inst()->value("ui", "mixerchanneldeletionwarning", "1").toInt(); + if (!needConfirm) { return true; } + + // is the channel is not in use, there is no need for user confirmation + if (!getGUI()->mixerView()->getMixer()->isChannelInUse(index)) { return true; } + + QString messageRemoveTrack = tr("This Mixer Channel is being used.\n" + "Are you sure you want to remove this channel?\n\n" + "Warning: This operation can not be undone."); + + QString messageTitleRemoveTrack = tr("Confirm removal"); + QString askAgainText = tr("Don't ask again"); + auto askAgainCheckBox = new QCheckBox(askAgainText, nullptr); + connect(askAgainCheckBox, &QCheckBox::stateChanged, [](int state) { + // Invert button state, if it's checked we *shouldn't* ask again + ConfigManager::inst()->setValue("ui", "mixerchanneldeletionwarning", state ? "0" : "1"); + }); + + QMessageBox mb(this); + mb.setText(messageRemoveTrack); + mb.setWindowTitle(messageTitleRemoveTrack); + mb.setIcon(QMessageBox::Warning); + mb.addButton(QMessageBox::Cancel); + mb.addButton(QMessageBox::Ok); + mb.setCheckBox(askAgainCheckBox); + mb.setDefaultButton(QMessageBox::Cancel); + + int answer = mb.exec(); + + return answer == QMessageBox::Ok; + } + void MixerChannelView::removeChannel() { + if (!confirmRemoval(m_channelIndex)) { return; } auto mix = getGUI()->mixerView(); mix->deleteChannel(m_channelIndex); } diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 8b2ecdc56..e97b5414f 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -23,9 +23,7 @@ */ -#include #include -#include #include #include #include @@ -377,12 +375,6 @@ void MixerView::deleteChannel(int index) // can't delete master if (index == 0) return; - // if there is no user confirmation, do nothing - if (!confirmRemoval(index)) - { - return; - } - // Disconnect from the solo/mute models of the channel we are about to delete disconnectFromSoloAndMute(index); @@ -421,47 +413,6 @@ void MixerView::deleteChannel(int index) updateMaxChannelSelector(); } -bool MixerView::confirmRemoval(int index) -{ - // if config variable is set to false, there is no need for user confirmation - bool needConfirm = ConfigManager::inst()->value("ui", "mixerchanneldeletionwarning", "1").toInt(); - if (!needConfirm) { return true; } - - Mixer* mix = getMixer(); - - if (!mix->isChannelInUse(index)) - { - // is the channel is not in use, there is no need for user confirmation - return true; - } - - QString messageRemoveTrack = tr("This Mixer Channel is being used.\n" - "Are you sure you want to remove this channel?\n\n" - "Warning: This operation can not be undone."); - - QString messageTitleRemoveTrack = tr("Confirm removal"); - QString askAgainText = tr("Don't ask again"); - auto askAgainCheckBox = new QCheckBox(askAgainText, nullptr); - connect(askAgainCheckBox, &QCheckBox::stateChanged, [](int state) { - // Invert button state, if it's checked we *shouldn't* ask again - ConfigManager::inst()->setValue("ui", "mixerchanneldeletionwarning", state ? "0" : "1"); - }); - - QMessageBox mb(this); - mb.setText(messageRemoveTrack); - mb.setWindowTitle(messageTitleRemoveTrack); - mb.setIcon(QMessageBox::Warning); - mb.addButton(QMessageBox::Cancel); - mb.addButton(QMessageBox::Ok); - mb.setCheckBox(askAgainCheckBox); - mb.setDefaultButton(QMessageBox::Cancel); - - int answer = mb.exec(); - - return answer == QMessageBox::Ok; -} - - void MixerView::deleteUnusedChannels() { Mixer* mix = getMixer(); From 0e1eb712d834c12f20fe6e959a1561209c5d0efe Mon Sep 17 00:00:00 2001 From: Lassi <57831590+87x@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:16:47 +0200 Subject: [PATCH 112/191] Bump project year (#7167) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e45db03e4..d10a62d10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,7 @@ INCLUDE(GenerateExportHeader) STRING(TOUPPER "${CMAKE_PROJECT_NAME}" PROJECT_NAME_UCASE) -SET(PROJECT_YEAR 2023) +SET(PROJECT_YEAR 2024) SET(PROJECT_AUTHOR "LMMS Developers") SET(PROJECT_URL "https://lmms.io") From 66081ba1b1924b45aff1a6ab07336d463360224d Mon Sep 17 00:00:00 2001 From: Alexander Medvedev <71594357+Snowiiii@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:49:13 +0100 Subject: [PATCH 113/191] CMake: Replace EXEC_PROGRAM with execute_process (#7166) * CMake: Replace EXEC_PROGRAM with execute_process exec_program is Deprecated since version 3.0: Use the execute_process() command instead. --- cmake/modules/DetectMachine.cmake | 2 +- cmake/modules/FindWine.cmake | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/modules/DetectMachine.cmake b/cmake/modules/DetectMachine.cmake index 388efeb82..65bc6d2b7 100644 --- a/cmake/modules/DetectMachine.cmake +++ b/cmake/modules/DetectMachine.cmake @@ -92,7 +92,7 @@ IF(WIN32) endif() ELSE() # Detect target architecture based on compiler target triple e.g. "x86_64-pc-linux" - EXEC_PROGRAM( ${CMAKE_C_COMPILER} ARGS "-dumpmachine ${CMAKE_C_FLAGS}" OUTPUT_VARIABLE Machine ) + execute_process(COMMAND ${CMAKE_C_COMPILER} -dumpmachine ${CMAKE_C_FLAGS} OUTPUT_VARIABLE Machine) MESSAGE("Machine: ${Machine}") STRING(REGEX MATCH "i.86" IS_X86 "${Machine}") STRING(REGEX MATCH "86_64|amd64" IS_X86_64 "${Machine}") diff --git a/cmake/modules/FindWine.cmake b/cmake/modules/FindWine.cmake index 024dac1ea..aabb5ef78 100644 --- a/cmake/modules/FindWine.cmake +++ b/cmake/modules/FindWine.cmake @@ -32,8 +32,8 @@ FIND_PROGRAM(WINE_CXX FIND_PROGRAM(WINE_BUILD NAMES winebuild) # Detect wine paths and handle linking problems IF(WINE_CXX) - EXEC_PROGRAM(${WINE_CXX} ARGS "-m32 -v /dev/zero" OUTPUT_VARIABLE WINEBUILD_OUTPUT_32) - EXEC_PROGRAM(${WINE_CXX} ARGS "-m64 -v /dev/zero" OUTPUT_VARIABLE WINEBUILD_OUTPUT_64) + execute_process(COMMAND ${WINE_CXX} -m32 -v /dev/zero OUTPUT_VARIABLE WINEBUILD_OUTPUT_32) + execute_process(COMMAND ${WINE_CXX} -m64 -v /dev/zero OUTPUT_VARIABLE WINEBUILD_OUTPUT_64) _findwine_find_flags("${WINEBUILD_OUTPUT_32}" "^-isystem/usr/include$" BUGGED_WINEGCC) _findwine_find_flags("${WINEBUILD_OUTPUT_32}" "^-isystem" WINEGCC_INCLUDE_DIR) _findwine_find_flags("${WINEBUILD_OUTPUT_32}" "libwinecrt0\\.a.*" WINECRT_32) From 45fd3267879b93d4972c6c28cf7e5e7007b4324e Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:10:26 -0700 Subject: [PATCH 114/191] Make Compressor background themeable (#7157) --- data/themes/classic/style.css | 1 + data/themes/default/style.css | 1 + .../Compressor/CompressorControlDialog.cpp | 20 +++--------------- plugins/Compressor/CompressorControlDialog.h | 2 ++ plugins/Compressor/artwork.png | Bin 1405 -> 0 bytes 5 files changed, 7 insertions(+), 17 deletions(-) delete mode 100755 plugins/Compressor/artwork.png diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index f61d4ba58..2398c541e 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -996,6 +996,7 @@ lmms--gui--CompressorControlDialog { qproperty-textColor: rgba(209, 216, 228, 50); qproperty-graphColor: rgba(209, 216, 228, 50); qproperty-resetColor: rgba(200, 100, 15, 200); + qproperty-backgroundColor: rgba(7, 8, 9, 255); } lmms--gui--CompressorControlDialog lmms--gui--Knob { diff --git a/data/themes/default/style.css b/data/themes/default/style.css index e1f0cf395..a0d202347 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -1038,6 +1038,7 @@ lmms--gui--CompressorControlDialog { qproperty-textColor: rgba(209, 216, 228, 50); qproperty-graphColor: rgba(209, 216, 228, 50); qproperty-resetColor: rgba(200, 100, 15, 200); + qproperty-backgroundColor: rgba(7, 8, 9, 255); } lmms--gui--CompressorControlDialog lmms--gui--Knob { diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index ab81c84ec..d7350ba59 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -45,26 +45,12 @@ namespace lmms::gui CompressorControlDialog::CompressorControlDialog(CompressorControls* controls) : EffectControlDialog(controls), - m_controls(controls), - m_inVolAreaColor(209, 216, 228, 17), - m_inVolColor(209, 216, 228, 100), - m_outVolAreaColor(209, 216, 228, 30), - m_outVolColor(209, 216, 228, 240), - m_gainReductionColor(180, 100, 100, 210), - m_kneeColor(39, 171, 95, 255), - m_kneeColor2(9, 171, 160, 255), - m_threshColor(39, 171, 95, 100), - m_textColor(209, 216, 228, 50), - m_graphColor(209, 216, 228, 50), - m_resetColor(200, 100, 15, 200) + m_controls(controls) { setAutoFillBackground(false); setAttribute(Qt::WA_OpaquePaintEvent, true); setAttribute(Qt::WA_NoSystemBackground, true); - QPalette pal; - pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); - setPalette(pal); setMinimumSize(MIN_COMP_SCREEN_X, MIN_COMP_SCREEN_Y); resize(COMP_SCREEN_X, COMP_SCREEN_Y); @@ -615,7 +601,7 @@ void CompressorControlDialog::paintEvent(QPaintEvent *event) m_p.begin(this); m_p.setCompositionMode(QPainter::CompositionMode_Source); - m_p.fillRect(0, 0, m_windowSizeX, m_windowSizeY, QColor("transparent")); + m_p.fillRect(0, 0, m_windowSizeX, m_windowSizeY, m_backgroundColor); m_p.setCompositionMode(QPainter::CompositionMode_SourceOver); m_p.drawPixmap(0, 0, m_graphPixmap); @@ -683,7 +669,7 @@ void CompressorControlDialog::drawGraph() // Redraw graph m_p.setPen(QPen(m_graphColor, 1)); - for (int i = 1; i < m_dbRange / COMP_GRID_SPACING + 1; ++i) + for (int i = 0; i < m_dbRange / COMP_GRID_SPACING + 1; ++i) { m_p.drawLine(0, dbfsToYPoint(-COMP_GRID_SPACING * i), m_windowSizeX, dbfsToYPoint(-COMP_GRID_SPACING * i)); m_p.drawLine(dbfsToXPoint(-COMP_GRID_SPACING * i), 0, dbfsToXPoint(-COMP_GRID_SPACING * i), m_kneeWindowSizeY); diff --git a/plugins/Compressor/CompressorControlDialog.h b/plugins/Compressor/CompressorControlDialog.h index a61482ad8..cedba4b04 100755 --- a/plugins/Compressor/CompressorControlDialog.h +++ b/plugins/Compressor/CompressorControlDialog.h @@ -86,6 +86,7 @@ public: Q_PROPERTY(QColor textColor MEMBER m_textColor) Q_PROPERTY(QColor graphColor MEMBER m_graphColor) Q_PROPERTY(QColor resetColor MEMBER m_resetColor) + Q_PROPERTY(QColor backgroundColor MEMBER m_backgroundColor) protected: void resizeEvent(QResizeEvent *event) override; @@ -150,6 +151,7 @@ private: QColor m_textColor = QColor(209, 216, 228, 50); QColor m_graphColor = QColor(209, 216, 228, 50); QColor m_resetColor = QColor(200, 100, 15, 200); + QColor m_backgroundColor = QColor(7, 8, 9, 255); float m_peakAvg; float m_gainAvg; diff --git a/plugins/Compressor/artwork.png b/plugins/Compressor/artwork.png deleted file mode 100755 index 8f94e3d494b7e7577ec5c34ac46d894c54a0945e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1405 zcmeAS@N?(olHy`uVBq!ia0y~yV7kD-z$n4N1{5h}Id}s|u_bxCyDx` z7I;J!Gca%qgD@k*tT_@uLG}_)Usv`gECNgtBK3D?DFTHQGD9Ltobz*YQ}arITn2`K z{PNVI)D(rJN(FbnP<@}wywZv}fz96-7+CIkx;TbZ+P>{iZgJEM3=Udr-bDLPr zrnwyQmw%rw%fOKE#0zK!n-l{Bj|l^VL&e}4icrB&`CR}2 From a41da8155402092277449ca256721a05b5aa62d4 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Tue, 26 Mar 2024 20:19:52 +0100 Subject: [PATCH 115/191] Enable different colors for oscilloscope channels (#7155) Enable to set different colors for the oscilloscope channels: * Left channel color * Right channel color * Color of all other channels The clipping color is now used per channel, i.e. if the left channel clips but the right does not then only the signal of the left channel is painted in the clipping color. Enable setting the colors in the style sheets and adjust the style sheets of the default and classic theme accordingly. --- data/themes/classic/style.css | 4 ++- data/themes/default/style.css | 4 ++- include/Oscilloscope.h | 20 +++++++++--- src/gui/widgets/Oscilloscope.cpp | 56 +++++++++++++++++++++----------- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 2398c541e..08a04f01d 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -203,7 +203,9 @@ lmms--gui--GroupBox { lmms--gui--Oscilloscope { background: none; border: none; - qproperty-normalColor: rgb(71, 253, 133); + qproperty-leftChannelColor: rgb(71, 253, 133); + qproperty-rightChannelColor: rgb(238, 253, 71); + qproperty-otherChannelsColor: rgb(71, 235, 253); qproperty-clippingColor: rgb(255, 64, 64); } diff --git a/data/themes/default/style.css b/data/themes/default/style.css index a0d202347..89c14f20a 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -234,7 +234,9 @@ lmms--gui--GroupBox { lmms--gui--Oscilloscope { background: none; border: none; - qproperty-normalColor: rgb(71, 253, 133); + qproperty-leftChannelColor: rgb(71, 253, 133); + qproperty-rightChannelColor: rgb(238, 253, 71); + qproperty-otherChannelsColor: rgb(71, 235, 253); qproperty-clippingColor: rgb(255, 64, 64); } diff --git a/include/Oscilloscope.h b/include/Oscilloscope.h index 209370ce0..13c946aa5 100644 --- a/include/Oscilloscope.h +++ b/include/Oscilloscope.h @@ -38,7 +38,9 @@ class Oscilloscope : public QWidget { Q_OBJECT public: - Q_PROPERTY( QColor normalColor READ normalColor WRITE setNormalColor ) + Q_PROPERTY( QColor leftChannelColor READ leftChannelColor WRITE setLeftChannelColor ) + Q_PROPERTY( QColor rightChannelColor READ rightChannelColor WRITE setRightChannelColor ) + Q_PROPERTY( QColor otherChannelsColor READ otherChannelsColor WRITE setOtherChannelsColor ) Q_PROPERTY( QColor clippingColor READ clippingColor WRITE setClippingColor ) Oscilloscope( QWidget * _parent ); @@ -46,8 +48,14 @@ public: void setActive( bool _active ); - QColor const & normalColor() const; - void setNormalColor(QColor const & normalColor); + QColor const & leftChannelColor() const; + void setLeftChannelColor(QColor const & leftChannelColor); + + QColor const & rightChannelColor() const; + void setRightChannelColor(QColor const & rightChannelColor); + + QColor const & otherChannelsColor() const; + void setOtherChannelsColor(QColor const & otherChannelsColor); QColor const & clippingColor() const; void setClippingColor(QColor const & clippingColor); @@ -62,7 +70,7 @@ protected slots: void updateAudioBuffer( const lmms::surroundSampleFrame * buffer ); private: - QColor const & determineLineColor(float level) const; + bool clips(float level) const; private: QPixmap m_background; @@ -71,7 +79,9 @@ private: sampleFrame * m_buffer; bool m_active; - QColor m_normalColor; + QColor m_leftChannelColor; + QColor m_rightChannelColor; + QColor m_otherChannelsColor; QColor m_clippingColor; } ; diff --git a/src/gui/widgets/Oscilloscope.cpp b/src/gui/widgets/Oscilloscope.cpp index a689f53f3..f426e1b19 100644 --- a/src/gui/widgets/Oscilloscope.cpp +++ b/src/gui/widgets/Oscilloscope.cpp @@ -45,7 +45,9 @@ Oscilloscope::Oscilloscope( QWidget * _p ) : m_background( embed::getIconPixmap( "output_graph" ) ), m_points( new QPointF[Engine::audioEngine()->framesPerPeriod()] ), m_active( false ), - m_normalColor(71, 253, 133), + m_leftChannelColor(71, 253, 133), + m_rightChannelColor(71, 253, 133), + m_otherChannelsColor(71, 253, 133), m_clippingColor(255, 64, 64) { setFixedSize( m_background.width(), m_background.height() ); @@ -112,14 +114,34 @@ void Oscilloscope::setActive( bool _active ) } -QColor const & Oscilloscope::normalColor() const +QColor const & Oscilloscope::leftChannelColor() const { - return m_normalColor; + return m_leftChannelColor; } -void Oscilloscope::setNormalColor(QColor const & normalColor) +void Oscilloscope::setLeftChannelColor(QColor const & leftChannelColor) { - m_normalColor = normalColor; + m_leftChannelColor = leftChannelColor; +} + +QColor const & Oscilloscope::rightChannelColor() const +{ + return m_rightChannelColor; +} + +void Oscilloscope::setRightChannelColor(QColor const & rightChannelColor) +{ + m_rightChannelColor = rightChannelColor; +} + +QColor const & Oscilloscope::otherChannelsColor() const +{ + return m_otherChannelsColor; +} + +void Oscilloscope::setOtherChannelsColor(QColor const & otherChannelsColor) +{ + m_otherChannelsColor = otherChannelsColor; } QColor const & Oscilloscope::clippingColor() const @@ -147,11 +169,9 @@ void Oscilloscope::paintEvent( QPaintEvent * ) const fpp_t frames = audioEngine->framesPerPeriod(); AudioEngine::StereoSample peakValues = audioEngine->getPeakValues(m_buffer, frames); - const float max_level = qMax( peakValues.left, peakValues.right ); - // Set the color of the line according to the maximum level - float const maxLevelWithAppliedMasterGain = max_level * masterOutput; - p.setPen(QPen(determineLineColor(maxLevelWithAppliedMasterGain), 0.7)); + auto const leftChannelClips = clips(peakValues.left * masterOutput); + auto const rightChannelClips = clips(peakValues.right * masterOutput); p.setRenderHint( QPainter::Antialiasing ); @@ -162,8 +182,14 @@ void Oscilloscope::paintEvent( QPaintEvent * ) int x_base = 2; const qreal y_base = height() / 2 - 0.5; + qreal const width = 0.7; for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch ) { + QColor color = ch == 0 ? (leftChannelClips ? clippingColor() : leftChannelColor()) : // Check left channel + ch == 1 ? (rightChannelClips ? clippingColor() : rightChannelColor()) : // Check right channel + otherChannelsColor(); // Any other channel + p.setPen(QPen(color, width)); + for( int frame = 0; frame < frames; ++frame ) { sample_t const clippedSample = AudioEngine::clip(m_buffer[frame][ch]); @@ -193,17 +219,9 @@ void Oscilloscope::mousePressEvent( QMouseEvent * _me ) } } - -QColor const & Oscilloscope::determineLineColor(float level) const +bool Oscilloscope::clips(float level) const { - if( level <= 1.0f ) - { - return normalColor(); - } - else - { - return clippingColor(); - } + return level > 1.0f; } From 286d15724aa701641e94005ab456d28f6f0b084a Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Wed, 27 Mar 2024 18:29:27 +0530 Subject: [PATCH 116/191] Refactor `gui_templates.h` (#7159) * remove the gui_templates header where functions not used * refactor and replace template with function argument * refactor: merge pointSizeF function with pointSize * removed template per michael's review * use std::max for more readability * remove the QDesktopWidget header * cleanup arguments and remove parentheses from return --- include/gui_templates.h | 44 +++++++------------ .../AudioFileProcessorView.cpp | 4 +- .../AudioFileProcessorWaveView.cpp | 2 +- plugins/CarlaBase/Carla.cpp | 4 +- .../DualFilter/DualFilterControlDialog.cpp | 4 +- plugins/LadspaBrowser/LadspaBrowser.cpp | 2 +- plugins/Monstro/Monstro.cpp | 10 ++--- plugins/Patman/Patman.cpp | 4 +- plugins/Stk/Mallets/Mallets.cpp | 2 +- plugins/Vestige/Vestige.cpp | 8 ++-- plugins/VstEffect/VstEffectControlDialog.cpp | 2 +- plugins/ZynAddSubFx/ZynAddSubFx.cpp | 2 +- src/core/audio/AudioJack.cpp | 1 - src/core/audio/AudioOss.cpp | 1 - src/core/audio/AudioPortAudio.cpp | 1 - src/core/audio/AudioPulseAudio.cpp | 1 - src/core/audio/AudioSdl.cpp | 1 - src/core/audio/AudioSndio.cpp | 1 - src/core/audio/AudioSoundIo.cpp | 1 - src/gui/AudioAlsaSetupWidget.cpp | 1 - src/gui/EffectView.cpp | 4 +- src/gui/Lv2ViewBase.cpp | 3 +- src/gui/MidiSetupWidget.cpp | 1 - src/gui/MixerChannelView.cpp | 2 +- src/gui/SampleTrackWindow.cpp | 1 - src/gui/editors/AutomationEditor.cpp | 4 +- src/gui/editors/PianoRoll.cpp | 4 +- src/gui/instrument/EnvelopeAndLfoView.cpp | 6 +-- .../instrument/InstrumentFunctionViews.cpp | 8 ++-- src/gui/instrument/InstrumentMidiIOView.cpp | 2 +- .../instrument/InstrumentSoundShapingView.cpp | 4 +- src/gui/instrument/InstrumentTrackWindow.cpp | 2 +- src/gui/instrument/InstrumentTuningView.cpp | 2 +- src/gui/instrument/PianoView.cpp | 2 +- src/gui/menus/MidiPortMenu.cpp | 1 - src/gui/modals/SetupDialog.cpp | 1 - src/gui/tracks/TrackOperationsWidget.cpp | 1 - src/gui/widgets/ComboBox.cpp | 2 +- src/gui/widgets/GroupBox.cpp | 2 +- src/gui/widgets/Knob.cpp | 6 +-- src/gui/widgets/LcdFloatSpinBox.cpp | 2 +- src/gui/widgets/LcdWidget.cpp | 4 +- src/gui/widgets/LedCheckBox.cpp | 7 +-- src/gui/widgets/MeterDialog.cpp | 4 +- src/gui/widgets/Oscilloscope.cpp | 2 +- src/gui/widgets/TabBar.cpp | 2 +- src/gui/widgets/TabWidget.cpp | 8 ++-- 47 files changed, 77 insertions(+), 106 deletions(-) diff --git a/include/gui_templates.h b/include/gui_templates.h index c0afbdfc0..4833c6cdf 100644 --- a/include/gui_templates.h +++ b/include/gui_templates.h @@ -27,45 +27,33 @@ #include "lmmsconfig.h" +#include #include #include -#include +#include -namespace lmms +// TODO: remove once qt5 support is dropped +#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) + #include +#endif + +namespace lmms::gui { // return DPI-independent font-size - font with returned font-size has always // the same size in pixels -template -inline QFont pointSize( QFont _f ) +inline QFont pointSize(QFont fontPointer, float fontSize) { - static const float DPI = 96; -#ifdef LMMS_BUILD_WIN32 - _f.setPointSizeF( ((float) SIZE+0.5f) * DPI / - QApplication::desktop()->logicalDpiY() ); -#else - _f.setPointSizeF( (float) SIZE * DPI / - QApplication::desktop()->logicalDpiY() ); -#endif - return( _f ); + // to calculate DPI of a screen to make it HiDPI ready + qreal devicePixelRatio = QGuiApplication::primaryScreen()->devicePixelRatio(); + qreal scaleFactor = std::max(devicePixelRatio, 1.0); // Ensure scaleFactor is at least 1.0 + + fontPointer.setPointSizeF(fontSize * scaleFactor); + return fontPointer; } -inline QFont pointSizeF( QFont _f, float SIZE ) -{ - static const float DPI = 96; -#ifdef LMMS_BUILD_WIN32 - _f.setPointSizeF( (SIZE+0.5f) * DPI / - QApplication::desktop()->logicalDpiY() ); -#else - _f.setPointSizeF( SIZE * DPI / - QApplication::desktop()->logicalDpiY() ); -#endif - return( _f ); -} - - -} // namespace lmms +} // namespace lmms::gui #endif // LMMS_GUI_TEMPLATES_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp index d16b1d019..94f0da4fb 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp @@ -134,7 +134,7 @@ AudioFileProcessorView::AudioFileProcessorView(Instrument* instrument, // interpolation selector m_interpBox = new ComboBox(this); m_interpBox->setGeometry(142, 62, 82, ComboBox::DEFAULT_HEIGHT); - m_interpBox->setFont(pointSize<8>(m_interpBox->font())); + m_interpBox->setFont(pointSize(m_interpBox->font(), 8)); // wavegraph m_waveView = 0; @@ -228,7 +228,7 @@ void AudioFileProcessorView::paintEvent(QPaintEvent*) int idx = a->sample().sampleFile().length(); - p.setFont(pointSize<8>(font())); + p.setFont(pointSize(font(), 8)); QFontMetrics fm(p.font()); diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index 507c4e7c0..818fb14cb 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -273,7 +273,7 @@ void AudioFileProcessorWaveView::paintEvent(QPaintEvent * pe) p.fillRect(s_padding, s_padding, m_graph.width(), 14, g); p.setPen(QColor(255, 255, 255)); - p.setFont(pointSize<8>(font())); + p.setFont(pointSize(font(), 8)); QString length_text; const int length = m_sample->sampleDuration().count(); diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index 819736e92..e58cb14af 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -632,7 +632,7 @@ CarlaInstrumentView::CarlaInstrumentView(CarlaInstrument* const instrument, QWid m_toggleUIButton->setCheckable( true ); m_toggleUIButton->setChecked( false ); m_toggleUIButton->setIcon( embed::getIconPixmap( "zoom" ) ); - m_toggleUIButton->setFont( pointSize<8>( m_toggleUIButton->font() ) ); + m_toggleUIButton->setFont(pointSize(m_toggleUIButton->font(), 8)); connect( m_toggleUIButton, SIGNAL( clicked(bool) ), this, SLOT( toggleUI( bool ) ) ); m_toggleUIButton->setToolTip( @@ -642,7 +642,7 @@ CarlaInstrumentView::CarlaInstrumentView(CarlaInstrument* const instrument, QWid m_toggleParamsWindowButton = new QPushButton(tr("Params"), this); m_toggleParamsWindowButton->setIcon(embed::getIconPixmap("controller")); m_toggleParamsWindowButton->setCheckable(true); - m_toggleParamsWindowButton->setFont(pointSize<8>(m_toggleParamsWindowButton->font())); + m_toggleParamsWindowButton->setFont(pointSize(m_toggleParamsWindowButton->font(), 8)); #if CARLA_VERSION_HEX < CARLA_MIN_PARAM_VERSION m_toggleParamsWindowButton->setEnabled(false); m_toggleParamsWindowButton->setToolTip(tr("Available from Carla version 2.1 and up.")); diff --git a/plugins/DualFilter/DualFilterControlDialog.cpp b/plugins/DualFilter/DualFilterControlDialog.cpp index d316e3372..5a912ac85 100644 --- a/plugins/DualFilter/DualFilterControlDialog.cpp +++ b/plugins/DualFilter/DualFilterControlDialog.cpp @@ -76,12 +76,12 @@ DualFilterControlDialog::DualFilterControlDialog( DualFilterControls* controls ) auto m_filter1ComboBox = new ComboBox(this); m_filter1ComboBox->setGeometry( 19, 70, 137, ComboBox::DEFAULT_HEIGHT ); - m_filter1ComboBox->setFont( pointSize<8>( m_filter1ComboBox->font() ) ); + m_filter1ComboBox->setFont(pointSize(m_filter1ComboBox->font(), 8)); m_filter1ComboBox->setModel( &controls->m_filter1Model ); auto m_filter2ComboBox = new ComboBox(this); m_filter2ComboBox->setGeometry( 217, 70, 137, ComboBox::DEFAULT_HEIGHT ); - m_filter2ComboBox->setFont( pointSize<8>( m_filter2ComboBox->font() ) ); + m_filter2ComboBox->setFont(pointSize(m_filter2ComboBox->font(), 8)); m_filter2ComboBox->setModel( &controls->m_filter2Model ); } diff --git a/plugins/LadspaBrowser/LadspaBrowser.cpp b/plugins/LadspaBrowser/LadspaBrowser.cpp index 31be64056..54d019aad 100644 --- a/plugins/LadspaBrowser/LadspaBrowser.cpp +++ b/plugins/LadspaBrowser/LadspaBrowser.cpp @@ -172,7 +172,7 @@ QWidget * LadspaBrowserView::createTab( QWidget * _parent, const QString & _txt, auto title = new QLabel(type + _txt, tab); QFont f = title->font(); f.setBold( true ); - title->setFont( pointSize<12>( f ) ); + title->setFont(pointSize(f, 12)); layout->addSpacing( 5 ); layout->addWidget( title ); diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index 9563f756d..76ab6e477 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -1694,7 +1694,7 @@ QWidget * MonstroView::setupOperatorsView( QWidget * _parent ) m_osc2WaveBox = new ComboBox( view ); m_osc2WaveBox -> setGeometry( 204, O2ROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_osc2WaveBox->setFont( pointSize<8>( m_osc2WaveBox->font() ) ); + m_osc2WaveBox->setFont(pointSize(m_osc2WaveBox->font(), 8)); maketinyled( m_osc2SyncHButton, 212, O2ROW - 3, tr( "Hard sync oscillator 2" ) ) maketinyled( m_osc2SyncRButton, 191, O2ROW - 3, tr( "Reverse sync oscillator 2" ) ) @@ -1709,18 +1709,18 @@ QWidget * MonstroView::setupOperatorsView( QWidget * _parent ) m_osc3Wave1Box = new ComboBox( view ); m_osc3Wave1Box -> setGeometry( 160, O3ROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_osc3Wave1Box->setFont( pointSize<8>( m_osc3Wave1Box->font() ) ); + m_osc3Wave1Box->setFont(pointSize(m_osc3Wave1Box->font(), 8)); m_osc3Wave2Box = new ComboBox( view ); m_osc3Wave2Box -> setGeometry( 204, O3ROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_osc3Wave2Box->setFont( pointSize<8>( m_osc3Wave2Box->font() ) ); + m_osc3Wave2Box->setFont(pointSize(m_osc3Wave2Box->font(), 8)); maketinyled( m_osc3SyncHButton, 212, O3ROW - 3, tr( "Hard sync oscillator 3" ) ) maketinyled( m_osc3SyncRButton, 191, O3ROW - 3, tr( "Reverse sync oscillator 3" ) ) m_lfo1WaveBox = new ComboBox( view ); m_lfo1WaveBox -> setGeometry( 2, LFOROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_lfo1WaveBox->setFont( pointSize<8>( m_lfo1WaveBox->font() ) ); + m_lfo1WaveBox->setFont(pointSize(m_lfo1WaveBox->font(), 8)); maketsknob( m_lfo1AttKnob, LFOCOL1, LFOROW, tr( "Attack" ), " ms", "lfoKnob" ) maketsknob( m_lfo1RateKnob, LFOCOL2, LFOROW, tr( "Rate" ), " ms", "lfoKnob" ) @@ -1728,7 +1728,7 @@ QWidget * MonstroView::setupOperatorsView( QWidget * _parent ) m_lfo2WaveBox = new ComboBox( view ); m_lfo2WaveBox -> setGeometry( 127, LFOROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_lfo2WaveBox->setFont( pointSize<8>( m_lfo2WaveBox->font() ) ); + m_lfo2WaveBox->setFont(pointSize(m_lfo2WaveBox->font(), 8)); maketsknob(m_lfo2AttKnob, LFOCOL4, LFOROW, tr("Attack"), " ms", "lfoKnob") maketsknob(m_lfo2RateKnob, LFOCOL5, LFOROW, tr("Rate"), " ms", "lfoKnob") diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index e525498a5..25664ae0d 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -548,7 +548,7 @@ void PatmanView::updateFilename() m_displayFilename = ""; int idx = m_pi->m_patchFile.length(); - QFontMetrics fm( pointSize<8>( font() ) ); + QFontMetrics fm(pointSize(font(), 8)); // simple algorithm for creating a text from the filename that // matches in the white rectangle @@ -618,7 +618,7 @@ void PatmanView::paintEvent( QPaintEvent * ) { QPainter p( this ); - p.setFont( pointSize<8>( font() ) ); + p.setFont(pointSize(font() ,8)); p.drawText( 8, 116, 235, 16, Qt::AlignLeft | Qt::TextSingleLine | Qt::AlignVCenter, m_displayFilename ); diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index c67814b5f..1d7cbd86b 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -450,7 +450,7 @@ MalletsInstrumentView::MalletsInstrumentView( MalletsInstrument * _instrument, m_presetsCombo = new ComboBox( this, tr( "Instrument" ) ); m_presetsCombo->setGeometry( 140, 50, 99, ComboBox::DEFAULT_HEIGHT ); - m_presetsCombo->setFont( pointSize<8>( m_presetsCombo->font() ) ); + m_presetsCombo->setFont(pointSize(m_presetsCombo->font(), 8)); connect( &_instrument->m_presetsModel, SIGNAL( dataChanged() ), this, SLOT( changePreset() ) ); diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index 583075c0c..a3ec267e5 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -587,7 +587,7 @@ VestigeInstrumentView::VestigeInstrumentView( Instrument * _instrument, m_toggleGUIButton = new QPushButton( tr( "Show/hide GUI" ), this ); m_toggleGUIButton->setGeometry( 20, 130, 200, 24 ); m_toggleGUIButton->setIcon( embed::getIconPixmap( "zoom" ) ); - m_toggleGUIButton->setFont( pointSize<8>( m_toggleGUIButton->font() ) ); + m_toggleGUIButton->setFont(pointSize(m_toggleGUIButton->font(), 8)); connect( m_toggleGUIButton, SIGNAL( clicked() ), this, SLOT( toggleGUI() ) ); @@ -596,7 +596,7 @@ VestigeInstrumentView::VestigeInstrumentView( Instrument * _instrument, this); note_off_all_btn->setGeometry( 20, 160, 200, 24 ); note_off_all_btn->setIcon( embed::getIconPixmap( "stop" ) ); - note_off_all_btn->setFont( pointSize<8>( note_off_all_btn->font() ) ); + note_off_all_btn->setFont(pointSize(note_off_all_btn->font(), 8)); connect( note_off_all_btn, SIGNAL( clicked() ), this, SLOT( noteOffAll() ) ); @@ -881,7 +881,7 @@ void VestigeInstrumentView::paintEvent( QPaintEvent * ) tr( "No VST plugin loaded" ); QFont f = p.font(); f.setBold( true ); - p.setFont( pointSize<10>( f ) ); + p.setFont(pointSize(f, 10)); p.setPen( QColor( 255, 255, 255 ) ); p.drawText( 10, 100, plugin_name ); @@ -893,7 +893,7 @@ void VestigeInstrumentView::paintEvent( QPaintEvent * ) { p.setPen( QColor( 0, 0, 0 ) ); f.setBold( false ); - p.setFont( pointSize<8>( f ) ); + p.setFont(pointSize(f, 8)); p.drawText( 10, 114, tr( "by " ) + m_vi->m_plugin->vendorString() ); p.setPen( QColor( 255, 255, 255 ) ); diff --git a/plugins/VstEffect/VstEffectControlDialog.cpp b/plugins/VstEffect/VstEffectControlDialog.cpp index 5bee94155..671eef561 100644 --- a/plugins/VstEffect/VstEffectControlDialog.cpp +++ b/plugins/VstEffect/VstEffectControlDialog.cpp @@ -246,7 +246,7 @@ VstEffectControlDialog::VstEffectControlDialog( VstEffectControls * _ctl ) : tb->addWidget(space1); tbLabel = new QLabel( tr( "Effect by: " ), this ); - tbLabel->setFont( pointSize<7>( f ) ); + tbLabel->setFont(pointSize(f, 7)); tbLabel->setTextFormat(Qt::RichText); tbLabel->setAlignment( Qt::AlignTop | Qt::AlignLeft ); tb->addWidget( tbLabel ); diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index 01fa6400b..4988e1b8b 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -541,7 +541,7 @@ ZynAddSubFxView::ZynAddSubFxView( Instrument * _instrument, QWidget * _parent ) m_toggleUIButton->setCheckable( true ); m_toggleUIButton->setChecked( false ); m_toggleUIButton->setIcon( embed::getIconPixmap( "zoom" ) ); - m_toggleUIButton->setFont( pointSize<8>( m_toggleUIButton->font() ) ); + m_toggleUIButton->setFont(pointSize(m_toggleUIButton->font(), 8)); connect( m_toggleUIButton, SIGNAL( toggled( bool ) ), this, SLOT( toggleUI() ) ); diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index 61d7814ed..64819cbeb 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -37,7 +37,6 @@ #include "LcdSpinBox.h" #include "MainWindow.h" #include "MidiJack.h" -#include "gui_templates.h" namespace lmms { diff --git a/src/core/audio/AudioOss.cpp b/src/core/audio/AudioOss.cpp index bd6d46c95..1653e93fa 100644 --- a/src/core/audio/AudioOss.cpp +++ b/src/core/audio/AudioOss.cpp @@ -34,7 +34,6 @@ #include "LcdSpinBox.h" #include "AudioEngine.h" #include "Engine.h" -#include "gui_templates.h" #ifdef LMMS_HAVE_UNISTD_H #include diff --git a/src/core/audio/AudioPortAudio.cpp b/src/core/audio/AudioPortAudio.cpp index f303545d2..2fbdb04b5 100644 --- a/src/core/audio/AudioPortAudio.cpp +++ b/src/core/audio/AudioPortAudio.cpp @@ -53,7 +53,6 @@ void AudioPortAudioSetupUtil::updateChannels() #include "Engine.h" #include "ConfigManager.h" -#include "gui_templates.h" #include "ComboBox.h" #include "AudioEngine.h" diff --git a/src/core/audio/AudioPulseAudio.cpp b/src/core/audio/AudioPulseAudio.cpp index a0c5ccaf9..63b81a9e9 100644 --- a/src/core/audio/AudioPulseAudio.cpp +++ b/src/core/audio/AudioPulseAudio.cpp @@ -32,7 +32,6 @@ #include "ConfigManager.h" #include "LcdSpinBox.h" #include "AudioEngine.h" -#include "gui_templates.h" #include "Engine.h" namespace lmms diff --git a/src/core/audio/AudioSdl.cpp b/src/core/audio/AudioSdl.cpp index 679912c50..da81886ac 100644 --- a/src/core/audio/AudioSdl.cpp +++ b/src/core/audio/AudioSdl.cpp @@ -32,7 +32,6 @@ #include "AudioEngine.h" #include "ConfigManager.h" -#include "gui_templates.h" namespace lmms { diff --git a/src/core/audio/AudioSndio.cpp b/src/core/audio/AudioSndio.cpp index d934dfb9c..7d8e7fa8c 100644 --- a/src/core/audio/AudioSndio.cpp +++ b/src/core/audio/AudioSndio.cpp @@ -35,7 +35,6 @@ #include "LcdSpinBox.h" #include "AudioEngine.h" #include "Engine.h" -#include "gui_templates.h" #include "ConfigManager.h" diff --git a/src/core/audio/AudioSoundIo.cpp b/src/core/audio/AudioSoundIo.cpp index 6e8a03e38..a3d72e36b 100644 --- a/src/core/audio/AudioSoundIo.cpp +++ b/src/core/audio/AudioSoundIo.cpp @@ -32,7 +32,6 @@ #include "Engine.h" #include "debug.h" #include "ConfigManager.h" -#include "gui_templates.h" #include "ComboBox.h" #include "AudioEngine.h" diff --git a/src/gui/AudioAlsaSetupWidget.cpp b/src/gui/AudioAlsaSetupWidget.cpp index 7db822b4b..bc0ecde8e 100644 --- a/src/gui/AudioAlsaSetupWidget.cpp +++ b/src/gui/AudioAlsaSetupWidget.cpp @@ -31,7 +31,6 @@ #include "ConfigManager.h" #include "LcdSpinBox.h" -#include "gui_templates.h" namespace lmms::gui { diff --git a/src/gui/EffectView.cpp b/src/gui/EffectView.cpp index 7f7f9ee9d..cbe2e4e95 100644 --- a/src/gui/EffectView.cpp +++ b/src/gui/EffectView.cpp @@ -90,7 +90,7 @@ EffectView::EffectView( Effect * _model, QWidget * _parent ) : { auto ctls_btn = new QPushButton(tr("Controls"), this); QFont f = ctls_btn->font(); - ctls_btn->setFont( pointSize<8>( f ) ); + ctls_btn->setFont(pointSize(f, 8)); ctls_btn->setGeometry( 150, 14, 50, 20 ); connect( ctls_btn, SIGNAL(clicked()), this, SLOT(editControls())); @@ -257,7 +257,7 @@ void EffectView::paintEvent( QPaintEvent * ) QPainter p( this ); p.drawPixmap( 0, 0, m_bg ); - QFont f = pointSizeF( font(), 7.5f ); + QFont f = pointSize(font(), 7.5f); f.setBold( true ); p.setFont( f ); diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 77268bb9b..6de47f450 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -157,8 +157,7 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) : m_toggleUIButton->setCheckable(true); m_toggleUIButton->setChecked(false); m_toggleUIButton->setIcon(embed::getIconPixmap("zoom")); - m_toggleUIButton->setFont( - pointSize<8>(m_toggleUIButton->font())); + m_toggleUIButton->setFont(pointSize(m_toggleUIButton->font(), 8)); btnBox->addWidget(m_toggleUIButton, 0); } btnBox->addStretch(1); diff --git a/src/gui/MidiSetupWidget.cpp b/src/gui/MidiSetupWidget.cpp index 2385def02..0e6678727 100644 --- a/src/gui/MidiSetupWidget.cpp +++ b/src/gui/MidiSetupWidget.cpp @@ -28,7 +28,6 @@ #include #include "ConfigManager.h" -#include "gui_templates.h" namespace lmms::gui diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index bd518093b..9b43991d3 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -76,7 +76,7 @@ namespace lmms::gui m_renameLineEdit = new QLineEdit{mixerName, nullptr}; m_renameLineEdit->setFixedWidth(65); - m_renameLineEdit->setFont(pointSizeF(font(), 7.5f)); + m_renameLineEdit->setFont(pointSize(font(), 7.5f)); m_renameLineEdit->setReadOnly(true); m_renameLineEdit->installEventFilter(this); diff --git a/src/gui/SampleTrackWindow.cpp b/src/gui/SampleTrackWindow.cpp index c0dd8e04e..630119253 100644 --- a/src/gui/SampleTrackWindow.cpp +++ b/src/gui/SampleTrackWindow.cpp @@ -33,7 +33,6 @@ #include "EffectRackView.h" #include "embed.h" -#include "gui_templates.h" #include "GuiApplication.h" #include "Knob.h" #include "MainWindow.h" diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 8fd892597..ae026fff3 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1065,7 +1065,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) p.fillRect( 0, 0, width(), height(), bgColor ); // set font-size to 8 - p.setFont( pointSize<8>( p.font() ) ); + p.setFont(pointSize(p.font(), 8)); int grid_height = height() - TOP_MARGIN - SCROLLBAR_SIZE; @@ -1423,7 +1423,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) { QFont f = p.font(); f.setBold( true ); - p.setFont( pointSize<14>( f ) ); + p.setFont(pointSize(f, 14)); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::BrightText ) ); p.drawText( VALUES_WIDTH + 20, TOP_MARGIN + 40, diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index ff8832325..47e658f11 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -3338,7 +3338,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) // display note editing info //QFont f = p.font(); f.setBold( false ); - p.setFont( pointSize<10>( f ) ); + p.setFont(pointSize(f, 10)); p.setPen(m_noteModeColor); p.drawText( QRect( 0, keyAreaBottom(), m_whiteKeyWidth, noteEditBottom() - keyAreaBottom()), @@ -3601,7 +3601,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) { QFont f = p.font(); f.setBold( true ); - p.setFont( pointSize<14>( f ) ); + p.setFont(pointSize(f, 14)); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::BrightText ) ); p.drawText(m_whiteKeyWidth + 20, PR_TOP_MARGIN + 40, diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index c3bf53b39..4043ea229 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -207,7 +207,7 @@ EnvelopeAndLfoView::EnvelopeAndLfoView( QWidget * _parent ) : m_lfoWaveBtnGrp->addButton( random_lfo_btn ); m_x100Cb = new LedCheckBox( tr( "FREQ x 100" ), this ); - m_x100Cb->setFont( pointSizeF( m_x100Cb->font(), 6.5 ) ); + m_x100Cb->setFont(pointSize(m_x100Cb->font(), 6.5)); m_x100Cb->move( LFO_PREDELAY_KNOB_X, LFO_GRAPH_Y + 36 ); m_x100Cb->setToolTip(tr("Multiply LFO frequency by 100")); @@ -215,7 +215,7 @@ EnvelopeAndLfoView::EnvelopeAndLfoView( QWidget * _parent ) : m_controlEnvAmountCb = new LedCheckBox( tr( "MODULATE ENV AMOUNT" ), this ); m_controlEnvAmountCb->move( LFO_PREDELAY_KNOB_X, LFO_GRAPH_Y + 54 ); - m_controlEnvAmountCb->setFont( pointSizeF( m_controlEnvAmountCb->font(), 6.5 ) ); + m_controlEnvAmountCb->setFont(pointSize(m_controlEnvAmountCb->font(), 6.5)); m_controlEnvAmountCb->setToolTip( tr( "Control envelope amount by this LFO" ) ); @@ -340,7 +340,7 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) // draw LFO-graph p.drawPixmap(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph); - p.setFont( pointSize<8>( p.font() ) ); + p.setFont(pointSize(p.font(), 8)); const float gray_amount = 1.0f - fabsf( m_amountKnob->value() ); diff --git a/src/gui/instrument/InstrumentFunctionViews.cpp b/src/gui/instrument/InstrumentFunctionViews.cpp index c9aa04272..ea1648600 100644 --- a/src/gui/instrument/InstrumentFunctionViews.cpp +++ b/src/gui/instrument/InstrumentFunctionViews.cpp @@ -57,7 +57,7 @@ InstrumentFunctionNoteStackingView::InstrumentFunctionNoteStackingView( Instrume mainLayout->setVerticalSpacing( 1 ); auto chordLabel = new QLabel(tr("Chord:")); - chordLabel->setFont( pointSize<8>( chordLabel->font() ) ); + chordLabel->setFont(pointSize(chordLabel->font(), 8)); m_chordRangeKnob->setLabel( tr( "RANGE" ) ); m_chordRangeKnob->setHintText( tr( "Chord range:" ), " " + tr( "octave(s)" ) ); @@ -146,13 +146,13 @@ InstrumentFunctionArpeggioView::InstrumentFunctionArpeggioView( InstrumentFuncti m_arpGateKnob->setHintText( tr( "Arpeggio gate:" ), tr( "%" ) ); auto arpChordLabel = new QLabel(tr("Chord:")); - arpChordLabel->setFont( pointSize<8>( arpChordLabel->font() ) ); + arpChordLabel->setFont(pointSize(arpChordLabel->font(), 8)); auto arpDirectionLabel = new QLabel(tr("Direction:")); - arpDirectionLabel->setFont( pointSize<8>( arpDirectionLabel->font() ) ); + arpDirectionLabel->setFont(pointSize(arpDirectionLabel->font(), 8)); auto arpModeLabel = new QLabel(tr("Mode:")); - arpModeLabel->setFont( pointSize<8>( arpModeLabel->font() ) ); + arpModeLabel->setFont(pointSize(arpModeLabel->font(), 8)); mainLayout->addWidget( arpChordLabel, 0, 0 ); mainLayout->addWidget( m_arpComboBox, 1, 0 ); diff --git a/src/gui/instrument/InstrumentMidiIOView.cpp b/src/gui/instrument/InstrumentMidiIOView.cpp index e321d061e..1e95751ea 100644 --- a/src/gui/instrument/InstrumentMidiIOView.cpp +++ b/src/gui/instrument/InstrumentMidiIOView.cpp @@ -155,7 +155,7 @@ InstrumentMidiIOView::InstrumentMidiIOView( QWidget* parent ) : auto baseVelocityHelp = new QLabel(tr("Specify the velocity normalization base for MIDI-based instruments at 100% note velocity.")); baseVelocityHelp->setWordWrap( true ); - baseVelocityHelp->setFont( pointSize<8>( baseVelocityHelp->font() ) ); + baseVelocityHelp->setFont(pointSize(baseVelocityHelp->font(), 8)); baseVelocityLayout->addWidget( baseVelocityHelp ); diff --git a/src/gui/instrument/InstrumentSoundShapingView.cpp b/src/gui/instrument/InstrumentSoundShapingView.cpp index 1bfc166b3..59df3097c 100644 --- a/src/gui/instrument/InstrumentSoundShapingView.cpp +++ b/src/gui/instrument/InstrumentSoundShapingView.cpp @@ -77,7 +77,7 @@ InstrumentSoundShapingView::InstrumentSoundShapingView( QWidget * _parent ) : m_filterComboBox = new ComboBox( m_filterGroupBox ); m_filterComboBox->setGeometry( 14, 22, 120, ComboBox::DEFAULT_HEIGHT ); - m_filterComboBox->setFont( pointSize<8>( m_filterComboBox->font() ) ); + m_filterComboBox->setFont(pointSize(m_filterComboBox->font(), 8)); m_filterCutKnob = new Knob( KnobType::Bright26, m_filterGroupBox ); @@ -94,7 +94,7 @@ InstrumentSoundShapingView::InstrumentSoundShapingView( QWidget * _parent ) : m_singleStreamInfoLabel = new QLabel( tr( "Envelopes, LFOs and filters are not supported by the current instrument." ), this ); m_singleStreamInfoLabel->setWordWrap( true ); - m_singleStreamInfoLabel->setFont( pointSize<8>( m_singleStreamInfoLabel->font() ) ); + m_singleStreamInfoLabel->setFont(pointSize(m_singleStreamInfoLabel->font(), 8)); m_singleStreamInfoLabel->setGeometry( TARGETS_TABWIDGET_X, TARGETS_TABWIDGET_Y, diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index a726dd5b9..b6a51624b 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -107,7 +107,7 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : // setup line edit for changing instrument track name m_nameLineEdit = new QLineEdit; - m_nameLineEdit->setFont( pointSize<9>( m_nameLineEdit->font() ) ); + m_nameLineEdit->setFont(pointSize(m_nameLineEdit->font(), 9)); connect( m_nameLineEdit, SIGNAL( textChanged( const QString& ) ), this, SLOT( textChanged( const QString& ) ) ); diff --git a/src/gui/instrument/InstrumentTuningView.cpp b/src/gui/instrument/InstrumentTuningView.cpp index 355d7d18c..41c18213b 100644 --- a/src/gui/instrument/InstrumentTuningView.cpp +++ b/src/gui/instrument/InstrumentTuningView.cpp @@ -60,7 +60,7 @@ InstrumentTuningView::InstrumentTuningView(InstrumentTrack *it, QWidget *parent) auto tlabel = new QLabel(tr("Enables the use of global transposition")); tlabel->setWordWrap(true); - tlabel->setFont(pointSize<8>(tlabel->font())); + tlabel->setFont(pointSize(tlabel->font(), 8)); masterPitchLayout->addWidget(tlabel); // Microtuner settings diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index c8882898b..87ee6af9b 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -807,7 +807,7 @@ void PianoView::paintEvent( QPaintEvent * ) QPainter p( this ); // set smaller font for printing number of every octave - p.setFont( pointSize( p.font() ) ); + p.setFont(pointSize(p.font(), LABEL_TEXT_SIZE)); // draw bar above the keyboard (there will be the labels diff --git a/src/gui/menus/MidiPortMenu.cpp b/src/gui/menus/MidiPortMenu.cpp index b99c3a0b7..6c573fdf5 100644 --- a/src/gui/menus/MidiPortMenu.cpp +++ b/src/gui/menus/MidiPortMenu.cpp @@ -24,7 +24,6 @@ */ #include "MidiPortMenu.h" -#include "gui_templates.h" namespace lmms::gui { diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index fffa94c82..63560e33d 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -37,7 +37,6 @@ #include "embed.h" #include "Engine.h" #include "FileDialog.h" -#include "gui_templates.h" #include "MainWindow.h" #include "MidiSetupWidget.h" #include "ProjectJournal.h" diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index e846370e6..de119c64f 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -38,7 +38,6 @@ #include "DataFile.h" #include "embed.h" #include "Engine.h" -#include "gui_templates.h" #include "InstrumentTrackView.h" #include "PixmapButton.h" #include "Song.h" diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index ccc0c675b..b11990b27 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -49,7 +49,7 @@ ComboBox::ComboBox( QWidget * _parent, const QString & _name ) : { setFixedHeight( ComboBox::DEFAULT_HEIGHT ); - setFont( pointSize<9>( font() ) ); + setFont(pointSize(font(), 9)); connect( &m_menu, SIGNAL(triggered(QAction*)), this, SLOT(setItem(QAction*))); diff --git a/src/gui/widgets/GroupBox.cpp b/src/gui/widgets/GroupBox.cpp index e3e71a812..229ab13cd 100644 --- a/src/gui/widgets/GroupBox.cpp +++ b/src/gui/widgets/GroupBox.cpp @@ -111,7 +111,7 @@ void GroupBox::paintEvent( QPaintEvent * pe ) // draw text p.setPen( palette().color( QPalette::Active, QPalette::Text ) ); - p.setFont( pointSize<8>( font() ) ); + p.setFont(pointSize(font(), 8)); int const captionX = ledButtonShown() ? 22 : 6; p.drawText(captionX, m_titleBarHeight, m_caption); diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 00a9363c8..a6411d6cf 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -139,7 +139,7 @@ void Knob::setLabel( const QString & txt ) if( m_knobPixmap ) { setFixedSize(qMax( m_knobPixmap->width(), - horizontalAdvance(QFontMetrics(pointSizeF(font(), 6.5)), m_label)), + horizontalAdvance(QFontMetrics(pointSize(font(), 6.5)), m_label)), m_knobPixmap->height() + 10); } @@ -459,7 +459,7 @@ void Knob::paintEvent( QPaintEvent * _me ) { if (!m_isHtmlLabel) { - p.setFont(pointSizeF(p.font(), 6.5)); + p.setFont(pointSize(p.font(), 6.5f)); p.setPen(textColor()); p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2, @@ -467,7 +467,7 @@ void Knob::paintEvent( QPaintEvent * _me ) } else { - m_tdRenderer->setDefaultFont(pointSizeF(p.font(), 6.5)); + m_tdRenderer->setDefaultFont(pointSize(p.font(), 6.5f)); p.translate((width() - m_tdRenderer->idealWidth()) / 2, (height() - m_tdRenderer->pageSize().height()) / 2); m_tdRenderer->drawContents(&p); } diff --git a/src/gui/widgets/LcdFloatSpinBox.cpp b/src/gui/widgets/LcdFloatSpinBox.cpp index c7e20467a..667a03481 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -245,7 +245,7 @@ void LcdFloatSpinBox::paintEvent(QPaintEvent*) // Label if (!m_label.isEmpty()) { - p.setFont(pointSizeF(p.font(), 6.5)); + p.setFont(pointSize(p.font(), 6.5f)); p.setPen(m_wholeDisplay.textShadowColor()); p.drawText(width() / 2 - p.fontMetrics().boundingRect(m_label).width() / 2 + 1, height(), m_label); p.setPen(m_wholeDisplay.textColor()); diff --git a/src/gui/widgets/LcdWidget.cpp b/src/gui/widgets/LcdWidget.cpp index a409fee8b..fa7dea1da 100644 --- a/src/gui/widgets/LcdWidget.cpp +++ b/src/gui/widgets/LcdWidget.cpp @@ -203,7 +203,7 @@ void LcdWidget::paintEvent( QPaintEvent* ) // Label if( !m_label.isEmpty() ) { - p.setFont( pointSizeF( p.font(), 6.5 ) ); + p.setFont(pointSize(p.font(), 6.5f)); p.setPen( textShadowColor() ); p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2 + 1, @@ -255,7 +255,7 @@ void LcdWidget::updateSize() setFixedSize( qMax( m_cellWidth * m_numDigits + marginX1 + marginX2, - horizontalAdvance(QFontMetrics(pointSizeF(font(), 6.5)), m_label) + horizontalAdvance(QFontMetrics(pointSize(font(), 6.5f)), m_label) ), m_cellHeight + (2 * marginY) + 9 ); diff --git a/src/gui/widgets/LedCheckBox.cpp b/src/gui/widgets/LedCheckBox.cpp index 1dbf650ed..3cb85deff 100644 --- a/src/gui/widgets/LedCheckBox.cpp +++ b/src/gui/widgets/LedCheckBox.cpp @@ -92,10 +92,7 @@ void LedCheckBox::initUi( LedColor _color ) m_ledOnPixmap = embed::getIconPixmap(names[static_cast(_color)].toUtf8().constData()); m_ledOffPixmap = embed::getIconPixmap("led_off"); - if (m_legacyMode) - { - setFont( pointSize<7>( font() ) ); - } + if (m_legacyMode){ setFont(pointSize(font(), 7)); } setText( m_text ); } @@ -116,7 +113,7 @@ void LedCheckBox::onTextUpdated() void LedCheckBox::paintLegacy(QPaintEvent * pe) { QPainter p( this ); - p.setFont( pointSize<7>( font() ) ); + p.setFont(pointSize(font(), 7)); p.drawPixmap(0, 0, model()->value() ? m_ledOnPixmap : m_ledOffPixmap); diff --git a/src/gui/widgets/MeterDialog.cpp b/src/gui/widgets/MeterDialog.cpp index ced08382e..eb8e54353 100644 --- a/src/gui/widgets/MeterDialog.cpp +++ b/src/gui/widgets/MeterDialog.cpp @@ -60,7 +60,7 @@ MeterDialog::MeterDialog( QWidget * _parent, bool _simple ) : { auto num_label = new QLabel(tr("Meter Numerator"), num); QFont f = num_label->font(); - num_label->setFont( pointSize<7>( f ) ); + num_label->setFont(pointSize(f, 7)); num_layout->addSpacing( 5 ); num_layout->addWidget( num_label ); } @@ -84,7 +84,7 @@ MeterDialog::MeterDialog( QWidget * _parent, bool _simple ) : { auto den_label = new QLabel(tr("Meter Denominator"), den); QFont f = den_label->font(); - den_label->setFont( pointSize<7>( f ) ); + den_label->setFont(pointSize(f, 7)); den_layout->addSpacing( 5 ); den_layout->addWidget( den_label ); } diff --git a/src/gui/widgets/Oscilloscope.cpp b/src/gui/widgets/Oscilloscope.cpp index f426e1b19..bd944937a 100644 --- a/src/gui/widgets/Oscilloscope.cpp +++ b/src/gui/widgets/Oscilloscope.cpp @@ -203,7 +203,7 @@ void Oscilloscope::paintEvent( QPaintEvent * ) else { p.setPen( QColor( 192, 192, 192 ) ); - p.setFont( pointSize<7>( p.font() ) ); + p.setFont(pointSize(p.font(), 7)); p.drawText( 6, height()-5, tr( "Click to enable" ) ); } } diff --git a/src/gui/widgets/TabBar.cpp b/src/gui/widgets/TabBar.cpp index e29494551..ce706d5f8 100644 --- a/src/gui/widgets/TabBar.cpp +++ b/src/gui/widgets/TabBar.cpp @@ -90,7 +90,7 @@ TabButton * TabBar::addTab( QWidget * _w, const QString & _text, int _id, _w->setFixedSize( _w->parentWidget()->size() ); } - b->setFont( pointSize<8>( b->font() ) ); + b->setFont(pointSize(b->font(), 8)); return( b ); } diff --git a/src/gui/widgets/TabWidget.cpp b/src/gui/widgets/TabWidget.cpp index 27671933e..2c93dba3e 100644 --- a/src/gui/widgets/TabWidget.cpp +++ b/src/gui/widgets/TabWidget.cpp @@ -58,7 +58,7 @@ TabWidget::TabWidget(const QString& caption, QWidget* parent, bool usePixmap, m_tabheight = caption.isEmpty() ? m_tabbarHeight - 3 : m_tabbarHeight - 4; - setFont(pointSize<8>(font())); + setFont(pointSize(font(), 8)); setAutoFillBackground(true); QColor bg_color = QApplication::palette().color(QPalette::Active, QPalette::Window).darker(132); @@ -70,7 +70,7 @@ TabWidget::TabWidget(const QString& caption, QWidget* parent, bool usePixmap, void TabWidget::addTab(QWidget* w, const QString& name, const char* pixmap, int idx) { - setFont(pointSize<8>(font())); + setFont(pointSize(font(), 8)); // Append tab when position is not given if (idx < 0/* || m_widgets.contains(idx) == true*/) @@ -216,7 +216,7 @@ void TabWidget::resizeEvent(QResizeEvent*) void TabWidget::paintEvent(QPaintEvent* pe) { QPainter p(this); - p.setFont(pointSize<7>(font())); + p.setFont(pointSize(font(), 7)); // Draw background QBrush bg_color = p.background(); @@ -232,7 +232,7 @@ void TabWidget::paintEvent(QPaintEvent* pe) // Draw title, if any if (!m_caption.isEmpty()) { - p.setFont(pointSize<8>(p.font())); + p.setFont(pointSize(p.font(), 8)); p.setPen(tabTitleText()); p.drawText(5, 11, m_caption); } From 682be4e82af8aecc235480ee8d39d412a0130b67 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Wed, 27 Mar 2024 06:00:49 -0700 Subject: [PATCH 117/191] Fix DynamicsProcessor bugs (#7168) --- plugins/DynamicsProcessor/DynamicsProcessor.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/DynamicsProcessor/DynamicsProcessor.cpp b/plugins/DynamicsProcessor/DynamicsProcessor.cpp index 6bdf41eee..f58dcedac 100644 --- a/plugins/DynamicsProcessor/DynamicsProcessor.cpp +++ b/plugins/DynamicsProcessor/DynamicsProcessor.cpp @@ -56,7 +56,7 @@ Plugin::Descriptor PLUGIN_EXPORT dynamicsprocessor_plugin_descriptor = } const float DYN_NOISE_FLOOR = 0.00001f; // -100dBFS noise floor -const double DNF_LOG = 5.0; +const double DNF_LOG = -1.0; DynProcEffect::DynProcEffect( Model * _parent, const Descriptor::SubPluginFeatures::Key * _key ) : @@ -82,12 +82,12 @@ DynProcEffect::~DynProcEffect() inline void DynProcEffect::calcAttack() { - m_attCoeff = std::pow(10.f, ( DNF_LOG / ( m_dpControls.m_attackModel.value() * 0.001 ) ) / Engine::audioEngine()->processingSampleRate() ); + m_attCoeff = std::exp((DNF_LOG / (m_dpControls.m_attackModel.value() * 0.001)) / Engine::audioEngine()->processingSampleRate()); } inline void DynProcEffect::calcRelease() { - m_relCoeff = std::pow(10.f, ( -DNF_LOG / ( m_dpControls.m_releaseModel.value() * 0.001 ) ) / Engine::audioEngine()->processingSampleRate() ); + m_relCoeff = std::exp((DNF_LOG / (m_dpControls.m_releaseModel.value() * 0.001)) / Engine::audioEngine()->processingSampleRate()); } @@ -155,15 +155,15 @@ bool DynProcEffect::processAudioBuffer( sampleFrame * _buf, const double t = m_rms[i]->update( s[i] ); if( t > m_currentPeak[i] ) { - m_currentPeak[i] = qMin( m_currentPeak[i] * m_attCoeff, t ); + m_currentPeak[i] = m_currentPeak[i] * m_attCoeff + (1 - m_attCoeff) * t; } else if( t < m_currentPeak[i] ) { - m_currentPeak[i] = qMax( m_currentPeak[i] * m_relCoeff, t ); + m_currentPeak[i] = m_currentPeak[i] * m_relCoeff + (1 - m_relCoeff) * t; } - m_currentPeak[i] = qBound( DYN_NOISE_FLOOR, m_currentPeak[i], 10.0f ); + m_currentPeak[i] = std::max(DYN_NOISE_FLOOR, m_currentPeak[i]); } // account for stereo mode From 6f5f2c2ddd40f3b06f6f2cd428f401d2e25ce13f Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Thu, 28 Mar 2024 18:13:32 +0100 Subject: [PATCH 118/191] Fix glitch in SlicerT (#7174) --- plugins/SlicerT/SlicerT.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index c9e75df60..1c3397046 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -144,8 +144,6 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) workingBuffer[i + offset][1] *= fadeValue; } - instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); - emit isPlaying(noteDone, sliceStart, sliceEnd); } else { emit isPlaying(-1, 0, 0); } From b2f2fc4ad1330516a691f1b8d38a2b4e7f020999 Mon Sep 17 00:00:00 2001 From: saker Date: Thu, 28 Mar 2024 17:21:31 -0400 Subject: [PATCH 119/191] Revisit the initialization for local variables (#7143) * clang-tidy: Apply cppcoreguidelines-init-variables everywhere (treating NaNs as zeros) * Initialize msec and tick outside switch * Update plugins/Vestige/Vestige.cpp Co-authored-by: Kevin Zander * Update plugins/Vestige/Vestige.cpp Co-authored-by: Kevin Zander * Update plugins/Vestige/Vestige.cpp Co-authored-by: Kevin Zander * Update plugins/VstEffect/VstEffectControls.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update plugins/VstEffect/VstEffectControls.cpp Co-authored-by: Kevin Zander * Update plugins/VstEffect/VstEffectControls.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Use initialization with = * Use tabs * Use static_cast * Update DrumSynth.cpp Co-authored-by: Kevin Zander * Update DrumSynth.cpp Co-authored-by: Kevin Zander * Update DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Do not use tabs for alignment in src/core/DrumSynth.cpp Co-authored-by: Dalton Messmer * Move x variable inside loop * Use ternary operator for b variable * Revert "Use tabs" This reverts commit 07afd8a83f58b539c3673310b2aad4b63c9198a0. * Remove unnecessary variables in XpressiveView * Simplify initialization in Plugin * Combine declaration and initialization in EqCurve * Combine declaration and initialization in Song * Combine declaration and initialization in AudioAlsa * Combine declaration and initialization in EqCurve (again) * Missed some * Undo changes made to non-LMMS files * Undo indentation changes in SidInstrument.cpp * Combine declaration with assignment in IoHelper * Combine declaration with assignment using auto in Carla * Combine declaration with assignment * Combine declaration with assignment in BasicFilters * Simplify assignments in AudioFileProcessorWaveView::zoom * Simplify out sample variable in BitInvader * Remove sampleLength variable in DelayEffect * Move gain variable in DynamicsProcessor * Combine peak variable declaration with assignment in EqSpectrumView * Move left/right lfo variables in for loop in FlangerEffect * Use ternary operator for group variable in LadspaControlDialog * Combine declaration with assignment in Lb302 * Combine declaration with assignment in MidiExport * Combine declaration with assignment in MidiFile * Combine declaration with assignment in MidiImport * Use ternary operator for vel_adjusted variable in OpulenZ * Move tmpL and dcblkL variables in for loop in ReverbSC * Combine declaration with initialization in SlicerT * Combine declaration with assignment in SaSpectrumView * Combine declaration with assignment in SaWaterfallView * Combine declaration with assignment in StereoEnhancerEffect * Combine declaration with assignment in VibratingString * Combine declaration with assignment in VstEffectControls * Combine declaration with assignment in Xpressive * Combine declaration with assignment in AutomatableModel * Combine declaration with assignment in AutomationClip * Move sample variable in for loop in BandLimitedWave * Combine declaration with assignment in DataFile * Combine declaration with assignment in DrumSynth * Combine declaration with assignment in Effect * Remove redundant assignment to nphsLeft in InstrumentPlayHandle * Combine declaration with assignment in LadspaManager * Combine declaration with assignment in LinkedModelGroups * Combine declaration with assignment in MemoryHelper * Combine declaration with assignment in AudioAlsa * Combine declaration with assignment in AudioFileOgg * Combine declaration with assignment in AudioPortAudio * Combine declaration with assignment in AudioSoundIo * Combine declaration with assignment in Lv2Evbuf * Combine declaration with assignment in Lv2Proc * Combine declaration with assignment in main * Combine declaration with assignment in MidiAlsaRaw * Combine declaration with assignment in MidiAlsaSeq * Combine declaration with assignment in MidiController * Combine declaration with assignment in MidiJack * Combine declaration with assignment in MidiSndio * Combine declaration with assignment in ControlLayout * Combine declaration with assignment in MainWindow * Combine declaration with assignment in ProjectNotes * Use ternary operator for nextValue variable in AutomationClipView * Combine declaration with assignment in AutomationEditor * Move length variable in for-loop in PianoRoll * Combine declaration with assignment in ControllerConnectionDialog * Combine declaration with assignment in Graph * Combine declaration with assignment in LcdFloatSpinBox * Combine declaration with assignment in TimeDisplayWidget * Remove currentNote variable in InstrumentTrack * Combine declaration with assignment in DrumSynth (again) * Use ternary operator for factor variable in BitInvader * Use ternary operator for highestBandwich variable in EqCurve Bandwich? * Move sum variable into for loop in Graph * Fix format in MidiSndio * Fixup a few more * Cleanup error variables * Use ternary operators and combine declaration with initialization * Combine declaration with initialization * Update plugins/LadspaEffect/LadspaControlDialog.cpp Co-authored-by: Kevin Zander * Update plugins/OpulenZ/OpulenZ.cpp Co-authored-by: Kevin Zander * Update plugins/SpectrumAnalyzer/SaProcessor.cpp Co-authored-by: Kevin Zander * Update src/core/midi/MidiAlsaRaw.cpp Co-authored-by: Kevin Zander * Update src/gui/MainWindow.cpp Co-authored-by: Kevin Zander * Update src/gui/clips/AutomationClipView.cpp Co-authored-by: Kevin Zander * Update src/gui/editors/AutomationEditor.cpp Co-authored-by: Kevin Zander * Update src/gui/widgets/Fader.cpp Co-authored-by: Kevin Zander * Move static_cast conversion into separate variable * Use real index when interpolating * Remove empty line * Make helpBtn a private member * Move controller type into separate variable * Fix format of DrumSynth::waveform function * Use tabs and static_cast * Remove redundant if branch * Refactor using static_cast/reinterpret_cast * Add std namespace prefix * Store repeated conditional into boolean variable * Cast to int before assigning to m_currentLength * Rename note_frames to noteFrames * Update src/core/Controller.cpp Co-authored-by: Kevin Zander * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander * Update src/gui/widgets/Graph.cpp Co-authored-by: Kevin Zander * Revert changes that initialized variables redudantly For situations where the initialization is more complex or passed into a function by a pointer, we dont need to do initialization ourselves since it is already done for us, just in a different way. * Remove redundant err variable * Remove explicit check of err variable * Clean up changes and address review * Do not initialize to 0/nullptr when not needed * Wrap condition in parentheses for readability --------- Co-authored-by: Kevin Zander Co-authored-by: Dalton Messmer --- include/BasicFilters.h | 41 ++++---- include/IoHelper.h | 5 +- include/LocaleHelper.h | 3 +- .../AudioFileProcessorWaveView.cpp | 35 +++---- plugins/BitInvader/BitInvader.cpp | 47 +++------ plugins/CarlaBase/Carla.cpp | 5 +- plugins/Delay/DelayEffect.cpp | 5 +- .../DynamicsProcessor/DynamicsProcessor.cpp | 19 +--- plugins/Eq/EqCurve.cpp | 79 +++++++-------- plugins/Eq/EqFilter.h | 71 ++++++-------- plugins/Eq/EqSpectrumView.cpp | 3 +- plugins/Flanger/FlangerEffect.cpp | 5 +- plugins/FreeBoy/game-music-emu | 2 +- plugins/LadspaEffect/LadspaControlDialog.cpp | 14 +-- plugins/Lb302/Lb302.cpp | 21 ++-- plugins/MidiExport/MidiExport.cpp | 11 +-- plugins/MidiExport/MidiFile.hpp | 41 +++++--- plugins/MidiImport/MidiImport.h | 4 +- plugins/OpulenZ/OpulenZ.cpp | 95 ++++++++---------- plugins/Patman/Patman.cpp | 9 +- plugins/Sid/SidInstrument.cpp | 19 ++-- plugins/SlicerT/SlicerT.cpp | 9 +- plugins/SpectrumAnalyzer/SaProcessor.cpp | 41 +++----- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 24 ++--- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 3 +- plugins/StereoEnhancer/StereoEnhancer.cpp | 11 +-- plugins/Vectorscope/VectorView.cpp | 14 +-- plugins/Vestige/Vestige.cpp | 3 +- plugins/Vibed/VibratingString.cpp | 6 +- plugins/Vibed/VibratingString.h | 25 ++--- plugins/VstEffect/VstEffectControls.cpp | 3 +- plugins/Xpressive/ExprSynth.cpp | 10 +- plugins/Xpressive/Xpressive.cpp | 12 +-- plugins/Xpressive/Xpressive.h | 1 + src/core/AutomatableModel.cpp | 5 +- src/core/AutomationClip.cpp | 8 +- src/core/BandLimitedWave.cpp | 13 ++- src/core/Controller.cpp | 24 ++--- src/core/DataFile.cpp | 7 +- src/core/DrumSynth.cpp | 88 +++++++++-------- src/core/Effect.cpp | 4 +- src/core/InstrumentPlayHandle.cpp | 5 +- src/core/LadspaManager.cpp | 6 +- src/core/LinkedModelGroups.cpp | 7 +- src/core/MemoryHelper.cpp | 7 +- src/core/Plugin.cpp | 4 +- src/core/Song.cpp | 13 ++- src/core/audio/AudioAlsa.cpp | 83 +++++----------- src/core/audio/AudioDevice.cpp | 6 +- src/core/audio/AudioFileOgg.cpp | 10 +- src/core/audio/AudioPortAudio.cpp | 12 +-- src/core/audio/AudioSoundIo.cpp | 36 +++---- src/core/lv2/Lv2Evbuf.cpp | 9 +- src/core/lv2/Lv2Proc.cpp | 8 +- src/core/main.cpp | 12 +-- src/core/midi/MidiAlsaRaw.cpp | 35 +++---- src/core/midi/MidiAlsaSeq.cpp | 5 +- src/core/midi/MidiController.cpp | 6 +- src/core/midi/MidiJack.cpp | 5 +- src/core/midi/MidiSndio.cpp | 13 +-- src/gui/ControlLayout.cpp | 3 +- src/gui/MainWindow.cpp | 3 +- src/gui/ProjectNotes.cpp | 4 +- src/gui/clips/AutomationClipView.cpp | 15 +-- src/gui/editors/AutomationEditor.cpp | 98 +++++-------------- src/gui/editors/PianoRoll.cpp | 11 +-- src/gui/editors/SongEditor.cpp | 14 +-- src/gui/modals/ControllerConnectionDialog.cpp | 5 +- src/gui/widgets/Fader.cpp | 14 ++- src/gui/widgets/Graph.cpp | 3 +- src/gui/widgets/TimeDisplayWidget.cpp | 15 +-- src/tracks/InstrumentTrack.cpp | 12 +-- 72 files changed, 503 insertions(+), 821 deletions(-) diff --git a/include/BasicFilters.h b/include/BasicFilters.h index 215c32dfb..25dcf834c 100644 --- a/include/BasicFilters.h +++ b/include/BasicFilters.h @@ -340,7 +340,7 @@ public: inline sample_t update( sample_t _in0, ch_cnt_t _chnl ) { - sample_t out; + sample_t out = 0.0f; switch( m_type ) { case FilterType::Moog: @@ -375,7 +375,6 @@ public: // input signal is linear-interpolated after oversampling, output signal is averaged from oversampled outputs case FilterType::Tripole: { - out = 0.0f; float ip = 0.0f; for( int i = 0; i < 4; ++i ) { @@ -431,7 +430,6 @@ public: case FilterType::Highpass_SV: { float hp; - for( int i = 0; i < 2; ++i ) // 2x oversample { m_delay2[_chnl] = m_delay2[_chnl] + m_svf1 * m_delay1[_chnl]; @@ -444,8 +442,7 @@ public: case FilterType::Notch_SV: { - float hp1, hp2; - + float hp1; for( int i = 0; i < 2; ++i ) // 2x oversample { m_delay2[_chnl] = m_delay2[_chnl] + m_svf1 * m_delay1[_chnl]; /* delay2/4 = lowpass output */ @@ -453,7 +450,7 @@ public: m_delay1[_chnl] = m_svf1 * hp1 + m_delay1[_chnl]; /* delay1/3 = bandpass output */ m_delay4[_chnl] = m_delay4[_chnl] + m_svf2 * m_delay3[_chnl]; - hp2 = m_delay2[_chnl] - m_delay4[_chnl] - m_svq * m_delay3[_chnl]; + float hp2 = m_delay2[_chnl] - m_delay4[_chnl] - m_svq * m_delay3[_chnl]; m_delay3[_chnl] = m_svf2 * hp2 + m_delay3[_chnl]; } @@ -469,19 +466,19 @@ public: case FilterType::Lowpass_RC12: { - sample_t lp, bp, hp, in; + sample_t lp = 0.0f; for( int n = 4; n != 0; --n ) { - in = _in0 + m_rcbp0[_chnl] * m_rcq; + sample_t in = _in0 + m_rcbp0[_chnl] * m_rcq; in = std::clamp(in, -1.0f, 1.0f); lp = in * m_rcb + m_rclp0[_chnl] * m_rca; lp = std::clamp(lp, -1.0f, 1.0f); - hp = m_rcc * ( m_rchp0[_chnl] + in - m_rclast0[_chnl] ); + sample_t hp = m_rcc * (m_rchp0[_chnl] + in - m_rclast0[_chnl]); hp = std::clamp(hp, -1.0f, 1.0f); - bp = hp * m_rcb + m_rcbp0[_chnl] * m_rca; + sample_t bp = hp * m_rcb + m_rcbp0[_chnl] * m_rca; bp = std::clamp(bp, -1.0f, 1.0f); m_rclast0[_chnl] = in; @@ -494,10 +491,10 @@ public: case FilterType::Highpass_RC12: case FilterType::Bandpass_RC12: { - sample_t hp, bp, in; + sample_t hp, bp; for( int n = 4; n != 0; --n ) { - in = _in0 + m_rcbp0[_chnl] * m_rcq; + sample_t in = _in0 + m_rcbp0[_chnl] * m_rcq; in = std::clamp(in, -1.0f, 1.0f); hp = m_rcc * ( m_rchp0[_chnl] + in - m_rclast0[_chnl] ); @@ -515,20 +512,20 @@ public: case FilterType::Lowpass_RC24: { - sample_t lp, bp, hp, in; + sample_t lp; for( int n = 4; n != 0; --n ) { // first stage is as for the 12dB case... - in = _in0 + m_rcbp0[_chnl] * m_rcq; + sample_t in = _in0 + m_rcbp0[_chnl] * m_rcq; in = std::clamp(in, -1.0f, 1.0f); lp = in * m_rcb + m_rclp0[_chnl] * m_rca; lp = std::clamp(lp, -1.0f, 1.0f); - hp = m_rcc * ( m_rchp0[_chnl] + in - m_rclast0[_chnl] ); + sample_t hp = m_rcc * ( m_rchp0[_chnl] + in - m_rclast0[_chnl] ); hp = std::clamp(hp, -1.0f, 1.0f); - bp = hp * m_rcb + m_rcbp0[_chnl] * m_rca; + sample_t bp = hp * m_rcb + m_rcbp0[_chnl] * m_rca; bp = std::clamp(bp, -1.0f, 1.0f); m_rclast0[_chnl] = in; @@ -559,11 +556,11 @@ public: case FilterType::Highpass_RC24: case FilterType::Bandpass_RC24: { - sample_t hp, bp, in; + sample_t hp, bp; for( int n = 4; n != 0; --n ) { // first stage is as for the 12dB case... - in = _in0 + m_rcbp0[_chnl] * m_rcq; + sample_t in = _in0 + m_rcbp0[_chnl] * m_rcq; in = std::clamp(in, -1.0f, 1.0f); hp = m_rcc * ( m_rchp0[_chnl] + in - m_rclast0[_chnl] ); @@ -600,20 +597,18 @@ public: case FilterType::FastFormant: { if (std::abs(_in0) < 1.0e-10f && std::abs(m_vflast[0][_chnl]) < 1.0e-10f) { return 0.0f; } // performance hack - skip processing when the numbers get too small - sample_t hp, bp, in; - out = 0; const int os = m_type == FilterType::FastFormant ? 1 : 4; // no oversampling for fast formant for( int o = 0; o < os; ++o ) { // first formant - in = _in0 + m_vfbp[0][_chnl] * m_vfq; + sample_t in = _in0 + m_vfbp[0][_chnl] * m_vfq; in = std::clamp(in, -1.0f, 1.0f); - hp = m_vfc[0] * ( m_vfhp[0][_chnl] + in - m_vflast[0][_chnl] ); + sample_t hp = m_vfc[0] * ( m_vfhp[0][_chnl] + in - m_vflast[0][_chnl] ); hp = std::clamp(hp, -1.0f, 1.0f); - bp = hp * m_vfb[0] + m_vfbp[0][_chnl] * m_vfa[0]; + sample_t bp = hp * m_vfb[0] + m_vfbp[0][_chnl] * m_vfa[0]; bp = std::clamp(bp, -1.0f, 1.0f); m_vflast[0][_chnl] = in; diff --git a/include/IoHelper.h b/include/IoHelper.h index 40c576b83..3c453fa58 100644 --- a/include/IoHelper.h +++ b/include/IoHelper.h @@ -75,13 +75,12 @@ inline FILE* F_OPEN_UTF8(std::string const& fname, const char* mode){ inline int fileToDescriptor(FILE* f, bool closeFile = true) { - int fh; if (f == nullptr) {return -1;} #ifdef LMMS_BUILD_WIN32 - fh = _dup(_fileno(f)); + int fh = _dup(_fileno(f)); #else - fh = dup(fileno(f)); + int fh = dup(fileno(f)); #endif if (closeFile) {fclose(f);} diff --git a/include/LocaleHelper.h b/include/LocaleHelper.h index 9c829fcaa..c1e1b4735 100644 --- a/include/LocaleHelper.h +++ b/include/LocaleHelper.h @@ -39,10 +39,9 @@ namespace lmms::LocaleHelper inline double toDouble(QString str, bool* ok = nullptr) { bool isOkay; - double value; QLocale c(QLocale::C); c.setNumberOptions(QLocale::RejectGroupSeparator); - value = c.toDouble(str, &isOkay); + double value = c.toDouble(str, &isOkay); if (!isOkay) { QLocale german(QLocale::German); diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index 818fb14cb..89e328972 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -354,32 +354,21 @@ void AudioFileProcessorWaveView::zoom(const bool out) const double comp_ratio = double(qMin(d_from, d_to)) / qMax(1, qMax(d_from, d_to)); - f_cnt_t new_from; - f_cnt_t new_to; + const auto boundedFrom = std::clamp(m_from + step_from, 0, start); + const auto boundedTo = std::clamp(m_to + step_to, end, frames); - if ((out && d_from < d_to) || (! out && d_to < d_from)) - { - new_from = qBound(0, m_from + step_from, start); - new_to = qBound( - end, - m_to + f_cnt_t(step_to * (new_from == m_from ? 1 : comp_ratio)), - frames - ); - } - else - { - new_to = qBound(end, m_to + step_to, frames); - new_from = qBound( - 0, - m_from + f_cnt_t(step_from * (new_to == m_to ? 1 : comp_ratio)), - start - ); - } + const auto toStep = static_cast(step_from * (boundedTo == m_to ? 1 : comp_ratio)); + const auto newFrom + = (out && d_from < d_to) || (!out && d_to < d_from) ? boundedFrom : std::clamp(m_from + toStep, 0, start); - if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) + const auto fromStep = static_cast(step_to * (boundedFrom == m_from ? 1 : comp_ratio)); + const auto newTo + = (out && d_from < d_to) || (!out && d_to < d_from) ? std::clamp(m_to + fromStep, end, frames) : boundedTo; + + if (static_cast(newTo - newFrom) / m_sample->sampleRate() > 0.05) { - setFrom(new_from); - setTo(new_to); + setFrom(newFrom); + setTo(newTo); } } diff --git a/plugins/BitInvader/BitInvader.cpp b/plugins/BitInvader/BitInvader.cpp index 4685478ff..4743e262d 100644 --- a/plugins/BitInvader/BitInvader.cpp +++ b/plugins/BitInvader/BitInvader.cpp @@ -110,34 +110,18 @@ sample_t BSynth::nextStringSample( float sample_length ) sample_realindex -= sample_length; } - sample_t sample; - - if (interpolation) { - - // find position in shape - int a = static_cast(sample_realindex); - int b; - if (a < (sample_length-1)) { - b = static_cast(sample_realindex+1); - } else { - b = 0; - } - - // Nachkommaanteil - const float frac = fraction( sample_realindex ); - - sample = linearInterpolate( sample_shape[a], sample_shape[b], frac ); - - } else { - // No interpolation - sample_index = static_cast(sample_realindex); - sample = sample_shape[sample_index]; - } - - // progress in shape + const auto currentRealIndex = sample_realindex; + const auto currentIndex = static_cast(sample_realindex); sample_realindex += sample_step; - return sample; + if (!interpolation) + { + sample_index = currentIndex; + return sample_shape[sample_index]; + } + + const auto nextIndex = currentIndex < sample_length - 1 ? currentIndex + 1 : 0; + return linearInterpolate(sample_shape[currentIndex], sample_shape[nextIndex], fraction(currentRealIndex)); } /*********************************************************************** @@ -276,16 +260,7 @@ void BitInvader::playNote( NotePlayHandle * _n, { if (!_n->m_pluginData) { - float factor; - if( !m_normalize.value() ) - { - factor = defaultNormalizationFactor; - } - else - { - factor = m_normalizeFactor; - } - + float factor = !m_normalize.value() ? defaultNormalizationFactor : m_normalizeFactor; _n->m_pluginData = new BSynth( const_cast( m_graph.samples() ), _n, diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index e58cb14af..cb52fe25e 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -1116,16 +1116,15 @@ void CarlaParamsView::clearKnobs() } // Remove spacers - QLayoutItem* item; for (int16_t i=m_inputScrollAreaLayout->count() - 1; i > 0; i--) { - item = m_inputScrollAreaLayout->takeAt(i); + auto item = m_inputScrollAreaLayout->takeAt(i); if (item->widget()) {continue;} delete item; } for (int16_t i=m_outputScrollAreaLayout->count() - 1; i > 0; i--) { - item = m_outputScrollAreaLayout->takeAt(i); + auto item = m_outputScrollAreaLayout->takeAt(i); if (item->widget()) {continue;} delete item; } diff --git a/plugins/Delay/DelayEffect.cpp b/plugins/Delay/DelayEffect.cpp index 05204f355..0050cd6fa 100644 --- a/plugins/Delay/DelayEffect.cpp +++ b/plugins/Delay/DelayEffect.cpp @@ -115,7 +115,7 @@ bool DelayEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) { m_outGain = dbfsToAmp( m_delayControls.m_outGainModel.value() ); } - int sampleLength; + for( fpp_t f = 0; f < frames; ++f ) { dryS[0] = buf[f][0]; @@ -123,8 +123,7 @@ bool DelayEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) m_delay->setFeedback( *feedbackPtr ); m_lfo->setFrequency( *lfoTimePtr ); - sampleLength = *lengthPtr * Engine::audioEngine()->processingSampleRate(); - m_currentLength = sampleLength; + m_currentLength = static_cast(*lengthPtr * Engine::audioEngine()->processingSampleRate()); m_delay->setLength( m_currentLength + ( *amplitudePtr * ( float )m_lfo->tick() ) ); m_delay->tick( buf[f] ); diff --git a/plugins/DynamicsProcessor/DynamicsProcessor.cpp b/plugins/DynamicsProcessor/DynamicsProcessor.cpp index f58dcedac..583128bfb 100644 --- a/plugins/DynamicsProcessor/DynamicsProcessor.cpp +++ b/plugins/DynamicsProcessor/DynamicsProcessor.cpp @@ -106,7 +106,6 @@ bool DynProcEffect::processAudioBuffer( sampleFrame * _buf, int i = 0; auto sm_peak = std::array{0.0f, 0.0f}; - float gain; double out_sum = 0.0; const float d = dryLevel(); @@ -196,20 +195,10 @@ bool DynProcEffect::processAudioBuffer( sampleFrame * _buf, if( sm_peak[i] > DYN_NOISE_FLOOR ) { - if ( lookup < 1 ) - { - gain = frac * samples[0]; - } - else - if ( lookup < 200 ) - { - gain = linearInterpolate( samples[ lookup - 1 ], - samples[ lookup ], frac ); - } - else - { - gain = samples[199]; - }; + float gain; + if (lookup < 1) { gain = frac * samples[0]; } + else if (lookup < 200) { gain = linearInterpolate(samples[lookup - 1], samples[lookup], frac); } + else { gain = samples[199]; } s[i] *= gain; s[i] /= sm_peak[i]; diff --git a/plugins/Eq/EqCurve.cpp b/plugins/Eq/EqCurve.cpp index bb721a7e4..0493963b1 100644 --- a/plugins/Eq/EqCurve.cpp +++ b/plugins/Eq/EqCurve.cpp @@ -208,15 +208,14 @@ float EqHandle::getPeakCurve( float x ) double Q = getResonance(); double A = pow( 10, yPixelToGain( EqHandle::y(), m_heigth, m_pixelsPerUnitHeight ) / 40 ); double alpha = s * sinh( log( 2 ) / 2 * Q * w0 / sinf( w0 ) ); - double a0, a1, a2, b0, b1, b2; // coeffs to calculate //calc coefficents - b0 = 1 + alpha * A; - b1 = -2 * c; - b2 = 1 - alpha * A; - a0 = 1 + alpha / A; - a1 = -2 * c; - a2 = 1 - alpha / A; + double b0 = 1 + alpha * A; + double b1 = -2 * c; + double b2 = 1 - alpha * A; + double a0 = 1 + alpha / A; + double a1 = -2 * c; + double a2 = 1 - alpha / A; //normalise b0 /= a0; @@ -245,15 +244,15 @@ float EqHandle::getHighShelfCurve( float x ) double s = sinf( w0 ); double A = pow( 10, yPixelToGain( EqHandle::y(), m_heigth, m_pixelsPerUnitHeight ) * 0.025 ); double beta = sqrt( A ) / m_resonance; - double a0, a1, a2, b0, b1, b2; // coeffs to calculate //calc coefficents - b0 = A * ( ( A + 1 ) + ( A - 1 ) * c + beta * s); - b1 = -2 * A * ( ( A - 1 ) + ( A + 1 ) * c ); - b2 = A * ( ( A + 1 ) + ( A - 1 ) * c - beta * s); - a0 = ( A + 1 ) - ( A - 1 ) * c + beta * s; - a1 = 2 * ( ( A - 1 ) - ( A + 1 ) * c ); - a2 = ( A + 1 ) - ( A - 1 ) * c - beta * s; + double b0 = A * ((A + 1) + (A - 1) * c + beta * s); + double b1 = -2 * A * ((A - 1) + (A + 1) * c); + double b2 = A * ((A + 1) + (A - 1) * c - beta * s); + double a0 = (A + 1) - (A - 1) * c + beta * s; + double a1 = 2 * ((A - 1) - (A + 1) * c); + double a2 = (A + 1) - (A - 1) * c - beta * s; + //normalise b0 /= a0; b1 /= a0; @@ -281,15 +280,14 @@ float EqHandle::getLowShelfCurve( float x ) double s = sinf( w0 ); double A = pow( 10, yPixelToGain( EqHandle::y(), m_heigth, m_pixelsPerUnitHeight ) / 40 ); double beta = sqrt( A ) / m_resonance; - double a0, a1, a2, b0, b1, b2; // coeffs to calculate //calc coefficents - b0 = A * ( ( A + 1 ) - ( A - 1 ) * c + beta * s ); - b1 = 2 * A * ( ( A - 1 ) - ( A + 1 ) * c ) ; - b2 = A * ( ( A + 1 ) - ( A - 1 ) * c - beta * s); - a0 = ( A + 1 ) + ( A - 1 ) * c + beta * s; - a1 = -2 * ( ( A - 1 ) + ( A + 1 ) * c ); - a2 = ( A + 1 ) + ( A - 1) * c - beta * s; + double b0 = A * ((A + 1) - (A - 1) * c + beta * s); + double b1 = 2 * A * ((A - 1) - (A + 1) * c); + double b2 = A * ((A + 1) - (A - 1) * c - beta * s); + double a0 = (A + 1) + (A - 1) * c + beta * s; + double a1 = -2 * ((A - 1) + (A + 1) * c); + double a2 = (A + 1) + (A - 1) * c - beta * s; //normalise b0 /= a0; @@ -318,14 +316,14 @@ float EqHandle::getLowCutCurve( float x ) double s = sinf( w0 ); double resonance = getResonance(); double alpha = s / (2 * resonance); - double a0, a1, a2, b0, b1, b2; // coeffs to calculate - b0 = ( 1 + c ) * 0.5; - b1 = ( -( 1 + c ) ); - b2 = ( 1 + c ) * 0.5; - a0 = 1 + alpha; - a1 = ( -2 * c ); - a2 = 1 - alpha; + double b0 = (1 + c) * 0.5; + double b1 = (-(1 + c)); + double b2 = (1 + c) * 0.5; + double a0 = 1 + alpha; + double a1 = (-2 * c); + double a2 = 1 - alpha; + //normalise b0 /= a0; b1 /= a0; @@ -361,14 +359,14 @@ float EqHandle::getHighCutCurve( float x ) double s = sinf( w0 ); double resonance = getResonance(); double alpha = s / (2 * resonance); - double a0, a1, a2, b0, b1, b2; // coeffs to calculate - b0 = ( 1 - c ) * 0.5; - b1 = 1 - c; - b2 = ( 1 - c ) * 0.5; - a0 = 1 + alpha; - a1 = -2 * c; - a2 = 1 - alpha; + double b0 = (1 - c) * 0.5; + double b1 = 1 - c; + double b2 = (1 - c) * 0.5; + double a0 = 1 + alpha; + double a1 = -2 * c; + double a2 = 1 - alpha; + //normalise b0 /= a0; b1 /= a0; @@ -569,16 +567,7 @@ void EqHandle::mouseReleaseEvent( QGraphicsSceneMouseEvent *event ) void EqHandle::wheelEvent( QGraphicsSceneWheelEvent *wevent ) { - float highestBandwich; - if( m_type != EqHandleType::Para ) - { - highestBandwich = 10; - } - else - { - highestBandwich = 4; - } - + float highestBandwich = m_type != EqHandleType::Para ? 10 : 4; int numDegrees = wevent->delta() / 120; float numSteps = 0; if( wevent->modifiers() == Qt::ControlModifier ) diff --git a/plugins/Eq/EqFilter.h b/plugins/Eq/EqFilter.h index c64f6d5c3..df2b50493 100644 --- a/plugins/Eq/EqFilter.h +++ b/plugins/Eq/EqFilter.h @@ -190,15 +190,13 @@ public : float s = sinf( w0 ); float alpha = s / ( 2 * m_res ); - float a0, a1, a2, b0, b1, b2; // coeffs to calculate - //calc coefficents - b0 = ( 1 + c ) * 0.5; - b1 = ( -( 1 + c ) ); - b2 = ( 1 + c ) * 0.5; - a0 = 1 + alpha; - a1 = ( -2 * c ); - a2 = 1 - alpha; + float b0 = (1 + c) * 0.5; + float b1 = (-(1 + c)); + float b2 = (1 + c) * 0.5; + float a0 = 1 + alpha; + float a1 = (-2 * c); + float a2 = 1 - alpha; //normalise b0 /= a0; @@ -235,15 +233,13 @@ public : float s = sinf( w0 ); float alpha = s / ( 2 * m_res ); - float a0, a1, a2, b0, b1, b2; // coeffs to calculate - //calc coefficents - b0 = ( 1 - c ) * 0.5; - b1 = 1 - c; - b2 = ( 1 - c ) * 0.5; - a0 = 1 + alpha; - a1 = -2 * c; - a2 = 1 - alpha; + float b0 = (1 - c) * 0.5; + float b1 = 1 - c; + float b2 = (1 - c) * 0.5; + float a0 = 1 + alpha; + float a1 = -2 * c; + float a2 = 1 - alpha; //normalise b0 /= a0; @@ -279,15 +275,13 @@ public: float A = pow( 10, m_gain * 0.025); float alpha = s * sinh( log( 2 ) / 2 * m_bw * w0 / sinf(w0) ); - float a0, a1, a2, b0, b1, b2; // coeffs to calculate - //calc coefficents - b0 = 1 + alpha*A; - b1 = -2*c; - b2 = 1 - alpha*A; - a0 = 1 + alpha/A; - a1 = -2*c; - a2 = 1 - alpha/A; + float b0 = 1 + alpha * A; + float b1 = -2 * c; + float b2 = 1 - alpha * A; + float a0 = 1 + alpha / A; + float a1 = -2 * c; + float a2 = 1 - alpha / A; //normalise b0 /= a0; @@ -345,15 +339,13 @@ public : // float alpha = s / ( 2 * m_res ); float beta = sqrt( A ) / m_res; - float a0, a1, a2, b0, b1, b2; // coeffs to calculate - //calc coefficents - b0 = A * ( ( A+1 ) - ( A-1 ) * c + beta * s ); - b1 = 2 * A * ( ( A - 1 ) - ( A + 1 ) * c) ; - b2 = A * ( ( A + 1 ) - ( A - 1 ) * c - beta * s); - a0 = ( A + 1 ) + ( A - 1 ) * c + beta * s; - a1 = -2 * ( ( A - 1 ) + ( A + 1 ) * c ); - a2 = ( A + 1 ) + ( A - 1) * c - beta * s; + float b0 = A * ((A + 1) - (A - 1) * c + beta * s); + float b1 = 2 * A * ((A - 1) - (A + 1) * c); + float b2 = A * ((A + 1) - (A - 1) * c - beta * s); + float a0 = (A + 1) + (A - 1) * c + beta * s; + float a1 = -2 * ((A - 1) + (A + 1) * c); + float a2 = (A + 1) + (A - 1) * c - beta * s; //normalise b0 /= a0; @@ -383,15 +375,14 @@ public : float A = pow( 10, m_gain * 0.025 ); float beta = sqrt( A ) / m_res; - float a0, a1, a2, b0, b1, b2; // coeffs to calculate - //calc coefficents - b0 = A *( ( A +1 ) + ( A - 1 ) * c + beta * s); - b1 = -2 * A * ( ( A - 1 ) + ( A + 1 ) * c ); - b2 = A * ( ( A + 1 ) + ( A - 1 ) * c - beta * s); - a0 = ( A + 1 ) - ( A - 1 ) * c + beta * s; - a1 = 2 * ( ( A - 1 ) - ( A + 1 ) * c ); - a2 = ( A + 1) - ( A - 1 ) * c - beta * s; + float b0 = A * ((A + 1) + (A - 1) * c + beta * s); + float b1 = -2 * A * ((A - 1) + (A + 1) * c); + float b2 = A * ((A + 1) + (A - 1) * c - beta * s); + float a0 = (A + 1) - (A - 1) * c + beta * s; + float a1 = 2 * ((A - 1) - (A + 1) * c); + float a2 = (A + 1) - (A - 1) * c - beta * s; + //normalise b0 /= a0; b1 /= a0; diff --git a/plugins/Eq/EqSpectrumView.cpp b/plugins/Eq/EqSpectrumView.cpp index 540450883..a90682b36 100644 --- a/plugins/Eq/EqSpectrumView.cpp +++ b/plugins/Eq/EqSpectrumView.cpp @@ -228,13 +228,12 @@ void EqSpectrumView::paintEvent(QPaintEvent *event) //Now we calculate the path m_path = QPainterPath(); float *bands = m_analyser->m_bands; - float peak; m_path.moveTo( 0, height() ); m_peakSum = 0; const float fallOff = 1.07; for( int x = 0; x < MAX_BANDS; ++x, ++bands ) { - peak = *bands != 0. ? (fh * 2.0 / 3.0 * (20. * log10(*bands / energy) - LOWER_Y) / (-LOWER_Y)) : 0.; + float peak = *bands != 0. ? (fh * 2.0 / 3.0 * (20. * log10(*bands / energy) - LOWER_Y) / (-LOWER_Y)) : 0.; if( peak < 0 ) { diff --git a/plugins/Flanger/FlangerEffect.cpp b/plugins/Flanger/FlangerEffect.cpp index 60b5df67b..ddba0cb77 100644 --- a/plugins/Flanger/FlangerEffect.cpp +++ b/plugins/Flanger/FlangerEffect.cpp @@ -108,10 +108,11 @@ bool FlangerEffect::processAudioBuffer( sampleFrame *buf, const fpp_t frames ) m_lDelay->setFeedback( m_flangerControls.m_feedbackModel.value() ); m_rDelay->setFeedback( m_flangerControls.m_feedbackModel.value() ); auto dryS = std::array{}; - float leftLfo; - float rightLfo; for( fpp_t f = 0; f < frames; ++f ) { + float leftLfo; + float rightLfo; + buf[f][0] += m_noise->tick() * noise; buf[f][1] += m_noise->tick() * noise; dryS[0] = buf[f][0]; diff --git a/plugins/FreeBoy/game-music-emu b/plugins/FreeBoy/game-music-emu index 6b676192d..21a064ea6 160000 --- a/plugins/FreeBoy/game-music-emu +++ b/plugins/FreeBoy/game-music-emu @@ -1 +1 @@ -Subproject commit 6b676192d98302e698ac78fe3c00833eae6a74e5 +Subproject commit 21a064ea66a5cdf71910e207c4756095c266814f diff --git a/plugins/LadspaEffect/LadspaControlDialog.cpp b/plugins/LadspaEffect/LadspaControlDialog.cpp index 2a5437fb1..5189b0cde 100644 --- a/plugins/LadspaEffect/LadspaControlDialog.cpp +++ b/plugins/LadspaEffect/LadspaControlDialog.cpp @@ -88,17 +88,9 @@ void LadspaControlDialog::updateEffectView( LadspaControls * _ctl ) int col = 0; BufferDataType last_port = BufferDataType::None; - QGroupBox * grouper; - if( _ctl->m_processors > 1 ) - { - grouper = new QGroupBox( tr( "Channel " ) + - QString::number( proc + 1 ), - this ); - } - else - { - grouper = new QGroupBox( this ); - } + auto grouper = _ctl->m_processors > 1 + ? new QGroupBox(tr("Channel ") + QString::number(proc + 1), this) + : new QGroupBox(this); auto gl = new QGridLayout(grouper); grouper->setLayout( gl ); diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index 44583cbc5..e0b31360f 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -166,12 +166,10 @@ void Lb302FilterIIR2::recalc() void Lb302FilterIIR2::envRecalc() { - float k, w; - Lb302Filter::envRecalc(); - w = vcf_e0 + vcf_c0; // e0 is adjusted for Hz and doesn't need ENVINC - k = exp(-w/vcf_rescoeff); // Does this mean c0 is inheritantly? + float w = vcf_e0 + vcf_c0; // e0 is adjusted for Hz and doesn't need ENVINC + float k = exp(-w/vcf_rescoeff); // Does this mean c0 is inheritantly? vcf_a = 2.0*cos(2.0*w) * k; vcf_b = -k*k; @@ -219,18 +217,15 @@ void Lb302Filter3Pole::recalc() // TODO: Try using k instead of vcf_reso void Lb302Filter3Pole::envRecalc() { - float w,k; - float kfco; - Lb302Filter::envRecalc(); // e0 is adjusted for Hz and doesn't need ENVINC - w = vcf_e0 + vcf_c0; - k = (fs->cutoff > 0.975)?0.975:fs->cutoff; + float w = vcf_e0 + vcf_c0; + float k = (fs->cutoff > 0.975)?0.975:fs->cutoff; // sampleRateCutoff should not be changed to anything dynamic that is outside the // scope of LB302 (like e.g. the audio engine's sample rate) as this changes the filter's cutoff // behavior without any modification to its controls. - kfco = 50.f + (k)*((2300.f-1600.f*(fs->envmod))+(w) * + float kfco = 50.f + (k)*((2300.f-1600.f*(fs->envmod))+(w) * (700.f+1500.f*(k)+(1500.f+(k)*(sampleRateCutoff/2.f-6000.f)) * (fs->envmod)) ); //+iacc*(.3+.7*kfco*kenvmod)*kaccent*kaccurve*2000 @@ -461,8 +456,6 @@ inline float GET_INC(float freq) { int Lb302Synth::process(sampleFrame *outbuf, const int size) { const float sampleRatio = 44100.f / Engine::audioEngine()->processingSampleRate(); - float w; - float samp; // Hold on to the current VCF, and use it throughout this period Lb302Filter *filter = vcf.loadAcquire(); @@ -566,7 +559,7 @@ int Lb302Synth::process(sampleFrame *outbuf, const int size) vco_k = -0.5 ; } else if (vco_k>0.5) { - w = 2.0*(vco_k-0.5)-1.0; + float w = 2.0 * (vco_k - 0.5) - 1.0; vco_k = 0.5 - sqrtf(1.0-(w*w)); } vco_k *= 2.0; // MOOG wave gets filtered away @@ -610,7 +603,7 @@ int Lb302Synth::process(sampleFrame *outbuf, const int size) #ifdef LB_FILTERED //samp = vcf->process(vco_k)*2.0*vca_a; //samp = vcf->process(vco_k)*2.0; - samp = filter->process(vco_k) * vca_a; + float samp = filter->process(vco_k) * vca_a; //printf("%f %d\n", vco_c, sample_cnt); diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 2600a40f2..b3b08a0e1 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -76,21 +76,18 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, f.open(QIODevice::WriteOnly); QDataStream midiout(&f); - InstrumentTrack* instTrack; - PatternTrack* patternTrack; QDomElement element; int nTracks = 0; auto buffer = std::array{}; - uint32_t size; for (const Track* track : tracks) if (track->type() == Track::Type::Instrument) nTracks++; for (const Track* track : patternStoreTracks) if (track->type() == Track::Type::Instrument) nTracks++; // midi header MidiFile::MIDIHeader header(nTracks); - size = header.writeToBuffer(buffer.data()); + uint32_t size = header.writeToBuffer(buffer.data()); midiout.writeRawData((char *)buffer.data(), size); std::vector>> plists; @@ -108,7 +105,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, //mtrack.addProgramChange(0, 0); mtrack.addTempo(tempo, 0); - instTrack = dynamic_cast(track); + auto instTrack = dynamic_cast(track); element = instTrack->saveState(dataFile, dataFile.content()); int base_pitch = 0; @@ -146,7 +143,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, if (track->type() == Track::Type::Pattern) { - patternTrack = dynamic_cast(track); + auto patternTrack = dynamic_cast(track); element = patternTrack->saveState(dataFile, dataFile.content()); std::vector> plist; @@ -184,7 +181,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, //mtrack.addProgramChange(0, 0); mtrack.addTempo(tempo, 0); - instTrack = dynamic_cast(track); + auto instTrack = dynamic_cast(track); element = instTrack->saveState(dataFile, dataFile.content()); int base_pitch = 0; diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index 79c8dcce2..26f203cd6 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -25,6 +25,7 @@ #include #include #include +#include using std::string; using std::vector; @@ -47,8 +48,8 @@ int writeVarLength(uint32_t val, uint8_t *buffer) byte in question is the last in the stream */ int size = 0; - uint8_t result, little_endian[4]; - result = val & 0x7F; + uint8_t little_endian[4]; + uint8_t result = val & 0x7F; little_endian[size++] = result; val = val >> 7; while (val > 0) @@ -129,31 +130,37 @@ struct Event inline int writeToBuffer(uint8_t *buffer) const { - uint8_t code, fourbytes[4]; - int size=0; - switch (type) + int size = 0; + switch (type) { case NOTE_ON: - code = 0x9 << 4 | channel; + { + uint8_t code = 0x9 << 4 | channel; size += writeVarLength(time, buffer+size); buffer[size++] = code; buffer[size++] = pitch; buffer[size++] = volume; break; + } case NOTE_OFF: - code = 0x8 << 4 | channel; + { + uint8_t code = 0x8 << 4 | channel; size += writeVarLength(time, buffer+size); buffer[size++] = code; buffer[size++] = pitch; buffer[size++] = volume; break; + } case TEMPO: - code = 0xFF; + { + uint8_t code = 0xFF; size += writeVarLength(time, buffer+size); buffer[size++] = code; buffer[size++] = 0x51; buffer[size++] = 0x03; - writeBigEndian4(int(60000000.0 / tempo), fourbytes); + + std::array fourbytes; + writeBigEndian4(int(60000000.0 / tempo), fourbytes.data()); //printf("tempo of %x translates to ", tempo); /* @@ -164,23 +171,27 @@ struct Event buffer[size++] = fourbytes[2]; buffer[size++] = fourbytes[3]; break; + } case PROG_CHANGE: - code = 0xC << 4 | channel; + { + uint8_t code = 0xC << 4 | channel; size += writeVarLength(time, buffer+size); buffer[size++] = code; buffer[size++] = programNumber; break; + } case TRACK_NAME: + { size += writeVarLength(time, buffer+size); buffer[size++] = 0xFF; buffer[size++] = 0x03; size += writeVarLength(trackName.size(), buffer+size); trackName.copy((char *)(&buffer[size]), trackName.size()); size += trackName.size(); -// buffer[size++] = '\0'; -// buffer[size++] = '\0'; - break; + // buffer[size++] = '\0'; + // buffer[size++] = '\0'; + } } return size; } // writeEventsToBuffer @@ -275,7 +286,7 @@ class MIDITrack vector _events = events; std::sort(_events.begin(), _events.end()); vector::const_iterator it; - uint32_t time_last = 0, tmp; + uint32_t time_last = 0; for (it = _events.begin(); it!=_events.end(); ++it) { Event e = *it; @@ -283,7 +294,7 @@ class MIDITrack printf("error: e.time=%d time_last=%d\n", e.time, time_last); assert(false); } - tmp = e.time; + uint32_t tmp = e.time; e.time -= time_last; time_last = tmp; start += e.writeToBuffer(buffer+start); diff --git a/plugins/MidiImport/MidiImport.h b/plugins/MidiImport/MidiImport.h index 817d06be8..0870511b5 100644 --- a/plugins/MidiImport/MidiImport.h +++ b/plugins/MidiImport/MidiImport.h @@ -61,10 +61,10 @@ private: inline int readInt( int _bytes ) { - int c, value = 0; + int value = 0; do { - c = readByte(); + int c = readByte(); if( c == -1 ) { return( -1 ); diff --git a/plugins/OpulenZ/OpulenZ.cpp b/plugins/OpulenZ/OpulenZ.cpp index d90d5f343..f0571ba5c 100644 --- a/plugins/OpulenZ/OpulenZ.cpp +++ b/plugins/OpulenZ/OpulenZ.cpp @@ -244,14 +244,12 @@ void OpulenzInstrument::reloadEmulator() { // This shall only be called from code protected by the holy Mutex! void OpulenzInstrument::setVoiceVelocity(int voice, int vel) { - int vel_adjusted; + int vel_adjusted = !fm_mdl.value() + ? 63 - (op1_lvl_mdl.value() * vel / 127.0) + : 63 - op1_lvl_mdl.value(); + // Velocity calculation, some kind of approximation // Only calculate for operator 1 if in adding mode, don't want to change timbre - if( fm_mdl.value() == false ) { - vel_adjusted = 63 - ( op1_lvl_mdl.value() * vel/127.0) ; - } else { - vel_adjusted = 63 - op1_lvl_mdl.value(); - } theEmulator->write(0x40+adlib_opadd[voice], ( (int)op1_scale_mdl.value() & 0x03 << 6) + ( vel_adjusted & 0x3f ) ); @@ -297,66 +295,60 @@ int OpulenzInstrument::pushVoice(int v) { bool OpulenzInstrument::handleMidiEvent( const MidiEvent& event, const TimePos& time, f_cnt_t offset ) { emulatorMutex.lock(); - int key, vel, voice, tmp_pb; - switch(event.type()) { - case MidiNoteOn: - key = event.key(); - vel = event.velocity(); - - voice = popVoice(); - if( voice != OPL2_NO_VOICE ) { + int key = event.key(); + int vel = event.velocity(); + switch (event.type()) + { + case MidiNoteOn: + if (int voice = popVoice(); voice != OPL2_NO_VOICE) + { // Turn voice on, NB! the frequencies are straight by voice number, // not by the adlib_opadd table! - theEmulator->write(0xA0+voice, fnums[key] & 0xff); - theEmulator->write(0xB0+voice, 32 + ((fnums[key] & 0x1f00) >> 8) ); + theEmulator->write(0xA0 + voice, fnums[key] & 0xff); + theEmulator->write(0xB0 + voice, 32 + ((fnums[key] & 0x1f00) >> 8)); setVoiceVelocity(voice, vel); voiceNote[voice] = key; velocities[key] = vel; } - break; - case MidiNoteOff: - key = event.key(); - for(voice=0; voicewrite(0xA0+voice, fnums[key] & 0xff); - theEmulator->write(0xB0+voice, (fnums[key] & 0x1f00) >> 8 ); - voiceNote[voice] |= OPL2_VOICE_FREE; + break; + case MidiNoteOff: + for (int voice = 0; voice < OPL2_VOICES; ++voice) + { + if (voiceNote[voice] == key) + { + theEmulator->write(0xA0 + voice, fnums[key] & 0xff); + theEmulator->write(0xB0 + voice, (fnums[key] & 0x1f00) >> 8); + voiceNote[voice] |= OPL2_VOICE_FREE; pushVoice(voice); - } - } - velocities[key] = 0; - break; - case MidiKeyPressure: - key = event.key(); - vel = event.velocity(); - if( velocities[key] != 0) { - velocities[key] = vel; - } - for(voice=0; voicewrite(0xA0+v, fnums[vn] & 0xff); - theEmulator->write(0xB0+v, (playing ? 32 : 0) + ((fnums[vn] & 0x1f00) >> 8) ); - } - break; + theEmulator->write(0xA0 + v, fnums[vn] & 0xff); + theEmulator->write(0xB0 + v, (playing ? 32 : 0) + ((fnums[vn] & 0x1f00) >> 8)); + } + break; case MidiControlChange: switch (event.controllerNumber()) { case MidiControllerRegisteredParameterNumberLSB: @@ -382,7 +374,7 @@ bool OpulenzInstrument::handleMidiEvent( const MidiEvent& event, const TimePos& printf("Midi event type %d\n",event.type()); #endif break; - } + } emulatorMutex.unlock(); return true; } @@ -504,9 +496,8 @@ void OpulenzInstrument::loadPatch(const unsigned char inst[14]) { } void OpulenzInstrument::tuneEqual(int center, float Hz) { - float tmp; for(int n=0; n<128; ++n) { - tmp = Hz*pow( 2.0, ( n - center ) * ( 1.0 / 12.0 ) + pitchbend * ( 1.0 / 1200.0 ) ); + float tmp = Hz * pow(2.0, (n - center) * (1.0 / 12.0) + pitchbend * (1.0 / 1200.0)); fnums[n] = Hz2fnum( tmp ); } } diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index 25664ae0d..a50363777 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -298,17 +298,16 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( SKIP_BYTES( 2 + 2 + 36 ); f_cnt_t frames; - sample_t * wave_samples; + std::unique_ptr wave_samples; if( modes & MODES_16BIT ) { frames = data_length >> 1; - wave_samples = new sample_t[frames]; + wave_samples = std::make_unique(frames); for( f_cnt_t frame = 0; frame < frames; ++frame ) { short sample; if ( fread( &sample, 2, 1, fd ) != 1 ) { - delete[] wave_samples; fclose( fd ); return( LoadError::IO ); } @@ -326,13 +325,12 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( else { frames = data_length; - wave_samples = new sample_t[frames]; + wave_samples = std::make_unique(frames); for( f_cnt_t frame = 0; frame < frames; ++frame ) { char sample; if ( fread( &sample, 1, 1, fd ) != 1 ) { - delete[] wave_samples; fclose( fd ); return( LoadError::IO ); } @@ -366,7 +364,6 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( m_patchSamples.push_back(psample); - delete[] wave_samples; delete[] data; } fclose( fd ); diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index 7f9edf13f..d85939eb8 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -241,24 +241,21 @@ f_cnt_t SidInstrument::desiredReleaseFrames() const static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, short *ptr, int samples) { - int tdelta2; - int result; int total = 0; - int c; // customly added int residdelay = 0; int badline = rand() % NUMSIDREGS; - for (c = 0; c < NUMSIDREGS; c++) + for (int c = 0; c < NUMSIDREGS; c++) { unsigned char o = sidorder[c]; // Extra delay for loading the waveform (and mt_chngate,x) if ((o == 4) || (o == 11) || (o == 18)) { - tdelta2 = SIDWAVEDELAY; - result = sid->clock(tdelta2, ptr, samples); + int tdelta2 = SIDWAVEDELAY; + int result = sid->clock(tdelta2, ptr, samples); total += result; ptr += result; samples -= result; @@ -268,8 +265,8 @@ static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, sh // Possible random badline delay once per writing if ((badline == c) && (residdelay)) { - tdelta2 = residdelay; - result = sid->clock(tdelta2, ptr, samples); + int tdelta2 = residdelay; + int result = sid->clock(tdelta2, ptr, samples); total += result; ptr += result; samples -= result; @@ -278,14 +275,14 @@ static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, sh sid->write(o, sidreg[o]); - tdelta2 = SIDWRITEDELAY; - result = sid->clock(tdelta2, ptr, samples); + int tdelta2 = SIDWRITEDELAY; + int result = sid->clock(tdelta2, ptr, samples); total += result; ptr += result; samples -= result; tdelta -= SIDWRITEDELAY; } - result = sid->clock(tdelta, ptr, samples); + int result = sid->clock(tdelta, ptr, samples); total += result; return total; diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 1c3397046..732ebfb44 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -198,7 +198,6 @@ void SlicerT::findSlices() int lastPoint = -minDist - 1; // to always store 0 first float spectralFlux = 0; float prevFlux = 1E-10; // small value, no divison by zero - float real, imag, magnitude, diff; for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) { @@ -209,12 +208,12 @@ void SlicerT::findSlices() // calculate spectral flux in regard to last window for (int j = 0; j < windowSize / 2; j++) // only use niquistic frequencies { - real = fftOut[j][0]; - imag = fftOut[j][1]; - magnitude = std::sqrt(real * real + imag * imag); + float real = fftOut[j][0]; + float imag = fftOut[j][1]; + float magnitude = std::sqrt(real * real + imag * imag); // using L2-norm (euclidean distance) - diff = std::sqrt(std::pow(magnitude - prevMags[j], 2)); + float diff = std::sqrt(std::pow(magnitude - prevMags[j], 2)); spectralFlux += diff; prevMags[j] = magnitude; diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 38e8e9e92..c55acbdf6 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -209,7 +209,6 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) memset(pixel, 0, waterfallWidth() * sizeof (QRgb)); // add newest result on top - int target; // pixel being constructed float accL = 0; // accumulators for merging multiple bins float accR = 0; for (unsigned int i = 0; i < binCount(); i++) @@ -233,7 +232,8 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) if (band_end - band_start > 1.0) { // band spans multiple pixels: draw all pixels it covers - for (target = std::max((int)band_start, 0); target < band_end && target < waterfallWidth(); target++) + for (int target = std::max(static_cast(band_start), 0); + target < band_end && target < waterfallWidth(); target++) { pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); } @@ -245,7 +245,7 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) else { // sub-pixel drawing; add contribution of current band - target = (int)band_start; + int target = static_cast(band_start); if ((int)band_start == (int)band_end) { // band ends within current target pixel, accumulate @@ -270,7 +270,8 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) else { // Linear: always draws one or more pixels per band - for (target = std::max((int)band_start, 0); target < band_end && target < waterfallWidth(); target++) + for (int target = std::max(static_cast(band_start), 0); + target < band_end && target < waterfallWidth(); target++) { pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); } @@ -361,30 +362,20 @@ void SaProcessor::setWaterfallActive(bool active) // Reallocate data buffers according to newly set block size. void SaProcessor::reallocateBuffers() { - unsigned int new_size_index = m_controls->m_blockSizeModel.value(); - unsigned int new_in_size, new_fft_size; - unsigned int new_bins; + m_zeroPadFactor = m_controls->m_zeroPaddingModel.value(); // get new block sizes and bin count based on selected index - if (new_size_index < FFT_BLOCK_SIZES.size()) - { - new_in_size = FFT_BLOCK_SIZES[new_size_index]; - } - else - { - new_in_size = FFT_BLOCK_SIZES.back(); - } - m_zeroPadFactor = m_controls->m_zeroPaddingModel.value(); - if (new_size_index + m_zeroPadFactor < FFT_BLOCK_SIZES.size()) - { - new_fft_size = FFT_BLOCK_SIZES[new_size_index + m_zeroPadFactor]; - } - else - { - new_fft_size = FFT_BLOCK_SIZES.back(); - } + const unsigned int new_size_index = m_controls->m_blockSizeModel.value(); - new_bins = new_fft_size / 2 +1; + const unsigned int new_in_size = new_size_index < FFT_BLOCK_SIZES.size() + ? FFT_BLOCK_SIZES[new_size_index] + : FFT_BLOCK_SIZES.back(); + + const unsigned int new_fft_size = (new_size_index + m_zeroPadFactor < FFT_BLOCK_SIZES.size()) + ? FFT_BLOCK_SIZES[new_size_index + m_zeroPadFactor] + : FFT_BLOCK_SIZES.back(); + + const unsigned int new_bins = new_fft_size / 2 + 1; // Use m_reallocating to tell analyze() to avoid asking for the lock. This // is needed because under heavy load the FFT thread requests data lock so diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 0d9c2af87..7f73ed7cc 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -647,14 +647,13 @@ float SaSpectrumView::ampToYPixel(float amplitude, unsigned int height) std::vector> SaSpectrumView::makeLogFreqTics(int low, int high) { std::vector> result; - int i, j; auto a = std::array{10, 20, 50}; // sparse series multipliers auto b = std::array{14, 30, 70}; // additional (denser) series // generate main steps (powers of 10); use the series to specify smaller steps - for (i = 1; i <= high; i *= 10) + for (int i = 1; i <= high; i *= 10) { - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) { // insert a label from sparse series if it falls within bounds if (i * a[j] >= low && i * a[j] <= high) @@ -691,7 +690,7 @@ std::vector> SaSpectrumView::makeLogFreqTics(int low std::vector> SaSpectrumView::makeLinearFreqTics(int low, int high) { std::vector> result; - int i, increment; + int increment; // select a suitable increment based on zoom level if (high - low < 500) {increment = 50;} @@ -700,7 +699,7 @@ std::vector> SaSpectrumView::makeLinearFreqTics(int else {increment = 2000;} // generate steps based on increment, starting at 0 - for (i = 0; i <= high; i += increment) + for (int i = 0; i <= high; i += increment) { if (i >= low) { @@ -724,7 +723,6 @@ std::vector> SaSpectrumView::makeLinearFreqTics(int std::vector> SaSpectrumView::makeLogAmpTics(int low, int high) { std::vector> result; - float i; double increment; // Base zoom level on selected range and how close is the current height @@ -744,7 +742,7 @@ std::vector> SaSpectrumView::makeLogAmpTics(int lo // Generate n dB increments, start checking at -90 dB. Limits are tweaked // just a little bit to make sure float comparisons do not miss edges. - for (i = 0.000000001; 10 * log10(i) <= (high + 0.001); i *= increment) + for (float i = 0.000000001; 10 * log10(i) <= (high + 0.001); i *= increment) { if (10 * log10(i) >= (low - 0.001)) { @@ -764,8 +762,6 @@ std::vector> SaSpectrumView::makeLogAmpTics(int lo std::vector> SaSpectrumView::makeLinearAmpTics(int low, int high) { std::vector> result; - double i, nearest; - // make about 5 labels when window is small, 10 if it is big float split = (float)height() / sizeHint().height() >= 1.5 ? 10.0 : 5.0; @@ -777,28 +773,28 @@ std::vector> SaSpectrumView::makeLinearAmpTics(int // multiples, just generate a few evenly spaced increments across the range, // paying attention only to the decimal places to keep labels short. // Limits are shifted a bit so that float comparisons do not miss edges. - for (i = 0; i <= (lin_high + 0.0001); i += (lin_high - lin_low) / split) + for (double i = 0; i <= (lin_high + 0.0001); i += (lin_high - lin_low) / split) { if (i >= (lin_low - 0.0001)) { if (i >= 9.99 && i < 99.9) { - nearest = std::round(i); + double nearest = std::round(i); result.emplace_back(nearest, std::to_string(nearest).substr(0, 2)); } else if (i >= 0.099) { // also covers numbers above 100 - nearest = std::round(i * 10) / 10; + double nearest = std::round(i * 10) / 10; result.emplace_back(nearest, std::to_string(nearest).substr(0, 3)); } else if (i >= 0.0099) { - nearest = std::round(i * 1000) / 1000; + double nearest = std::round(i * 1000) / 1000; result.emplace_back(nearest, std::to_string(nearest).substr(0, 4)); } else if (i >= 0.00099) { - nearest = std::round(i * 10000) / 10000; + double nearest = std::round(i * 10000) / 10000; result.emplace_back(nearest, std::to_string(nearest).substr(1, 4)); } else if (i > -0.01 && i < 0.01) diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 598bad725..024c3aea4 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -213,7 +213,6 @@ float SaWaterfallView::yPixelToTime(float position, int height) std::vector> SaWaterfallView::makeTimeTics() { std::vector> result; - float i; // get time value of the last line float limit = yPixelToTime(m_displayBottom, m_displayHeight); @@ -223,7 +222,7 @@ std::vector> SaWaterfallView::makeTimeTics() if (increment < 0.1) {increment = 0.1;} // NOTE: labels positions are rounded to match the (rounded) label value - for (i = 0; i <= limit; i += increment) + for (float i = 0; i <= limit; i += increment) { if (i > 99) { diff --git a/plugins/StereoEnhancer/StereoEnhancer.cpp b/plugins/StereoEnhancer/StereoEnhancer.cpp index a7937a2ec..784003056 100644 --- a/plugins/StereoEnhancer/StereoEnhancer.cpp +++ b/plugins/StereoEnhancer/StereoEnhancer.cpp @@ -90,10 +90,6 @@ bool StereoEnhancerEffect::processAudioBuffer( sampleFrame * _buf, // audio with this effect double out_sum = 0.0; - float width; - int frameIndex = 0; - - if( !isEnabled() || !isRunning() ) { return( false ); @@ -110,10 +106,10 @@ bool StereoEnhancerEffect::processAudioBuffer( sampleFrame * _buf, m_delayBuffer[m_currFrame][1] = _buf[f][1]; // Get the width knob value from the Stereo Enhancer effect - width = m_seFX.wideCoeff(); + float width = m_seFX.wideCoeff(); // Calculate the correct sample frame for processing - frameIndex = m_currFrame - width; + int frameIndex = m_currFrame - width; if( frameIndex < 0 ) { @@ -149,8 +145,7 @@ bool StereoEnhancerEffect::processAudioBuffer( sampleFrame * _buf, void StereoEnhancerEffect::clearMyBuffer() { - int i; - for (i = 0; i < DEFAULT_BUFFER_SIZE; i++) + for (int i = 0; i < DEFAULT_BUFFER_SIZE; i++) { m_delayBuffer[i][0] = 0.0f; m_delayBuffer[i][1] = 0.0f; diff --git a/plugins/Vectorscope/VectorView.cpp b/plugins/Vectorscope/VectorView.cpp index f856f6429..c4776a688 100644 --- a/plugins/Vectorscope/VectorView.cpp +++ b/plugins/Vectorscope/VectorView.cpp @@ -138,8 +138,6 @@ void VectorView::paintEvent(QPaintEvent *event) std::size_t frameCount = inBuffer.size(); // Draw new points on top - float left, right; - int x, y; const bool logScale = m_controls->m_logarithmicModel.value(); const unsigned short activeSize = hq ? m_displaySize : m_displaySize / 2; @@ -164,6 +162,8 @@ void VectorView::paintEvent(QPaintEvent *event) // The longer the line is, the dimmer, simulating real electron trace on luminescent screen. for (std::size_t frame = 0; frame < frameCount; frame++) { + float left = 0.0f; + float right = 0.0f; float inLeft = inBuffer[frame][0] * m_zoom; float inRight = inBuffer[frame][1] * m_zoom; // Scale left and right channel from (-1.0, 1.0) to display range @@ -185,8 +185,8 @@ void VectorView::paintEvent(QPaintEvent *event) } // Rotate display coordinates 45 degrees, flip Y axis and make sure the result stays within bounds - x = saturate(right - left + activeSize / 2.f); - y = saturate(activeSize - (right + left + activeSize / 2.f)); + int x = saturate(right - left + activeSize / 2.f); + int y = saturate(activeSize - (right + left + activeSize / 2.f)); // Estimate number of points needed to fill space between the old and new pixel. Cap at 100. unsigned char points = std::min((int)sqrt((m_oldX - x) * (m_oldX - x) + (m_oldY - y) * (m_oldY - y)), 100); @@ -222,6 +222,8 @@ void VectorView::paintEvent(QPaintEvent *event) // one full-color pixel per sample. for (std::size_t frame = 0; frame < frameCount; frame++) { + float left = 0.0f; + float right = 0.0f; float inLeft = inBuffer[frame][0] * m_zoom; float inRight = inBuffer[frame][1] * m_zoom; if (logScale) { @@ -235,8 +237,8 @@ void VectorView::paintEvent(QPaintEvent *event) left = inLeft * (activeSize - 1) / 4; right = inRight * (activeSize - 1) / 4; } - x = saturate(right - left + activeSize / 2.f); - y = saturate(activeSize - (right + left + activeSize / 2.f)); + int x = saturate(right - left + activeSize / 2.f); + int y = saturate(activeSize - (right + left + activeSize / 2.f)); ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = m_controls->m_colorFG.rgb(); } } diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index a3ec267e5..552b1f3ff 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -1053,7 +1053,6 @@ void ManageVestigeInstrumentView::syncPlugin( void ) auto paramStr = std::array{}; QStringList s_dumpValues; const QMap & dump = m_vi->m_plugin->parameterDump(); - float f_value; for( int i = 0; i < m_vi->paramCount; i++ ) { @@ -1063,7 +1062,7 @@ void ManageVestigeInstrumentView::syncPlugin( void ) { sprintf(paramStr.data(), "param%d", i); s_dumpValues = dump[paramStr.data()].split(":"); - f_value = LocaleHelper::toFloat(s_dumpValues.at(2)); + float f_value = LocaleHelper::toFloat(s_dumpValues.at(2)); m_vi->knobFModel[ i ]->setAutomatedValue( f_value ); m_vi->knobFModel[ i ]->setInitValue( f_value ); } diff --git a/plugins/Vibed/VibratingString.cpp b/plugins/Vibed/VibratingString.cpp index 44ade3c3a..216a5fbf3 100644 --- a/plugins/Vibed/VibratingString.cpp +++ b/plugins/Vibed/VibratingString.cpp @@ -75,12 +75,10 @@ std::unique_ptr VibratingString::initDelayLine(int l if (len > 0) { dl->data = std::make_unique(len); - float r; - float offset = 0.0f; for (int i = 0; i < dl->length; ++i) { - r = static_cast(std::rand()) / RAND_MAX; - offset = (m_randomize / 2.0f - m_randomize) * r; + float r = static_cast(std::rand()) / RAND_MAX; + float offset = (m_randomize / 2.0f - m_randomize) * r; dl->data[i] = offset; } } diff --git a/plugins/Vibed/VibratingString.h b/plugins/Vibed/VibratingString.h index efdaec282..0ad5a6e6e 100644 --- a/plugins/Vibed/VibratingString.h +++ b/plugins/Vibed/VibratingString.h @@ -49,8 +49,6 @@ public: sample_t nextSample() { - sample_t ym0; - sample_t ypM; for (int i = 0; i < m_oversample; ++i) { // Output at pickup position @@ -58,9 +56,9 @@ public: m_outsamp[i] += toBridgeAccess(m_toBridge.get(), m_pickupLoc); // Sample traveling into "bridge" - ym0 = toBridgeAccess(m_toBridge.get(), 1); + sample_t ym0 = toBridgeAccess(m_toBridge.get(), 1); // Sample to "nut" - ypM = fromBridgeAccess(m_fromBridge.get(), m_fromBridge->length - 2); + sample_t ypM = fromBridgeAccess(m_fromBridge.get(), m_fromBridge->length - 2); // String state update @@ -105,21 +103,18 @@ private: */ void setDelayLine(DelayLine* dl, int pick, const float* values, int len, float scale, bool state) { - float r; - float offset; - if (!state) { for (int i = 0; i < pick; ++i) { - r = static_cast(std::rand()) / RAND_MAX; - offset = (m_randomize / 2.0f - m_randomize) * r; + float r = static_cast(std::rand()) / RAND_MAX; + float offset = (m_randomize / 2.0f - m_randomize) * r; dl->data[i] = scale * values[dl->length - i - 1] + offset; } for (int i = pick; i < dl->length; ++i) { - r = static_cast(std::rand()) / RAND_MAX; - offset = (m_randomize / 2.0f - m_randomize) * r; + float r = static_cast(std::rand()) / RAND_MAX; + float offset = (m_randomize / 2.0f - m_randomize) * r; dl->data[i] = scale * values[i - pick] + offset; } } @@ -129,8 +124,8 @@ private: { for (int i = pick; i < dl->length; ++i) { - r = static_cast(std::rand()) / RAND_MAX; - offset = (m_randomize / 2.0f - m_randomize) * r; + float r = static_cast(std::rand()) / RAND_MAX; + float offset = (m_randomize / 2.0f - m_randomize) * r; dl->data[i] = scale * values[i - pick] + offset; } } @@ -138,8 +133,8 @@ private: { for (int i = 0; i < len; ++i) { - r = static_cast(std::rand()) / RAND_MAX; - offset = (m_randomize / 2.0f - m_randomize) * r; + float r = static_cast(std::rand()) / RAND_MAX; + float offset = (m_randomize / 2.0f - m_randomize) * r; dl->data[i+pick] = scale * values[i] + offset; } } diff --git a/plugins/VstEffect/VstEffectControls.cpp b/plugins/VstEffect/VstEffectControls.cpp index af90e4646..c9eb49234 100644 --- a/plugins/VstEffect/VstEffectControls.cpp +++ b/plugins/VstEffect/VstEffectControls.cpp @@ -452,7 +452,6 @@ void ManageVSTEffectView::syncPlugin() auto paramStr = std::array{}; QStringList s_dumpValues; const QMap & dump = m_effect->m_plugin->parameterDump(); - float f_value; for( int i = 0; i < m_vi2->paramCount; i++ ) { @@ -463,7 +462,7 @@ void ManageVSTEffectView::syncPlugin() { sprintf(paramStr.data(), "param%d", i); s_dumpValues = dump[paramStr.data()].split(":"); - f_value = LocaleHelper::toFloat(s_dumpValues.at(2)); + float f_value = LocaleHelper::toFloat(s_dumpValues.at(2)); m_vi2->knobFModel[ i ]->setAutomatedValue( f_value ); m_vi2->knobFModel[ i ]->setInitValue( f_value ); } diff --git a/plugins/Xpressive/ExprSynth.cpp b/plugins/Xpressive/ExprSynth.cpp index 991e0d3e6..736b47598 100644 --- a/plugins/Xpressive/ExprSynth.cpp +++ b/plugins/Xpressive/ExprSynth.cpp @@ -322,14 +322,8 @@ struct RandomVectorSeedFunction : public exprtk::ifunction inline float operator()(const float& index,const float& seed) override { - int irseed; - if (seed < 0 || std::isnan(seed) || std::isinf(seed)) - { - irseed=0; - } - else - irseed=(int)seed; - return randv(index,irseed); + const int irseed = seed < 0 || std::isnan(seed) || std::isinf(seed) ? 0 : static_cast(seed); + return randv(index, irseed); } static const int data_size=sizeof(random_data)/sizeof(int); diff --git a/plugins/Xpressive/Xpressive.cpp b/plugins/Xpressive/Xpressive.cpp index babc37231..f738dc965 100644 --- a/plugins/Xpressive/Xpressive.cpp +++ b/plugins/Xpressive/Xpressive.cpp @@ -256,13 +256,12 @@ void Xpressive::smooth(float smoothness,const graphModel * in,graphModel * out) auto const guassian = new float[guass_size]; float sum = 0.0f; float temp = 0.0f; - int i; - for (i = 0; i < guass_size; i++ ) + for (int i = 0; i < guass_size; i++) { temp = (i - guass_center) / delta; sum += guassian[i] = a * powf(F_E, -0.5f * temp * temp); } - for (i = 0; i < guass_size; i++ ) + for (int i = 0; i < guass_size; i++) { guassian[i] = guassian[i] / sum; } @@ -336,13 +335,6 @@ XpressiveView::XpressiveView(Instrument * _instrument, QWidget * _parent) : pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("wavegraph")); m_graph->setPalette(pal); - PixmapButton * m_w1Btn; - PixmapButton * m_w2Btn; - PixmapButton * m_w3Btn; - PixmapButton * m_o1Btn; - PixmapButton * m_o2Btn; - PixmapButton * m_helpBtn; - m_w1Btn = new PixmapButton(this, nullptr); m_w1Btn->move(3, ROW_BTN); m_w1Btn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("w1_active")); diff --git a/plugins/Xpressive/Xpressive.h b/plugins/Xpressive/Xpressive.h index b91957ac4..974b82b17 100644 --- a/plugins/Xpressive/Xpressive.h +++ b/plugins/Xpressive/Xpressive.h @@ -190,6 +190,7 @@ private: PixmapButton *m_w3Btn; PixmapButton *m_o1Btn; PixmapButton *m_o2Btn; + PixmapButton *m_helpBtn; PixmapButton *m_sinWaveBtn; PixmapButton *m_triangleWaveBtn; PixmapButton *m_sqrWaveBtn; diff --git a/src/core/AutomatableModel.cpp b/src/core/AutomatableModel.cpp index c701f28e3..abda0b43e 100644 --- a/src/core/AutomatableModel.cpp +++ b/src/core/AutomatableModel.cpp @@ -613,10 +613,9 @@ ValueBuffer * AutomatableModel::valueBuffer() float val = m_value; // make sure our m_value doesn't change midway - ValueBuffer * vb; if (m_controllerConnection && m_useControllerValue && m_controllerConnection->getController()->isSampleExact()) { - vb = m_controllerConnection->valueBuffer(); + auto vb = m_controllerConnection->valueBuffer(); if( vb ) { float * values = vb->values(); @@ -656,7 +655,7 @@ ValueBuffer * AutomatableModel::valueBuffer() if (lm && lm->controllerConnection() && lm->useControllerValue() && lm->controllerConnection()->getController()->isSampleExact()) { - vb = lm->valueBuffer(); + auto vb = lm->valueBuffer(); float * values = vb->values(); float * nvalues = m_valueBuffer.values(); for (int i = 0; i < vb->length(); i++) diff --git a/src/core/AutomationClip.cpp b/src/core/AutomationClip.cpp index 603550270..0e4271822 100644 --- a/src/core/AutomationClip.cpp +++ b/src/core/AutomationClip.cpp @@ -1222,11 +1222,9 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) // TODO: This behavior means that a very small difference between the inValue and outValue can // result in a big change in the curve. In the future, allowing the user to manually adjust // the tangents would be better. - float inTangent; - float outTangent; if (OFFSET(it) == 0) { - inTangent = (INVAL(it + 1) - OUTVAL(it - 1)) / (POS(it + 1) - POS(it - 1)); + float inTangent = (INVAL(it + 1) - OUTVAL(it - 1)) / (POS(it + 1) - POS(it - 1)); it.value().setInTangent(inTangent); // inTangent == outTangent in this case it.value().setOutTangent(inTangent); @@ -1234,9 +1232,9 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) else { // Calculate the left side of the curve - inTangent = (INVAL(it) - OUTVAL(it - 1)) / (POS(it) - POS(it - 1)); + float inTangent = (INVAL(it) - OUTVAL(it - 1)) / (POS(it) - POS(it - 1)); // Calculate the right side of the curve - outTangent = (INVAL(it + 1) - OUTVAL(it)) / (POS(it + 1) - POS(it)); + float outTangent = (INVAL(it + 1) - OUTVAL(it)) / (POS(it + 1) - POS(it)); it.value().setInTangent(inTangent); it.value().setOutTangent(outTangent); } diff --git a/src/core/BandLimitedWave.cpp b/src/core/BandLimitedWave.cpp index cb09dc5b8..060ff510a 100644 --- a/src/core/BandLimitedWave.cpp +++ b/src/core/BandLimitedWave.cpp @@ -49,11 +49,11 @@ QDataStream& operator<< ( QDataStream &out, WaveMipMap &waveMipMap ) QDataStream& operator>> ( QDataStream &in, WaveMipMap &waveMipMap ) { - sample_t sample; for( int tbl = 0; tbl <= MAXTBL; tbl++ ) { for( int i = 0; i < TLENS[tbl]; i++ ) { + sample_t sample; in >> sample; waveMipMap.setSampleAt( tbl, i, sample ); } @@ -67,9 +67,8 @@ void BandLimitedWave::generateWaves() // don't generate if they already exist if( s_wavesGenerated ) return; - int i; -// set wavetable directory + // set wavetable directory s_wavetableDir = "data:wavetables/"; // set wavetable files @@ -89,7 +88,7 @@ void BandLimitedWave::generateWaves() } else { - for( i = 0; i <= MAXTBL; i++ ) + for (int i = 0; i <= MAXTBL; i++) { const int len = TLENS[i]; //const double om = 1.0 / len; @@ -131,7 +130,7 @@ void BandLimitedWave::generateWaves() } else { - for( i = 0; i <= MAXTBL; i++ ) + for (int i = 0; i <= MAXTBL; i++) { const int len = TLENS[i]; //const double om = 1.0 / len; @@ -172,7 +171,7 @@ void BandLimitedWave::generateWaves() } else { - for( i = 0; i <= MAXTBL; i++ ) + for (int i = 0; i <= MAXTBL; i++) { const int len = TLENS[i]; //const double om = 1.0 / len; @@ -215,7 +214,7 @@ void BandLimitedWave::generateWaves() } else { - for( i = 0; i <= MAXTBL; i++ ) + for (int i = 0; i <= MAXTBL; i++) { const int len = TLENS[i]; diff --git a/src/core/Controller.cpp b/src/core/Controller.cpp index 3e1b596b8..81fc13c86 100644 --- a/src/core/Controller.cpp +++ b/src/core/Controller.cpp @@ -220,24 +220,12 @@ Controller * Controller::create( ControllerType _ct, Model * _parent ) Controller * Controller::create( const QDomElement & _this, Model * _parent ) { - Controller * c; - if( static_cast(_this.attribute( "type" ).toInt()) == ControllerType::Peak ) - { - c = PeakController::getControllerBySetting( _this ); - } - else - { - c = create( - static_cast( _this.attribute( "type" ).toInt() ), - _parent ); - } - - if( c != nullptr ) - { - c->restoreState( _this ); - } - - return( c ); + const auto controllerType = static_cast(_this.attribute("type").toInt()); + auto controller = controllerType == ControllerType::Peak + ? PeakController::getControllerBySetting(_this) + : create(controllerType, _parent); + if (controller) { controller->restoreState(_this); } + return controller; } diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 3c6309db8..eedb7f01d 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -1194,12 +1194,11 @@ void DataFile::upgrade_1_2_0_rc3() "pattern" ); for( int j = 0; !patterns.item( j ).isNull(); ++j ) { - int patternLength, steps; QDomElement el = patterns.item( j ).toElement(); if( el.attribute( "len" ) != "" ) { - patternLength = el.attribute( "len" ).toInt(); - steps = patternLength / 12; + int patternLength = el.attribute( "len" ).toInt(); + int steps = patternLength / 12; el.setAttribute( "steps", steps ); } } @@ -1456,7 +1455,7 @@ void DataFile::upgrade_1_3_0() if(num == 4) { // don't modify port 4, but some other ones: - int zoom_port; + int zoom_port = 0; if (plugin == "Equalizer5Band") zoom_port = 36; else if (plugin == "Equalizer8Band") diff --git a/src/core/DrumSynth.cpp b/src/core/DrumSynth.cpp index decc6bfa2..031b19c1c 100644 --- a/src/core/DrumSynth.cpp +++ b/src/core/DrumSynth.cpp @@ -69,16 +69,16 @@ float mem_t=1.0f, mem_o=1.0f, mem_n=1.0f, mem_b=1.0f, mem_tune=1.0f, mem_time=1. int DrumSynth::LongestEnv() { - long e, eon, p; - float l=0.f; + float l = 0.f; - for(e=1; e<7; e++) //3 - { - eon = e - 1; if(eon>2) eon=eon-1; - p = 0; - while (envpts[e][0][p + 1] >= 0.f) p++; - envData[e][MAX] = envpts[e][0][p] * timestretch; - if(chkOn[eon]==1) if(envData[e][MAX]>l) l=envData[e][MAX]; + for (long e = 1; e < 7; e++) // 3 + { + long eon = e - 1; + if (eon > 2) { eon = eon - 1; } + long p = 0; + while (envpts[e][0][p + 1] >= 0.f) { p++; } + envData[e][MAX] = envpts[e][0][p] * timestretch; + if (chkOn[eon] == 1 && envData[e][MAX] > l) { l = envData[e][MAX]; } } //l *= timestretch; @@ -102,16 +102,15 @@ float DrumSynth::LoudestEnv() void DrumSynth::UpdateEnv(int e, long t) { - float endEnv, dT; - //0.2's added - envData[e][NEXTT] = envpts[e][0][(long)(envData[e][PNT] + 1.f)] * timestretch; //get next point - if(envData[e][NEXTT] < 0) envData[e][NEXTT] = 442000 * timestretch; //if end point, hold - envData[e][ENV] = envpts[e][1][(long)(envData[e][PNT] + 0.f)] * 0.01f; //this level - endEnv = envpts[e][1][(long)(envData[e][PNT] + 1.f)] * 0.01f; //next level - dT = envData[e][NEXTT] - (float)t; - if(dT < 1.0) dT = 1.0; - envData[e][dENV] = (endEnv - envData[e][ENV]) / dT; - envData[e][PNT] = envData[e][PNT] + 1.0f; + // 0.2's added + envData[e][NEXTT] = envpts[e][0][static_cast(envData[e][PNT] + 1.f)] * timestretch; // get next point + if (envData[e][NEXTT] < 0) { envData[e][NEXTT] = 442000 * timestretch; } // if end point, hold + envData[e][ENV] = envpts[e][1][static_cast(envData[e][PNT] + 0.f)] * 0.01f; // this level + float endEnv = envpts[e][1][static_cast(envData[e][PNT] + 1.f)] * 0.01f; // next level + float dT = envData[e][NEXTT] - static_cast(t); + if (dT < 1.0) { dT = 1.0; } + envData[e][dENV] = (endEnv - envData[e][ENV]) / dT; + envData[e][PNT] = envData[e][PNT] + 1.0f; } @@ -149,34 +148,41 @@ void DrumSynth::GetEnv(int env, const char *sec, const char *key, QString ini) float DrumSynth::waveform(float ph, int form) { - float w; + float w; - switch (form) - { - case 0: w = (float)sin(fmod(ph,TwoPi)); break; //sine - case 1: w = (float)fabs(2.0f*(float)sin(fmod(0.5f*ph,TwoPi)))-1.f; break; //sine^2 - case 2: while(ph1.f) w=2.f-w; - break; - case 3: w = ph - TwoPi * (float)(int)(ph / TwoPi); //saw - w = (0.3183098f * w) - 1.f; break; - default: w = (sin(fmod(ph,TwoPi))>0.0)? 1.f: -1.f; break; //square - } + switch (form) + { + case 0: + w = static_cast(sin(fmod(ph, TwoPi))); + break; // sine + case 1: + w = static_cast(fabs(2.0f * static_cast(sin(fmod(0.5f * ph, TwoPi))) - 1.f)); + break; // sine^2 + case 2: + while (ph < TwoPi) { ph += TwoPi; } + w = 0.6366197f * static_cast(fmod(ph, TwoPi) - 1.f); // tri + if (w > 1.f) { w = 2.f - w; } + break; + case 3: + w = ph - TwoPi * static_cast(static_cast(ph / TwoPi)); // saw + w = (0.3183098f * w) - 1.f; + break; + default: + w = (sin(fmod(ph, TwoPi)) > 0.0) ? 1.f : -1.f; + break; // square + } - return w; + return w; } int DrumSynth::GetPrivateProfileString(const char *sec, const char *key, const char *def, char *buffer, int size, QString file) { - stringstream is; - bool inSection = false; - char *line; - char *k, *b; - int len = 0; + stringstream is; + bool inSection = false; + int len = 0; - line = (char*)malloc(200); + char* line = static_cast(malloc(200)); // Use QFile to handle unicode file name on Windows // Previously we used ifstream directly @@ -201,8 +207,8 @@ int DrumSynth::GetPrivateProfileString(const char *sec, const char *key, const c if (line[0] == '[') break; - k = strtok(line, " \t="); - b = strtok(nullptr, "\n\r\0"); + char* k = strtok(line, " \t="); + char* b = strtok(nullptr, "\n\r\0"); if (k != 0 && strcasecmp(k, key)==0) { if (b==0) { diff --git a/src/core/Effect.cpp b/src/core/Effect.cpp index 51b701d9b..7ede64e6b 100644 --- a/src/core/Effect.cpp +++ b/src/core/Effect.cpp @@ -210,8 +210,8 @@ void Effect::resample( int _i, const sampleFrame * _src_buf, m_srcData[_i].data_out = _dst_buf[0].data (); m_srcData[_i].src_ratio = (double) _dst_sr / _src_sr; m_srcData[_i].end_of_input = 0; - int error; - if( ( error = src_process( m_srcState[_i], &m_srcData[_i] ) ) ) + + if (int error = src_process(m_srcState[_i], &m_srcData[_i])) { qFatal( "Effect::resample(): error while resampling: %s\n", src_strerror( error ) ); diff --git a/src/core/InstrumentPlayHandle.cpp b/src/core/InstrumentPlayHandle.cpp index 097719ad8..06b8837a8 100644 --- a/src/core/InstrumentPlayHandle.cpp +++ b/src/core/InstrumentPlayHandle.cpp @@ -46,11 +46,10 @@ void InstrumentPlayHandle::play(sampleFrame * working_buffer) // ensure that all our nph's have been processed first auto nphv = NotePlayHandle::nphsOfInstrumentTrack(instrumentTrack, true); - - bool nphsLeft; + + bool nphsLeft = false; do { - nphsLeft = false; for (const NotePlayHandle * constNotePlayHandle : nphv) { if (constNotePlayHandle->state() != ThreadableJob::ProcessingState::Done && diff --git a/src/core/LadspaManager.cpp b/src/core/LadspaManager.cpp index 064c928ef..cc63af630 100644 --- a/src/core/LadspaManager.cpp +++ b/src/core/LadspaManager.cpp @@ -139,11 +139,7 @@ void LadspaManager::addPlugins( LADSPA_Descriptor_Function _descriptor_func, const QString & _file ) { - const LADSPA_Descriptor * descriptor; - - for( long pluginIndex = 0; - ( descriptor = _descriptor_func( pluginIndex ) ) != nullptr; - ++pluginIndex ) + for (long pluginIndex = 0; const auto descriptor = _descriptor_func(pluginIndex); ++pluginIndex) { ladspa_key_t key( _file, QString( descriptor->Label ) ); if( m_ladspaManagerMap.contains( key ) ) diff --git a/src/core/LinkedModelGroups.cpp b/src/core/LinkedModelGroups.cpp index 83bebc2a0..c52bce433 100644 --- a/src/core/LinkedModelGroups.cpp +++ b/src/core/LinkedModelGroups.cpp @@ -143,9 +143,7 @@ bool LinkedModelGroup::containsModel(const QString &name) const void LinkedModelGroups::linkAllModels() { LinkedModelGroup* first = getGroup(0); - LinkedModelGroup* cur; - - for (std::size_t i = 1; (cur = getGroup(i)); ++i) + for (size_t i = 1; auto cur = getGroup(i); ++i) { first->linkControls(cur); } @@ -172,8 +170,7 @@ void LinkedModelGroups::saveSettings(QDomDocument& doc, QDomElement& that) void LinkedModelGroups::loadSettings(const QDomElement& that) { QDomElement models = that.firstChildElement("models"); - LinkedModelGroup* grp0; - if (!models.isNull() && (grp0 = getGroup(0))) + if (auto grp0 = getGroup(0); !models.isNull() && grp0) { // only load the first group, the others are linked to the first grp0->loadValues(models); diff --git a/src/core/MemoryHelper.cpp b/src/core/MemoryHelper.cpp index de80ef770..8f990d57e 100644 --- a/src/core/MemoryHelper.cpp +++ b/src/core/MemoryHelper.cpp @@ -36,15 +36,14 @@ namespace lmms */ void* MemoryHelper::alignedMalloc( size_t byteNum ) { - char *ptr, *ptr2, *aligned_ptr; int align_mask = LMMS_ALIGN_SIZE - 1; - ptr = static_cast( malloc( byteNum + LMMS_ALIGN_SIZE + sizeof( int ) ) ); + char* ptr = static_cast(malloc(byteNum + LMMS_ALIGN_SIZE + sizeof(int))); if( ptr == nullptr ) return nullptr; - ptr2 = ptr + sizeof( int ); - aligned_ptr = ptr2 + ( LMMS_ALIGN_SIZE - ( ( size_t ) ptr2 & align_mask ) ); + char* ptr2 = ptr + sizeof(int); + char* aligned_ptr = ptr2 + (LMMS_ALIGN_SIZE - ((size_t)ptr2 & align_mask)); ptr2 = aligned_ptr - sizeof( int ); *( ( int* ) ptr2 ) = ( int )( aligned_ptr - ptr ); diff --git a/src/core/Plugin.cpp b/src/core/Plugin.cpp index 973914501..f165ddf75 100644 --- a/src/core/Plugin.cpp +++ b/src/core/Plugin.cpp @@ -226,8 +226,8 @@ Plugin * Plugin::instantiate(const QString& pluginName, Model * parent, } else { - InstantiationHook instantiationHook; - if ((instantiationHook = ( InstantiationHook ) pi.library->resolve( "lmms_plugin_main" ))) + auto instantiationHook = reinterpret_cast(pi.library->resolve("lmms_plugin_main")); + if (instantiationHook) { inst = instantiationHook(parent, data); if(!inst) { diff --git a/src/core/Song.cpp b/src/core/Song.cpp index ddc55707c..81a263287 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -945,13 +945,12 @@ void Song::createNewProject() m_oldFileName = ""; setProjectFileName(""); - Track * t; - t = Track::create( Track::Type::Instrument, this ); - dynamic_cast( t )->loadInstrument( - "tripleoscillator" ); - t = Track::create(Track::Type::Instrument, Engine::patternStore()); - dynamic_cast( t )->loadInstrument( - "kicker" ); + auto tripleOscTrack = Track::create(Track::Type::Instrument, this); + dynamic_cast(tripleOscTrack)->loadInstrument("tripleoscillator"); + + auto kickerTrack = Track::create(Track::Type::Instrument, Engine::patternStore()); + dynamic_cast(kickerTrack)->loadInstrument("kicker"); + Track::create( Track::Type::Sample, this ); Track::create( Track::Type::Pattern, this ); Track::create( Track::Type::Automation, this ); diff --git a/src/core/audio/AudioAlsa.cpp b/src/core/audio/AudioAlsa.cpp index 201a967a3..d80bc8912 100644 --- a/src/core/audio/AudioAlsa.cpp +++ b/src/core/audio/AudioAlsa.cpp @@ -53,12 +53,7 @@ AudioAlsa::AudioAlsa( bool & _success_ful, AudioEngine* _audioEngine ) : "Could not avoid possible interception by PulseAudio\n" ); } - int err; - - if( ( err = snd_pcm_open( &m_handle, - probeDevice().toLatin1().constData(), - SND_PCM_STREAM_PLAYBACK, - 0 ) ) < 0 ) + if (int err = snd_pcm_open(&m_handle, probeDevice().toLatin1().constData(), SND_PCM_STREAM_PLAYBACK, 0); err < 0) { printf( "Playback open error: %s\n", snd_strerror( err ) ); return; @@ -67,14 +62,13 @@ AudioAlsa::AudioAlsa( bool & _success_ful, AudioEngine* _audioEngine ) : snd_pcm_hw_params_malloc( &m_hwParams ); snd_pcm_sw_params_malloc( &m_swParams ); - if( ( err = setHWParams( channels(), - SND_PCM_ACCESS_RW_INTERLEAVED ) ) < 0 ) + if (int err = setHWParams(channels(), SND_PCM_ACCESS_RW_INTERLEAVED); err < 0) { printf( "Setting of hwparams failed: %s\n", snd_strerror( err ) ); return; } - if( ( err = setSWParams() ) < 0 ) + if (int err = setSWParams(); err < 0) { printf( "Setting of swparams failed: %s\n", snd_strerror( err ) ); @@ -83,9 +77,8 @@ AudioAlsa::AudioAlsa( bool & _success_ful, AudioEngine* _audioEngine ) : // set FD_CLOEXEC flag for all file descriptors so forked processes // do not inherit them - struct pollfd * ufds; int count = snd_pcm_poll_descriptors_count( m_handle ); - ufds = new pollfd[count]; + auto ufds = new pollfd[count]; snd_pcm_poll_descriptors( m_handle, ufds, count ); for (int i = 0; i < std::max(3, count); ++i) { @@ -160,7 +153,7 @@ AudioAlsa::DeviceInfoCollection AudioAlsa::getAvailableDevices() { DeviceInfoCollection deviceInfos; - char **hints; + char** hints = nullptr; /* Enumerate sound devices */ int err = snd_device_name_hint(-1, "pcm", (void***)&hints); @@ -261,25 +254,21 @@ void AudioAlsa::applyQualitySettings() snd_pcm_close( m_handle ); } - int err; - if( ( err = snd_pcm_open( &m_handle, - probeDevice().toLatin1().constData(), - SND_PCM_STREAM_PLAYBACK, - 0 ) ) < 0 ) + if (int err = snd_pcm_open(&m_handle, probeDevice().toLatin1().constData(), SND_PCM_STREAM_PLAYBACK, 0); + err < 0) { printf( "Playback open error: %s\n", snd_strerror( err ) ); return; } - if( ( err = setHWParams( channels(), - SND_PCM_ACCESS_RW_INTERLEAVED ) ) < 0 ) + if (int err = setHWParams(channels(), SND_PCM_ACCESS_RW_INTERLEAVED); err < 0) { printf( "Setting of hwparams failed: %s\n", snd_strerror( err ) ); return; } - if( ( err = setSWParams() ) < 0 ) + if (int err = setSWParams(); err < 0) { printf( "Setting of swparams failed: %s\n", snd_strerror( err ) ); @@ -370,10 +359,8 @@ void AudioAlsa::run() int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) { - int err, dir; - // choose all parameters - if( ( err = snd_pcm_hw_params_any( m_handle, m_hwParams ) ) < 0 ) + if (int err = snd_pcm_hw_params_any(m_handle, m_hwParams); err < 0) { printf( "Broken configuration for playback: no configurations " "available: %s\n", snd_strerror( err ) ); @@ -381,8 +368,7 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) } // set the interleaved read/write format - if( ( err = snd_pcm_hw_params_set_access( m_handle, m_hwParams, - _access ) ) < 0 ) + if (int err = snd_pcm_hw_params_set_access(m_handle, m_hwParams, _access); err < 0) { printf( "Access type not available for playback: %s\n", snd_strerror( err ) ); @@ -390,11 +376,9 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) } // set the sample format - if( ( snd_pcm_hw_params_set_format( m_handle, m_hwParams, - SND_PCM_FORMAT_S16_LE ) ) < 0 ) + if (int err = snd_pcm_hw_params_set_format(m_handle, m_hwParams, SND_PCM_FORMAT_S16_LE); err < 0) { - if( ( snd_pcm_hw_params_set_format( m_handle, m_hwParams, - SND_PCM_FORMAT_S16_BE ) ) < 0 ) + if (int err = snd_pcm_hw_params_set_format(m_handle, m_hwParams, SND_PCM_FORMAT_S16_BE); err < 0) { printf( "Neither little- nor big-endian available for " "playback: %s\n", snd_strerror( err ) ); @@ -408,8 +392,7 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) } // set the count of channels - if( ( err = snd_pcm_hw_params_set_channels( m_handle, m_hwParams, - _channels ) ) < 0 ) + if (int err = snd_pcm_hw_params_set_channels(m_handle, m_hwParams, _channels); err < 0) { printf( "Channel count (%i) not available for playbacks: %s\n" "(Does your soundcard not support surround?)\n", @@ -418,11 +401,9 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) } // set the sample rate - if( ( err = snd_pcm_hw_params_set_rate( m_handle, m_hwParams, - sampleRate(), 0 ) ) < 0 ) + if (int err = snd_pcm_hw_params_set_rate(m_handle, m_hwParams, sampleRate(), 0); err < 0) { - if( ( err = snd_pcm_hw_params_set_rate( m_handle, m_hwParams, - audioEngine()->baseSampleRate(), 0 ) ) < 0 ) + if (int err = snd_pcm_hw_params_set_rate(m_handle, m_hwParams, audioEngine()->baseSampleRate(), 0); err < 0) { printf( "Could not set sample rate: %s\n", snd_strerror( err ) ); @@ -432,36 +413,29 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) m_periodSize = audioEngine()->framesPerPeriod(); m_bufferSize = m_periodSize * 8; - dir = 0; - err = snd_pcm_hw_params_set_period_size_near( m_handle, m_hwParams, - &m_periodSize, &dir ); - if( err < 0 ) + int dir; + if (int err = snd_pcm_hw_params_set_period_size_near(m_handle, m_hwParams, &m_periodSize, &dir); err < 0) { printf( "Unable to set period size %lu for playback: %s\n", m_periodSize, snd_strerror( err ) ); return err; } dir = 0; - err = snd_pcm_hw_params_get_period_size( m_hwParams, &m_periodSize, - &dir ); - if( err < 0 ) + if (int err = snd_pcm_hw_params_get_period_size(m_hwParams, &m_periodSize, &dir); err < 0) { printf( "Unable to get period size for playback: %s\n", snd_strerror( err ) ); } dir = 0; - err = snd_pcm_hw_params_set_buffer_size_near( m_handle, m_hwParams, - &m_bufferSize ); - if( err < 0 ) + if (int err = snd_pcm_hw_params_set_buffer_size_near(m_handle, m_hwParams, &m_bufferSize); err < 0) { printf( "Unable to set buffer size %lu for playback: %s\n", m_bufferSize, snd_strerror( err ) ); return ( err ); } - err = snd_pcm_hw_params_get_buffer_size( m_hwParams, &m_bufferSize ); - if( 2 * m_periodSize > m_bufferSize ) + if (int err = snd_pcm_hw_params_get_buffer_size(m_hwParams, &m_bufferSize); 2 * m_periodSize > m_bufferSize) { printf( "buffer to small, could not use\n" ); return ( err ); @@ -469,8 +443,7 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) // write the parameters to device - err = snd_pcm_hw_params( m_handle, m_hwParams ); - if( err < 0 ) + if (int err = snd_pcm_hw_params(m_handle, m_hwParams); err < 0) { printf( "Unable to set hw params for playback: %s\n", snd_strerror( err ) ); @@ -485,10 +458,8 @@ int AudioAlsa::setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ) int AudioAlsa::setSWParams() { - int err; - // get the current swparams - if( ( err = snd_pcm_sw_params_current( m_handle, m_swParams ) ) < 0 ) + if (int err = snd_pcm_sw_params_current(m_handle, m_swParams); err < 0) { printf( "Unable to determine current swparams for playback: %s" "\n", snd_strerror( err ) ); @@ -496,8 +467,7 @@ int AudioAlsa::setSWParams() } // start the transfer when a period is full - if( ( err = snd_pcm_sw_params_set_start_threshold( m_handle, - m_swParams, m_periodSize ) ) < 0 ) + if (int err = snd_pcm_sw_params_set_start_threshold(m_handle, m_swParams, m_periodSize); err < 0) { printf( "Unable to set start threshold mode for playback: %s\n", snd_strerror( err ) ); @@ -506,8 +476,7 @@ int AudioAlsa::setSWParams() // allow the transfer when at least m_periodSize samples can be // processed - if( ( err = snd_pcm_sw_params_set_avail_min( m_handle, m_swParams, - m_periodSize ) ) < 0 ) + if (int err = snd_pcm_sw_params_set_avail_min(m_handle, m_swParams, m_periodSize); err < 0) { printf( "Unable to set avail min for playback: %s\n", snd_strerror( err ) ); @@ -527,7 +496,7 @@ int AudioAlsa::setSWParams() #endif // write the parameters to the playback device - if( ( err = snd_pcm_sw_params( m_handle, m_swParams ) ) < 0 ) + if (int err = snd_pcm_sw_params(m_handle, m_swParams); err < 0) { printf( "Unable to set sw params for playback: %s\n", snd_strerror( err ) ); diff --git a/src/core/audio/AudioDevice.cpp b/src/core/audio/AudioDevice.cpp index 58ba3932e..743716622 100644 --- a/src/core/audio/AudioDevice.cpp +++ b/src/core/audio/AudioDevice.cpp @@ -195,8 +195,7 @@ fpp_t AudioDevice::resample( const surroundSampleFrame * _src, m_srcData.data_out = _dst[0].data (); m_srcData.src_ratio = (double) _dst_sr / _src_sr; m_srcData.end_of_input = 0; - int error; - if( ( error = src_process( m_srcState, &m_srcData ) ) ) + if (int error = src_process(m_srcState, &m_srcData)) { printf( "AudioDevice::resample(): error while resampling: %s\n", src_strerror( error ) ); @@ -213,12 +212,11 @@ int AudioDevice::convertToS16( const surroundSampleFrame * _ab, { if( _convert_endian ) { - int_sample_t temp; for( fpp_t frame = 0; frame < _frames; ++frame ) { for( ch_cnt_t chnl = 0; chnl < channels(); ++chnl ) { - temp = static_cast(AudioEngine::clip(_ab[frame][chnl]) * OUTPUT_SAMPLE_MULTIPLIER); + auto temp = static_cast(AudioEngine::clip(_ab[frame][chnl]) * OUTPUT_SAMPLE_MULTIPLIER); ( _output_buffer + frame * channels() )[chnl] = ( temp & 0x00ff ) << 8 | diff --git a/src/core/audio/AudioFileOgg.cpp b/src/core/audio/AudioFileOgg.cpp index 9d5f0c809..3818273d6 100644 --- a/src/core/audio/AudioFileOgg.cpp +++ b/src/core/audio/AudioFileOgg.cpp @@ -156,7 +156,6 @@ bool AudioFileOgg::startEncoding() ogg_packet header_main; ogg_packet header_comments; ogg_packet header_codebooks; - int result; // Build the packets vorbis_analysis_headerout( &m_vd, m_comments, &header_main, @@ -167,14 +166,9 @@ bool AudioFileOgg::startEncoding() ogg_stream_packetin( &m_os, &header_comments ); ogg_stream_packetin( &m_os, &header_codebooks ); - while( ( result = ogg_stream_flush( &m_os, &m_og ) ) ) + while (ogg_stream_flush(&m_os, &m_og)) { - if( !result ) - { - break; - } - int ret = writePage(); - if( ret != m_og.header_len + m_og.body_len ) + if (int ret = writePage(); ret != m_og.header_len + m_og.body_len) { // clean up finishEncoding(); diff --git a/src/core/audio/AudioPortAudio.cpp b/src/core/audio/AudioPortAudio.cpp index 2fbdb04b5..1cb8c8eed 100644 --- a/src/core/audio/AudioPortAudio.cpp +++ b/src/core/audio/AudioPortAudio.cpp @@ -92,10 +92,9 @@ AudioPortAudio::AudioPortAudio( bool & _success_ful, AudioEngine * _audioEngine PaDeviceIndex inDevIdx = -1; PaDeviceIndex outDevIdx = -1; - const PaDeviceInfo * di; for( int i = 0; i < Pa_GetDeviceCount(); ++i ) { - di = Pa_GetDeviceInfo( i ); + const auto di = Pa_GetDeviceInfo(i); if( di->name == device && Pa_GetHostApiInfo( di->hostApi )->name == backend ) { @@ -343,10 +342,9 @@ void AudioPortAudioSetupUtil::updateBackends() return; } - const PaHostApiInfo * hi; for( int i = 0; i < Pa_GetHostApiCount(); ++i ) { - hi = Pa_GetHostApiInfo( i ); + const auto hi = Pa_GetHostApiInfo(i); m_backendModel.addItem( hi->name ); } @@ -367,10 +365,9 @@ void AudioPortAudioSetupUtil::updateDevices() // get active backend const QString& backend = m_backendModel.currentText(); int hostApi = 0; - const PaHostApiInfo * hi; for( int i = 0; i < Pa_GetHostApiCount(); ++i ) { - hi = Pa_GetHostApiInfo( i ); + const auto hi = Pa_GetHostApiInfo(i); if( backend == hi->name ) { hostApi = i; @@ -380,10 +377,9 @@ void AudioPortAudioSetupUtil::updateDevices() // get devices for selected backend m_deviceModel.clear(); - const PaDeviceInfo * di; for( int i = 0; i < Pa_GetDeviceCount(); ++i ) { - di = Pa_GetDeviceInfo( i ); + const auto di = Pa_GetDeviceInfo(i); if( di->hostApi == hostApi ) { m_deviceModel.addItem( di->name ); diff --git a/src/core/audio/AudioSoundIo.cpp b/src/core/audio/AudioSoundIo.cpp index a3d72e36b..c16327a90 100644 --- a/src/core/audio/AudioSoundIo.cpp +++ b/src/core/audio/AudioSoundIo.cpp @@ -69,7 +69,6 @@ AudioSoundIo::AudioSoundIo( bool & outSuccessful, AudioEngine * _audioEngine ) : const QString& configDeviceId = ConfigManager::inst()->value( "audiosoundio", "out_device_id" ); const QString& configDeviceRaw = ConfigManager::inst()->value( "audiosoundio", "out_device_raw" ); - int err; int outDeviceCount = 0; int backendCount = soundio_backend_count(m_soundio); for (int i = 0; i < backendCount; i += 1) @@ -77,11 +76,7 @@ AudioSoundIo::AudioSoundIo( bool & outSuccessful, AudioEngine * _audioEngine ) : SoundIoBackend backend = soundio_get_backend(m_soundio, i); if (configBackend == soundio_backend_name(backend)) { - if ((err = soundio_connect_backend(m_soundio, backend))) - { - // error occurred, leave outDeviceCount 0 - } - else + if (!soundio_connect_backend(m_soundio, backend)) { soundio_flush_events(m_soundio); if (m_disconnectErr) @@ -98,7 +93,7 @@ AudioSoundIo::AudioSoundIo( bool & outSuccessful, AudioEngine * _audioEngine ) : if (outDeviceCount <= 0) { // try connecting to the default backend - if ((err = soundio_connect(m_soundio))) + if (int err = soundio_connect(m_soundio)) { fprintf(stderr, "Unable to initialize soundio: %s\n", soundio_strerror(err)); return; @@ -179,7 +174,7 @@ AudioSoundIo::AudioSoundIo( bool & outSuccessful, AudioEngine * _audioEngine ) : m_outstream->layout = *soundio_channel_layout_get_default(channels()); m_outstream->format = SoundIoFormatFloat32NE; - if ((err = soundio_outstream_open(m_outstream))) + if (int err = soundio_outstream_open(m_outstream)) { fprintf(stderr, "Unable to initialize soundio: %s\n", soundio_strerror(err)); return; @@ -214,8 +209,6 @@ AudioSoundIo::~AudioSoundIo() void AudioSoundIo::startProcessing() { - int err; - m_outBufFrameIndex = 0; m_outBufFramesTotal = 0; m_outBufSize = audioEngine()->framesPerPeriod(); @@ -224,7 +217,7 @@ void AudioSoundIo::startProcessing() if (! m_outstreamStarted) { - if ((err = soundio_outstream_start(m_outstream))) + if (int err = soundio_outstream_start(m_outstream)) { fprintf(stderr, "AudioSoundIo::startProcessing() :: soundio unable to start stream: %s\n", @@ -236,7 +229,7 @@ void AudioSoundIo::startProcessing() m_stopped = false; - if ((err = soundio_outstream_pause(m_outstream, false))) + if (int err = soundio_outstream_pause(m_outstream, false)) { m_stopped = true; fprintf(stderr, @@ -247,12 +240,10 @@ void AudioSoundIo::startProcessing() void AudioSoundIo::stopProcessing() { - int err; - m_stopped = true; if (m_outstream) { - if ((err = soundio_outstream_pause(m_outstream, true))) + if (int err = soundio_outstream_pause(m_outstream, true)) { fprintf(stderr, "AudioSoundIo::stopProcessing() :: pausing result error: %s\n", @@ -281,16 +272,14 @@ void AudioSoundIo::writeCallback(int frameCountMin, int frameCountMax) { if (m_stopped) {return;} const struct SoundIoChannelLayout *layout = &m_outstream->layout; - SoundIoChannelArea *areas; + SoundIoChannelArea* areas; int bytesPerSample = m_outstream->bytes_per_sample; - int err; - int framesLeft = frameCountMax; while (framesLeft > 0) { int frameCount = framesLeft; - if ((err = soundio_outstream_begin_write(m_outstream, &areas, &frameCount))) + if (int err = soundio_outstream_begin_write(m_outstream, &areas, &frameCount)) { errorCallback(err); return; @@ -332,7 +321,7 @@ void AudioSoundIo::writeCallback(int frameCountMin, int frameCountMax) m_outBufFrameIndex += 1; } - if ((err = soundio_outstream_end_write(m_outstream))) + if (int err = soundio_outstream_end_write(m_outstream)) { errorCallback(err); return; @@ -372,11 +361,10 @@ void AudioSoundIo::setupWidget::reconnectSoundIo() soundio_disconnect(m_soundio); - int err; int backend_index = m_backendModel.findText(configBackend); if (backend_index < 0) { - if ((err = soundio_connect(m_soundio))) + if (int err = soundio_connect(m_soundio)) { fprintf(stderr, "soundio: unable to connect backend: %s\n", soundio_strerror(err)); return; @@ -387,11 +375,11 @@ void AudioSoundIo::setupWidget::reconnectSoundIo() else { SoundIoBackend backend = soundio_get_backend(m_soundio, backend_index); - if ((err = soundio_connect_backend(m_soundio, backend))) + if (int err = soundio_connect_backend(m_soundio, backend)) { fprintf(stderr, "soundio: unable to connect %s backend: %s\n", soundio_backend_name(backend), soundio_strerror(err)); - if ((err = soundio_connect(m_soundio))) + if (int err = soundio_connect(m_soundio)) { fprintf(stderr, "soundio: unable to connect backend: %s\n", soundio_strerror(err)); return; diff --git a/src/core/lv2/Lv2Evbuf.cpp b/src/core/lv2/Lv2Evbuf.cpp index acfb9b8aa..07a3d58e6 100644 --- a/src/core/lv2/Lv2Evbuf.cpp +++ b/src/core/lv2/Lv2Evbuf.cpp @@ -129,12 +129,11 @@ lv2_evbuf_next(LV2_Evbuf_Iterator iter) LV2_Evbuf* evbuf = iter.evbuf; uint32_t offset = iter.offset; - uint32_t size; - size = ((LV2_Atom_Event*) - ((char*)LV2_ATOM_CONTENTS(LV2_Atom_Sequence, &evbuf->buf.atom) - + offset))->body.size; - offset += lv2_evbuf_pad_size(sizeof(LV2_Atom_Event) + size); + const auto contents = static_cast(LV2_ATOM_CONTENTS(LV2_Atom_Sequence, &evbuf->buf.atom)) + offset; + const uint32_t size = reinterpret_cast(contents)->body.size; + + offset += lv2_evbuf_pad_size(sizeof(LV2_Atom_Event) + size); LV2_Evbuf_Iterator next = { evbuf, offset }; return next; } diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 77177a1c0..4715aeb7f 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -425,12 +425,8 @@ void Lv2Proc::handleMidiInputEvent(const MidiEvent &event, const TimePos &time, AutomatableModel *Lv2Proc::modelAtPort(const QString &uri) { - // unused currently - AutomatableModel *mod; - auto itr = m_connectedModels.find(uri.toUtf8().data()); - if (itr != m_connectedModels.end()) { mod = itr->second; } - else { mod = nullptr; } - return mod; + const auto itr = m_connectedModels.find(uri.toUtf8().data()); + return itr != m_connectedModels.end() ? itr->second : nullptr; } diff --git a/src/core/main.cpp b/src/core/main.cpp index 25a6ab9c5..cadffdafa 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -906,19 +906,13 @@ int main( int argc, char * * argv ) mb.setWindowIcon( embed::getIconPixmap( "icon_small" ) ); mb.setWindowFlags( Qt::WindowCloseButtonHint ); - QPushButton * recover; - QPushButton * discard; - QPushButton * exit; - // setting all buttons to the same roles allows us // to have a custom layout - discard = mb.addButton( MainWindow::tr( "Discard" ), - QMessageBox::AcceptRole ); - recover = mb.addButton( MainWindow::tr( "Recover" ), - QMessageBox::AcceptRole ); + auto discard = mb.addButton(MainWindow::tr("Discard"), QMessageBox::AcceptRole); + auto recover = mb.addButton(MainWindow::tr("Recover"), QMessageBox::AcceptRole); // have a hidden exit button - exit = mb.addButton( "", QMessageBox::RejectRole); + auto exit = mb.addButton("", QMessageBox::RejectRole); exit->setVisible(false); // set icons diff --git a/src/core/midi/MidiAlsaRaw.cpp b/src/core/midi/MidiAlsaRaw.cpp index 23364fc01..f091b789f 100644 --- a/src/core/midi/MidiAlsaRaw.cpp +++ b/src/core/midi/MidiAlsaRaw.cpp @@ -39,10 +39,7 @@ MidiAlsaRaw::MidiAlsaRaw() : m_outputp( &m_output ), m_quit( false ) { - int err; - if( ( err = snd_rawmidi_open( m_inputp, m_outputp, - probeDevice().toLatin1().constData(), - 0 ) ) < 0 ) + if (int err = snd_rawmidi_open(m_inputp, m_outputp, probeDevice().toLatin1().constData(), 0); err < 0) { printf( "cannot open MIDI-device: %s\n", snd_strerror( err ) ); return; @@ -111,29 +108,27 @@ void MidiAlsaRaw::run() { msleep( 5 ); // must do that, otherwise this thread takes // too much CPU-time, even with LowPriority... - int err = poll( m_pfds, m_npfds, 10000 ); - if( err < 0 && errno == EINTR ) + if (int err = poll(m_pfds, m_npfds, 10000); err < 0 && errno == EINTR) { printf( "MidiAlsaRaw::run(): Got EINTR while " "polling. Will stop polling MIDI-events from " "MIDI-port.\n" ); break; } - if( err < 0 ) + else if (err < 0) { printf( "poll failed: %s\nWill stop polling " "MIDI-events from MIDI-port.\n", strerror( errno ) ); break; } - if( err == 0 ) + else if (err == 0) { //printf( "there seems to be no active MIDI-device %d\n", ++cnt ); continue; } - unsigned short revents; - if( ( err = snd_rawmidi_poll_descriptors_revents( - m_input, m_pfds, m_npfds, &revents ) ) < 0 ) + unsigned short revents = 0; + if (int err = snd_rawmidi_poll_descriptors_revents(m_input, m_pfds, m_npfds, &revents); err < 0) { printf( "cannot get poll events: %s\nWill stop polling " "MIDI-events from MIDI-port.\n", @@ -149,25 +144,19 @@ void MidiAlsaRaw::run() { continue; } - err = snd_rawmidi_read(m_input, buf.data(), buf.size()); - if( err == -EAGAIN ) - { - continue; - } - if( err < 0 ) + + if (int err = snd_rawmidi_read(m_input, buf.data(), buf.size()); err == -EAGAIN) { continue; } + else if (err < 0) { printf( "cannot read from port \"%s\": %s\nWill stop " "polling MIDI-events from MIDI-port.\n", /*port_name*/"default", snd_strerror( err ) ); break; } - if( err == 0 ) + else if (err == 0) { continue; } + else { - continue; - } - for( int i = 0; i < err; ++i ) - { - parseData( buf[i] ); + for (int i = 0; i < err; ++i) { parseData(buf[i]); } } } diff --git a/src/core/midi/MidiAlsaSeq.cpp b/src/core/midi/MidiAlsaSeq.cpp index 0b3bab819..e0b8b486e 100644 --- a/src/core/midi/MidiAlsaSeq.cpp +++ b/src/core/midi/MidiAlsaSeq.cpp @@ -78,10 +78,7 @@ MidiAlsaSeq::MidiAlsaSeq() : m_quit( false ), m_portListUpdateTimer( this ) { - int err; - if( ( err = snd_seq_open( &m_seqHandle, - probeDevice().toLatin1().constData(), - SND_SEQ_OPEN_DUPLEX, 0 ) ) < 0 ) + if (int err = snd_seq_open(&m_seqHandle, probeDevice().toLatin1().constData(), SND_SEQ_OPEN_DUPLEX, 0); err < 0) { fprintf( stderr, "cannot open sequencer: %s\n", snd_strerror( err ) ); diff --git a/src/core/midi/MidiController.cpp b/src/core/midi/MidiController.cpp index 0ae76d352..112d9d974 100644 --- a/src/core/midi/MidiController.cpp +++ b/src/core/midi/MidiController.cpp @@ -74,11 +74,11 @@ void MidiController::updateName() void MidiController::processInEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) { - unsigned char controllerNum; switch(event.type()) { case MidiControlChange: - controllerNum = event.controllerNumber(); + { + unsigned char controllerNum = event.controllerNumber(); if (m_midiPort.inputController() == controllerNum && (m_midiPort.inputChannel() == event.channel() + 1 || m_midiPort.inputChannel() == 0)) @@ -89,7 +89,7 @@ void MidiController::processInEvent(const MidiEvent& event, const TimePos& time, emit valueChanged(); } break; - + } default: // Don't care - maybe add special cases for pitch and mod later break; diff --git a/src/core/midi/MidiJack.cpp b/src/core/midi/MidiJack.cpp index 145a72ecc..29e7e27ec 100644 --- a/src/core/midi/MidiJack.cpp +++ b/src/core/midi/MidiJack.cpp @@ -179,7 +179,6 @@ QString MidiJack::probeDevice() // we read data from jack void MidiJack::JackMidiRead(jack_nframes_t nframes) { - unsigned int i,b; void* port_buf = jack_port_get_buffer(m_input_port, nframes); jack_midi_event_t in_event; jack_nframes_t event_index = 0; @@ -188,13 +187,13 @@ void MidiJack::JackMidiRead(jack_nframes_t nframes) int rval = jack_midi_event_get(&in_event, port_buf, 0); if (rval == 0 /* 0 = success */) { - for(i=0; i 0; n--, p++) + for (char* p = buf; n > 0; n--, p++) { parseData( *p ); } diff --git a/src/gui/ControlLayout.cpp b/src/gui/ControlLayout.cpp index 5e9a21101..75133c8e3 100644 --- a/src/gui/ControlLayout.cpp +++ b/src/gui/ControlLayout.cpp @@ -101,8 +101,7 @@ ControlLayout::ControlLayout(QWidget *parent, int margin, int hSpacing, int vSpa ControlLayout::~ControlLayout() { - QLayoutItem *item; - while ((item = takeAt(0))) { delete item; } + while (auto item = takeAt(0)) { delete item; } } void ControlLayout::onTextChanged(const QString&) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 0413e3bd6..072edc0ec 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1108,8 +1108,7 @@ void MainWindow::updateViewMenu() // Here we should put all look&feel -stuff from configmanager // that is safe to change on the fly. There is probably some // more elegant way to do this. - QAction *qa; - qa = new QAction(tr( "Volume as dBFS" ), this); + auto qa = new QAction(tr("Volume as dBFS"), this); qa->setData("displaydbfs"); qa->setCheckable( true ); qa->setChecked( ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ); diff --git a/src/gui/ProjectNotes.cpp b/src/gui/ProjectNotes.cpp index f131a017c..a71f146c6 100644 --- a/src/gui/ProjectNotes.cpp +++ b/src/gui/ProjectNotes.cpp @@ -108,10 +108,8 @@ void ProjectNotes::setText( const QString & _text ) void ProjectNotes::setupActions() { QToolBar * tb = addToolBar( tr( "Edit Actions" ) ); - QAction * a; - a = new QAction( embed::getIconPixmap( "edit_undo" ), tr( "&Undo" ), - this ); + auto a = new QAction(embed::getIconPixmap("edit_undo"), tr("&Undo"), this); a->setShortcut( tr( "%1+Z" ).arg(UI_CTRL_KEY) ); connect( a, SIGNAL(triggered()), m_edit, SLOT(undo())); tb->addAction( a ); diff --git a/src/gui/clips/AutomationClipView.cpp b/src/gui/clips/AutomationClipView.cpp index 7ddb70151..9b71bb74c 100644 --- a/src/gui/clips/AutomationClipView.cpp +++ b/src/gui/clips/AutomationClipView.cpp @@ -314,24 +314,17 @@ void AutomationClipView::paintEvent( QPaintEvent * ) // the outValue of the current node). When we have nodes with linear or cubic progression // the value of the end of the shape between the two nodes will be the inValue of // the next node. - float nextValue; - if( m_clip->progressionType() == AutomationClip::ProgressionType::Discrete ) - { - nextValue = OUTVAL(it); - } - else - { - nextValue = INVAL(it + 1); - } + float nextValue = m_clip->progressionType() == AutomationClip::ProgressionType::Discrete + ? OUTVAL(it) + : INVAL(it + 1); QPainterPath path; QPointF origin = QPointF(POS(it) * ppTick, 0.0f); path.moveTo( origin ); path.moveTo(QPointF(POS(it) * ppTick,values[0])); - float x; for (int i = POS(it) + 1; i < POS(it + 1); i++) { - x = i * ppTick; + float x = i * ppTick; if( x > ( width() - BORDER_WIDTH ) ) break; float value = values[i - POS(it)]; path.lineTo( QPointF( x, value ) ); diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index ae026fff3..d23682d6e 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -320,8 +320,6 @@ void AutomationEditor::drawLine( int x0In, float y0, int x1In, float y1 ) auto deltay = qAbs(y1 - y0); int x = x0; float y = y0; - int xstep; - int ystep; if( deltax < AutomationClip::quantization() ) { @@ -332,34 +330,14 @@ void AutomationEditor::drawLine( int x0In, float y0, int x1In, float y1 ) float yscale = deltay / ( deltax ); - if( x0 < x1 ) - { - xstep = AutomationClip::quantization(); - } - else - { - xstep = -( AutomationClip::quantization() ); - } + int xstep = (x0 < x1 ? 1 : -1) * AutomationClip::quantization(); + int ystep = y0 < y1 ? 1 : -1; + float lineAdjust = ystep * yscale; - float lineAdjust; - if( y0 < y1 ) - { - ystep = 1; - lineAdjust = yscale; - } - else - { - ystep = -1; - lineAdjust = -( yscale ); - } - - int i = 0; - while( i < deltax ) + for (int i = 0; i < deltax; ++i) { y = y0 + ( ystep * yscale * i ) + lineAdjust; - x += xstep; - i += 1; m_clip->removeNode(TimePos(x)); m_clip->putValue( TimePos( x ), y ); } @@ -979,7 +957,6 @@ inline void AutomationEditor::drawCross( QPainter & p ) inline void AutomationEditor::drawAutomationPoint(QPainter & p, timeMap::iterator it) { int x = xCoordOfTick(POS(it)); - int y; // Below (m_ppb * AutomationClip::quantization() / 576) is used because: // 1 bar equals to 192/quantization() notes. Hence, to calculate the number of pixels // per note we would have (m_ppb * 1 bar / (192/quantization()) notes per bar), or @@ -988,7 +965,7 @@ inline void AutomationEditor::drawAutomationPoint(QPainter & p, timeMap::iterato const int outerRadius = qBound(3, (m_ppb * AutomationClip::quantization()) / 576, 5); // Draw a circle for the outValue - y = yCoordOfLevel(OUTVAL(it)); + int y = yCoordOfLevel(OUTVAL(it)); p.setPen(QPen(m_nodeOutValueColor.lighter(200))); p.setBrush(QBrush(m_nodeOutValueColor)); p.drawEllipse(x - outerRadius, y - outerRadius, outerRadius * 2, outerRadius * 2); @@ -1006,7 +983,6 @@ inline void AutomationEditor::drawAutomationPoint(QPainter & p, timeMap::iterato inline void AutomationEditor::drawAutomationTangents(QPainter& p, timeMap::iterator it) { int x = xCoordOfTick(POS(it)); - int y, tx, ty; // The tangent value correlates the variation in the node value related to the increase // in ticks. So to have a proportionate drawing of the tangent line, we need to find the @@ -1020,9 +996,9 @@ inline void AutomationEditor::drawAutomationTangents(QPainter& p, timeMap::itera p.setPen(QPen(m_nodeTangentLineColor)); p.setBrush(QBrush(m_nodeTangentLineColor)); - y = yCoordOfLevel(INVAL(it)); - tx = x - 20; - ty = y + 20 * INTAN(it) * proportion; + int y = yCoordOfLevel(INVAL(it)); + int tx = x - 20; + int ty = y + 20 * INTAN(it) * proportion; p.drawLine(x, y, tx, ty); p.setBrush(QBrush(m_nodeTangentLineColor.darker(200))); p.drawEllipse(tx - 3, ty - 3, 6, 6); @@ -1101,7 +1077,6 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } else { - int y; int level = (int) m_bottomLevel; int printable = qMax( 1, 5 * DEFAULT_Y_DELTA / m_y_delta ); @@ -1116,7 +1091,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) { const QString & label = m_clip->firstObject() ->displayValue( level ); - y = yCoordOfLevel( level ); + int y = yCoordOfLevel(level); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::Shadow ) ); p.drawText( 1, y - font_height + 1, @@ -1139,7 +1114,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) if( m_clip ) { - int tick, x, q; + int q; int x_line_end = (int)( m_y_auto || m_topLevel < m_maxLevel ? TOP_MARGIN : grid_bottom - ( m_topLevel - m_bottomLevel ) * m_y_delta ); @@ -1163,10 +1138,8 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) // 3 independent loops, because quantization might not divide evenly into // exotic denominators (e.g. 7/11 time), which are allowed ATM. // First quantization grid... - for( tick = m_currentPosition - m_currentPosition % q, - x = xCoordOfTick( tick ); - x<=width(); - tick += q, x = xCoordOfTick( tick ) ) + for (int tick = m_currentPosition - m_currentPosition % q, x = xCoordOfTick(tick); x <= width(); + tick += q, x = xCoordOfTick(tick)) { p.setPen(m_lineColor); p.drawLine( x, grid_bottom, x, x_line_end ); @@ -1187,10 +1160,9 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } else { - float y; for( int level = (int)m_bottomLevel; level <= m_topLevel; level++) { - y = yCoordOfLevel( (float)level ); + float y = yCoordOfLevel(static_cast(level)); p.setPen(level % 10 == 0 ? m_beatLineColor : m_lineColor); @@ -1226,10 +1198,8 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) int ticksPerBeat = DefaultTicksPerBar / Engine::getSong()->getTimeSigModel().getDenominator(); - for( tick = m_currentPosition - m_currentPosition % ticksPerBeat, - x = xCoordOfTick( tick ); - x<=width(); - tick += ticksPerBeat, x = xCoordOfTick( tick ) ) + for (int tick = m_currentPosition - m_currentPosition % ticksPerBeat, x = xCoordOfTick(tick); x <= width(); + tick += ticksPerBeat, x = xCoordOfTick(tick)) { p.setPen(m_beatLineColor); p.drawLine( x, grid_bottom, x, x_line_end ); @@ -1316,10 +1286,8 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } // and finally bars - for( tick = m_currentPosition - m_currentPosition % TimePos::ticksPerBar(), - x = xCoordOfTick( tick ); - x<=width(); - tick += TimePos::ticksPerBar(), x = xCoordOfTick( tick ) ) + for (int tick = m_currentPosition - m_currentPosition % TimePos::ticksPerBar(), x = xCoordOfTick(tick); + x <= width(); tick += TimePos::ticksPerBar(), x = xCoordOfTick(tick)) { p.setPen(m_barLineColor); p.drawLine( x, grid_bottom, x, x_line_end ); @@ -1365,15 +1333,9 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) // the outValue of the current node). When we have nodes with linear or cubic progression // the value of the end of the shape between the two nodes will be the inValue of // the next node. - float nextValue; - if( m_clip->progressionType() == AutomationClip::ProgressionType::Discrete ) - { - nextValue = OUTVAL(it); - } - else - { - nextValue = INVAL(it + 1); - } + float nextValue = m_clip->progressionType() == AutomationClip::ProgressionType::Discrete + ? OUTVAL(it) + : INVAL(it + 1); p.setRenderHints( QPainter::Antialiasing, true ); QPainterPath path; @@ -1523,25 +1485,11 @@ void AutomationEditor::drawLevelTick(QPainter & p, int tick, float value) || ( value > m_topLevel && m_topLevel >= 0 ) || ( value < m_bottomLevel && m_bottomLevel <= 0 ) ) { - int y_start = yCoordOfLevel( value ); - int rect_height; - - if( m_y_auto ) - { - int y_end = (int)( grid_bottom - + ( grid_bottom - TOP_MARGIN ) - * m_minLevel - / ( m_maxLevel - m_minLevel ) ); - - rect_height = y_end - y_start; - } - else - { - rect_height = (int)( value * m_y_delta ); - } + const int y_start = yCoordOfLevel(value); + const int y_end = grid_bottom + (grid_bottom - TOP_MARGIN) * m_minLevel / (m_maxLevel - m_minLevel); + const int rect_height = m_y_auto ? y_end - y_start : value * m_y_delta; QBrush currentColor = m_graphColor; - p.fillRect( x, y_start, rect_width, rect_height, currentColor ); } #ifdef LMMS_DEBUG diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 47e658f11..9d22a21bd 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -3030,10 +3030,9 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) if (hasValidMidiClip()) { - int pianoAreaHeight, partialKeyVisible, topKey, topNote; - pianoAreaHeight = keyAreaBottom() - keyAreaTop(); - m_pianoKeysVisible = pianoAreaHeight / m_keyLineHeight; - partialKeyVisible = pianoAreaHeight % m_keyLineHeight; + int pianoAreaHeight = keyAreaBottom() - keyAreaTop(); + int m_pianoKeysVisible = pianoAreaHeight / m_keyLineHeight; + int partialKeyVisible = pianoAreaHeight % m_keyLineHeight; // check if we're below the minimum key area size if (m_pianoKeysVisible * m_keyLineHeight < KEY_AREA_MIN_HEIGHT) { @@ -3058,8 +3057,8 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) PR_TOP_MARGIN - PR_BOTTOM_MARGIN; partialKeyVisible = 0; } - topKey = qBound(0, m_startKey + m_pianoKeysVisible - 1, NumKeys - 1); - topNote = topKey % KeysPerOctave; + int topKey = std::clamp(m_startKey + m_pianoKeysVisible - 1, 0, NumKeys - 1); + int topNote = topKey % KeysPerOctave; // if not resizing the note edit area, we can change m_notesEditHeight if (m_action != Action::ResizeNoteEditArea && partialKeyVisible != 0) { diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 400c03aa9..1806f2931 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -774,17 +774,9 @@ static inline void animateScroll( QScrollBar *scrollBar, int newVal, bool smooth void SongEditor::updatePosition( const TimePos & t ) { - int widgetWidth, trackOpWidth; - if( ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt() ) - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT; - trackOpWidth = TRACK_OP_WIDTH_COMPACT; - } - else - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH; - trackOpWidth = TRACK_OP_WIDTH; - } + const bool compactTrackButtons = ConfigManager::inst()->value("ui", "compacttrackbuttons").toInt(); + const auto widgetWidth = compactTrackButtons ? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT : DEFAULT_SETTINGS_WIDGET_WIDTH; + const auto trackOpWidth = compactTrackButtons ? TRACK_OP_WIDTH_COMPACT : TRACK_OP_WIDTH; if( ( m_song->isPlaying() && m_song->m_playMode == Song::PlayMode::Song && m_timeLine->autoScroll() == TimeLineWidget::AutoScrollState::Enabled) || diff --git a/src/gui/modals/ControllerConnectionDialog.cpp b/src/gui/modals/ControllerConnectionDialog.cpp index 4d1090d5c..12e26d03c 100644 --- a/src/gui/modals/ControllerConnectionDialog.cpp +++ b/src/gui/modals/ControllerConnectionDialog.cpp @@ -299,9 +299,8 @@ void ControllerConnectionDialog::selectController() { if( m_midiControllerSpinBox->model()->value() > 0 ) { - MidiController * mc; - mc = m_midiController->copyToMidiController( Engine::getSong() ); - + auto mc = m_midiController->copyToMidiController(Engine::getSong()); + /* if( m_targetModel->getTrack() && !m_targetModel->getTrack()->displayName().isEmpty() ) diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index 6dbd9fbc3..840fe2991 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -199,15 +199,13 @@ void Fader::mousePressEvent( QMouseEvent* mouseEvent ) void Fader::mouseDoubleClickEvent( QMouseEvent* mouseEvent ) { bool ok; - float newValue; // TODO: dbV handling - newValue = QInputDialog::getDouble( this, tr( "Set value" ), - tr( "Please enter a new value between %1 and %2:" ). - arg( model()->minValue() * m_conversionFactor ). - arg( model()->maxValue() * m_conversionFactor ), - model()->getRoundedValue() * m_conversionFactor, - model()->minValue() * m_conversionFactor, - model()->maxValue() * m_conversionFactor, model()->getDigitCount(), &ok ) / m_conversionFactor; + auto minv = model()->minValue() * m_conversionFactor; + auto maxv = model()->maxValue() * m_conversionFactor; + float newValue = QInputDialog::getDouble(this, tr("Set value"), + tr("Please enter a new value between %1 and %2:").arg(minv).arg(maxv), + model()->getRoundedValue() * m_conversionFactor, minv, maxv, model()->getDigitCount(), &ok) + / m_conversionFactor; if( ok ) { diff --git a/src/gui/widgets/Graph.cpp b/src/gui/widgets/Graph.cpp index 922b98668..0781d4f11 100644 --- a/src/gui/widgets/Graph.cpp +++ b/src/gui/widgets/Graph.cpp @@ -643,11 +643,10 @@ void graphModel::convolve(const float *convolution, // store values in temporary array QVector temp = m_samples; const int graphLength = length(); - float sum; // make a cyclic convolution for ( int i = 0; i < graphLength; i++ ) { - sum = 0; + float sum = 0.0f; for ( int j = 0; j < convolutionLength; j++ ) { sum += convolution[j] * temp[( i + j ) % graphLength]; diff --git a/src/gui/widgets/TimeDisplayWidget.cpp b/src/gui/widgets/TimeDisplayWidget.cpp index 3dad6b1b0..92eaf1efe 100644 --- a/src/gui/widgets/TimeDisplayWidget.cpp +++ b/src/gui/widgets/TimeDisplayWidget.cpp @@ -91,24 +91,25 @@ void TimeDisplayWidget::updateTime() switch( m_displayMode ) { case DisplayMode::MinutesSeconds: - int msec; - msec = s->getMilliseconds(); + { + int msec = s->getMilliseconds(); m_majorLCD.setValue(msec / 60000); m_minorLCD.setValue((msec / 1000) % 60); m_milliSecondsLCD.setValue(msec % 1000); break; - + } case DisplayMode::BarsTicks: - int tick; - tick = s->getPlayPos().getTicks(); + { + int tick = s->getPlayPos().getTicks(); m_majorLCD.setValue((int)(tick / s->ticksPerBar()) + 1); m_minorLCD.setValue((tick % s->ticksPerBar()) / (s->ticksPerBar() / s->getTimeSigModel().getNumerator() ) +1); m_milliSecondsLCD.setValue((tick % s->ticksPerBar()) % (s->ticksPerBar() / s->getTimeSigModel().getNumerator())); break; - - default: break; + } + default: + break; } } diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index cdd360e70..a32d301c4 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -772,17 +772,17 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, } } - Note * cur_note; - while( nit != notes.end() && - ( cur_note = *nit )->pos() == cur_start ) + while (nit != notes.end() && (*nit)->pos() == cur_start) { + const auto currentNote = *nit; + // If the note is a Step Note, frames will be 0 so the NotePlayHandle // plays for the whole length of the sample - const auto note_frames = cur_note->type() == Note::Type::Step + const auto noteFrames = currentNote->type() == Note::Type::Step ? 0 - : cur_note->length().frames(frames_per_tick); + : currentNote->length().frames(frames_per_tick); - NotePlayHandle* notePlayHandle = NotePlayHandleManager::acquire( this, _offset, note_frames, *cur_note ); + NotePlayHandle* notePlayHandle = NotePlayHandleManager::acquire(this, _offset, noteFrames, *currentNote); notePlayHandle->setPatternTrack(pattern_track); // are we playing global song? if( _clip_num < 0 ) From bad57356d73c2ebfc147863a54e052fe83fe94a6 Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 29 Mar 2024 12:36:20 -0400 Subject: [PATCH 120/191] Fix infinite loop in `InstrumentPlayHandle::play` (#7176) --- src/core/InstrumentPlayHandle.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core/InstrumentPlayHandle.cpp b/src/core/InstrumentPlayHandle.cpp index 06b8837a8..ef7d78f3f 100644 --- a/src/core/InstrumentPlayHandle.cpp +++ b/src/core/InstrumentPlayHandle.cpp @@ -47,22 +47,21 @@ void InstrumentPlayHandle::play(sampleFrame * working_buffer) // ensure that all our nph's have been processed first auto nphv = NotePlayHandle::nphsOfInstrumentTrack(instrumentTrack, true); - bool nphsLeft = false; + bool nphsLeft; do { - for (const NotePlayHandle * constNotePlayHandle : nphv) + nphsLeft = false; + for (const auto& handle : nphv) { - if (constNotePlayHandle->state() != ThreadableJob::ProcessingState::Done && - !constNotePlayHandle->isFinished()) + if (handle->state() != ThreadableJob::ProcessingState::Done && !handle->isFinished()) { nphsLeft = true; - NotePlayHandle * notePlayHandle = const_cast(constNotePlayHandle); - notePlayHandle->process(); + const_cast(handle)->process(); } } } while (nphsLeft); - + m_instrument->play(working_buffer); // Process the audio buffer that the instrument has just worked on... From 03f885abc252f3e8207eabfbdca4dd9de5e34b99 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:39:56 -0700 Subject: [PATCH 121/191] Fix B/B Track Label Button (#7177) * Fix Pattern Track Label Button --- src/gui/tracks/PatternTrackView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/tracks/PatternTrackView.cpp b/src/gui/tracks/PatternTrackView.cpp index be039ba79..ac0b42e2d 100644 --- a/src/gui/tracks/PatternTrackView.cpp +++ b/src/gui/tracks/PatternTrackView.cpp @@ -26,6 +26,7 @@ #include "Engine.h" #include "GuiApplication.h" +#include "MainWindow.h" #include "PatternEditor.h" #include "PatternStore.h" #include "PatternTrack.h" @@ -74,8 +75,7 @@ bool PatternTrackView::close() void PatternTrackView::clickedTrackLabel() { Engine::patternStore()->setCurrentPattern(m_patternTrack->patternIndex()); - getGUI()->patternEditor()->parentWidget()->show(); - getGUI()->patternEditor()->setFocus(Qt::ActiveWindowFocusReason); + getGUI()->mainWindow()->togglePatternEditorWin(true); } From 9c591b178f95b5b9b461f00c6b2b7dc71e2c20b3 Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Sat, 30 Mar 2024 00:15:26 -0500 Subject: [PATCH 122/191] PianoRoll::paintEvent: don't shadow member variable (#7181) --- src/gui/editors/PianoRoll.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 9d22a21bd..8623b5da7 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -3031,7 +3031,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) if (hasValidMidiClip()) { int pianoAreaHeight = keyAreaBottom() - keyAreaTop(); - int m_pianoKeysVisible = pianoAreaHeight / m_keyLineHeight; + m_pianoKeysVisible = pianoAreaHeight / m_keyLineHeight; int partialKeyVisible = pianoAreaHeight % m_keyLineHeight; // check if we're below the minimum key area size if (m_pianoKeysVisible * m_keyLineHeight < KEY_AREA_MIN_HEIGHT) From a98c70091147008408ac208129ce3cf2909bc8ab Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 30 Mar 2024 08:53:08 +0100 Subject: [PATCH 123/191] Optimize map accesses in LadspaManager (#7173) Some methods in `LadspaManager` performed repeated searches through the map by first calling `contains` and then by actually fetching the entry. This is fixed by using `find` on the map. It returns an iterator which can directly provide the result or indicate that nothing was found. That way the map is only searched once. --- src/core/LadspaManager.cpp | 54 ++++++++++++++------------------------ 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/src/core/LadspaManager.cpp b/src/core/LadspaManager.cpp index cc63af630..e4d472bd1 100644 --- a/src/core/LadspaManager.cpp +++ b/src/core/LadspaManager.cpp @@ -122,14 +122,8 @@ LadspaManager::~LadspaManager() LadspaManagerDescription * LadspaManager::getDescription( const ladspa_key_t & _plugin ) { - if( m_ladspaManagerMap.contains( _plugin ) ) - { - return( m_ladspaManagerMap[_plugin] ); - } - else - { - return( nullptr ); - } + auto const it = m_ladspaManagerMap.find(_plugin); + return it != m_ladspaManagerMap.end() ? *it : nullptr; } @@ -519,24 +513,16 @@ bool LadspaManager::isInteger( const ladspa_key_t & _plugin, bool LadspaManager::isEnum( const ladspa_key_t & _plugin, uint32_t _port ) { - if( m_ladspaManagerMap.contains( _plugin ) - && _port < getPortCount( _plugin ) ) + auto const * desc = getDescriptor(_plugin); + if (desc && _port < desc->PortCount) { - LADSPA_Descriptor_Function descriptorFunction = - m_ladspaManagerMap[_plugin]->descriptorFunction; - const LADSPA_Descriptor * descriptor = - descriptorFunction( - m_ladspaManagerMap[_plugin]->index ); LADSPA_PortRangeHintDescriptor hintDescriptor = - descriptor->PortRangeHints[_port].HintDescriptor; + desc->PortRangeHints[_port].HintDescriptor; // This is an LMMS extension to ladspa - return( LADSPA_IS_HINT_INTEGER( hintDescriptor ) && - LADSPA_IS_HINT_TOGGLED( hintDescriptor ) ); - } - else - { - return( false ); + return LADSPA_IS_HINT_INTEGER(hintDescriptor) && LADSPA_IS_HINT_TOGGLED(hintDescriptor); } + + return false; } @@ -562,22 +548,20 @@ const void * LadspaManager::getImplementationData( -const LADSPA_Descriptor * LadspaManager::getDescriptor( - const ladspa_key_t & _plugin ) +const LADSPA_Descriptor * LadspaManager::getDescriptor(const ladspa_key_t & _plugin) { - if( m_ladspaManagerMap.contains( _plugin ) ) + auto const it = m_ladspaManagerMap.find(_plugin); + if (it != m_ladspaManagerMap.end()) { - LADSPA_Descriptor_Function descriptorFunction = - m_ladspaManagerMap[_plugin]->descriptorFunction; - const LADSPA_Descriptor * descriptor = - descriptorFunction( - m_ladspaManagerMap[_plugin]->index ); - return( descriptor ); - } - else - { - return( nullptr ); + auto const plugin = *it; + + LADSPA_Descriptor_Function descriptorFunction = plugin->descriptorFunction; + const LADSPA_Descriptor* descriptor = descriptorFunction(plugin->index); + + return descriptor; } + + return nullptr; } From b622fa22064727dd6bebce47b5c7da69cdfadcab Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 30 Mar 2024 16:30:21 +0100 Subject: [PATCH 124/191] Conditionally remove use of QApplication::desktop in ComboBox.cpp (#7179) Prepare the application for Qt6 by conditionally removing the use of `QApplication::desktop` in `ComboBox.cpp`. The method was already deprecated in Qt5 and is removed in Qt6. Instead the method `QWidget::screen` is used now if the Qt version is equal to or newer than 5.14 (because the method was only introduced with that version). Fall back to `QApplication::desktop` if an older version is detected during the build. This is for example the case for the CI builds which are based on Ubuntu 18.04. Generalize the check if the menu can be shown in the screen. The code now also does handles the case where the menu would go out of screen along the X axis. Also remove the unused include `embed.h`. --- src/gui/widgets/ComboBox.cpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index b11990b27..6e5eff58b 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -26,16 +26,20 @@ #include "ComboBox.h" -#include -#include #include #include #include +#include #include "CaptionMenu.h" -#include "embed.h" #include "gui_templates.h" +#define QT_SUPPORTS_WIDGET_SCREEN (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) +#if !QT_SUPPORTS_WIDGET_SCREEN +#include +#include +#endif + namespace lmms::gui { const int CB_ARROW_BTN_WIDTH = 18; @@ -116,15 +120,23 @@ void ComboBox::mousePressEvent( QMouseEvent* event ) a->setData( i ); } - QPoint gpos = mapToGlobal( QPoint( 0, height() ) ); - if( gpos.y() + m_menu.sizeHint().height() < qApp->desktop()->height() ) + QPoint gpos = mapToGlobal(QPoint(0, height())); + + #if (QT_SUPPORTS_WIDGET_SCREEN) + bool const menuCanBeFullyShown = screen()->geometry().contains(QRect(gpos, m_menu.sizeHint())); + #else + bool const menuCanBeFullyShown = gpos.y() + m_menu.sizeHint().height() < qApp->desktop()->height(); + #endif + + if (menuCanBeFullyShown) { - m_menu.exec( gpos ); + m_menu.exec(gpos); } else { - m_menu.exec( mapToGlobal( QPoint( width(), 0 ) ) ); + m_menu.exec(mapToGlobal(QPoint(width(), 0))); } + m_pressed = false; update(); } From c271d2831458d4d33610ebccd3517319a916d014 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 30 Mar 2024 19:04:57 +0100 Subject: [PATCH 125/191] Ensure that build and target are directories in .gitignore (#6884) Ensure that `build` and `target` are directories in `.gitignore`. --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1b855f204..cc2823ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -/build -/target +/build/ +/target/ .*.sw? .DS_Store *~ From 5d5d8f8f1424949fcd77d1a4a39eeb5479a0a10a Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 1 Apr 2024 14:57:43 +0200 Subject: [PATCH 126/191] Only repaint LcdWidget if necessary (#7187) Some analysis done with Callgrind showed that the `LcdWidget` spends quite some time repainting itself even when nothing has changed. Some widget instances are used in song update contexts where `LcdWidget::setValue` is called 60 times per second. This commit fixes the problem by only updating the `LcdWidget` if its value really has changed. Adjust the condition in the if-clause so that it becomes clearer what's the interval of interest. --- src/gui/widgets/LcdWidget.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/gui/widgets/LcdWidget.cpp b/src/gui/widgets/LcdWidget.cpp index fa7dea1da..b30195d6c 100644 --- a/src/gui/widgets/LcdWidget.cpp +++ b/src/gui/widgets/LcdWidget.cpp @@ -78,20 +78,26 @@ void LcdWidget::setValue(int value) } } - m_display = s; + if (m_display != s) + { + m_display = s; - update(); + update(); + } } void LcdWidget::setValue(float value) { - if (value < 0 && value > -1) + if (-1 < value && value < 0) { QString s = QString::number(static_cast(value)); s.prepend('-'); - m_display = s; - update(); + if (m_display != s) + { + m_display = s; + update(); + } } else { From 9dd7f4dde456efa95586a0c4744255039f965c78 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 1 Apr 2024 20:06:18 +0200 Subject: [PATCH 127/191] Remove "GENERAL SETTINGS" tab (#7188) Remove the "GENERAL SETTINGS" tab from the instrument track and sample track window. This removes clutter as well as dependencies to the non-scalable `TabWidget`. Remove superfluous calls to `setSpacing` which have set the default value of 6. Use the default content margins of (9, 9, 9, 9) instead of (8, 8, 8, 8). --- src/gui/SampleTrackWindow.cpp | 7 +------ src/gui/instrument/InstrumentTrackWindow.cpp | 6 +----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/gui/SampleTrackWindow.cpp b/src/gui/SampleTrackWindow.cpp index 630119253..81a2ca89b 100644 --- a/src/gui/SampleTrackWindow.cpp +++ b/src/gui/SampleTrackWindow.cpp @@ -40,7 +40,6 @@ #include "SampleTrackView.h" #include "Song.h" #include "SubWindow.h" -#include "TabWidget.h" #include "TrackLabelButton.h" namespace lmms::gui @@ -69,13 +68,9 @@ SampleTrackWindow::SampleTrackWindow(SampleTrackView * tv) : vlayout->setContentsMargins(0, 0, 0, 0); vlayout->setSpacing(0); - auto generalSettingsWidget = new TabWidget(tr("GENERAL SETTINGS"), this); - + auto generalSettingsWidget = new QWidget(this); auto generalSettingsLayout = new QVBoxLayout(generalSettingsWidget); - generalSettingsLayout->setContentsMargins(8, 18, 8, 8); - generalSettingsLayout->setSpacing(6); - auto nameWidget = new QWidget(generalSettingsWidget); auto nameLayout = new QHBoxLayout(nameWidget); nameLayout->setContentsMargins(0, 0, 0, 0); diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index b6a51624b..5387346a9 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -93,13 +93,9 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : vlayout->setContentsMargins(0, 0, 0, 0); vlayout->setSpacing( 0 ); - auto generalSettingsWidget = new TabWidget(tr("GENERAL SETTINGS"), this); - + auto generalSettingsWidget = new QWidget(this); auto generalSettingsLayout = new QVBoxLayout(generalSettingsWidget); - generalSettingsLayout->setContentsMargins( 8, 18, 8, 8 ); - generalSettingsLayout->setSpacing( 6 ); - auto nameAndChangeTrackWidget = new QWidget(generalSettingsWidget); auto nameAndChangeTrackLayout = new QHBoxLayout(nameAndChangeTrackWidget); nameAndChangeTrackLayout->setContentsMargins( 0, 0, 0, 0 ); From b14f8ab8fd1f675cd0c69793219e5a70c0248a91 Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Thu, 4 Apr 2024 01:14:00 +0200 Subject: [PATCH 128/191] Refactor ArpDirection::Down and ArpDirection::DownAndUp (#7007) Fixes an issue with the arpeggiation when it is set to go downwards and the cycle is over 0. --- src/core/InstrumentFunctions.cpp | 34 +++++++++++--------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/core/InstrumentFunctions.cpp b/src/core/InstrumentFunctions.cpp index 976363d3d..39c994ab6 100644 --- a/src/core/InstrumentFunctions.cpp +++ b/src/core/InstrumentFunctions.cpp @@ -433,42 +433,24 @@ void InstrumentFunctionArpeggio::processNote( NotePlayHandle * _n ) int cur_arp_idx = 0; // process according to arpeggio-direction... - if( dir == ArpDirection::Up ) + if (dir == ArpDirection::Up || dir == ArpDirection::Down) { cur_arp_idx = ( cur_frame / arp_frames ) % range; } - else if( dir == ArpDirection::Down ) - { - cur_arp_idx = range - ( cur_frame / arp_frames ) % - range - 1; - } - else if( dir == ArpDirection::UpAndDown && range > 1 ) + else if ((dir == ArpDirection::UpAndDown || dir == ArpDirection::DownAndUp) && range > 1) { // imagine, we had to play the arp once up and then // once down -> makes 2 * range possible notes... // because we don't play the lower and upper notes // twice, we have to subtract 2 - cur_arp_idx = ( cur_frame / arp_frames ) % ( range * 2 - 2 ); + cur_arp_idx = (cur_frame / arp_frames) % (range * 2 - (2 * static_cast(m_arpRepeatsModel.value()))); // if greater than range, we have to play down... // looks like the code for arp_dir==DOWN... :) - if( cur_arp_idx >= range ) + if (cur_arp_idx >= range) { - cur_arp_idx = range - cur_arp_idx % ( range - 1 ) - 1; + cur_arp_idx = range - cur_arp_idx % (range - 1) - static_cast(m_arpRepeatsModel.value()); } } - else if( dir == ArpDirection::DownAndUp && range > 1 ) - { - // copied from ArpDirection::UpAndDown above - cur_arp_idx = ( cur_frame / arp_frames ) % ( range * 2 - 2 ); - // if greater than range, we have to play down... - // looks like the code for arp_dir==DOWN... :) - if( cur_arp_idx >= range ) - { - cur_arp_idx = range - cur_arp_idx % ( range - 1 ) - 1; - } - // inverts direction - cur_arp_idx = range - cur_arp_idx - 1; - } else if( dir == ArpDirection::Random ) { // just pick a random chord-index @@ -485,6 +467,12 @@ void InstrumentFunctionArpeggio::processNote( NotePlayHandle * _n ) cur_arp_idx %= static_cast( range / m_arpRepeatsModel.value() ); } + // If ArpDirection::Down or ArpDirection::DownAndUp, invert the final range. + if (dir == ArpDirection::Down || dir == ArpDirection::DownAndUp) + { + cur_arp_idx = static_cast(range / m_arpRepeatsModel.value()) - cur_arp_idx - 1; + } + // now calculate final key for our arp-note const int sub_note_key = base_note_key + (cur_arp_idx / cur_chord_size ) * KeysPerOctave + chord_table.chords()[selected_arp][cur_arp_idx % cur_chord_size]; From 20fec28befe95f5bc08453f776f7b5252877407d Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Thu, 4 Apr 2024 21:40:31 +0200 Subject: [PATCH 129/191] Font size adjustments (#7185) Adjust and rename the function `pointSize` so that it sets the font size in pixels. Rename `pointSize` to `adjustedToPixelSize` because that's what it does now. It returns a font adjusted to a given pixel size. Rename `fontPointer` to `font` because it's not a pointer but a copy. Rename `fontSize` to simply `size`. This works if the intended model is that users use global fractional scaling. In that case pixel sized fonts are also scaled so that they should stay legible for different screen sizes and pixel densities. ## Adjust plugins with regards to adjustedToPixelSize Adjust the plugins with regards to the use of `adjustedToPixelSize`. Remove the explicit setting of the font size of combo boxes in the following places to make the combo boxes consistent: * `AudioFileProcessorView.cpp` * `DualFilterControlDialog.cpp` * `Monstro.cpp` (does not even seem to use text) * `Mallets.cpp` Remove calls to `adjustedToPixelSize` in the following places because they can deal with different font sizes: * `LadspaBrowser.cpp` Set an explicit point sized font size for the "Show GUI" button in `ZynAddSubFx.cpp` Increase the font size of the buttons in the Vestige plugin and reduce code repetition by introducing a single variable for the font size. I was not able to find out where the font in `VstEffectControlDialog.cpp` is shown. So it is left as is for now. ## Adjust the font sizes in the area of GUI editors and instruments. Increase the font size to 10 pixels in the following places: * Effect view: "Controls" button and the display of the effect name at the bottom * Automation editor: Min and max value display to the left of the editor * InstrumentFunctionViews: Labels "Chord:", "Direction:" and "Mode:" * InstrumentMidiIOView: Message display "Specify the velocity normalization base for MIDI-based instruments at 100% note velocity." * InstrumentSoundShapingView: Message display "Envelopes, LFOs and filters are not supported by the current instrument." * InstrumentTuningView: Message display "Enables the use of global transposition" Increase the font size to 12 pixels in the mixer channel view, i.e. the display of the channel name. Render messages in system font size in the following areas because there should be enough space for almost all sizes: * Automation editor: Message display "Please open an automation clip by double-clicking on it!" * Piano roll: Message display "Please open a clip by double-clicking on it!" Use the application font for the line edit that can be used to change the instrument name. Remove overrides which explicitly set the font size for LED check boxes in: * EnvelopeAndLfoView: Labels "FREQ x 100" and "MODULATE ENV AMOUNT" Remove overrides which explicitly set the font size for combo boxes in: * InstrumentSoundShapingView: Filter combo box ## Adjust font sizes in widgets Adjust the font sizes in the area of the custom GUI widgets. Increase and unify the pixel font size to 10 pixels in the following classes: * `ComboBox` * `GroupBox` * `Knob` * `LcdFloatSpinBox` * `LcdWidget` * `LedCheckBox` * `Oscilloscope`: Display of "Click to enable" * `TabWidget` Shorten the text in `EnvelopeAndLfoView` from "MODULATE ENV AMOUNT" to "MOD ENV AMOUNT" to make it fit with the new font size of `LedCheckBox`. Remove the setting of the font size in pixels from `MeterDialog` because it's displayed in a layout and can accommodate all font sizes. Note: the dialog can be triggered from a LADSPA plugin with tempo sync, e.g. "Allpass delay line". Right click on the time parameter and select "Tempo Sync > Custom..." from the context menu. Remove the setting of the font size in `TabBar` as none of the added `TabButton` instances displays text in the first place. Remove the setting of the font size in `TabWidget::addTab` because the font size is already set in the constructor. It would be an unexpected size effect of setting a tab anyway. Remove a duplicate call to setting the font size in `TabWidget::paintEvent`. Remove unnecessary includes of `gui_templates.h` wherever this is possible now. ## Direct use of setPixelSize Directly use `setPixelSize` when drawing the "Note Velocity" and "Note Panning" strings as they will likely never be drawn using point sizes. --- include/gui_templates.h | 24 ++++--------------- .../AudioFileProcessorView.cpp | 3 +-- .../AudioFileProcessorWaveView.cpp | 2 +- plugins/CarlaBase/Carla.cpp | 4 ++-- .../DualFilter/DualFilterControlDialog.cpp | 3 --- plugins/LadspaBrowser/LadspaBrowser.cpp | 2 -- plugins/Monstro/Monstro.cpp | 6 ----- plugins/Patman/Patman.cpp | 4 ++-- plugins/Stk/Mallets/Mallets.cpp | 2 -- plugins/Vestige/Vestige.cpp | 9 +++---- plugins/VstEffect/VstEffectControlDialog.cpp | 2 +- plugins/ZynAddSubFx/ZynAddSubFx.cpp | 6 +++-- src/gui/EffectView.cpp | 4 ++-- src/gui/Lv2ViewBase.cpp | 2 +- src/gui/MixerChannelView.cpp | 2 +- src/gui/editors/AutomationEditor.cpp | 7 +++--- src/gui/editors/PianoRoll.cpp | 13 +++++----- src/gui/instrument/EnvelopeAndLfoView.cpp | 7 ++---- .../instrument/InstrumentFunctionViews.cpp | 9 +++---- src/gui/instrument/InstrumentMidiIOView.cpp | 2 +- .../instrument/InstrumentSoundShapingView.cpp | 4 ++-- src/gui/instrument/InstrumentTrackWindow.cpp | 2 -- src/gui/instrument/InstrumentTuningView.cpp | 2 +- src/gui/instrument/PianoView.cpp | 2 +- src/gui/widgets/ComboBox.cpp | 2 +- src/gui/widgets/GroupBox.cpp | 2 +- src/gui/widgets/Knob.cpp | 7 +++--- src/gui/widgets/LcdFloatSpinBox.cpp | 2 +- src/gui/widgets/LcdWidget.cpp | 4 ++-- src/gui/widgets/LedCheckBox.cpp | 4 ++-- src/gui/widgets/MeterDialog.cpp | 3 --- src/gui/widgets/Oscilloscope.cpp | 2 +- src/gui/widgets/TabBar.cpp | 3 --- src/gui/widgets/TabWidget.cpp | 7 ++---- 34 files changed, 59 insertions(+), 100 deletions(-) diff --git a/include/gui_templates.h b/include/gui_templates.h index 4833c6cdf..bbb5f80da 100644 --- a/include/gui_templates.h +++ b/include/gui_templates.h @@ -25,35 +25,19 @@ #ifndef LMMS_GUI_TEMPLATES_H #define LMMS_GUI_TEMPLATES_H -#include "lmmsconfig.h" - -#include #include #include -#include - -// TODO: remove once qt5 support is dropped -#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) - #include -#endif namespace lmms::gui { - -// return DPI-independent font-size - font with returned font-size has always -// the same size in pixels -inline QFont pointSize(QFont fontPointer, float fontSize) +// Convenience method to set the font size in pixels +inline QFont adjustedToPixelSize(QFont font, int size) { - // to calculate DPI of a screen to make it HiDPI ready - qreal devicePixelRatio = QGuiApplication::primaryScreen()->devicePixelRatio(); - qreal scaleFactor = std::max(devicePixelRatio, 1.0); // Ensure scaleFactor is at least 1.0 - - fontPointer.setPointSizeF(fontSize * scaleFactor); - return fontPointer; + font.setPixelSize(size); + return font; } - } // namespace lmms::gui #endif // LMMS_GUI_TEMPLATES_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp index 94f0da4fb..b7d5802dc 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp @@ -134,7 +134,6 @@ AudioFileProcessorView::AudioFileProcessorView(Instrument* instrument, // interpolation selector m_interpBox = new ComboBox(this); m_interpBox->setGeometry(142, 62, 82, ComboBox::DEFAULT_HEIGHT); - m_interpBox->setFont(pointSize(m_interpBox->font(), 8)); // wavegraph m_waveView = 0; @@ -228,7 +227,7 @@ void AudioFileProcessorView::paintEvent(QPaintEvent*) int idx = a->sample().sampleFile().length(); - p.setFont(pointSize(font(), 8)); + p.setFont(adjustedToPixelSize(font(), 8)); QFontMetrics fm(p.font()); diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index 89e328972..1742ee3a7 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -273,7 +273,7 @@ void AudioFileProcessorWaveView::paintEvent(QPaintEvent * pe) p.fillRect(s_padding, s_padding, m_graph.width(), 14, g); p.setPen(QColor(255, 255, 255)); - p.setFont(pointSize(font(), 8)); + p.setFont(adjustedToPixelSize(font(), 8)); QString length_text; const int length = m_sample->sampleDuration().count(); diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index cb52fe25e..c95a965c9 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -632,7 +632,7 @@ CarlaInstrumentView::CarlaInstrumentView(CarlaInstrument* const instrument, QWid m_toggleUIButton->setCheckable( true ); m_toggleUIButton->setChecked( false ); m_toggleUIButton->setIcon( embed::getIconPixmap( "zoom" ) ); - m_toggleUIButton->setFont(pointSize(m_toggleUIButton->font(), 8)); + m_toggleUIButton->setFont(adjustedToPixelSize(m_toggleUIButton->font(), 8)); connect( m_toggleUIButton, SIGNAL( clicked(bool) ), this, SLOT( toggleUI( bool ) ) ); m_toggleUIButton->setToolTip( @@ -642,7 +642,7 @@ CarlaInstrumentView::CarlaInstrumentView(CarlaInstrument* const instrument, QWid m_toggleParamsWindowButton = new QPushButton(tr("Params"), this); m_toggleParamsWindowButton->setIcon(embed::getIconPixmap("controller")); m_toggleParamsWindowButton->setCheckable(true); - m_toggleParamsWindowButton->setFont(pointSize(m_toggleParamsWindowButton->font(), 8)); + m_toggleParamsWindowButton->setFont(adjustedToPixelSize(m_toggleParamsWindowButton->font(), 8)); #if CARLA_VERSION_HEX < CARLA_MIN_PARAM_VERSION m_toggleParamsWindowButton->setEnabled(false); m_toggleParamsWindowButton->setToolTip(tr("Available from Carla version 2.1 and up.")); diff --git a/plugins/DualFilter/DualFilterControlDialog.cpp b/plugins/DualFilter/DualFilterControlDialog.cpp index 5a912ac85..a674a4a42 100644 --- a/plugins/DualFilter/DualFilterControlDialog.cpp +++ b/plugins/DualFilter/DualFilterControlDialog.cpp @@ -29,7 +29,6 @@ #include "Knob.h" #include "LedCheckBox.h" #include "ComboBox.h" -#include "gui_templates.h" namespace lmms::gui { @@ -76,12 +75,10 @@ DualFilterControlDialog::DualFilterControlDialog( DualFilterControls* controls ) auto m_filter1ComboBox = new ComboBox(this); m_filter1ComboBox->setGeometry( 19, 70, 137, ComboBox::DEFAULT_HEIGHT ); - m_filter1ComboBox->setFont(pointSize(m_filter1ComboBox->font(), 8)); m_filter1ComboBox->setModel( &controls->m_filter1Model ); auto m_filter2ComboBox = new ComboBox(this); m_filter2ComboBox->setGeometry( 217, 70, 137, ComboBox::DEFAULT_HEIGHT ); - m_filter2ComboBox->setFont(pointSize(m_filter2ComboBox->font(), 8)); m_filter2ComboBox->setModel( &controls->m_filter2Model ); } diff --git a/plugins/LadspaBrowser/LadspaBrowser.cpp b/plugins/LadspaBrowser/LadspaBrowser.cpp index 54d019aad..e6a31e15a 100644 --- a/plugins/LadspaBrowser/LadspaBrowser.cpp +++ b/plugins/LadspaBrowser/LadspaBrowser.cpp @@ -32,7 +32,6 @@ #include -#include "gui_templates.h" #include "LadspaDescription.h" #include "LadspaPortDialog.h" #include "TabBar.h" @@ -172,7 +171,6 @@ QWidget * LadspaBrowserView::createTab( QWidget * _parent, const QString & _txt, auto title = new QLabel(type + _txt, tab); QFont f = title->font(); f.setBold( true ); - title->setFont(pointSize(f, 12)); layout->addSpacing( 5 ); layout->addWidget( title ); diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index 76ab6e477..3de9fbce6 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -30,7 +30,6 @@ #include "ComboBox.h" #include "Engine.h" #include "InstrumentTrack.h" -#include "gui_templates.h" #include "lmms_math.h" #include "interpolation.h" @@ -1694,7 +1693,6 @@ QWidget * MonstroView::setupOperatorsView( QWidget * _parent ) m_osc2WaveBox = new ComboBox( view ); m_osc2WaveBox -> setGeometry( 204, O2ROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_osc2WaveBox->setFont(pointSize(m_osc2WaveBox->font(), 8)); maketinyled( m_osc2SyncHButton, 212, O2ROW - 3, tr( "Hard sync oscillator 2" ) ) maketinyled( m_osc2SyncRButton, 191, O2ROW - 3, tr( "Reverse sync oscillator 2" ) ) @@ -1709,18 +1707,15 @@ QWidget * MonstroView::setupOperatorsView( QWidget * _parent ) m_osc3Wave1Box = new ComboBox( view ); m_osc3Wave1Box -> setGeometry( 160, O3ROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_osc3Wave1Box->setFont(pointSize(m_osc3Wave1Box->font(), 8)); m_osc3Wave2Box = new ComboBox( view ); m_osc3Wave2Box -> setGeometry( 204, O3ROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_osc3Wave2Box->setFont(pointSize(m_osc3Wave2Box->font(), 8)); maketinyled( m_osc3SyncHButton, 212, O3ROW - 3, tr( "Hard sync oscillator 3" ) ) maketinyled( m_osc3SyncRButton, 191, O3ROW - 3, tr( "Reverse sync oscillator 3" ) ) m_lfo1WaveBox = new ComboBox( view ); m_lfo1WaveBox -> setGeometry( 2, LFOROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_lfo1WaveBox->setFont(pointSize(m_lfo1WaveBox->font(), 8)); maketsknob( m_lfo1AttKnob, LFOCOL1, LFOROW, tr( "Attack" ), " ms", "lfoKnob" ) maketsknob( m_lfo1RateKnob, LFOCOL2, LFOROW, tr( "Rate" ), " ms", "lfoKnob" ) @@ -1728,7 +1723,6 @@ QWidget * MonstroView::setupOperatorsView( QWidget * _parent ) m_lfo2WaveBox = new ComboBox( view ); m_lfo2WaveBox -> setGeometry( 127, LFOROW + 7, 42, ComboBox::DEFAULT_HEIGHT ); - m_lfo2WaveBox->setFont(pointSize(m_lfo2WaveBox->font(), 8)); maketsknob(m_lfo2AttKnob, LFOCOL4, LFOROW, tr("Attack"), " ms", "lfoKnob") maketsknob(m_lfo2RateKnob, LFOCOL5, LFOROW, tr("Rate"), " ms", "lfoKnob") diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index a50363777..d2f4aee4e 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -545,7 +545,7 @@ void PatmanView::updateFilename() m_displayFilename = ""; int idx = m_pi->m_patchFile.length(); - QFontMetrics fm(pointSize(font(), 8)); + QFontMetrics fm(adjustedToPixelSize(font(), 8)); // simple algorithm for creating a text from the filename that // matches in the white rectangle @@ -615,7 +615,7 @@ void PatmanView::paintEvent( QPaintEvent * ) { QPainter p( this ); - p.setFont(pointSize(font() ,8)); + p.setFont(adjustedToPixelSize(font() ,8)); p.drawText( 8, 116, 235, 16, Qt::AlignLeft | Qt::TextSingleLine | Qt::AlignVCenter, m_displayFilename ); diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index 1d7cbd86b..3fb7fc0ff 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -37,7 +37,6 @@ #include "AudioEngine.h" #include "ConfigManager.h" #include "Engine.h" -#include "gui_templates.h" #include "GuiApplication.h" #include "InstrumentTrack.h" @@ -450,7 +449,6 @@ MalletsInstrumentView::MalletsInstrumentView( MalletsInstrument * _instrument, m_presetsCombo = new ComboBox( this, tr( "Instrument" ) ); m_presetsCombo->setGeometry( 140, 50, 99, ComboBox::DEFAULT_HEIGHT ); - m_presetsCombo->setFont(pointSize(m_presetsCombo->font(), 8)); connect( &_instrument->m_presetsModel, SIGNAL( dataChanged() ), this, SLOT( changePreset() ) ); diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index 552b1f3ff..834b583ed 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -583,11 +583,12 @@ VestigeInstrumentView::VestigeInstrumentView( Instrument * _instrument, m_selPresetButton->setMenu(menu); + constexpr int buttonFontSize = 12; m_toggleGUIButton = new QPushButton( tr( "Show/hide GUI" ), this ); m_toggleGUIButton->setGeometry( 20, 130, 200, 24 ); m_toggleGUIButton->setIcon( embed::getIconPixmap( "zoom" ) ); - m_toggleGUIButton->setFont(pointSize(m_toggleGUIButton->font(), 8)); + m_toggleGUIButton->setFont(adjustedToPixelSize(m_toggleGUIButton->font(), buttonFontSize)); connect( m_toggleGUIButton, SIGNAL( clicked() ), this, SLOT( toggleGUI() ) ); @@ -596,7 +597,7 @@ VestigeInstrumentView::VestigeInstrumentView( Instrument * _instrument, this); note_off_all_btn->setGeometry( 20, 160, 200, 24 ); note_off_all_btn->setIcon( embed::getIconPixmap( "stop" ) ); - note_off_all_btn->setFont(pointSize(note_off_all_btn->font(), 8)); + note_off_all_btn->setFont(adjustedToPixelSize(note_off_all_btn->font(), buttonFontSize)); connect( note_off_all_btn, SIGNAL( clicked() ), this, SLOT( noteOffAll() ) ); @@ -881,7 +882,7 @@ void VestigeInstrumentView::paintEvent( QPaintEvent * ) tr( "No VST plugin loaded" ); QFont f = p.font(); f.setBold( true ); - p.setFont(pointSize(f, 10)); + p.setFont(adjustedToPixelSize(f, 10)); p.setPen( QColor( 255, 255, 255 ) ); p.drawText( 10, 100, plugin_name ); @@ -893,7 +894,7 @@ void VestigeInstrumentView::paintEvent( QPaintEvent * ) { p.setPen( QColor( 0, 0, 0 ) ); f.setBold( false ); - p.setFont(pointSize(f, 8)); + p.setFont(adjustedToPixelSize(f, 8)); p.drawText( 10, 114, tr( "by " ) + m_vi->m_plugin->vendorString() ); p.setPen( QColor( 255, 255, 255 ) ); diff --git a/plugins/VstEffect/VstEffectControlDialog.cpp b/plugins/VstEffect/VstEffectControlDialog.cpp index 671eef561..0fb4913a3 100644 --- a/plugins/VstEffect/VstEffectControlDialog.cpp +++ b/plugins/VstEffect/VstEffectControlDialog.cpp @@ -246,7 +246,7 @@ VstEffectControlDialog::VstEffectControlDialog( VstEffectControls * _ctl ) : tb->addWidget(space1); tbLabel = new QLabel( tr( "Effect by: " ), this ); - tbLabel->setFont(pointSize(f, 7)); + tbLabel->setFont(adjustedToPixelSize(f, 7)); tbLabel->setTextFormat(Qt::RichText); tbLabel->setAlignment( Qt::AlignTop | Qt::AlignLeft ); tb->addWidget( tbLabel ); diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index 4988e1b8b..c406e04e0 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -40,7 +40,6 @@ #include "DataFile.h" #include "InstrumentPlayHandle.h" #include "InstrumentTrack.h" -#include "gui_templates.h" #include "Song.h" #include "StringPairDrag.h" #include "RemoteZynAddSubFx.h" @@ -541,7 +540,10 @@ ZynAddSubFxView::ZynAddSubFxView( Instrument * _instrument, QWidget * _parent ) m_toggleUIButton->setCheckable( true ); m_toggleUIButton->setChecked( false ); m_toggleUIButton->setIcon( embed::getIconPixmap( "zoom" ) ); - m_toggleUIButton->setFont(pointSize(m_toggleUIButton->font(), 8)); + QFont f = m_toggleUIButton->font(); + f.setPointSizeF(12); + m_toggleUIButton->setFont(f); + connect( m_toggleUIButton, SIGNAL( toggled( bool ) ), this, SLOT( toggleUI() ) ); diff --git a/src/gui/EffectView.cpp b/src/gui/EffectView.cpp index cbe2e4e95..76010232a 100644 --- a/src/gui/EffectView.cpp +++ b/src/gui/EffectView.cpp @@ -90,7 +90,7 @@ EffectView::EffectView( Effect * _model, QWidget * _parent ) : { auto ctls_btn = new QPushButton(tr("Controls"), this); QFont f = ctls_btn->font(); - ctls_btn->setFont(pointSize(f, 8)); + ctls_btn->setFont(adjustedToPixelSize(f, 10)); ctls_btn->setGeometry( 150, 14, 50, 20 ); connect( ctls_btn, SIGNAL(clicked()), this, SLOT(editControls())); @@ -257,7 +257,7 @@ void EffectView::paintEvent( QPaintEvent * ) QPainter p( this ); p.drawPixmap( 0, 0, m_bg ); - QFont f = pointSize(font(), 7.5f); + QFont f = adjustedToPixelSize(font(), 10); f.setBold( true ); p.setFont( f ); diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 6de47f450..4fcf6b77b 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -157,7 +157,7 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) : m_toggleUIButton->setCheckable(true); m_toggleUIButton->setChecked(false); m_toggleUIButton->setIcon(embed::getIconPixmap("zoom")); - m_toggleUIButton->setFont(pointSize(m_toggleUIButton->font(), 8)); + m_toggleUIButton->setFont(adjustedToPixelSize(m_toggleUIButton->font(), 8)); btnBox->addWidget(m_toggleUIButton, 0); } btnBox->addStretch(1); diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index 9b43991d3..8a1ed2e8f 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -76,7 +76,7 @@ namespace lmms::gui m_renameLineEdit = new QLineEdit{mixerName, nullptr}; m_renameLineEdit->setFixedWidth(65); - m_renameLineEdit->setFont(pointSize(font(), 7.5f)); + m_renameLineEdit->setFont(adjustedToPixelSize(font(), 12)); m_renameLineEdit->setReadOnly(true); m_renameLineEdit->installEventFilter(this); diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index d23682d6e..46521b3f0 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1040,8 +1040,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) QBrush bgColor = p.background(); p.fillRect( 0, 0, width(), height(), bgColor ); - // set font-size to 8 - p.setFont(pointSize(p.font(), 8)); + p.setFont(adjustedToPixelSize(p.font(), 10)); int grid_height = height() - TOP_MARGIN - SCROLLBAR_SIZE; @@ -1383,9 +1382,9 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } else { - QFont f = p.font(); + QFont f = font(); f.setBold( true ); - p.setFont(pointSize(f, 14)); + p.setFont(f); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::BrightText ) ); p.drawText( VALUES_WIDTH + 20, TOP_MARGIN + 40, diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 8623b5da7..3b9273ade 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -59,7 +59,6 @@ #include "DetuningHelper.h" #include "embed.h" #include "GuiApplication.h" -#include "gui_templates.h" #include "InstrumentTrack.h" #include "MainWindow.h" #include "MidiClip.h" @@ -3335,9 +3334,9 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) m_whiteKeyWidth, noteEditBottom() - keyAreaBottom()), bgColor); // display note editing info - //QFont f = p.font(); - f.setBold( false ); - p.setFont(pointSize(f, 10)); + f.setBold(false); + f.setPixelSize(10); + p.setFont(f); p.setPen(m_noteModeColor); p.drawText( QRect( 0, keyAreaBottom(), m_whiteKeyWidth, noteEditBottom() - keyAreaBottom()), @@ -3598,9 +3597,9 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) } else { - QFont f = p.font(); - f.setBold( true ); - p.setFont(pointSize(f, 14)); + QFont f = font(); + f.setBold(true); + p.setFont(f); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::BrightText ) ); p.drawText(m_whiteKeyWidth + 20, PR_TOP_MARGIN + 40, diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index 4043ea229..7f30f3ac1 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -207,15 +207,12 @@ EnvelopeAndLfoView::EnvelopeAndLfoView( QWidget * _parent ) : m_lfoWaveBtnGrp->addButton( random_lfo_btn ); m_x100Cb = new LedCheckBox( tr( "FREQ x 100" ), this ); - m_x100Cb->setFont(pointSize(m_x100Cb->font(), 6.5)); m_x100Cb->move( LFO_PREDELAY_KNOB_X, LFO_GRAPH_Y + 36 ); m_x100Cb->setToolTip(tr("Multiply LFO frequency by 100")); - m_controlEnvAmountCb = new LedCheckBox( tr( "MODULATE ENV AMOUNT" ), - this ); + m_controlEnvAmountCb = new LedCheckBox(tr("MOD ENV AMOUNT"), this); m_controlEnvAmountCb->move( LFO_PREDELAY_KNOB_X, LFO_GRAPH_Y + 54 ); - m_controlEnvAmountCb->setFont(pointSize(m_controlEnvAmountCb->font(), 6.5)); m_controlEnvAmountCb->setToolTip( tr( "Control envelope amount by this LFO" ) ); @@ -340,7 +337,7 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) // draw LFO-graph p.drawPixmap(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph); - p.setFont(pointSize(p.font(), 8)); + p.setFont(adjustedToPixelSize(p.font(), 8)); const float gray_amount = 1.0f - fabsf( m_amountKnob->value() ); diff --git a/src/gui/instrument/InstrumentFunctionViews.cpp b/src/gui/instrument/InstrumentFunctionViews.cpp index ea1648600..ad8abe735 100644 --- a/src/gui/instrument/InstrumentFunctionViews.cpp +++ b/src/gui/instrument/InstrumentFunctionViews.cpp @@ -57,7 +57,7 @@ InstrumentFunctionNoteStackingView::InstrumentFunctionNoteStackingView( Instrume mainLayout->setVerticalSpacing( 1 ); auto chordLabel = new QLabel(tr("Chord:")); - chordLabel->setFont(pointSize(chordLabel->font(), 8)); + chordLabel->setFont(adjustedToPixelSize(chordLabel->font(), 10)); m_chordRangeKnob->setLabel( tr( "RANGE" ) ); m_chordRangeKnob->setHintText( tr( "Chord range:" ), " " + tr( "octave(s)" ) ); @@ -145,14 +145,15 @@ InstrumentFunctionArpeggioView::InstrumentFunctionArpeggioView( InstrumentFuncti m_arpGateKnob->setLabel( tr( "GATE" ) ); m_arpGateKnob->setHintText( tr( "Arpeggio gate:" ), tr( "%" ) ); + constexpr int labelFontSize = 10; auto arpChordLabel = new QLabel(tr("Chord:")); - arpChordLabel->setFont(pointSize(arpChordLabel->font(), 8)); + arpChordLabel->setFont(adjustedToPixelSize(arpChordLabel->font(), labelFontSize)); auto arpDirectionLabel = new QLabel(tr("Direction:")); - arpDirectionLabel->setFont(pointSize(arpDirectionLabel->font(), 8)); + arpDirectionLabel->setFont(adjustedToPixelSize(arpDirectionLabel->font(), labelFontSize)); auto arpModeLabel = new QLabel(tr("Mode:")); - arpModeLabel->setFont(pointSize(arpModeLabel->font(), 8)); + arpModeLabel->setFont(adjustedToPixelSize(arpModeLabel->font(), labelFontSize)); mainLayout->addWidget( arpChordLabel, 0, 0 ); mainLayout->addWidget( m_arpComboBox, 1, 0 ); diff --git a/src/gui/instrument/InstrumentMidiIOView.cpp b/src/gui/instrument/InstrumentMidiIOView.cpp index 1e95751ea..f5f4ae095 100644 --- a/src/gui/instrument/InstrumentMidiIOView.cpp +++ b/src/gui/instrument/InstrumentMidiIOView.cpp @@ -155,7 +155,7 @@ InstrumentMidiIOView::InstrumentMidiIOView( QWidget* parent ) : auto baseVelocityHelp = new QLabel(tr("Specify the velocity normalization base for MIDI-based instruments at 100% note velocity.")); baseVelocityHelp->setWordWrap( true ); - baseVelocityHelp->setFont(pointSize(baseVelocityHelp->font(), 8)); + baseVelocityHelp->setFont(adjustedToPixelSize(baseVelocityHelp->font(), 10)); baseVelocityLayout->addWidget( baseVelocityHelp ); diff --git a/src/gui/instrument/InstrumentSoundShapingView.cpp b/src/gui/instrument/InstrumentSoundShapingView.cpp index 59df3097c..b96db8495 100644 --- a/src/gui/instrument/InstrumentSoundShapingView.cpp +++ b/src/gui/instrument/InstrumentSoundShapingView.cpp @@ -77,7 +77,6 @@ InstrumentSoundShapingView::InstrumentSoundShapingView( QWidget * _parent ) : m_filterComboBox = new ComboBox( m_filterGroupBox ); m_filterComboBox->setGeometry( 14, 22, 120, ComboBox::DEFAULT_HEIGHT ); - m_filterComboBox->setFont(pointSize(m_filterComboBox->font(), 8)); m_filterCutKnob = new Knob( KnobType::Bright26, m_filterGroupBox ); @@ -94,7 +93,8 @@ InstrumentSoundShapingView::InstrumentSoundShapingView( QWidget * _parent ) : m_singleStreamInfoLabel = new QLabel( tr( "Envelopes, LFOs and filters are not supported by the current instrument." ), this ); m_singleStreamInfoLabel->setWordWrap( true ); - m_singleStreamInfoLabel->setFont(pointSize(m_singleStreamInfoLabel->font(), 8)); + // TODO Could also be rendered in system font size... + m_singleStreamInfoLabel->setFont(adjustedToPixelSize(m_singleStreamInfoLabel->font(), 10)); m_singleStreamInfoLabel->setGeometry( TARGETS_TABWIDGET_X, TARGETS_TABWIDGET_Y, diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index 5387346a9..86d9086c8 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -44,7 +44,6 @@ #include "GroupBox.h" #include "MixerChannelLcdSpinBox.h" #include "GuiApplication.h" -#include "gui_templates.h" #include "Instrument.h" #include "InstrumentFunctions.h" #include "InstrumentFunctionViews.h" @@ -103,7 +102,6 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : // setup line edit for changing instrument track name m_nameLineEdit = new QLineEdit; - m_nameLineEdit->setFont(pointSize(m_nameLineEdit->font(), 9)); connect( m_nameLineEdit, SIGNAL( textChanged( const QString& ) ), this, SLOT( textChanged( const QString& ) ) ); diff --git a/src/gui/instrument/InstrumentTuningView.cpp b/src/gui/instrument/InstrumentTuningView.cpp index 41c18213b..daa361aad 100644 --- a/src/gui/instrument/InstrumentTuningView.cpp +++ b/src/gui/instrument/InstrumentTuningView.cpp @@ -60,7 +60,7 @@ InstrumentTuningView::InstrumentTuningView(InstrumentTrack *it, QWidget *parent) auto tlabel = new QLabel(tr("Enables the use of global transposition")); tlabel->setWordWrap(true); - tlabel->setFont(pointSize(tlabel->font(), 8)); + tlabel->setFont(adjustedToPixelSize(tlabel->font(), 10)); masterPitchLayout->addWidget(tlabel); // Microtuner settings diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index 87ee6af9b..13628d97e 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -807,7 +807,7 @@ void PianoView::paintEvent( QPaintEvent * ) QPainter p( this ); // set smaller font for printing number of every octave - p.setFont(pointSize(p.font(), LABEL_TEXT_SIZE)); + p.setFont(adjustedToPixelSize(p.font(), LABEL_TEXT_SIZE)); // draw bar above the keyboard (there will be the labels diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index 6e5eff58b..eb019876a 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -53,7 +53,7 @@ ComboBox::ComboBox( QWidget * _parent, const QString & _name ) : { setFixedHeight( ComboBox::DEFAULT_HEIGHT ); - setFont(pointSize(font(), 9)); + setFont(adjustedToPixelSize(font(), 10)); connect( &m_menu, SIGNAL(triggered(QAction*)), this, SLOT(setItem(QAction*))); diff --git a/src/gui/widgets/GroupBox.cpp b/src/gui/widgets/GroupBox.cpp index 229ab13cd..e7d78acb9 100644 --- a/src/gui/widgets/GroupBox.cpp +++ b/src/gui/widgets/GroupBox.cpp @@ -111,7 +111,7 @@ void GroupBox::paintEvent( QPaintEvent * pe ) // draw text p.setPen( palette().color( QPalette::Active, QPalette::Text ) ); - p.setFont(pointSize(font(), 8)); + p.setFont(adjustedToPixelSize(font(), 10)); int const captionX = ledButtonShown() ? 22 : 6; p.drawText(captionX, m_titleBarHeight, m_caption); diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index a6411d6cf..d282f72c2 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -139,7 +139,7 @@ void Knob::setLabel( const QString & txt ) if( m_knobPixmap ) { setFixedSize(qMax( m_knobPixmap->width(), - horizontalAdvance(QFontMetrics(pointSize(font(), 6.5)), m_label)), + horizontalAdvance(QFontMetrics(adjustedToPixelSize(font(), 10)), m_label)), m_knobPixmap->height() + 10); } @@ -459,7 +459,7 @@ void Knob::paintEvent( QPaintEvent * _me ) { if (!m_isHtmlLabel) { - p.setFont(pointSize(p.font(), 6.5f)); + p.setFont(adjustedToPixelSize(p.font(), 10)); p.setPen(textColor()); p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2, @@ -467,7 +467,8 @@ void Knob::paintEvent( QPaintEvent * _me ) } else { - m_tdRenderer->setDefaultFont(pointSize(p.font(), 6.5f)); + // TODO setHtmlLabel is never called so this will never be executed. Remove functionality? + m_tdRenderer->setDefaultFont(adjustedToPixelSize(p.font(), 10)); p.translate((width() - m_tdRenderer->idealWidth()) / 2, (height() - m_tdRenderer->pageSize().height()) / 2); m_tdRenderer->drawContents(&p); } diff --git a/src/gui/widgets/LcdFloatSpinBox.cpp b/src/gui/widgets/LcdFloatSpinBox.cpp index 667a03481..c71d66568 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -245,7 +245,7 @@ void LcdFloatSpinBox::paintEvent(QPaintEvent*) // Label if (!m_label.isEmpty()) { - p.setFont(pointSize(p.font(), 6.5f)); + p.setFont(adjustedToPixelSize(p.font(), 10)); p.setPen(m_wholeDisplay.textShadowColor()); p.drawText(width() / 2 - p.fontMetrics().boundingRect(m_label).width() / 2 + 1, height(), m_label); p.setPen(m_wholeDisplay.textColor()); diff --git a/src/gui/widgets/LcdWidget.cpp b/src/gui/widgets/LcdWidget.cpp index b30195d6c..7370a939f 100644 --- a/src/gui/widgets/LcdWidget.cpp +++ b/src/gui/widgets/LcdWidget.cpp @@ -209,7 +209,7 @@ void LcdWidget::paintEvent( QPaintEvent* ) // Label if( !m_label.isEmpty() ) { - p.setFont(pointSize(p.font(), 6.5f)); + p.setFont(adjustedToPixelSize(p.font(), 10)); p.setPen( textShadowColor() ); p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2 + 1, @@ -261,7 +261,7 @@ void LcdWidget::updateSize() setFixedSize( qMax( m_cellWidth * m_numDigits + marginX1 + marginX2, - horizontalAdvance(QFontMetrics(pointSize(font(), 6.5f)), m_label) + horizontalAdvance(QFontMetrics(adjustedToPixelSize(font(), 10)), m_label) ), m_cellHeight + (2 * marginY) + 9 ); diff --git a/src/gui/widgets/LedCheckBox.cpp b/src/gui/widgets/LedCheckBox.cpp index 3cb85deff..42e49a8ae 100644 --- a/src/gui/widgets/LedCheckBox.cpp +++ b/src/gui/widgets/LedCheckBox.cpp @@ -92,7 +92,7 @@ void LedCheckBox::initUi( LedColor _color ) m_ledOnPixmap = embed::getIconPixmap(names[static_cast(_color)].toUtf8().constData()); m_ledOffPixmap = embed::getIconPixmap("led_off"); - if (m_legacyMode){ setFont(pointSize(font(), 7)); } + if (m_legacyMode){ setFont(adjustedToPixelSize(font(), 10)); } setText( m_text ); } @@ -113,7 +113,7 @@ void LedCheckBox::onTextUpdated() void LedCheckBox::paintLegacy(QPaintEvent * pe) { QPainter p( this ); - p.setFont(pointSize(font(), 7)); + p.setFont(adjustedToPixelSize(font(), 10)); p.drawPixmap(0, 0, model()->value() ? m_ledOnPixmap : m_ledOffPixmap); diff --git a/src/gui/widgets/MeterDialog.cpp b/src/gui/widgets/MeterDialog.cpp index eb8e54353..d01dca9a8 100644 --- a/src/gui/widgets/MeterDialog.cpp +++ b/src/gui/widgets/MeterDialog.cpp @@ -30,7 +30,6 @@ #include "MeterDialog.h" #include "MeterModel.h" -#include "gui_templates.h" #include "LcdSpinBox.h" namespace lmms::gui @@ -60,7 +59,6 @@ MeterDialog::MeterDialog( QWidget * _parent, bool _simple ) : { auto num_label = new QLabel(tr("Meter Numerator"), num); QFont f = num_label->font(); - num_label->setFont(pointSize(f, 7)); num_layout->addSpacing( 5 ); num_layout->addWidget( num_label ); } @@ -84,7 +82,6 @@ MeterDialog::MeterDialog( QWidget * _parent, bool _simple ) : { auto den_label = new QLabel(tr("Meter Denominator"), den); QFont f = den_label->font(); - den_label->setFont(pointSize(f, 7)); den_layout->addSpacing( 5 ); den_layout->addWidget( den_label ); } diff --git a/src/gui/widgets/Oscilloscope.cpp b/src/gui/widgets/Oscilloscope.cpp index bd944937a..8c342cc34 100644 --- a/src/gui/widgets/Oscilloscope.cpp +++ b/src/gui/widgets/Oscilloscope.cpp @@ -203,7 +203,7 @@ void Oscilloscope::paintEvent( QPaintEvent * ) else { p.setPen( QColor( 192, 192, 192 ) ); - p.setFont(pointSize(p.font(), 7)); + p.setFont(adjustedToPixelSize(p.font(), 10)); p.drawText( 6, height()-5, tr( "Click to enable" ) ); } } diff --git a/src/gui/widgets/TabBar.cpp b/src/gui/widgets/TabBar.cpp index ce706d5f8..d77573a92 100644 --- a/src/gui/widgets/TabBar.cpp +++ b/src/gui/widgets/TabBar.cpp @@ -25,7 +25,6 @@ #include "TabBar.h" #include "TabButton.h" -#include "gui_templates.h" namespace lmms::gui @@ -90,8 +89,6 @@ TabButton * TabBar::addTab( QWidget * _w, const QString & _text, int _id, _w->setFixedSize( _w->parentWidget()->size() ); } - b->setFont(pointSize(b->font(), 8)); - return( b ); } diff --git a/src/gui/widgets/TabWidget.cpp b/src/gui/widgets/TabWidget.cpp index 2c93dba3e..a370c1ea9 100644 --- a/src/gui/widgets/TabWidget.cpp +++ b/src/gui/widgets/TabWidget.cpp @@ -58,7 +58,7 @@ TabWidget::TabWidget(const QString& caption, QWidget* parent, bool usePixmap, m_tabheight = caption.isEmpty() ? m_tabbarHeight - 3 : m_tabbarHeight - 4; - setFont(pointSize(font(), 8)); + setFont(adjustedToPixelSize(font(), 10)); setAutoFillBackground(true); QColor bg_color = QApplication::palette().color(QPalette::Active, QPalette::Window).darker(132); @@ -70,8 +70,6 @@ TabWidget::TabWidget(const QString& caption, QWidget* parent, bool usePixmap, void TabWidget::addTab(QWidget* w, const QString& name, const char* pixmap, int idx) { - setFont(pointSize(font(), 8)); - // Append tab when position is not given if (idx < 0/* || m_widgets.contains(idx) == true*/) { @@ -216,7 +214,7 @@ void TabWidget::resizeEvent(QResizeEvent*) void TabWidget::paintEvent(QPaintEvent* pe) { QPainter p(this); - p.setFont(pointSize(font(), 7)); + p.setFont(adjustedToPixelSize(font(), 10)); // Draw background QBrush bg_color = p.background(); @@ -232,7 +230,6 @@ void TabWidget::paintEvent(QPaintEvent* pe) // Draw title, if any if (!m_caption.isEmpty()) { - p.setFont(pointSize(p.font(), 8)); p.setPen(tabTitleText()); p.drawText(5, 11, m_caption); } From d447cb0648cff249f0f4c2311ea17217576354e4 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Fri, 5 Apr 2024 11:25:39 +0200 Subject: [PATCH 130/191] Use layouts for the instrument sound shaping tab / Extract classes for Envelope and LFO graphs (#7193) Move the envelope and LFO graphs into their own classes. Besides the improved code organization this step had to be done to be able to use layouts in `EnvelopeAndLfoView`. The class previously had fixed layouts mixed with custom rendering in the paint event. Mouse events are now also handled in both new classes instead of in `EnvelopeAndLfoView`. ## Layouts in EnvelopeAndLfoView Use layouts to align the elements of the `EnvelopeAndLfoView`. This removes lots of hard-coded values. Add helper lambdas for the repeated creation of `Knob` and `PixmapButton` instances. The spacing that is explicitly introduced between the envelope and LFO should be removed once there is a more open layout. ## Layouts for InstrumentSoundShapingView Use layouts to align the elements of the `InstrumentSoundShapingView`. ## Info text improvements in LFO graph Draw the info text at around 20% of the LFO graph's height. This prepares the dialog to be scaled later. Write "1000 ms/LFO" instead of "ms/LFO: 1000" with a larger gap. ## Accessors for EnvelopeAndLfoParameters Make the enum `LfoShape` in `EnvelopeAndLfoParameters` public so that it can be used without friend declarations. Add accessor methods for the model of the LFO. ## Other improvements * Adjust include orders * Variable initialization in headers * Prevention of most vexing parses --- include/EnvelopeAndLfoParameters.h | 45 +- include/EnvelopeAndLfoView.h | 16 +- include/EnvelopeGraph.h | 66 +++ include/InstrumentSoundShapingView.h | 2 +- include/LfoGraph.h | 68 +++ src/gui/CMakeLists.txt | 2 + src/gui/instrument/EnvelopeAndLfoView.cpp | 473 +++++------------- src/gui/instrument/EnvelopeGraph.cpp | 157 ++++++ .../instrument/InstrumentSoundShapingView.cpp | 93 ++-- src/gui/instrument/LfoGraph.cpp | 168 +++++++ 10 files changed, 662 insertions(+), 428 deletions(-) create mode 100644 include/EnvelopeGraph.h create mode 100644 include/LfoGraph.h create mode 100644 src/gui/instrument/EnvelopeGraph.cpp create mode 100644 src/gui/instrument/LfoGraph.cpp diff --git a/include/EnvelopeAndLfoParameters.h b/include/EnvelopeAndLfoParameters.h index 2a8d3a685..50bfdf787 100644 --- a/include/EnvelopeAndLfoParameters.h +++ b/include/EnvelopeAndLfoParameters.h @@ -71,7 +71,18 @@ public: using LfoList = QList; LfoList m_lfos; - } ; + }; + + enum class LfoShape + { + SineWave, + TriangleWave, + SawWave, + SquareWave, + UserDefinedWave, + RandomWave, + Count + }; EnvelopeAndLfoParameters( float _value_for_zero_amount, Model * _parent ); @@ -114,6 +125,28 @@ public: return m_rFrames; } + // Envelope + const FloatModel& getPredelayModel() const { return m_predelayModel; } + const FloatModel& getAttackModel() const { return m_attackModel; } + const FloatModel& getHoldModel() const { return m_holdModel; } + const FloatModel& getDecayModel() const { return m_decayModel; } + const FloatModel& getSustainModel() const { return m_sustainModel; } + const FloatModel& getReleaseModel() const { return m_releaseModel; } + const FloatModel& getAmountModel() const { return m_amountModel; } + FloatModel& getAmountModel() { return m_amountModel; } + + + // LFO + inline f_cnt_t getLfoPredelayFrames() const { return m_lfoPredelayFrames; } + inline f_cnt_t getLfoAttackFrames() const { return m_lfoAttackFrames; } + inline f_cnt_t getLfoOscillationFrames() const { return m_lfoOscillationFrames; } + + const FloatModel& getLfoAmountModel() const { return m_lfoAmountModel; } + FloatModel& getLfoAmountModel() { return m_lfoAmountModel; } + const TempoSyncKnobModel& getLfoSpeedModel() const { return m_lfoSpeedModel; } + const BoolModel& getX100Model() const { return m_x100Model; } + const IntModel& getLfoWaveModel() const { return m_lfoWaveModel; } + std::shared_ptr getLfoUserWave() const { return m_userWave; } public slots: void updateSampleVars(); @@ -170,16 +203,6 @@ private: bool m_bad_lfoShapeData; std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); - enum class LfoShape - { - SineWave, - TriangleWave, - SawWave, - SquareWave, - UserDefinedWave, - RandomWave, - Count - } ; constexpr static auto NumLfoShapes = static_cast(LfoShape::Count); sample_t lfoShapeSample( fpp_t _frame_offset ); diff --git a/include/EnvelopeAndLfoView.h b/include/EnvelopeAndLfoView.h index d545aaa06..0063dc788 100644 --- a/include/EnvelopeAndLfoView.h +++ b/include/EnvelopeAndLfoView.h @@ -29,10 +29,6 @@ #include #include "ModelView.h" -#include "embed.h" - -class QPaintEvent; -class QPixmap; namespace lmms { @@ -47,6 +43,8 @@ class Knob; class LedCheckBox; class PixmapButton; class TempoSyncKnob; +class EnvelopeGraph; +class LfoGraph; @@ -63,8 +61,6 @@ protected: void dragEnterEvent( QDragEnterEvent * _dee ) override; void dropEvent( QDropEvent * _de ) override; - void mousePressEvent( QMouseEvent * _me ) override; - void paintEvent( QPaintEvent * _pe ) override; protected slots: @@ -72,13 +68,10 @@ protected slots: private: - QPixmap m_envGraph = embed::getIconPixmap("envelope_graph"); - QPixmap m_lfoGraph = embed::getIconPixmap("lfo_graph"); - EnvelopeAndLfoParameters * m_params; - // envelope stuff + EnvelopeGraph* m_envelopeGraph; Knob * m_predelayKnob; Knob * m_attackKnob; Knob * m_holdKnob; @@ -88,6 +81,7 @@ private: Knob * m_amountKnob; // LFO stuff + LfoGraph* m_lfoGraph; Knob * m_lfoPredelayKnob; Knob * m_lfoAttackKnob; TempoSyncKnob * m_lfoSpeedKnob; @@ -97,8 +91,6 @@ private: LedCheckBox * m_x100Cb; LedCheckBox * m_controlEnvAmountCb; - - float m_randomGraph; } ; } // namespace gui diff --git a/include/EnvelopeGraph.h b/include/EnvelopeGraph.h new file mode 100644 index 000000000..8cfeaf11f --- /dev/null +++ b/include/EnvelopeGraph.h @@ -0,0 +1,66 @@ +/* + * EnvelopeGraph.h - Displays envelope graphs + * + * Copyright (c) 2004-2009 Tobias Doerffel + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_ENVELOPE_GRAPH_H +#define LMMS_GUI_ENVELOPE_GRAPH_H + +#include + +#include "ModelView.h" +#include "embed.h" + +namespace lmms +{ + +class EnvelopeAndLfoParameters; + +namespace gui +{ + +class EnvelopeGraph : public QWidget, public ModelView +{ +public: + EnvelopeGraph(QWidget* parent); + +protected: + void modelChanged() override; + + void mousePressEvent(QMouseEvent* me) override; + void paintEvent(QPaintEvent* pe) override; + +private: + void toggleAmountModel(); + +private: + QPixmap m_envGraph = embed::getIconPixmap("envelope_graph"); + + EnvelopeAndLfoParameters* m_params; +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_ENVELOPE_GRAPH_H diff --git a/include/InstrumentSoundShapingView.h b/include/InstrumentSoundShapingView.h index 8f671514a..c9caea28c 100644 --- a/include/InstrumentSoundShapingView.h +++ b/include/InstrumentSoundShapingView.h @@ -56,7 +56,7 @@ private: void modelChanged() override; - InstrumentSoundShaping * m_ss; + InstrumentSoundShaping * m_ss = nullptr; TabWidget * m_targetsTabWidget; EnvelopeAndLfoView * m_envLfoViews[InstrumentSoundShaping::NumTargets]; diff --git a/include/LfoGraph.h b/include/LfoGraph.h new file mode 100644 index 000000000..733db3a34 --- /dev/null +++ b/include/LfoGraph.h @@ -0,0 +1,68 @@ +/* + * LfoGraph.h - Displays LFO graphs + * + * Copyright (c) 2004-2009 Tobias Doerffel + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_LFO_GRAPH_H +#define LMMS_GUI_LFO_GRAPH_H + +#include + +#include "ModelView.h" +#include "embed.h" + +namespace lmms +{ + +class EnvelopeAndLfoParameters; + +namespace gui +{ + +class LfoGraph : public QWidget, public ModelView +{ +public: + LfoGraph(QWidget* parent); + +protected: + void modelChanged() override; + + void mousePressEvent(QMouseEvent* me) override; + void paintEvent(QPaintEvent* pe) override; + +private: + void toggleAmountModel(); + +private: + QPixmap m_lfoGraph = embed::getIconPixmap("lfo_graph"); + + EnvelopeAndLfoParameters* m_params = nullptr; + + float m_randomGraph {0.}; +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LFO_GRAPH_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index e2342b465..791d6fbff 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -62,12 +62,14 @@ SET(LMMS_SRCS gui/editors/TrackContainerView.cpp gui/instrument/EnvelopeAndLfoView.cpp + gui/instrument/EnvelopeGraph.cpp gui/instrument/InstrumentFunctionViews.cpp gui/instrument/InstrumentMidiIOView.cpp gui/instrument/InstrumentTuningView.cpp gui/instrument/InstrumentSoundShapingView.cpp gui/instrument/InstrumentTrackWindow.cpp gui/instrument/InstrumentView.cpp + gui/instrument/LfoGraph.cpp gui/instrument/PianoView.cpp gui/menus/MidiPortMenu.cpp diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index 7f30f3ac1..c2e642b01 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -23,202 +23,170 @@ * */ -#include -#include - #include "EnvelopeAndLfoView.h" + +#include "EnvelopeGraph.h" +#include "LfoGraph.h" #include "EnvelopeAndLfoParameters.h" #include "SampleLoader.h" -#include "embed.h" -#include "Engine.h" #include "gui_templates.h" #include "Knob.h" #include "LedCheckBox.h" -#include "AudioEngine.h" #include "DataFile.h" -#include "Oscillator.h" #include "PixmapButton.h" #include "StringPairDrag.h" #include "TempoSyncKnob.h" #include "TextFloat.h" #include "Track.h" +#include + + namespace lmms { -extern const float SECS_PER_ENV_SEGMENT; -extern const float SECS_PER_LFO_OSCILLATION; - - namespace gui { - -const int ENV_GRAPH_X = 6; -const int ENV_GRAPH_Y = 6; - -const int ENV_KNOBS_Y = 43; -const int ENV_KNOBS_LBL_Y = ENV_KNOBS_Y+35; -const int KNOB_X_SPACING = 32; -const int PREDELAY_KNOB_X = 6; -const int ATTACK_KNOB_X = PREDELAY_KNOB_X+KNOB_X_SPACING; -const int HOLD_KNOB_X = ATTACK_KNOB_X+KNOB_X_SPACING; -const int DECAY_KNOB_X = HOLD_KNOB_X+KNOB_X_SPACING; -const int SUSTAIN_KNOB_X = DECAY_KNOB_X+KNOB_X_SPACING; -const int RELEASE_KNOB_X = SUSTAIN_KNOB_X+KNOB_X_SPACING; -const int AMOUNT_KNOB_X = RELEASE_KNOB_X+KNOB_X_SPACING; - -const int TIME_UNIT_WIDTH = 40; - -const int LFO_GRAPH_X = 6; -const int LFO_GRAPH_Y = ENV_KNOBS_LBL_Y+14; -const int LFO_KNOB_Y = LFO_GRAPH_Y-2; -const int LFO_PREDELAY_KNOB_X = LFO_GRAPH_X + 100; -const int LFO_ATTACK_KNOB_X = LFO_PREDELAY_KNOB_X+KNOB_X_SPACING; -const int LFO_SPEED_KNOB_X = LFO_ATTACK_KNOB_X+KNOB_X_SPACING; -const int LFO_AMOUNT_KNOB_X = LFO_SPEED_KNOB_X+KNOB_X_SPACING; -const int LFO_SHAPES_X = LFO_GRAPH_X;//PREDELAY_KNOB_X; -const int LFO_SHAPES_Y = LFO_GRAPH_Y + 50; - -EnvelopeAndLfoView::EnvelopeAndLfoView( QWidget * _parent ) : - QWidget( _parent ), - ModelView( nullptr, this ), - m_params( nullptr ) +EnvelopeAndLfoView::EnvelopeAndLfoView(QWidget * parent) : + QWidget(parent), + ModelView(nullptr, this), + m_params(nullptr) { + // Helper lambdas for consistent repeated buiding of certain widgets + auto buildKnob = [&](const QString& label, const QString& hintText) + { + auto knob = new Knob(KnobType::Bright26, this); + knob->setLabel(label); + knob->setHintText(hintText, ""); + + return knob; + }; - m_predelayKnob = new Knob( KnobType::Bright26, this ); - m_predelayKnob->setLabel( tr( "DEL" ) ); - m_predelayKnob->move( PREDELAY_KNOB_X, ENV_KNOBS_Y ); - m_predelayKnob->setHintText( tr( "Pre-delay:" ), "" ); + auto buildPixmapButton = [&](const QString& activePixmap, const QString& inactivePixmap) + { + auto button = new PixmapButton(this, nullptr); + button->setActiveGraphic(embed::getIconPixmap(activePixmap)); + button->setInactiveGraphic(embed::getIconPixmap(inactivePixmap)); + + return button; + }; + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(5, 5, 5, 5); + + // Envelope + + QVBoxLayout* envelopeLayout = new QVBoxLayout(); + mainLayout->addLayout(envelopeLayout); + + QHBoxLayout* graphAndAmountLayout = new QHBoxLayout(); + envelopeLayout->addLayout(graphAndAmountLayout); + + m_envelopeGraph = new EnvelopeGraph(this); + graphAndAmountLayout->addWidget(m_envelopeGraph); + + m_amountKnob = buildKnob(tr("AMT"), tr("Modulation amount:")); + graphAndAmountLayout->addWidget(m_amountKnob, 0, Qt::AlignCenter); + + QHBoxLayout* envKnobsLayout = new QHBoxLayout(); + envelopeLayout->addLayout(envKnobsLayout); + + m_predelayKnob = buildKnob(tr("DEL"), tr("Pre-delay:")); + envKnobsLayout->addWidget(m_predelayKnob); + + m_attackKnob = buildKnob(tr("ATT"), tr("Attack:")); + envKnobsLayout->addWidget(m_attackKnob); + + m_holdKnob = buildKnob(tr("HOLD"), tr("Hold:")); + envKnobsLayout->addWidget(m_holdKnob); + + m_decayKnob = buildKnob(tr("DEC"), tr("Decay:")); + envKnobsLayout->addWidget(m_decayKnob); + + m_sustainKnob = buildKnob(tr("SUST"), tr("Sustain:")); + envKnobsLayout->addWidget(m_sustainKnob); + + m_releaseKnob = buildKnob(tr("REL"), tr("Release:")); + envKnobsLayout->addWidget(m_releaseKnob); - m_attackKnob = new Knob( KnobType::Bright26, this ); - m_attackKnob->setLabel( tr( "ATT" ) ); - m_attackKnob->move( ATTACK_KNOB_X, ENV_KNOBS_Y ); - m_attackKnob->setHintText( tr( "Attack:" ), "" ); + // Add some space between the envelope and LFO section + mainLayout->addSpacing(10); - m_holdKnob = new Knob( KnobType::Bright26, this ); - m_holdKnob->setLabel( tr( "HOLD" ) ); - m_holdKnob->move( HOLD_KNOB_X, ENV_KNOBS_Y ); - m_holdKnob->setHintText( tr( "Hold:" ), "" ); + // LFO + QHBoxLayout* lfoLayout = new QHBoxLayout(); + mainLayout->addLayout(lfoLayout); - m_decayKnob = new Knob( KnobType::Bright26, this ); - m_decayKnob->setLabel( tr( "DEC" ) ); - m_decayKnob->move( DECAY_KNOB_X, ENV_KNOBS_Y ); - m_decayKnob->setHintText( tr( "Decay:" ), "" ); + QVBoxLayout* graphAndTypesLayout = new QVBoxLayout(); + lfoLayout->addLayout(graphAndTypesLayout); + m_lfoGraph = new LfoGraph(this); + graphAndTypesLayout->addWidget(m_lfoGraph); - m_sustainKnob = new Knob( KnobType::Bright26, this ); - m_sustainKnob->setLabel( tr( "SUST" ) ); - m_sustainKnob->move( SUSTAIN_KNOB_X, ENV_KNOBS_Y ); - m_sustainKnob->setHintText( tr( "Sustain:" ), "" ); + QHBoxLayout* typesLayout = new QHBoxLayout(); + graphAndTypesLayout->addLayout(typesLayout); + typesLayout->setContentsMargins(0, 0, 0, 0); + typesLayout->setSpacing(0); + auto sin_lfo_btn = buildPixmapButton("sin_wave_active", "sin_wave_inactive"); + auto triangle_lfo_btn = buildPixmapButton("triangle_wave_active", "triangle_wave_inactive"); + auto saw_lfo_btn = buildPixmapButton("saw_wave_active", "saw_wave_inactive"); + auto sqr_lfo_btn = buildPixmapButton("square_wave_active","square_wave_inactive"); + auto random_lfo_btn = buildPixmapButton("random_wave_active", "random_wave_inactive"); + m_userLfoBtn = buildPixmapButton("usr_wave_active", "usr_wave_inactive"); - m_releaseKnob = new Knob( KnobType::Bright26, this ); - m_releaseKnob->setLabel( tr( "REL" ) ); - m_releaseKnob->move( RELEASE_KNOB_X, ENV_KNOBS_Y ); - m_releaseKnob->setHintText( tr( "Release:" ), "" ); + connect(m_userLfoBtn, SIGNAL(toggled(bool)), this, SLOT(lfoUserWaveChanged())); + typesLayout->addWidget(sin_lfo_btn); + typesLayout->addWidget(triangle_lfo_btn); + typesLayout->addWidget(saw_lfo_btn); + typesLayout->addWidget(sqr_lfo_btn); + typesLayout->addWidget(random_lfo_btn); + typesLayout->addWidget(m_userLfoBtn); - m_amountKnob = new Knob( KnobType::Bright26, this ); - m_amountKnob->setLabel( tr( "AMT" ) ); - m_amountKnob->move( AMOUNT_KNOB_X, ENV_GRAPH_Y ); - m_amountKnob->setHintText( tr( "Modulation amount:" ), "" ); + m_lfoWaveBtnGrp = new automatableButtonGroup(this); + m_lfoWaveBtnGrp->addButton(sin_lfo_btn); + m_lfoWaveBtnGrp->addButton(triangle_lfo_btn); + m_lfoWaveBtnGrp->addButton(saw_lfo_btn); + m_lfoWaveBtnGrp->addButton(sqr_lfo_btn); + m_lfoWaveBtnGrp->addButton(m_userLfoBtn); + m_lfoWaveBtnGrp->addButton(random_lfo_btn); + QVBoxLayout* knobsAndCheckBoxesLayout = new QVBoxLayout(); + lfoLayout->addLayout(knobsAndCheckBoxesLayout); + QHBoxLayout* lfoKnobsLayout = new QHBoxLayout(); + knobsAndCheckBoxesLayout->addLayout(lfoKnobsLayout); + m_lfoPredelayKnob = buildKnob(tr("DEL"), tr("Pre-delay:")); + lfoKnobsLayout->addWidget(m_lfoPredelayKnob); - m_lfoPredelayKnob = new Knob( KnobType::Bright26, this ); - m_lfoPredelayKnob->setLabel( tr( "DEL" ) ); - m_lfoPredelayKnob->move( LFO_PREDELAY_KNOB_X, LFO_KNOB_Y ); - m_lfoPredelayKnob->setHintText( tr( "Pre-delay:" ), "" ); + m_lfoAttackKnob = buildKnob(tr("ATT"), tr("Attack:")); + lfoKnobsLayout->addWidget(m_lfoAttackKnob); + m_lfoSpeedKnob = new TempoSyncKnob(KnobType::Bright26, this); + m_lfoSpeedKnob->setLabel(tr("SPD")); + m_lfoSpeedKnob->setHintText(tr("Frequency:"), ""); + lfoKnobsLayout->addWidget(m_lfoSpeedKnob); - m_lfoAttackKnob = new Knob( KnobType::Bright26, this ); - m_lfoAttackKnob->setLabel( tr( "ATT" ) ); - m_lfoAttackKnob->move( LFO_ATTACK_KNOB_X, LFO_KNOB_Y ); - m_lfoAttackKnob->setHintText( tr( "Attack:" ), "" ); + m_lfoAmountKnob = buildKnob(tr("AMT"), tr("Modulation amount:")); + lfoKnobsLayout->addWidget(m_lfoAmountKnob); + QVBoxLayout* checkBoxesLayout = new QVBoxLayout(); + knobsAndCheckBoxesLayout->addLayout(checkBoxesLayout); - m_lfoSpeedKnob = new TempoSyncKnob( KnobType::Bright26, this ); - m_lfoSpeedKnob->setLabel( tr( "SPD" ) ); - m_lfoSpeedKnob->move( LFO_SPEED_KNOB_X, LFO_KNOB_Y ); - m_lfoSpeedKnob->setHintText( tr( "Frequency:" ), "" ); - - - m_lfoAmountKnob = new Knob( KnobType::Bright26, this ); - m_lfoAmountKnob->setLabel( tr( "AMT" ) ); - m_lfoAmountKnob->move( LFO_AMOUNT_KNOB_X, LFO_KNOB_Y ); - m_lfoAmountKnob->setHintText( tr( "Modulation amount:" ), "" ); - - auto sin_lfo_btn = new PixmapButton(this, nullptr); - sin_lfo_btn->move( LFO_SHAPES_X, LFO_SHAPES_Y ); - sin_lfo_btn->setActiveGraphic( embed::getIconPixmap( - "sin_wave_active" ) ); - sin_lfo_btn->setInactiveGraphic( embed::getIconPixmap( - "sin_wave_inactive" ) ); - - auto triangle_lfo_btn = new PixmapButton(this, nullptr); - triangle_lfo_btn->move( LFO_SHAPES_X+15, LFO_SHAPES_Y ); - triangle_lfo_btn->setActiveGraphic( embed::getIconPixmap( - "triangle_wave_active" ) ); - triangle_lfo_btn->setInactiveGraphic( embed::getIconPixmap( - "triangle_wave_inactive" ) ); - - auto saw_lfo_btn = new PixmapButton(this, nullptr); - saw_lfo_btn->move( LFO_SHAPES_X+30, LFO_SHAPES_Y ); - saw_lfo_btn->setActiveGraphic( embed::getIconPixmap( - "saw_wave_active" ) ); - saw_lfo_btn->setInactiveGraphic( embed::getIconPixmap( - "saw_wave_inactive" ) ); - - auto sqr_lfo_btn = new PixmapButton(this, nullptr); - sqr_lfo_btn->move( LFO_SHAPES_X+45, LFO_SHAPES_Y ); - sqr_lfo_btn->setActiveGraphic( embed::getIconPixmap( - "square_wave_active" ) ); - sqr_lfo_btn->setInactiveGraphic( embed::getIconPixmap( - "square_wave_inactive" ) ); - - m_userLfoBtn = new PixmapButton( this, nullptr ); - m_userLfoBtn->move( LFO_SHAPES_X+75, LFO_SHAPES_Y ); - m_userLfoBtn->setActiveGraphic( embed::getIconPixmap( - "usr_wave_active" ) ); - m_userLfoBtn->setInactiveGraphic( embed::getIconPixmap( - "usr_wave_inactive" ) ); - - connect( m_userLfoBtn, SIGNAL(toggled(bool)), - this, SLOT(lfoUserWaveChanged())); - - auto random_lfo_btn = new PixmapButton(this, nullptr); - random_lfo_btn->move( LFO_SHAPES_X+60, LFO_SHAPES_Y ); - random_lfo_btn->setActiveGraphic( embed::getIconPixmap( - "random_wave_active" ) ); - random_lfo_btn->setInactiveGraphic( embed::getIconPixmap( - "random_wave_inactive" ) ); - - m_lfoWaveBtnGrp = new automatableButtonGroup( this ); - m_lfoWaveBtnGrp->addButton( sin_lfo_btn ); - m_lfoWaveBtnGrp->addButton( triangle_lfo_btn ); - m_lfoWaveBtnGrp->addButton( saw_lfo_btn ); - m_lfoWaveBtnGrp->addButton( sqr_lfo_btn ); - m_lfoWaveBtnGrp->addButton( m_userLfoBtn ); - m_lfoWaveBtnGrp->addButton( random_lfo_btn ); - - m_x100Cb = new LedCheckBox( tr( "FREQ x 100" ), this ); - m_x100Cb->move( LFO_PREDELAY_KNOB_X, LFO_GRAPH_Y + 36 ); + m_x100Cb = new LedCheckBox(tr("FREQ x 100"), this); m_x100Cb->setToolTip(tr("Multiply LFO frequency by 100")); - + checkBoxesLayout->addWidget(m_x100Cb); m_controlEnvAmountCb = new LedCheckBox(tr("MOD ENV AMOUNT"), this); - m_controlEnvAmountCb->move( LFO_PREDELAY_KNOB_X, LFO_GRAPH_Y + 54 ); - m_controlEnvAmountCb->setToolTip( - tr( "Control envelope amount by this LFO" ) ); - - - setAcceptDrops( true ); + m_controlEnvAmountCb->setToolTip(tr("Control envelope amount by this LFO")); + checkBoxesLayout->addWidget(m_controlEnvAmountCb); + setAcceptDrops(true); } @@ -235,6 +203,7 @@ EnvelopeAndLfoView::~EnvelopeAndLfoView() void EnvelopeAndLfoView::modelChanged() { m_params = castModel(); + m_envelopeGraph->setModel(m_params); m_predelayKnob->setModel( &m_params->m_predelayModel ); m_attackKnob->setModel( &m_params->m_attackModel ); m_holdKnob->setModel( &m_params->m_holdModel ); @@ -242,6 +211,8 @@ void EnvelopeAndLfoView::modelChanged() m_sustainKnob->setModel( &m_params->m_sustainModel ); m_releaseKnob->setModel( &m_params->m_releaseModel ); m_amountKnob->setModel( &m_params->m_amountModel ); + + m_lfoGraph->setModel(m_params); m_lfoPredelayKnob->setModel( &m_params->m_lfoPredelayModel ); m_lfoAttackKnob->setModel( &m_params->m_lfoAttackModel ); m_lfoSpeedKnob->setModel( &m_params->m_lfoSpeedModel ); @@ -254,40 +225,6 @@ void EnvelopeAndLfoView::modelChanged() -void EnvelopeAndLfoView::mousePressEvent( QMouseEvent * _me ) -{ - if( _me->button() != Qt::LeftButton ) - { - return; - } - - if (QRect(ENV_GRAPH_X, ENV_GRAPH_Y, m_envGraph.width(), m_envGraph.height()).contains(_me->pos())) - { - if( m_params->m_amountModel.value() < 1.0f ) - { - m_params->m_amountModel.setValue( 1.0f ); - } - else - { - m_params->m_amountModel.setValue( 0.0f ); - } - } - else if (QRect(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph.width(), m_lfoGraph.height()).contains(_me->pos())) - { - if( m_params->m_lfoAmountModel.value() < 1.0f ) - { - m_params->m_lfoAmountModel.setValue( 1.0f ); - } - else - { - m_params->m_lfoAmountModel.setValue( 0.0f ); - } - } -} - - - - void EnvelopeAndLfoView::dragEnterEvent( QDragEnterEvent * _dee ) { StringPairDrag::processDragEnterEvent( _dee, @@ -327,167 +264,6 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) -void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) -{ - QPainter p( this ); - p.setRenderHint( QPainter::Antialiasing ); - - // draw envelope-graph - p.drawPixmap(ENV_GRAPH_X, ENV_GRAPH_Y, m_envGraph); - // draw LFO-graph - p.drawPixmap(LFO_GRAPH_X, LFO_GRAPH_Y, m_lfoGraph); - - p.setFont(adjustedToPixelSize(p.font(), 8)); - - const float gray_amount = 1.0f - fabsf( m_amountKnob->value() ); - - p.setPen( QPen( QColor( static_cast( 96 * gray_amount ), - static_cast( 255 - 159 * gray_amount ), - static_cast( 128 - 32 * gray_amount ) ), - 2 ) ); - - const QColor end_points_color( 0x99, 0xAF, 0xFF ); - const QColor end_points_bg_color( 0, 0, 2 ); - - const int y_base = ENV_GRAPH_Y + m_envGraph.height() - 3; - const int avail_height = m_envGraph.height() - 6; - - int x1 = static_cast( m_predelayKnob->value() * TIME_UNIT_WIDTH ); - int x2 = x1 + static_cast( m_attackKnob->value() * TIME_UNIT_WIDTH ); - int x3 = x2 + static_cast( m_holdKnob->value() * TIME_UNIT_WIDTH ); - int x4 = x3 + static_cast( ( m_decayKnob->value() * - ( 1 - m_sustainKnob->value() ) ) * TIME_UNIT_WIDTH ); - int x5 = x4 + static_cast( m_releaseKnob->value() * TIME_UNIT_WIDTH ); - - if( x5 > 174 ) - { - x1 = ( x1 * 174 ) / x5; - x2 = ( x2 * 174 ) / x5; - x3 = ( x3 * 174 ) / x5; - x4 = ( x4 * 174 ) / x5; - x5 = ( x5 * 174 ) / x5; - } - x1 += ENV_GRAPH_X + 2; - x2 += ENV_GRAPH_X + 2; - x3 += ENV_GRAPH_X + 2; - x4 += ENV_GRAPH_X + 2; - x5 += ENV_GRAPH_X + 2; - - p.drawLine( x1, y_base, x2, y_base - avail_height ); - p.fillRect( x1 - 1, y_base - 2, 4, 4, end_points_bg_color ); - p.fillRect( x1, y_base - 1, 2, 2, end_points_color ); - - p.drawLine( x2, y_base - avail_height, x3, y_base - avail_height ); - p.fillRect( x2 - 1, y_base - 2 - avail_height, 4, 4, - end_points_bg_color ); - p.fillRect( x2, y_base - 1 - avail_height, 2, 2, end_points_color ); - - p.drawLine( x3, y_base-avail_height, x4, static_cast( y_base - - avail_height + - ( 1 - m_sustainKnob->value() ) * avail_height ) ); - p.fillRect( x3 - 1, y_base - 2 - avail_height, 4, 4, - end_points_bg_color ); - p.fillRect( x3, y_base - 1 - avail_height, 2, 2, end_points_color ); - - p.drawLine( x4, static_cast( y_base - avail_height + - ( 1 - m_sustainKnob->value() ) * - avail_height ), x5, y_base ); - p.fillRect( x4 - 1, static_cast( y_base - avail_height + - ( 1 - m_sustainKnob->value() ) * - avail_height ) - 2, 4, 4, - end_points_bg_color ); - p.fillRect( x4, static_cast( y_base - avail_height + - ( 1 - m_sustainKnob->value() ) * - avail_height ) - 1, 2, 2, - end_points_color ); - p.fillRect( x5 - 1, y_base - 2, 4, 4, end_points_bg_color ); - p.fillRect( x5, y_base - 1, 2, 2, end_points_color ); - - int LFO_GRAPH_W = m_lfoGraph.width() - 3; // subtract border - int LFO_GRAPH_H = m_lfoGraph.height() - 6; // subtract border - int graph_x_base = LFO_GRAPH_X + 2; - int graph_y_base = LFO_GRAPH_Y + 3 + LFO_GRAPH_H / 2; - - const float frames_for_graph = SECS_PER_LFO_OSCILLATION * - Engine::audioEngine()->baseSampleRate() / 10; - - const float lfo_gray_amount = 1.0f - fabsf( m_lfoAmountKnob->value() ); - p.setPen( QPen( QColor( static_cast( 96 * lfo_gray_amount ), - static_cast( 255 - 159 * lfo_gray_amount ), - static_cast( 128 - 32 * - lfo_gray_amount ) ), - 1.5 ) ); - - - float osc_frames = m_params->m_lfoOscillationFrames; - - if( m_params->m_x100Model.value() ) - { - osc_frames *= 100.0f; - } - - float old_y = 0; - for( int x = 0; x <= LFO_GRAPH_W; ++x ) - { - float val = 0.0; - float cur_sample = x * frames_for_graph / LFO_GRAPH_W; - if( static_cast( cur_sample ) > - m_params->m_lfoPredelayFrames ) - { - float phase = ( cur_sample -= - m_params->m_lfoPredelayFrames ) / - osc_frames; - switch( static_cast(m_params->m_lfoWaveModel.value()) ) - { - case EnvelopeAndLfoParameters::LfoShape::SineWave: - default: - val = Oscillator::sinSample( phase ); - break; - case EnvelopeAndLfoParameters::LfoShape::TriangleWave: - val = Oscillator::triangleSample( - phase ); - break; - case EnvelopeAndLfoParameters::LfoShape::SawWave: - val = Oscillator::sawSample( phase ); - break; - case EnvelopeAndLfoParameters::LfoShape::SquareWave: - val = Oscillator::squareSample( phase ); - break; - case EnvelopeAndLfoParameters::LfoShape::RandomWave: - if( x % (int)( 900 * m_lfoSpeedKnob->value() + 1 ) == 0 ) - { - m_randomGraph = Oscillator::noiseSample( 0.0f ); - } - val = m_randomGraph; - break; - case EnvelopeAndLfoParameters::LfoShape::UserDefinedWave: - val = Oscillator::userWaveSample(m_params->m_userWave.get(), phase); - break; - } - if( static_cast( cur_sample ) <= - m_params->m_lfoAttackFrames ) - { - val *= cur_sample / m_params->m_lfoAttackFrames; - } - } - float cur_y = -LFO_GRAPH_H / 2.0f * val; - p.drawLine( QLineF( graph_x_base + x - 1, graph_y_base + old_y, - graph_x_base + x, - graph_y_base + cur_y ) ); - old_y = cur_y; - } - - p.setPen( QColor( 201, 201, 225 ) ); - int ms_per_osc = static_cast( SECS_PER_LFO_OSCILLATION * - m_lfoSpeedKnob->value() * - 1000.0f ); - p.drawText(LFO_GRAPH_X + 4, LFO_GRAPH_Y + m_lfoGraph.height() - 6, tr("ms/LFO:")); - p.drawText(LFO_GRAPH_X + 52, LFO_GRAPH_Y + m_lfoGraph.height() - 6, QString::number(ms_per_osc)); -} - - - - void EnvelopeAndLfoView::lfoUserWaveChanged() { if( static_cast(m_params->m_lfoWaveModel.value()) == @@ -502,11 +278,6 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() } } - - - - - } // namespace gui } // namespace lmms diff --git a/src/gui/instrument/EnvelopeGraph.cpp b/src/gui/instrument/EnvelopeGraph.cpp new file mode 100644 index 000000000..4d866b3cc --- /dev/null +++ b/src/gui/instrument/EnvelopeGraph.cpp @@ -0,0 +1,157 @@ +/* + * EnvelopeGraph.cpp - Displays envelope graphs + * + * Copyright (c) 2004-2014 Tobias Doerffel + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include +#include + +#include "EnvelopeGraph.h" + +#include "EnvelopeAndLfoParameters.h" + +namespace lmms +{ + +namespace gui +{ + +const int TIME_UNIT_WIDTH = 40; + + +EnvelopeGraph::EnvelopeGraph(QWidget* parent) : + QWidget(parent), + ModelView(nullptr, this), + m_params(nullptr) +{ + setFixedSize(m_envGraph.size()); +} + +void EnvelopeGraph::modelChanged() +{ + m_params = castModel(); +} + +void EnvelopeGraph::mousePressEvent(QMouseEvent* me) +{ + if(me->button() == Qt::LeftButton) + { + toggleAmountModel(); + } +} + +void EnvelopeGraph::paintEvent(QPaintEvent*) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Draw the graph background + p.drawPixmap(rect(), m_envGraph); + + const auto * params = castModel(); + if (!params) + { + return; + } + + const float amount = params->getAmountModel().value(); + const float predelay = params->getPredelayModel().value(); + const float attack = params->getAttackModel().value(); + const float hold = params->getHoldModel().value(); + const float decay = params->getDecayModel().value(); + const float sustain = params->getSustainModel().value(); + const float release = params->getReleaseModel().value(); + + const float gray_amount = 1.0f - fabsf(amount); + + p.setPen(QPen(QColor(static_cast(96 * gray_amount), + static_cast(255 - 159 * gray_amount), + static_cast(128 - 32 * gray_amount)), + 2)); + + const QColor end_points_color(0x99, 0xAF, 0xFF); + const QColor end_points_bg_color(0, 0, 2); + + const int y_base = m_envGraph.height() - 3; + const int avail_height = m_envGraph.height() - 6; + + int x1 = static_cast(predelay * TIME_UNIT_WIDTH); + int x2 = x1 + static_cast(attack * TIME_UNIT_WIDTH); + int x3 = x2 + static_cast(hold * TIME_UNIT_WIDTH); + int x4 = x3 + static_cast((decay * (1 - sustain)) * TIME_UNIT_WIDTH); + int x5 = x4 + static_cast(release * TIME_UNIT_WIDTH); + + if (x5 > 174) + { + x1 = (x1 * 174) / x5; + x2 = (x2 * 174) / x5; + x3 = (x3 * 174) / x5; + x4 = (x4 * 174) / x5; + x5 = (x5 * 174) / x5; + } + x1 += 2; + x2 += 2; + x3 += 2; + x4 += 2; + x5 += 2; + + p.drawLine(x1, y_base, x2, y_base - avail_height); + p.fillRect(x1 - 1, y_base - 2, 4, 4, end_points_bg_color); + p.fillRect(x1, y_base - 1, 2, 2, end_points_color); + + p.drawLine(x2, y_base - avail_height, x3, y_base - avail_height); + p.fillRect(x2 - 1, y_base - 2 - avail_height, 4, 4, + end_points_bg_color); + p.fillRect(x2, y_base - 1 - avail_height, 2, 2, end_points_color); + + const int sustainHeight = static_cast(y_base - avail_height + (1 - sustain) * avail_height); + + p.drawLine(x3, y_base-avail_height, x4, sustainHeight); + p.fillRect(x3 - 1, y_base - 2 - avail_height, 4, 4, end_points_bg_color); + p.fillRect(x3, y_base - 1 - avail_height, 2, 2, end_points_color); + + p.drawLine(x4, sustainHeight, x5, y_base); + p.fillRect(x4 - 1, sustainHeight - 2, 4, 4, end_points_bg_color); + p.fillRect(x4, sustainHeight - 1, 2, 2, end_points_color); + p.fillRect(x5 - 1, y_base - 2, 4, 4, end_points_bg_color); + p.fillRect(x5, y_base - 1, 2, 2, end_points_color); +} + +void EnvelopeGraph::toggleAmountModel() +{ + auto* params = castModel(); + auto& amountModel = params->getAmountModel(); + + if (amountModel.value() < 1.0f ) + { + amountModel.setValue( 1.0f ); + } + else + { + amountModel.setValue( 0.0f ); + } +} + +} // namespace gui + +} // namespace lmms diff --git a/src/gui/instrument/InstrumentSoundShapingView.cpp b/src/gui/instrument/InstrumentSoundShapingView.cpp index b96db8495..a3a78e256 100644 --- a/src/gui/instrument/InstrumentSoundShapingView.cpp +++ b/src/gui/instrument/InstrumentSoundShapingView.cpp @@ -22,9 +22,11 @@ * */ -#include - #include "InstrumentSoundShapingView.h" + +#include +#include + #include "EnvelopeAndLfoParameters.h" #include "EnvelopeAndLfoView.h" #include "ComboBox.h" @@ -37,69 +39,54 @@ namespace lmms::gui { -const int TARGETS_TABWIDGET_X = 4; -const int TARGETS_TABWIDGET_Y = 5; -const int TARGETS_TABWIDGET_WIDTH = 242; -const int TARGETS_TABWIDGET_HEIGTH = 175; - -const int FILTER_GROUPBOX_X = TARGETS_TABWIDGET_X; -const int FILTER_GROUPBOX_Y = TARGETS_TABWIDGET_Y+TARGETS_TABWIDGET_HEIGTH+5; -const int FILTER_GROUPBOX_WIDTH = TARGETS_TABWIDGET_WIDTH; -const int FILTER_GROUPBOX_HEIGHT = 245-FILTER_GROUPBOX_Y; - - - -InstrumentSoundShapingView::InstrumentSoundShapingView( QWidget * _parent ) : - QWidget( _parent ), - ModelView( nullptr, this ), - m_ss( nullptr ) +InstrumentSoundShapingView::InstrumentSoundShapingView(QWidget* parent) : + QWidget(parent), + ModelView(nullptr, this) { - m_targetsTabWidget = new TabWidget( tr( "TARGET" ), this ); - m_targetsTabWidget->setGeometry( TARGETS_TABWIDGET_X, - TARGETS_TABWIDGET_Y, - TARGETS_TABWIDGET_WIDTH, - TARGETS_TABWIDGET_HEIGTH ); + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(5, 5, 5, 5); - for( int i = 0; i < InstrumentSoundShaping::NumTargets; ++i ) + m_targetsTabWidget = new TabWidget(tr("TARGET"), this); + + for (int i = 0; i < InstrumentSoundShaping::NumTargets; ++i) { - m_envLfoViews[i] = new EnvelopeAndLfoView( m_targetsTabWidget ); - m_targetsTabWidget->addTab( m_envLfoViews[i], - tr( InstrumentSoundShaping::targetNames[i][0] ), - nullptr ); + m_envLfoViews[i] = new EnvelopeAndLfoView(m_targetsTabWidget); + m_targetsTabWidget->addTab(m_envLfoViews[i], + tr(InstrumentSoundShaping::targetNames[i][0]), nullptr); } - - m_filterGroupBox = new GroupBox( tr( "FILTER" ), this ); - m_filterGroupBox->setGeometry( FILTER_GROUPBOX_X, FILTER_GROUPBOX_Y, - FILTER_GROUPBOX_WIDTH, - FILTER_GROUPBOX_HEIGHT ); + mainLayout->addWidget(m_targetsTabWidget, 1); - m_filterComboBox = new ComboBox( m_filterGroupBox ); - m_filterComboBox->setGeometry( 14, 22, 120, ComboBox::DEFAULT_HEIGHT ); + m_filterGroupBox = new GroupBox(tr("FILTER"), this); + QHBoxLayout* filterLayout = new QHBoxLayout(m_filterGroupBox); + QMargins filterMargins = filterLayout->contentsMargins(); + filterMargins.setTop(18); + filterLayout->setContentsMargins(filterMargins); + + m_filterComboBox = new ComboBox(m_filterGroupBox); + filterLayout->addWidget(m_filterComboBox); + + m_filterCutKnob = new Knob(KnobType::Bright26, m_filterGroupBox); + m_filterCutKnob->setLabel(tr("FREQ")); + m_filterCutKnob->setHintText(tr("Cutoff frequency:"), " " + tr("Hz")); + filterLayout->addWidget(m_filterCutKnob); + + m_filterResKnob = new Knob(KnobType::Bright26, m_filterGroupBox); + m_filterResKnob->setLabel(tr("Q/RESO")); + m_filterResKnob->setHintText(tr("Q/Resonance:"), ""); + filterLayout->addWidget(m_filterResKnob); + + mainLayout->addWidget(m_filterGroupBox); - m_filterCutKnob = new Knob( KnobType::Bright26, m_filterGroupBox ); - m_filterCutKnob->setLabel( tr( "FREQ" ) ); - m_filterCutKnob->move( 140, 18 ); - m_filterCutKnob->setHintText( tr( "Cutoff frequency:" ), " " + tr( "Hz" ) ); - - - m_filterResKnob = new Knob( KnobType::Bright26, m_filterGroupBox ); - m_filterResKnob->setLabel( tr( "Q/RESO" ) ); - m_filterResKnob->move( 196, 18 ); - m_filterResKnob->setHintText( tr( "Q/Resonance:" ), "" ); - - - m_singleStreamInfoLabel = new QLabel( tr( "Envelopes, LFOs and filters are not supported by the current instrument." ), this ); - m_singleStreamInfoLabel->setWordWrap( true ); + m_singleStreamInfoLabel = new QLabel(tr("Envelopes, LFOs and filters are not supported by the current instrument."), this); + m_singleStreamInfoLabel->setWordWrap(true); // TODO Could also be rendered in system font size... m_singleStreamInfoLabel->setFont(adjustedToPixelSize(m_singleStreamInfoLabel->font(), 10)); + m_singleStreamInfoLabel->setFixedWidth(242); - m_singleStreamInfoLabel->setGeometry( TARGETS_TABWIDGET_X, - TARGETS_TABWIDGET_Y, - TARGETS_TABWIDGET_WIDTH, - TARGETS_TABWIDGET_HEIGTH ); + mainLayout->addWidget(m_singleStreamInfoLabel, 0, Qt::AlignTop); } diff --git a/src/gui/instrument/LfoGraph.cpp b/src/gui/instrument/LfoGraph.cpp new file mode 100644 index 000000000..d02f583d0 --- /dev/null +++ b/src/gui/instrument/LfoGraph.cpp @@ -0,0 +1,168 @@ +/* + * LfoGraph.cpp - Displays LFO graphs + * + * Copyright (c) 2004-2014 Tobias Doerffel + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "LfoGraph.h" + +#include +#include + +#include "EnvelopeAndLfoParameters.h" +#include "Oscillator.h" + +#include "gui_templates.h" + +namespace lmms +{ + +extern const float SECS_PER_LFO_OSCILLATION; + +namespace gui +{ + +LfoGraph::LfoGraph(QWidget* parent) : + QWidget(parent), + ModelView(nullptr, this) +{ + setFixedSize(m_lfoGraph.size()); +} + +void LfoGraph::modelChanged() +{ + m_params = castModel(); +} + +void LfoGraph::mousePressEvent(QMouseEvent* me) +{ + if (me->button() == Qt::LeftButton) + { + toggleAmountModel(); + } +} + +void LfoGraph::paintEvent(QPaintEvent*) +{ + QPainter p{this}; + p.setRenderHint(QPainter::Antialiasing); + + // Draw the graph background + p.drawPixmap(rect(), m_lfoGraph); + + const auto* params = castModel(); + if (!params) { return; } + + const float amount = params->getLfoAmountModel().value(); + const float lfoSpeed = params->getLfoSpeedModel().value(); + const f_cnt_t predelayFrames = params->getLfoPredelayFrames(); + const f_cnt_t attackFrames = params->getLfoAttackFrames(); + const f_cnt_t oscillationFrames = params->getLfoOscillationFrames(); + const bool x100 = params->getX100Model().value(); + const int waveModel = params->getLfoWaveModel().value(); + + int LFO_GRAPH_W = m_lfoGraph.width() - 3; // subtract border + int LFO_GRAPH_H = m_lfoGraph.height() - 6; // subtract border + int graph_x_base = 2; + int graph_y_base = 3 + LFO_GRAPH_H / 2; + + const float frames_for_graph = + SECS_PER_LFO_OSCILLATION * Engine::audioEngine()->baseSampleRate() / 10; + + const float gray = 1.0 - fabsf(amount); + const auto red = static_cast(96 * gray); + const auto green = static_cast(255 - 159 * gray); + const auto blue = static_cast(128 - 32 * gray); + const QColor penColor(red, green, blue); + p.setPen(QPen(penColor, 1.5)); + + float osc_frames = oscillationFrames; + + if (x100) { osc_frames *= 100.0f; } + + float old_y = 0; + for (int x = 0; x <= LFO_GRAPH_W; ++x) + { + float val = 0.0; + float cur_sample = x * frames_for_graph / LFO_GRAPH_W; + if (static_cast(cur_sample) > predelayFrames) + { + float phase = (cur_sample -= predelayFrames) / osc_frames; + switch (static_cast(waveModel)) + { + case EnvelopeAndLfoParameters::LfoShape::SineWave: + default: + val = Oscillator::sinSample(phase); + break; + case EnvelopeAndLfoParameters::LfoShape::TriangleWave: + val = Oscillator::triangleSample(phase); + break; + case EnvelopeAndLfoParameters::LfoShape::SawWave: + val = Oscillator::sawSample(phase); + break; + case EnvelopeAndLfoParameters::LfoShape::SquareWave: + val = Oscillator::squareSample(phase); + break; + case EnvelopeAndLfoParameters::LfoShape::RandomWave: + if (x % (int)(900 * lfoSpeed + 1) == 0) + { + m_randomGraph = Oscillator::noiseSample(0.0); + } + val = m_randomGraph; + break; + case EnvelopeAndLfoParameters::LfoShape::UserDefinedWave: + val = Oscillator::userWaveSample(m_params->getLfoUserWave().get(), phase); + break; + } + + if (static_cast(cur_sample) <= attackFrames) + { + val *= cur_sample / attackFrames; + } + } + + float cur_y = -LFO_GRAPH_H / 2.0f * val; + p.drawLine(QLineF(graph_x_base + x - 1, graph_y_base + old_y, graph_x_base + x, graph_y_base + cur_y)); + old_y = cur_y; + } + + // Draw the info text + int ms_per_osc = static_cast(SECS_PER_LFO_OSCILLATION * lfoSpeed * 1000.0); + + QFont f = p.font(); + f.setPixelSize(height() * 0.2); + p.setFont(f); + p.setPen(QColor(201, 201, 225)); + p.drawText(4, m_lfoGraph.height() - 6, tr("%1 ms/LFO").arg(ms_per_osc)); +} + +void LfoGraph::toggleAmountModel() +{ + auto* params = castModel(); + auto& lfoAmountModel = params->getLfoAmountModel(); + + lfoAmountModel.setValue(lfoAmountModel.value() < 1.0 ? 1.0 : 0.0); +} + +} // namespace gui + +} // namespace lmms From ba4fda7c977926fd51664b99aca5e991019339d9 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Fri, 5 Apr 2024 12:48:46 +0200 Subject: [PATCH 131/191] Scalable consistent faders with themeable gradients, marker at unity, dbFS by default (#7045) * Render fader levels in code with a gradient Render the fader level in code using a gradient instead of using pixmaps. The problem with the pixmaps is that they don't "know" how a fader instance is configured with regards to the minimum and maximum value. This means that the display can give quite a wrong impression. The rendering of levels has been unified in the method `paintLevels`. It can render using dbFS and linear scale. The method `paintLinearLevels` has been removed completely, i.e. there's no more code that renders using pixmaps. Much of the previous code relied on the size of the background image `fader_background.png`, e.g. the initialization of the size. For now the `Fader` widget is initially resized to the size of that background image as it is present in the default and classic theme (see `Fader::init`). All rendering uses the size of the widget itself to determine where to draw what. This means that the widget is prepared to be resizable. The method `paintLevels` first renders the background of the level indicators and uses these as clipping paths for all other rendering operations, e.g. for the rendering of the levels themselves. Levels are rendered using a gradient which is defined with the following stops: * Two stops for the ok levels. * One stop for warning levels. * One stop for clipping levels. Peak indicators do not use the three distinct colors anymore but instead use the color of the gradient at that position of the peak. This makes everything look "smooth". The code now also renders a marker at unity position, i.e. at position 1.0 in linear levels and 0 dbFS in dbFS scale. The painting code makes lots of use of the class `PaintHelper`. This class is configured with a minimum and maximum value and can then return linear factors for given values. There are two supported modes: * Map min to 0 and max to 1 * Map min to 1 and max to 0 It can also compute rectangles that correspond to a given value. These methods can be given rectangles that are supposed to represent the span from min to max. The returned result is then a rectangle that fills the lower part of the source rectangle according to the given value with regards to min and max (`getMeterRect`). Another method returns a rectangle of height 1 which lies inside the given source rectangle at the corresponding level (`getPersistentPeakRect`). The method `paintLevels` uses a mapping function to map the amplitude values (current peak value, persistent peak, etc.) to the display values. There's one mapper that keeps the original value and it is used to display everything in a linear scale. Another mapper maps everything to dbFS and uses these values as display everything in a dbFS scale. The following values must be mapped for the left and right channel to make this work: * Min and max display values (min and max peak values) * The current peak value * The persistent peak value * The value for unity, i.e. 1.0 in linear levels and 0 dbFS in dbFS scale. Remove the method `calculateDisplayPeak` which was used in the old method to render linear levels. `Fader::setPeak` now uses `std::clamp` instead of doing "manual" comparisons. The LMMS plugins Compressor, EQ and Delay are still configured to use linear displays. It should be considered to switch them to dbFS/logarithmic displays as well and to remove the code that renders linearly. * Remove unused pixmaps from `Fader` Remove the now unused pixmaps for the background and the LEDs from the `Fader` class and remove the files from the default and classic theme directories. * Rename peak properties and use them to render levels Rename the peak properties as follows: * peakGreen -> peakOk * peakRed -> peakClip * peakYellow -> peakWarn The reasoning is that a style might for example use a different color than green to indicate levels that are ok. Use the properties to initialize the gradient that is used to render the levels. Initialize the properties to the colors of the current default theme so that it's not mandatory to set them in a style sheet. Up until now they have all been initialized as black. * Always render the knob in the middle of the fader Render the knob in the middle of the fader regardless of the width. The previous implementation was dependent on the fader pixmap having a matching width because it always rendered at x=0. * Set size policy of fader to minimum expanding Set the size policy of the fader to minimum expanding in both directions. This will make the fader grow in layouts if there is space. * Default dbFS levels and better peak values Default to dbFS levels for all faders and set some better minimum and maximum peak values. * Fix faders of Crossover EQ Fix the faders of the Crossover EQ which were initialized and rendered much too wide and with a line at unity. The large width also resulted in the knobs being rendered outside of view. Resize the fader to the minimum size so that it is constructed at a sane default. Introduce a property that allows to control if the unity line is rendered. The property is available in style sheets and defaults to the unity lines being rendered. Adjust the paint code to evaluate the property. Initialize the faders of the Crossover EQ such that the unity line is not drawn. * Remove EqFader constructor with pixmaps Remove the constructor of `EqFader` that takes the pixmaps to the fader background, leds and knob. The background and leds pixmaps are not used by the base class `Fader` for rendering anymore to make the `Fader` resizable. A pixmap is still used to render the knob but the constructor that takes the knob as an argument does not do anything meaningful with it, i.e. all faders are rendered with the default knob anyway. Remove the resources for the fader background, leds and knob as they are not used and the knob was the same image as the default knob anyway. Remove the static pixmaps from the constructor of `EqControlsDialog`. Switch the instantiations of the EQ's faders to use the remaining constructor of `EqFader`. This constructor sets a different fixed size of (23, 116) compared to the removed constructor which set a size of (23, 80). Therefore all faders that used the removed constructor are now set explicitly to a fixed size of (23, 80). The constructor that's now used also calls a different base constructor than the removed one. The difference between the two base constructors of `Fader` is that one of them sets the member `m_conversionFactor` to 100.0 whereas the other one keeps the default of 1.0. The adjusted faders in `EqControlsDialog` are thus now constructed with the conversion factor set to 100. However, all of them already call `setDisplayConversion` with `false` after construction which results in the conversion factor being reset to 1.0. So the result should be the same as before the changes. * Remove background and LEDs pixmap from Fader constructor Remove the parameters for the background and LEDs pixmap from the second `Fader` constructor. Make the knob pixmap parameter in the constructor a const reference. Assign the reference to the knob pixmap of the `Fader` itself. This enables clients to use their own fader knobs as is the case with the Crossover EQ. The EQ now renders using it's own knobs again. Make the second constructor delegate to the first one. This will additionally set the conversion factor to 100 but this is not a problem with the current code because the only user of the second constructor, the Crossover EQ, already calls `setDisplayConversion` with the parameter set to `false`, hence reinstating a conversion factor of 1. Remove the resources for the background and LEDs from the Crossover EQ as they are not used anymore. Remove the three QPixmap members from `CrossoverEQControlDialog` as they are not needed. The background and LEDs are not used anyway and the knob is passed in as a constant reference which is copied. Hence we can use a local variable in the constructor of `CrossoverEQControlDialog`. * Remove the init method from Fader Remove the `init` method from `Fader` as it is not needed anymore due to the constructor delegation. Tidy up the parameter lists and use of spaces in the constructor. * Introduce range with solid warn color Introduce a second point in the gradient for the warn colors so that we get a certain range with the full/solid warn color. The colors are distributed as follows now. The solid ok range goes from -inf dbFS to -12 dbFS. The warn range goes from -6 dbFS to 0 dbFS. In between the colors are interpolated. Values above 0 dbFS interpolate from the warn color to the clip color. This is now quite similar to the previous implementation. # Analysis of the previous pixmap implementation The pixmap implementation used pixmaps with a height of 116 pixels to map 51 dbFS (-42 dbFS to 9 dbFS) across the whole height. The pixels of the LED pixmap were distributed as follows along the Y-axis: * Margin: 4 * Red: 18 * Yellow: 14 * Green: 76 * Margin: 4 Due to the margins the actual red, yellow and green areas only represent a range of (1 - (4+4) / 116) * 51 ~ 47,48 dbFS. This range is distributed as follows across the colors: Red: 7.91 dbFS Yellow: 6.16 dbFS Green: 33.41 dbFS The borders between the colors are located along the following dbFS values: * Red/yellow: 9 - (4 + 18) / 116 * 51 dbFS ~ -0.67 dbFS * Yellow/green: 9 - (4 + 18 + 14) / 116 * 51 dbFS ~ -6.83 dbFS * The green marker is rendered for values above -40.24 dbFS. * Remove unused method Fader::clips * Fader: Correctly render arbitrary ranges Adjust the `Fader` so that it can correctly render arbitrary ranges of min and max peak values, e.g. that it would render a display range of [-12 dbFS, -42 dbFS] correctly. Until now the gradient was defined to start at the top of the levels rectangle and end at the bottom. As a result the top was always rendered in the "clip" color and the bottom in the "ok" color. However, this is wrong, e.g. if we configure the `Fader` with a max value of -12 dbFS and a min value of -42 dbFS. In that case the whole range of the fader should be rendered with the "ok" color. The fix is to compute the correct window coordinates of the start and end point of gradient using from the "window" of values that the `Fader` displays and then to map the in-between colors accordingly. See the added comments in the code for more details. Add the templated helper class `LinearMap` to `lmms_math.h`. The class defines a linear function/map which is initialized using two points. With the `map` function it is then possible to evaluate arbitrary X-coordinates. * Remove unused methods in PaintHelper Remove the now unused mapping methods from `PaintHelper`. Their functionality has been replaced with the usage of `LinearMap` in the code. * Fix some builds Include `cassert` for some builds that otherwise fail. * Opaque unity marker with styling option Make the unity marker opaque by default and enable to style it with the style sheets. None of the two style sheets uses this option though. * Darker default color for the unity line * Move code Move the computation of most mapped values at the top right after the definition of the mapper so that it is readily available in all phases of the painting code. * Render unity lines in background Render the unity lines before rendering the levels so that they get overdrawn and do not stick out when they are crossed. * Don't draw transparent white lines anymore Don't draw the transparent white lines anymore as they were mostly clipped anyway and only create "smudge". * Full on clip color at unity Adjust the gradient so that the full on clip color shows starting at unity. There is only a very short transition from the end of warning to clipping making it look like a solid color "standing" on top of a gradient. * Fix discrepancy between levels and unity markers This commit removes the helper class `PaintHelper` and now uses two lambdas to compute the rectangles for the peak indicators and levels. It uses the linear map which maps the peak values (in dbFS or linear) to window coordinates of the widget. The change fixes a discrepancy in the following implementation for which the full on clip rectangle rendered slightly below the unity marker. * Fix fader display for Equalizer shelves and peaks The peak values for the shelves and peaks of the Equalizer plugin are computed in `EqEffect::peakBand`. The previous implementation evaluated the bins of the corresponding frequency spectrum and determined the "loudest" one. The value of this bin was then converted to dbFS and mapped to the interval [0, inf[ where all values less or equal to -60 dbFS were mapped to 0 and a value of 40 dbFS was mapped to 1. So effectively everything was mapped somewhere into [0, 1] yet in a quite "distorted" way because a signal of 40 dbFS resulted in being displayed as unity in the fader. This commit directly returns the value of the maximum bin, i.e. it does not map first to dbFS and then linearize the result anymore. This should work because the `Fader` class assumes a "linear" input signal and if the value of the bin was previously mapped to dbFS it should have some "linear" character. Please note that this is still somewhat of a "proxy" value because ideally the summed amplitude of all relevant bins in the frequency range would be shown and not just the "loudest" one. ## Other changes Rename `peakBand` to `linearPeakBand` to make more clear that a linear value is returned. Handle a potential division by zero by checking the value of `fft->getEnergy()` before using it. Index into `fft->m_bands` so that no parallel incrementing of the pointer is needed. This also enables the removal of the local variable `b`. * Improve the rendering of the levels The levels rendering now more explicitly distinguished between the rendering of the level outline/border and the level meters. The level rectangles are "inset" with regards to the borders so that there is a margin between the level borders and the meter readings. This margin is now also applied to the top and bottom of the levels. Levels are now also rendered as rounded rectangles similar to the level borders. Only render the levels and peaks if their values are greater than the minimum level. Make the radius of the rounded rectangles more pronounced by increasing its value from 1 to 2. Decrease the margins so that the level readings become wider, i.e. so that they are rendered with more pixels. Add the lambda `computeLevelMarkerRect` so that the rendering of the level markers is more decoupled from the rendering of the peak markers. * Reduce code repetition Reduce code repetition in `EqEffect::setBandPeaks` by introducing a lambda. Adjust code formatting. * Code review changes Code review changes in `Fader.cpp`. Mostly whitespace adjustments. Split up the calculation of the meter width to make it more understandable. This also reduces the number of parentheses. * Use MEMBER instead of READ/WRITE Use `MEMBER` instead of `READ`/`WRITE` for some properties that are not called explicitly from outside of the class. * Use default member initializers for Fader Use default member initializers for the members in `Fader` that have previously been initialized in the constructor list. * Make code clearer Make code clearer in `Fader::FadermouseDoubleClickEvent`. Only divide if the dialog was accepted with OK. --- data/themes/classic/fader_background.png | Bin 1391 -> 0 bytes data/themes/classic/fader_leds.png | Bin 3638 -> 0 bytes data/themes/classic/style.css | 6 +- data/themes/default/fader_background.png | Bin 905 -> 0 bytes data/themes/default/fader_leds.png | Bin 295 -> 0 bytes data/themes/default/style.css | 6 +- include/Fader.h | 99 ++-- include/lmms_math.h | 31 +- .../CrossoverEQ/CrossoverEQControlDialog.cpp | 19 +- .../CrossoverEQ/CrossoverEQControlDialog.h | 6 - plugins/CrossoverEQ/fader_bg.png | Bin 340 -> 0 bytes plugins/CrossoverEQ/fader_empty.png | Bin 230 -> 0 bytes plugins/Eq/EqControls.h | 2 + plugins/Eq/EqControlsDialog.cpp | 17 +- plugins/Eq/EqEffect.cpp | 62 +-- plugins/Eq/EqEffect.h | 2 +- plugins/Eq/EqFader.h | 14 - plugins/Eq/faderback.png | Bin 700 -> 0 bytes plugins/Eq/faderknob.png | Bin 350 -> 0 bytes plugins/Eq/faderleds.png | Bin 280 -> 0 bytes src/gui/MixerChannelView.cpp | 3 - src/gui/widgets/Fader.cpp | 437 ++++++++---------- 22 files changed, 328 insertions(+), 376 deletions(-) delete mode 100644 data/themes/classic/fader_background.png delete mode 100644 data/themes/classic/fader_leds.png delete mode 100644 data/themes/default/fader_background.png delete mode 100644 data/themes/default/fader_leds.png delete mode 100644 plugins/CrossoverEQ/fader_bg.png delete mode 100644 plugins/CrossoverEQ/fader_empty.png delete mode 100644 plugins/Eq/faderback.png delete mode 100755 plugins/Eq/faderknob.png delete mode 100644 plugins/Eq/faderleds.png diff --git a/data/themes/classic/fader_background.png b/data/themes/classic/fader_background.png deleted file mode 100644 index 682ff4c92815efe638b9adfd9b7ef723639ccd3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1391 zcmV-#1(5oQP))#srQ|EuLfBj_~LCj!sG-WQZtWe9i zyuJNU$LV|qlYoSghU9N)8K#Qc_4?`TPe%SSf|(t?2#Og@6{?EMUoSk{gIDj>v0lp-I7glz0>F8`3#|@TfE-h>j(6?2#6RgwdPMuXx%MYVW!Z0NX^-i z3o~pk(5z5XT;Kk_Q}b7Fyd@0@Sp&KDulWRK0*Qc$QHuAun&EN>`E-5-6M3Jf1T}@_ z3{}JJcIk-xH2PNbUuB{U+d)p`tW}vvV=KOQt@&v+q0EFb&y5!^mpjO(*T%PL7@%rx zLOUaW86oTw$IO_EdGGEp5wXw2kO&%QgZ;Zs=;RX@TX8l=t~>jjEAmfUQE|IoI@g?o zKZyl%DUSTXZt~}`^wxPoC!fyP7q#Z5 zd8f~%)m&qP`fj-#uvOQw%|L$9{_BgYIZh2BG z#)Q^hfA8;!!c{Pd`_SOTO?@7mn}*apsOH{o?i4<>PH8EwavyC)mRfV4PSxHucirNh z!pyKzT)FCYw-CD`riwh=-x0`HZmyK3T=zJXL!VdOa)jbq^!aD@w{6|R^qKu@YH>{7 zA~B(*KtqdcbEuznPn28TJ5OlTTq3txJkj<(uNyRxcT0}C_h$deC$!})*4)CLv*4Oq z#76W9y>#vtlGrUxab=k9_)xBH&K-&$OIvA_K(DE zJ1L4cP03w|+7Y?pI)$dC&uvHLRxO{>;^z9N(5F@2BH=7l?V&zrj^tmLyrr*$UiSHl z{g)fAjihL`TY^9P@AHbhvj1!)lAlMnEsSBs{s$nhcXRA$JU{mgWjo^{pX#A{|0C|^ z+=ohY#Wg>FeA^;zLRA&HdunltJBvD{rQTVn;(8~A9;erEf9v~DulxH!`rOekiRiY2 zW?H3l?atz;`%rfaj53?==Lh;+gpmgFdLMHi8qT7+1yBC%i4>Y5`OmG_kC;%OxR!B6 zs=TLiNmdz`<^z%<=TK@|agNkNcT$Tw(VqOfR#da@96Gt$TB}^m$k{e`i%8rmu2pXT zTgBMt6uoO@{vTR#KvoOYb%-aDCF3f^pxL}zHop=Puu$uBJ*dyemim+UO>Rwp_EYGZ zTI81gA8=>!MW366o7>EC1Nlmys~*Ar68l_O+7}DYWV!yuLgeS&YrZhil0sJ`n?AJN xS)|pS#g9z%@c_lg7v#^L|9Kzg{d(>`{{j1^Dm5N5f$IPO002ovPDHLkV1jua!}|aL diff --git a/data/themes/classic/fader_leds.png b/data/themes/classic/fader_leds.png deleted file mode 100644 index 6c673cf36d1841a9c2206f6ac523fe3618405624..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3638 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 diff --git a/data/themes/default/fader_leds.png b/data/themes/default/fader_leds.png deleted file mode 100644 index 707902e04c62f0158a7e6690497d68a1f00e20c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^;y_%&!3HGryjF$)DVAa<&kznEsNqQI0P;BtJR*yM z>aT+^qm#z$3ZS55iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0vZJY5_^ zG8*6BQsg^iAj0-w+JX%%&5!vwM8x{e=x`mB-4MeNdu3W{?~-Im&&}^wK70Cp`m@e! zCd;NA3fVftM^7sLTKDUB1)PfWc1;YCmEZn3-CX^?Rb_lZ%>Fww4?irh*b{r!d&|yz z_S`ckr(26z$k;{i*(v({{MG9hvhTiq`sB1Mw+vr9NFgd!^Q~glFXjE;7ys|7KYrge XUC}b^)r6lw_cD08`njxgN@xNA2mo}K diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 89c14f20a..f13ec09d8 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -694,9 +694,9 @@ lmms--gui--MixerChannelView QGraphicsView { /* persistent peak markers for fx peak meters */ lmms--gui--Fader { - qproperty-peakGreen: #0ad45c; - qproperty-peakYellow: #d6ec52; - qproperty-peakRed: #c12038; + qproperty-peakOk: #0ad45c; + qproperty-peakWarn: #d6ec52; + qproperty-peakClip: #c12038; } lmms--gui--TimeLineWidget { diff --git a/include/Fader.h b/include/Fader.h index 20132f71d..a3158a8b4 100644 --- a/include/Fader.h +++ b/include/Fader.h @@ -54,6 +54,7 @@ #include "AutomatableModelView.h" #include "embed.h" +#include "lmms_math.h" namespace lmms::gui @@ -66,21 +67,21 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView { Q_OBJECT public: - Q_PROPERTY( QColor peakGreen READ peakGreen WRITE setPeakGreen ) - Q_PROPERTY( QColor peakRed READ peakRed WRITE setPeakRed ) - Q_PROPERTY( QColor peakYellow READ peakYellow WRITE setPeakYellow ) - Q_PROPERTY( bool levelsDisplayedInDBFS READ getLevelsDisplayedInDBFS WRITE setLevelsDisplayedInDBFS ) + Q_PROPERTY(QColor peakOk MEMBER m_peakOk) + Q_PROPERTY(QColor peakClip MEMBER m_peakClip) + Q_PROPERTY(QColor peakWarn MEMBER m_peakWarn) + Q_PROPERTY(bool levelsDisplayedInDBFS MEMBER m_levelsDisplayedInDBFS) + Q_PROPERTY(bool renderUnityLine READ getRenderUnityLine WRITE setRenderUnityLine) + Q_PROPERTY(QColor unityMarker MEMBER m_unityMarker) - Fader( FloatModel * _model, const QString & _name, QWidget * _parent ); - Fader( FloatModel * _model, const QString & _name, QWidget * _parent, QPixmap * back, QPixmap * leds, QPixmap * knob ); + Fader(FloatModel* model, const QString& name, QWidget* parent); + Fader(FloatModel* model, const QString& name, QWidget* parent, const QPixmap& knob); ~Fader() override = default; - void init(FloatModel * model, QString const & name); - - void setPeak_L( float fPeak ); + void setPeak_L(float fPeak); float getPeak_L() { return m_fPeakValue_L; } - void setPeak_R( float fPeak ); + void setPeak_R(float fPeak); float getPeak_R() { return m_fPeakValue_R; } inline float getMinPeak() const { return m_fMinPeak; } @@ -89,43 +90,31 @@ public: inline float getMaxPeak() const { return m_fMaxPeak; } inline void setMaxPeak(float maxPeak) { m_fMaxPeak = maxPeak; } - QColor const & peakGreen() const; - void setPeakGreen( const QColor & c ); + inline bool getRenderUnityLine() const { return m_renderUnityLine; } + inline void setRenderUnityLine(bool value = true) { m_renderUnityLine = value; } - QColor const & peakRed() const; - void setPeakRed( const QColor & c ); - - QColor const & peakYellow() const; - void setPeakYellow( const QColor & c ); - - inline bool getLevelsDisplayedInDBFS() const { return m_levelsDisplayedInDBFS; } - inline void setLevelsDisplayedInDBFS(bool value = true) { m_levelsDisplayedInDBFS = value; } - - void setDisplayConversion( bool b ) + void setDisplayConversion(bool b) { m_conversionFactor = b ? 100.0 : 1.0; } - inline void setHintText( const QString & _txt_before, - const QString & _txt_after ) + inline void setHintText(const QString& txt_before, + const QString& txt_after) { - setDescription( _txt_before ); - setUnit( _txt_after ); + setDescription(txt_before); + setUnit(txt_after); } private: - void contextMenuEvent( QContextMenuEvent * _me ) override; - void mousePressEvent( QMouseEvent *ev ) override; - void mouseDoubleClickEvent( QMouseEvent* mouseEvent ) override; - void mouseMoveEvent( QMouseEvent *ev ) override; - void mouseReleaseEvent( QMouseEvent * _me ) override; - void wheelEvent( QWheelEvent *ev ) override; - void paintEvent( QPaintEvent *ev ) override; + void contextMenuEvent(QContextMenuEvent* me) override; + void mousePressEvent(QMouseEvent* ev) override; + void mouseDoubleClickEvent(QMouseEvent* mouseEvent) override; + void mouseMoveEvent(QMouseEvent* ev) override; + void mouseReleaseEvent(QMouseEvent* me) override; + void wheelEvent(QWheelEvent* ev) override; + void paintEvent(QPaintEvent* ev) override; - inline bool clips(float const & value) const { return value >= 1.0f; } - - void paintDBFSLevels(QPaintEvent *ev, QPainter & painter); - void paintLinearLevels(QPaintEvent *ev, QPainter & painter); + void paintLevels(QPaintEvent* ev, QPainter& painter, bool linear = false); int knobPosY() const { @@ -135,37 +124,37 @@ private: return height() - ((height() - m_knob.height()) * (realVal / fRange)); } - void setPeak( float fPeak, float &targetPeak, float &persistentPeak, QElapsedTimer &lastPeakTimer ); - int calculateDisplayPeak( float fPeak ); + void setPeak(float fPeak, float& targetPeak, float& persistentPeak, QElapsedTimer& lastPeakTimer); void updateTextFloat(); // Private members private: - float m_fPeakValue_L; - float m_fPeakValue_R; - float m_persistentPeak_L; - float m_persistentPeak_R; - float m_fMinPeak; - float m_fMaxPeak; + float m_fPeakValue_L {0.}; + float m_fPeakValue_R {0.}; + float m_persistentPeak_L {0.}; + float m_persistentPeak_R {0.}; + float m_fMinPeak {dbfsToAmp(-42)}; + float m_fMaxPeak {dbfsToAmp(9)}; QElapsedTimer m_lastPeakTimer_L; QElapsedTimer m_lastPeakTimer_R; - QPixmap m_back; - QPixmap m_leds; - QPixmap m_knob; + QPixmap m_knob {embed::getIconPixmap("fader_knob")}; - bool m_levelsDisplayedInDBFS; + bool m_levelsDisplayedInDBFS {true}; - int m_moveStartPoint; - float m_startValue; + int m_moveStartPoint {-1}; + float m_startValue {0.}; - static SimpleTextFloat * s_textFloat; + static SimpleTextFloat* s_textFloat; - QColor m_peakGreen; - QColor m_peakRed; - QColor m_peakYellow; + QColor m_peakOk {10, 212, 92}; + QColor m_peakClip {193, 32, 56}; + QColor m_peakWarn {214, 236, 82}; + QColor m_unityMarker {63, 63, 63, 255}; + + bool m_renderUnityLine {true}; } ; diff --git a/include/lmms_math.h b/include/lmms_math.h index f6455d693..3f58e3b75 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -32,6 +32,7 @@ #include "lmms_constants.h" #include "lmmsconfig.h" +#include namespace lmms { @@ -269,18 +270,18 @@ static inline float safeDbfsToAmp( float dbfs ) //! @brief Converts linear amplitude (>0-1.0) to dBFS scale. //! @param amp Linear amplitude, where 1.0 = 0dBFS. ** Must be larger than zero! ** //! @return Amplitude in dBFS. -static inline float ampToDbfs( float amp ) +static inline float ampToDbfs(float amp) { - return log10f( amp ) * 20.0f; + return log10f(amp) * 20.0f; } //! @brief Converts dBFS-scale to linear amplitude with 0dBFS = 1.0 //! @param dbfs The dBFS value to convert. ** Must be a real number - not inf/nan! ** //! @return Linear amplitude -static inline float dbfsToAmp( float dbfs ) +static inline float dbfsToAmp(float dbfs) { - return std::pow(10.f, dbfs * 0.05f ); + return std::pow(10.f, dbfs * 0.05f); } @@ -352,6 +353,28 @@ static inline int numDigitsAsInt(float f) return digits; } +template +class LinearMap +{ +public: + LinearMap(T x1, T y1, T x2, T y2) + { + T const dx = x2 - x1; + assert (dx != T(0)); + + m_a = (y2 - y1) / dx; + m_b = y1 - m_a * x1; + } + + T map(T x) const + { + return m_a * x + m_b; + } + +private: + T m_a; + T m_b; +}; } // namespace lmms diff --git a/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp b/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp index 12b560b23..a4f44f5d3 100644 --- a/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp +++ b/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp @@ -32,6 +32,9 @@ #include "Knob.h" #include "Fader.h" +#include + + namespace lmms::gui { @@ -64,30 +67,32 @@ CrossoverEQControlDialog::CrossoverEQControlDialog( CrossoverEQControls * contro xover34->setLabel( "3/4" ); xover34->setHintText( tr( "Band 3/4 crossover:" ), " Hz" ); - m_fader_bg = QPixmap( PLUGIN_NAME::getIconPixmap( "fader_bg" ) ); - m_fader_empty = QPixmap( PLUGIN_NAME::getIconPixmap( "fader_empty" ) ); - m_fader_knob = QPixmap( PLUGIN_NAME::getIconPixmap( "fader_knob2" ) ); + QPixmap const fader_knob(PLUGIN_NAME::getIconPixmap("fader_knob2")); // faders - auto gain1 = new Fader(&controls->m_gain1, tr("Band 1 gain"), this, &m_fader_bg, &m_fader_empty, &m_fader_knob); + auto gain1 = new Fader(&controls->m_gain1, tr("Band 1 gain"), this, fader_knob); gain1->move( 7, 56 ); gain1->setDisplayConversion( false ); gain1->setHintText( tr( "Band 1 gain:" ), " dBFS" ); + gain1->setRenderUnityLine(false); - auto gain2 = new Fader(&controls->m_gain2, tr("Band 2 gain"), this, &m_fader_bg, &m_fader_empty, &m_fader_knob); + auto gain2 = new Fader(&controls->m_gain2, tr("Band 2 gain"), this, fader_knob); gain2->move( 47, 56 ); gain2->setDisplayConversion( false ); gain2->setHintText( tr( "Band 2 gain:" ), " dBFS" ); + gain2->setRenderUnityLine(false); - auto gain3 = new Fader(&controls->m_gain3, tr("Band 3 gain"), this, &m_fader_bg, &m_fader_empty, &m_fader_knob); + auto gain3 = new Fader(&controls->m_gain3, tr("Band 3 gain"), this, fader_knob); gain3->move( 87, 56 ); gain3->setDisplayConversion( false ); gain3->setHintText( tr( "Band 3 gain:" ), " dBFS" ); + gain3->setRenderUnityLine(false); - auto gain4 = new Fader(&controls->m_gain4, tr("Band 4 gain"), this, &m_fader_bg, &m_fader_empty, &m_fader_knob); + auto gain4 = new Fader(&controls->m_gain4, tr("Band 4 gain"), this, fader_knob); gain4->move( 127, 56 ); gain4->setDisplayConversion( false ); gain4->setHintText( tr( "Band 4 gain:" ), " dBFS" ); + gain4->setRenderUnityLine(false); // leds auto mute1 = new LedCheckBox("", this, tr("Band 1 mute"), LedCheckBox::LedColor::Green); diff --git a/plugins/CrossoverEQ/CrossoverEQControlDialog.h b/plugins/CrossoverEQ/CrossoverEQControlDialog.h index 9ddb5d9bf..0f25600f9 100644 --- a/plugins/CrossoverEQ/CrossoverEQControlDialog.h +++ b/plugins/CrossoverEQ/CrossoverEQControlDialog.h @@ -27,7 +27,6 @@ #ifndef CROSSOVEREQ_CONTROL_DIALOG_H #define CROSSOVEREQ_CONTROL_DIALOG_H -#include #include "EffectControlDialog.h" namespace lmms @@ -46,11 +45,6 @@ class CrossoverEQControlDialog : public EffectControlDialog public: CrossoverEQControlDialog( CrossoverEQControls * controls ); ~CrossoverEQControlDialog() override = default; - -private: - QPixmap m_fader_bg; - QPixmap m_fader_empty; - QPixmap m_fader_knob; }; diff --git a/plugins/CrossoverEQ/fader_bg.png b/plugins/CrossoverEQ/fader_bg.png deleted file mode 100644 index ca4eedafdc951c9741a3a9b3e520de47f1c8adc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^3P7B~!3HFsZ#L=!QY^(zo*^7SP{WbZ0pxQQctjQh z)n5l;MkkHg6+l7B64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1Spdb&7< zWHi3L<;d0Kz~FGPr|-&4tB~|pe%mr1F3MpO*MD)|Es)**-}wU#r+2MWw>vKc_^l17D?|1H qmxTi^lYZPQczOR51JjS{2KC%hoB3*oLR^5JWAJqKb6Mw<&;$U+GI-kn diff --git a/plugins/CrossoverEQ/fader_empty.png b/plugins/CrossoverEQ/fader_empty.png deleted file mode 100644 index 797a0d3bc2d06622281319ac066f484753e8cb6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^3P7B~!3HFsZ#L=!Qfx`y?k)`fL2$v|<&%LToCO|{ z#S9GG!XV7ZFl&wkP>{XE)7O>#0f(rF9y@dPsa&9tWQl7;iF1B#Zfaf$gL6@8Vo7R> zLV0FMhJw4NZ$Nk>pEyvFpQnpsNXEUl=L{Jc7&w?0Wd1*A+{UkxaO>ISXEPf}1P2(a XT$qnaRDQSyG?~HE)z4*}Q$iB}YtT!* diff --git a/plugins/Eq/EqControls.h b/plugins/Eq/EqControls.h index 6db82f3e3..80680c7fb 100644 --- a/plugins/Eq/EqControls.h +++ b/plugins/Eq/EqControls.h @@ -66,6 +66,8 @@ public: float m_inPeakR; float m_outPeakL; float m_outPeakR; + + // The following are linear peaks float m_lowShelfPeakL, m_lowShelfPeakR; float m_para1PeakL, m_para1PeakR; float m_para2PeakL, m_para2PeakR; diff --git a/plugins/Eq/EqControlsDialog.cpp b/plugins/Eq/EqControlsDialog.cpp index 634bde846..17de9ce98 100644 --- a/plugins/Eq/EqControlsDialog.cpp +++ b/plugins/Eq/EqControlsDialog.cpp @@ -72,18 +72,16 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : setBand( 6, &controls->m_highShelfActiveModel, &controls->m_highShelfFreqModel, &controls->m_highShelfResModel, &controls->m_highShelfGainModel, QColor(255 ,255, 255), tr( "High-shelf" ), &controls->m_highShelfPeakL, &controls->m_highShelfPeakR,0,0,0,0,0,0 ); setBand( 7, &controls->m_lpActiveModel, &controls->m_lpFreqModel, &controls->m_lpResModel, 0, QColor(255 ,255, 255), tr( "LP" ) ,0,0,0,0,0, &controls->m_lp12Model, &controls->m_lp24Model, &controls->m_lp48Model); - static auto s_faderBg = PLUGIN_NAME::getIconPixmap("faderback"); - static auto s_faderLeds = PLUGIN_NAME::getIconPixmap("faderleds"); - static auto s_faderKnob = PLUGIN_NAME::getIconPixmap("faderknob"); + QSize const faderSize(23, 80); - auto GainFaderIn = new EqFader(&controls->m_inGainModel, tr("Input gain"), this, &s_faderBg, &s_faderLeds, &s_faderKnob, - &controls->m_inPeakL, &controls->m_inPeakR); + auto GainFaderIn = new EqFader(&controls->m_inGainModel, tr("Input gain"), this, &controls->m_inPeakL, &controls->m_inPeakR); + GainFaderIn->setFixedSize(faderSize); GainFaderIn->move( 23, 295 ); GainFaderIn->setDisplayConversion( false ); GainFaderIn->setHintText( tr( "Gain" ), "dBv"); - auto GainFaderOut = new EqFader(&controls->m_outGainModel, tr("Output gain"), this, &s_faderBg, &s_faderLeds, &s_faderKnob, - &controls->m_outPeakL, &controls->m_outPeakR); + auto GainFaderOut = new EqFader(&controls->m_outGainModel, tr("Output gain"), this, &controls->m_outPeakL, &controls->m_outPeakR); + GainFaderOut->setFixedSize(faderSize); GainFaderOut->move( 453, 295); GainFaderOut->setDisplayConversion( false ); GainFaderOut->setHintText( tr( "Gain" ), "dBv" ); @@ -92,8 +90,9 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : int distance = 126; for( int i = 1; i < m_parameterWidget->bandCount() - 1; i++ ) { - auto gainFader = new EqFader(m_parameterWidget->getBandModels(i)->gain, tr(""), this, &s_faderBg, &s_faderLeds, - &s_faderKnob, m_parameterWidget->getBandModels(i)->peakL, m_parameterWidget->getBandModels(i)->peakR); + auto gainFader = new EqFader(m_parameterWidget->getBandModels(i)->gain, tr(""), this, + m_parameterWidget->getBandModels(i)->peakL, m_parameterWidget->getBandModels(i)->peakR); + gainFader->setFixedSize(faderSize); gainFader->move( distance, 295 ); distance += 44; gainFader->setMinimumHeight(80); diff --git a/plugins/Eq/EqEffect.cpp b/plugins/Eq/EqEffect.cpp index 31be4d0f5..d8d2b2b29 100644 --- a/plugins/Eq/EqEffect.cpp +++ b/plugins/Eq/EqEffect.cpp @@ -287,24 +287,23 @@ bool EqEffect::processAudioBuffer( sampleFrame *buf, const fpp_t frames ) -float EqEffect::peakBand( float minF, float maxF, EqAnalyser *fft, int sr ) +float EqEffect::linearPeakBand(float minF, float maxF, EqAnalyser* fft, int sr) { auto const fftEnergy = fft->getEnergy(); if (fftEnergy == 0.) { return 0.; } - float peak = -60; - float *b = fft->m_bands; - float h = 0; - for( int x = 0; x < MAX_BANDS; x++, b++ ) + + float peakLinear = 0.; + + for (int i = 0; i < MAX_BANDS; ++i) { - if( bandToFreq( x ,sr) >= minF && bandToFreq( x,sr ) <= maxF ) + if (bandToFreq(i, sr) >= minF && bandToFreq(i, sr) <= maxF) { - h = 20. * log10(*b / fftEnergy); - peak = h > peak ? h : peak; + peakLinear = std::max(peakLinear, fft->m_bands[i] / fftEnergy); } } - return ( peak + 60 ) / 100; + return peakLinear; } @@ -312,45 +311,34 @@ float EqEffect::peakBand( float minF, float maxF, EqAnalyser *fft, int sr ) void EqEffect::setBandPeaks( EqAnalyser *fft, int samplerate ) { + auto computePeakBand = [&](const FloatModel& freqModel, const FloatModel& bwModel) + { + float const freq = freqModel.value(); + float const bw = bwModel.value(); + + return linearPeakBand(freq * (1 - bw * 0.5), freq * (1 + bw * 0.5), fft, samplerate); + }; + m_eqControls.m_lowShelfPeakR = m_eqControls.m_lowShelfPeakL = - peakBand( m_eqControls.m_lowShelfFreqModel.value() - * ( 1 - m_eqControls.m_lowShelfResModel.value() * 0.5 ), - m_eqControls.m_lowShelfFreqModel.value(), - fft , samplerate ); + linearPeakBand(m_eqControls.m_lowShelfFreqModel.value() * (1 - m_eqControls.m_lowShelfResModel.value() * 0.5), + m_eqControls.m_lowShelfFreqModel.value(), fft , samplerate); m_eqControls.m_para1PeakL = m_eqControls.m_para1PeakR = - peakBand( m_eqControls.m_para1FreqModel.value() - * ( 1 - m_eqControls.m_para1BwModel.value() * 0.5 ), - m_eqControls.m_para1FreqModel.value() - * ( 1 + m_eqControls.m_para1BwModel.value() * 0.5 ), - fft , samplerate ); + computePeakBand(m_eqControls.m_para1FreqModel, m_eqControls.m_para1BwModel); m_eqControls.m_para2PeakL = m_eqControls.m_para2PeakR = - peakBand( m_eqControls.m_para2FreqModel.value() - * ( 1 - m_eqControls.m_para2BwModel.value() * 0.5 ), - m_eqControls.m_para2FreqModel.value() - * ( 1 + m_eqControls.m_para2BwModel.value() * 0.5 ), - fft , samplerate ); + computePeakBand(m_eqControls.m_para2FreqModel, m_eqControls.m_para2BwModel); m_eqControls.m_para3PeakL = m_eqControls.m_para3PeakR = - peakBand( m_eqControls.m_para3FreqModel.value() - * ( 1 - m_eqControls.m_para3BwModel.value() * 0.5 ), - m_eqControls.m_para3FreqModel.value() - * ( 1 + m_eqControls.m_para3BwModel.value() * 0.5 ), - fft , samplerate ); + computePeakBand(m_eqControls.m_para3FreqModel, m_eqControls.m_para3BwModel); m_eqControls.m_para4PeakL = m_eqControls.m_para4PeakR = - peakBand( m_eqControls.m_para4FreqModel.value() - * ( 1 - m_eqControls.m_para4BwModel.value() * 0.5 ), - m_eqControls.m_para4FreqModel.value() - * ( 1 + m_eqControls.m_para4BwModel.value() * 0.5 ), - fft , samplerate ); + computePeakBand(m_eqControls.m_para4FreqModel, m_eqControls.m_para4BwModel); m_eqControls.m_highShelfPeakL = m_eqControls.m_highShelfPeakR = - peakBand( m_eqControls.m_highShelfFreqModel.value(), - m_eqControls.m_highShelfFreqModel.value() - * ( 1 + m_eqControls.m_highShelfResModel.value() * 0.5 ), - fft, samplerate ); + linearPeakBand(m_eqControls.m_highShelfFreqModel.value(), + m_eqControls.m_highShelfFreqModel.value() * (1 + m_eqControls.m_highShelfResModel.value() * 0.5), + fft, samplerate); } extern "C" diff --git a/plugins/Eq/EqEffect.h b/plugins/Eq/EqEffect.h index 9b23b51b5..7e91ee401 100644 --- a/plugins/Eq/EqEffect.h +++ b/plugins/Eq/EqEffect.h @@ -87,7 +87,7 @@ private: float m_inGain; float m_outGain; - float peakBand( float minF, float maxF, EqAnalyser *, int ); + float linearPeakBand(float minF, float maxF, EqAnalyser*, int); inline float bandToFreq ( int index , int sampleRate ) { diff --git a/plugins/Eq/EqFader.h b/plugins/Eq/EqFader.h index 9db0fbe2d..d8897af5c 100644 --- a/plugins/Eq/EqFader.h +++ b/plugins/Eq/EqFader.h @@ -42,20 +42,6 @@ class EqFader : public Fader public: Q_OBJECT public: - EqFader( FloatModel * model, const QString & name, QWidget * parent, QPixmap * backg, QPixmap * leds, QPixmap * knobpi, float* lPeak, float* rPeak ) : - Fader( model, name, parent, backg, leds, knobpi ) - { - setMinimumSize( 23, 80 ); - setMaximumSize( 23, 80 ); - resize( 23, 80 ); - m_lPeak = lPeak; - m_rPeak = rPeak; - connect( getGUI()->mainWindow(), SIGNAL( periodicUpdate() ), this, SLOT( updateVuMeters() ) ); - m_model = model; - setPeak_L( 0 ); - setPeak_R( 0 ); - } - EqFader( FloatModel * model, const QString & name, QWidget * parent, float* lPeak, float* rPeak ) : Fader( model, name, parent ) { diff --git a/plugins/Eq/faderback.png b/plugins/Eq/faderback.png deleted file mode 100644 index 2a03c3a5c1e6c1469d3f3bc9887f0340cf909ee0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 700 zcmV;t0z>_YP)Uq`u<52<($j6>xy;fp;+A#SwQ6_&36G;FNc~XQEBlrzui)v z^6&q*w<|3Q^4)SyysuRh=o$XU%tWY}pngEd_1Jhs9jfMkh|epJTb9oS5mfx*4RiSQ z`n*v<32^GSV-vF@Yl%0gv;2wjt*hKQ+ZK5~za^rP48LBVhx;wg_Nk(X&hYEaP%EIt z3d##;nO|?k^a&Gw;S3Jv>v;f47-NnA&evzuyaDk3_;CL+J>VV2nB(Ka{R@EcUqv;48Oz86fDPhF iH30Y!k<;W3CI1EQEiz6+DKUQl0000aT+^qm#z$3ZS55iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0wwJzX3_ zJUZWAv*uzl6li!@pHiEk9#zTB9nP};k?Wehv)@EjFiUI4N38v!l)P5W?IH&wyHl4+ ze49awV5n-yl%|sl3q!spGB7JJe`%e_$UIwlo4NSJHJ9Hm?@c>B`%d2OM6)X@UuU;+{nNVKt=T|tGI+ZBxvX1LivkZ+cTDT%wSn;@#WCY;2kpdD$y#d zgMS>Z$ez2zeM!OL-#6D^x2)Y6(Y{cn_Nr=K&Sw3^2R)hP=62hgtk2%H>TsxE{`aT@ z2jSp9`~3O3-~G3~K0N2%yR8k)a3FsycF|0s-+T8i{K>c=Qk3C$4c~g80~tJB{an^L HB{Ts5T`6*6 diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index 8a1ed2e8f..22251d551 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -122,9 +122,6 @@ namespace lmms::gui soloMuteLayout->addWidget(m_muteButton, 0, Qt::AlignHCenter); m_fader = new Fader{&mixerChannel->m_volumeModel, tr("Fader %1").arg(channelIndex), this}; - m_fader->setLevelsDisplayedInDBFS(); - m_fader->setMinPeak(dbfsToAmp(-42)); - m_fader->setMaxPeak(dbfsToAmp(9)); m_effectRackView = new EffectRackView{&mixerChannel->m_fxChain, mixerView->m_racksWidget}; m_effectRackView->setFixedWidth(EffectRackView::DEFAULT_WIDTH); diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index 840fe2991..370cc7502 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include "lmms_math.h" #include "embed.h" @@ -59,100 +60,60 @@ namespace lmms::gui { -SimpleTextFloat * Fader::s_textFloat = nullptr; +SimpleTextFloat* Fader::s_textFloat = nullptr; -Fader::Fader( FloatModel * _model, const QString & _name, QWidget * _parent ) : - QWidget( _parent ), - FloatModelView( _model, this ), - m_fPeakValue_L( 0.0 ), - m_fPeakValue_R( 0.0 ), - m_persistentPeak_L( 0.0 ), - m_persistentPeak_R( 0.0 ), - m_fMinPeak( 0.01f ), - m_fMaxPeak( 1.1 ), - m_back(embed::getIconPixmap("fader_background")), - m_leds(embed::getIconPixmap("fader_leds")), - m_knob(embed::getIconPixmap("fader_knob")), - m_levelsDisplayedInDBFS(false), - m_moveStartPoint( -1 ), - m_startValue( 0 ), - m_peakGreen( 0, 0, 0 ), - m_peakRed( 0, 0, 0 ), - m_peakYellow( 0, 0, 0 ) +Fader::Fader(FloatModel* model, const QString& name, QWidget* parent) : + QWidget(parent), + FloatModelView(model, this) { - if( s_textFloat == nullptr ) + if (s_textFloat == nullptr) { s_textFloat = new SimpleTextFloat; } - init(_model, _name); + setWindowTitle(name); + setAttribute(Qt::WA_OpaquePaintEvent, false); + // For now resize the widget to the size of the previous background image "fader_background.png" as it was found in the classic and default theme + constexpr QSize minimumSize(23, 116); + setMinimumSize(minimumSize); + resize(minimumSize); + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + setModel(model); + setHintText("Volume:", "%"); m_conversionFactor = 100.0; } -Fader::Fader( FloatModel * model, const QString & name, QWidget * parent, QPixmap * back, QPixmap * leds, QPixmap * knob ) : - QWidget( parent ), - FloatModelView( model, this ), - m_fPeakValue_L( 0.0 ), - m_fPeakValue_R( 0.0 ), - m_persistentPeak_L( 0.0 ), - m_persistentPeak_R( 0.0 ), - m_fMinPeak( 0.01f ), - m_fMaxPeak( 1.1 ), - m_back(*back), - m_leds(*leds), - m_knob(*knob), - m_levelsDisplayedInDBFS(false), - m_moveStartPoint( -1 ), - m_startValue( 0 ), - m_peakGreen( 0, 0, 0 ), - m_peakRed( 0, 0, 0 ) +Fader::Fader(FloatModel* model, const QString& name, QWidget* parent, const QPixmap& knob) : + Fader(model, name, parent) { - if( s_textFloat == nullptr ) - { - s_textFloat = new SimpleTextFloat; - } - - init(model, name); -} - -void Fader::init(FloatModel * model, QString const & name) -{ - setWindowTitle( name ); - setAttribute( Qt::WA_OpaquePaintEvent, false ); - QSize backgroundSize = m_back.size(); - setMinimumSize( backgroundSize ); - setMaximumSize( backgroundSize ); - resize( backgroundSize ); - setModel( model ); - setHintText( "Volume:","%"); + m_knob = knob; } - -void Fader::contextMenuEvent( QContextMenuEvent * _ev ) +void Fader::contextMenuEvent(QContextMenuEvent* ev) { - CaptionMenu contextMenu( windowTitle() ); - addDefaultActions( &contextMenu ); - contextMenu.exec( QCursor::pos() ); - _ev->accept(); + CaptionMenu contextMenu(windowTitle()); + addDefaultActions(&contextMenu); + contextMenu.exec(QCursor::pos()); + ev->accept(); } -void Fader::mouseMoveEvent( QMouseEvent *mouseEvent ) +void Fader::mouseMoveEvent(QMouseEvent* mouseEvent) { - if( m_moveStartPoint >= 0 ) + if (m_moveStartPoint >= 0) { int dy = m_moveStartPoint - mouseEvent->globalY(); float delta = dy * (model()->maxValue() - model()->minValue()) / (float)(height() - (m_knob).height()); const auto step = model()->step(); - float newValue = static_cast( static_cast( ( m_startValue + delta ) / step + 0.5 ) ) * step; - model()->setValue( newValue ); + float newValue = static_cast(static_cast((m_startValue + delta) / step + 0.5)) * step; + model()->setValue(newValue); updateTextFloat(); } @@ -161,16 +122,16 @@ void Fader::mouseMoveEvent( QMouseEvent *mouseEvent ) -void Fader::mousePressEvent( QMouseEvent* mouseEvent ) +void Fader::mousePressEvent(QMouseEvent* mouseEvent) { - if( mouseEvent->button() == Qt::LeftButton && - ! ( mouseEvent->modifiers() & Qt::ControlModifier ) ) + if (mouseEvent->button() == Qt::LeftButton && + !(mouseEvent->modifiers() & Qt::ControlModifier)) { - AutomatableModel *thisModel = model(); - if( thisModel ) + AutomatableModel* thisModel = model(); + if (thisModel) { thisModel->addJournalCheckPoint(); - thisModel->saveJournallingState( false ); + thisModel->saveJournallingState(false); } if (mouseEvent->y() >= knobPosY() - (m_knob).height() && mouseEvent->y() < knobPosY()) @@ -190,37 +151,36 @@ void Fader::mousePressEvent( QMouseEvent* mouseEvent ) } else { - AutomatableModelView::mousePressEvent( mouseEvent ); + AutomatableModelView::mousePressEvent(mouseEvent); } } -void Fader::mouseDoubleClickEvent( QMouseEvent* mouseEvent ) +void Fader::mouseDoubleClickEvent(QMouseEvent* mouseEvent) { bool ok; - // TODO: dbV handling + // TODO: dbFS handling auto minv = model()->minValue() * m_conversionFactor; auto maxv = model()->maxValue() * m_conversionFactor; - float newValue = QInputDialog::getDouble(this, tr("Set value"), + float enteredValue = QInputDialog::getDouble(this, tr("Set value"), tr("Please enter a new value between %1 and %2:").arg(minv).arg(maxv), - model()->getRoundedValue() * m_conversionFactor, minv, maxv, model()->getDigitCount(), &ok) - / m_conversionFactor; + model()->getRoundedValue() * m_conversionFactor, minv, maxv, model()->getDigitCount(), &ok); - if( ok ) + if (ok) { - model()->setValue( newValue ); + model()->setValue(enteredValue / m_conversionFactor); } } -void Fader::mouseReleaseEvent( QMouseEvent * mouseEvent ) +void Fader::mouseReleaseEvent(QMouseEvent* mouseEvent) { - if( mouseEvent && mouseEvent->button() == Qt::LeftButton ) + if (mouseEvent && mouseEvent->button() == Qt::LeftButton) { - AutomatableModel *thisModel = model(); - if( thisModel ) + AutomatableModel* thisModel = model(); + if (thisModel) { thisModel->restoreJournallingState(); } @@ -230,20 +190,20 @@ void Fader::mouseReleaseEvent( QMouseEvent * mouseEvent ) } -void Fader::wheelEvent ( QWheelEvent *ev ) +void Fader::wheelEvent (QWheelEvent* ev) { ev->accept(); if (ev->angleDelta().y() > 0) { - model()->incValue( 1 ); + model()->incValue(1); } else { - model()->incValue( -1 ); + model()->incValue(-1); } updateTextFloat(); - s_textFloat->setVisibilityTimeOut( 1000 ); + s_textFloat->setVisibilityTimeOut(1000); } @@ -251,21 +211,14 @@ void Fader::wheelEvent ( QWheelEvent *ev ) /// /// Set peak value (0.0 .. 1.0) /// -void Fader::setPeak( float fPeak, float &targetPeak, float &persistentPeak, QElapsedTimer &lastPeakTimer ) +void Fader::setPeak(float fPeak, float& targetPeak, float& persistentPeak, QElapsedTimer& lastPeakTimer) { - if( fPeak < m_fMinPeak ) - { - fPeak = m_fMinPeak; - } - else if( fPeak > m_fMaxPeak ) - { - fPeak = m_fMaxPeak; - } + fPeak = std::clamp(fPeak, m_fMinPeak, m_fMaxPeak); - if( targetPeak != fPeak) + if (targetPeak != fPeak) { targetPeak = fPeak; - if( targetPeak >= persistentPeak ) + if (targetPeak >= persistentPeak) { persistentPeak = targetPeak; lastPeakTimer.restart(); @@ -273,25 +226,25 @@ void Fader::setPeak( float fPeak, float &targetPeak, float &persistentPeak, QEla update(); } - if( persistentPeak > 0 && lastPeakTimer.elapsed() > 1500 ) + if (persistentPeak > 0 && lastPeakTimer.elapsed() > 1500) { - persistentPeak = qMax( 0, persistentPeak-0.05 ); + persistentPeak = qMax(0, persistentPeak-0.05); update(); } } -void Fader::setPeak_L( float fPeak ) +void Fader::setPeak_L(float fPeak) { - setPeak( fPeak, m_fPeakValue_L, m_persistentPeak_L, m_lastPeakTimer_L ); + setPeak(fPeak, m_fPeakValue_L, m_persistentPeak_L, m_lastPeakTimer_L); } -void Fader::setPeak_R( float fPeak ) +void Fader::setPeak_R(float fPeak) { - setPeak( fPeak, m_fPeakValue_R, m_persistentPeak_R, m_lastPeakTimer_R ); + setPeak(fPeak, m_fPeakValue_R, m_persistentPeak_R, m_lastPeakTimer_R); } @@ -299,169 +252,185 @@ void Fader::setPeak_R( float fPeak ) // update tooltip showing value and adjust position while changing fader value void Fader::updateTextFloat() { - if( ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() && m_conversionFactor == 100.0 ) + if (ConfigManager::inst()->value("app", "displaydbfs").toInt() && m_conversionFactor == 100.0) { - s_textFloat->setText( QString("Volume: %1 dBFS"). - arg( ampToDbfs( model()->value() ), 3, 'f', 2 ) ); + s_textFloat->setText(QString("Volume: %1 dBFS"). + arg(ampToDbfs(model()->value()), 3, 'f', 2)); } else { - s_textFloat->setText( m_description + " " + QString("%1 ").arg( model()->value() * m_conversionFactor ) + " " + m_unit ); + s_textFloat->setText(m_description + " " + QString("%1 ").arg(model()->value() * m_conversionFactor) + " " + m_unit); } s_textFloat->moveGlobal(this, QPoint(width() + 2, knobPosY() - s_textFloat->height() / 2)); } -inline int Fader::calculateDisplayPeak( float fPeak ) -{ - int peak = static_cast(m_back.height() - (fPeak / (m_fMaxPeak - m_fMinPeak)) * m_back.height()); - - return qMin(peak, m_back.height()); -} - - -void Fader::paintEvent( QPaintEvent * ev) +void Fader::paintEvent(QPaintEvent* ev) { QPainter painter(this); - // Draw the background - painter.drawPixmap(ev->rect(), m_back, ev->rect()); - // Draw the levels with peaks - if (getLevelsDisplayedInDBFS()) - { - paintDBFSLevels(ev, painter); - } - else - { - paintLinearLevels(ev, painter); - } + paintLevels(ev, painter, !m_levelsDisplayedInDBFS); // Draw the knob - painter.drawPixmap(0, knobPosY() - m_knob.height(), m_knob); + painter.drawPixmap((width() - m_knob.width()) / 2, knobPosY() - m_knob.height(), m_knob); } -void Fader::paintDBFSLevels(QPaintEvent * ev, QPainter & painter) +void Fader::paintLevels(QPaintEvent* ev, QPainter& painter, bool linear) { - int height = m_back.height(); - int width = m_back.width() / 2; - int center = m_back.width() - width; + std::function mapper = [this](float value) { return ampToDbfs(qMax(0.0001, value)); }; - float const maxDB(ampToDbfs(m_fMaxPeak)); - float const minDB(ampToDbfs(m_fMinPeak)); + if (linear) + { + mapper = [this](float value) { return value; }; + } - // We will need to divide by the span between min and max several times. It's more - // efficient to calculate the reciprocal once and then to multiply. - float const fullSpanReciprocal = 1 / (maxDB - minDB); + const float mappedMinPeak(mapper(m_fMinPeak)); + const float mappedMaxPeak(mapper(m_fMaxPeak)); + const float mappedPeakL(mapper(m_fPeakValue_L)); + const float mappedPeakR(mapper(m_fPeakValue_R)); + const float mappedPersistentPeakL(mapper(m_persistentPeak_L)); + const float mappedPersistentPeakR(mapper(m_persistentPeak_R)); + const float mappedUnity(mapper(1.f)); + painter.save(); + + const QRect baseRect = rect(); + + const int height = baseRect.height(); + + const int margin = 1; + const int distanceBetweenMeters = 2; + + const int numberOfMeters = 2; + + // Compute the width of a single meter by removing the margins and the space between meters + const int leftAndRightMargin = 2 * margin; + const int pixelsBetweenAllMeters = distanceBetweenMeters * (numberOfMeters - 1); + const int remainingPixelsForMeters = baseRect.width() - leftAndRightMargin - pixelsBetweenAllMeters; + const int meterWidth = remainingPixelsForMeters / numberOfMeters; + + QRect leftMeterOutlineRect(margin, margin, meterWidth, height - 2 * margin); + QRect rightMeterOutlineRect(baseRect.width() - margin - meterWidth, margin, meterWidth, height - 2 * margin); + + QMargins removedMargins(1, 1, 1, 1); + QRect leftMeterRect = leftMeterOutlineRect.marginsRemoved(removedMargins); + QRect rightMeterRect = rightMeterOutlineRect.marginsRemoved(removedMargins); + + QPainterPath path; + qreal radius = 2; + path.addRoundedRect(leftMeterOutlineRect, radius, radius); + path.addRoundedRect(rightMeterOutlineRect, radius, radius); + painter.fillPath(path, Qt::black); + + // Now clip everything to the paths of the meters + painter.setClipPath(path); + + // This linear map performs the following mapping: + // Value (dbFS or linear) -> window coordinates of the widget + // It is for example used to determine the height of peaks, markers and to define the gradient for the levels + const LinearMap valuesToWindowCoordinates(mappedMaxPeak, leftMeterRect.y(), mappedMinPeak, leftMeterRect.y() + leftMeterRect.height()); + + // This lambda takes a value (in dbFS or linear) and a rectangle and computes a rectangle + // that represent the value within the rectangle. It is for example used to compute the unity indicators. + const auto computeLevelMarkerRect = [&valuesToWindowCoordinates](const QRect& rect, float peak) -> QRect + { + return QRect(rect.x(), valuesToWindowCoordinates.map(peak), rect.width(), 1); + }; + + // This lambda takes a peak value (in dbFS or linear) and a rectangle and computes a rectangle + // that represent the peak value within the rectangle. It's used to compute the peak indicators + // which "dance" on top of the level meters. + const auto computePeakRect = [&valuesToWindowCoordinates](const QRect& rect, float peak) -> QRect + { + return QRect(rect.x(), valuesToWindowCoordinates.map(peak), rect.width(), 1); + }; + + // This lambda takes a peak value (in dbFS or linear) and a rectangle and returns an adjusted copy of the + // rectangle that represents the peak value. It is used to compute the level meters themselves. + const auto computeLevelRect = [&valuesToWindowCoordinates](const QRect& rect, float peak) -> QRect + { + QRect result(rect); + result.setTop(valuesToWindowCoordinates.map(peak)); + + return result; + }; + + // Draw left and right level markers for the unity lines (0 dbFS, 1.0 amplitude) + if (getRenderUnityLine()) + { + const auto unityRectL = computeLevelMarkerRect(leftMeterRect, mappedUnity); + painter.fillRect(unityRectL, m_unityMarker); + + const auto unityRectR = computeLevelMarkerRect(rightMeterRect, mappedUnity); + painter.fillRect(unityRectR, m_unityMarker); + } + + // These values define where the gradient changes values, i.e. the ranges + // for clipping, warning and ok. + // Please ensure that "clip starts" is the maximum value and that "ok ends" + // is the minimum value and that all other values lie inbetween. Otherwise + // there will be warnings when the gradient is defined. + const float mappedClipStarts(mapper(dbfsToAmp(0.f))); + const float mappedWarnEnd(mapper(dbfsToAmp(-0.01))); + const float mappedWarnStart(mapper(dbfsToAmp(-6.f))); + const float mappedOkEnd(mapper(dbfsToAmp(-12.f))); + + // Prepare the gradient for the meters + // + // The idea is the following. We want to be able to render arbitrary ranges of min and max values. + // Therefore we first compute the start and end point of the gradient in window coordinates. + // The gradient is assumed to start with the clip color and to end with the ok color with warning values in between. + // We know the min and max peaks that map to a rectangle where we draw the levels. We can use the values of the min and max peaks + // as well as the Y-coordinates of the rectangle to compute a map which will give us the coordinates of the value where the clipping + // starts and where the ok area end. These coordinates are used to initialize the gradient. Please note that the gradient might thus + // extend the rectangle into which we paint. + float clipStartYCoord = valuesToWindowCoordinates.map(mappedClipStarts); + float okEndYCoord = valuesToWindowCoordinates.map(mappedOkEnd); + + QLinearGradient linearGrad(0, clipStartYCoord, 0, okEndYCoord); + + // We already know for the gradient that the clip color will be at 0 and that the ok color is at 1. + // What's left to do is to map the inbetween values into the interval [0,1]. + const LinearMap mapBetweenClipAndOk(mappedClipStarts, 0.f, mappedOkEnd, 1.f); + + linearGrad.setColorAt(0, m_peakClip); + linearGrad.setColorAt(mapBetweenClipAndOk.map(mappedWarnEnd), m_peakWarn); + linearGrad.setColorAt(mapBetweenClipAndOk.map(mappedWarnStart), m_peakWarn); + linearGrad.setColorAt(1, m_peakOk); // Draw left levels - float const leftSpan = ampToDbfs(qMax(0.0001, m_fPeakValue_L)) - minDB; - int peak_L = height * leftSpan * fullSpanReciprocal; - QRect drawRectL( 0, height - peak_L, width, peak_L ); // Source and target are identical - painter.drawPixmap(drawRectL, m_leds, drawRectL); - - float const persistentLeftPeakDBFS = ampToDbfs(qMax(0.0001, m_persistentPeak_L)); - int persistentPeak_L = height * (1 - (persistentLeftPeakDBFS - minDB) * fullSpanReciprocal); - // the LED's have a 4px padding and we don't want the peaks - // to draw on the fader background - if( persistentPeak_L <= 4 ) + if (mappedPeakL > mappedMinPeak) { - persistentPeak_L = 4; - } - if( persistentLeftPeakDBFS > minDB ) - { - QColor const & peakColor = clips(m_persistentPeak_L) ? peakRed() : - persistentLeftPeakDBFS >= -6 ? peakYellow() : peakGreen(); - painter.fillRect( QRect( 2, persistentPeak_L, 7, 1 ), peakColor ); + QPainterPath leftMeterPath; + leftMeterPath.addRoundedRect(computeLevelRect(leftMeterRect, mappedPeakL), radius, radius); + painter.fillPath(leftMeterPath, linearGrad); } + // Draw left peaks + if (mappedPersistentPeakL > mappedMinPeak) + { + const auto peakRectL = computePeakRect(leftMeterRect, mappedPersistentPeakL); + painter.fillRect(peakRectL, linearGrad); + } // Draw right levels - float const rightSpan = ampToDbfs(qMax(0.0001, m_fPeakValue_R)) - minDB; - int peak_R = height * rightSpan * fullSpanReciprocal; - QRect const drawRectR( center, height - peak_R, width, peak_R ); // Source and target are identical - painter.drawPixmap(drawRectR, m_leds, drawRectR); - - float const persistentRightPeakDBFS = ampToDbfs(qMax(0.0001, m_persistentPeak_R)); - int persistentPeak_R = height * (1 - (persistentRightPeakDBFS - minDB) * fullSpanReciprocal); - // the LED's have a 4px padding and we don't want the peaks - // to draw on the fader background - if( persistentPeak_R <= 4 ) + if (mappedPeakR > mappedMinPeak) { - persistentPeak_R = 4; - } - if( persistentRightPeakDBFS > minDB ) - { - QColor const & peakColor = clips(m_persistentPeak_R) ? peakRed() : - persistentRightPeakDBFS >= -6 ? peakYellow() : peakGreen(); - painter.fillRect( QRect( 14, persistentPeak_R, 7, 1 ), peakColor ); - } -} - -void Fader::paintLinearLevels(QPaintEvent * ev, QPainter & painter) -{ - // peak leds - //float fRange = abs( m_fMaxPeak ) + abs( m_fMinPeak ); - - int height = m_back.height(); - int width = m_back.width() / 2; - int center = m_back.width() - width; - - int peak_L = calculateDisplayPeak( m_fPeakValue_L - m_fMinPeak ); - int persistentPeak_L = qMax( 3, calculateDisplayPeak( m_persistentPeak_L - m_fMinPeak ) ); - painter.drawPixmap(QRect(0, peak_L, width, height - peak_L), m_leds, QRect(0, peak_L, width, height - peak_L)); - - if( m_persistentPeak_L > 0.05 ) - { - painter.fillRect( QRect( 2, persistentPeak_L, 7, 1 ), ( m_persistentPeak_L < 1.0 ) - ? peakGreen() - : peakRed() ); + QPainterPath rightMeterPath; + rightMeterPath.addRoundedRect(computeLevelRect(rightMeterRect, mappedPeakR), radius, radius); + painter.fillPath(rightMeterPath, linearGrad); } - int peak_R = calculateDisplayPeak( m_fPeakValue_R - m_fMinPeak ); - int persistentPeak_R = qMax( 3, calculateDisplayPeak( m_persistentPeak_R - m_fMinPeak ) ); - painter.drawPixmap(QRect(center, peak_R, width, height - peak_R), m_leds, QRect(center, peak_R, width, height - peak_R)); - - if( m_persistentPeak_R > 0.05 ) + // Draw right peaks + if (mappedPersistentPeakR > mappedMinPeak) { - painter.fillRect( QRect( 14, persistentPeak_R, 7, 1 ), ( m_persistentPeak_R < 1.0 ) - ? peakGreen() - : peakRed() ); + const auto peakRectR = computePeakRect(rightMeterRect, mappedPersistentPeakR); + painter.fillRect(peakRectR, linearGrad); } + + painter.restore(); } - -QColor const & Fader::peakGreen() const -{ - return m_peakGreen; -} - -QColor const & Fader::peakRed() const -{ - return m_peakRed; -} - -QColor const & Fader::peakYellow() const -{ - return m_peakYellow; -} - -void Fader::setPeakGreen( const QColor & c ) -{ - m_peakGreen = c; -} - -void Fader::setPeakRed( const QColor & c ) -{ - m_peakRed = c; -} - -void Fader::setPeakYellow( const QColor & c ) -{ - m_peakYellow = c; -} - - } // namespace lmms::gui From 922eb7f2ba9e70df08346f76b2088160594c00fe Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Fri, 5 Apr 2024 13:15:48 +0200 Subject: [PATCH 132/191] =?UTF-8?q?Fix=20NaNs=20for=20some=20dbFS=20value?= =?UTF-8?q?=20displays=20(-=E2=88=9E=20dbFS)=20(#7142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix some NaNs in the context of the display of dbFS values when "View > Volume as dbFS" is checked. They occur during the display of the current value when a mixer fader or the volume knob of an instrument is pulled down completely. The fix is to detect these cases and to display "-∞ dbFS". Also fix a problem with the editor where dbFS values can be entered for volume knobs. When the knob is turned completely to the left and the amplification is 0 then the initially displayed value is set to -96 dBFS, i.e. the lower limit that is shown in the dialog. This is done because the dialog likely cannot handle displaying or entering "-∞". --- src/gui/widgets/Fader.cpp | 13 +++++++++++-- src/gui/widgets/FloatModelEditorBase.cpp | 16 +++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index 370cc7502..1eb06756a 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -254,8 +254,17 @@ void Fader::updateTextFloat() { if (ConfigManager::inst()->value("app", "displaydbfs").toInt() && m_conversionFactor == 100.0) { - s_textFloat->setText(QString("Volume: %1 dBFS"). - arg(ampToDbfs(model()->value()), 3, 'f', 2)); + QString label(tr("Volume: %1 dBFS")); + + auto const modelValue = model()->value(); + if (modelValue <= 0.) + { + s_textFloat->setText(label.arg("-∞")); + } + else + { + s_textFloat->setText(label.arg(ampToDbfs(modelValue), 3, 'f', 2)); + } } else { diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp index 7421908e2..dd6be8958 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -388,12 +388,15 @@ void FloatModelEditorBase::enterValue() if (isVolumeKnob() && ConfigManager::inst()->value("app", "displaydbfs").toInt()) { + auto const initalValue = model()->getRoundedValue() / 100.0; + auto const initialDbValue = initalValue > 0. ? ampToDbfs(initalValue) : -96; + new_val = QInputDialog::getDouble( this, tr("Set value"), tr("Please enter a new value between " "-96.0 dBFS and 6.0 dBFS:"), - ampToDbfs(model()->getRoundedValue() / 100.0), - -96.0, 6.0, model()->getDigitCount(), &ok); + initialDbValue, -96.0, 6.0, model()->getDigitCount(), &ok); + if (new_val <= -96.0) { new_val = 0.0f; @@ -439,9 +442,12 @@ QString FloatModelEditorBase::displayValue() const if (isVolumeKnob() && ConfigManager::inst()->value("app", "displaydbfs").toInt()) { - return m_description.trimmed() + QString(" %1 dBFS"). - arg(ampToDbfs(model()->getRoundedValue() / volumeRatio()), - 3, 'f', 2); + auto const valueToVolumeRatio = model()->getRoundedValue() / volumeRatio(); + return m_description.trimmed() + ( + valueToVolumeRatio == 0. + ? QString(" -∞ dBFS") + : QString(" %1 dBFS").arg(ampToDbfs(valueToVolumeRatio), 3, 'f', 2) + ); } return m_description.trimmed() + QString(" %1"). From 2472e9ee4e3f92049acbf8730df03de770de554f Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Fri, 5 Apr 2024 17:24:28 +0200 Subject: [PATCH 133/191] GUI adjustments around base velocity (#7196) Change the label of the group box from "CUSTOM BASE VELOCITY" to "VELOCITY MAPPING". Remove the long explanation text from the group box and add the following tool tip for the LcdSpinBox: "MIDI notes at this velocity correspond to 100% note velocity." Change the label of the spin box from "BASE VELOCITY" to "MIDI VELOCITY" because that's what the value actually represents. --- src/gui/instrument/InstrumentMidiIOView.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/gui/instrument/InstrumentMidiIOView.cpp b/src/gui/instrument/InstrumentMidiIOView.cpp index f5f4ae095..e3f10bd1a 100644 --- a/src/gui/instrument/InstrumentMidiIOView.cpp +++ b/src/gui/instrument/InstrumentMidiIOView.cpp @@ -144,7 +144,7 @@ InstrumentMidiIOView::InstrumentMidiIOView( QWidget* parent ) : midiOutputLayout->insertWidget( 0, m_wpBtn ); } - auto baseVelocityGroupBox = new GroupBox(tr("CUSTOM BASE VELOCITY")); + auto baseVelocityGroupBox = new GroupBox(tr("VELOCITY MAPPING")); baseVelocityGroupBox->setLedButtonShown(false); layout->addWidget( baseVelocityGroupBox ); @@ -152,15 +152,9 @@ InstrumentMidiIOView::InstrumentMidiIOView( QWidget* parent ) : baseVelocityLayout->setContentsMargins( 8, 18, 8, 8 ); baseVelocityLayout->setSpacing( 6 ); - auto baseVelocityHelp - = new QLabel(tr("Specify the velocity normalization base for MIDI-based instruments at 100% note velocity.")); - baseVelocityHelp->setWordWrap( true ); - baseVelocityHelp->setFont(adjustedToPixelSize(baseVelocityHelp->font(), 10)); - - baseVelocityLayout->addWidget( baseVelocityHelp ); - m_baseVelocitySpinBox = new LcdSpinBox( 3, baseVelocityGroupBox ); - m_baseVelocitySpinBox->setLabel( tr( "BASE VELOCITY" ) ); + m_baseVelocitySpinBox->setLabel(tr("MIDI VELOCITY")); + m_baseVelocitySpinBox->setToolTip(tr("MIDI notes at this velocity correspond to 100% note velocity.")); baseVelocityLayout->addWidget( m_baseVelocitySpinBox ); layout->addStretch(); From 1f5f28fd8ab21d1de6a04918c4eede08a70cc62a Mon Sep 17 00:00:00 2001 From: Levin Oehlmann Date: Tue, 9 Apr 2024 01:17:26 +0200 Subject: [PATCH 134/191] Don't auto-quantize notes when recording MIDI input (#6714) * Don't auto-quantize notes when recording MIDI input. * Add midi:autoquantize to config file and a widget to set it in the MIDI settings. * Quantize notes during recording if midi:autoquantize is enabled. * Apply suggestions from code review: Formatting Style formatting Co-authored-by: saker * Cache the auto quantization setting in a PianoRoll member variable, and update it on ConfigManager::valueChanged() * Apply suggestions from code review: Formatting & temp variable One formatting change, and reusing an existing variable instead of introducting a new local one. Co-authored-by: saker Co-authored-by: IanCaio * Update src/gui/modals/SetupDialog.cpp Good catch. Co-authored-by: IanCaio * Fix logic bug in PianoRoll's midi/autoquantize value observer. * Use '!' instead of 'not' to please MSVC. * autoquantize: Add an explicit check for consistency with the rest of the PR, and give the setting a default value in SetupDialog constructor. * Integrate MIDI auto-quantize checkbox into the resizable layout, and add a tool tip. --------- Co-authored-by: saker Co-authored-by: IanCaio --- include/PianoRoll.h | 1 + include/SetupDialog.h | 2 ++ src/gui/editors/PianoRoll.cpp | 19 +++++++++++++++++-- src/gui/modals/SetupDialog.cpp | 23 +++++++++++++++++++++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 881732be1..35550a5b3 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -376,6 +376,7 @@ private: TimePos m_currentPosition; bool m_recording; + bool m_doAutoQuantization{false}; QList m_recordingNotes; Note * m_currentNote; diff --git a/include/SetupDialog.h b/include/SetupDialog.h index ce81bb477..7a1304d9a 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -109,6 +109,7 @@ private slots: // MIDI settings widget. void midiInterfaceChanged(const QString & driver); + void toggleMidiAutoQuantization(bool enabled); // Paths settings widget. void openWorkingDir(); @@ -190,6 +191,7 @@ private: MswMap m_midiIfaceSetupWidgets; trMap m_midiIfaceNames; QComboBox * m_assignableMidiDevices; + bool m_midiAutoQuantize; // Paths settings widgets. QString m_workingDir; diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 3b9273ade..7d4a9552a 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -160,6 +160,7 @@ PianoRoll::PianoRoll() : m_midiClip( nullptr ), m_currentPosition(), m_recording( false ), + m_doAutoQuantization(ConfigManager::inst()->value("midi", "autoquantize").toInt() != 0), m_currentNote( nullptr ), m_action( Action::None ), m_noteEditMode( NoteEditMode::Volume ), @@ -240,6 +241,15 @@ PianoRoll::PianoRoll() : connect( markChordAction, &QAction::triggered, [this](){ markSemiTone(SemiToneMarkerAction::MarkCurrentChord); }); connect( unmarkAllAction, &QAction::triggered, [this](){ markSemiTone(SemiToneMarkerAction::UnmarkAll); }); connect( copyAllNotesAction, &QAction::triggered, [this](){ markSemiTone(SemiToneMarkerAction::CopyAllNotesOnKey); }); + connect(ConfigManager::inst(), &ConfigManager::valueChanged, + [this](QString const& cls, QString const& attribute, QString const& value) + { + if (!(cls == "midi" && attribute == "autoquantize")) + { + return; + } + this->m_doAutoQuantization = (value.toInt() != 0); + }); markScaleAction->setEnabled( false ); markChordAction->setEnabled( false ); @@ -4108,8 +4118,13 @@ void PianoRoll::finishRecordNote(const Note & n ) Note n1(n.length(), it->pos(), it->key(), it->getVolume(), it->getPanning(), n.detuning()); - n1.quantizeLength( quantization() ); - m_midiClip->addNote( n1 ); + + if (m_doAutoQuantization) + { + n1.quantizeLength(quantization()); + n1.quantizePos(quantization()); + } + m_midiClip->addNote(n1, false); update(); m_recordingNotes.erase( it ); break; diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 63560e33d..ba7814f31 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -146,6 +146,8 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : "audioengine", "hqaudio").toInt()), m_bufferSize(ConfigManager::inst()->value( "audioengine", "framesperaudiobuffer").toInt()), + m_midiAutoQuantize(ConfigManager::inst()->value( + "midi", "autoquantize", "0").toInt() != 0), m_workingDir(QDir::toNativeSeparators(ConfigManager::inst()->workingDir())), m_vstDir(QDir::toNativeSeparators(ConfigManager::inst()->vstDir())), m_ladspaDir(QDir::toNativeSeparators(ConfigManager::inst()->ladspaDir())), @@ -159,8 +161,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : { setWindowIcon(embed::getIconPixmap("setup_general")); setWindowTitle(tr("Settings")); - // TODO: Equivalent to the new setWindowFlag(Qt::WindowContextHelpButtonHint, false) - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowFlag(Qt::WindowContextHelpButtonHint, false); setModal(true); Engine::projectJournal()->setJournalling(false); @@ -716,10 +717,22 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : m_assignableMidiDevices->setCurrentIndex(current); } + // MIDI Recording tab + auto* midiRecordingTab = new QGroupBox(tr("Behavior when recording"), midi_w); + auto* midiRecordingLayout = new QVBoxLayout(midiRecordingTab); + { + auto *box = addCheckBox(tr("Auto-quantize notes in Piano Roll"), + midiRecordingTab, midiRecordingLayout, + m_midiAutoQuantize, SLOT(toggleMidiAutoQuantization(bool)), + false); + box->setToolTip(tr("If enabled, notes will be automatically quantized when recording them from a MIDI controller. If disabled, they are always recorded at the highest possible resolution.")); + } + // MIDI layout ordering. midi_layout->addWidget(midiInterfaceBox); midi_layout->addWidget(ms_w); midi_layout->addWidget(midiAutoAssignBox); + midi_layout->addWidget(midiRecordingTab); midi_layout->addStretch(); @@ -965,6 +978,7 @@ void SetupDialog::accept() m_midiIfaceNames[m_midiInterfaces->currentText()]); ConfigManager::inst()->setValue("midi", "midiautoassign", m_assignableMidiDevices->currentText()); + ConfigManager::inst()->setValue("midi", "autoquantize", QString::number(m_midiAutoQuantize)); ConfigManager::inst()->setWorkingDir(QDir::fromNativeSeparators(m_workingDir)); @@ -1253,6 +1267,11 @@ void SetupDialog::midiInterfaceChanged(const QString & iface) m_midiIfaceSetupWidgets[m_midiIfaceNames[iface]]->show(); } +void SetupDialog::toggleMidiAutoQuantization(bool enabled) +{ + m_midiAutoQuantize = enabled; +} + // Paths settings slots. From 8e40038a2d864e667e8cd3075c26eb11b977d5fa Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Thu, 11 Apr 2024 17:49:00 +0200 Subject: [PATCH 135/191] Scalable envelope graph (#7194) Make the graph scalable by adjusting the painting code of the envelope so that it does not assume fixed widths and heights anymore. Remove the setting of a fixed size from the envelope graph and only set a minimum size. Make three scaling modes available which can be selected via a context menu in the graph: * "Dynamic": This modes corresponds to the rendering strategy of the previous implementation. Initially 80/182 of the available width is used as the maximum width per segment. This can be interpreted like a "zoomed" version of the absolute mode. If the needed space becomes larger than the full width though then it falls back to relative rendering. * "Absolute": Each of the five segments is assigned 1/5 of the available width. The envelopes will always fit but might appear small depending of the current settings. This is a good mode to compare envelopes though. * "Relative": If there is at least one non-zero segment then the whole width is always used to present the envelope. The default scaling mode is "Dynamic". ## Technical details The new painting code is more or less divided into two parts. The first part calculates `QPointF` instances for the different points. In the second part these points are then used to draw the lines and markers. This makes the actual rendering code much more straight forward, readable and maintainable. The interpolation between the line color of an inactive and an active envelope has also been restructured so that it is much more obvious that we are doing an interpolation in the first place. The colors at both ends of the interpolation are explicit now and can therefore be adjusted much easier. The actual color interpolation is done in the helper function `interpolateInRgb` which is provided by the new class `ColorHelper`. This class will later also be needed when the LFO graph is made scalable. The line is rendered as a polyline instead of single line segments. The drawing of the markers has been abstracted into a lambda (with some outside captures though) so that it can be easily adjusted if necessary. The markers are rendered as circles instead of rectangles because that looks much nicer especially when the widget is rendered at a larger size. The width of the lines and marker outlines is determined using the size of the widget so that it scales with the size. A `lerp` function has been added to `lmms_math.h`. --- include/ColorHelper.h | 54 ++++++ include/EnvelopeGraph.h | 17 +- include/lmms_math.h | 7 + src/gui/instrument/EnvelopeGraph.cpp | 248 +++++++++++++++++++-------- 4 files changed, 251 insertions(+), 75 deletions(-) create mode 100644 include/ColorHelper.h diff --git a/include/ColorHelper.h b/include/ColorHelper.h new file mode 100644 index 000000000..78f99b9e2 --- /dev/null +++ b/include/ColorHelper.h @@ -0,0 +1,54 @@ +/* ColorHelper.h - Helper methods for color related algorithms, etc. + * + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_COLOR_HELPER_H +#define LMMS_GUI_COLOR_HELPER_H + +#include + +namespace lmms::gui +{ + +class ColorHelper +{ +public: + static QColor interpolateInRgb(const QColor& a, const QColor& b, float t) + { + qreal ar, ag, ab, aa; + a.getRgbF(&ar, &ag, &ab, &aa); + + qreal br, bg, bb, ba; + b.getRgbF(&br, &bg, &bb, &ba); + + const float interH = lerp(ar, br, t); + const float interS = lerp(ag, bg, t); + const float interV = lerp(ab, bb, t); + const float interA = lerp(aa, ba, t); + + return QColor::fromRgbF(interH, interS, interV, interA); + } +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_COLOR_HELPER_H diff --git a/include/EnvelopeGraph.h b/include/EnvelopeGraph.h index 8cfeaf11f..4f8a5c386 100644 --- a/include/EnvelopeGraph.h +++ b/include/EnvelopeGraph.h @@ -41,14 +41,23 @@ namespace gui class EnvelopeGraph : public QWidget, public ModelView { +public: + enum class ScalingMode + { + Dynamic, + Absolute, + Relative + }; + public: EnvelopeGraph(QWidget* parent); protected: void modelChanged() override; - void mousePressEvent(QMouseEvent* me) override; - void paintEvent(QPaintEvent* pe) override; + void mousePressEvent(QMouseEvent*) override; + void contextMenuEvent(QContextMenuEvent*) override; + void paintEvent(QPaintEvent*) override; private: void toggleAmountModel(); @@ -56,7 +65,9 @@ private: private: QPixmap m_envGraph = embed::getIconPixmap("envelope_graph"); - EnvelopeAndLfoParameters* m_params; + EnvelopeAndLfoParameters* m_params = nullptr; + + ScalingMode m_scaling = ScalingMode::Dynamic; }; } // namespace gui diff --git a/include/lmms_math.h b/include/lmms_math.h index 3f58e3b75..bf5e53a2b 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -327,6 +327,13 @@ static inline T absMin( T a, T b ) return std::abs(a) < std::abs(b) ? a : b; } +//! Returns the linear interpolation of the two values +template +constexpr T lerp(T a, T b, F t) +{ + return (1. - t) * a + t * b; +} + // @brief Calculate number of digits which LcdSpinBox would show for a given number // @note Once we upgrade to C++20, we could probably use std::formatted_size static inline int numDigitsAsInt(float f) diff --git a/src/gui/instrument/EnvelopeGraph.cpp b/src/gui/instrument/EnvelopeGraph.cpp index 4d866b3cc..4cf5da74b 100644 --- a/src/gui/instrument/EnvelopeGraph.cpp +++ b/src/gui/instrument/EnvelopeGraph.cpp @@ -23,12 +23,18 @@ * */ -#include -#include - #include "EnvelopeGraph.h" +#include +#include +#include + #include "EnvelopeAndLfoParameters.h" +#include "lmms_math.h" +#include "ColorHelper.h" + +#include + namespace lmms { @@ -36,15 +42,11 @@ namespace lmms namespace gui { -const int TIME_UNIT_WIDTH = 40; - - EnvelopeGraph::EnvelopeGraph(QWidget* parent) : QWidget(parent), - ModelView(nullptr, this), - m_params(nullptr) + ModelView(nullptr, this) { - setFixedSize(m_envGraph.size()); + setMinimumSize(m_envGraph.size()); } void EnvelopeGraph::modelChanged() @@ -54,87 +56,196 @@ void EnvelopeGraph::modelChanged() void EnvelopeGraph::mousePressEvent(QMouseEvent* me) { - if(me->button() == Qt::LeftButton) + if (me->button() == Qt::LeftButton) { toggleAmountModel(); } +} + +void EnvelopeGraph::contextMenuEvent(QContextMenuEvent* event) +{ + QMenu menu(this); + QMenu* scalingMenu = menu.addMenu(tr("Scaling")); + scalingMenu->setToolTipsVisible(true); + + auto switchTo = [&](ScalingMode scaling) { - toggleAmountModel(); - } + if (m_scaling != scaling) + { + m_scaling = scaling; + update(); + } + }; + + auto addScalingEntry = [scalingMenu, &switchTo, this](const QString& text, const QString& toolTip, ScalingMode scaling) + { + QAction* action = scalingMenu->addAction(text, [&switchTo, scaling]() { switchTo(scaling); }); + action->setCheckable(true); + action->setChecked(m_scaling == scaling); + action->setToolTip(toolTip); + }; + + addScalingEntry( + tr("Dynamic"), + tr("Uses absolute spacings but switches to relative spacing if it's running out of space"), + ScalingMode::Dynamic); + addScalingEntry( + tr("Absolute"), + tr("Provides enough potential space for each segment but does not scale"), + ScalingMode::Absolute); + addScalingEntry( + tr("Relative"), + tr("Always uses all of the available space to display the envelope graph"), + ScalingMode::Relative); + + menu.exec(event->globalPos()); } void EnvelopeGraph::paintEvent(QPaintEvent*) { - QPainter p(this); + QPainter p{this}; p.setRenderHint(QPainter::Antialiasing); // Draw the graph background p.drawPixmap(rect(), m_envGraph); - const auto * params = castModel(); - if (!params) - { - return; - } + const auto* params = castModel(); + if (!params) { return; } + // For the calculation of the percentages we will for now make use of the knowledge + // that the range goes from 0 to a positive max value, i.e. that it is in [0, max]. const float amount = params->getAmountModel().value(); + const float predelay = params->getPredelayModel().value(); + const float predelayPercentage = predelay / params->getPredelayModel().maxValue(); + const float attack = params->getAttackModel().value(); + const float attackPercentage = attack / params->getAttackModel().maxValue(); + const float hold = params->getHoldModel().value(); + const float holdPercentage = hold / params->getHoldModel().maxValue(); + const float decay = params->getDecayModel().value(); + const float decayPercentage = decay / params->getDecayModel().maxValue(); + const float sustain = params->getSustainModel().value(); + const float release = params->getReleaseModel().value(); + const float releasePercentage = release / params->getReleaseModel().maxValue(); - const float gray_amount = 1.0f - fabsf(amount); + // The margin to the left and right so that we do not clip too much of the lines and markers + const float margin = 2.0; + const float availableWidth = width() - margin * 2; - p.setPen(QPen(QColor(static_cast(96 * gray_amount), - static_cast(255 - 159 * gray_amount), - static_cast(128 - 32 * gray_amount)), - 2)); - - const QColor end_points_color(0x99, 0xAF, 0xFF); - const QColor end_points_bg_color(0, 0, 2); - - const int y_base = m_envGraph.height() - 3; - const int avail_height = m_envGraph.height() - 6; - - int x1 = static_cast(predelay * TIME_UNIT_WIDTH); - int x2 = x1 + static_cast(attack * TIME_UNIT_WIDTH); - int x3 = x2 + static_cast(hold * TIME_UNIT_WIDTH); - int x4 = x3 + static_cast((decay * (1 - sustain)) * TIME_UNIT_WIDTH); - int x5 = x4 + static_cast(release * TIME_UNIT_WIDTH); - - if (x5 > 174) + // Now determine the maximum width for one segment according to the scaling setting. + // The different scalings use different means to compute the maximum available width per segment. + const auto computeMaximumSegmentWidthAbsolute = [&]() -> float { - x1 = (x1 * 174) / x5; - x2 = (x2 * 174) / x5; - x3 = (x3 * 174) / x5; - x4 = (x4 * 174) / x5; - x5 = (x5 * 174) / x5; + return availableWidth / 5.; + }; + + const auto computeMaximumSegmentWidthRelative = [&]() -> float + { + const float sumOfSegments = predelayPercentage + attackPercentage + holdPercentage + decayPercentage + releasePercentage; + + return sumOfSegments != 0. + ? availableWidth / sumOfSegments + : computeMaximumSegmentWidthAbsolute(); + }; + + const auto computeMaximumSegmentWidthDynamic = [&]() -> float + { + const float sumOfSegments = predelayPercentage + attackPercentage + holdPercentage + decayPercentage + releasePercentage; + + float preliminarySegmentWidth = 80. / 182. * availableWidth; + + const float neededWidth = sumOfSegments * preliminarySegmentWidth; + + if (neededWidth > availableWidth && sumOfSegments != 0.) + { + return computeMaximumSegmentWidthRelative(); + } + + return preliminarySegmentWidth; + }; + + // This is the maximum width that each of the five segments (DAHDR) can occupy. + float maximumSegmentWidth; + + switch (m_scaling) + { + case ScalingMode::Absolute: + maximumSegmentWidth = computeMaximumSegmentWidthAbsolute(); + break; + case ScalingMode::Relative: + maximumSegmentWidth = computeMaximumSegmentWidthRelative(); + break; + case ScalingMode::Dynamic: + default: + maximumSegmentWidth = computeMaximumSegmentWidthDynamic(); + break; } - x1 += 2; - x2 += 2; - x3 += 2; - x4 += 2; - x5 += 2; - p.drawLine(x1, y_base, x2, y_base - avail_height); - p.fillRect(x1 - 1, y_base - 2, 4, 4, end_points_bg_color); - p.fillRect(x1, y_base - 1, 2, 2, end_points_color); + // Compute the actual widths that the segments occupy and add them to the + // previous x coordinates starting at the margin. + const float predelayX = margin + predelayPercentage * maximumSegmentWidth; + const float attackX = predelayX + attackPercentage * maximumSegmentWidth; + const float holdX = attackX + holdPercentage * maximumSegmentWidth; + const float decayX = holdX + (decayPercentage * (1 - sustain)) * maximumSegmentWidth; + const float releaseX = decayX + releasePercentage * maximumSegmentWidth; - p.drawLine(x2, y_base - avail_height, x3, y_base - avail_height); - p.fillRect(x2 - 1, y_base - 2 - avail_height, 4, 4, - end_points_bg_color); - p.fillRect(x2, y_base - 1 - avail_height, 2, 2, end_points_color); + // Now compute the "full" points including y coordinates + const int yTop = 3; + const qreal yBase = height() - 3; + const int availableHeight = yBase - yTop; - const int sustainHeight = static_cast(y_base - avail_height + (1 - sustain) * avail_height); + const QPointF predelayPoint{predelayX, yBase}; + const QPointF attackPoint{attackX, yTop}; + const QPointF holdPoint{holdX, yTop}; + const QPointF decayPoint{decayX, yTop + (1 - sustain) * availableHeight}; + const QPointF releasePoint{releaseX, yBase}; - p.drawLine(x3, y_base-avail_height, x4, sustainHeight); - p.fillRect(x3 - 1, y_base - 2 - avail_height, 4, 4, end_points_bg_color); - p.fillRect(x3, y_base - 1 - avail_height, 2, 2, end_points_color); - - p.drawLine(x4, sustainHeight, x5, y_base); - p.fillRect(x4 - 1, sustainHeight - 2, 4, 4, end_points_bg_color); - p.fillRect(x4, sustainHeight - 1, 2, 2, end_points_color); - p.fillRect(x5 - 1, y_base - 2, 4, 4, end_points_bg_color); - p.fillRect(x5, y_base - 1, 2, 2, end_points_color); + // Now that we have all points we can draw the lines + + // Compute the color of the lines based on the amount of the envelope + const float absAmount = std::abs(amount); + const QColor noAmountColor{96, 91, 96}; + const QColor fullAmountColor{0, 255, 128}; + const QColor lineColor{ColorHelper::interpolateInRgb(noAmountColor, fullAmountColor, absAmount)}; + + // Determine the line width so that it scales with the widget + // Use the minimum value of the current width and height to compute it. + const qreal lineWidth = std::min(width(), height()) / 20.; + const QPen linePen{lineColor, lineWidth}; + p.setPen(linePen); + + QPolygonF linePoly; + linePoly << predelayPoint << attackPoint << holdPoint << decayPoint << releasePoint; + p.drawPolyline(linePoly); + + // Now draw all marker on top of the lines + const QColor markerFillColor{153, 175, 255}; + const QColor markerOutlineColor{0, 0, 0}; + + QPen pen; + pen.setWidthF(lineWidth * 0.75); + pen.setBrush(markerOutlineColor); + p.setPen(pen); + p.setBrush(markerFillColor); + + // Compute the size of the circle we will draw based on the line width + const qreal baseRectSize = lineWidth * 3; + const QSizeF rectSize{baseRectSize, baseRectSize}; + + auto drawMarker = [&](const QPointF& point) + { + // Create a rectangle that has the given point at its center + QRectF bgRect{point + QPointF(-baseRectSize / 2, -baseRectSize / 2), rectSize}; + p.drawEllipse(bgRect); + }; + + drawMarker(predelayPoint); + drawMarker(attackPoint); + drawMarker(holdPoint); + drawMarker(decayPoint); + drawMarker(releasePoint); } void EnvelopeGraph::toggleAmountModel() @@ -142,14 +253,7 @@ void EnvelopeGraph::toggleAmountModel() auto* params = castModel(); auto& amountModel = params->getAmountModel(); - if (amountModel.value() < 1.0f ) - { - amountModel.setValue( 1.0f ); - } - else - { - amountModel.setValue( 0.0f ); - } + amountModel.setValue(amountModel.value() < 1.0 ? 1.0 : 0.0); } } // namespace gui From 815f88dd09692079bf68063f8192e4d144b8fe4b Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 14 Apr 2024 10:31:27 +0200 Subject: [PATCH 136/191] Scalable LFO graph (#7203) ## Scalable LFO graph Make the rendering of the LFO graph scalable. Change the fixed size to a minimum size. Adjust the rendering code such that it uses the width and height of the widget instead of the background pixmap. Only draw only poly line once instead of many line segments. Collect the points in the for-loop to be able to do so. This makes the code a bit more understandable because we now compute exacly one point per iteration in the for-loop. Use the same interpolation for the line color like in the envelope graph. Rename some variables to make them consistent with the other code. Remove the member `m_params` which is not used anyway. This also allows for the removal of the overridden `modelChanged` method. ## Use "Hz" instead of "ms/LFO" Use the more common unit "Hz" to display the frequency of the LFO instead of "ms/LFO". The frequency is always displayed with three digits after the decimal separator to prevent "jumps". ## Take "Freq * 100" into account This commit fixes a bug where the "Freq * 100" option was not taken into account when computing and displaying the frequency of the LFO. ## Keep info text legible Draw a slightly transparent black rectangle underneath the text to keep it legible, e.g. for high frequencies with an LFO amount of 1 which results in a very bright and dense graph. ## Extract drawing of info text into method Extract the drawing of the info text into its own private method `drawInfoText`. --- include/LfoGraph.h | 5 +- src/gui/instrument/LfoGraph.cpp | 113 ++++++++++++++++++++------------ 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/include/LfoGraph.h b/include/LfoGraph.h index 733db3a34..9d566770f 100644 --- a/include/LfoGraph.h +++ b/include/LfoGraph.h @@ -45,19 +45,16 @@ public: LfoGraph(QWidget* parent); protected: - void modelChanged() override; - void mousePressEvent(QMouseEvent* me) override; void paintEvent(QPaintEvent* pe) override; private: + void drawInfoText(const EnvelopeAndLfoParameters&); void toggleAmountModel(); private: QPixmap m_lfoGraph = embed::getIconPixmap("lfo_graph"); - EnvelopeAndLfoParameters* m_params = nullptr; - float m_randomGraph {0.}; }; diff --git a/src/gui/instrument/LfoGraph.cpp b/src/gui/instrument/LfoGraph.cpp index d02f583d0..7edbacb09 100644 --- a/src/gui/instrument/LfoGraph.cpp +++ b/src/gui/instrument/LfoGraph.cpp @@ -30,6 +30,7 @@ #include "EnvelopeAndLfoParameters.h" #include "Oscillator.h" +#include "ColorHelper.h" #include "gui_templates.h" @@ -45,12 +46,7 @@ LfoGraph::LfoGraph(QWidget* parent) : QWidget(parent), ModelView(nullptr, this) { - setFixedSize(m_lfoGraph.size()); -} - -void LfoGraph::modelChanged() -{ - m_params = castModel(); + setMinimumSize(m_lfoGraph.size()); } void LfoGraph::mousePressEvent(QMouseEvent* me) @@ -78,81 +74,114 @@ void LfoGraph::paintEvent(QPaintEvent*) const f_cnt_t attackFrames = params->getLfoAttackFrames(); const f_cnt_t oscillationFrames = params->getLfoOscillationFrames(); const bool x100 = params->getX100Model().value(); - const int waveModel = params->getLfoWaveModel().value(); + const int lfoWaveModel = params->getLfoWaveModel().value(); + const auto * userWave = params->getLfoUserWave().get(); - int LFO_GRAPH_W = m_lfoGraph.width() - 3; // subtract border - int LFO_GRAPH_H = m_lfoGraph.height() - 6; // subtract border - int graph_x_base = 2; - int graph_y_base = 3 + LFO_GRAPH_H / 2; + const int margin = 3; + const int lfoGraphWidth = width() - margin; // subtract margin + const int lfoGraphHeight = height() - 2 * margin; // subtract margin + int graphBaseX = 2; + int graphBaseY = margin + lfoGraphHeight / 2; - const float frames_for_graph = + const float framesForGraph = SECS_PER_LFO_OSCILLATION * Engine::audioEngine()->baseSampleRate() / 10; - const float gray = 1.0 - fabsf(amount); - const auto red = static_cast(96 * gray); - const auto green = static_cast(255 - 159 * gray); - const auto blue = static_cast(128 - 32 * gray); - const QColor penColor(red, green, blue); - p.setPen(QPen(penColor, 1.5)); + float oscFrames = oscillationFrames * (x100 ? 100. : 1.); - float osc_frames = oscillationFrames; + QPolygonF polyLine; + polyLine << QPointF(graphBaseX - 1, graphBaseY); - if (x100) { osc_frames *= 100.0f; } - - float old_y = 0; - for (int x = 0; x <= LFO_GRAPH_W; ++x) + // Collect the points for the poly line by sampling the LFO according to its shape + for (int x = 0; x <= lfoGraphWidth; ++x) { - float val = 0.0; - float cur_sample = x * frames_for_graph / LFO_GRAPH_W; - if (static_cast(cur_sample) > predelayFrames) + float value = 0.0; + float currentSample = x * framesForGraph / lfoGraphWidth; + const auto sampleAsFrameCount = static_cast(currentSample); + if (sampleAsFrameCount > predelayFrames) { - float phase = (cur_sample -= predelayFrames) / osc_frames; - switch (static_cast(waveModel)) + currentSample -= predelayFrames; + const float phase = currentSample / oscFrames; + + const auto lfoShape = static_cast(lfoWaveModel); + switch (lfoShape) { case EnvelopeAndLfoParameters::LfoShape::SineWave: default: - val = Oscillator::sinSample(phase); + value = Oscillator::sinSample(phase); break; case EnvelopeAndLfoParameters::LfoShape::TriangleWave: - val = Oscillator::triangleSample(phase); + value = Oscillator::triangleSample(phase); break; case EnvelopeAndLfoParameters::LfoShape::SawWave: - val = Oscillator::sawSample(phase); + value = Oscillator::sawSample(phase); break; case EnvelopeAndLfoParameters::LfoShape::SquareWave: - val = Oscillator::squareSample(phase); + value = Oscillator::squareSample(phase); break; case EnvelopeAndLfoParameters::LfoShape::RandomWave: if (x % (int)(900 * lfoSpeed + 1) == 0) { m_randomGraph = Oscillator::noiseSample(0.0); } - val = m_randomGraph; + value = m_randomGraph; break; case EnvelopeAndLfoParameters::LfoShape::UserDefinedWave: - val = Oscillator::userWaveSample(m_params->getLfoUserWave().get(), phase); + value = Oscillator::userWaveSample(userWave, phase); break; } - if (static_cast(cur_sample) <= attackFrames) + if (sampleAsFrameCount <= attackFrames) { - val *= cur_sample / attackFrames; + value *= currentSample / attackFrames; } } - float cur_y = -LFO_GRAPH_H / 2.0f * val; - p.drawLine(QLineF(graph_x_base + x - 1, graph_y_base + old_y, graph_x_base + x, graph_y_base + cur_y)); - old_y = cur_y; + const float currentY = -lfoGraphHeight / 2.0f * value; + + polyLine << QPointF(graphBaseX + x, graphBaseY + currentY); } - // Draw the info text - int ms_per_osc = static_cast(SECS_PER_LFO_OSCILLATION * lfoSpeed * 1000.0); + // Compute the color of the lines based on the amount of the LFO + const float absAmount = std::abs(amount); + const QColor noAmountColor{96, 91, 96}; + const QColor fullAmountColor{0, 255, 128}; + const QColor lineColor{ColorHelper::interpolateInRgb(noAmountColor, fullAmountColor, absAmount)}; + p.setPen(QPen(lineColor, 1.5)); + + p.drawPolyline(polyLine); + + drawInfoText(*params); +} + +void LfoGraph::drawInfoText(const EnvelopeAndLfoParameters& params) +{ + QPainter p(this); + + const float lfoSpeed = params.getLfoSpeedModel().value(); + const bool x100 = params.getX100Model().value(); + + const float hertz = 1. / (SECS_PER_LFO_OSCILLATION * lfoSpeed) * (x100 ? 100. : 1.); + const auto infoText = tr("%1 Hz").arg(hertz, 0, 'f', 3); + + // First configure the font so that we get correct results for the font metrics used below QFont f = p.font(); f.setPixelSize(height() * 0.2); p.setFont(f); + + // This is the position where the text and its rectangle will be rendered + const QPoint textPosition(4, height() - 6); + + // Draw a slightly transparent black rectangle underneath the text to keep it legible + const QFontMetrics fontMetrics(f); + // This gives the bounding rectangle if the text was rendered at the origin ... + const auto boundingRect = fontMetrics.boundingRect(infoText); + // ... so we translate it to the actual position where the text will be rendered. + p.fillRect(boundingRect.translated(textPosition), QColor{0, 0, 0, 192}); + + // Now draw the actual info text p.setPen(QColor(201, 201, 225)); - p.drawText(4, m_lfoGraph.height() - 6, tr("%1 ms/LFO").arg(ms_per_osc)); + p.drawText(textPosition, infoText); } void LfoGraph::toggleAmountModel() From d3ab31558cb10c5dbb6c5a56ecafa53c87b69015 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Sun, 14 Apr 2024 12:54:10 +0100 Subject: [PATCH 137/191] Support VSTs on Linux even if Wine is unavailable (#7205) --- CMakeLists.txt | 68 ++++++++++++++++--------- plugins/Vestige/CMakeLists.txt | 27 +++++----- plugins/VstBase/CMakeLists.txt | 6 +-- plugins/VstBase/RemoteVstPlugin32.cmake | 8 --- plugins/VstBase/RemoteVstPlugin64.cmake | 6 --- plugins/VstEffect/CMakeLists.txt | 8 +-- src/3rdparty/CMakeLists.txt | 2 +- 7 files changed, 66 insertions(+), 59 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d10a62d10..8401c3c41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,8 +94,8 @@ OPTION(WANT_STK "Include Stk (Synthesis Toolkit) support" ON) OPTION(WANT_SWH "Include Steve Harris's LADSPA plugins" ON) OPTION(WANT_TAP "Include Tom's Audio Processing LADSPA plugins" ON) OPTION(WANT_VST "Include VST support" ON) -OPTION(WANT_VST_32 "Include 32-bit VST support" ON) -OPTION(WANT_VST_64 "Include 64-bit VST support" ON) +OPTION(WANT_VST_32 "Include 32-bit Windows VST support" ON) +OPTION(WANT_VST_64 "Include 64-bit Windows VST support" ON) OPTION(WANT_WINMM "Include WinMM MIDI support" OFF) OPTION(WANT_DEBUG_FPE "Debug floating point exceptions" OFF) option(WANT_DEBUG_ASAN "Enable AddressSanitizer" OFF) @@ -131,6 +131,9 @@ IF(LMMS_BUILD_WIN32) SET(WANT_WINMM ON) SET(BUNDLE_QT_TRANSLATIONS ON) SET(LMMS_HAVE_WINMM TRUE) + if(NOT LMMS_BUILD_WIN64) + set(WANT_VST_64 OFF) + endif() SET(STATUS_ALSA "") SET(STATUS_OSS "") SET(STATUS_PULSEAUDIO "") @@ -587,26 +590,42 @@ IF(WANT_SNDIO) ENDIF(WANT_SNDIO) # check for WINE -IF(WANT_VST) - FIND_PACKAGE(Wine) - IF(WINE_FOUND) - SET(LMMS_SUPPORT_VST TRUE) - IF(WINE_LIBRARY_FIX) - SET(STATUS_VST "OK, with workaround linking ${WINE_LIBRARY_FIX}") - ELSE() - SET(STATUS_VST "OK") - ENDIF() - ELSEIF(WANT_VST_NOWINE) - SET(LMMS_SUPPORT_VST TRUE) - SET(STATUS_VST "OK") - ELSE(WINE_FOUND) - SET(STATUS_VST "not found, please install (lib)wine-dev (or similar) - 64 bit systems additionally need gcc-multilib and g++-multilib") - ENDIF(WINE_FOUND) -ENDIF(WANT_VST) -IF(LMMS_BUILD_WIN32) - SET(LMMS_SUPPORT_VST TRUE) - SET(STATUS_VST "OK") -ENDIF(LMMS_BUILD_WIN32) +if(WANT_VST) + if((WANT_VST_32 OR WANT_VST_64) AND NOT LMMS_BUILD_WIN32) + find_package(Wine) + include(CheckWineGcc) + endif() + macro(check_vst bits) + if(NOT WANT_VST_${bits}) + set(STATUS_VST_${bits} "Not built, as requested") + elseif(LMMS_BUILD_WIN32) + set(STATUS_VST_${bits} "OK") + set(LMMS_HAVE_VST_${bits} TRUE) + elseif(NOT WINE_FOUND) + set(STATUS_VST_${bits} "not found, please install (lib)wine-dev (or similar) - 64 bit systems additionally need gcc-multilib and g++-multilib") + else() + CheckWineGcc("${bits}" "${WINEGCC}" WINEGCC_WORKING) + if(WINEGCC_WORKING) + set(LMMS_HAVE_VST_${bits} TRUE) + if(WINE_LIBRARY_FIX) + set(STATUS_VST_${bits} "OK, with workaround linking ${WINE_LIBRARY_FIX}") + else() + set(STATUS_VST_${bits} "OK") + endif() + else() + set(STATUS_VST_${bits} "winegcc fails to compile ${bits}-bit binaries, please make sure you have ${bits}-bit GCC libraries") + endif() + endif() + endmacro() + check_vst(32) + check_vst(64) + if(LMMS_HAVE_VST_32 OR LMMS_HAVE_VST_64 OR LMMS_BUILD_LINUX) + set(LMMS_HAVE_VST TRUE) + set(STATUS_VST "OK") + else() + set(STATUS_VST "No hosts selected and available") + endif() +endif() IF(WANT_DEBUG_FPE) IF(LMMS_BUILD_LINUX OR LMMS_BUILD_APPLE) @@ -852,8 +871,9 @@ MESSAGE( "* SoundFont2 player : ${STATUS_FLUIDSYNTH}\n" "* Sid instrument : ${STATUS_SID}\n" "* Stk Mallets : ${STATUS_STK}\n" -"* VST-instrument hoster : ${STATUS_VST}\n" -"* VST-effect hoster : ${STATUS_VST}\n" +"* VST plugin host : ${STATUS_VST}\n" +" * 32-bit Windows host : ${STATUS_VST_32}\n" +" * 64-bit Windows host : ${STATUS_VST_64}\n" "* CALF LADSPA plugins : ${STATUS_CALF}\n" "* CAPS LADSPA plugins : ${STATUS_CAPS}\n" "* CMT LADSPA plugins : ${STATUS_CMT}\n" diff --git a/plugins/Vestige/CMakeLists.txt b/plugins/Vestige/CMakeLists.txt index a51b051fd..0a5847889 100644 --- a/plugins/Vestige/CMakeLists.txt +++ b/plugins/Vestige/CMakeLists.txt @@ -1,14 +1,15 @@ -IF(LMMS_SUPPORT_VST) - INCLUDE(BuildPlugin) - LINK_DIRECTORIES("${CMAKE_CURRENT_BINARY_DIR}/..") - IF(LMMS_BUILD_LINUX) - LINK_LIBRARIES(-Wl,--enable-new-dtags) - SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) - SET(CMAKE_INSTALL_RPATH "$ORIGIN") - ELSE() - SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${PLUGIN_DIR}") - ENDIF() - BUILD_PLUGIN(vestige Vestige.cpp Vestige.h MOCFILES Vestige.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") - TARGET_LINK_LIBRARIES(vestige vstbase) -ENDIF(LMMS_SUPPORT_VST) +if(NOT LMMS_HAVE_VST) + return() +endif() +include(BuildPlugin) +link_directories("${CMAKE_CURRENT_BINARY_DIR}/..") +if(LMMS_BUILD_LINUX) + link_libraries(-Wl,--enable-new-dtags) + set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) + set(CMAKE_INSTALL_RPATH "$ORIGIN") +else() + set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${PLUGIN_DIR}") +endif() +build_plugin(vestige Vestige.cpp Vestige.h MOCFILES Vestige.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") +target_link_libraries(vestige vstbase) diff --git a/plugins/VstBase/CMakeLists.txt b/plugins/VstBase/CMakeLists.txt index 8d3262b1a..046f515ea 100644 --- a/plugins/VstBase/CMakeLists.txt +++ b/plugins/VstBase/CMakeLists.txt @@ -1,4 +1,4 @@ -IF(NOT LMMS_SUPPORT_VST) +if(NOT LMMS_HAVE_VST) RETURN() ENDIF() @@ -47,11 +47,11 @@ foreach(var ${export_variables}) endforeach() # build 32 bit version of RemoteVstPlugin -IF(WANT_VST_32) +if(LMMS_HAVE_VST_32) INCLUDE("${CMAKE_CURRENT_LIST_DIR}/RemoteVstPlugin32.cmake") ENDIF() # build 64 bit version of RemoteVstPlugin -IF(WANT_VST_64) +if(LMMS_HAVE_VST_64) INCLUDE("${CMAKE_CURRENT_LIST_DIR}/RemoteVstPlugin64.cmake") ENDIF() diff --git a/plugins/VstBase/RemoteVstPlugin32.cmake b/plugins/VstBase/RemoteVstPlugin32.cmake index f39bd93d0..0f98d34e0 100644 --- a/plugins/VstBase/RemoteVstPlugin32.cmake +++ b/plugins/VstBase/RemoteVstPlugin32.cmake @@ -47,13 +47,6 @@ ELSEIF(LMMS_BUILD_WIN64 AND MSVC) INSTALL_EXTERNAL_PROJECT(RemoteVstPlugin32) ELSEIF(LMMS_BUILD_LINUX) - # Use winegcc - INCLUDE(CheckWineGcc) - CheckWineGcc(32 "${WINEGCC}" WINEGCC_WORKING) - IF(NOT WINEGCC_WORKING) - MESSAGE(WARNING "winegcc fails to compile 32-bit binaries, please make sure you have 32-bit GCC libraries") - RETURN() - ENDIF() ExternalProject_Add(RemoteVstPlugin32 "${EXTERNALPROJECT_ARGS}" CMAKE_ARGS @@ -63,7 +56,6 @@ ELSEIF(LMMS_BUILD_LINUX) ) INSTALL(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/../32/RemoteVstPlugin32" "${CMAKE_CURRENT_BINARY_DIR}/../32/RemoteVstPlugin32.exe.so" DESTINATION "${PLUGIN_DIR}/32") - ELSEIF(CMAKE_TOOLCHAIN_FILE_32) ExternalProject_Add(RemoteVstPlugin32 "${EXTERNALPROJECT_ARGS}" diff --git a/plugins/VstBase/RemoteVstPlugin64.cmake b/plugins/VstBase/RemoteVstPlugin64.cmake index 65b33a162..2f4a745ac 100644 --- a/plugins/VstBase/RemoteVstPlugin64.cmake +++ b/plugins/VstBase/RemoteVstPlugin64.cmake @@ -1,12 +1,6 @@ IF(LMMS_BUILD_WIN64) ADD_SUBDIRECTORY(RemoteVstPlugin) ELSEIF(LMMS_BUILD_LINUX) - INCLUDE(CheckWineGcc) - CheckWineGcc(64 "${WINEGCC}" WINEGCC_WORKING) - IF(NOT WINEGCC_WORKING) - MESSAGE(WARNING "winegcc fails to compile 64-bit binaries, please make sure you have 64-bit GCC libraries") - RETURN() - ENDIF() ExternalProject_Add(RemoteVstPlugin64 "${EXTERNALPROJECT_ARGS}" CMAKE_ARGS diff --git a/plugins/VstEffect/CMakeLists.txt b/plugins/VstEffect/CMakeLists.txt index 68ef141d9..3cbe8e8cc 100644 --- a/plugins/VstEffect/CMakeLists.txt +++ b/plugins/VstEffect/CMakeLists.txt @@ -1,4 +1,7 @@ -IF(LMMS_SUPPORT_VST) +if(NOT LMMS_HAVE_VST) + return() +endif() + INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/../vst_base") LINK_DIRECTORIES("${CMAKE_CURRENT_BINARY_DIR}/..") @@ -12,6 +15,3 @@ ENDIF() BUILD_PLUGIN(vsteffect VstEffect.cpp VstEffectControls.cpp VstEffectControlDialog.cpp VstSubPluginFeatures.cpp VstEffect.h VstEffectControls.h VstEffectControlDialog.h VstSubPluginFeatures.h MOCFILES VstEffectControlDialog.h VstEffectControls.h EMBEDDED_RESOURCES *.png) TARGET_LINK_LIBRARIES(vsteffect vstbase) - -ENDIF(LMMS_SUPPORT_VST) - diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index f1578a970..3339eb926 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -1,4 +1,4 @@ -IF(LMMS_BUILD_LINUX AND WANT_VST) +if(LMMS_BUILD_LINUX AND LMMS_HAVE_VST) set(BUILD_SHARED_LIBS OFF) add_subdirectory(qt5-x11embed) ENDIF() From d2c2a805064e504edd06841609be28e0ac38a26e Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 14 Apr 2024 18:03:39 +0200 Subject: [PATCH 138/191] Update CMT submodule / Upgrade code for CMT delays (#7206) ## Bump CMT to d8bf8084aa3 Bump the CMT submodule to commit d8bf8084aa3 which contains the underlying fixes for issue #5167. The CMT delay uses `sprintf` calls to generate the technical names and display names of the delays. These calls are locale dependent. As a consequence for example the feedback delay might have been saved either as "fbdelay_0.1s" (point) or "fbdelay_0,1s" (comma) in a save file. The CMT fix makes sure that all delays use points in their names and thus that they now always report the same name strings. ## Add upgrade routine for CMT delays Add an upgrade routine for CMT delays which works in conjunction with the upgraded CMT submodule. Because the delays will now always report their name with points old save files which might contain versions with the comma must be upgraded to a name with a point. --- include/DataFile.h | 1 + plugins/LadspaEffect/cmt/cmt | 2 +- src/core/DataFile.cpp | 41 +++++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/include/DataFile.h b/include/DataFile.h index ce5d4edf4..452481a7f 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -129,6 +129,7 @@ private: void upgrade_midiCCIndexing(); void upgrade_loopsRename(); void upgrade_noteTypes(); + void upgrade_fixCMTDelays(); // List of all upgrade methods static const std::vector UPGRADE_METHODS; diff --git a/plugins/LadspaEffect/cmt/cmt b/plugins/LadspaEffect/cmt/cmt index 6e6e291fb..d8bf8084a 160000 --- a/plugins/LadspaEffect/cmt/cmt +++ b/plugins/LadspaEffect/cmt/cmt @@ -1 +1 @@ -Subproject commit 6e6e291fbad1138c808860ba3f140a963b52fa58 +Subproject commit d8bf8084aa3a47497092f5ab99c843a55090d151 diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index eedb7f01d..00c6845f3 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -83,7 +83,8 @@ const std::vector DataFile::UPGRADE_METHODS = { &DataFile::upgrade_defaultTripleOscillatorHQ, &DataFile::upgrade_mixerRename , &DataFile::upgrade_bbTcoRename, &DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing, - &DataFile::upgrade_loopsRename , &DataFile::upgrade_noteTypes + &DataFile::upgrade_loopsRename , &DataFile::upgrade_noteTypes, + &DataFile::upgrade_fixCMTDelays }; // Vector of all versions that have upgrade routines. @@ -1684,6 +1685,44 @@ void DataFile::upgrade_noteTypes() } } +void DataFile::upgrade_fixCMTDelays() +{ + static const QMap nameMap { + { "delay_0,01s", "delay_0.01s" }, + { "delay_0,1s", "delay_0.1s" }, + { "fbdelay_0,01s", "fbdelay_0.01s" }, + { "fbdelay_0,1s", "fbdelay_0.1s" } + }; + + const auto effects = elementsByTagName("effect"); + + for (int i = 0; i < effects.size(); ++i) + { + auto effect = effects.item(i).toElement(); + + // We are only interested in LADSPA plugins + if (effect.attribute("name") != "ladspaeffect") { continue; } + + // Fetch all attributes (LMMS) beneath the LADSPA effect so that we can check the value of the plugin attribute (XML) + auto attributes = effect.elementsByTagName("attribute"); + for (int j = 0; j < attributes.size(); ++j) + { + auto attribute = attributes.item(j).toElement(); + + if (attribute.attribute("name") == "plugin") + { + const auto attributeValue = attribute.attribute("value"); + + const auto it = nameMap.constFind(attributeValue); + if (it != nameMap.constEnd()) + { + attribute.setAttribute("value", *it); + } + } + } + } +} + /** \brief Note range has been extended to match MIDI specification * From d5e1d9e853f72468ab24ea04005ca6e63a98dcca Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 17 Apr 2024 19:21:23 +0200 Subject: [PATCH 139/191] Move icon determination into TrackLabelButton again (#7209) Move icon determination into TrackLabelButton again Fully undo the changes made in commit 88e0e94dcdb because the intermediate revert made in commit 04ecf733951 seems to have led to a performance problem due to the icon being set over and over again in `TrackLabelButton::paintEvent`. The original intention of the changes made in pull request #7114 was to remove the painting code that dynamically determines the icon over and over again. Ideally the icon that is used by an instrument should be somewhat of a "static" property that should be known very early on when an instrument view is created. There should not be any need to dynamically resolve the icon over and over, especially not in a button class very far down in the widget hierarchy. However, due to technical reasons this is not the case in the current code. See pull request #7132 for more details. --- include/InstrumentTrackView.h | 2 -- src/gui/tracks/InstrumentTrackView.cpp | 7 ------- src/gui/tracks/TrackLabelButton.cpp | 25 +++++++++++++++++++++---- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index cfde89bde..c7d524b36 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -71,8 +71,6 @@ public: // Create a menu for assigning/creating channels for this track QMenu * createMixerMenu( QString title, QString newMixerLabel ) override; - QPixmap determinePixmap(); - protected: void modelChanged() override; diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index c812999fd..1d9991c31 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -41,7 +41,6 @@ #include "MixerView.h" #include "GuiApplication.h" #include "Instrument.h" -#include "InstrumentTrack.h" #include "InstrumentTrackWindow.h" #include "MainWindow.h" #include "MidiClient.h" @@ -397,12 +396,6 @@ QMenu * InstrumentTrackView::createMixerMenu(QString title, QString newMixerLabe return mixerMenu; } -QPixmap InstrumentTrackView::determinePixmap() -{ - return determinePixmap(dynamic_cast(getTrack())); -} - - QPixmap InstrumentTrackView::determinePixmap(InstrumentTrack* instrumentTrack) { if (instrumentTrack) diff --git a/src/gui/tracks/TrackLabelButton.cpp b/src/gui/tracks/TrackLabelButton.cpp index c164b780e..871d42316 100644 --- a/src/gui/tracks/TrackLabelButton.cpp +++ b/src/gui/tracks/TrackLabelButton.cpp @@ -31,6 +31,8 @@ #include "ConfigManager.h" #include "embed.h" #include "InstrumentTrackView.h" +#include "Instrument.h" +#include "InstrumentTrack.h" #include "RenameDialog.h" #include "TrackRenameLineEdit.h" #include "TrackView.h" @@ -181,12 +183,27 @@ void TrackLabelButton::mouseReleaseEvent( QMouseEvent *_me ) void TrackLabelButton::paintEvent(QPaintEvent* pe) { - InstrumentTrackView* instrumentTrackView = dynamic_cast(m_trackView); - if (instrumentTrackView) + if (m_trackView->getTrack()->type() == Track::Type::Instrument) { - setIcon(instrumentTrackView->determinePixmap()); + auto it = dynamic_cast(m_trackView->getTrack()); + const PixmapLoader* pl; + auto get_logo = [](InstrumentTrack* it) -> const PixmapLoader* + { + return it->instrument()->key().isValid() + ? it->instrument()->key().logo() + : it->instrument()->descriptor()->logo; + }; + if (it && it->instrument() && + it->instrument()->descriptor() && + (pl = get_logo(it))) + { + if (pl->pixmapName() != m_iconName) + { + m_iconName = pl->pixmapName(); + setIcon(pl->pixmap()); + } + } } - QToolButton::paintEvent(pe); } From df11a989021d6452d06a9038b3ea0a9dee9b0e66 Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:21:58 +0530 Subject: [PATCH 140/191] Bump Qt minimum version to 5.9 (#7204) * bump qt minimum version to 5.9 * cleanup cmake checks * Remove the obsoleted check. * Missed this while removing the check. --- CMakeLists.txt | 2 +- plugins/VstBase/VstPlugin.cpp | 4 ---- src/core/ConfigManager.cpp | 2 -- src/core/main.cpp | 2 -- src/gui/MainWindow.cpp | 6 ------ 5 files changed, 1 insertion(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8401c3c41..48d1904f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,7 +179,7 @@ check_library_exists(rt shm_open "" LMMS_HAVE_LIBRT) LIST(APPEND CMAKE_PREFIX_PATH "${CMAKE_INSTALL_PREFIX}") -FIND_PACKAGE(Qt5 5.6.0 COMPONENTS Core Gui Widgets Xml REQUIRED) +FIND_PACKAGE(Qt5 5.9.0 COMPONENTS Core Gui Widgets Xml REQUIRED) FIND_PACKAGE(Qt5 COMPONENTS LinguistTools QUIET) INCLUDE_DIRECTORIES( diff --git a/plugins/VstBase/VstPlugin.cpp b/plugins/VstBase/VstPlugin.cpp index b23ae39bf..5dcd864f8 100644 --- a/plugins/VstBase/VstPlugin.cpp +++ b/plugins/VstBase/VstPlugin.cpp @@ -735,14 +735,12 @@ void VstPlugin::createUI( QWidget * parent ) QWidget* container = nullptr; -#if QT_VERSION >= 0x050100 if (m_embedMethod == "qt" ) { QWindow* vw = QWindow::fromWinId(m_pluginWindowID); container = QWidget::createWindowContainer(vw, parent ); container->installEventFilter(this); } else -#endif #ifdef LMMS_BUILD_WIN32 if (m_embedMethod == "win32" ) @@ -801,7 +799,6 @@ void VstPlugin::createUI( QWidget * parent ) bool VstPlugin::eventFilter(QObject *obj, QEvent *event) { -#if QT_VERSION >= 0x050100 if (embedMethod() == "qt" && obj == m_pluginWidget) { if (event->type() == QEvent::Show) { @@ -809,7 +806,6 @@ bool VstPlugin::eventFilter(QObject *obj, QEvent *event) } qDebug() << obj << event; } -#endif return false; } diff --git a/src/core/ConfigManager.cpp b/src/core/ConfigManager.cpp index 61d84770a..19ce80ddb 100644 --- a/src/core/ConfigManager.cpp +++ b/src/core/ConfigManager.cpp @@ -192,9 +192,7 @@ QStringList ConfigManager::availableVstEmbedMethods() { QStringList methods; methods.append("none"); -#if QT_VERSION >= 0x050100 methods.append("qt"); -#endif #ifdef LMMS_BUILD_WIN32 methods.append("win32"); #endif diff --git a/src/core/main.cpp b/src/core/main.cpp index cadffdafa..abea43970 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -356,9 +356,7 @@ int main( int argc, char * * argv ) // don't let OS steal the menu bar. FIXME: only effective on Qt4 QCoreApplication::setAttribute( Qt::AA_DontUseNativeMenuBar ); #endif -#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); -#endif QCoreApplication * app = coreOnly ? new QCoreApplication( argc, argv ) : new gui::MainApplication(argc, argv); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 072edc0ec..3522d0e2d 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -324,10 +324,7 @@ void MainWindow::finalize() SLOT(onExportProjectMidi()), Qt::CTRL + Qt::Key_M ); -// Prevent dangling separator at end of menu per https://bugreports.qt.io/browse/QTBUG-40071 -#if !(defined(LMMS_BUILD_APPLE) && (QT_VERSION < 0x050600)) project_menu->addSeparator(); -#endif project_menu->addAction( embed::getIconPixmap( "exit" ), tr( "&Quit" ), qApp, SLOT(closeAllWindows()), Qt::CTRL + Qt::Key_Q ); @@ -400,10 +397,7 @@ void MainWindow::finalize() this, SLOT(help())); } -// Prevent dangling separator at end of menu per https://bugreports.qt.io/browse/QTBUG-40071 -#if !(defined(LMMS_BUILD_APPLE) && (QT_VERSION < 0x050600)) help_menu->addSeparator(); -#endif help_menu->addAction( embed::getIconPixmap( "icon_small" ), tr( "About" ), this, SLOT(aboutLMMS())); From bda042e1eb59e4c7508faa072051c50c2e12894d Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Sat, 20 Apr 2024 23:21:29 +0100 Subject: [PATCH 141/191] Add native system semaphore and Windows shared memory (#7212) --- .github/workflows/build.yml | 24 ++- include/RemotePluginBase.h | 31 ++- include/SystemSemaphore.h | 61 ++++++ .../VstBase/RemoteVstPlugin/CMakeLists.txt | 13 +- plugins/VstBase/RemoteVstPlugin32.cmake | 18 -- src/common/CMakeLists.txt | 1 + src/common/SharedMemory.cpp | 136 +++++++------ src/common/SystemSemaphore.cpp | 181 ++++++++++++++++++ 8 files changed, 347 insertions(+), 118 deletions(-) create mode 100644 include/SystemSemaphore.h create mode 100644 src/common/SystemSemaphore.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b08c3ba20..546fb017e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -218,7 +218,6 @@ jobs: name: msvc-${{ matrix.arch }} runs-on: windows-2019 env: - qt-version: '5.15.2' CCACHE_MAXSIZE: 0 CCACHE_NOCOMPRESS: 1 steps: @@ -246,22 +245,21 @@ jobs: path: ~\AppData\Local\ccache - name: Install tools run: choco install ccache - - name: Install 64-bit Qt - if: matrix.arch == 'x64' + - name: Install Qt uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946 with: - version: ${{ env.qt-version }} - arch: win64_msvc2019_64 + version: '5.15.2' + arch: |- + ${{ + fromJSON(' + { + "x86": "win32_msvc2019", + "x64": "win64_msvc2019_64" + } + ')[matrix.arch] + }} archives: qtbase qtsvg qttools cache: true - - name: Install 32-bit Qt - uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946 - with: - version: ${{ env.qt-version }} - arch: win32_msvc2019 - archives: qtbase qtsvg qttools - cache: true - set-env: ${{ matrix.arch == 'x86' }} - name: Set up build environment uses: ilammy/msvc-dev-cmd@cec98b9d092141f74527d0afa6feb2af698cfe89 with: diff --git a/include/RemotePluginBase.h b/include/RemotePluginBase.h index 357be1bea..5214b6f92 100644 --- a/include/RemotePluginBase.h +++ b/include/RemotePluginBase.h @@ -41,10 +41,6 @@ #ifdef LMMS_HAVE_PROCESS_H #include #endif - -#include -#include -#include #else // !(LMMS_HAVE_SYS_IPC_H && LMMS_HAVE_SEMAPHORE_H) #ifdef LMMS_HAVE_UNISTD_H #include @@ -75,6 +71,7 @@ #include #include #include +#include #ifndef SYNC_WITH_SHM_FIFO #include @@ -85,6 +82,7 @@ #ifdef SYNC_WITH_SHM_FIFO #include "SharedMemory.h" +#include "SystemSemaphore.h" #endif namespace lmms @@ -120,12 +118,11 @@ class shmFifo } ; public: +#ifndef BUILD_REMOTE_PLUGIN_CLIENT // constructor for master-side shmFifo() : m_invalid( false ), m_master( true ), - m_dataSem( QString() ), - m_messageSem( QString() ), m_lockDepth( 0 ) { m_data.create(QUuid::createUuid().toString().toStdString()); @@ -133,26 +130,21 @@ public: static int k = 0; m_data->dataSem.semKey = ( getpid()<<10 ) + ++k; m_data->messageSem.semKey = ( getpid()<<10 ) + ++k; - m_dataSem.setKey( QString::number( m_data->dataSem.semKey ), - 1, QSystemSemaphore::Create ); - m_messageSem.setKey( QString::number( - m_data->messageSem.semKey ), - 0, QSystemSemaphore::Create ); + m_dataSem = SystemSemaphore{std::to_string(m_data->dataSem.semKey), 1u}; + m_messageSem = SystemSemaphore{std::to_string(m_data->messageSem.semKey), 0u}; } +#endif // constructor for remote-/client-side - use _shm_key for making up // the connection to master shmFifo(const std::string& shmKey) : m_invalid( false ), m_master( false ), - m_dataSem( QString() ), - m_messageSem( QString() ), m_lockDepth( 0 ) { m_data.attach(shmKey); - m_dataSem.setKey( QString::number( m_data->dataSem.semKey ) ); - m_messageSem.setKey( QString::number( - m_data->messageSem.semKey ) ); + m_dataSem = SystemSemaphore{std::to_string(m_data->dataSem.semKey)}; + m_messageSem = SystemSemaphore{std::to_string(m_data->messageSem.semKey)}; } inline bool isInvalid() const @@ -336,11 +328,10 @@ private: volatile bool m_invalid; bool m_master; SharedMemory m_data; - QSystemSemaphore m_dataSem; - QSystemSemaphore m_messageSem; + SystemSemaphore m_dataSem; + SystemSemaphore m_messageSem; std::atomic_int m_lockDepth; - -} ; +}; #endif // SYNC_WITH_SHM_FIFO diff --git a/include/SystemSemaphore.h b/include/SystemSemaphore.h new file mode 100644 index 000000000..931c472bb --- /dev/null +++ b/include/SystemSemaphore.h @@ -0,0 +1,61 @@ +/* + * SystemSemaphore.h + * + * Copyright (c) 2024 Dominic Clark + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +#ifndef LMMS_SYSTEM_SEMAPHORE_H +#define LMMS_SYSTEM_SEMAPHORE_H + +#include +#include + +namespace lmms { + +namespace detail { + +class SystemSemaphoreImpl; + +} // namespace detail + +class SystemSemaphore +{ +public: + SystemSemaphore() noexcept; + SystemSemaphore(std::string key, unsigned int value); + explicit SystemSemaphore(std::string key); + ~SystemSemaphore(); + + SystemSemaphore(SystemSemaphore&& other) noexcept; + auto operator=(SystemSemaphore&& other) noexcept -> SystemSemaphore&; + + auto acquire() noexcept -> bool; + auto release() noexcept -> bool; + + auto key() const noexcept -> const std::string& { return m_key; } + +private: + std::string m_key; + std::unique_ptr m_impl; +}; + +} // namespace lmms + +#endif // LMMS_SYSTEM_SEMAPHORE_H diff --git a/plugins/VstBase/RemoteVstPlugin/CMakeLists.txt b/plugins/VstBase/RemoteVstPlugin/CMakeLists.txt index 3356ae596..c75b95ab7 100644 --- a/plugins/VstBase/RemoteVstPlugin/CMakeLists.txt +++ b/plugins/VstBase/RemoteVstPlugin/CMakeLists.txt @@ -22,6 +22,9 @@ FOREACH( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) SET("CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG}" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") ENDFOREACH() +# Import of windows.h breaks min()/max() +add_definitions(-DNOMINMAX) + ADD_SUBDIRECTORY("${LMMS_SOURCE_DIR}/src/common" common) if(NOT IS_WIN) @@ -64,12 +67,6 @@ if(MSVC) ) endif() - -if(WIN32) - find_package(Qt5Core REQUIRED) - target_link_libraries(${EXE_NAME} Qt5::Core) -endif() - if(IS_MINGW) SET(CMAKE_REQUIRED_FLAGS "-std=c++17") @@ -96,7 +93,9 @@ if(LMMS_BUILD_WIN32) set(NOOP_COMMAND "${CMAKE_COMMAND}" "-E" "echo") endif() if(STRIP) - set(STRIP_COMMAND "$,${NOOP_COMMAND},${STRIP}>") + # TODO CMake 3.19: Now that CONFIG generator expressions support testing for + # multiple configurations, combine the OR into a single CONFIG expression. + set(STRIP_COMMAND "$,$>,${NOOP_COMMAND},${STRIP}>") else() set(STRIP_COMMAND "${NOOP_COMMAND}") endif() diff --git a/plugins/VstBase/RemoteVstPlugin32.cmake b/plugins/VstBase/RemoteVstPlugin32.cmake index 0f98d34e0..5afb7db81 100644 --- a/plugins/VstBase/RemoteVstPlugin32.cmake +++ b/plugins/VstBase/RemoteVstPlugin32.cmake @@ -17,23 +17,6 @@ ENDMACRO() IF(LMMS_BUILD_WIN32 AND NOT LMMS_BUILD_WIN64) ADD_SUBDIRECTORY(RemoteVstPlugin) ELSEIF(LMMS_BUILD_WIN64 AND MSVC) - IF(NOT QT_32_PREFIX) - SET(LMMS_MSVC_YEAR_FOR_QT ${LMMS_MSVC_YEAR}) - - if(LMMS_MSVC_YEAR_FOR_QT EQUAL 2019 AND Qt5_VERSION VERSION_LESS "5.15") - SET(LMMS_MSVC_YEAR_FOR_QT 2017) # Qt only provides binaries for MSVC 2017, but 2019 is binary compatible - endif() - - GET_FILENAME_COMPONENT(QT_BIN_DIR ${QT_QMAKE_EXECUTABLE} DIRECTORY) - SET(QT_32_PREFIX "${QT_BIN_DIR}/../../msvc${LMMS_MSVC_YEAR_FOR_QT}") - ENDIF() - - #TODO: qt5 installed using vcpkg: I don't know how to detect if the user built the x86 version of qt5 from here. At least not cleanly. - #So for the moment, we'll allow the built. - IF(NOT (IS_DIRECTORY ${QT_32_PREFIX} AND EXISTS ${QT_32_PREFIX}/bin/qmake.exe)) - MESSAGE(WARNING "No Qt 32 bit installation found at ${QT_32_PREFIX}. If you're using VCPKG you can ignore this message if you've built x86-windows version of qt5") - ENDIF() - ExternalProject_Add(RemoteVstPlugin32 "${EXTERNALPROJECT_ARGS}" CMAKE_GENERATOR "${LMMS_MSVC_GENERATOR}" @@ -42,7 +25,6 @@ ELSEIF(LMMS_BUILD_WIN64 AND MSVC) CMAKE_ARGS "${EXTERNALPROJECT_CMAKE_ARGS}" "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" - "-DCMAKE_PREFIX_PATH=${QT_32_PREFIX}" ) INSTALL_EXTERNAL_PROJECT(RemoteVstPlugin32) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 877bea3fc..9f349f091 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -1,6 +1,7 @@ set(COMMON_SRCS RemotePluginBase.cpp SharedMemory.cpp + SystemSemaphore.cpp ) foreach(SRC ${COMMON_SRCS}) diff --git a/src/common/SharedMemory.cpp b/src/common/SharedMemory.cpp index 005e726ed..470016530 100644 --- a/src/common/SharedMemory.cpp +++ b/src/common/SharedMemory.cpp @@ -23,45 +23,44 @@ #include "SharedMemory.h" +#include +#include + #include "lmmsconfig.h" +#include "RaiiHelpers.h" #ifdef LMMS_HAVE_UNISTD_H # include #endif -#if _POSIX_SHARED_MEMORY_OBJECTS > 0 -# include -# +#if _POSIX_SHARED_MEMORY_OBJECTS > 0 || defined(LMMS_BUILD_APPLE) # include # include # include -# -# include "RaiiHelpers.h" +#elif defined(LMMS_BUILD_WIN32) +# include #else -# include -# -# include -# include +# error "No shared memory implementation available" #endif - namespace lmms::detail { -#if _POSIX_SHARED_MEMORY_OBJECTS > 0 - +#if _POSIX_SHARED_MEMORY_OBJECTS > 0 || defined(LMMS_BUILD_APPLE) namespace { +[[noreturn]] void throwSystemError(const char* message) +{ + throw std::system_error{errno, std::generic_category(), message}; +} + template int retryWhileInterrupted(F&& function) noexcept(std::is_nothrow_invocable_v) { - int result; - do - { - result = function(); + while (true) { + const auto result = function(); + if (result != -1 || errno != EINTR) { return result; } } - while (result == -1 && errno == EINTR); - return result; } void deleteFileDescriptor(int fd) noexcept { retryWhileInterrupted([fd]() noexcept { return close(fd); }); } @@ -82,22 +81,15 @@ public: const auto fd = FileDescriptor{ retryWhileInterrupted([&]() noexcept { return shm_open(m_key.c_str(), openFlags, 0); }) }; - if (!fd) - { - throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: shm_open() failed"}; - } + if (!fd) { throwSystemError("SharedMemoryImpl: shm_open() failed"); } + auto stat = (struct stat){}; - if (fstat(fd.get(), &stat) == -1) - { - throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: fstat() failed"}; - } + if (fstat(fd.get(), &stat) == -1) { throwSystemError("SharedMemoryImpl: fstat() failed"); } m_size = stat.st_size; + const auto mappingProtection = readOnly ? PROT_READ : PROT_READ | PROT_WRITE; m_mapping = mmap(nullptr, m_size, mappingProtection, MAP_SHARED, fd.get(), 0); - if (m_mapping == MAP_FAILED) - { - throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: mmap() failed"}; - } + if (m_mapping == MAP_FAILED) { throwSystemError("SharedMemoryImpl: mmap() failed"); } } SharedMemoryImpl(const std::string& key, std::size_t size, bool readOnly) : @@ -107,21 +99,16 @@ public: const auto fd = FileDescriptor{ retryWhileInterrupted([&]() noexcept { return shm_open(m_key.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); }) }; - if (fd.get() == -1) - { - throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: shm_open() failed"}; - } + if (fd.get() == -1) { throwSystemError("SharedMemoryImpl: shm_open() failed"); } m_object.reset(m_key.c_str()); - if (retryWhileInterrupted([&]() noexcept { return ftruncate(fd.get(), m_size); }) == -1) - { - throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: ftruncate() failed"}; + + if (retryWhileInterrupted([&]() noexcept { return ftruncate(fd.get(), m_size); }) == -1) { + throwSystemError("SharedMemoryImpl: ftruncate() failed"); } + const auto mappingProtection = readOnly ? PROT_READ : PROT_READ | PROT_WRITE; m_mapping = mmap(nullptr, m_size, mappingProtection, MAP_SHARED, fd.get(), 0); - if (m_mapping == MAP_FAILED) - { - throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: mmap() failed"}; - } + if (m_mapping == MAP_FAILED) { throwSystemError("SharedMemoryImpl: mmap() failed"); } } SharedMemoryImpl(const SharedMemoryImpl&) = delete; @@ -132,7 +119,7 @@ public: munmap(m_mapping, m_size); } - void* get() { return m_mapping; } + auto get() const noexcept -> void* { return m_mapping; } private: std::string m_key; @@ -141,38 +128,67 @@ private: ShmObject m_object; }; -#else +#elif defined(LMMS_BUILD_WIN32) + +namespace { + +auto sizeToHighAndLow(std::size_t size) -> std::pair +{ + if constexpr(sizeof(std::size_t) <= sizeof(DWORD)) { + return {0, size}; + } else { + return {static_cast(size >> 32), static_cast(size)}; + } +} + +[[noreturn]] void throwLastError(const char* message) +{ + throw std::system_error{static_cast(GetLastError()), std::system_category(), message}; +} + +using UniqueHandle = UniqueNullableResource; +using FileView = UniqueNullableResource; + +} // namespace class SharedMemoryImpl { public: - SharedMemoryImpl(const std::string& key, bool readOnly) : - m_shm{QString::fromStdString(key)} + SharedMemoryImpl(const std::string& key, bool readOnly) { - const auto mode = readOnly ? QSharedMemory::ReadOnly : QSharedMemory::ReadWrite; - if (!m_shm.attach(mode)) - { - throw std::runtime_error{"SharedMemoryImpl: QSharedMemory::attach() failed"}; - } + const auto access = readOnly ? FILE_MAP_READ : FILE_MAP_WRITE; + m_mapping.reset(OpenFileMappingA(access, false, key.c_str())); + if (!m_mapping) { throwLastError("SharedMemoryImpl: OpenFileMappingA() failed"); } + + m_view.reset(MapViewOfFile(m_mapping.get(), access, 0, 0, 0)); + if (!m_view) { throwLastError("SharedMemoryImpl: MapViewOfFile() failed"); } } - SharedMemoryImpl(const std::string& key, std::size_t size, bool readOnly) : - m_shm{QString::fromStdString(key)} + SharedMemoryImpl(const std::string& key, std::size_t size, bool readOnly) { - const auto mode = readOnly ? QSharedMemory::ReadOnly : QSharedMemory::ReadWrite; - if (!m_shm.create(size, mode)) - { - throw std::runtime_error{"SharedMemoryImpl: QSharedMemory::create() failed"}; + const auto [high, low] = sizeToHighAndLow(size); + m_mapping.reset(CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, high, low, key.c_str())); + // This constructor is supposed to create a new shared memory object, + // but passing the name of an existing object causes CreateFileMappingA + // to succeed and return a handle to that object. Thus we have to check + // GetLastError() too. + if (!m_mapping || GetLastError() == ERROR_ALREADY_EXISTS) { + throwLastError("SharedMemoryImpl: CreateFileMappingA() failed"); } + + const auto access = readOnly ? FILE_MAP_READ : FILE_MAP_WRITE; + m_view.reset(MapViewOfFile(m_mapping.get(), access, 0, 0, 0)); + if (!m_view) { throwLastError("SharedMemoryImpl: MapViewOfFile() failed"); } } SharedMemoryImpl(const SharedMemoryImpl&) = delete; - SharedMemoryImpl& operator=(const SharedMemoryImpl&) = delete; + auto operator=(const SharedMemoryImpl&) -> SharedMemoryImpl& = delete; - void* get() { return m_shm.data(); } + auto get() const noexcept -> void* { return m_view.get(); } private: - QSharedMemory m_shm; + UniqueHandle m_mapping; + FileView m_view; }; #endif @@ -196,7 +212,7 @@ SharedMemoryData::~SharedMemoryData() = default; SharedMemoryData::SharedMemoryData(SharedMemoryData&& other) noexcept : m_key{std::move(other.m_key)}, m_impl{std::move(other.m_impl)}, - m_ptr{other.m_ptr} + m_ptr{std::exchange(other.m_ptr, nullptr)} { } } // namespace lmms::detail diff --git a/src/common/SystemSemaphore.cpp b/src/common/SystemSemaphore.cpp new file mode 100644 index 000000000..02c9a2888 --- /dev/null +++ b/src/common/SystemSemaphore.cpp @@ -0,0 +1,181 @@ +/* + * SystemSemaphore.cpp + * + * Copyright (c) 2024 Dominic Clark + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +#include "SystemSemaphore.h" + +#include +#include +#include +#include + +#include "lmmsconfig.h" +#include "RaiiHelpers.h" + +#ifdef LMMS_HAVE_UNISTD_H +# include +#endif + +#if (_POSIX_SEMAPHORES > 0 && !defined(__MINGW32__)) || defined(LMMS_BUILD_APPLE) +# include +# include +#elif defined(LMMS_BUILD_WIN32) +# include +#else +# error "No system semaphore implementation available" +#endif + +namespace lmms { + +namespace detail { + +#if (_POSIX_SEMAPHORES > 0 && !defined(__MINGW32__)) || defined(LMMS_BUILD_APPLE) + +namespace { + +[[noreturn]] void throwSystemError(const char* message) +{ + throw std::system_error{errno, std::generic_category(), message}; +} + +template +auto retryWhileInterrupted(F&& function, std::invoke_result_t error = -1) + noexcept(std::is_nothrow_invocable_v) -> auto +{ + while (true) + { + const auto result = function(); + if (result != error || errno != EINTR) { return result; } + } +} + +using UniqueSemaphore = UniqueNullableResource; + +} // namespace + +class SystemSemaphoreImpl +{ +public: + SystemSemaphoreImpl(const std::string& key, unsigned int value) : + m_key{"/" + key}, + m_sem{retryWhileInterrupted([&]() noexcept { + return sem_open(m_key.c_str(), O_CREAT | O_EXCL, 0600, value); + }, SEM_FAILED)} + { + if (m_sem == SEM_FAILED) { throwSystemError("SystemSemaphoreImpl: sem_open() failed"); } + m_ownedSemaphore.reset(m_key.c_str()); + } + + explicit SystemSemaphoreImpl(const std::string& key) : + m_key{"/" + key}, + m_sem{retryWhileInterrupted([&]() noexcept { + return sem_open(m_key.c_str(), 0); + }, SEM_FAILED)} + { + if (m_sem == SEM_FAILED) { throwSystemError("SystemSemaphoreImpl: sem_open() failed"); } + } + + ~SystemSemaphoreImpl() + { + // We can't use `UniqueNullableResource` for `m_sem`, as the null value + // (`SEM_FAILED`) is not a constant expression on macOS (it's defined as + // `(sem_t*) -1`), so can't be used as a template parameter. + sem_close(m_sem); + } + + auto acquire() noexcept -> bool + { + return retryWhileInterrupted([&]() noexcept { + return sem_wait(m_sem); + }) == 0; + } + + auto release() noexcept -> bool { return sem_post(m_sem) == 0; } + +private: + std::string m_key; + sem_t* m_sem; + UniqueSemaphore m_ownedSemaphore; +}; + +#elif defined(LMMS_BUILD_WIN32) + +namespace { + +[[noreturn]] void throwSystemError(const char* message) +{ + throw std::system_error{static_cast(GetLastError()), std::system_category(), message}; +} + +using UniqueHandle = UniqueNullableResource; + +} // namespace + +class SystemSemaphoreImpl +{ +public: + SystemSemaphoreImpl(const std::string& key, unsigned int value) : + m_sem{CreateSemaphoreA(nullptr, value, std::numeric_limits::max(), key.c_str())} + { + if (!m_sem || GetLastError() == ERROR_ALREADY_EXISTS) { + throwSystemError("SystemSemaphoreImpl: CreateSemaphoreA failed"); + } + } + + explicit SystemSemaphoreImpl(const std::string& key) : + m_sem{OpenSemaphoreA(SEMAPHORE_MODIFY_STATE | SYNCHRONIZE, false, key.c_str())} + { + if (!m_sem) { throwSystemError("SystemSemaphoreImpl: OpenSemaphoreA failed"); } + } + + auto acquire() noexcept -> bool { return WaitForSingleObject(m_sem.get(), INFINITE) == WAIT_OBJECT_0; } + auto release() noexcept -> bool { return ReleaseSemaphore(m_sem.get(), 1, nullptr); } + +private: + UniqueHandle m_sem; +}; + +#endif + +} // namespace detail + +SystemSemaphore::SystemSemaphore() noexcept = default; + +SystemSemaphore::SystemSemaphore(std::string key, unsigned int value) : + m_key{std::move(key)}, + m_impl{std::make_unique(m_key, value)} +{} + +SystemSemaphore::SystemSemaphore(std::string key) : + m_key{std::move(key)}, + m_impl{std::make_unique(m_key)} +{} + +SystemSemaphore::~SystemSemaphore() = default; + +SystemSemaphore::SystemSemaphore(SystemSemaphore&& other) noexcept = default; +auto SystemSemaphore::operator=(SystemSemaphore&& other) noexcept -> SystemSemaphore& = default; + +auto SystemSemaphore::acquire() noexcept -> bool { return m_impl->acquire(); } +auto SystemSemaphore::release() noexcept -> bool { return m_impl->release(); } + +} // namespace lmms From 62e2a39a7ef5177d35aee3b86852e8afda06ef08 Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Sat, 20 Apr 2024 21:36:48 -0500 Subject: [PATCH 142/191] SlicerT::findSlices - check if lower_bound did not find anything --- plugins/SlicerT/SlicerT.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 732ebfb44..9bf667985 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -234,8 +234,9 @@ void SlicerT::findSlices() for (float& sliceValue : m_slicePoints) { - int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); - if (std::abs(sliceValue - closestZeroCrossing) < windowSize) { sliceValue = closestZeroCrossing; } + auto closestZeroCrossing = std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); + if (closestZeroCrossing == zeroCrossings.end()) { continue; } + if (std::abs(sliceValue - *closestZeroCrossing) < windowSize) { sliceValue = *closestZeroCrossing; } } float beatsPerMin = m_originalBPM.value() / 60.0f; From 71dd300f43d6c1e69cbf1362174a8747427ff248 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 24 Apr 2024 20:23:36 +0200 Subject: [PATCH 143/191] Instrument release time in milliseconds (#7217) Make instruments report their release time in milliseconds so that it becomes independent of the sample rate and sounds the same at any sample rate. Technically this is done by removing the virtual keyword from `desiredReleaseFrames` so that it cannot be overridden anymore. The method now only serves to compute the number of frames from the given release time in milliseconds. A new virtual method `desiredReleaseTimeMs` is added which instruments can override. The default returns 0 ms just like the default implementation previously returned 0 frames. The method `computeReleaseTimeMsByFrameCount` is added for instruments that still use a hard coded release in frames. As of now this is only `SidInstrument`. Add the helper method `getSampleRate` to `Instrument`. Adjust several instruments to report their release times in milliseconds. The times are computed by taking the release in frames and assuming a sample rate of 44.1 kHz. In most cases the times are rounded to a "nice" next value, e.g.: * 64 frames -> 1.5 ms (66 frames) * 128 frames -> 3.0 ms (132 frames) * 512 frames -> 12. ms (529 frames) * 1000 frames -> 23 ms (1014 samples) In parentheses the number of frames are shown which result from the rounded number of milliseconds when converted back assuming a sample rate of 44.1 kHz. The difference should not be noticeable in existing projects. Remove the overrides for instruments that return the same value as the base class `Instrument` anyway. These are: * GigPlayer * Lb302 * Sf2Player For `MonstroInstrument` the implementation is adjusted to behave in a very similar way. First the maximum of the envelope release times is computed. These are already available in milliseconds. Then the maximum of that value and 1.5 ms is taken and returned as the result. --- include/Instrument.h | 28 +++++++++++++++---- .../AudioFileProcessor/AudioFileProcessor.h | 4 +-- plugins/BitInvader/BitInvader.h | 4 +-- plugins/FreeBoy/FreeBoy.cpp | 18 ++---------- plugins/FreeBoy/FreeBoy.h | 2 +- plugins/GigPlayer/GigPlayer.h | 5 ---- plugins/Kicker/Kicker.h | 4 +-- plugins/Lb302/Lb302.h | 5 ---- plugins/Monstro/Monstro.cpp | 7 ++--- plugins/Monstro/Monstro.h | 2 +- plugins/Nes/Nes.h | 4 +-- plugins/Patman/Patman.h | 4 +-- plugins/Sf2Player/Sf2Player.h | 5 ---- plugins/Sid/SidInstrument.cpp | 12 ++------ plugins/Sid/SidInstrument.h | 2 +- plugins/TripleOscillator/TripleOscillator.h | 4 +-- plugins/Watsyn/Watsyn.h | 4 +-- src/core/Instrument.cpp | 9 ++++++ 18 files changed, 57 insertions(+), 66 deletions(-) diff --git a/include/Instrument.h b/include/Instrument.h index 243bdba61..0c28e7f3a 100644 --- a/include/Instrument.h +++ b/include/Instrument.h @@ -34,6 +34,9 @@ #include "Plugin.h" #include "TimePos.h" +#include + + namespace lmms { @@ -91,15 +94,26 @@ public: virtual f_cnt_t beatLen( NotePlayHandle * _n ) const; - // some instruments need a certain number of release-frames even - // if no envelope is active - such instruments can re-implement this - // method for returning how many frames they at least like to have for - // release - virtual f_cnt_t desiredReleaseFrames() const + // This method can be overridden by instruments that need a certain + // release time even if no envelope is active. It returns the time + // in milliseconds that these instruments would like to have for + // their release stage. + virtual float desiredReleaseTimeMs() const { - return 0; + return 0.f; } + // Converts the desired release time in milliseconds to the corresponding + // number of frames depending on the sample rate. + f_cnt_t desiredReleaseFrames() const + { + const sample_rate_t sampleRate = getSampleRate(); + + return static_cast(std::ceil(desiredReleaseTimeMs() * sampleRate / 1000.f)); + } + + sample_rate_t getSampleRate() const; + virtual Flags flags() const { return Flag::NoFlags; @@ -142,6 +156,8 @@ protected: // desiredReleaseFrames() frames are left void applyRelease( sampleFrame * buf, const NotePlayHandle * _n ); + float computeReleaseTimeMsByFrameCount(f_cnt_t frames) const; + private: InstrumentTrack * m_instrumentTrack; diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 7ade1ec4f..00ad92129 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -56,9 +56,9 @@ public: auto beatLen(NotePlayHandle* note) const -> int override; - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return 128; + return 3.f; } gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/BitInvader/BitInvader.h b/plugins/BitInvader/BitInvader.h index 6dce9db83..f4d248ec8 100644 --- a/plugins/BitInvader/BitInvader.h +++ b/plugins/BitInvader/BitInvader.h @@ -85,9 +85,9 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return( 64 ); + return 1.5f; } gui::PluginView * instantiateView( QWidget * _parent ) override; diff --git a/plugins/FreeBoy/FreeBoy.cpp b/plugins/FreeBoy/FreeBoy.cpp index f2dc95699..5a5581bc0 100644 --- a/plugins/FreeBoy/FreeBoy.cpp +++ b/plugins/FreeBoy/FreeBoy.cpp @@ -220,22 +220,10 @@ QString FreeBoyInstrument::nodeName() const -/*f_cnt_t FreeBoyInstrument::desiredReleaseFrames() const +float FreeBoyInstrument::desiredReleaseTimeMs() const { - const float samplerate = Engine::audioEngine()->processingSampleRate(); - int maxrel = 0; - for( int i = 0 ; i < 3 ; ++i ) - { - if( maxrel < m_voice[i]->m_releaseModel.value() ) - maxrel = m_voice[i]->m_releaseModel.value(); - } - - return f_cnt_t( float(relTime[maxrel])*samplerate/1000.0 ); -}*/ - -f_cnt_t FreeBoyInstrument::desiredReleaseFrames() const -{ - return f_cnt_t( 1000 ); + // Previous implementation was 1000 samples. At 44.1 kHz this is somewhat shy of 23. ms. + return 23.f; } diff --git a/plugins/FreeBoy/FreeBoy.h b/plugins/FreeBoy/FreeBoy.h index 747305414..501377715 100644 --- a/plugins/FreeBoy/FreeBoy.h +++ b/plugins/FreeBoy/FreeBoy.h @@ -62,7 +62,7 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override; + float desiredReleaseTimeMs() const override; gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 986018654..85b1736a0 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -259,11 +259,6 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override - { - return 0; - } - Flags flags() const override { return Flag::IsSingleStreamed | Flag::IsNotBendable; diff --git a/plugins/Kicker/Kicker.h b/plugins/Kicker/Kicker.h index b5d065598..820265dd5 100644 --- a/plugins/Kicker/Kicker.h +++ b/plugins/Kicker/Kicker.h @@ -69,9 +69,9 @@ public: return Flag::IsNotBendable; } - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return( 512 ); + return 12.f; } gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/Lb302/Lb302.h b/plugins/Lb302/Lb302.h index 237a3f3f8..2f4f64dcb 100644 --- a/plugins/Lb302/Lb302.h +++ b/plugins/Lb302/Lb302.h @@ -168,11 +168,6 @@ public: return Flag::IsSingleStreamed; } - f_cnt_t desiredReleaseFrames() const override - { - return 0; //4048; - } - gui::PluginView* instantiateView( QWidget * _parent ) override; private: diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index 3de9fbce6..65da50ea6 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -1326,13 +1326,12 @@ QString MonstroInstrument::nodeName() const return monstro_plugin_descriptor.name; } - -f_cnt_t MonstroInstrument::desiredReleaseFrames() const +float MonstroInstrument::desiredReleaseTimeMs() const { - return qMax( 64, qMax( m_env1_relF, m_env2_relF ) ); + const auto maxEnvelope = std::max(m_env1_rel, m_env2_rel); + return std::max(1.5f, maxEnvelope); } - gui::PluginView* MonstroInstrument::instantiateView( QWidget * _parent ) { return( new gui::MonstroView( this, _parent ) ); diff --git a/plugins/Monstro/Monstro.h b/plugins/Monstro/Monstro.h index 919409b2d..0df18d5c4 100644 --- a/plugins/Monstro/Monstro.h +++ b/plugins/Monstro/Monstro.h @@ -366,7 +366,7 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override; + float desiredReleaseTimeMs() const override; gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/Nes/Nes.h b/plugins/Nes/Nes.h index a05b3a2f8..39e0a6719 100644 --- a/plugins/Nes/Nes.h +++ b/plugins/Nes/Nes.h @@ -222,9 +222,9 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return( 8 ); + return 0.2f; } gui::PluginView* instantiateView( QWidget * parent ) override; diff --git a/plugins/Patman/Patman.h b/plugins/Patman/Patman.h index 486524522..16b98deee 100644 --- a/plugins/Patman/Patman.h +++ b/plugins/Patman/Patman.h @@ -71,9 +71,9 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return( 128 ); + return 3.f; } gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/Sf2Player/Sf2Player.h b/plugins/Sf2Player/Sf2Player.h index 1af370e05..4760d572e 100644 --- a/plugins/Sf2Player/Sf2Player.h +++ b/plugins/Sf2Player/Sf2Player.h @@ -80,11 +80,6 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override - { - return 0; - } - Flags flags() const override { return Flag::IsSingleStreamed; diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index d85939eb8..b646836b5 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -221,24 +221,18 @@ QString SidInstrument::nodeName() const } - - -f_cnt_t SidInstrument::desiredReleaseFrames() const +float SidInstrument::desiredReleaseTimeMs() const { - const float samplerate = Engine::audioEngine()->processingSampleRate(); int maxrel = 0; for (const auto& voice : m_voice) { - if( maxrel < voice->m_releaseModel.value() ) - maxrel = (int)voice->m_releaseModel.value(); + maxrel = std::max(maxrel, static_cast(voice->m_releaseModel.value())); } - return f_cnt_t( float(relTime[maxrel])*samplerate/1000.0 ); + return computeReleaseTimeMsByFrameCount(relTime[maxrel]); } - - static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, short *ptr, int samples) { int total = 0; diff --git a/plugins/Sid/SidInstrument.h b/plugins/Sid/SidInstrument.h index 1a133b58b..8d5af8df0 100644 --- a/plugins/Sid/SidInstrument.h +++ b/plugins/Sid/SidInstrument.h @@ -111,7 +111,7 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override; + float desiredReleaseTimeMs() const override; gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/TripleOscillator/TripleOscillator.h b/plugins/TripleOscillator/TripleOscillator.h index 4b6d97835..011352de4 100644 --- a/plugins/TripleOscillator/TripleOscillator.h +++ b/plugins/TripleOscillator/TripleOscillator.h @@ -121,9 +121,9 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return( 128 ); + return 3.f; } gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/plugins/Watsyn/Watsyn.h b/plugins/Watsyn/Watsyn.h index d238edbde..b34e28f60 100644 --- a/plugins/Watsyn/Watsyn.h +++ b/plugins/Watsyn/Watsyn.h @@ -150,9 +150,9 @@ public: QString nodeName() const override; - f_cnt_t desiredReleaseFrames() const override + float desiredReleaseTimeMs() const override { - return( 64 ); + return 1.5f; } gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index a7cfc467b..2dfdc78f5 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -195,8 +195,17 @@ void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n ) } } +float Instrument::computeReleaseTimeMsByFrameCount(f_cnt_t frames) const +{ + return frames / getSampleRate() * 1000.; +} +sample_rate_t Instrument::getSampleRate() const +{ + return Engine::audioEngine()->processingSampleRate(); +} + QString Instrument::fullDisplayName() const { From a53e5ba2f671ca5c1cc133dc8f763b8150e09c9b Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 27 Apr 2024 07:59:02 -0400 Subject: [PATCH 144/191] Remove high quality mode from codebase (#7219) Many, many years ago (93a456c), high quality mode was merely disabled after it was noted that it can be problematic rather than being completely removed. Remove it from the codebase so we can get rid of some code and clean things up a bit. --- include/AudioAlsa.h | 1 - include/AudioDevice.h | 6 +---- include/AudioJack.h | 1 - include/AudioOss.h | 1 - include/AudioPortAudio.h | 1 - include/AudioPulseAudio.h | 1 - include/AudioSdl.h | 1 - include/AudioSndio.h | 1 - include/SetupDialog.h | 2 -- include/SongEditor.h | 2 -- src/core/audio/AudioAlsa.cpp | 42 ------------------------------ src/core/audio/AudioDevice.cpp | 9 ------- src/core/audio/AudioJack.cpp | 18 ------------- src/core/audio/AudioOss.cpp | 35 ------------------------- src/core/audio/AudioPortAudio.cpp | 32 ----------------------- src/core/audio/AudioPulseAudio.cpp | 16 ------------ src/core/audio/AudioSdl.cpp | 31 ---------------------- src/core/audio/AudioSndio.cpp | 14 ---------- src/gui/editors/SongEditor.cpp | 24 ----------------- src/gui/modals/SetupDialog.cpp | 20 -------------- 20 files changed, 1 insertion(+), 257 deletions(-) diff --git a/include/AudioAlsa.h b/include/AudioAlsa.h index 975532071..92c47a6ac 100644 --- a/include/AudioAlsa.h +++ b/include/AudioAlsa.h @@ -84,7 +84,6 @@ public: private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; void run() override; int setHWParams( const ch_cnt_t _channels, snd_pcm_access_t _access ); diff --git a/include/AudioDevice.h b/include/AudioDevice.h index c6ee46efc..def233a11 100644 --- a/include/AudioDevice.h +++ b/include/AudioDevice.h @@ -89,9 +89,7 @@ public: virtual void stopProcessing(); - virtual void applyQualitySettings(); - - + void applyQualitySettings(); protected: // subclasses can re-implement this for being used in conjunction with @@ -129,8 +127,6 @@ protected: return m_audioEngine; } - bool hqAudio() const; - static void stopProcessingThread( QThread * thread ); diff --git a/include/AudioJack.h b/include/AudioJack.h index 6efb262ed..01f41f092 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -93,7 +93,6 @@ private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; void registerPort(AudioPort* port) override; void unregisterPort(AudioPort* port) override; diff --git a/include/AudioOss.h b/include/AudioOss.h index 55f64de85..91d456073 100644 --- a/include/AudioOss.h +++ b/include/AudioOss.h @@ -79,7 +79,6 @@ class setupWidget : public gui::AudioDeviceSetupWidget private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; void run() override; int m_audioFD; diff --git a/include/AudioPortAudio.h b/include/AudioPortAudio.h index 01b8f3fd7..4465b18c1 100644 --- a/include/AudioPortAudio.h +++ b/include/AudioPortAudio.h @@ -109,7 +109,6 @@ public: private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; #ifdef PORTAUDIO_V19 static int _process_callback( const void *_inputBuffer, void * _outputBuffer, diff --git a/include/AudioPulseAudio.h b/include/AudioPulseAudio.h index b6a998274..db3c566bf 100644 --- a/include/AudioPulseAudio.h +++ b/include/AudioPulseAudio.h @@ -88,7 +88,6 @@ public: private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; void run() override; volatile bool m_quit; diff --git a/include/AudioSdl.h b/include/AudioSdl.h index 62db8b68a..5062f79ea 100644 --- a/include/AudioSdl.h +++ b/include/AudioSdl.h @@ -74,7 +74,6 @@ public: private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; static void sdlAudioCallback( void * _udata, Uint8 * _buf, int _len ); void sdlAudioCallback( Uint8 * _buf, int _len ); diff --git a/include/AudioSndio.h b/include/AudioSndio.h index 594ca94e7..beb4913eb 100644 --- a/include/AudioSndio.h +++ b/include/AudioSndio.h @@ -75,7 +75,6 @@ public: private: void startProcessing() override; void stopProcessing() override; - void applyQualitySettings() override; void run() override; struct sio_hdl *m_hdl; diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 7a1304d9a..871a80bcd 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -102,7 +102,6 @@ private slots: // Audio settings widget. void audioInterfaceChanged(const QString & driver); - void toggleHQAudioDev(bool enabled); void updateBufferSizeWarning(int value); void setBufferSize(int value); void resetBufferSize(); @@ -180,7 +179,6 @@ private: AswMap m_audioIfaceSetupWidgets; trMap m_audioIfaceNames; bool m_NaNHandler; - bool m_hqAudioDev; int m_bufferSize; QSlider * m_bufferSizeSlider; QLabel * m_bufferSizeLbl; diff --git a/include/SongEditor.h b/include/SongEditor.h index ee9e83f44..19d652b4f 100644 --- a/include/SongEditor.h +++ b/include/SongEditor.h @@ -97,8 +97,6 @@ protected: void mouseReleaseEvent(QMouseEvent * me) override; private slots: - void setHighQuality( bool ); - void setMasterVolume( int new_val ); void showMasterVolumeFloat(); void updateMasterVolumeFloat( int new_val ); diff --git a/src/core/audio/AudioAlsa.cpp b/src/core/audio/AudioAlsa.cpp index d80bc8912..eda0f7a31 100644 --- a/src/core/audio/AudioAlsa.cpp +++ b/src/core/audio/AudioAlsa.cpp @@ -240,48 +240,6 @@ void AudioAlsa::stopProcessing() stopProcessingThread( this ); } - - - -void AudioAlsa::applyQualitySettings() -{ - if( hqAudio() ) - { - setSampleRate( Engine::audioEngine()->processingSampleRate() ); - - if( m_handle != nullptr ) - { - snd_pcm_close( m_handle ); - } - - if (int err = snd_pcm_open(&m_handle, probeDevice().toLatin1().constData(), SND_PCM_STREAM_PLAYBACK, 0); - err < 0) - { - printf( "Playback open error: %s\n", - snd_strerror( err ) ); - return; - } - - if (int err = setHWParams(channels(), SND_PCM_ACCESS_RW_INTERLEAVED); err < 0) - { - printf( "Setting of hwparams failed: %s\n", - snd_strerror( err ) ); - return; - } - if (int err = setSWParams(); err < 0) - { - printf( "Setting of swparams failed: %s\n", - snd_strerror( err ) ); - return; - } - } - - AudioDevice::applyQualitySettings(); -} - - - - void AudioAlsa::run() { auto temp = new surroundSampleFrame[audioEngine()->framesPerPeriod()]; diff --git a/src/core/audio/AudioDevice.cpp b/src/core/audio/AudioDevice.cpp index 743716622..80f6439b8 100644 --- a/src/core/audio/AudioDevice.cpp +++ b/src/core/audio/AudioDevice.cpp @@ -250,13 +250,4 @@ void AudioDevice::clearS16Buffer( int_sample_t * _outbuf, const fpp_t _frames ) memset( _outbuf, 0, _frames * channels() * BYTES_PER_INT_SAMPLE ); } - - - -bool AudioDevice::hqAudio() const -{ - return ConfigManager::inst()->value( "audioengine", "hqaudio" ).toInt(); -} - - } // namespace lmms \ No newline at end of file diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index 64819cbeb..4d27602ef 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -229,24 +229,6 @@ void AudioJack::stopProcessing() m_stopped = true; } - - - -void AudioJack::applyQualitySettings() -{ - if (hqAudio()) - { - setSampleRate(Engine::audioEngine()->processingSampleRate()); - - if (jack_get_sample_rate(m_client) != sampleRate()) { setSampleRate(jack_get_sample_rate(m_client)); } - } - - AudioDevice::applyQualitySettings(); -} - - - - void AudioJack::registerPort(AudioPort* port) { #ifdef AUDIO_PORT_SUPPORT diff --git a/src/core/audio/AudioOss.cpp b/src/core/audio/AudioOss.cpp index 1653e93fa..e18260d61 100644 --- a/src/core/audio/AudioOss.cpp +++ b/src/core/audio/AudioOss.cpp @@ -254,41 +254,6 @@ void AudioOss::stopProcessing() stopProcessingThread( this ); } - - - -void AudioOss::applyQualitySettings() -{ - if( hqAudio() ) - { - setSampleRate( Engine::audioEngine()->processingSampleRate() ); - - unsigned int value = sampleRate(); - if ( ioctl( m_audioFD, SNDCTL_DSP_SPEED, &value ) < 0 ) - { - perror( "SNDCTL_DSP_SPEED" ); - printf( "Couldn't set audio frequency\n" ); - return; - } - if( value != sampleRate() ) - { - value = audioEngine()->baseSampleRate(); - if ( ioctl( m_audioFD, SNDCTL_DSP_SPEED, &value ) < 0 ) - { - perror( "SNDCTL_DSP_SPEED" ); - printf( "Couldn't set audio frequency\n" ); - return; - } - setSampleRate( value ); - } - } - - AudioDevice::applyQualitySettings(); -} - - - - void AudioOss::run() { auto temp = new surroundSampleFrame[audioEngine()->framesPerPeriod()]; diff --git a/src/core/audio/AudioPortAudio.cpp b/src/core/audio/AudioPortAudio.cpp index 1cb8c8eed..bc5d7dd9d 100644 --- a/src/core/audio/AudioPortAudio.cpp +++ b/src/core/audio/AudioPortAudio.cpp @@ -229,38 +229,6 @@ void AudioPortAudio::stopProcessing() } - - -void AudioPortAudio::applyQualitySettings() -{ - if( hqAudio() ) - { - - setSampleRate( Engine::audioEngine()->processingSampleRate() ); - int samples = audioEngine()->framesPerPeriod(); - - PaError err = Pa_OpenStream( - &m_paStream, - supportsCapture() ? &m_inputParameters : nullptr, // The input parameter - &m_outputParameters, // The outputparameter - sampleRate(), - samples, - paNoFlag, // Don't use any flags - _process_callback, // our callback function - this ); - - if( err != paNoError ) - { - printf( "Couldn't open PortAudio: %s\n", Pa_GetErrorText( err ) ); - return; - } - } - - AudioDevice::applyQualitySettings(); -} - - - int AudioPortAudio::process_callback( const float *_inputBuffer, float * _outputBuffer, diff --git a/src/core/audio/AudioPulseAudio.cpp b/src/core/audio/AudioPulseAudio.cpp index 63b81a9e9..b32e6eaf2 100644 --- a/src/core/audio/AudioPulseAudio.cpp +++ b/src/core/audio/AudioPulseAudio.cpp @@ -109,22 +109,6 @@ void AudioPulseAudio::stopProcessing() } - - -void AudioPulseAudio::applyQualitySettings() -{ - if( hqAudio() ) - { -// setSampleRate( engine::audioEngine()->processingSampleRate() ); - - } - - AudioDevice::applyQualitySettings(); -} - - - - /* This routine is called whenever the stream state changes */ static void stream_state_callback( pa_stream *s, void * userdata ) { diff --git a/src/core/audio/AudioSdl.cpp b/src/core/audio/AudioSdl.cpp index da81886ac..0d960c107 100644 --- a/src/core/audio/AudioSdl.cpp +++ b/src/core/audio/AudioSdl.cpp @@ -191,37 +191,6 @@ void AudioSdl::stopProcessing() } } - - - -void AudioSdl::applyQualitySettings() -{ - // Better than if (0) -#if 0 - if( 0 )//hqAudio() ) - { - SDL_CloseAudio(); - - setSampleRate( Engine::audioEngine()->processingSampleRate() ); - - m_audioHandle.freq = sampleRate(); - - SDL_AudioSpec actual; - - // open the audio device, forcing the desired format - if( SDL_OpenAudio( &m_audioHandle, &actual ) < 0 ) - { - qCritical( "Couldn't open SDL-audio: %s\n", SDL_GetError() ); - } - } -#endif - - AudioDevice::applyQualitySettings(); -} - - - - void AudioSdl::sdlAudioCallback( void * _udata, Uint8 * _buf, int _len ) { auto _this = static_cast(_udata); diff --git a/src/core/audio/AudioSndio.cpp b/src/core/audio/AudioSndio.cpp index 7d8e7fa8c..535b885da 100644 --- a/src/core/audio/AudioSndio.cpp +++ b/src/core/audio/AudioSndio.cpp @@ -139,20 +139,6 @@ void AudioSndio::stopProcessing() stopProcessingThread( this ); } - -void AudioSndio::applyQualitySettings() -{ - if( hqAudio() ) - { - setSampleRate( Engine::audioEngine()->processingSampleRate() ); - - /* change sample rate to sampleRate() */ - } - - AudioDevice::applyQualitySettings(); -} - - void AudioSndio::run() { surroundSampleFrame * temp = new surroundSampleFrame[audioEngine()->framesPerPeriod()]; diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 1806f2931..d486be5c8 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -142,17 +142,6 @@ SongEditor::SongEditor( Song * song ) : int tempoSpinBoxCol = getGUI()->mainWindow()->addWidgetToToolBar( m_tempoSpinBox, 0 ); -#if 0 - toolButton * hq_btn = new toolButton( embed::getIconPixmap( "hq_mode" ), - tr( "High quality mode" ), - nullptr, nullptr, tb ); - hq_btn->setCheckable( true ); - connect( hq_btn, SIGNAL(toggled(bool)), - this, SLOT(setHighQuality(bool))); - hq_btn->setFixedWidth( 42 ); - getGUI()->mainWindow()->addWidgetToToolBar( hq_btn, 1, col ); -#endif - getGUI()->mainWindow()->addWidgetToToolBar( new TimeDisplayWidget, 1, tempoSpinBoxCol ); getGUI()->mainWindow()->addSpacingToToolBar( 10 ); @@ -333,19 +322,6 @@ QString SongEditor::getSnapSizeString() const } } - - - -void SongEditor::setHighQuality( bool hq ) -{ - Engine::audioEngine()->changeQuality( AudioEngine::qualitySettings( - hq ? AudioEngine::qualitySettings::Mode::HighQuality : - AudioEngine::qualitySettings::Mode::Draft ) ); -} - - - - void SongEditor::scrolled( int new_pos ) { update(); diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index ba7814f31..9e2b1fe4a 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -142,8 +142,6 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : "ui", "disableautoquit", "1").toInt()), m_NaNHandler(ConfigManager::inst()->value( "app", "nanhandler", "1").toInt()), - m_hqAudioDev(ConfigManager::inst()->value( - "audioengine", "hqaudio").toInt()), m_bufferSize(ConfigManager::inst()->value( "audioengine", "framesperaudiobuffer").toInt()), m_midiAutoQuantize(ConfigManager::inst()->value( @@ -560,10 +558,6 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // audio_layout->addWidget(useNaNHandler); // useNaNHandler->setChecked(m_NaNHandler); - // HQ mode checkbox - auto hqaudio = addCheckBox(tr("HQ mode for output audio device"), audioInterfaceBox, nullptr, - m_hqAudioDev, SLOT(toggleHQAudioDev(bool)), false); - // Buffer size group QGroupBox * bufferSizeBox = new QGroupBox(tr("Buffer size"), audio_w); QVBoxLayout * bufferSizeLayout = new QVBoxLayout(bufferSizeBox); @@ -605,7 +599,6 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Audio layout ordering. audio_layout->addWidget(audioInterfaceBox); audio_layout->addWidget(as_w); - audio_layout->addWidget(hqaudio); audio_layout->addWidget(bufferSizeBox); audio_layout->addStretch(); @@ -970,8 +963,6 @@ void SetupDialog::accept() m_audioIfaceNames[m_audioInterfaces->currentText()]); ConfigManager::inst()->setValue("app", "nanhandler", QString::number(m_NaNHandler)); - ConfigManager::inst()->setValue("audioengine", "hqaudio", - QString::number(m_hqAudioDev)); ConfigManager::inst()->setValue("audioengine", "framesperaudiobuffer", QString::number(m_bufferSize)); ConfigManager::inst()->setValue("audioengine", "mididev", @@ -1174,17 +1165,6 @@ void SetupDialog::toggleDisableAutoQuit(bool enabled) m_disableAutoQuit = enabled; } - - - -// Audio settings slots. - -void SetupDialog::toggleHQAudioDev(bool enabled) -{ - m_hqAudioDev = enabled; -} - - void SetupDialog::audioInterfaceChanged(const QString & iface) { for(AswMap::iterator it = m_audioIfaceSetupWidgets.begin(); From 6c846684cd845c683f4eb0af13c7407399acece5 Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:51:09 +0530 Subject: [PATCH 145/191] Replace call `QT5_WRAP_UI` with `CMAKE_AUTOUIC` (#7200) --- cmake/modules/BuildPlugin.cmake | 7 +++---- plugins/GigPlayer/CMakeLists.txt | 2 +- plugins/Sf2Player/CMakeLists.txt | 2 +- src/CMakeLists.txt | 8 ++++---- src/gui/CMakeLists.txt | 7 ------- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/cmake/modules/BuildPlugin.cmake b/cmake/modules/BuildPlugin.cmake index 70e518c93..111bdf36b 100644 --- a/cmake/modules/BuildPlugin.cmake +++ b/cmake/modules/BuildPlugin.cmake @@ -1,12 +1,12 @@ # BuildPlugin.cmake - Copyright (c) 2008 Tobias Doerffel # # description: build LMMS-plugin -# usage: BUILD_PLUGIN( MOCFILES EMBEDDED_RESOURCES UICFILES LINK ) +# usage: BUILD_PLUGIN( MOCFILES EMBEDDED_RESOURCES LINK ) INCLUDE(GenQrc) MACRO(BUILD_PLUGIN PLUGIN_NAME) - CMAKE_PARSE_ARGUMENTS(PLUGIN "" "LINK;EXPORT_BASE_NAME" "MOCFILES;EMBEDDED_RESOURCES;UICFILES" ${ARGN}) + CMAKE_PARSE_ARGUMENTS(PLUGIN "" "LINK;EXPORT_BASE_NAME" "MOCFILES;EMBEDDED_RESOURCES" ${ARGN}) SET(PLUGIN_SOURCES ${PLUGIN_UNPARSED_ARGUMENTS}) INCLUDE_DIRECTORIES("${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_BINARY_DIR}" "${CMAKE_SOURCE_DIR}/include") @@ -31,10 +31,9 @@ MACRO(BUILD_PLUGIN PLUGIN_NAME) ENDIF(ER_LEN) QT5_WRAP_CPP(plugin_MOC_out ${PLUGIN_MOCFILES}) - QT5_WRAP_UI(plugin_UIC_out ${PLUGIN_UICFILES}) FOREACH(f ${PLUGIN_SOURCES}) - ADD_FILE_DEPENDENCIES(${f} ${RCC_OUT} ${plugin_UIC_out}) + ADD_FILE_DEPENDENCIES(${f} ${RCC_OUT}) ENDFOREACH(f) IF(LMMS_BUILD_APPLE) diff --git a/plugins/GigPlayer/CMakeLists.txt b/plugins/GigPlayer/CMakeLists.txt index 7b634b605..6ec8fe169 100644 --- a/plugins/GigPlayer/CMakeLists.txt +++ b/plugins/GigPlayer/CMakeLists.txt @@ -1,6 +1,7 @@ if(LMMS_HAVE_GIG) INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}) + SET(CMAKE_AUTOUIC ON) # Required for not crashing loading files with libgig SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions") @@ -17,7 +18,6 @@ if(LMMS_HAVE_GIG) build_plugin(gigplayer GigPlayer.cpp GigPlayer.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES GigPlayer.h PatchesDialog.h - UICFILES PatchesDialog.ui EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" ) target_link_libraries(gigplayer SampleRate::samplerate) diff --git a/plugins/Sf2Player/CMakeLists.txt b/plugins/Sf2Player/CMakeLists.txt index 1d004a6c5..464b8bd3e 100644 --- a/plugins/Sf2Player/CMakeLists.txt +++ b/plugins/Sf2Player/CMakeLists.txt @@ -1,9 +1,9 @@ if(LMMS_HAVE_FLUIDSYNTH) + SET(CMAKE_AUTOUIC ON) include(BuildPlugin) build_plugin(sf2player Sf2Player.cpp Sf2Player.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES Sf2Player.h PatchesDialog.h - UICFILES PatchesDialog.ui EMBEDDED_RESOURCES *.png ) target_link_libraries(sf2player fluidsynth SampleRate::samplerate) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d71b34c59..ae5b55404 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,9 +4,9 @@ CONFIGURE_FILE("lmmsconfig.h.in" "${CMAKE_BINARY_DIR}/lmmsconfig.h") CONFIGURE_FILE("lmmsversion.h.in" "${CMAKE_BINARY_DIR}/lmmsversion.h") SET(LMMS_SRCS "") -SET(LMMS_UIS "") SET(CMAKE_AUTOMOC ON) +SET(CMAKE_AUTOUIC ON) SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # Enable C++17 @@ -23,7 +23,6 @@ ADD_SUBDIRECTORY(tracks) LIST(APPEND LMMS_SRCS ${LMMS_COMMON_SRCS}) -QT5_WRAP_UI(LMMS_UI_OUT ${LMMS_UIS}) INCLUDE_DIRECTORIES( "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_BINARY_DIR}" @@ -100,7 +99,6 @@ SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) ADD_LIBRARY(lmmsobjs OBJECT ${LMMS_SRCS} ${LMMS_INCLUDES} - ${LMMS_UI_OUT} ${LMMS_RCC_OUT} ) @@ -134,7 +132,7 @@ IF(NOT CMAKE_VERSION VERSION_LESS 3.6) SET_PROPERTY(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT lmms) ENDIF() -SET_DIRECTORY_PROPERTIES(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${LMMS_RCC_OUT} ${LMMS_UI_OUT} lmmsconfig.h lmms.1.gz") +SET_DIRECTORY_PROPERTIES(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${LMMS_RCC_OUT} lmmsconfig.h lmms.1.gz") IF(LMMS_BUILD_WIN32) SET(EXTRA_LIBRARIES "winmm") @@ -212,6 +210,8 @@ set_target_properties(lmms PROPERTIES WIN32_EXECUTABLE $> ) +set_target_properties(lmmsobjs PROPERTIES AUTOUIC_SEARCH_PATHS "gui/modals") + IF(LMMS_BUILD_MSYS) # ENABLE_EXPORTS property has no effect in some MSYS2 configurations. # Add the linker flag manually to create liblmms.dll.a import library diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 791d6fbff..f6115dbf6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -131,10 +131,3 @@ SET(LMMS_SRCS PARENT_SCOPE ) -set(LMMS_UIS - ${LMMS_UIS} - gui/modals/about_dialog.ui - gui/modals/export_project.ui - - PARENT_SCOPE -) From 5c0db46a609fd9245ba90c24244b50ea7a0c44fb Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 27 Apr 2024 17:45:55 +0200 Subject: [PATCH 146/191] Streamline instrument flags (#7227) ## Instrument flags as a property of an instrument The instruments flags (single streamed, MIDI based, not bendable) are properties of an instrument that do not change over time. Therefore the flags are made a property of the instrument which is initialized at construction time. Adjust the constructors of all instruments which overrode the `flags` method to pass their flags into the `Instrument` constructor. ## Add helper methods for flags Add helper methods for the flags. This makes the code more concise and readable and clients do not need to know the technical details on how to evaluate a flag. ## Remove the flags methods Remove the flags methods to make it an implementation detail on how the flags are managed. --- include/Instrument.h | 21 +++++++++++++++----- plugins/CarlaBase/Carla.cpp | 7 +------ plugins/CarlaBase/Carla.h | 1 - plugins/GigPlayer/GigPlayer.cpp | 2 +- plugins/GigPlayer/GigPlayer.h | 5 ----- plugins/Kicker/Kicker.cpp | 2 +- plugins/Kicker/Kicker.h | 5 ----- plugins/Lb302/Lb302.cpp | 2 +- plugins/Lb302/Lb302.h | 5 ----- plugins/Lv2Instrument/Lv2Instrument.cpp | 8 +++++++- plugins/Lv2Instrument/Lv2Instrument.h | 8 -------- plugins/OpulenZ/OpulenZ.cpp | 2 +- plugins/OpulenZ/OpulenZ.h | 5 ----- plugins/Sf2Player/Sf2Player.cpp | 2 +- plugins/Sf2Player/Sf2Player.h | 5 ----- plugins/Vestige/Vestige.cpp | 2 +- plugins/Vestige/Vestige.h | 5 ----- plugins/Vibed/Vibed.cpp | 2 +- plugins/Vibed/Vibed.h | 2 -- plugins/ZynAddSubFx/ZynAddSubFx.cpp | 2 +- plugins/ZynAddSubFx/ZynAddSubFx.h | 5 ----- src/core/Instrument.cpp | 6 ++++-- src/core/InstrumentSoundShaping.cpp | 2 +- src/core/NotePlayHandle.cpp | 2 +- src/gui/instrument/InstrumentTrackWindow.cpp | 6 +++--- src/tracks/InstrumentTrack.cpp | 5 ++--- 26 files changed, 43 insertions(+), 76 deletions(-) diff --git a/include/Instrument.h b/include/Instrument.h index 0c28e7f3a..e2e980372 100644 --- a/include/Instrument.h +++ b/include/Instrument.h @@ -62,7 +62,8 @@ public: Instrument(InstrumentTrack * _instrument_track, const Descriptor * _descriptor, - const Descriptor::SubPluginFeatures::Key * key = nullptr); + const Descriptor::SubPluginFeatures::Key * key = nullptr, + Flags flags = Flag::NoFlags); ~Instrument() override = default; // -------------------------------------------------------------------- @@ -114,9 +115,19 @@ public: sample_rate_t getSampleRate() const; - virtual Flags flags() const + bool isSingleStreamed() const { - return Flag::NoFlags; + return m_flags.testFlag(Instrument::Flag::IsSingleStreamed); + } + + bool isMidiBased() const + { + return m_flags.testFlag(Instrument::Flag::IsMidiBased); + } + + bool isBendable() const + { + return !m_flags.testFlag(Instrument::Flag::IsNotBendable); } // sub-classes can re-implement this for receiving all incoming @@ -161,8 +172,8 @@ protected: private: InstrumentTrack * m_instrumentTrack; - -} ; + Flags m_flags; +}; LMMS_DECLARE_OPERATORS_FOR_FLAGS(Instrument::Flag) diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index c95a965c9..d96a1e4d4 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -150,7 +150,7 @@ static const char* host_ui_save_file(NativeHostHandle, bool isDir, const char* t CarlaInstrument::CarlaInstrument(InstrumentTrack* const instrumentTrack, const Descriptor* const descriptor, const bool isPatchbay) - : Instrument(instrumentTrack, descriptor), + : Instrument(instrumentTrack, descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased | Flag::IsNotBendable), kIsPatchbay(isPatchbay), fHandle(nullptr), fDescriptor(isPatchbay ? carla_get_native_patchbay_plugin() : carla_get_native_rack_plugin()), @@ -343,11 +343,6 @@ intptr_t CarlaInstrument::handleDispatcher(const NativeHostDispatcherOpcode opco // ------------------------------------------------------------------- -Instrument::Flags CarlaInstrument::flags() const -{ - return Flag::IsSingleStreamed | Flag::IsMidiBased | Flag::IsNotBendable; -} - QString CarlaInstrument::nodeName() const { return descriptor()->name; diff --git a/plugins/CarlaBase/Carla.h b/plugins/CarlaBase/Carla.h index 3d0e424a2..76a9b45b7 100644 --- a/plugins/CarlaBase/Carla.h +++ b/plugins/CarlaBase/Carla.h @@ -190,7 +190,6 @@ public: intptr_t handleDispatcher(const NativeHostDispatcherOpcode opcode, const int32_t index, const intptr_t value, void* const ptr, const float opt); // LMMS functions - Flags flags() const override; QString nodeName() const override; void saveSettings(QDomDocument& doc, QDomElement& parent) override; void loadSettings(const QDomElement& elem) override; diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 2d67f0ddf..ecde81461 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -81,7 +81,7 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : - Instrument( _instrument_track, &gigplayer_plugin_descriptor ), + Instrument(_instrument_track, &gigplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsNotBendable), m_instance( nullptr ), m_instrument( nullptr ), m_filename( "" ), diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 85b1736a0..50d1acd40 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -259,11 +259,6 @@ public: QString nodeName() const override; - Flags flags() const override - { - return Flag::IsSingleStreamed | Flag::IsNotBendable; - } - gui::PluginView* instantiateView( QWidget * _parent ) override; QString getCurrentPatchName(); diff --git a/plugins/Kicker/Kicker.cpp b/plugins/Kicker/Kicker.cpp index e6418e2da..85fbf8e2b 100644 --- a/plugins/Kicker/Kicker.cpp +++ b/plugins/Kicker/Kicker.cpp @@ -64,7 +64,7 @@ Plugin::Descriptor PLUGIN_EXPORT kicker_plugin_descriptor = KickerInstrument::KickerInstrument( InstrumentTrack * _instrument_track ) : - Instrument( _instrument_track, &kicker_plugin_descriptor ), + Instrument(_instrument_track, &kicker_plugin_descriptor, nullptr, Flag::IsNotBendable), m_startFreqModel( 150.0f, 5.0f, 1000.0f, 1.0f, this, tr( "Start frequency" ) ), m_endFreqModel( 40.0f, 5.0f, 1000.0f, 1.0f, this, tr( "End frequency" ) ), m_decayModel( 440.0f, 5.0f, 5000.0f, 1.0f, 5000.0f, this, tr( "Length" ) ), diff --git a/plugins/Kicker/Kicker.h b/plugins/Kicker/Kicker.h index 820265dd5..508787707 100644 --- a/plugins/Kicker/Kicker.h +++ b/plugins/Kicker/Kicker.h @@ -64,11 +64,6 @@ public: QString nodeName() const override; - Flags flags() const override - { - return Flag::IsNotBendable; - } - float desiredReleaseTimeMs() const override { return 12.f; diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index e0b31360f..b0a6490b2 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -269,7 +269,7 @@ float Lb302Filter3Pole::process(const float& samp) // Lb302Synth::Lb302Synth( InstrumentTrack * _instrumentTrack ) : - Instrument( _instrumentTrack, &lb302_plugin_descriptor ), + Instrument(_instrumentTrack, &lb302_plugin_descriptor, nullptr, Flag::IsSingleStreamed), vcf_cut_knob( 0.75f, 0.0f, 1.5f, 0.005f, this, tr( "VCF Cutoff Frequency" ) ), vcf_res_knob( 0.75f, 0.0f, 1.25f, 0.005f, this, tr( "VCF Resonance" ) ), vcf_mod_knob( 0.1f, 0.0f, 1.0f, 0.005f, this, tr( "VCF Envelope Mod" ) ), diff --git a/plugins/Lb302/Lb302.h b/plugins/Lb302/Lb302.h index 2f4f64dcb..086b78a8a 100644 --- a/plugins/Lb302/Lb302.h +++ b/plugins/Lb302/Lb302.h @@ -163,11 +163,6 @@ public: QString nodeName() const override; - Flags flags() const override - { - return Flag::IsSingleStreamed; - } - gui::PluginView* instantiateView( QWidget * _parent ) override; private: diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 316829327..766790cc1 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -73,7 +73,13 @@ Plugin::Descriptor PLUGIN_EXPORT lv2instrument_plugin_descriptor = Lv2Instrument::Lv2Instrument(InstrumentTrack *instrumentTrackArg, Descriptor::SubPluginFeatures::Key *key) : - Instrument(instrumentTrackArg, &lv2instrument_plugin_descriptor, key), + Instrument(instrumentTrackArg, &lv2instrument_plugin_descriptor, key, +#ifdef LV2_INSTRUMENT_USE_MIDI + Flag::IsSingleStreamed | Flag::IsMidiBased +#else + Flag::IsSingleStreamed +#endif + ), Lv2ControlBase(this, key->attributes["uri"]) { clearRunningNotes(); diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h index de41dc958..9ae48c64c 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.h +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -84,14 +84,6 @@ public: /* misc */ - Flags flags() const override - { -#ifdef LV2_INSTRUMENT_USE_MIDI - return Flag::IsSingleStreamed | Flag::IsMidiBased; -#else - return Flag::IsSingleStreamed; -#endif - } gui::PluginView* instantiateView(QWidget *parent) override; private slots: diff --git a/plugins/OpulenZ/OpulenZ.cpp b/plugins/OpulenZ/OpulenZ.cpp index f0571ba5c..bf1459d6f 100644 --- a/plugins/OpulenZ/OpulenZ.cpp +++ b/plugins/OpulenZ/OpulenZ.cpp @@ -95,7 +95,7 @@ QMutex OpulenzInstrument::emulatorMutex; const auto adlib_opadd = std::array{0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12}; OpulenzInstrument::OpulenzInstrument( InstrumentTrack * _instrument_track ) : - Instrument( _instrument_track, &opulenz_plugin_descriptor ), + Instrument(_instrument_track, &opulenz_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased), m_patchModel( 0, 0, 127, this, tr( "Patch" ) ), op1_a_mdl(14.0, 0.0, 15.0, 1.0, this, tr( "Op 1 attack" ) ), op1_d_mdl(14.0, 0.0, 15.0, 1.0, this, tr( "Op 1 decay" ) ), diff --git a/plugins/OpulenZ/OpulenZ.h b/plugins/OpulenZ/OpulenZ.h index a3e11a6c0..a245b03ad 100644 --- a/plugins/OpulenZ/OpulenZ.h +++ b/plugins/OpulenZ/OpulenZ.h @@ -64,11 +64,6 @@ public: QString nodeName() const override; gui::PluginView* instantiateView( QWidget * _parent ) override; - Flags flags() const override - { - return Flag::IsSingleStreamed | Flag::IsMidiBased; - } - bool handleMidiEvent( const MidiEvent& event, const TimePos& time, f_cnt_t offset = 0 ) override; void play( sampleFrame * _working_buffer ) override; diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 7795671c5..9ded939d7 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -122,7 +122,7 @@ struct Sf2PluginData Sf2Instrument::Sf2Instrument( InstrumentTrack * _instrument_track ) : - Instrument( _instrument_track, &sf2player_plugin_descriptor ), + Instrument(_instrument_track, &sf2player_plugin_descriptor, nullptr, Flag::IsSingleStreamed), m_srcState( nullptr ), m_synth(nullptr), m_font( nullptr ), diff --git a/plugins/Sf2Player/Sf2Player.h b/plugins/Sf2Player/Sf2Player.h index 4760d572e..ec7ace47f 100644 --- a/plugins/Sf2Player/Sf2Player.h +++ b/plugins/Sf2Player/Sf2Player.h @@ -80,11 +80,6 @@ public: QString nodeName() const override; - Flags flags() const override - { - return Flag::IsSingleStreamed; - } - gui::PluginView* instantiateView( QWidget * _parent ) override; QString getCurrentPatchName(); diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index 834b583ed..1de713960 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -152,7 +152,7 @@ private: VestigeInstrument::VestigeInstrument( InstrumentTrack * _instrument_track ) : - Instrument( _instrument_track, &vestige_plugin_descriptor ), + Instrument(_instrument_track, &vestige_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased), m_plugin( nullptr ), m_pluginMutex(), m_subWindow( nullptr ), diff --git a/plugins/Vestige/Vestige.h b/plugins/Vestige/Vestige.h index 9ac66f74d..529893ba0 100644 --- a/plugins/Vestige/Vestige.h +++ b/plugins/Vestige/Vestige.h @@ -70,11 +70,6 @@ public: virtual void loadFile( const QString & _file ); - virtual Flags flags() const - { - return Flag::IsSingleStreamed | Flag::IsMidiBased; - } - virtual bool handleMidiEvent( const MidiEvent& event, const TimePos& time, f_cnt_t offset = 0 ); virtual gui::PluginView* instantiateView( QWidget * _parent ); diff --git a/plugins/Vibed/Vibed.cpp b/plugins/Vibed/Vibed.cpp index ddf9097a5..3dc4bfa56 100644 --- a/plugins/Vibed/Vibed.cpp +++ b/plugins/Vibed/Vibed.cpp @@ -97,7 +97,7 @@ private: }; Vibed::Vibed(InstrumentTrack* instrumentTrack) : - Instrument(instrumentTrack, &vibedstrings_plugin_descriptor) + Instrument(instrumentTrack, &vibedstrings_plugin_descriptor, nullptr, Flag::IsNotBendable) { for (int harm = 0; harm < s_stringCount; ++harm) { diff --git a/plugins/Vibed/Vibed.h b/plugins/Vibed/Vibed.h index 18d334c4d..ec8395da1 100644 --- a/plugins/Vibed/Vibed.h +++ b/plugins/Vibed/Vibed.h @@ -65,8 +65,6 @@ public: QString nodeName() const override; - Flags flags() const override { return Flag::IsNotBendable; } - gui::PluginView* instantiateView(QWidget* parent) override; private: diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index c406e04e0..85b240380 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -103,7 +103,7 @@ bool ZynAddSubFxRemotePlugin::processMessage( const message & _m ) ZynAddSubFxInstrument::ZynAddSubFxInstrument( InstrumentTrack * _instrumentTrack ) : - Instrument( _instrumentTrack, &zynaddsubfx_plugin_descriptor ), + Instrument(_instrumentTrack, &zynaddsubfx_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased), m_hasGUI( false ), m_plugin( nullptr ), m_remotePlugin( nullptr ), diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.h b/plugins/ZynAddSubFx/ZynAddSubFx.h index a391203f3..b4f7c434c 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.h +++ b/plugins/ZynAddSubFx/ZynAddSubFx.h @@ -86,11 +86,6 @@ public: QString nodeName() const override; - Flags flags() const override - { - return Flag::IsSingleStreamed | Flag::IsMidiBased; - } - gui::PluginView* instantiateView( QWidget * _parent ) override; diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index 2dfdc78f5..3e884eaca 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -37,9 +37,11 @@ namespace lmms Instrument::Instrument(InstrumentTrack * _instrument_track, const Descriptor * _descriptor, - const Descriptor::SubPluginFeatures::Key *key) : + const Descriptor::SubPluginFeatures::Key *key, + Flags flags) : Plugin(_descriptor, nullptr/* _instrument_track*/, key), - m_instrumentTrack( _instrument_track ) + m_instrumentTrack( _instrument_track ), + m_flags(flags) { } diff --git a/src/core/InstrumentSoundShaping.cpp b/src/core/InstrumentSoundShaping.cpp index 07c3bbf7c..2e00760cc 100644 --- a/src/core/InstrumentSoundShaping.cpp +++ b/src/core/InstrumentSoundShaping.cpp @@ -303,7 +303,7 @@ f_cnt_t InstrumentSoundShaping::releaseFrames() const f_cnt_t ret_val = m_instrumentTrack->instrument()->desiredReleaseFrames(); - if( m_instrumentTrack->instrument()->flags().testFlag( Instrument::Flag::IsSingleStreamed ) ) + if (m_instrumentTrack->instrument()->isSingleStreamed()) { return ret_val; } diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index 2c1c21931..d7882b525 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -109,7 +109,7 @@ NotePlayHandle::NotePlayHandle( InstrumentTrack* instrumentTrack, m_instrumentTrack->midiNoteOn( *this ); } - if(m_instrumentTrack->instrument() && m_instrumentTrack->instrument()->flags() & Instrument::Flag::IsSingleStreamed ) + if (m_instrumentTrack->instrument() && m_instrumentTrack->instrument()->isSingleStreamed()) { setUsesBuffer( false ); } diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index 86d9086c8..8b868bb50 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -350,7 +350,7 @@ void InstrumentTrackWindow::modelChanged() m_mixerChannelNumber->setModel( &m_track->m_mixerChannelModel ); m_pianoView->setModel( &m_track->m_piano ); - if( m_track->instrument() && m_track->instrument()->flags().testFlag( Instrument::Flag::IsNotBendable ) == false ) + if (m_track->instrument() && m_track->instrument()->isBendable()) { m_pitchKnob->setModel( &m_track->m_pitchModel ); m_pitchRangeSpinBox->setModel( &m_track->m_pitchRangeModel ); @@ -368,7 +368,7 @@ void InstrumentTrackWindow::modelChanged() m_pitchRangeLabel->hide(); } - if (m_track->instrument() && m_track->instrument()->flags().testFlag(Instrument::Flag::IsMidiBased)) + if (m_track->instrument() && m_track->instrument()->isMidiBased()) { m_tuningView->microtunerNotSupportedLabel()->show(); m_tuningView->microtunerGroupBox()->hide(); @@ -462,7 +462,7 @@ void InstrumentTrackWindow::updateInstrumentView() m_tabWidget->addTab( m_instrumentView, tr( "Plugin" ), "plugin_tab", 0 ); m_tabWidget->setActiveTab( 0 ); - m_ssView->setFunctionsHidden( m_track->m_instrument->flags().testFlag( Instrument::Flag::IsSingleStreamed ) ); + m_ssView->setFunctionsHidden(m_track->m_instrument->isSingleStreamed()); modelChanged(); // Get the instrument window to refresh m_track->dataChanged(); // Get the text on the trackButton to change diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index a32d301c4..3317ea71b 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -233,8 +233,7 @@ void InstrumentTrack::processAudioBuffer( sampleFrame* buf, const fpp_t frames, // We could do that in all other cases as well but the overhead for silence test is bigger than // what we potentially save. While playing a note, a NotePlayHandle-driven instrument will produce sound in // 99 of 100 cases so that test would be a waste of time. - if( m_instrument->flags().testFlag( Instrument::Flag::IsSingleStreamed ) && - MixHelpers::isSilent( buf, frames ) ) + if (m_instrument->isSingleStreamed() && MixHelpers::isSilent(buf, frames)) { // at least pass one silent buffer to allow if( m_silentBuffersProcessed ) @@ -263,7 +262,7 @@ void InstrumentTrack::processAudioBuffer( sampleFrame* buf, const fpp_t frames, // instruments using instrument-play-handles will call this method // without any knowledge about notes, so they pass NULL for n, which // is no problem for us since we just bypass the envelopes+LFOs - if( m_instrument->flags().testFlag( Instrument::Flag::IsSingleStreamed ) == false && n != nullptr ) + if (!m_instrument->isSingleStreamed() && n != nullptr) { const f_cnt_t offset = n->noteOffset(); m_soundShaping.processAudioBuffer( buf + offset, frames - offset, n ); From 86363819c556a63e7666d173118a50013eb93ce9 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 27 Apr 2024 21:17:12 +0200 Subject: [PATCH 147/191] Fix Kicker's release stage (#7226) When applying its release stage Kicker did not take the frames before the release into account but instead always applied the release to the full buffer. This potentially lead to a jump in the attenuation values instead of a clean linear decay. See #7225 for more details. --- plugins/Kicker/Kicker.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/Kicker/Kicker.cpp b/plugins/Kicker/Kicker.cpp index 85fbf8e2b..5cbb18105 100644 --- a/plugins/Kicker/Kicker.cpp +++ b/plugins/Kicker/Kicker.cpp @@ -188,13 +188,22 @@ void KickerInstrument::playNote( NotePlayHandle * _n, if( _n->isReleased() ) { - const float done = _n->releaseFramesDone(); + // We need this to check if the release has ended const float desired = desiredReleaseFrames(); - for( fpp_t f = 0; f < frames; ++f ) + + // This can be considered the current release frame in the "global" context of the release. + // We need it with the desired number of release frames to compute the linear decay. + fpp_t currentReleaseFrame = _n->releaseFramesDone(); + + // Start applying the release at the correct frame + const float framesBeforeRelease = _n->framesBeforeRelease(); + for (fpp_t f = framesBeforeRelease; f < frames; ++f, ++currentReleaseFrame) { - const float fac = ( done+f < desired ) ? ( 1.0f - ( ( done+f ) / desired ) ) : 0; - _working_buffer[f+offset][0] *= fac; - _working_buffer[f+offset][1] *= fac; + const bool releaseStillActive = currentReleaseFrame < desired; + const float attenuation = releaseStillActive ? (1.0f - (currentReleaseFrame / desired)) : 0.f; + + _working_buffer[f + offset][0] *= attenuation; + _working_buffer[f + offset][1] *= attenuation; } } } From c0a4df49a24f17ddb83fa060c78b91f61369d924 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 29 Apr 2024 16:47:17 +0200 Subject: [PATCH 148/191] Lb302: Consistent decay in time (#7230) The previous implementation of Lb302`s decay used a fixed decay factor that was multiplied with the signal until the minimum threshold of 1/65536 was crossed. This fixed factor resulted in different lengths in time for different sample rates. This is fixed by computing the decay factor by taking the sample rate into account as well. The new static method `computeDecayFactor` computes the factor that is needed to make a signal decay from 1 to a given attenuation over a given time. The parameters used in the call to that method in `Lb302Synth::process` have been fine-tuned such that, at a sample rate of 44.1 kHz, they result in a factor very close to the previous hard-coded factor of 0.99897516. --- plugins/Lb302/Lb302.cpp | 21 +++++++++++++++++++-- plugins/Lb302/Lb302.h | 1 - 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index b0a6490b2..b85c26b0d 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -268,6 +268,16 @@ float Lb302Filter3Pole::process(const float& samp) // LBSynth // +static float computeDecayFactor(float decayTimeInSeconds, float targetedAttenuation) +{ + // This is the number of samples that correspond to the decay time in seconds + auto samplesNeededForDecay = decayTimeInSeconds * Engine::audioEngine()->processingSampleRate(); + + // This computes the factor that's needed to make a signal with a value of 1 decay to the + // targeted attenuation over the time in number of samples. + return std::pow(targetedAttenuation, 1. / samplesNeededForDecay); +} + Lb302Synth::Lb302Synth( InstrumentTrack * _instrumentTrack ) : Instrument(_instrumentTrack, &lb302_plugin_descriptor, nullptr, Flag::IsSingleStreamed), vcf_cut_knob( 0.75f, 0.0f, 1.5f, 0.005f, this, tr( "VCF Cutoff Frequency" ) ), @@ -282,7 +292,6 @@ Lb302Synth::Lb302Synth( InstrumentTrack * _instrumentTrack ) : deadToggle( false, this, tr( "Dead" ) ), db24Toggle( false, this, tr( "24dB/oct Filter" ) ), vca_attack(1.0 - 0.96406088), - vca_decay(0.99897516), vca_a0(0.5), vca_a(0.), vca_mode(VcaMode::NeverPlayed) @@ -481,6 +490,14 @@ int Lb302Synth::process(sampleFrame *outbuf, const int size) // TODO: NORMAL RELEASE // vca_mode = 1; + // Note: this has to be computed during processing and cannot be initialized + // in the constructor because it's dependent on the sample rate and that might + // change during rendering! + // + // At 44.1 kHz this will compute something very close to the previously + // hard coded value of 0.99897516. + auto decay = computeDecayFactor(0.245260770975f, 1.f / 65536.f); + for( int i=0; i= ENVINC float vca_attack, // Amp attack - vca_decay, // Amp decay vca_a0, // Initial amplifier coefficient vca_a; // Amplifier coefficient. From bb6a77aa0fccb270dfe98c014c3142a2578e0aeb Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 29 Apr 2024 19:22:37 +0200 Subject: [PATCH 149/191] Only set sample clips for valid files (#7224) Check if a non-empty buffer was loaded and only set the sample clip if that's the case. ## Other changes Move setting the song to modified towards the core in the context of `SampleClip`. Previously the `SampleClipView` did this but it's none of it's business. Introduce `SampleClip::changeLengthToSampleLength` which changes the length of the clip to the length of the sample. This was also previously done by the view which is again the wrong place to do the necessary calculations. An unnecessary `static_cast` was removed while carrying over the code. Add the method `SampleClip::hasSampleFileLoaded` which checks if the loaded sample corresponds to a given file name. Fix code formatting. --- include/SampleClip.h | 2 ++ src/core/SampleClip.cpp | 14 ++++++++++++++ src/gui/clips/SampleClipView.cpp | 22 ++++++++++++---------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/include/SampleClip.h b/include/SampleClip.h index da11996b1..3beca338b 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -55,7 +55,9 @@ public: SampleClip& operator=( const SampleClip& that ) = delete; void changeLength( const TimePos & _length ) override; + void changeLengthToSampleLength(); const QString& sampleFile() const; + bool hasSampleFileLoaded(const QString & filename) const; void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; void loadSettings( const QDomElement & _this ) override; diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 42d4f6441..9a1c0731a 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -112,6 +112,11 @@ void SampleClip::changeLength( const TimePos & _length ) Clip::changeLength(std::max(static_cast(_length), 1)); } +void SampleClip::changeLengthToSampleLength() +{ + int length = m_sample.sampleSize() / Engine::framesPerTick(); + changeLength(length); +} @@ -120,6 +125,11 @@ const QString& SampleClip::sampleFile() const return m_sample.sampleFile(); } +bool SampleClip::hasSampleFileLoaded(const QString & filename) const +{ + return m_sample.sampleFile() == filename; +} + void SampleClip::setSampleBuffer(std::shared_ptr sb) { { @@ -129,6 +139,8 @@ void SampleClip::setSampleBuffer(std::shared_ptr sb) updateLength(); emit sampleChanged(); + + Engine::getSong()->setModified(); } void SampleClip::setSampleFile(const QString& sf) @@ -210,6 +222,8 @@ void SampleClip::setIsPlaying(bool isPlaying) void SampleClip::updateLength() { emit sampleChanged(); + + Engine::getSong()->setModified(); } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index ef46325e4..30f09caa8 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -127,7 +127,6 @@ void SampleClipView::dropEvent( QDropEvent * _de ) m_clip->updateLength(); update(); _de->accept(); - Engine::getSong()->setModified(); } else { @@ -181,18 +180,21 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { - QString af = SampleLoader::openAudioFile(); + const QString selectedAudioFile = SampleLoader::openAudioFile(); - if ( af.isEmpty() ) {} //Don't do anything if no file is loaded - else if (af == m_clip->m_sample.sampleFile()) - { //Instead of reloading the existing file, just reset the size - int length = static_cast(m_clip->m_sample.sampleSize() / Engine::framesPerTick()); - m_clip->changeLength(length); + if (selectedAudioFile.isEmpty()) { return; } + + if (m_clip->hasSampleFileLoaded(selectedAudioFile)) + { + m_clip->changeLengthToSampleLength(); } else - { //Otherwise load the new file as ususal - m_clip->setSampleFile( af ); - Engine::getSong()->setModified(); + { + auto sampleBuffer = SampleLoader::createBufferFromFile(selectedAudioFile); + if (sampleBuffer != SampleBuffer::emptyBuffer()) + { + m_clip->setSampleBuffer(sampleBuffer); + } } } From 9ca9143f5b26ae0e0ab54dcecf5c81a3d4549d05 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 1 May 2024 18:03:35 +0200 Subject: [PATCH 150/191] Fix upgrade routine for BPM renamings (#7235) Fix the upgrade routine that was introduced with pull request #6747 which added the BPM value to some file names. This also simplifies the implementation by using a map. Note: this also removes the code about the prefix `factorysample:`. If it is used in some files these entries will also have to be added to the map. --- src/core/DataFile.cpp | 82 ++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 00c6845f3..b7fea852e 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -1874,38 +1874,41 @@ void DataFile::upgrade_sampleAndHold() // Change loops' filenames in s void DataFile::upgrade_loopsRename() { - static constexpr auto loopBPMs = std::array{ - std::pair{"bassloops/briff01", "140"}, - std::pair{"bassloops/rave_bass01", "180"}, - std::pair{"bassloops/rave_bass02", "180"}, - std::pair{"bassloops/tb303_01", "123"}, - std::pair{"bassloops/techno_bass01", "140"}, - std::pair{"bassloops/techno_bass02", "140"}, - std::pair{"bassloops/techno_synth01", "140"}, - std::pair{"bassloops/techno_synth02", "140"}, - std::pair{"bassloops/techno_synth03", "130"}, - std::pair{"bassloops/techno_synth04", "140"}, - std::pair{"beats/909beat01", "122"}, - std::pair{"beats/break01", "168"}, - std::pair{"beats/break02", "141"}, - std::pair{"beats/break03", "168"}, - std::pair{"beats/electro_beat01", "120"}, - std::pair{"beats/electro_beat02", "119"}, - std::pair{"beats/house_loop01", "142"}, - std::pair{"beats/jungle01", "168"}, - std::pair{"beats/rave_hihat01", "180"}, - std::pair{"beats/rave_hihat02", "180"}, - std::pair{"beats/rave_kick01", "180"}, - std::pair{"beats/rave_kick02", "180"}, - std::pair{"beats/rave_snare01", "180"}, - std::pair{"latin/latin_brass01", "140"}, - std::pair{"latin/latin_guitar01", "126"}, - std::pair{"latin/latin_guitar02", "140"}, - std::pair{"latin/latin_guitar03", "120"}, + auto createEntry = [](const QString& originalName, const QString& bpm, const QString& extension = "ogg") + { + const QString replacement = originalName + " - " + bpm + " BPM." + extension; + return std::pair{originalName + "." + extension, replacement}; }; - const QString prefix = "factorysample:", - extension = ".ogg"; + static const QMap namesToNamesWithBPMsMap { + { createEntry("bassloops/briff01", "140") }, + { createEntry("bassloops/rave_bass01", "180") }, + { createEntry("bassloops/rave_bass02", "180") }, + { createEntry("bassloops/tb303_01", "123") }, + { createEntry("bassloops/techno_bass01", "140") }, + { createEntry("bassloops/techno_bass02", "140") }, + { createEntry("bassloops/techno_synth01", "140") }, + { createEntry("bassloops/techno_synth02", "140") }, + { createEntry("bassloops/techno_synth03", "130") }, + { createEntry("bassloops/techno_synth04", "140") }, + { createEntry("beats/909beat01", "122") }, + { createEntry("beats/break01", "168") }, + { createEntry("beats/break02", "141") }, + { createEntry("beats/break03", "168") }, + { createEntry("beats/electro_beat01", "120") }, + { createEntry("beats/electro_beat02", "119") }, + { createEntry("beats/house_loop01", "142") }, + { createEntry("beats/jungle01", "168") }, + { createEntry("beats/rave_hihat01", "180") }, + { createEntry("beats/rave_hihat02", "180") }, + { createEntry("beats/rave_kick01", "180") }, + { createEntry("beats/rave_kick02", "180") }, + { createEntry("beats/rave_snare01", "180") }, + { createEntry("latin/latin_brass01", "140") }, + { createEntry("latin/latin_guitar01", "126") }, + { createEntry("latin/latin_guitar02", "140") }, + { createEntry("latin/latin_guitar03", "120") } + }; // Replace loop sample names for (const auto& [elem, srcAttrs] : ELEMENTS_WITH_RESOURCES) @@ -1919,20 +1922,13 @@ void DataFile::upgrade_loopsRename() auto item = elements.item(i).toElement(); if (item.isNull() || !item.hasAttribute(srcAttr)) { continue; } - for (const auto& cur : loopBPMs) - { - QString x = cur.first, // loop name - y = cur.second, // BPM - srcVal = item.attribute(srcAttr), - pattern = prefix + x + extension; - if (srcVal == pattern) - { - // Add " - X BPM" to filename - item.setAttribute(srcAttr, - prefix + x + " - " + y + " BPM" + - extension); - } + const QString srcVal = item.attribute(srcAttr); + + const auto it = namesToNamesWithBPMsMap.constFind(srcVal); + if (it != namesToNamesWithBPMsMap.constEnd()) + { + item.setAttribute(srcAttr, *it); } } } From d5f5d00a6f1faabdba4f710d04ad303bd439a6f2 Mon Sep 17 00:00:00 2001 From: Pascal <81458575+khoidauminh@users.noreply.github.com> Date: Thu, 2 May 2024 07:13:55 +0700 Subject: [PATCH 151/191] Fix shifting of sample waveform during zoom (#7222) The original code was doing division in `int`, which causes lost of accuracy and results in the waveform randomly shifting when zooming in and out. This should fix it by casting variables to `float` before dividing, as well as keeping the values in `float` type. The pull request also changes some type declaration to explicit to increase readability --- src/gui/SampleWaveform.cpp | 57 +++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/gui/SampleWaveform.cpp b/src/gui/SampleWaveform.cpp index 68b09b482..01d21a8c5 100644 --- a/src/gui/SampleWaveform.cpp +++ b/src/gui/SampleWaveform.cpp @@ -28,55 +28,66 @@ namespace lmms::gui { void SampleWaveform::visualize(Parameters parameters, QPainter& painter, const QRect& rect) { - const auto x = rect.x(); - const auto height = rect.height(); - const auto width = rect.width(); - const auto centerY = rect.center().y(); + const int x = rect.x(); + const int height = rect.height(); + const int width = rect.width(); + const int centerY = rect.center().y(); - const auto halfHeight = height / 2; + const int halfHeight = height / 2; const auto color = painter.pen().color(); const auto rmsColor = color.lighter(123); - const auto framesPerPixel = std::max(1, parameters.size / width); + const float framesPerPixel = std::max(1.0f, static_cast(parameters.size) / width); - constexpr auto maxFramesPerPixel = 512; - const auto resolution = std::max(1, framesPerPixel / maxFramesPerPixel); - const auto framesPerResolution = framesPerPixel / resolution; + constexpr float maxFramesPerPixel = 512.0f; + const float resolution = std::max(1.0f, framesPerPixel / maxFramesPerPixel); + const float framesPerResolution = framesPerPixel / resolution; - const auto numPixels = std::min(parameters.size, width); + const size_t numPixels = std::min(parameters.size, width); auto min = std::vector(numPixels, 1); auto max = std::vector(numPixels, -1); - auto squared = std::vector(numPixels); + auto squared = std::vector(numPixels, 0); + + const size_t maxFrames = numPixels * static_cast(framesPerPixel); + + int pixelIndex = 0; - const auto maxFrames = numPixels * framesPerPixel; for (int i = 0; i < maxFrames; i += resolution) { - const auto pixelIndex = i / framesPerPixel; - const auto frameIndex = !parameters.reversed ? i : maxFrames - i; + pixelIndex = i / framesPerPixel; + const int frameIndex = !parameters.reversed ? i : maxFrames - i; const auto& frame = parameters.buffer[frameIndex]; - const auto value = std::accumulate(frame.begin(), frame.end(), 0.0f) / frame.size(); + const float value = std::accumulate(frame.begin(), frame.end(), 0.0f) / frame.size(); if (value > max[pixelIndex]) { max[pixelIndex] = value; } if (value < min[pixelIndex]) { min[pixelIndex] = value; } squared[pixelIndex] += value * value; } + + while (pixelIndex < numPixels) + { + max[pixelIndex] = 0.0; + min[pixelIndex] = 0.0; + + pixelIndex++; + } for (int i = 0; i < numPixels; i++) { - const auto lineY1 = centerY - max[i] * halfHeight * parameters.amplification; - const auto lineY2 = centerY - min[i] * halfHeight * parameters.amplification; - const auto lineX = i + x; + const int lineY1 = centerY - max[i] * halfHeight * parameters.amplification; + const int lineY2 = centerY - min[i] * halfHeight * parameters.amplification; + const int lineX = i + x; painter.drawLine(lineX, lineY1, lineX, lineY2); - const auto rms = std::sqrt(squared[i] / framesPerResolution); - const auto maxRMS = std::clamp(rms, min[i], max[i]); - const auto minRMS = std::clamp(-rms, min[i], max[i]); + const float rms = std::sqrt(squared[i] / framesPerResolution); + const float maxRMS = std::clamp(rms, min[i], max[i]); + const float minRMS = std::clamp(-rms, min[i], max[i]); - const auto rmsLineY1 = centerY - maxRMS * halfHeight * parameters.amplification; - const auto rmsLineY2 = centerY - minRMS * halfHeight * parameters.amplification; + const int rmsLineY1 = centerY - maxRMS * halfHeight * parameters.amplification; + const int rmsLineY2 = centerY - minRMS * halfHeight * parameters.amplification; painter.setPen(rmsColor); painter.drawLine(lineX, rmsLineY1, lineX, rmsLineY2); From 9bdc8adf33d1232f01746ea2966d9cab965105cb Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 5 May 2024 10:05:22 +0200 Subject: [PATCH 152/191] Inform effects about changes in enabled model (#7237) Add the virtual method `Effect::onEnabledChanged` which can be overridden by effects so that they can react to state changes with regards to being enabled or bypassed. The call of this methods is connected to state changes of the enabled model in the constructor of `Effect`. Implement the method in `DualFilterEffect` by resetting the history of the two filters. This is done to prevent pops that have been reported in #4612. --- include/Effect.h | 2 ++ plugins/DualFilter/DualFilter.cpp | 5 +++++ plugins/DualFilter/DualFilter.h | 2 ++ src/core/Effect.cpp | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/include/Effect.h b/include/Effect.h index 8b2ff81f0..7180e8275 100644 --- a/include/Effect.h +++ b/include/Effect.h @@ -208,6 +208,8 @@ protected: } void reinitSRC(); + virtual void onEnabledChanged() {} + private: EffectChain * m_parent; diff --git a/plugins/DualFilter/DualFilter.cpp b/plugins/DualFilter/DualFilter.cpp index 4e66db988..40afa342a 100644 --- a/plugins/DualFilter/DualFilter.cpp +++ b/plugins/DualFilter/DualFilter.cpp @@ -218,6 +218,11 @@ bool DualFilterEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames return isRunning(); } +void DualFilterEffect::onEnabledChanged() +{ + m_filter1->clearHistory(); + m_filter2->clearHistory(); +} diff --git a/plugins/DualFilter/DualFilter.h b/plugins/DualFilter/DualFilter.h index c179edbcc..29161039a 100644 --- a/plugins/DualFilter/DualFilter.h +++ b/plugins/DualFilter/DualFilter.h @@ -47,6 +47,8 @@ public: return &m_dfControls; } +protected: + void onEnabledChanged() override; private: DualFilterControls m_dfControls; diff --git a/src/core/Effect.cpp b/src/core/Effect.cpp index 7ede64e6b..aa6e56cd2 100644 --- a/src/core/Effect.cpp +++ b/src/core/Effect.cpp @@ -61,6 +61,10 @@ Effect::Effect( const Plugin::Descriptor * _desc, { m_autoQuitDisabled = true; } + + // Call the virtual method onEnabledChanged so that effects can react to changes, + // e.g. by resetting state. + connect(&m_enabledModel, &BoolModel::dataChanged, [this] { onEnabledChanged(); }); } From 9b6e33aa5c8b8dfdc1e3012767b12b13b5c33d53 Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 5 May 2024 04:37:43 -0400 Subject: [PATCH 153/191] Remove global oversampling (#7228) Oversampling can have many different effects to the audio signal such as latency, phase issues, clipping, smearing, etc, so this should really be an option on a per-plugin basis, not globally across all of LMMS (which, in some places, shouldn't really need to oversample at all but were oversampled anyways). --- include/AudioDevice.h | 12 --- include/AudioDummy.h | 2 +- include/AudioEngine.h | 55 +----------- include/BandLimitedWave.h | 2 +- include/Effect.h | 8 +- include/Oscillator.h | 2 +- include/Sample.h | 4 +- include/SampleBuffer.h | 4 +- include/SampleLoader.h | 2 +- .../AudioFileProcessor/AudioFileProcessor.cpp | 2 +- plugins/BassBooster/BassBooster.cpp | 2 +- plugins/BitInvader/BitInvader.cpp | 2 +- plugins/Bitcrush/Bitcrush.cpp | 4 +- plugins/CarlaBase/Carla.cpp | 2 +- plugins/Compressor/Compressor.cpp | 4 +- plugins/CrossoverEQ/CrossoverEQ.cpp | 4 +- plugins/Delay/DelayEffect.cpp | 12 +-- plugins/Dispersion/Dispersion.cpp | 2 +- plugins/DualFilter/DualFilter.cpp | 4 +- plugins/DualFilter/DualFilterControls.cpp | 4 +- .../DynamicsProcessor/DynamicsProcessor.cpp | 12 +-- plugins/Eq/EqCurve.cpp | 12 +-- plugins/Eq/EqEffect.cpp | 2 +- plugins/Eq/EqSpectrumView.cpp | 2 +- plugins/Flanger/FlangerEffect.cpp | 16 ++-- plugins/FreeBoy/FreeBoy.cpp | 2 +- plugins/GigPlayer/GigPlayer.cpp | 4 +- plugins/Kicker/Kicker.cpp | 4 +- plugins/LOMM/LOMM.cpp | 4 +- plugins/LadspaBrowser/LadspaPortDialog.cpp | 4 +- plugins/LadspaEffect/LadspaEffect.cpp | 6 +- plugins/Lb302/Lb302.cpp | 18 ++-- plugins/Monstro/Monstro.cpp | 2 +- plugins/MultitapEcho/MultitapEcho.cpp | 2 +- plugins/MultitapEcho/MultitapEchoControls.cpp | 2 +- plugins/Nes/Nes.cpp | 2 +- plugins/OpulenZ/OpulenZ.cpp | 4 +- plugins/Organic/Organic.cpp | 4 +- plugins/ReverbSC/ReverbSC.cpp | 12 +-- plugins/Sf2Player/Sf2Player.cpp | 8 +- plugins/Sfxr/Sfxr.cpp | 2 +- plugins/Sid/SidInstrument.cpp | 2 +- plugins/SlicerT/SlicerT.cpp | 4 +- plugins/SpectrumAnalyzer/SaProcessor.cpp | 4 +- plugins/Stk/Mallets/Mallets.cpp | 6 +- plugins/TripleOscillator/TripleOscillator.cpp | 4 +- plugins/Vibed/Vibed.cpp | 2 +- plugins/VstBase/VstPlugin.cpp | 2 +- plugins/Watsyn/Watsyn.cpp | 2 +- plugins/Xpressive/Xpressive.cpp | 8 +- plugins/ZynAddSubFx/ZynAddSubFx.cpp | 4 +- src/core/AudioEngine.cpp | 16 +--- src/core/Controller.cpp | 2 +- src/core/Engine.cpp | 2 +- src/core/EnvelopeAndLfoParameters.cpp | 4 +- src/core/Instrument.cpp | 2 +- src/core/InstrumentFunctions.cpp | 2 +- src/core/InstrumentSoundShaping.cpp | 2 +- src/core/LfoController.cpp | 2 +- src/core/Oscillator.cpp | 6 +- src/core/PeakController.cpp | 2 +- src/core/RemotePlugin.cpp | 2 +- src/core/RingBuffer.cpp | 8 +- src/core/Sample.cpp | 2 +- src/core/SampleClip.cpp | 2 +- src/core/SampleDecoder.cpp | 2 +- src/core/SamplePlayHandle.cpp | 2 +- src/core/VstSyncController.cpp | 2 +- src/core/audio/AudioDevice.cpp | 90 ++----------------- src/core/lv2/Lv2Proc.cpp | 8 +- src/core/main.cpp | 33 +------ src/gui/Lv2ViewBase.cpp | 2 +- src/gui/modals/ExportProjectDialog.cpp | 7 +- src/gui/modals/SetupDialog.cpp | 2 +- src/gui/modals/export_project.ui | 31 ------- 75 files changed, 155 insertions(+), 373 deletions(-) diff --git a/include/AudioDevice.h b/include/AudioDevice.h index def233a11..577bb7d0e 100644 --- a/include/AudioDevice.h +++ b/include/AudioDevice.h @@ -89,8 +89,6 @@ public: virtual void stopProcessing(); - void applyQualitySettings(); - protected: // subclasses can re-implement this for being used in conjunction with // processNextBuffer() @@ -110,13 +108,6 @@ protected: void clearS16Buffer( int_sample_t * _outbuf, const fpp_t _frames ); - // resample given buffer from samplerate _src_sr to samplerate _dst_sr - fpp_t resample( const surroundSampleFrame * _src, - const fpp_t _frames, - surroundSampleFrame * _dst, - const sample_rate_t _src_sr, - const sample_rate_t _dst_sr ); - inline void setSampleRate( const sample_rate_t _new_sr ) { m_sampleRate = _new_sr; @@ -142,9 +133,6 @@ private: QMutex m_devMutex; - SRC_DATA m_srcData; - SRC_STATE * m_srcState; - surroundSampleFrame * m_buffer; }; diff --git a/include/AudioDummy.h b/include/AudioDummy.h index e34260171..6907ad167 100644 --- a/include/AudioDummy.h +++ b/include/AudioDummy.h @@ -104,7 +104,7 @@ private: delete[] b; } - const int microseconds = static_cast( audioEngine()->framesPerPeriod() * 1000000.0f / audioEngine()->processingSampleRate() - timer.elapsed() ); + const int microseconds = static_cast( audioEngine()->framesPerPeriod() * 1000000.0f / audioEngine()->outputSampleRate() - timer.elapsed() ); if( microseconds > 0 ) { usleep( microseconds ); diff --git a/include/AudioEngine.h b/include/AudioEngine.h index 67c2edd86..e434b7f15 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -108,13 +108,6 @@ public: struct qualitySettings { - enum class Mode - { - Draft, - HighQuality, - FinalMix - } ; - enum class Interpolation { Linear, @@ -123,53 +116,11 @@ public: SincBest } ; - enum class Oversampling - { - None, - X2, - X4, - X8 - } ; - Interpolation interpolation; - Oversampling oversampling; - qualitySettings(Mode m) + qualitySettings(Interpolation i) : + interpolation(i) { - switch (m) - { - case Mode::Draft: - interpolation = Interpolation::Linear; - oversampling = Oversampling::None; - break; - case Mode::HighQuality: - interpolation = - Interpolation::SincFastest; - oversampling = Oversampling::X2; - break; - case Mode::FinalMix: - interpolation = Interpolation::SincBest; - oversampling = Oversampling::X8; - break; - } - } - - qualitySettings(Interpolation i, Oversampling o) : - interpolation(i), - oversampling(o) - { - } - - int sampleRateMultiplier() const - { - switch( oversampling ) - { - case Oversampling::None: return 1; - case Oversampling::X2: return 2; - case Oversampling::X4: return 4; - case Oversampling::X8: return 8; - } - return 1; } int libsrcInterpolation() const @@ -289,8 +240,6 @@ public: sample_rate_t baseSampleRate() const; sample_rate_t outputSampleRate() const; sample_rate_t inputSampleRate() const; - sample_rate_t processingSampleRate() const; - inline float masterGain() const { diff --git a/include/BandLimitedWave.h b/include/BandLimitedWave.h index 1f402aa6e..1c1a052ca 100644 --- a/include/BandLimitedWave.h +++ b/include/BandLimitedWave.h @@ -107,7 +107,7 @@ public: */ static inline float freqToLen( float f ) { - return freqToLen( f, Engine::audioEngine()->processingSampleRate() ); + return freqToLen( f, Engine::audioEngine()->outputSampleRate() ); } /*! \brief This method converts frequency to wavelength, but you can use any custom sample rate with it. diff --git a/include/Effect.h b/include/Effect.h index 7180e8275..c3745a352 100644 --- a/include/Effect.h +++ b/include/Effect.h @@ -111,7 +111,7 @@ public: inline f_cnt_t timeout() const { - const float samples = Engine::audioEngine()->processingSampleRate() * m_autoQuitModel.value() / 1000.0f; + const float samples = Engine::audioEngine()->outputSampleRate() * m_autoQuitModel.value() / 1000.0f; return 1 + ( static_cast( samples ) / Engine::audioEngine()->framesPerPeriod() ); } @@ -192,7 +192,7 @@ protected: sample_rate_t _dst_sr ) { resample( 0, _src_buf, - Engine::audioEngine()->processingSampleRate(), + Engine::audioEngine()->outputSampleRate(), _dst_buf, _dst_sr, Engine::audioEngine()->framesPerPeriod() ); } @@ -202,9 +202,9 @@ protected: sample_rate_t _src_sr ) { resample( 1, _src_buf, _src_sr, _dst_buf, - Engine::audioEngine()->processingSampleRate(), + Engine::audioEngine()->outputSampleRate(), Engine::audioEngine()->framesPerPeriod() * _src_sr / - Engine::audioEngine()->processingSampleRate() ); + Engine::audioEngine()->outputSampleRate() ); } void reinitSRC(); diff --git a/include/Oscillator.h b/include/Oscillator.h index a480bf524..ea0227bd0 100644 --- a/include/Oscillator.h +++ b/include/Oscillator.h @@ -204,7 +204,7 @@ public: control.f1 + 1 : 0; control.band = waveTableBandFromFreq( - m_freq * m_detuning_div_samplerate * Engine::audioEngine()->processingSampleRate()); + m_freq * m_detuning_div_samplerate * Engine::audioEngine()->outputSampleRate()); return control; } diff --git a/include/Sample.h b/include/Sample.h index 86fba1ddc..92ac1a58a 100644 --- a/include/Sample.h +++ b/include/Sample.h @@ -77,8 +77,8 @@ public: }; Sample() = default; - Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); - Sample(const sampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->outputSampleRate()); + Sample(const sampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->outputSampleRate()); Sample(const Sample& other); Sample(Sample&& other); explicit Sample(const QString& audioFile); diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 4089eb446..6f40b33d1 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -56,7 +56,7 @@ public: SampleBuffer(const QString& base64, int sampleRate); SampleBuffer(std::vector data, int sampleRate); SampleBuffer( - const sampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + const sampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->outputSampleRate()); friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept; auto toBase64() const -> QString; @@ -91,7 +91,7 @@ public: private: std::vector m_data; QString m_audioFile; - sample_rate_t m_sampleRate = Engine::audioEngine()->processingSampleRate(); + sample_rate_t m_sampleRate = Engine::audioEngine()->outputSampleRate(); }; } // namespace lmms diff --git a/include/SampleLoader.h b/include/SampleLoader.h index 7dbdbdc33..fd8f11357 100644 --- a/include/SampleLoader.h +++ b/include/SampleLoader.h @@ -39,7 +39,7 @@ public: static QString openWaveformFile(const QString& previousFile = ""); static std::shared_ptr createBufferFromFile(const QString& filePath); static std::shared_ptr createBufferFromBase64( - const QString& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); + const QString& base64, int sampleRate = Engine::audioEngine()->outputSampleRate()); private: static void displayError(const QString& message); }; diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 7cc5ee3fa..fbbcbca73 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -284,7 +284,7 @@ auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> int // Otherwise, use the remaining sample duration const auto baseFreq = instrumentTrack()->baseFreq(); const auto freqFactor = baseFreq / note->frequency() - * Engine::audioEngine()->processingSampleRate() + * Engine::audioEngine()->outputSampleRate() / Engine::audioEngine()->baseSampleRate(); const auto startFrame = m_nextPlayStartPoint >= m_sample.endFrame() diff --git a/plugins/BassBooster/BassBooster.cpp b/plugins/BassBooster/BassBooster.cpp index e6b25b0d1..aa0d3ebcd 100644 --- a/plugins/BassBooster/BassBooster.cpp +++ b/plugins/BassBooster/BassBooster.cpp @@ -118,7 +118,7 @@ bool BassBoosterEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames inline void BassBoosterEffect::changeFrequency() { - const sample_t fac = Engine::audioEngine()->processingSampleRate() / 44100.0f; + const sample_t fac = Engine::audioEngine()->outputSampleRate() / 44100.0f; m_bbFX.leftFX().setFrequency( m_bbControls.m_freqModel.value() * fac ); m_bbFX.rightFX().setFrequency( m_bbControls.m_freqModel.value() * fac ); diff --git a/plugins/BitInvader/BitInvader.cpp b/plugins/BitInvader/BitInvader.cpp index 4743e262d..21940182b 100644 --- a/plugins/BitInvader/BitInvader.cpp +++ b/plugins/BitInvader/BitInvader.cpp @@ -265,7 +265,7 @@ void BitInvader::playNote( NotePlayHandle * _n, const_cast( m_graph.samples() ), _n, m_interpolation.value(), factor, - Engine::audioEngine()->processingSampleRate() ); + Engine::audioEngine()->outputSampleRate() ); } const fpp_t frames = _n->framesLeftForCurrentPeriod(); diff --git a/plugins/Bitcrush/Bitcrush.cpp b/plugins/Bitcrush/Bitcrush.cpp index df4a8605d..251750a32 100644 --- a/plugins/Bitcrush/Bitcrush.cpp +++ b/plugins/Bitcrush/Bitcrush.cpp @@ -59,7 +59,7 @@ Plugin::Descriptor PLUGIN_EXPORT bitcrush_plugin_descriptor = BitcrushEffect::BitcrushEffect( Model * parent, const Descriptor::SubPluginFeatures::Key * key ) : Effect( &bitcrush_plugin_descriptor, parent, key ), m_controls( this ), - m_sampleRate( Engine::audioEngine()->processingSampleRate() ), + m_sampleRate( Engine::audioEngine()->outputSampleRate() ), m_filter( m_sampleRate ) { m_buffer = new sampleFrame[Engine::audioEngine()->framesPerPeriod() * OS_RATE]; @@ -83,7 +83,7 @@ BitcrushEffect::~BitcrushEffect() void BitcrushEffect::sampleRateChanged() { - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); m_filter.setSampleRate( m_sampleRate ); m_filter.setLowpass( m_sampleRate * ( CUTOFF_RATIO * OS_RATIO ) ); m_needsUpdate = true; diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index d96a1e4d4..197971ee9 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -259,7 +259,7 @@ uint32_t CarlaInstrument::handleGetBufferSize() const double CarlaInstrument::handleGetSampleRate() const { - return Engine::audioEngine()->processingSampleRate(); + return Engine::audioEngine()->outputSampleRate(); } bool CarlaInstrument::handleIsOffline() const diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp index e59562b02..91cc61029 100755 --- a/plugins/Compressor/Compressor.cpp +++ b/plugins/Compressor/Compressor.cpp @@ -56,7 +56,7 @@ CompressorEffect::CompressorEffect(Model* parent, const Descriptor::SubPluginFea Effect(&compressor_plugin_descriptor, parent, key), m_compressorControls(this) { - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); m_yL[0] = m_yL[1] = COMP_NOISE_FLOOR; @@ -560,7 +560,7 @@ inline void CompressorEffect::calcTiltFilter(sample_t inputSample, sample_t &out void CompressorEffect::changeSampleRate() { - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); m_coeffPrecalc = COMP_LOG / (m_sampleRate * 0.001f); diff --git a/plugins/CrossoverEQ/CrossoverEQ.cpp b/plugins/CrossoverEQ/CrossoverEQ.cpp index 4dca94a4c..6e59627f6 100644 --- a/plugins/CrossoverEQ/CrossoverEQ.cpp +++ b/plugins/CrossoverEQ/CrossoverEQ.cpp @@ -55,7 +55,7 @@ Plugin::Descriptor PLUGIN_EXPORT crossovereq_plugin_descriptor = CrossoverEQEffect::CrossoverEQEffect( Model* parent, const Descriptor::SubPluginFeatures::Key* key ) : Effect( &crossovereq_plugin_descriptor, parent, key ), m_controls( this ), - m_sampleRate( Engine::audioEngine()->processingSampleRate() ), + m_sampleRate( Engine::audioEngine()->outputSampleRate() ), m_lp1( m_sampleRate ), m_lp2( m_sampleRate ), m_lp3( m_sampleRate ), @@ -78,7 +78,7 @@ CrossoverEQEffect::~CrossoverEQEffect() void CrossoverEQEffect::sampleRateChanged() { - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); m_lp1.setSampleRate( m_sampleRate ); m_lp2.setSampleRate( m_sampleRate ); m_lp3.setSampleRate( m_sampleRate ); diff --git a/plugins/Delay/DelayEffect.cpp b/plugins/Delay/DelayEffect.cpp index 0050cd6fa..71f6fdf9a 100644 --- a/plugins/Delay/DelayEffect.cpp +++ b/plugins/Delay/DelayEffect.cpp @@ -58,8 +58,8 @@ DelayEffect::DelayEffect( Model* parent, const Plugin::Descriptor::SubPluginFeat m_delayControls( this ) { m_delay = 0; - m_delay = new StereoDelay( 20, Engine::audioEngine()->processingSampleRate() ); - m_lfo = new Lfo( Engine::audioEngine()->processingSampleRate() ); + m_delay = new StereoDelay( 20, Engine::audioEngine()->outputSampleRate() ); + m_lfo = new Lfo( Engine::audioEngine()->outputSampleRate() ); m_outGain = 1.0; } @@ -88,7 +88,7 @@ bool DelayEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) return( false ); } double outSum = 0.0; - const float sr = Engine::audioEngine()->processingSampleRate(); + const float sr = Engine::audioEngine()->outputSampleRate(); const float d = dryLevel(); const float w = wetLevel(); auto dryS = std::array{}; @@ -123,7 +123,7 @@ bool DelayEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) m_delay->setFeedback( *feedbackPtr ); m_lfo->setFrequency( *lfoTimePtr ); - m_currentLength = static_cast(*lengthPtr * Engine::audioEngine()->processingSampleRate()); + m_currentLength = static_cast(*lengthPtr * Engine::audioEngine()->outputSampleRate()); m_delay->setLength( m_currentLength + ( *amplitudePtr * ( float )m_lfo->tick() ) ); m_delay->tick( buf[f] ); @@ -151,8 +151,8 @@ bool DelayEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) void DelayEffect::changeSampleRate() { - m_lfo->setSampleRate( Engine::audioEngine()->processingSampleRate() ); - m_delay->setSampleRate( Engine::audioEngine()->processingSampleRate() ); + m_lfo->setSampleRate( Engine::audioEngine()->outputSampleRate() ); + m_delay->setSampleRate( Engine::audioEngine()->outputSampleRate() ); } diff --git a/plugins/Dispersion/Dispersion.cpp b/plugins/Dispersion/Dispersion.cpp index fb28e1f47..72e4c2103 100644 --- a/plugins/Dispersion/Dispersion.cpp +++ b/plugins/Dispersion/Dispersion.cpp @@ -52,7 +52,7 @@ Plugin::Descriptor PLUGIN_EXPORT dispersion_plugin_descriptor = DispersionEffect::DispersionEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : Effect(&dispersion_plugin_descriptor, parent, key), m_dispersionControls(this), - m_sampleRate(Engine::audioEngine()->processingSampleRate()), + m_sampleRate(Engine::audioEngine()->outputSampleRate()), m_amountVal(0) { } diff --git a/plugins/DualFilter/DualFilter.cpp b/plugins/DualFilter/DualFilter.cpp index 40afa342a..6f91bb251 100644 --- a/plugins/DualFilter/DualFilter.cpp +++ b/plugins/DualFilter/DualFilter.cpp @@ -57,8 +57,8 @@ DualFilterEffect::DualFilterEffect( Model* parent, const Descriptor::SubPluginFe Effect( &dualfilter_plugin_descriptor, parent, key ), m_dfControls( this ) { - m_filter1 = new BasicFilters<2>( Engine::audioEngine()->processingSampleRate() ); - m_filter2 = new BasicFilters<2>( Engine::audioEngine()->processingSampleRate() ); + m_filter1 = new BasicFilters<2>( Engine::audioEngine()->outputSampleRate() ); + m_filter2 = new BasicFilters<2>( Engine::audioEngine()->outputSampleRate() ); // ensure filters get updated m_filter1changed = true; diff --git a/plugins/DualFilter/DualFilterControls.cpp b/plugins/DualFilter/DualFilterControls.cpp index e862e6ae1..4a4091aa2 100644 --- a/plugins/DualFilter/DualFilterControls.cpp +++ b/plugins/DualFilter/DualFilterControls.cpp @@ -111,8 +111,8 @@ void DualFilterControls::updateFilters() delete m_effect->m_filter1; delete m_effect->m_filter2; - m_effect->m_filter1 = new BasicFilters<2>( Engine::audioEngine()->processingSampleRate() ); - m_effect->m_filter2 = new BasicFilters<2>( Engine::audioEngine()->processingSampleRate() ); + m_effect->m_filter1 = new BasicFilters<2>( Engine::audioEngine()->outputSampleRate() ); + m_effect->m_filter2 = new BasicFilters<2>( Engine::audioEngine()->outputSampleRate() ); // flag filters as needing recalculation diff --git a/plugins/DynamicsProcessor/DynamicsProcessor.cpp b/plugins/DynamicsProcessor/DynamicsProcessor.cpp index 583128bfb..a11cc28c6 100644 --- a/plugins/DynamicsProcessor/DynamicsProcessor.cpp +++ b/plugins/DynamicsProcessor/DynamicsProcessor.cpp @@ -64,8 +64,8 @@ DynProcEffect::DynProcEffect( Model * _parent, m_dpControls( this ) { m_currentPeak[0] = m_currentPeak[1] = DYN_NOISE_FLOOR; - m_rms[0] = new RmsHelper( 64 * Engine::audioEngine()->processingSampleRate() / 44100 ); - m_rms[1] = new RmsHelper( 64 * Engine::audioEngine()->processingSampleRate() / 44100 ); + m_rms[0] = new RmsHelper( 64 * Engine::audioEngine()->outputSampleRate() / 44100 ); + m_rms[1] = new RmsHelper( 64 * Engine::audioEngine()->outputSampleRate() / 44100 ); calcAttack(); calcRelease(); } @@ -82,12 +82,12 @@ DynProcEffect::~DynProcEffect() inline void DynProcEffect::calcAttack() { - m_attCoeff = std::exp((DNF_LOG / (m_dpControls.m_attackModel.value() * 0.001)) / Engine::audioEngine()->processingSampleRate()); + m_attCoeff = std::exp((DNF_LOG / (m_dpControls.m_attackModel.value() * 0.001)) / Engine::audioEngine()->outputSampleRate()); } inline void DynProcEffect::calcRelease() { - m_relCoeff = std::exp((DNF_LOG / (m_dpControls.m_releaseModel.value() * 0.001)) / Engine::audioEngine()->processingSampleRate()); + m_relCoeff = std::exp((DNF_LOG / (m_dpControls.m_releaseModel.value() * 0.001)) / Engine::audioEngine()->outputSampleRate()); } @@ -122,8 +122,8 @@ bool DynProcEffect::processAudioBuffer( sampleFrame * _buf, if( m_needsUpdate ) { - m_rms[0]->setSize( 64 * Engine::audioEngine()->processingSampleRate() / 44100 ); - m_rms[1]->setSize( 64 * Engine::audioEngine()->processingSampleRate() / 44100 ); + m_rms[0]->setSize( 64 * Engine::audioEngine()->outputSampleRate() / 44100 ); + m_rms[1]->setSize( 64 * Engine::audioEngine()->outputSampleRate() / 44100 ); calcAttack(); calcRelease(); m_needsUpdate = false; diff --git a/plugins/Eq/EqCurve.cpp b/plugins/Eq/EqCurve.cpp index 0493963b1..3ec8901e1 100644 --- a/plugins/Eq/EqCurve.cpp +++ b/plugins/Eq/EqCurve.cpp @@ -201,7 +201,7 @@ bool EqHandle::mousePressed() const float EqHandle::getPeakCurve( float x ) { double freqZ = xPixelToFreq( EqHandle::x(), m_width ); - const int SR = Engine::audioEngine()->processingSampleRate(); + const int SR = Engine::audioEngine()->outputSampleRate(); double w0 = 2 * LD_PI * freqZ / SR ; double c = cosf( w0 ); double s = sinf( w0 ); @@ -238,7 +238,7 @@ float EqHandle::getPeakCurve( float x ) float EqHandle::getHighShelfCurve( float x ) { double freqZ = xPixelToFreq( EqHandle::x(), m_width ); - const int SR = Engine::audioEngine()->processingSampleRate(); + const int SR = Engine::audioEngine()->outputSampleRate(); double w0 = 2 * LD_PI * freqZ / SR; double c = cosf( w0 ); double s = sinf( w0 ); @@ -274,7 +274,7 @@ float EqHandle::getHighShelfCurve( float x ) float EqHandle::getLowShelfCurve( float x ) { double freqZ = xPixelToFreq( EqHandle::x(), m_width ); - const int SR = Engine::audioEngine()->processingSampleRate(); + const int SR = Engine::audioEngine()->outputSampleRate(); double w0 = 2 * LD_PI * freqZ / SR ; double c = cosf( w0 ); double s = sinf( w0 ); @@ -310,7 +310,7 @@ float EqHandle::getLowShelfCurve( float x ) float EqHandle::getLowCutCurve( float x ) { double freqZ = xPixelToFreq( EqHandle::x(), m_width ); - const int SR = Engine::audioEngine()->processingSampleRate(); + const int SR = Engine::audioEngine()->outputSampleRate(); double w0 = 2 * LD_PI * freqZ / SR ; double c = cosf( w0 ); double s = sinf( w0 ); @@ -353,7 +353,7 @@ float EqHandle::getLowCutCurve( float x ) float EqHandle::getHighCutCurve( float x ) { double freqZ = xPixelToFreq( EqHandle::x(), m_width ); - const int SR = Engine::audioEngine()->processingSampleRate(); + const int SR = Engine::audioEngine()->outputSampleRate(); double w0 = 2 * LD_PI * freqZ / SR ; double c = cosf( w0 ); double s = sinf( w0 ); @@ -527,7 +527,7 @@ void EqHandle::setlp48() double EqHandle::calculateGain(const double freq, const double a1, const double a2, const double b0, const double b1, const double b2 ) { - const int SR = Engine::audioEngine()->processingSampleRate(); + const int SR = Engine::audioEngine()->outputSampleRate(); const double w = 2 * LD_PI * freq / SR ; const double PHI = pow( sin( w / 2 ), 2 ) * 4; diff --git a/plugins/Eq/EqEffect.cpp b/plugins/Eq/EqEffect.cpp index d8d2b2b29..b81512c1c 100644 --- a/plugins/Eq/EqEffect.cpp +++ b/plugins/Eq/EqEffect.cpp @@ -66,7 +66,7 @@ EqEffect::EqEffect( Model *parent, const Plugin::Descriptor::SubPluginFeatures:: bool EqEffect::processAudioBuffer( sampleFrame *buf, const fpp_t frames ) { - const int sampleRate = Engine::audioEngine()->processingSampleRate(); + const int sampleRate = Engine::audioEngine()->outputSampleRate(); //wet/dry controls const float dry = dryLevel(); diff --git a/plugins/Eq/EqSpectrumView.cpp b/plugins/Eq/EqSpectrumView.cpp index a90682b36..f91b3ebc3 100644 --- a/plugins/Eq/EqSpectrumView.cpp +++ b/plugins/Eq/EqSpectrumView.cpp @@ -102,7 +102,7 @@ void EqAnalyser::analyze( sampleFrame *buf, const fpp_t frames ) return; } - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); const int LOWEST_FREQ = 0; const int HIGHEST_FREQ = m_sampleRate / 2; diff --git a/plugins/Flanger/FlangerEffect.cpp b/plugins/Flanger/FlangerEffect.cpp index ddba0cb77..c06747137 100644 --- a/plugins/Flanger/FlangerEffect.cpp +++ b/plugins/Flanger/FlangerEffect.cpp @@ -58,9 +58,9 @@ FlangerEffect::FlangerEffect( Model *parent, const Plugin::Descriptor::SubPlugin Effect( &flanger_plugin_descriptor, parent, key ), m_flangerControls( this ) { - m_lfo = new QuadratureLfo( Engine::audioEngine()->processingSampleRate() ); - m_lDelay = new MonoDelay( 1, Engine::audioEngine()->processingSampleRate() ); - m_rDelay = new MonoDelay( 1, Engine::audioEngine()->processingSampleRate() ); + m_lfo = new QuadratureLfo( Engine::audioEngine()->outputSampleRate() ); + m_lDelay = new MonoDelay( 1, Engine::audioEngine()->outputSampleRate() ); + m_rDelay = new MonoDelay( 1, Engine::audioEngine()->outputSampleRate() ); m_noise = new Noise; } @@ -99,9 +99,9 @@ bool FlangerEffect::processAudioBuffer( sampleFrame *buf, const fpp_t frames ) double outSum = 0.0; const float d = dryLevel(); const float w = wetLevel(); - const float length = m_flangerControls.m_delayTimeModel.value() * Engine::audioEngine()->processingSampleRate(); + const float length = m_flangerControls.m_delayTimeModel.value() * Engine::audioEngine()->outputSampleRate(); const float noise = m_flangerControls.m_whiteNoiseAmountModel.value(); - float amplitude = m_flangerControls.m_lfoAmountModel.value() * Engine::audioEngine()->processingSampleRate(); + float amplitude = m_flangerControls.m_lfoAmountModel.value() * Engine::audioEngine()->outputSampleRate(); bool invertFeedback = m_flangerControls.m_invertFeedbackModel.value(); m_lfo->setFrequency( 1.0/m_flangerControls.m_lfoFrequencyModel.value() ); m_lfo->setOffset( m_flangerControls.m_lfoPhaseModel.value() / 180 * D_PI ); @@ -143,9 +143,9 @@ bool FlangerEffect::processAudioBuffer( sampleFrame *buf, const fpp_t frames ) void FlangerEffect::changeSampleRate() { - m_lfo->setSampleRate( Engine::audioEngine()->processingSampleRate() ); - m_lDelay->setSampleRate( Engine::audioEngine()->processingSampleRate() ); - m_rDelay->setSampleRate( Engine::audioEngine()->processingSampleRate() ); + m_lfo->setSampleRate( Engine::audioEngine()->outputSampleRate() ); + m_lDelay->setSampleRate( Engine::audioEngine()->outputSampleRate() ); + m_rDelay->setSampleRate( Engine::audioEngine()->outputSampleRate() ); } diff --git a/plugins/FreeBoy/FreeBoy.cpp b/plugins/FreeBoy/FreeBoy.cpp index 5a5581bc0..07497f6c0 100644 --- a/plugins/FreeBoy/FreeBoy.cpp +++ b/plugins/FreeBoy/FreeBoy.cpp @@ -231,7 +231,7 @@ float FreeBoyInstrument::desiredReleaseTimeMs() const void FreeBoyInstrument::playNote(NotePlayHandle* nph, sampleFrame* workingBuffer) { const f_cnt_t tfp = nph->totalFramesPlayed(); - const int samplerate = Engine::audioEngine()->processingSampleRate(); + const int samplerate = Engine::audioEngine()->outputSampleRate(); const fpp_t frames = nph->framesLeftForCurrentPeriod(); const f_cnt_t offset = nph->noteOffset(); diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index ecde81461..7c0d0d26b 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -323,7 +323,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) void GigInstrument::play( sampleFrame * _working_buffer ) { const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - const int rate = Engine::audioEngine()->processingSampleRate(); + const int rate = Engine::audioEngine()->outputSampleRate(); // Initialize to zeros std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); @@ -746,7 +746,7 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample ) if( gignote.midiNote >= keyLow && gignote.midiNote <= keyHigh ) { float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity ); - float length = (float) pSample->SamplesTotal / Engine::audioEngine()->processingSampleRate(); + float length = (float) pSample->SamplesTotal / Engine::audioEngine()->outputSampleRate(); // TODO: sample panning? crossfade different layers? diff --git a/plugins/Kicker/Kicker.cpp b/plugins/Kicker/Kicker.cpp index 5cbb18105..06385608f 100644 --- a/plugins/Kicker/Kicker.cpp +++ b/plugins/Kicker/Kicker.cpp @@ -160,7 +160,7 @@ void KickerInstrument::playNote( NotePlayHandle * _n, { const fpp_t frames = _n->framesLeftForCurrentPeriod(); const f_cnt_t offset = _n->noteOffset(); - const float decfr = m_decayModel.value() * Engine::audioEngine()->processingSampleRate() / 1000.0f; + const float decfr = m_decayModel.value() * Engine::audioEngine()->outputSampleRate() / 1000.0f; const f_cnt_t tfp = _n->totalFramesPlayed(); if (!_n->m_pluginData) @@ -184,7 +184,7 @@ void KickerInstrument::playNote( NotePlayHandle * _n, } auto so = static_cast(_n->m_pluginData); - so->update( _working_buffer + offset, frames, Engine::audioEngine()->processingSampleRate() ); + so->update( _working_buffer + offset, frames, Engine::audioEngine()->outputSampleRate() ); if( _n->isReleased() ) { diff --git a/plugins/LOMM/LOMM.cpp b/plugins/LOMM/LOMM.cpp index a0bd556ef..bffcfb9cb 100644 --- a/plugins/LOMM/LOMM.cpp +++ b/plugins/LOMM/LOMM.cpp @@ -50,7 +50,7 @@ extern "C" LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : Effect(&lomm_plugin_descriptor, parent, key), m_lommControls(this), - m_sampleRate(Engine::audioEngine()->processingSampleRate()), + m_sampleRate(Engine::audioEngine()->outputSampleRate()), m_lp1(m_sampleRate), m_lp2(m_sampleRate), m_hp1(m_sampleRate), @@ -76,7 +76,7 @@ LOMMEffect::LOMMEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* void LOMMEffect::changeSampleRate() { - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); m_lp1.setSampleRate(m_sampleRate); m_lp2.setSampleRate(m_sampleRate); m_hp1.setSampleRate(m_sampleRate); diff --git a/plugins/LadspaBrowser/LadspaPortDialog.cpp b/plugins/LadspaBrowser/LadspaPortDialog.cpp index e25679511..bf4d0038a 100644 --- a/plugins/LadspaBrowser/LadspaPortDialog.cpp +++ b/plugins/LadspaBrowser/LadspaPortDialog.cpp @@ -90,11 +90,11 @@ LadspaPortDialog::LadspaPortDialog( const ladspa_key_t & _key ) { if( min != NOHINT ) { - min *= Engine::audioEngine()->processingSampleRate(); + min *= Engine::audioEngine()->outputSampleRate(); } if( max != NOHINT ) { - max *= Engine::audioEngine()->processingSampleRate(); + max *= Engine::audioEngine()->outputSampleRate(); } } diff --git a/plugins/LadspaEffect/LadspaEffect.cpp b/plugins/LadspaEffect/LadspaEffect.cpp index 837bd554c..ccf92474b 100644 --- a/plugins/LadspaEffect/LadspaEffect.cpp +++ b/plugins/LadspaEffect/LadspaEffect.cpp @@ -143,13 +143,13 @@ bool LadspaEffect::processAudioBuffer( sampleFrame * _buf, sampleFrame * o_buf = nullptr; QVarLengthArray sBuf(_frames * DEFAULT_CHANNELS); - if( m_maxSampleRate < Engine::audioEngine()->processingSampleRate() ) + if( m_maxSampleRate < Engine::audioEngine()->outputSampleRate() ) { o_buf = _buf; _buf = reinterpret_cast(sBuf.data()); sampleDown( o_buf, _buf, m_maxSampleRate ); frames = _frames * m_maxSampleRate / - Engine::audioEngine()->processingSampleRate(); + Engine::audioEngine()->outputSampleRate(); } // Copy the LMMS audio buffer to the LADSPA input buffer and initialize @@ -587,7 +587,7 @@ sample_rate_t LadspaEffect::maxSamplerate( const QString & _name ) { return( __buggy_plugins[_name] ); } - return( Engine::audioEngine()->processingSampleRate() ); + return( Engine::audioEngine()->outputSampleRate() ); } diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index b85c26b0d..ff55d2665 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -72,7 +72,7 @@ namespace lmms { -//#define engine::audioEngine()->processingSampleRate() 44100.0f +//#define engine::audioEngine()->outputSampleRate() 44100.0f const float sampleRateCutoff = 44100.0f; extern "C" @@ -111,8 +111,8 @@ void Lb302Filter::recalc() { vcf_e1 = exp(6.109 + 1.5876*(fs->envmod) + 2.1553*(fs->cutoff) - 1.2*(1.0-(fs->reso))); vcf_e0 = exp(5.613 - 0.8*(fs->envmod) + 2.1553*(fs->cutoff) - 0.7696*(1.0-(fs->reso))); - vcf_e0*=M_PI/Engine::audioEngine()->processingSampleRate(); - vcf_e1*=M_PI/Engine::audioEngine()->processingSampleRate(); + vcf_e0*=M_PI/Engine::audioEngine()->outputSampleRate(); + vcf_e1*=M_PI/Engine::audioEngine()->outputSampleRate(); vcf_e1 -= vcf_e0; vcf_rescoeff = exp(-1.20 + 3.455*(fs->reso)); @@ -233,7 +233,7 @@ void Lb302Filter3Pole::envRecalc() #ifdef LB_24_IGNORE_ENVELOPE // kfcn = fs->cutoff; - kfcn = 2.0 * kfco / Engine::audioEngine()->processingSampleRate(); + kfcn = 2.0 * kfco / Engine::audioEngine()->outputSampleRate(); #else kfcn = w; #endif @@ -414,7 +414,7 @@ void Lb302Synth::filterChanged() float d = 0.2 + (2.3*vcf_dec_knob.value()); - d *= Engine::audioEngine()->processingSampleRate(); // d *= smpl rate + d *= Engine::audioEngine()->outputSampleRate(); // d *= smpl rate fs.envdecay = pow(0.1, 1.0/d * ENVINC); // decay is 0.1 to the 1/d * ENVINC // vcf_envdecay is now adjusted for both // sampling rate and ENVINC @@ -448,7 +448,7 @@ void Lb302Synth::recalcFilter() // THIS IS OLD 3pole/24dB code, I may reintegrate it. Don't need it // right now. Should be toggled by LB_24_RES_TRICK at the moment. - /*kfcn = 2.0 * (((vcf_cutoff*3000))) / engine::audioEngine()->processingSampleRate(); + /*kfcn = 2.0 * (((vcf_cutoff*3000))) / engine::audioEngine()->outputSampleRate(); kp = ((-2.7528*kfcn + 3.0429)*kfcn + 1.718)*kfcn - 0.9984; kp1 = kp+1.0; kp1h = 0.5*kp1; @@ -459,12 +459,12 @@ void Lb302Synth::recalcFilter() } inline float GET_INC(float freq) { - return freq/Engine::audioEngine()->processingSampleRate(); // TODO: Use actual sampling rate. + return freq/Engine::audioEngine()->outputSampleRate(); // TODO: Use actual sampling rate. } int Lb302Synth::process(sampleFrame *outbuf, const int size) { - const float sampleRatio = 44100.f / Engine::audioEngine()->processingSampleRate(); + const float sampleRatio = 44100.f / Engine::audioEngine()->outputSampleRate(); // Hold on to the current VCF, and use it throughout this period Lb302Filter *filter = vcf.loadAcquire(); @@ -648,7 +648,7 @@ int Lb302Synth::process(sampleFrame *outbuf, const int size) // Handle Envelope if(vca_mode==VcaMode::Attack) { vca_a+=(vca_a0-vca_a)*vca_attack; - if(sample_cnt>=0.5*Engine::audioEngine()->processingSampleRate()) + if(sample_cnt>=0.5*Engine::audioEngine()->outputSampleRate()) vca_mode = VcaMode::Idle; } else if(vca_mode == VcaMode::Decay) { diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index 65da50ea6..469b2da21 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -1447,7 +1447,7 @@ void MonstroInstrument::updateLFOAtts() void MonstroInstrument::updateSamplerate() { - m_samplerate = Engine::audioEngine()->processingSampleRate(); + m_samplerate = Engine::audioEngine()->outputSampleRate(); m_integrator = 0.5f - ( 0.5f - INTEGRATOR ) * 44100.0f / m_samplerate; m_fmCorrection = 44100.f / m_samplerate * FM_AMOUNT; diff --git a/plugins/MultitapEcho/MultitapEcho.cpp b/plugins/MultitapEcho/MultitapEcho.cpp index ff3ca828a..c64567f9b 100644 --- a/plugins/MultitapEcho/MultitapEcho.cpp +++ b/plugins/MultitapEcho/MultitapEcho.cpp @@ -55,7 +55,7 @@ MultitapEchoEffect::MultitapEchoEffect( Model* parent, const Descriptor::SubPlug m_stages( 1 ), m_controls( this ), m_buffer( 16100.0f ), - m_sampleRate( Engine::audioEngine()->processingSampleRate() ), + m_sampleRate( Engine::audioEngine()->outputSampleRate() ), m_sampleRatio( 1.0f / m_sampleRate ) { m_work = new sampleFrame[Engine::audioEngine()->framesPerPeriod()]; diff --git a/plugins/MultitapEcho/MultitapEchoControls.cpp b/plugins/MultitapEcho/MultitapEchoControls.cpp index 19564ba8a..4df05afc6 100644 --- a/plugins/MultitapEcho/MultitapEchoControls.cpp +++ b/plugins/MultitapEcho/MultitapEchoControls.cpp @@ -172,7 +172,7 @@ void MultitapEchoControls::lengthChanged() void MultitapEchoControls::sampleRateChanged() { - m_effect->m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_effect->m_sampleRate = Engine::audioEngine()->outputSampleRate(); m_effect->m_sampleRatio = 1.0f / m_effect->m_sampleRate; m_effect->updateFilters( 0, 19 ); } diff --git a/plugins/Nes/Nes.cpp b/plugins/Nes/Nes.cpp index df88c4942..c5cc3b0d0 100644 --- a/plugins/Nes/Nes.cpp +++ b/plugins/Nes/Nes.cpp @@ -552,7 +552,7 @@ void NesInstrument::playNote( NotePlayHandle * n, sampleFrame * workingBuffer ) if (!n->m_pluginData) { - auto nes = new NesObject(this, Engine::audioEngine()->processingSampleRate(), n); + auto nes = new NesObject(this, Engine::audioEngine()->outputSampleRate(), n); n->m_pluginData = nes; } diff --git a/plugins/OpulenZ/OpulenZ.cpp b/plugins/OpulenZ/OpulenZ.cpp index bf1459d6f..260ba353d 100644 --- a/plugins/OpulenZ/OpulenZ.cpp +++ b/plugins/OpulenZ/OpulenZ.cpp @@ -140,7 +140,7 @@ OpulenzInstrument::OpulenzInstrument( InstrumentTrack * _instrument_track ) : // Create an emulator - samplerate, 16 bit, mono emulatorMutex.lock(); - theEmulator = new CTemuopl(Engine::audioEngine()->processingSampleRate(), true, false); + theEmulator = new CTemuopl(Engine::audioEngine()->outputSampleRate(), true, false); theEmulator->init(); // Enable waveform selection theEmulator->write(0x01,0x20); @@ -231,7 +231,7 @@ OpulenzInstrument::~OpulenzInstrument() { void OpulenzInstrument::reloadEmulator() { delete theEmulator; emulatorMutex.lock(); - theEmulator = new CTemuopl(Engine::audioEngine()->processingSampleRate(), true, false); + theEmulator = new CTemuopl(Engine::audioEngine()->outputSampleRate(), true, false); theEmulator->init(); theEmulator->write(0x01,0x20); emulatorMutex.unlock(); diff --git a/plugins/Organic/Organic.cpp b/plugins/Organic/Organic.cpp index 2dba63629..e7b0cf792 100644 --- a/plugins/Organic/Organic.cpp +++ b/plugins/Organic/Organic.cpp @@ -605,10 +605,10 @@ void OscillatorObject::updateDetuning() { m_detuningLeft = powf( 2.0f, OrganicInstrument::s_harmonics[ static_cast( m_harmModel.value() ) ] + (float)m_detuneModel.value() * CENT ) / - Engine::audioEngine()->processingSampleRate(); + Engine::audioEngine()->outputSampleRate(); m_detuningRight = powf( 2.0f, OrganicInstrument::s_harmonics[ static_cast( m_harmModel.value() ) ] - (float)m_detuneModel.value() * CENT ) / - Engine::audioEngine()->processingSampleRate(); + Engine::audioEngine()->outputSampleRate(); } diff --git a/plugins/ReverbSC/ReverbSC.cpp b/plugins/ReverbSC/ReverbSC.cpp index 9006f8c9f..c73e421ec 100644 --- a/plugins/ReverbSC/ReverbSC.cpp +++ b/plugins/ReverbSC/ReverbSC.cpp @@ -55,7 +55,7 @@ ReverbSCEffect::ReverbSCEffect( Model* parent, const Descriptor::SubPluginFeatur m_reverbSCControls( this ) { sp_create(&sp); - sp->sr = Engine::audioEngine()->processingSampleRate(); + sp->sr = Engine::audioEngine()->outputSampleRate(); sp_revsc_create(&revsc); sp_revsc_init(sp, revsc); @@ -63,8 +63,8 @@ ReverbSCEffect::ReverbSCEffect( Model* parent, const Descriptor::SubPluginFeatur sp_dcblock_create(&dcblk[0]); sp_dcblock_create(&dcblk[1]); - sp_dcblock_init(sp, dcblk[0], Engine::audioEngine()->currentQualitySettings().sampleRateMultiplier() ); - sp_dcblock_init(sp, dcblk[1], Engine::audioEngine()->currentQualitySettings().sampleRateMultiplier() ); + sp_dcblock_init(sp, dcblk[0], 1); + sp_dcblock_init(sp, dcblk[1], 1); } ReverbSCEffect::~ReverbSCEffect() @@ -132,7 +132,7 @@ bool ReverbSCEffect::processAudioBuffer( sampleFrame* buf, const fpp_t frames ) void ReverbSCEffect::changeSampleRate() { // Change sr variable in Soundpipe. does not need to be destroyed - sp->sr = Engine::audioEngine()->processingSampleRate(); + sp->sr = Engine::audioEngine()->outputSampleRate(); mutex.lock(); sp_revsc_destroy(&revsc); @@ -145,8 +145,8 @@ void ReverbSCEffect::changeSampleRate() sp_dcblock_create(&dcblk[0]); sp_dcblock_create(&dcblk[1]); - sp_dcblock_init(sp, dcblk[0], Engine::audioEngine()->currentQualitySettings().sampleRateMultiplier() ); - sp_dcblock_init(sp, dcblk[1], Engine::audioEngine()->currentQualitySettings().sampleRateMultiplier() ); + sp_dcblock_init(sp, dcblk[0], 1); + sp_dcblock_init(sp, dcblk[1], 1); mutex.unlock(); } diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 9ded939d7..dd544dcf4 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -574,7 +574,7 @@ void Sf2Instrument::reloadSynth() double tempRate; // Set & get, returns the true sample rate - fluid_settings_setnum( m_settings, (char *) "synth.sample-rate", Engine::audioEngine()->processingSampleRate() ); + fluid_settings_setnum( m_settings, (char *) "synth.sample-rate", Engine::audioEngine()->outputSampleRate() ); fluid_settings_getnum( m_settings, (char *) "synth.sample-rate", &tempRate ); m_internalSampleRate = static_cast( tempRate ); @@ -616,7 +616,7 @@ void Sf2Instrument::reloadSynth() fluid_synth_set_interp_method( m_synth, -1, FLUID_INTERP_DEFAULT ); } m_synthMutex.unlock(); - if( m_internalSampleRate < Engine::audioEngine()->processingSampleRate() ) + if( m_internalSampleRate < Engine::audioEngine()->outputSampleRate() ) { m_synthMutex.lock(); if( m_srcState != nullptr ) @@ -872,10 +872,10 @@ void Sf2Instrument::renderFrames( f_cnt_t frames, sampleFrame * buf ) { m_synthMutex.lock(); fluid_synth_get_gain(m_synth); // This flushes voice updates as a side effect - if( m_internalSampleRate < Engine::audioEngine()->processingSampleRate() && + if( m_internalSampleRate < Engine::audioEngine()->outputSampleRate() && m_srcState != nullptr ) { - const fpp_t f = frames * m_internalSampleRate / Engine::audioEngine()->processingSampleRate(); + const fpp_t f = frames * m_internalSampleRate / Engine::audioEngine()->outputSampleRate(); #ifdef __GNUC__ sampleFrame tmp[f]; #else diff --git a/plugins/Sfxr/Sfxr.cpp b/plugins/Sfxr/Sfxr.cpp index e79b8e2ad..3817706fc 100644 --- a/plugins/Sfxr/Sfxr.cpp +++ b/plugins/Sfxr/Sfxr.cpp @@ -444,7 +444,7 @@ QString SfxrInstrument::nodeName() const void SfxrInstrument::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { - float currentSampleRate = Engine::audioEngine()->processingSampleRate(); + float currentSampleRate = Engine::audioEngine()->outputSampleRate(); fpp_t frameNum = _n->framesLeftForCurrentPeriod(); const f_cnt_t offset = _n->noteOffset(); diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index b646836b5..b745075aa 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -289,7 +289,7 @@ void SidInstrument::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { const int clockrate = C64_PAL_CYCLES_PER_SEC; - const int samplerate = Engine::audioEngine()->processingSampleRate(); + const int samplerate = Engine::audioEngine()->outputSampleRate(); if (!_n->m_pluginData) { diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 9bf667985..493dde6c3 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -88,7 +88,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } speedRatio *= pitchRatio; - speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()); + speedRatio *= Engine::audioEngine()->outputSampleRate() / static_cast(m_originalSample.sampleRate()); float sliceStart, sliceEnd; if (noteIndex == 0) // full sample at base note @@ -132,7 +132,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) playbackState->setNoteDone(nextNoteDone); // exponential fade out, applyRelease() not used since it extends the note length - int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate(); + int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->outputSampleRate(); int noteFramesLeft = noteLeft * m_originalSample.sampleSize() * speedRatio; for (int i = 0; i < frames; i++) { diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index c55acbdf6..b991ad3ea 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -53,7 +53,7 @@ SaProcessor::SaProcessor(const SaControls *controls) : m_terminate(false), m_inBlockSize(FFT_BLOCK_SIZES[0]), m_fftBlockSize(FFT_BLOCK_SIZES[0]), - m_sampleRate(Engine::audioEngine()->processingSampleRate()), + m_sampleRate(Engine::audioEngine()->outputSampleRate()), m_framesFilledUp(0), m_spectrumActive(false), m_waterfallActive(false), @@ -166,7 +166,7 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) #endif // update sample rate - m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_sampleRate = Engine::audioEngine()->outputSampleRate(); // apply FFT window for (unsigned int i = 0; i < m_inBlockSize; i++) diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index 3fb7fc0ff..7f4548ba4 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -342,7 +342,7 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, m_vibratoFreqModel.value(), p, (uint8_t) m_spreadModel.value(), - Engine::audioEngine()->processingSampleRate() ); + Engine::audioEngine()->outputSampleRate() ); } else if( p == 9 ) { @@ -355,7 +355,7 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, m_lfoSpeedModel.value(), m_adsrModel.value(), (uint8_t) m_spreadModel.value(), - Engine::audioEngine()->processingSampleRate() ); + Engine::audioEngine()->outputSampleRate() ); } else { @@ -368,7 +368,7 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, m_strikeModel.value() * 128.0, speed, (uint8_t) m_spreadModel.value(), - Engine::audioEngine()->processingSampleRate() ); + Engine::audioEngine()->outputSampleRate() ); } m.unlock(); static_cast(_n->m_pluginData)->setPresetIndex(p); diff --git a/plugins/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index d5f2e905f..97b773e67 100644 --- a/plugins/TripleOscillator/TripleOscillator.cpp +++ b/plugins/TripleOscillator/TripleOscillator.cpp @@ -177,7 +177,7 @@ void OscillatorObject::updateDetuningLeft() { m_detuningLeft = powf( 2.0f, ( (float)m_coarseModel.value() * 100.0f + (float)m_fineLeftModel.value() ) / 1200.0f ) - / Engine::audioEngine()->processingSampleRate(); + / Engine::audioEngine()->outputSampleRate(); } @@ -187,7 +187,7 @@ void OscillatorObject::updateDetuningRight() { m_detuningRight = powf( 2.0f, ( (float)m_coarseModel.value() * 100.0f + (float)m_fineRightModel.value() ) / 1200.0f ) - / Engine::audioEngine()->processingSampleRate(); + / Engine::audioEngine()->outputSampleRate(); } diff --git a/plugins/Vibed/Vibed.cpp b/plugins/Vibed/Vibed.cpp index 3dc4bfa56..f99c9140d 100644 --- a/plugins/Vibed/Vibed.cpp +++ b/plugins/Vibed/Vibed.cpp @@ -206,7 +206,7 @@ void Vibed::playNote(NotePlayHandle* n, sampleFrame* workingBuffer) if (!n->m_pluginData) { const auto newContainer = new StringContainer{n->frequency(), - Engine::audioEngine()->processingSampleRate(), s_sampleLength}; + Engine::audioEngine()->outputSampleRate(), s_sampleLength}; n->m_pluginData = newContainer; diff --git a/plugins/VstBase/VstPlugin.cpp b/plugins/VstBase/VstPlugin.cpp index 5dcd864f8..0361d4c25 100644 --- a/plugins/VstBase/VstPlugin.cpp +++ b/plugins/VstBase/VstPlugin.cpp @@ -338,7 +338,7 @@ void VstPlugin::updateSampleRate() { lock(); sendMessage( message( IdSampleRateInformation ). - addInt( Engine::audioEngine()->processingSampleRate() ) ); + addInt( Engine::audioEngine()->outputSampleRate() ) ); waitForMessage( IdInformationUpdated, true ); unlock(); } diff --git a/plugins/Watsyn/Watsyn.cpp b/plugins/Watsyn/Watsyn.cpp index 8e49942e1..822f9b519 100644 --- a/plugins/Watsyn/Watsyn.cpp +++ b/plugins/Watsyn/Watsyn.cpp @@ -332,7 +332,7 @@ void WatsynInstrument::playNote( NotePlayHandle * _n, if (!_n->m_pluginData) { auto w = new WatsynObject(&A1_wave[0], &A2_wave[0], &B1_wave[0], &B2_wave[0], m_amod.value(), m_bmod.value(), - Engine::audioEngine()->processingSampleRate(), _n, Engine::audioEngine()->framesPerPeriod(), this); + Engine::audioEngine()->outputSampleRate(), _n, Engine::audioEngine()->framesPerPeriod(), this); _n->m_pluginData = w; } diff --git a/plugins/Xpressive/Xpressive.cpp b/plugins/Xpressive/Xpressive.cpp index f738dc965..e90abb24c 100644 --- a/plugins/Xpressive/Xpressive.cpp +++ b/plugins/Xpressive/Xpressive.cpp @@ -204,14 +204,14 @@ void Xpressive::playNote(NotePlayHandle* nph, sampleFrame* working_buffer) { if (!nph->m_pluginData) { auto exprO1 = new ExprFront(m_outputExpression[0].constData(), - Engine::audioEngine()->processingSampleRate()); // give the "last" function a whole second - auto exprO2 = new ExprFront(m_outputExpression[1].constData(), Engine::audioEngine()->processingSampleRate()); + Engine::audioEngine()->outputSampleRate()); // give the "last" function a whole second + auto exprO2 = new ExprFront(m_outputExpression[1].constData(), Engine::audioEngine()->outputSampleRate()); auto init_expression_step1 = [this, nph](ExprFront* e) { //lambda function to init exprO1 and exprO2 //add the constants and the variables to the expression. e->add_constant("key", nph->key());//the key that was pressed. e->add_constant("bnote", nph->instrumentTrack()->baseNote()); // the base note - e->add_constant("srate", Engine::audioEngine()->processingSampleRate());// sample rate of the audio engine + e->add_constant("srate", Engine::audioEngine()->outputSampleRate());// sample rate of the audio engine e->add_constant("v", nph->getVolume() / 255.0); //volume of the note. e->add_constant("tempo", Engine::getSong()->getTempo());//tempo of the song. e->add_variable("A1", m_A1);//A1,A2,A3: general purpose input controls. @@ -225,7 +225,7 @@ void Xpressive::playNote(NotePlayHandle* nph, sampleFrame* working_buffer) { m_W2.setInterpolate(m_interpolateW2.value()); m_W3.setInterpolate(m_interpolateW3.value()); nph->m_pluginData = new ExprSynth(&m_W1, &m_W2, &m_W3, exprO1, exprO2, nph, - Engine::audioEngine()->processingSampleRate(), &m_panning1, &m_panning2, m_relTransition.value()); + Engine::audioEngine()->outputSampleRate(), &m_panning1, &m_panning2, m_relTransition.value()); } auto ps = static_cast(nph->m_pluginData); diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index 85b240380..a058c5b1e 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -451,7 +451,7 @@ void ZynAddSubFxInstrument::initPlugin() QDir( ConfigManager::inst()->factoryPresetsDir() + "/ZynAddSubFX" ).absolutePath() ) ) ); - m_remotePlugin->updateSampleRate( Engine::audioEngine()->processingSampleRate() ); + m_remotePlugin->updateSampleRate( Engine::audioEngine()->outputSampleRate() ); // temporary workaround until the VST synchronization feature gets stripped out of the RemotePluginClient class // causing not to send buffer size information requests @@ -463,7 +463,7 @@ void ZynAddSubFxInstrument::initPlugin() else { m_plugin = new LocalZynAddSubFx; - m_plugin->setSampleRate( Engine::audioEngine()->processingSampleRate() ); + m_plugin->setSampleRate( Engine::audioEngine()->outputSampleRate() ); m_plugin->setBufferSize( Engine::audioEngine()->framesPerPeriod() ); } diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 04e3c7c7c..31c4a3e5c 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -83,7 +83,7 @@ AudioEngine::AudioEngine( bool renderOnly ) : m_workers(), m_numWorkers( QThread::idealThreadCount()-1 ), m_newPlayHandles( PlayHandle::MaxNumber ), - m_qualitySettings( qualitySettings::Mode::Draft ), + m_qualitySettings(qualitySettings::Interpolation::Linear), m_masterGain( 1.0f ), m_audioDev( nullptr ), m_oldAudioDev( nullptr ), @@ -277,17 +277,6 @@ sample_rate_t AudioEngine::inputSampleRate() const baseSampleRate(); } - - - -sample_rate_t AudioEngine::processingSampleRate() const -{ - return outputSampleRate() * m_qualitySettings.sampleRateMultiplier(); -} - - - - bool AudioEngine::criticalXRuns() const { return cpuLoad() >= 99 && Engine::getSong()->isExporting() == false; @@ -459,7 +448,7 @@ const surroundSampleFrame *AudioEngine::renderNextBuffer() renderStageMix(); // STAGE 3: do master mix in mixer s_renderingThread = false; - m_profiler.finishPeriod(processingSampleRate(), m_framesPerPeriod); + m_profiler.finishPeriod(outputSampleRate(), m_framesPerPeriod); return m_outputBufferRead; } @@ -597,7 +586,6 @@ void AudioEngine::changeQuality(const struct qualitySettings & qs) stopProcessing(); m_qualitySettings = qs; - m_audioDev->applyQualitySettings(); emit sampleRateChanged(); emit qualitySettingsChanged(); diff --git a/src/core/Controller.cpp b/src/core/Controller.cpp index 81fc13c86..9218e3238 100644 --- a/src/core/Controller.cpp +++ b/src/core/Controller.cpp @@ -149,7 +149,7 @@ unsigned int Controller::runningFrames() // Get position in seconds float Controller::runningTime() { - return runningFrames() / Engine::audioEngine()->processingSampleRate(); + return runningFrames() / Engine::audioEngine()->outputSampleRate(); } diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp index 6c8104721..c1f609120 100644 --- a/src/core/Engine.cpp +++ b/src/core/Engine.cpp @@ -146,7 +146,7 @@ float Engine::framesPerTick(sample_rate_t sampleRate) void Engine::updateFramesPerTick() { - s_framesPerTick = s_audioEngine->processingSampleRate() * 60.0f * 4 / DefaultTicksPerBar / s_song->getTempo(); + s_framesPerTick = s_audioEngine->outputSampleRate() * 60.0f * 4 / DefaultTicksPerBar / s_song->getTempo(); } diff --git a/src/core/EnvelopeAndLfoParameters.cpp b/src/core/EnvelopeAndLfoParameters.cpp index 611700c51..861a62b51 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -410,7 +410,7 @@ void EnvelopeAndLfoParameters::updateSampleVars() QMutexLocker m(&m_paramMutex); const float frames_per_env_seg = SECS_PER_ENV_SEGMENT * - Engine::audioEngine()->processingSampleRate(); + Engine::audioEngine()->outputSampleRate(); // TODO: Remove the expKnobVals, time should be linear const auto predelay_frames = static_cast(frames_per_env_seg * expKnobVal(m_predelayModel.value())); @@ -509,7 +509,7 @@ void EnvelopeAndLfoParameters::updateSampleVars() const float frames_per_lfo_oscillation = SECS_PER_LFO_OSCILLATION * - Engine::audioEngine()->processingSampleRate(); + Engine::audioEngine()->outputSampleRate(); m_lfoPredelayFrames = static_cast( frames_per_lfo_oscillation * expKnobVal( m_lfoPredelayModel.value() ) ); m_lfoAttackFrames = static_cast( frames_per_lfo_oscillation * diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp index 3e884eaca..ca7ea7f25 100644 --- a/src/core/Instrument.cpp +++ b/src/core/Instrument.cpp @@ -205,7 +205,7 @@ float Instrument::computeReleaseTimeMsByFrameCount(f_cnt_t frames) const sample_rate_t Instrument::getSampleRate() const { - return Engine::audioEngine()->processingSampleRate(); + return Engine::audioEngine()->outputSampleRate(); } diff --git a/src/core/InstrumentFunctions.cpp b/src/core/InstrumentFunctions.cpp index 39c994ab6..549d658fc 100644 --- a/src/core/InstrumentFunctions.cpp +++ b/src/core/InstrumentFunctions.cpp @@ -369,7 +369,7 @@ void InstrumentFunctionArpeggio::processNote( NotePlayHandle * _n ) const int total_range = range * cnphv.size(); // number of frames that every note should be played - const auto arp_frames = (f_cnt_t)(m_arpTimeModel.value() / 1000.0f * Engine::audioEngine()->processingSampleRate()); + const auto arp_frames = (f_cnt_t)(m_arpTimeModel.value() / 1000.0f * Engine::audioEngine()->outputSampleRate()); const auto gated_frames = (f_cnt_t)(m_arpGateModel.value() * arp_frames / 100.0f); // used for calculating remaining frames for arp-note, we have to add diff --git a/src/core/InstrumentSoundShaping.cpp b/src/core/InstrumentSoundShaping.cpp index 2e00760cc..9a2185da1 100644 --- a/src/core/InstrumentSoundShaping.cpp +++ b/src/core/InstrumentSoundShaping.cpp @@ -158,7 +158,7 @@ void InstrumentSoundShaping::processAudioBuffer( sampleFrame* buffer, if( n->m_filter == nullptr ) { - n->m_filter = std::make_unique>( Engine::audioEngine()->processingSampleRate() ); + n->m_filter = std::make_unique>( Engine::audioEngine()->outputSampleRate() ); } n->m_filter->setFilterType( static_cast::FilterType>(m_filterModel.value()) ); diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 152e0ad8b..0c6d3d1ae 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -155,7 +155,7 @@ void LfoController::updatePhase() void LfoController::updateDuration() { - float newDurationF = Engine::audioEngine()->processingSampleRate() * m_speedModel.value(); + float newDurationF = Engine::audioEngine()->outputSampleRate() * m_speedModel.value(); switch(m_multiplierModel.value() ) { diff --git a/src/core/Oscillator.cpp b/src/core/Oscillator.cpp index 0330fad58..e45a3aa87 100644 --- a/src/core/Oscillator.cpp +++ b/src/core/Oscillator.cpp @@ -79,7 +79,7 @@ Oscillator::Oscillator(const IntModel *wave_shape_model, void Oscillator::update(sampleFrame* ab, const fpp_t frames, const ch_cnt_t chnl, bool modulator) { - if (m_freq >= Engine::audioEngine()->processingSampleRate() / 2) + if (m_freq >= Engine::audioEngine()->outputSampleRate() / 2) { BufferManager::clear(ab, frames); return; @@ -681,7 +681,7 @@ void Oscillator::updateFM( sampleFrame * _ab, const fpp_t _frames, m_subOsc->update( _ab, _frames, _chnl, true ); recalcPhase(); const float osc_coeff = m_freq * m_detuning_div_samplerate; - const float sampleRateCorrection = 44100.0f / Engine::audioEngine()->processingSampleRate(); + const float sampleRateCorrection = 44100.0f / Engine::audioEngine()->outputSampleRate(); for( fpp_t frame = 0; frame < _frames; ++frame ) { @@ -697,7 +697,7 @@ void Oscillator::updateFM( sampleFrame * _ab, const fpp_t _frames, template<> inline sample_t Oscillator::getSample(const float sample) { - const float current_freq = m_freq * m_detuning_div_samplerate * Engine::audioEngine()->processingSampleRate(); + const float current_freq = m_freq * m_detuning_div_samplerate * Engine::audioEngine()->outputSampleRate(); if (!m_useWaveTable || current_freq < OscillatorConstants::MAX_FREQ) { diff --git a/src/core/PeakController.cpp b/src/core/PeakController.cpp index cfcd3765c..1c38cf4cb 100644 --- a/src/core/PeakController.cpp +++ b/src/core/PeakController.cpp @@ -80,7 +80,7 @@ void PeakController::updateValueBuffer() { if( m_coeffNeedsUpdate ) { - const float ratio = 44100.0f / Engine::audioEngine()->processingSampleRate(); + const float ratio = 44100.0f / Engine::audioEngine()->outputSampleRate(); m_attackCoeff = 1.0f - powf( 2.0f, -0.3f * ( 1.0f - m_peakEffect->attackModel()->value() ) * ratio ); m_decayCoeff = 1.0f - powf( 2.0f, -0.3f * ( 1.0f - m_peakEffect->decayModel()->value() ) * ratio ); m_coeffNeedsUpdate = false; diff --git a/src/core/RemotePlugin.cpp b/src/core/RemotePlugin.cpp index 088bc3cd8..b46c547da 100644 --- a/src/core/RemotePlugin.cpp +++ b/src/core/RemotePlugin.cpp @@ -535,7 +535,7 @@ bool RemotePlugin::processMessage( const message & _m ) case IdSampleRateInformation: reply = true; - reply_message.addInt( Engine::audioEngine()->processingSampleRate() ); + reply_message.addInt( Engine::audioEngine()->outputSampleRate() ); break; case IdBufferSizeInformation: diff --git a/src/core/RingBuffer.cpp b/src/core/RingBuffer.cpp index 3f1ee7236..6cd3613ed 100644 --- a/src/core/RingBuffer.cpp +++ b/src/core/RingBuffer.cpp @@ -34,7 +34,7 @@ namespace lmms RingBuffer::RingBuffer( f_cnt_t size ) : m_fpp( Engine::audioEngine()->framesPerPeriod() ), - m_samplerate( Engine::audioEngine()->processingSampleRate() ), + m_samplerate( Engine::audioEngine()->outputSampleRate() ), m_size( size + m_fpp ) { m_buffer = new sampleFrame[ m_size ]; @@ -45,7 +45,7 @@ RingBuffer::RingBuffer( f_cnt_t size ) : RingBuffer::RingBuffer( float size ) : m_fpp( Engine::audioEngine()->framesPerPeriod() ), - m_samplerate( Engine::audioEngine()->processingSampleRate() ) + m_samplerate( Engine::audioEngine()->outputSampleRate() ) { m_size = msToFrames( size ) + m_fpp; m_buffer = new sampleFrame[ m_size ]; @@ -307,9 +307,9 @@ void RingBuffer::writeSwappedAddingMultiplied( sampleFrame * src, float offset, void RingBuffer::updateSamplerate() { - float newsize = static_cast( ( m_size - m_fpp ) * Engine::audioEngine()->processingSampleRate() ) / m_samplerate; + float newsize = static_cast( ( m_size - m_fpp ) * Engine::audioEngine()->outputSampleRate() ) / m_samplerate; m_size = static_cast( ceilf( newsize ) ) + m_fpp; - m_samplerate = Engine::audioEngine()->processingSampleRate(); + m_samplerate = Engine::audioEngine()->outputSampleRate(); delete[] m_buffer; m_buffer = new sampleFrame[ m_size ]; memset( m_buffer, 0, m_size * sizeof( sampleFrame ) ); diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp index a07b100bf..cd238b2ca 100644 --- a/src/core/Sample.cpp +++ b/src/core/Sample.cpp @@ -124,7 +124,7 @@ bool Sample::play(sampleFrame* dst, PlaybackState* state, size_t numFrames, floa const auto pastBounds = state->m_frameIndex >= m_endFrame || (state->m_frameIndex < 0 && state->m_backwards); if (loopMode == Loop::Off && pastBounds) { return false; } - const auto outputSampleRate = Engine::audioEngine()->processingSampleRate() * m_frequency / desiredFrequency; + const auto outputSampleRate = Engine::audioEngine()->outputSampleRate() * m_frequency / desiredFrequency; const auto inputSampleRate = m_buffer->sampleRate(); const auto resampleRatio = outputSampleRate / inputSampleRate; const auto marginSize = s_interpolationMargins[state->resampler().interpolationMode()]; diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 9a1c0731a..5ef001e20 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -307,7 +307,7 @@ void SampleClip::loadSettings( const QDomElement & _this ) if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) { auto sampleRate = _this.hasAttribute("sample_rate") ? _this.attribute("sample_rate").toInt() : - Engine::audioEngine()->processingSampleRate(); + Engine::audioEngine()->outputSampleRate(); auto buffer = gui::SampleLoader::createBufferFromBase64(_this.attribute("data"), sampleRate); m_sample = Sample(std::move(buffer)); diff --git a/src/core/SampleDecoder.cpp b/src/core/SampleDecoder.cpp index 94f8e387b..ec0fcc39a 100644 --- a/src/core/SampleDecoder.cpp +++ b/src/core/SampleDecoder.cpp @@ -101,7 +101,7 @@ auto decodeSampleDS(const QString& audioFile) -> std::optionalprocessingSampleRate(); + const auto engineRate = Engine::audioEngine()->outputSampleRate(); const auto frames = ds.GetDSFileSamples(audioFile, dataPtr, DEFAULT_CHANNELS, engineRate); const auto data = std::unique_ptr{dataPtr}; // NOLINT, we have to use a C-style array here diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp index 61ded132a..e23cfa473 100644 --- a/src/core/SamplePlayHandle.cpp +++ b/src/core/SamplePlayHandle.cpp @@ -145,7 +145,7 @@ bool SamplePlayHandle::isFromTrack( const Track * _track ) const f_cnt_t SamplePlayHandle::totalFrames() const { return (m_sample->endFrame() - m_sample->startFrame()) * - (static_cast(Engine::audioEngine()->processingSampleRate()) / m_sample->sampleRate()); + (static_cast(Engine::audioEngine()->outputSampleRate()) / m_sample->sampleRate()); } diff --git a/src/core/VstSyncController.cpp b/src/core/VstSyncController.cpp index c4b59eb6f..79344a5b5 100644 --- a/src/core/VstSyncController.cpp +++ b/src/core/VstSyncController.cpp @@ -155,7 +155,7 @@ void VstSyncController::updateSampleRate() { if (!m_syncData) { return; } - m_syncData->m_sampleRate = Engine::audioEngine()->processingSampleRate(); + m_syncData->m_sampleRate = Engine::audioEngine()->outputSampleRate(); #ifdef VST_SNC_LATENCY m_syncData->m_latency = m_syncData->m_bufferSize * m_syncData->m_bpm / ( (float) m_syncData->m_sampleRate * 60 ); diff --git a/src/core/audio/AudioDevice.cpp b/src/core/audio/AudioDevice.cpp index 80f6439b8..988230d5b 100644 --- a/src/core/audio/AudioDevice.cpp +++ b/src/core/audio/AudioDevice.cpp @@ -34,18 +34,11 @@ namespace lmms AudioDevice::AudioDevice( const ch_cnt_t _channels, AudioEngine* _audioEngine ) : m_supportsCapture( false ), - m_sampleRate( _audioEngine->processingSampleRate() ), + m_sampleRate( _audioEngine->outputSampleRate() ), m_channels( _channels ), m_audioEngine( _audioEngine ), m_buffer( new surroundSampleFrame[audioEngine()->framesPerPeriod()] ) { - int error; - if( ( m_srcState = src_new( - audioEngine()->currentQualitySettings().libsrcInterpolation(), - SURROUND_CHANNELS, &error ) ) == nullptr ) - { - printf( "Error: src_new() failed in audio_device.cpp!\n" ); - } } @@ -53,9 +46,7 @@ AudioDevice::AudioDevice( const ch_cnt_t _channels, AudioEngine* _audioEngine ) AudioDevice::~AudioDevice() { - src_delete( m_srcState ); delete[] m_buffer; - m_devMutex.tryLock(); unlock(); } @@ -73,39 +64,16 @@ void AudioDevice::processNextBuffer() } } - - - -fpp_t AudioDevice::getNextBuffer( surroundSampleFrame * _ab ) +fpp_t AudioDevice::getNextBuffer(surroundSampleFrame* _ab) { fpp_t frames = audioEngine()->framesPerPeriod(); - const surroundSampleFrame * b = audioEngine()->nextBuffer(); - if( !b ) - { - return 0; - } - // make sure, no other thread is accessing device - lock(); + const surroundSampleFrame* b = audioEngine()->nextBuffer(); + if (!b) { return 0; } - // resample if necessary - if( audioEngine()->processingSampleRate() != m_sampleRate ) - { - frames = resample( b, frames, _ab, audioEngine()->processingSampleRate(), m_sampleRate ); - } - else - { - memcpy( _ab, b, frames * sizeof( surroundSampleFrame ) ); - } - - // release lock - unlock(); - - if( audioEngine()->hasFifoWriter() ) - { - delete[] b; - } + memcpy(_ab, b, frames * sizeof(surroundSampleFrame)); + if (audioEngine()->hasFifoWriter()) { delete[] b; } return frames; } @@ -141,23 +109,6 @@ void AudioDevice::stopProcessingThread( QThread * thread ) - -void AudioDevice::applyQualitySettings() -{ - src_delete( m_srcState ); - - int error; - if( ( m_srcState = src_new( - audioEngine()->currentQualitySettings().libsrcInterpolation(), - SURROUND_CHANNELS, &error ) ) == nullptr ) - { - printf( "Error: src_new() failed in audio_device.cpp!\n" ); - } -} - - - - void AudioDevice::registerPort( AudioPort * ) { } @@ -176,35 +127,6 @@ void AudioDevice::renamePort( AudioPort * ) { } - - - -fpp_t AudioDevice::resample( const surroundSampleFrame * _src, - const fpp_t _frames, - surroundSampleFrame * _dst, - const sample_rate_t _src_sr, - const sample_rate_t _dst_sr ) -{ - if( m_srcState == nullptr ) - { - return _frames; - } - m_srcData.input_frames = _frames; - m_srcData.output_frames = _frames; - m_srcData.data_in = const_cast(_src[0].data()); - m_srcData.data_out = _dst[0].data (); - m_srcData.src_ratio = (double) _dst_sr / _src_sr; - m_srcData.end_of_input = 0; - if (int error = src_process(m_srcState, &m_srcData)) - { - printf( "AudioDevice::resample(): error while resampling: %s\n", - src_strerror( error ) ); - } - return static_cast(m_srcData.output_frames_gen); -} - - - int AudioDevice::convertToS16( const surroundSampleFrame * _ab, const fpp_t _frames, int_sample_t * _output_buffer, diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 4715aeb7f..682caea69 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -439,7 +439,7 @@ void Lv2Proc::initPlugin() m_features.createFeatureVectors(); m_instance = lilv_plugin_instantiate(m_plugin, - Engine::audioEngine()->processingSampleRate(), + Engine::audioEngine()->outputSampleRate(), m_features.featurePointers()); if (m_instance) @@ -507,7 +507,7 @@ void Lv2Proc::initMOptions() re-initialize, and this code section will be executed again, creating a new option vector. */ - float sampleRate = Engine::audioEngine()->processingSampleRate(); + float sampleRate = Engine::audioEngine()->outputSampleRate(); int32_t blockLength = Engine::audioEngine()->framesPerPeriod(); int32_t sequenceSize = defaultEvbufSize(); @@ -568,7 +568,7 @@ void Lv2Proc::createPort(std::size_t portNum) { AutoLilvNode node(lilv_port_get_name(m_plugin, lilvPort)); QString dispName = lilv_node_as_string(node.get()); - sample_rate_t sr = Engine::audioEngine()->processingSampleRate(); + sample_rate_t sr = Engine::audioEngine()->outputSampleRate(); if(meta.def() < meta.min(sr) || meta.def() > meta.max(sr)) { qWarning() << "Warning: Plugin" @@ -871,7 +871,7 @@ void Lv2Proc::dumpPort(std::size_t num) qDebug() << " visualization: " << Lv2Ports::toStr(port.m_vis); if (port.m_type == Lv2Ports::Type::Control || port.m_type == Lv2Ports::Type::Cv) { - sample_rate_t sr = Engine::audioEngine()->processingSampleRate(); + sample_rate_t sr = Engine::audioEngine()->outputSampleRate(); qDebug() << " default:" << port.def(); qDebug() << " min:" << port.min(sr); qDebug() << " max:" << port.max(sr); diff --git a/src/core/main.cpp b/src/core/main.cpp index abea43970..650ceab57 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -210,7 +210,6 @@ void printHelp() " -p, --profile Dump profiling information to file \n" " -s, --samplerate Specify output samplerate in Hz\n" " Range: 44100 (default) to 192000\n" - " -x, --oversampling Specify oversampling\n" " Possible values: 1, 2, 4, 8\n" " Default: 2\n\n", LMMS_VERSION, LMMS_PROJECT_COPYRIGHT ); @@ -361,7 +360,7 @@ int main( int argc, char * * argv ) new QCoreApplication( argc, argv ) : new gui::MainApplication(argc, argv); - AudioEngine::qualitySettings qs( AudioEngine::qualitySettings::Mode::HighQuality ); + AudioEngine::qualitySettings qs(AudioEngine::qualitySettings::Interpolation::Linear); OutputSettings os( 44100, OutputSettings::BitRateSettings(160, false), OutputSettings::BitDepth::Depth16Bit, OutputSettings::StereoMode::JointStereo ); ProjectRenderer::ExportFileFormat eff = ProjectRenderer::ExportFileFormat::Wave; @@ -646,36 +645,6 @@ int main( int argc, char * * argv ) return usageError( QString( "Invalid interpolation method %1" ).arg( argv[i] ) ); } } - else if( arg == "--oversampling" || arg == "-x" ) - { - ++i; - - if( i == argc ) - { - return usageError( "No oversampling specified" ); - } - - - int o = QString( argv[i] ).toUInt(); - - switch( o ) - { - case 1: - qs.oversampling = AudioEngine::qualitySettings::Oversampling::None; - break; - case 2: - qs.oversampling = AudioEngine::qualitySettings::Oversampling::X2; - break; - case 4: - qs.oversampling = AudioEngine::qualitySettings::Oversampling::X4; - break; - case 8: - qs.oversampling = AudioEngine::qualitySettings::Oversampling::X8; - break; - default: - return usageError( QString( "Invalid oversampling %1" ).arg( argv[i] ) ); - } - } else if( arg == "--import" ) { ++i; diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 4fcf6b77b..b93788ea8 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -74,7 +74,7 @@ Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* proc, int colNum) : break; case PortVis::Integer: { - sample_rate_t sr = Engine::audioEngine()->processingSampleRate(); + sample_rate_t sr = Engine::audioEngine()->outputSampleRate(); auto pMin = port.min(sr); auto pMax = port.max(sr); int numDigits = std::max(numDigitsAsInt(pMin), numDigitsAsInt(pMax)); diff --git a/src/gui/modals/ExportProjectDialog.cpp b/src/gui/modals/ExportProjectDialog.cpp index fe39082e4..8dfda4981 100644 --- a/src/gui/modals/ExportProjectDialog.cpp +++ b/src/gui/modals/ExportProjectDialog.cpp @@ -154,11 +154,8 @@ OutputSettings::StereoMode mapToStereoMode(int index) void ExportProjectDialog::startExport() { - AudioEngine::qualitySettings qs = - AudioEngine::qualitySettings( - static_cast(interpolationCB->currentIndex()), - static_cast(oversamplingCB->currentIndex()) ); - + auto qs = AudioEngine::qualitySettings( + static_cast(interpolationCB->currentIndex())); const auto samplerates = std::array{44100, 48000, 88200, 96000, 192000}; const auto bitrates = std::array{64, 128, 160, 192, 256, 320}; diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 9e2b1fe4a..6f05433b7 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -1223,7 +1223,7 @@ void SetupDialog::setBufferSize(int value) m_bufferSize = value * BUFFERSIZE_RESOLUTION; m_bufferSizeLbl->setText(tr("Frames: %1\nLatency: %2 ms").arg(m_bufferSize).arg( - 1000.0f * m_bufferSize / Engine::audioEngine()->processingSampleRate(), 0, 'f', 1)); + 1000.0f * m_bufferSize / Engine::audioEngine()->outputSampleRate(), 0, 'f', 1)); updateBufferSizeWarning(m_bufferSize); } diff --git a/src/gui/modals/export_project.ui b/src/gui/modals/export_project.ui index 6b175de78..797ae0790 100644 --- a/src/gui/modals/export_project.ui +++ b/src/gui/modals/export_project.ui @@ -404,37 +404,6 @@ - - - - Oversampling: - - - - - - - - 1x (None) - - - - - 2x - - - - - 4x - - - - - 8x - - - - From 20102c4ae47afa3aee8f92b1728c559ef7c455d3 Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 5 May 2024 05:44:25 -0400 Subject: [PATCH 154/191] Replace processingSampleRate with outputSampleRate in Lb302 (#7239) --- plugins/Lb302/Lb302.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index ff55d2665..5dfbd5992 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -271,7 +271,7 @@ float Lb302Filter3Pole::process(const float& samp) static float computeDecayFactor(float decayTimeInSeconds, float targetedAttenuation) { // This is the number of samples that correspond to the decay time in seconds - auto samplesNeededForDecay = decayTimeInSeconds * Engine::audioEngine()->processingSampleRate(); + auto samplesNeededForDecay = decayTimeInSeconds * Engine::audioEngine()->outputSampleRate(); // This computes the factor that's needed to make a signal with a value of 1 decay to the // targeted attenuation over the time in number of samples. From acefd06f9baca67ef4cc68c6dc0d2072e96f15e6 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Wed, 8 May 2024 01:06:03 -0400 Subject: [PATCH 155/191] Bump veal submodule (#6771) - Add MSVC support - Sync upstream changes --- plugins/LadspaEffect/calf/veal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/LadspaEffect/calf/veal b/plugins/LadspaEffect/calf/veal index fe628885b..422168dc6 160000 --- a/plugins/LadspaEffect/calf/veal +++ b/plugins/LadspaEffect/calf/veal @@ -1 +1 @@ -Subproject commit fe628885b761372b37136a3f2b7c3d56e179e3ba +Subproject commit 422168dc670bccdba14cfac2946f6ad413bce9ba From d71116b82a0a20355eb0978b8fb5379a368ba2af Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Wed, 8 May 2024 13:06:54 -0400 Subject: [PATCH 156/191] Update Linux CI to Ubuntu 20.04 (#7015) - Switch to Ubuntu 20.04 Docker image ghcr.io/lmms/linux.gcc:20.04 - Linux packages have migrated from Docker Hub to https://github.com/orgs/lmms/packages - Built using the Dockerfiles from Update Linux images lmms-ci-docker#15 - Updated the veal submodule to the latest commit on the default ladspa branch - Fixed an error when catching a polymorphic type with GCC 9. See: LMMS/veal@0ae9287 - Added GCC flag -Wno-format-truncation for ZynAddSubFx build. - Adds GCC flag -Wno-format-overflow for calf/veal build. Closes #6993 --- .github/workflows/build.yml | 12 ++++-------- plugins/LadspaEffect/calf/CMakeLists.txt | 4 +++- plugins/ZynAddSubFx/CMakeLists.txt | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 546fb017e..1926b593b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: linux: name: linux runs-on: ubuntu-latest - container: lmmsci/linux.gcc:18.04 + container: ghcr.io/lmms/linux.gcc:20.04 env: CMAKE_OPTS: >- -DUSE_WERROR=ON @@ -18,12 +18,8 @@ jobs: CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j2 steps: - - name: Update and configure Git - run: | - add-apt-repository ppa:git-core/ppa - apt-get update - apt-get --yes install git - git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Configure git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Check out uses: actions/checkout@v3 with: @@ -62,7 +58,7 @@ jobs: run: | ccache --cleanup echo "[ccache config]" - ccache --print-config + ccache --show-config echo "[ccache stats]" ccache --show-stats env: diff --git a/plugins/LadspaEffect/calf/CMakeLists.txt b/plugins/LadspaEffect/calf/CMakeLists.txt index 0c9cd8fa9..67bdc5cd2 100644 --- a/plugins/LadspaEffect/calf/CMakeLists.txt +++ b/plugins/LadspaEffect/calf/CMakeLists.txt @@ -35,10 +35,12 @@ SET_TARGET_PROPERTIES(veal PROPERTIES PREFIX "") TARGET_COMPILE_DEFINITIONS(veal PRIVATE DISABLE_OSC=1) SET(INLINE_FLAGS "") +SET(OTHER_FLAGS "") IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") SET(INLINE_FLAGS -finline-functions-called-once -finline-limit=80) + SET(OTHER_FLAGS -Wno-format-overflow) ENDIF() -target_compile_options(veal PRIVATE -fexceptions -O2 -finline-functions ${INLINE_FLAGS}) +target_compile_options(veal PRIVATE -fexceptions -O2 -finline-functions ${INLINE_FLAGS} ${OTHER_FLAGS}) if(LMMS_BUILD_WIN32) add_custom_command( diff --git a/plugins/ZynAddSubFx/CMakeLists.txt b/plugins/ZynAddSubFx/CMakeLists.txt index 3369a7938..35dc08c4d 100644 --- a/plugins/ZynAddSubFx/CMakeLists.txt +++ b/plugins/ZynAddSubFx/CMakeLists.txt @@ -29,7 +29,7 @@ if(NOT MSVC) endif() IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND NOT "${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "6.0.0") - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-misleading-indentation") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-misleading-indentation -Wno-format-truncation") ENDIF() IF(MINGW_PREFIX) From b71d4f2aab5d049fa29793b2a4f0d0e2585ffe6b Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Thu, 9 May 2024 15:25:51 +0200 Subject: [PATCH 157/191] Remove demo project Greshz-CoolSnip.mmpz (#7248) --- data/projects/shorties/Greshz-CoolSnip.mmpz | Bin 3889 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 data/projects/shorties/Greshz-CoolSnip.mmpz diff --git a/data/projects/shorties/Greshz-CoolSnip.mmpz b/data/projects/shorties/Greshz-CoolSnip.mmpz deleted file mode 100644 index dadeef1ba730184da82078e242f6657132d2a761..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3889 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 Date: Sat, 11 May 2024 21:40:54 +0200 Subject: [PATCH 158/191] Update zynaddsubfx (#7250) Update instrument submodule --- plugins/ZynAddSubFx/zynaddsubfx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ZynAddSubFx/zynaddsubfx b/plugins/ZynAddSubFx/zynaddsubfx index 7ad5663cb..aac04bc55 160000 --- a/plugins/ZynAddSubFx/zynaddsubfx +++ b/plugins/ZynAddSubFx/zynaddsubfx @@ -1 +1 @@ -Subproject commit 7ad5663cbeebc02d73fd3ad666e428c1287f2cda +Subproject commit aac04bc55c4114460009897686b597f98adfaa65 From 6d100d1bbaddf15a4de23eef2bf784556b87fd87 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Sun, 12 May 2024 01:35:10 -0400 Subject: [PATCH 159/191] Remove support for msys2 (#7251) --- CMakeLists.txt | 10 -- cmake/modules/BuildPlugin.cmake | 4 - cmake/modules/DetectMachine.cmake | 5 - cmake/msys/extract_debs.sh | 19 --- cmake/msys/fetch_ppa.sh | 53 ------- cmake/msys/msys_helper.sh | 226 ----------------------------- cmake/nsis/CMakeLists.txt | 19 --- cmake/toolchains/MSYS-32.cmake | 4 - cmake/toolchains/MSYS-64.cmake | 5 - cmake/toolchains/common/MSYS.cmake | 22 --- src/CMakeLists.txt | 8 +- 11 files changed, 1 insertion(+), 374 deletions(-) delete mode 100644 cmake/msys/extract_debs.sh delete mode 100644 cmake/msys/fetch_ppa.sh delete mode 100644 cmake/msys/msys_helper.sh delete mode 100644 cmake/toolchains/MSYS-32.cmake delete mode 100644 cmake/toolchains/MSYS-64.cmake delete mode 100644 cmake/toolchains/common/MSYS.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 48d1904f0..2e0158e75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,16 +145,6 @@ ELSE(LMMS_BUILD_WIN32) SET(STATUS_WINMM "") ENDIF(LMMS_BUILD_WIN32) - -# TODO: Fix linking issues with msys debug builds -IF(LMMS_BUILD_MSYS AND CMAKE_BUILD_TYPE STREQUAL "Debug") - SET(WANT_GIG OFF) - SET(WANT_STK OFF) - SET(WANT_SWH OFF) - SET(STATUS_GIG "not built as requested") - SET(STATUS_STK "not built as requested") -ENDIF() - SET(CMAKE_CXX_STANDARD_REQUIRED ON) CHECK_INCLUDE_FILES(pthread.h LMMS_HAVE_PTHREAD_H) diff --git a/cmake/modules/BuildPlugin.cmake b/cmake/modules/BuildPlugin.cmake index 111bdf36b..aaccd3c4b 100644 --- a/cmake/modules/BuildPlugin.cmake +++ b/cmake/modules/BuildPlugin.cmake @@ -44,10 +44,6 @@ MACRO(BUILD_PLUGIN PLUGIN_NAME) LINK_DIRECTORIES("${CMAKE_BINARY_DIR}" "${CMAKE_SOURCE_DIR}") LINK_LIBRARIES(${QT_LIBRARIES}) ENDIF(LMMS_BUILD_WIN32) - IF(LMMS_BUILD_MSYS AND CMAKE_BUILD_TYPE STREQUAL "Debug") - # Override Qt debug libraries with release versions - SET(QT_LIBRARIES "${QT_OVERRIDE_LIBRARIES}") - ENDIF() IF (NOT PLUGIN_LINK) SET(PLUGIN_LINK "MODULE") diff --git a/cmake/modules/DetectMachine.cmake b/cmake/modules/DetectMachine.cmake index 65bc6d2b7..b9aa4c8c6 100644 --- a/cmake/modules/DetectMachine.cmake +++ b/cmake/modules/DetectMachine.cmake @@ -12,11 +12,6 @@ ELSE() SET(LMMS_BUILD_LINUX 1) ENDIF(WIN32) -# LMMS_BUILD_MSYS also set in build_winXX.sh -IF(LMMS_BUILD_WIN32 AND CMAKE_COMPILER_IS_GNUCXX AND DEFINED ENV{MSYSCON}) - SET(LMMS_BUILD_MSYS TRUE) -ENDIF() - MESSAGE("PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}") SET(LMMS_HOST_X86 FALSE) SET(LMMS_HOST_X86_64 FALSE) diff --git a/cmake/msys/extract_debs.sh b/cmake/msys/extract_debs.sh deleted file mode 100644 index 939912bb2..000000000 --- a/cmake/msys/extract_debs.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -ppa_dir=./ppa/ - -pushd $ppa_dir - -for f in *.deb; do - echo "Extracting $f..." - ar xv "$f" - rm debian-binary - rm control.tar.* - tar xf data.tar.* --exclude=*mingw*/bin/fluid - rm data.tar.* -done - -popd - -echo "Your extracted files should be located in $ppa_dir" diff --git a/cmake/msys/fetch_ppa.sh b/cmake/msys/fetch_ppa.sh deleted file mode 100644 index ba8697c81..000000000 --- a/cmake/msys/fetch_ppa.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -# Trusty=14.04, Precise=12.04 -PPA_DISTRO=trusty - -# Architecture=i386, amd64 -PPA_ARCH=amd64 - -# These shouldn't change -PPA_HOST=http://ppa.launchpad.net -PPA_USER=tobydox -PPA_PROJECT=mingw-x-trusty -PPA_ROOT=$PPA_HOST/$PPA_USER/$PPA_PROJECT/ubuntu - -PPA_URL=$PPA_ROOT/dists/$PPA_DISTRO/main/binary-$PPA_ARCH/Packages.gz - -ppa_dir=./ppa/ - -temp_file=/tmp/ppa_listing_$$ -temp_temp_file=/tmp/ppa_listing_temp_$$ - -skip_files="binutils openssl flac libgig libogg libvorbis x-bootstrap zlib" -skip_files="$skip_files x-runtime gcc qt_4 qt5 x-stk pkgconfig" -skip_files="$skip_files glib2 libpng" - -echo "Connecting to $PPA_URL to get list of packages..." -wget -qO- $PPA_URL | gzip -d -c | grep "Filename:" > $temp_file - -for j in $skip_files ; do - grep -v "$j" $temp_file > $temp_temp_file - mv $temp_temp_file $temp_file -done - -line_count=$(wc -l $temp_file |awk '{print $1}') - -echo "Found $line_count packages for download..." - -echo "Downloading packages. They will be saved to $ppa_dir" - -mkdir $ppa_dir - -while read -r j -do - echo "Downloading $j..." - echo "$PPA_ROOT/$j" - wget -qO "$ppa_dir$(basename "$j")" "$(echo "$PPA_ROOT/$j" | sed 's/\/Filename: /\//gi')" -done < $temp_file - - -echo "Cleaning up temporary files..." -rm -rf $temp_file - -echo "Packages have been saved to $ppa_dir. Please run extract_debs.sh" diff --git a/cmake/msys/msys_helper.sh b/cmake/msys/msys_helper.sh deleted file mode 100644 index a6a7e6aae..000000000 --- a/cmake/msys/msys_helper.sh +++ /dev/null @@ -1,226 +0,0 @@ -#!/bin/bash - -set -eu - -# Git repo information -fork="lmms" # i.e. "lmms" or "tobydox" -branch="master" # i.e. "master" or "stable-1.2" - -# Console colors -red="\\x1B[1;31m" -green="\\x1B[1;32m" -yellow="\\x1B[1;33m" -plain="\\x1B[0m" - -function info() { echo -e "\n${green}$1${plain}"; } -function warn() { echo -e "\n${yellow}$1${plain}"; } -function err() { echo -e "\n${red}$1${plain}"; exit 1;} - -info "Checking for mingw environment" -if ! env | grep MINGW; then - err " - Failed. Please relaunch using MinGW shell" -fi - -info "Preparing the git directory..." -mkdir "$HOME/.git" || true -touch "$HOME/.git/config" > /dev/null 2>&1 -git config --global http.sslverify false - -info "Cloning the repository..." -if [ -d ./lmms ]; then - warn " - Skipping, ./lmms already exists" -else - git clone -b $branch https://github.com/$fork/lmms.git -fi - -info "Fetching ppa using cmake/msys/fetch_ppas.sh..." -if [ -d "$HOME/ppa" ]; then - warn " - Skipping, $HOME/ppa already exists" -else - lmms/cmake/msys/fetch_ppa.sh -fi - -info "Extracting debs to $HOME/ppa/opt/, etc..." -if [ -d "$HOME/ppa/opt" ]; then - warn " - Skipping, $HOME/ppa/opt already exists" -else - lmms/cmake/msys/extract_debs.sh -fi - -info "Preparing library merge, making all qt headers writable..." -chmod u+w /mingw64/include/qt4 -R -chmod u+w /mingw32/include/qt4 -R - -info "Merging mingw headers and libraries from ppa over existing system libraries..." -if ! find /mingw64 | grep sndfile.h; then - command cp -r "$HOME/ppa/opt/mingw"* / -else - warn " - Skipping, sndfile.h has already been merged" -fi - -fltkver="1.3.3" -oggver="1.3.2" -vorbisver="1.3.5" -flacver="1.3.2" -gigver="4.0.0" -stkver="4.5.1" - -mingw_root="/$(echo "$MSYSTEM"|tr '[:upper:]' '[:lower:]')" - -info "Downloading and building fltk $fltkver" -if ! command -v fluid; then - wget http://fltk.org/pub/fltk/$fltkver/fltk-$fltkver-source.tar.gz -O "$HOME/fltk-source.tar.gz" - tar zxf "$HOME/fltk-source.tar.gz" -C "$HOME/" - pushd "$HOME/fltk-$fltkver" - - info " - Compiling fltk $fltkver..." - ./configure --prefix="$mingw_root" --enable-shared - make - - info " - Installing fltk..." - make install - -# ln -s $mingw_root/usr/local/bin/fluid.exe $mingw_root/bin/fluid.exe - popd -else - warn " - Skipping, fluid binary already exists" -fi - -info "Downloading and building libogg $oggver" -if [ ! -e "$mingw_root/lib/libogg.dll.a" ]; then - wget http://downloads.xiph.org/releases/ogg/libogg-$oggver.tar.xz -O "$HOME/libogg-source.tar.xz" - tar xf "$HOME/libogg-source.tar.xz" -C "$HOME/" - pushd "$HOME/libogg-$oggver" - - info " - Compiling libogg $oggver..." - ./configure --prefix="$mingw_root" - make - - info " - Installing libogg..." - make install - # for some reason libgig needs this - ./configure --prefix="/opt$mingw_root" - make - - info " - Installing libogg..." - make install - - popd -else - warn " - Skipping, libogg binary already exists" -fi - -info "Downloading and building libvorbis $vorbisver" -if [ ! -e "$mingw_root/lib/libvorbis.dll.a" ]; then - wget http://downloads.xiph.org/releases/vorbis/libvorbis-$vorbisver.tar.xz -O "$HOME/libvorbis-source.tar.xz" - tar xf "$HOME/libvorbis-source.tar.xz" -C "$HOME/" - pushd "$HOME/libvorbis-$vorbisver" - - info " - Compiling libvorbis $vorbisver..." - ./configure --prefix="$mingw_root" - make - - info " - Installing libvorbis..." - make install - - # for some reason libgig needs this - ./configure --prefix="/opt$mingw_root" - make - info " - Installing libvorbis..." - make install - - popd -else - warn " - Skipping, libvorbis binary already exists" -fi - -info "Downloading and building flac $flacver" - -if [ ! -e "$mingw_root/lib/libFLAC.dll.a" ]; then - - wget http://downloads.xiph.org/releases/flac/flac-$flacver.tar.xz -O "$HOME/flac-source.tar.xz" - tar xf "$HOME/flac-source.tar.xz" -C "$HOME/" - pushd "$HOME/flac-$flacver" - - info " - Compiling flac $flacver..." - ./configure --prefix="$mingw_root" - make - - info " - Installing flac..." - make install - - # for some reason libgig needs this - ./configure --prefix="/opt$mingw_root" - make - - info " - Installing flac..." - make install - - popd -else - warn " - Skipping, libvorbis flac already exists" -fi - -info "Downloading and building libgig $gigver" - -if [ ! -e "$mingw_root/lib/libgig/libgig.dll.a" ]; then - wget http://download.linuxsampler.org/packages/libgig-$gigver.tar.bz2 -O "$HOME/gig-source.tar.xz" - tar xf "$HOME/gig-source.tar.xz" -C "$HOME/" - pushd "$HOME/libgig-$gigver" - - info " - Compiling libgig $gigver..." - ./configure --prefix="$mingw_root" - make - - info " - Installing libgig..." - make install - - mv "$mingw_root/lib/bin/libakai-0.dll" "$mingw_root/bin" - mv "$mingw_root/lib/bin/libgig-7.dll" "$mingw_root/bin" - - popd -else - warn " - Skipping, libgig binary already exists" -fi - -info "Downloading and building stk $stkver" - -if [ ! -e "$mingw_root/lib/libstk.dll" ]; then - wget http://ccrma.stanford.edu/software/stk/release/stk-$stkver.tar.gz -O "$HOME/stk-source.tar.xz" - tar xf "$HOME/stk-source.tar.xz" -C "$HOME/" - pushd "$HOME/stk-$stkver" - - info " - Compiling stk $stkver..." - ./configure --prefix="$mingw_root" - make - - info " - Installing stk..." - make install - - mv "$mingw_root/lib/libstk.so" "$mingw_root/lib/libstk.dll" - mv "$mingw_root/lib/libstk-$stkver.so" "$mingw_root/lib/libstk-$stkver.dll" - - popd -else - warn " - Skipping, stk binary already exists" -fi - -# make a symlink to make cmake happy -if [ "$mingw_root" = "/mingw64" ]; then - if [ ! -e /opt/mingw64/bin/x86_64-w64-mingw32-pkg-config ]; then - ln -s /usr/bin/pkg-config /opt/mingw64/bin/x86_64-w64-mingw32-pkg-config - fi -elif [ "$mingw_root" = "/mingw32" ]; then - if [ ! -e /opt/mingw32/bin/i686-w64-mingw32-pkg-config ]; then - ln -s /usr/bin/pkg-config /opt/mingw32/bin/i686-w64-mingw32-pkg-config - fi -fi - -info "Cleaning up..." -rm -rf "$HOME/fltk-$fltkver" -rm -rf "$HOME/libogg-$oggver" -rm -rf "$HOME/libvorbis-$vorbisver" -rm -rf "$HOME/flac-$flacver" -rm -rf "$HOME/libgig-$gigver" -rm -rf "$HOME/stk-$stkver" -info "Done." diff --git a/cmake/nsis/CMakeLists.txt b/cmake/nsis/CMakeLists.txt index ee1bd45c3..ba41b085f 100644 --- a/cmake/nsis/CMakeLists.txt +++ b/cmake/nsis/CMakeLists.txt @@ -44,25 +44,6 @@ IF(WIN64) ") ENDIF() -# Fix windows paths for msys -IF(LMMS_BUILD_MSYS) - STRING(REPLACE "/" "\\\\" CPACK_PACKAGE_ICON "${CPACK_PACKAGE_ICON}") - STRING(REPLACE "/" "\\\\" CPACK_NSIS_MUI_ICON "${CPACK_NSIS_MUI_ICON}") - STRING(REPLACE "/" "\\\\" CPACK_NSIS_DEFINES "${CPACK_NSIS_DEFINES}") - STRING(REPLACE "/" "\\\\" CMAKE_BINARY_DIR_FIX "${CMAKE_BINARY_DIR}") - - # FIXME: there's no easy way to fix $INST_DIR, so we'll redefine it manually - IF(WIN64) - SET(NSIS_ARCH "win64") - ELSE() - SET(NSIS_ARCH "win32") - ENDIF() - SET(CPACK_NSIS_DEFINES " - ${CPACK_NSIS_DEFINES} - !define /redef INST_DIR ${CMAKE_BINARY_DIR_FIX}\\\\_CPack_Packages\\\\${NSIS_ARCH}\\\\NSIS\\\\${CPACK_PACKAGE_FILE_NAME} - ") -ENDIF() - # Setup missing parent scopes SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_FILE_NAME}" PARENT_SCOPE) SET(CPACK_NSIS_DEFINES "${CPACK_NSIS_DEFINES}" PARENT_SCOPE) diff --git a/cmake/toolchains/MSYS-32.cmake b/cmake/toolchains/MSYS-32.cmake deleted file mode 100644 index 698dd5437..000000000 --- a/cmake/toolchains/MSYS-32.cmake +++ /dev/null @@ -1,4 +0,0 @@ -INCLUDE(${CMAKE_CURRENT_LIST_DIR}/common/MSYS.cmake) -INCLUDE(${CMAKE_CURRENT_LIST_DIR}/common/Win32.cmake) - -SET(MINGW_PREFIX /mingw32) \ No newline at end of file diff --git a/cmake/toolchains/MSYS-64.cmake b/cmake/toolchains/MSYS-64.cmake deleted file mode 100644 index 8becd51b3..000000000 --- a/cmake/toolchains/MSYS-64.cmake +++ /dev/null @@ -1,5 +0,0 @@ -INCLUDE(${CMAKE_CURRENT_LIST_DIR}/common/MSYS.cmake) -INCLUDE(${CMAKE_CURRENT_LIST_DIR}/common/Win64.cmake) - -SET(MINGW_PREFIX /mingw64) -SET(MINGW_PREFIX32 /mingw32) diff --git a/cmake/toolchains/common/MSYS.cmake b/cmake/toolchains/common/MSYS.cmake deleted file mode 100644 index 0b27e8d32..000000000 --- a/cmake/toolchains/common/MSYS.cmake +++ /dev/null @@ -1,22 +0,0 @@ -# The target environment -SET(CMAKE_FIND_ROOT_PATH ${MINGW_PREFIX}) -SET(CMAKE_INSTALL_PREFIX ${MINGW_PREFIX}) - -# Windows msys mingw ships with a mostly-suitable preconfigured environment -SET(STRIP ${MINGW_PREFIX}/bin/strip) -SET(CMAKE_RC_COMPILER ${MINGW_PREFIX}/bin/windres) -SET(CMAKE_C_COMPILER ${MINGW_PREFIX}/bin/gcc) -SET(CMAKE_CXX_COMPILER ${MINGW_PREFIX}/bin/g++) - -# For 32-bit vst support -IF(WIN64) - # Specify the 32-bit cross compiler - SET(CMAKE_C_COMPILER32 ${MINGW_PREFIX32}/bin/gcc) - SET(CMAKE_CXX_COMPILER32 ${MINGW_PREFIX32}/bin/g++) -ENDIF() - -# Msys compiler does not support @CMakeFiles/Include syntax -SET(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES OFF) -SET(CMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES OFF) - -SET(LMMS_BUILD_MSYS 1) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ae5b55404..294ae1a07 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -212,13 +212,7 @@ set_target_properties(lmms PROPERTIES set_target_properties(lmmsobjs PROPERTIES AUTOUIC_SEARCH_PATHS "gui/modals") -IF(LMMS_BUILD_MSYS) - # ENABLE_EXPORTS property has no effect in some MSYS2 configurations. - # Add the linker flag manually to create liblmms.dll.a import library - SET_PROPERTY(TARGET lmms - APPEND_STRING PROPERTY LINK_FLAGS " -Wl,--out-implib,liblmms.dll.a" - ) -ELSEIF(NOT WIN32) +IF(NOT WIN32) if(CMAKE_INSTALL_MANDIR) SET(INSTALL_MANDIR ${CMAKE_INSTALL_MANDIR}) ELSE(CMAKE_INSTALL_MANDIR) From 95e5f9715065d5cacd09edc1560163297b63d3ea Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Sun, 12 May 2024 17:38:14 +0100 Subject: [PATCH 160/191] Revamp resource embedding (#7241) --- include/TrackLabelButton.h | 4 +- include/embed.h | 132 ++++++++-------------- plugins/Eq/EqControlsDialog.cpp | 8 +- plugins/Eq/EqCurve.cpp | 6 +- plugins/LOMM/LOMMControlDialog.h | 3 +- src/gui/editors/PianoRoll.cpp | 2 +- src/gui/embed.cpp | 86 +++++++------- src/gui/instrument/EnvelopeAndLfoView.cpp | 9 +- 8 files changed, 106 insertions(+), 144 deletions(-) diff --git a/include/TrackLabelButton.h b/include/TrackLabelButton.h index 1d3620d12..e2cba02a6 100644 --- a/include/TrackLabelButton.h +++ b/include/TrackLabelButton.h @@ -25,6 +25,8 @@ #ifndef LMMS_GUI_TRACK_LABEL_BUTTON_H #define LMMS_GUI_TRACK_LABEL_BUTTON_H +#include + #include namespace lmms::gui @@ -63,7 +65,7 @@ private: private: TrackView * m_trackView; - QString m_iconName; + std::string m_iconName; TrackRenameLineEdit * m_renameLineEdit; QRect m_buttonRect; QString elideName( const QString &name ); diff --git a/include/embed.h b/include/embed.h index 7d69f7c7d..40a3622c6 100644 --- a/include/embed.h +++ b/include/embed.h @@ -25,118 +25,82 @@ #ifndef LMMS_EMBED_H #define LMMS_EMBED_H +#include +#include + #include #include #include "lmms_export.h" #include "lmms_basics.h" +namespace lmms { -namespace lmms -{ - -namespace embed -{ +namespace embed { /** * Return an image for the icon pixmap cache. * - * @param _name Identifier for the pixmap. If it is not in the icon pixmap + * @param name Identifier for the pixmap. If it is not in the icon pixmap * cache, it will be loaded from the artwork QDir search paths (exceptions are * compiled-in XPMs, you need to provide @p xpm for loading them). * @param xpm Must be XPM data if the source should be raw XPM data instead of * a file */ -QPixmap LMMS_EXPORT getIconPixmap( const QString& _name, - int _w = -1, int _h = -1 , const char** xpm = nullptr ); -QString LMMS_EXPORT getText( const char * _name ); +auto LMMS_EXPORT getIconPixmap(std::string_view name, + int width = -1, int height = -1, const char* const* xpm = nullptr) -> QPixmap; +auto LMMS_EXPORT getText(std::string_view name) -> QString; -} +} // namespace embed +class PixmapLoader +{ +public: + PixmapLoader() = default; + + explicit PixmapLoader(std::string name, const char* const* xpm = nullptr) : + m_name{std::move(name)}, + m_xpm{xpm} + { } + + virtual ~PixmapLoader() = default; + + auto pixmap(int width = -1, int height = -1) const -> QPixmap + { + return embed::getIconPixmap(m_name, width, height, m_xpm); + } + + auto pixmapName() const -> const std::string& { return m_name; } + +private: + std::string m_name; + const char* const* m_xpm = nullptr; +}; #ifdef PLUGIN_NAME -namespace PLUGIN_NAME -{ -inline QPixmap getIconPixmap( const QString& _name, - int _w = -1, int _h = -1, const char** xpm = nullptr ) +class PluginPixmapLoader : public PixmapLoader { - return embed::getIconPixmap(QString("%1/%2").arg(LMMS_STRINGIFY(PLUGIN_NAME), _name), _w, _h, xpm); +public: + PluginPixmapLoader() = default; + + explicit PluginPixmapLoader(std::string name, const char* const* xpm = nullptr) : + PixmapLoader{LMMS_STRINGIFY(PLUGIN_NAME) "/" + name, xpm} + { } +}; + +namespace PLUGIN_NAME { + +inline auto getIconPixmap(std::string_view name, + int width = -1, int height = -1, const char* const* xpm = nullptr) -> QPixmap +{ + return PluginPixmapLoader{std::string{name}, xpm}.pixmap(width, height); } -//QString getText( const char * _name ); } // namespace PLUGIN_NAME #endif // PLUGIN_NAME - -class PixmapLoader -{ -public: - PixmapLoader( const PixmapLoader * _ref ) : - m_name( _ref != nullptr ? _ref->m_name : QString() ), - m_xpm( _ref->m_xpm ) - { - } - - PixmapLoader( const QString & _name = QString(), - const char** xpm = nullptr ) : - m_name( _name ), - m_xpm(xpm) - { - } - - virtual QPixmap pixmap() const - { - if( !m_name.isEmpty() ) - { - return( embed::getIconPixmap( - m_name.toLatin1().constData(), -1, -1, m_xpm )); - } - return( QPixmap() ); - } - - virtual ~PixmapLoader() = default; - - virtual QString pixmapName() const - { - return m_name; - } - -protected: - QString m_name; - const char** m_xpm = nullptr; -} ; - - -#ifdef PLUGIN_NAME -class PluginPixmapLoader : public PixmapLoader -{ -public: - PluginPixmapLoader( const QString & _name = QString() ) : - PixmapLoader( _name ) - { - } - - QPixmap pixmap() const override - { - if( !m_name.isEmpty() ) - { - return( PLUGIN_NAME::getIconPixmap( - m_name.toLatin1().constData() ) ); - } - return( QPixmap() ); - } - - QString pixmapName() const override - { - return QString( LMMS_STRINGIFY(PLUGIN_NAME) ) + "::" + m_name; - } - -} ; -#endif // PLUGIN_NAME - - } // namespace lmms #endif // LMMS_EMBED_H diff --git a/plugins/Eq/EqControlsDialog.cpp b/plugins/Eq/EqControlsDialog.cpp index 17de9ce98..8394569f6 100644 --- a/plugins/Eq/EqControlsDialog.cpp +++ b/plugins/Eq/EqControlsDialog.cpp @@ -123,10 +123,10 @@ EqControlsDialog::EqControlsDialog( EqControls *controls ) : activeButton->setCheckable(true); activeButton->setModel( m_parameterWidget->getBandModels( i )->active ); - QString iconActiveFileName = "bandLabel" + QString::number(i+1); - QString iconInactiveFileName = "bandLabel" + QString::number(i+1) + "off"; - activeButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( iconActiveFileName.toLatin1() ) ); - activeButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( iconInactiveFileName.toLatin1() ) ); + const auto iconActiveFileName = "bandLabel" + std::to_string(i + 1); + const auto iconInactiveFileName = iconActiveFileName + "off"; + activeButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap(iconActiveFileName)); + activeButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap(iconInactiveFileName)); activeButton->move( distance - 2, 276 ); activeButton->setModel( m_parameterWidget->getBandModels( i )->active ); diff --git a/plugins/Eq/EqCurve.cpp b/plugins/Eq/EqCurve.cpp index 3ec8901e1..e1d28ca0a 100644 --- a/plugins/Eq/EqCurve.cpp +++ b/plugins/Eq/EqCurve.cpp @@ -185,9 +185,9 @@ QPainterPath EqHandle::getCurvePath() void EqHandle::loadPixmap() { - QString fileName = "handle" + QString::number(m_numb+1); - if ( !isActiveHandle() ) { fileName = fileName + "inactive"; } - m_circlePixmap = PLUGIN_NAME::getIconPixmap( fileName.toLatin1() ); + auto fileName = "handle" + std::to_string(m_numb + 1); + if (!isActiveHandle()) { fileName += "inactive"; } + m_circlePixmap = PLUGIN_NAME::getIconPixmap(fileName); } bool EqHandle::mousePressed() const diff --git a/plugins/LOMM/LOMMControlDialog.h b/plugins/LOMM/LOMMControlDialog.h index bf7e67c4c..3de38c984 100644 --- a/plugins/LOMM/LOMMControlDialog.h +++ b/plugins/LOMM/LOMMControlDialog.h @@ -86,7 +86,8 @@ public: return spinBox; } - PixmapButton* createPixmapButton(const QString& text, QWidget* parent, int x, int y, BoolModel* model, const QString& activeIcon, const QString& inactiveIcon, const QString& tooltip) + PixmapButton* createPixmapButton(const QString& text, QWidget* parent, int x, int y, BoolModel* model, + std::string_view activeIcon, std::string_view inactiveIcon, const QString& tooltip) { PixmapButton* button = new PixmapButton(parent, text); button->move(x, y); diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 7d4a9552a..75bac2243 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -342,7 +342,7 @@ PianoRoll::PianoRoll() : // Set up note length model m_noteLenModel.addItem( tr( "Last note" ), std::make_unique( "edit_draw" ) ); - const auto pixmaps = std::array{"whole", "half", "quarter", "eighth", + const auto pixmaps = std::array{"whole", "half", "quarter", "eighth", "sixteenth", "thirtysecond", "triplethalf", "tripletquarter", "tripleteighth", "tripletsixteenth", "tripletthirtysecond"}; diff --git a/src/gui/embed.cpp b/src/gui/embed.cpp index d934adcde..e912fd6d4 100644 --- a/src/gui/embed.cpp +++ b/src/gui/embed.cpp @@ -22,68 +22,62 @@ * */ +#include "embed.h" + #include +#include #include #include #include -#include "embed.h" -namespace lmms::embed +namespace lmms::embed { + +namespace { + +auto loadPixmap(const QString& name, int width, int height, const char* const* xpm) -> QPixmap { + if (xpm) { return QPixmap{xpm}; } -QPixmap getIconPixmap(const QString& pixmapName, - int width, int height, const char** xpm ) + const auto resourceName = QDir::isAbsolutePath(name) ? name : "artwork:" + name; + auto reader = QImageReader{resourceName}; + if (width > 0 && height > 0) { reader.setScaledSize(QSize{width, height}); } + + const auto pixmap = QPixmap::fromImageReader(&reader); + if (pixmap.isNull()) { + qWarning().nospace() << "Error loading icon pixmap " << name << ": " << reader.errorString(); + return QPixmap{1, 1}; + } + return pixmap; +} + +} // namespace + +auto getIconPixmap(std::string_view name, int width, int height, const char* const* xpm) -> QPixmap { - QString cacheName; - if (width > 0 && height > 0) - { - cacheName = QString("%1_%2_%3").arg(pixmapName, width, height); - } - else - { - cacheName = pixmapName; - } + if (name.empty()) { return QPixmap{}; } - // Return cached pixmap - QPixmap pixmap; - if( QPixmapCache::find(cacheName, &pixmap) ) - { - return pixmap; - } + const auto pixmapName = QString::fromUtf8(name.data(), name.size()); + const auto cacheName = (width > 0 && height > 0) + ? QStringLiteral("%1_%2_%3").arg(pixmapName, width, height) + : pixmapName; - if(xpm) - { - pixmap = QPixmap(xpm); - } - else - { - QImageReader reader(QString("artwork:%1").arg(pixmapName)); + // Return cached pixmap if it exists + if (auto pixmap = QPixmap{}; QPixmapCache::find(cacheName, &pixmap)) { return pixmap; } - if (width > 0 && height > 0) - { - reader.setScaledSize(QSize(width, height)); - } - - pixmap = QPixmap::fromImageReader(&reader); - - if (pixmap.isNull()) - { - qWarning().nospace() << "Error loading icon pixmap " << pixmapName << ": " << - reader.errorString().toLocal8Bit().data(); - return QPixmap(1,1); - } - } - - // Save to cache and return + // Load the pixmap and cache it before returning + const auto pixmap = loadPixmap(pixmapName, width, height, xpm); QPixmapCache::insert(cacheName, pixmap); return pixmap; } - -QString getText( const char * name ) +auto getText(std::string_view name) -> QString { - return QString::fromUtf8( (const char*) QResource(QString(":/%1").arg(name)).data()); + const auto resource = QResource{":/" + QString::fromUtf8(name.data(), name.size())}; +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + return QString::fromUtf8(resource.uncompressedData()); +#else + return QString::fromUtf8(reinterpret_cast(resource.data()), resource.size()); +#endif } - } // namespace lmms::embed diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index c2e642b01..1b639e6c3 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -25,6 +25,10 @@ #include "EnvelopeAndLfoView.h" +#include + +#include + #include "EnvelopeGraph.h" #include "LfoGraph.h" #include "EnvelopeAndLfoParameters.h" @@ -39,9 +43,6 @@ #include "TextFloat.h" #include "Track.h" -#include - - namespace lmms { @@ -63,7 +64,7 @@ EnvelopeAndLfoView::EnvelopeAndLfoView(QWidget * parent) : return knob; }; - auto buildPixmapButton = [&](const QString& activePixmap, const QString& inactivePixmap) + auto buildPixmapButton = [&](std::string_view activePixmap, std::string_view inactivePixmap) { auto button = new PixmapButton(this, nullptr); button->setActiveGraphic(embed::getIconPixmap(activePixmap)); From 36786dd83de8e7e19f1a96607b4dc5a7a1233aaf Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Mon, 13 May 2024 13:14:42 +0530 Subject: [PATCH 161/191] Enable LADSPA plugins on MSVC (#6973) Co-authored-by: Tres Finocchiaro Co-authored-by: Dominic Clark Co-authored-by: Dalton Messmer --- CMakeLists.txt | 1 + cmake/modules/PluginList.cmake | 9 --------- plugins/LadspaEffect/calf/CMakeLists.txt | 11 ++++++++++- plugins/LadspaEffect/caps/CMakeLists.txt | 12 +++++++++++- plugins/LadspaEffect/caps/basics.h | 5 ++++- plugins/LadspaEffect/caps/dsp/Eq.h | 6 +++--- plugins/LadspaEffect/caps/interface.cc | 11 ++++++++--- plugins/LadspaEffect/cmt/CMakeLists.txt | 11 ++++++++++- plugins/LadspaEffect/cmt/cmt | 2 +- plugins/LadspaEffect/swh/CMakeLists.txt | 14 +++++++++++--- plugins/LadspaEffect/tap/CMakeLists.txt | 10 ++++++++-- 11 files changed, 67 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e0158e75..1da10775b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -645,6 +645,7 @@ IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") ENDIF() ELSEIF(MSVC) # Remove any existing /W flags + string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS ${CMAKE_C_FLAGS}) STRING(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) SET(WERROR_FLAGS "/W2") IF(${USE_WERROR}) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 8c444aca2..8b26d4ed5 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -100,12 +100,3 @@ IF(LIST_PLUGINS) UNSET(LIST_PLUGINS CACHE) LIST_ALL_PLUGINS() ENDIF() - -IF(MSVC) - SET(MSVC_INCOMPATIBLE_PLUGINS - LadspaEffect - ) - message(WARNING "Compiling with MSVC. The following plugins are not available: ${MSVC_INCOMPATIBLE_PLUGINS}") - LIST(REMOVE_ITEM PLUGIN_LIST ${MSVC_INCOMPATIBLE_PLUGINS}) -ENDIF() - diff --git a/plugins/LadspaEffect/calf/CMakeLists.txt b/plugins/LadspaEffect/calf/CMakeLists.txt index 67bdc5cd2..038fa6afb 100644 --- a/plugins/LadspaEffect/calf/CMakeLists.txt +++ b/plugins/LadspaEffect/calf/CMakeLists.txt @@ -40,7 +40,16 @@ IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") SET(INLINE_FLAGS -finline-functions-called-once -finline-limit=80) SET(OTHER_FLAGS -Wno-format-overflow) ENDIF() -target_compile_options(veal PRIVATE -fexceptions -O2 -finline-functions ${INLINE_FLAGS} ${OTHER_FLAGS}) + +if(MSVC) + target_compile_options(veal PRIVATE /wd4099 /wd4244 /wd4305) +else() + target_compile_options(veal PRIVATE -fexceptions -O2 -finline-functions ${INLINE_FLAGS} ${OTHER_FLAGS}) +endif() + +if(MSVC) + target_link_options(veal PRIVATE "/EXPORT:ladspa_descriptor") +endif() if(LMMS_BUILD_WIN32) add_custom_command( diff --git a/plugins/LadspaEffect/caps/CMakeLists.txt b/plugins/LadspaEffect/caps/CMakeLists.txt index bdcf3a96a..f82fa5ab0 100644 --- a/plugins/LadspaEffect/caps/CMakeLists.txt +++ b/plugins/LadspaEffect/caps/CMakeLists.txt @@ -7,7 +7,16 @@ IF(LMMS_BUILD_WIN64) ADD_DEFINITIONS(-DLMMS_BUILD_WIN64) ENDIF(LMMS_BUILD_WIN64) SET_TARGET_PROPERTIES(caps PROPERTIES PREFIX "") -SET_TARGET_PROPERTIES(caps PROPERTIES COMPILE_FLAGS "-O2 -funroll-loops -Wno-write-strings") + +if(MSVC) + target_compile_options(caps PRIVATE /wd4244 /wd4305) +else() + target_compile_options(caps PRIVATE -O2 -funroll-loops -Wno-write-strings) +endif() + +if(MSVC) + target_link_options(caps PRIVATE "/EXPORT:ladspa_descriptor") +endif() IF(LMMS_BUILD_WIN32) add_custom_command( @@ -18,6 +27,7 @@ IF(LMMS_BUILD_WIN32) COMMAND_EXPAND_LISTS ) ENDIF(LMMS_BUILD_WIN32) + IF(NOT LMMS_BUILD_APPLE AND NOT LMMS_BUILD_OPENBSD) SET_TARGET_PROPERTIES(caps PROPERTIES LINK_FLAGS "${LINK_FLAGS} -shared -Wl,-no-undefined") ENDIF(NOT LMMS_BUILD_APPLE AND NOT LMMS_BUILD_OPENBSD) diff --git a/plugins/LadspaEffect/caps/basics.h b/plugins/LadspaEffect/caps/basics.h index df24e8c05..62eb77887 100644 --- a/plugins/LadspaEffect/caps/basics.h +++ b/plugins/LadspaEffect/caps/basics.h @@ -41,6 +41,9 @@ #include #include +#ifndef _USE_MATH_DEFINES +#define _USE_MATH_DEFINES +#endif #include #include @@ -76,7 +79,7 @@ #define MIN_GAIN .000001 /* -120 dB */ -/* smallest non-denormal 32 bit IEEE float is 1.18×10-38 */ +/* smallest non-denormal 32 bit IEEE float is 1.18×10^-38 */ #define NOISE_FLOOR .00000000000005 /* -266 dB */ typedef int8_t int8; diff --git a/plugins/LadspaEffect/caps/dsp/Eq.h b/plugins/LadspaEffect/caps/dsp/Eq.h index 92639e8a1..89c86dd18 100644 --- a/plugins/LadspaEffect/caps/dsp/Eq.h +++ b/plugins/LadspaEffect/caps/dsp/Eq.h @@ -62,11 +62,11 @@ class Eq { public: /* recursion coefficients, 3 per band */ - eq_sample __attribute__ ((aligned)) a[Bands], b[Bands], c[Bands]; + eq_sample a[Bands], b[Bands], c[Bands]; /* past outputs, 2 per band */ - eq_sample __attribute__ ((aligned)) y[2][Bands]; + eq_sample y[2][Bands]; /* current gain and recursion factor, each 1 per band = 2 */ - eq_sample __attribute__ ((aligned)) gain[Bands], gf[Bands]; + eq_sample gain[Bands], gf[Bands]; /* input history */ eq_sample x[2]; /* history index */ diff --git a/plugins/LadspaEffect/caps/interface.cc b/plugins/LadspaEffect/caps/interface.cc index 96e3d9806..4c7ca46b5 100644 --- a/plugins/LadspaEffect/caps/interface.cc +++ b/plugins/LadspaEffect/caps/interface.cc @@ -29,7 +29,7 @@ (2541 - 2580 donated to artemio@kdemail.net) */ -#include +// #include #include "basics.h" @@ -69,7 +69,6 @@ seed() extern "C" { -__attribute__ ((constructor)) void caps_so_init() { DescriptorStub ** d = descriptors; @@ -125,7 +124,6 @@ void caps_so_init() //seed(); } -__attribute__ ((destructor)) void caps_so_fini() { for (ulong i = 0; i < N; ++i) @@ -142,4 +140,11 @@ ladspa_descriptor (unsigned long i) return 0; } +struct CapsSoInit +{ + CapsSoInit() { caps_so_init(); } + ~CapsSoInit() { caps_so_fini(); } +}; +static CapsSoInit capsSoInit; + }; /* extern "C" */ diff --git a/plugins/LadspaEffect/cmt/CMakeLists.txt b/plugins/LadspaEffect/cmt/CMakeLists.txt index 75dba319d..65430d109 100644 --- a/plugins/LadspaEffect/cmt/CMakeLists.txt +++ b/plugins/LadspaEffect/cmt/CMakeLists.txt @@ -5,7 +5,12 @@ ADD_LIBRARY(cmt MODULE ${SOURCES}) INSTALL(TARGETS cmt LIBRARY DESTINATION "${PLUGIN_DIR}/ladspa") SET_TARGET_PROPERTIES(cmt PROPERTIES PREFIX "") -target_compile_options(cmt PRIVATE -Wall -O3 -fno-strict-aliasing) + +if(MSVC) + target_compile_options(cmt PRIVATE /wd4244 /wd4305) +else() + target_compile_options(cmt PRIVATE -Wall -O3 -fno-strict-aliasing) +endif() if(LMMS_BUILD_WIN32) add_custom_command( @@ -17,6 +22,10 @@ if(LMMS_BUILD_WIN32) ) endif() +if(MSVC) + target_link_options(cmt PRIVATE "/EXPORT:ladspa_descriptor") +endif() + if(NOT LMMS_BUILD_WIN32) target_compile_options(cmt PRIVATE -fPIC) endif() diff --git a/plugins/LadspaEffect/cmt/cmt b/plugins/LadspaEffect/cmt/cmt index d8bf8084a..24599fb45 160000 --- a/plugins/LadspaEffect/cmt/cmt +++ b/plugins/LadspaEffect/cmt/cmt @@ -1 +1 @@ -Subproject commit d8bf8084aa3a47497092f5ab99c843a55090d151 +Subproject commit 24599fb45b99fff6302136f13adb3817e5833e7d diff --git a/plugins/LadspaEffect/swh/CMakeLists.txt b/plugins/LadspaEffect/swh/CMakeLists.txt index a83001177..b76c95931 100644 --- a/plugins/LadspaEffect/swh/CMakeLists.txt +++ b/plugins/LadspaEffect/swh/CMakeLists.txt @@ -9,9 +9,13 @@ ELSE() ENDIF() # Additional compile flags -SET(COMPILE_FLAGS "${COMPILE_FLAGS} -O3 -Wall") -SET(COMPILE_FLAGS "${COMPILE_FLAGS} -fomit-frame-pointer -funroll-loops -ffast-math -c -fno-strict-aliasing") -SET(COMPILE_FLAGS "${COMPILE_FLAGS} ${PIC_FLAGS}") +if(MSVC) + set(COMPILE_FLAGS "${COMPILE_FLAGS} /wd4244 /wd4273 /wd4305") +else() + set(COMPILE_FLAGS "${COMPILE_FLAGS} -O3 -Wall") + set(COMPILE_FLAGS "${COMPILE_FLAGS} -fomit-frame-pointer -funroll-loops -ffast-math -c -fno-strict-aliasing") + set(COMPILE_FLAGS "${COMPILE_FLAGS} ${PIC_FLAGS}") +endif() # Loop over every XML file FILE(GLOB XML_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/ladspa/*.xml") @@ -34,6 +38,10 @@ FOREACH(_item ${XML_SOURCES}) # Add a library target for this C file, which depends on success of makestup.pl ADD_LIBRARY("${_plugin}" MODULE "${_out_file}") + if(MSVC) + target_link_options("${_plugin}" PRIVATE "/EXPORT:ladspa_descriptor") + endif() + # Vocoder does not use fftw IF(NOT ("${_plugin}" STREQUAL "vocoder_1337")) TARGET_LINK_LIBRARIES("${_plugin}" ${FFTW3F_LIBRARIES}) diff --git a/plugins/LadspaEffect/tap/CMakeLists.txt b/plugins/LadspaEffect/tap/CMakeLists.txt index c8d0a4eb8..b895c7531 100644 --- a/plugins/LadspaEffect/tap/CMakeLists.txt +++ b/plugins/LadspaEffect/tap/CMakeLists.txt @@ -1,10 +1,17 @@ INCLUDE_DIRECTORIES("${CMAKE_SOURCE_DIR}/include") FILE(GLOB PLUGIN_SOURCES tap-plugins/*.c) LIST(SORT PLUGIN_SOURCES) -SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -Wno-write-strings -fomit-frame-pointer -fno-strict-aliasing -funroll-loops -ffast-math") +if(MSVC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /wd4244 /fp:fast") +else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -Wno-write-strings -fomit-frame-pointer -fno-strict-aliasing -funroll-loops -ffast-math") +endif() FOREACH(_item ${PLUGIN_SOURCES}) GET_FILENAME_COMPONENT(_plugin "${_item}" NAME_WE) ADD_LIBRARY("${_plugin}" MODULE "${_item}") + if(MSVC) + target_link_options("${_plugin}" PRIVATE "/EXPORT:ladspa_descriptor") + endif() # TAP pinknoise will re-init srand(); use existing seed instead IF("${_plugin}" MATCHES "tap_pinknoise") TARGET_COMPILE_DEFINITIONS("${_plugin}" PRIVATE TAP_DISABLE_SRAND=1) @@ -24,4 +31,3 @@ FOREACH(_item ${PLUGIN_SOURCES}) TARGET_LINK_LIBRARIES("${_plugin}" m) ENDIF() ENDFOREACH() - From a66d21222183cb5dfedfb506f942654ac63099ad Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Thu, 16 May 2024 15:05:12 -0400 Subject: [PATCH 162/191] Fix native linux VST on ARM64 (#7257) --- plugins/VstBase/NativeLinuxRemoteVstPlugin64.cmake | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/VstBase/NativeLinuxRemoteVstPlugin64.cmake b/plugins/VstBase/NativeLinuxRemoteVstPlugin64.cmake index dc5823604..d9ef2897d 100644 --- a/plugins/VstBase/NativeLinuxRemoteVstPlugin64.cmake +++ b/plugins/VstBase/NativeLinuxRemoteVstPlugin64.cmake @@ -1,11 +1,13 @@ IF(LMMS_BUILD_LINUX) + if(LMMS_HOST_X86_64) + set(CXX_FLAGS -m64) + endif() ExternalProject_Add(NativeLinuxRemoteVstPlugin64 "${EXTERNALPROJECT_ARGS}" CMAKE_ARGS "${EXTERNALPROJECT_CMAKE_ARGS}" - "-DCMAKE_CXX_FLAGS=-m64 -DNATIVE_LINUX_VST" + "-DCMAKE_CXX_FLAGS=${CXX_FLAGS} -DNATIVE_LINUX_VST" "-DCMAKE_C_FLAGS=-DNATIVE_LINUX_VST" ) INSTALL(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/../NativeLinuxRemoteVstPlugin64" DESTINATION "${PLUGIN_DIR}") -ENDIF() - +ENDIF() \ No newline at end of file From 321b2b41671e6f4f9841fcac4ec323bc6512b55b Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Fri, 17 May 2024 01:21:46 +0530 Subject: [PATCH 163/191] Remove the missing `spoken.flac` file in unfa - Spoken (#7208) Remove spoken.flac using LMMS 1.0.3 --------- Co-authored-by: Tres Finocchiaro --- data/projects/demos/unfa-Spoken.mmpz | Bin 212173 -> 208433 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/projects/demos/unfa-Spoken.mmpz b/data/projects/demos/unfa-Spoken.mmpz index 66b7589d106d07a53ae3e850dc1fe4057d4ead5a..58d00d50d69191c64a189c5e80be81eb82c63746 100644 GIT binary patch literal 208433 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

d?|u!Ohqw( zQKM~QjYtSOqx3m#i`mc~ev?pj-;%vhSGrvz%XBzR;yM(^n5#;{MmuzKu~1+o1DYSv zd*s_}E0v72rjsRB0eRO266-R;jZX?X&KpjOV7iauzJC8N)kCqns5BKu%?|EVE_H(~ z(09qkXsmgd0%&{WPU<}j$tiet2>j1>4h{%T)?j244mXN-CreER_BQ{)!4yJ^Ys5E$ zhYA}mwz-36tkeM^!4s0dgUlan=IaH#wpK-mzHU?K6Uy+&fywp)-o!QqJr3fVjO)bp zYdQ3nq44--b7ZFoy_aLuY)js+%wBDs{MBYP~)peGof%=)B?3D zVjAf|cI@@`)-Nr<3TkVslJR0eMqC1r#GCdG74}mox=~b;WZwg36vQ3$6hSM=5thOZ z8VjfB)!wyEJ(oZk8WDSf#890vQ;sXCoqq1R*g@uvHoL6t|(uU+A5wf z1$jU1S6-{7EfoAburM_X;xTX&)K}0Kv#q>KxbGJe-|DhO5K0hbMe+!B7ajCY_l?;@ zaHNkwU$JGI}R$0=E6`Blss>rS0Oh9dn24xdW&Fv zVugF3Fo+YiSE(WiH)GfyCG)s+ORP|yVE4*I_jO;og~~d#Elb%|RR;90N{0+&cA5QQ z?3=xqUnp*(=1Agu)m*%1AE5HF`Q|PZvJoRsQ3r=$-Z`}4x@3W#e_V+q#B&UyGvcWX z2*7|M(K0B7L^8FtOgI;q;7x1W->f1cJb!P~u4XAk6c#USmndGf_kOa>9)&Kf#+X5g zPW01>eH|tL7j7By^n|~#d*@OuT9+V#KZz7ki%uU;?HtEEzzNu~;SDgC$r?Wo+~Uwc zWe)LG%UiqQVGHh=ZX@7cB^?9&|YM{|qf|mgE&qbC2XvIWi>^%<)Jfzp%@ zWJPcoF}oG(6_lkvI+YMf8-3vNi?H?6S#}ply3B`1Jyd30+DjoG zd)hS<=><-5vt>n8kO@QO&9)?6mzBZgaf{2(=!K#wI{A;2jy`~kqBG$*$?^^f^KJ=F zVP`MS#dS({qV<8pTB{O&2-hclJTioSuVil+W{Kukc+-|nluoL@g{ebCA`T-bmRG>> zpYqbKdFFv7#WBVt``|GT#h?AnlUt+5Jt{(^0HT0uws8hFv%Wd4IKJBFgmzqulgXxR zvih(TB8=ei?AovG5znr@PrU+aCnvpaYWmj~*wS<04-(ECaNK2FZ@3-BhoJkzef zeD+#(8^J=Bf|sTc6`WxVt7^2i)7sbOlc7vtKUYU4XMR1-$0_Mv1aJO!P&r#eXlG}r zSyL?h>I>Ul(A0&D)*K_9@5}(gGh)0@?kqdR57Ma4SeHq~mEGZfhQ}`5S+d(n8CNB& zD80bknSy00UT+lek9GlSpGV_;LuAQ{LqayI^dA-jyOu*XCHQ+seog;iT8DUq#Pm&s z^)pVsZ?I;!H57NC&+e@s6OrjYj|$? zwfH5TQ~(7~?0IoDGc|P^VZ~}kGr|#IB*k9?CNVV$`0O*H=5tNdQhZSckrYF)=mwYo ze^^?E?PNY`lpQp`9-9yJXO2($gn$Z>^@>9_eBfV3&)5rVrKE-|sFzXA1{mAWhtLCG zymU!)2dfQRC0vPbU_>BUfz*WxEGnzFT29|yRm+GvksACJV+!$bvv0-!h^|nqR+rw? z^ci{iOLXa59kOhq9rDkZfpCdr7=j43R&i}{Xi1#fZ$TI~JCwyrmqx zY&s&E+*Iy+su1oM^)g`m`$?`|SGJl33=?%2<)){z)|D!LwSc1T<8GQ6A9EIWh--;b+W|o*S*$*KjJjNv@ekdHqFm^L$Yi--8oTzC z;AnXDp7>_DAehlH;oIBMyH;gi1KUag?@|_2L=hrQ7B-7g5MHu)#9(RwU7eX&QrD70 zX{A-f_YP8B3R{>`FnslF-~Hp)(<%A>v+Zke`|S?$tY^c0v^szhKhC#yips0p1H<+R z9KpK*t5hBE_9?s9p!Mmz_Y!sL&ZYcXAn=d$>01u_JIK_5!vlX`6IH#J_c%|Jwt8c)0=dq*T-E(aF1wH- zDQ~?6)4k;y`n_{lunZfcxl4+Q4AcGytFTKWkh)wy59aZ+d$~!%|5co-c*GwG{Hv`UI+$i#fR2!_V-&yEt1Ko;&;WsVa}3?r_~c{!I%H zay10@RZOT?5rqFq40O{c@|6ADdDcL=+Hgc@jas zj~}4+1O&BjIKsgDzw&4KmPAsCoZ~(aE&LDeA@SKgmjF=G0+YCTg;OdGqz>R}dwjHY z1&m&w=4l60GnfjmL?RO_G+-=0nQ)Q&DC`;Y9$?4umG(qRHp0WLiw<{`fPX)=-2^RF zSvv+PNH6>lwIl)s#e9Ip-o%%b&W$Rf^DnKfZ9AtP@nfrmVb)MjZ>Grp#r&Xa7wleP z$%e*yyOk%Po8p>qgtGyUGQnz{VTHJTL%pP3j*WT(P}8n!V6*asQ8z9JH^kT86_L2N z7%UReIpz{|1Ne9=0pWf_3d+6q$X0D3avKLzJf>BGTvC@4H$VNq5it1^o%U@n93xOM zUus8&h~&XVIb1+_ac-5NOId}IvMeVqe14T`nQ-l-REx2BMT{fAE82~d7JtUh_Az7l0 zLF5sl*HW55FAJ%q#+*9Q0KytsD-hPijexKgQ}qFBG#Eo*XpiV(g?LM7Xz3s4J2W&d zbg`WR%$x1nu#rUq0B)N?619Oic7*|wa$E>?il3d`_INh)poSzAW6*_8!AP7_zxdjq zV?T5b@Nh2t9q45`pGF{ijfsJ)T*E*M)O&INbyyWkqa_Hp>x~bNHE_T-> zz279g)FeH8^Sh<#-UR4k@-09Y0}9%Qi}9^$v0}CM5REwwa^$(;XSO+ydAp2xx{SHI zjJfLma=wirO93L9n$yvTpy{%9ckgIO?>8)^uE^72*o(! z+yjtK4Qyo<^zeJh|}BjFdzrYdx1oduLRyG^z={M6OM~`vMXXFtBGxLRd&qU_s%nk7sn|Cch459NpuuY! zfeSaFBE^nk*>HCEk5KZ#vOgr9LwcG+`SCwCBl53Gm%=>}57*4ihy;H3As^t*jRg2U zInoSQ5_f1+i=3urM%8)1znZAH6F-|2-6Sf0vGo4Hkh z;J+aqMfc>bem_*ZLS+-m)Vwj%j~hL_H-l*BHDOYuy(uO(yJ6eYnen5z#QKYYa>StZ8b>cnbnWEf5N%n=4T)&_v9BMx>hY=q#N){1R_O zm&3x?GpOZLp}8^8&Mc02%r5XC_i$OjsT?U*A4QuxL2zN@8nzavpGD1OnDQeX>a%b6XhK-Q)$x{h@ zWJ;CtcXE6xFMANYE4R3c%&o}Jjesx(V;$rkl87>-MK#uZZkOoOA-9rF_SG3bfcxp< znlk87>2c(pGQ%tDd)o9#(W{GyhCxK;)2ncChztDC&o!k;DLCUN6A{JKTvxJY1^+_BunYK-Rd@S}-~Fu$8W3ee=>?5&q!z!PUp| zU+6WN*KsgB)X9T`A3CB)^8_Q~KG?VTIz)YsEQSQM>cLz<)+{D`tDUnz_mzmyI8wfg zF(t2i+lclJp*M!$tMh-&+6un0rKV;g(JxP~t5;y`l{3yadqIuXMJlXUxQy&^;}yOK zso;ZohcO(G2evK+wj~!cGndvs6$2{5EOOvodDuh(bHT4absU#P=VTfV31pA+GI428 zzjM7?+_+u6e=3{xys0+ylnltm93-#6CXyL&CSbx&Yw<#G9zo(AX%JNLvhD#J;DWkH zHUFp~x?WG_%~$-(F7z7D+6D@~79TU^;1EwgqDm6bh#|*EY_*sEMEV`m6ZPiGXMSKA zR`-;+A5>-JzJha?AJ1D?QNO1C;5|<$=*XpAw+_a4AzY=>ve>i1O3O1F6L5WP0cGkz zrT^vLT!+@%V1dSRcXJM5bFrXQusxtG&Ul#$g1(Tpe()9aSh&~G*d#n6DhU)9lKQac zfi;opR@q6#I|02G-J)lgosnw2$9PoM3|tqVdip+{ypj50Ze}0FALp)#Q0wnXeWe#6z6L~qwGfY`_;gj(0@LC zM_d_NP}oMXIYx_gjm?3BhaP6_x>g>|!YGdlUHj&fF-x=%KUor*BXR9#@?~7hn>05G zuP9gg4WhF#nD59NOKR3+T4RSe_=2KK4Bf38=uQ>#UaPd0CQoZv1eHe1cxGMBn?19+V?sM~VCx<+ z)i{sCTLU=d6}F+5Zx7SpsAEmffp)K`v)1ED4DxO-N9ou&VEH|6;Z2CBZ1Tv9TS7hO z!7h^?GtCB0cMVAAwy)=uKaef*5I$fC!?1oZebD85Ip5cV|0`B+s`cXa^pG$GBoCTO zKVD%fh>kErAOd|Au5A@dEII4>`H;#cUM( z7T7uLto;bMWVcGXC^+0hXFd}=H$vyg-jJZ{UG4QZ?MRsa?Y5vQ)CeVcpuBEwwXR)k z>j2uZV6_|ucX zq#y^l!-XoAyHyEUk%biDq+ig^S>ABHrP6l~LNHbkJrJHCOjDh&;cNY(P?qnR1gDGU zBMU;{!`~!Uk|#>|7Fkm@e^PSR7+OoL#o6i)SnAx~?_;iIA1Cf;NWOkqS-|eQ)6u^5 zN18m?AK6^j*|aD8a#FcATy3?&@c%}h)f}*cKzSCf$PtK}&c>^NnCm_g7AR4H$}{0H zNAF_&(*l}_^Jkhd?_#@SMg+M`-zIoQGDTf-akH3zum6A-8okYbIRoED-MFj zPZ%KsbKA7HCwza9YVoF3HpuL+WT8Izh7YqYq8?phf<=gL>r?yH6!ea9rO{w-0~XyV zKbS#|r8o;LJ32up*h<*s=+oMNOIAGlkiSIbT)-hZ#{FkK)j1T$=r0?) z=(Zbe|KU9m>bN0#p!rz>Qa{&xRYAx91*|eu5`F@zJj7JECHod71rg`_gX+MyQSK!t zvA~(wbQyBXjb|2e=5C^Sg!vpI;p$(Hv`xN~Qw;PIdxHxjwnrGnd+rVk+>9Y}%^;`v zv5KX0ly%609{u(?4S3b=XPL3nD6HH9nqdXN3afAa=$DI%qidE~9|K(2N zBtUm!i%0rdgGdCHjjsHmPNmlF=0{-I+EZyoG*1tth2l_zQH!n_l4fk;F4NKT44J${2zL}6ix0m6nsY7pvB2|Q3)4o zPk!S%>crgq_5fH6rhc<^_|$)Eu;$7F*j-{jlHFF#O&v75c=TfhD*P1DWFCGYmbnSe zb_nN=HYBajRJJ6TD{)Fj6C@!6qmEDcYLw3ASXM64fAB?jE~4`mf-Dn9?5$M8T=1&n zNm%_QMY3H3{2|5Sg@1}dJIO8kQ&qw@!gxVK-6fsV=nS3af|QdpAy@q{lIN;iBQ}-f zu8h`ED9zsf{L42-oJZJ%^44Udf+#dzVmyuFC~IENIXQ&F()GzyGod6|KZRly9+_z> z;nc@4)lBlPBwo6a-0Nf%QsYN^+EJ9vUk!1QXeAt5q>gzGtwo?Af*et+;Nh1uN?JRt za@9=4KKgKvpC2L7;73SQ_7M`rpML8MA`?;_2$P{oohTN8GT*kheKBL1a0eyLAkqukCy0q zEjk?eiN&F14b$KE%Am*`a3r+<*hIo{OlK6=5&57*zG4`Ic-l8eq-DX&>ZCB>3~|M8 zzu{s6IlgI*MZjnUPTOgatz5~5pWyWiPEr*EdwUD<&Y@3{CTRgd)cM3L8FYG}n5Z4n z#TgJXC7}~ch4x>BM7*;FNZ$u~ijL+krFTsyJGAbImE;+ftl1Y?($H#^*T^8pn9b5x~2EKrI%iR4|ls& z10`d*11K3<%YP+9_vEi+bW?eW$zxIhHWF=r&rx{&q;UF4Vf~ZB*waz_r&ogpV>gxd zRPnuu^_cV6&0UTSLwcn4X6!EKZ`@g)n2%Vgr^BSX!=$Ujq?6IE!*c`$7%SleW2G45 ze_|!aDhemf6Y0~^WVp|}kpd1BWSt_|S^XZ|-bpLs#1>?`$Rw zX2^wDtfB5@;${!eR=>(Dc>YrL`fOHjZq${>0=#%36L>Mna1M=bvpHN4A6|I<#|fl=@lvkGfyzJ9D(zF2J_Pi86W`1Mb+y1hbWFL)bYimo1Vm&1qnxZR09Ql^3NRpo1^hCn-FPXYa<$qw0>yZ<=42$ z4y$SGuft9(VnHNEynk#4H4+A4&d5TdAQ^mcNQWcx1_TjG10ZYmm*j7ULDp_dO*u$7>U!GzR`SK4v-#uUj%m zGtr?29v+f<2s22QLgY!falb#OGGm|K(TA#p*(ltS6ww=}aS=nQ3#rYr2{IA-c@-zQ zc|PNOH7iJSakXI$o)lp@IbAXNE*);Pv|7I^?)P*kPxN{O*ElWa=ir%~I&FS5xt+Uo z{=-iAE9dK*3%Rpai&}WDh^8z}{@rYab7xKY_{%9DOj)if3P#(KG8K`wD4#2`t%t+ zDK)NE1%E|@I#s{4UV+Uwe3&r|pi%Z9y2+E1?*+0*80;5Zw@n4?NQt|Ck-uEC)iqPW z2mj~l2s-lryXuJdJL2=K1fj;_${C$^k0o+mTNcBLT>#+&NqdRiROZUZ(25FDr>w89 z5!x3-M*yK432B7*PsPH9nDWxc=jSc=Zw1i8e4!Yas*wQ{Rxf>vr*&J%Ab4B>w!tNw zGMx-xy~VIqW$i%HsfZ5?n9P!nsKta*WmpP}5WkkApHdeFBRG3`7#Jnd#JW?#jz@`i z(WG+4x|_p}2a0z|1(HU)5Asai#EAElk4qTygO*k+xr^}19m%+xWFb*mj*}U4nX=iZ zuxNFLELCz;h7!)NHE-11x>mPZpHI|CM!VW(Sy|aHdv?0&buMWaZIEs=%$a;PZIyVP zJ<*Nu1<`jcXO|8~L*8n4p2I=Nh%cabuM&)4&2)%LS4QVQOAxQNNS%vum1Qc`ja;DA zm+GYC{i$l`szI5q%&HU0XAZ6@^KD#=QuU4v!lDRPJUCREMQltV*NNVAc1Sb+GJ!LN zwN!_(ElDzyUB77u_VUnEEOg?z?C@0DlgP4?m76}a8%)eQ*rjS$7^PGmxGITS(@?w+ z#Z*d52?Z%V`IRt}Ie=2j#OC2k=%ZQGaAs`eBU7cHb`O6YThE+IA;}*(lr&O!EwkRx zU}1c0XPYo^^Mcc1!(ED0GRre*zQrfs+^HLl9cxy_ofVorFSF6jwQ^9uM{{e2oq<6n z%r93h6U9QI(u6>IRCAez0X8KHfEYt!k=^GA$N4IUwRei0UfO9e95=^rI6SH*(x}NN z_Dp6`B~x;MBcxo6m^c@*H*2#H(c93Lst`Rm4w|I#Ekb z?Txo`Hh9J6gZ2lEw!xcFURJT_I*rbhn|p>;KRtGu{RT7gx@{HBa%0wG!cdg(p7o`M zaHEw2*&FvEW-rfPzAPnSP&q`5>)=qf7W^Y9a)ly(VWST~T`d&34_vIprdee(QfhA$ zS`>Ht1_?!Yk;}g3&Jt}vlzp{E=(>#(%j`jz4L1A9b)~1jpJlQdHj2JETBlBh+3V+Y` z_Fz7HI28VeXROr!A9yCmUVEde*Y?kO&1^s|+zAHO$E+^mP{VlpThEPMPtaWjX?pqL zV3xiq#pVCe6{-I3uIQIg^o&H@*U9ToaiAAqD>zNmJE&5>kQiAlK6S13>-o6K5U?B2 zhXw1l+iiTR@yp zF*yA#hh2I-<1@D;Ccf>Y<_efpMcluPM(f}AbF4@IzMq3opWrCxZkB0G9s2!uJ@@#Z z)^q!M+_<>D868QO#i0$Q0+mlWW)DTNvN5OSvqzWZ5NZM2HmxhxNO;ktDn5%O7UBf{ zCmQy&+$CegBJih8efd=kvRAL>>CkA)FPXOkJFi4;l^pv~K8OGHa;{z<-5*O(F74RQ zZiSIS&={@5PPAd{&kx*8a-(0cg|92b0_c> z=8{c?^FjWXHf8%RHzaolt?Y=7&uaz$OSA7w_A%Uw8Ir@%7HHV+<@dhz`?bswfu010 z+;Mc;j!+Z0TB!W2NnlpQrYqaoa1Zg9X>323hK&UeqkeV@G|Nbe3z8eH_}2uiaM@>( zl$LL`9?&D7T^8qU2Dn!G?iaHU@O5#wWcbui1k_tI&*>T>qL<+RL7HZT|9~{)2LZp~ z1y4all5X?x1RrVIyo>nX#%uT)*VXh(wx;4bWD=AHow) z+Jrco040($P9sMtX@S=z=yJaxeBr#>=%Uh)d4^~BO-%Qn8Qhd5207|0$_jVkFC_`^ zzcVd@`=@W?F_##%mhkZxF=I z_NBFsK9x$DNVY&dDWyOKZ))i9X zb5_FmGeQd&c=AlJ*Bu4R*?Ab`k32VO2|u4y-f|qd@5m@j94hg-KOXGwdx~C?0+vuw zQK?I0;%WUCCh=|O-E0=Wyc;@Aic{n{pO*DR{`qoneTU|{jpUR0eP%zg6cTj1Mt}sH z9ES6mO)M($bNPQte)Q_W{z-nk#cD$SbMkYn2D2>*k#UO6+E@BT{d&yYpSC)%x~RC> zqpinsa(0-XeJYmIqVuyijB||WI-xBA#q0I-93-L>Njz~3VvpVCwb-X%j|@FYU9(ONp9`Mk@dphZH(M#BJ|t4PU8$2fvQFK= z0GTM~hw9`TY+MA7l3LhMj+gC zI*?(RV0WJ%pBIrm;5zTqL#5nNw;v|!QZf#0~rBYGVc z@4W=rd*9#@;Vb;Tqc~Bu`EdA!zXI=y@C*O=R4(%ufzG=9*B0OLjmNo%*Drpr*i9J--e(MU&qqg9}|(8Z)J}+oFfjhZL4~A_8ZrF9(hh*_2~e%8Goc-!946< zw{_HPHzI)f{!Bq1*E1a*wZmf=!bcnrM1$otTOdZb8>}3vL>xyk2`I0$mN0{M z&O#{bp~oDaT+W+`dBDNZhf*9Cgg7A5J701m;&35zqn8A>B`mj`S<@MB@rd zcV^$=BZ3&|b?gR`eAI6sj#0;j2&kfI0Tjx}H0rq%BrZh(XP*#{VkCktOTD?k;iZ)j zyXmFps`r3()RcDz@MbseHhttA z5qpLU%4DYe+-Pp+&;P~b@YmV`JQaDvIx_GL?dv~C*s=|*Yv~5i9`~4zJw8^47V8kC(!Ltilz4t#HKrnwDS$le7)^xJws^Zyk7;ay464-(V>i<`q9J^A7;t>Gt=+ zIqGtph(SGdtu0=ir+94z@nr`+8*;`U@O1Tmw}-aq>C?GLgCE;Ns+=FwCcy1sfFx74 zk_!y>Y-DEcucjX91RcVMBICDyW@YN*f9| zJ8T^X&JOMUKW2wZn81umlVNtrW&FZ*>h;!fV2A6W z@3RW&S3u(^EX(o5BMi&2V`i{Hde?x=k=9N}n_Klq%+%*L*5fwTZw)gsZr{wHp@e@>gSwBA6^LTvVd6l(Pi<(0J|(} zpYYcvrT$QFMdu$bF7NJ*nrM3|Dbk6>scM7_bG)Yv83;K$NyAQ6h5Mkny96*mG|U8| z;RCTxd;#lJc-Q6LLw19MX zgLHRDcb9ZYcQ<@D&Nrzds~($AvXVnULPuj?iaG^D{vkET<#bBl?P zc7Jbro`+y{UOg9mO_~vwh(X0~bB3-}TWoJ~RFudbeOQ|W9`&s|IBZv>fvDC&TKI!L z?6m+agPS{{_|7>JC#R=@12>q=YBd74r!1!hdAHOmY#@<)g76%50JSlLxMPoj_((VP zLa?=6OK@_OmMq&g(PIkRRrw8)tEF zWi_MVv`ko9+}=?MNua9AQHhj|iHUx^1Y>Jj9KgZqt6><1d$nq-4cs_6(YwPshC`re z!ZDZB_?ua^YK;wmRo=Jm20jBf2Uau^W(Wt;-Wg>8*Z~x<4iWg%sb9}bPy_qhF$9Yp zXHqF(x24_#IB{DlHhm(cx>^{X1G~R>JAEhjg1udcLzJZXs^p55#9u@F`hBO`h_=AT zi)G1oY@v@F?>T5RI6qMbxu_sSIL)R1FIidk;s%u$I9eyJCHI!l0G7g)THJ7Ly3yKZa?`ej!5tUw4XxPE!7- zPLc37H)YG5CFgNPq1^ZTP!ot?hc+Pv(U1wjT%ym%-|LXOktHtc57;?nIsIocnDGr! zyGZDM28o>iDC)J=S-+5Wf77#N+2IUx`2%5q#mQo&8zMdb+##P-Tyh5sNvCRYr?ZqYfKi z38a_E2Fak>i|Q0Jqbjx=CHR@d<;{C!)0)SAMe77-Osi)&?zu=G8_QYPoCwpZ-BLcxlro1{ zL_bhXr);-9h#PR_>N-j1N{t{3_%{0xXT1NN7q;8I#iy8xvO z`9va3lE8AEkxr5I3_U26hhTl;fOSO%xhj+HRQvT``=&N!KYoCE-B*q63jMmSfXVUI zYx&?VZ`QxRhOs>hCV~cWE+0(qrg#qYZis^!vo~8!)4#lxZ^DBNlnZEJ&P8l(W(F^W zDoDuL1NLGb|JaKi{u=T#>zk;$#6GgQgi|Wxigt#*-aV+@K3LGrfn2EmKJRz3Fj2Jp zUhOZggU5e#++-3z9MK>1$2&T^|YRmsXbr#?$9R@}NMNZmCtpuGdF=axF+S%%V9+$Yc z*B&(roeXHN{N1(DV*S76IELkH%vPpEZ?ATXcH^c-9=^?(#D}~)Yfe1lJ1o~IQT&>= z#bp-X#9~!*hX^p7YqP6$3~Ax_q-!T2BZE~Te=3mvLO6%0@v(GjBd-Jp9i^+fjBU-s z=3i?RMx8^sB^Li>7B-6Khe%T~ z^){%=+4&LsD#Hj*O7p^I79rxsd#QA`fm1Xp$8TimBg@0j;a2^B8isM#gVFh<($Bms zRctj9@WQR`h+id}b@}ihQAk8pyInLi5%b`WS{WN)K-8L9dN)WA_vltP6kM4dS%dnW z3K;%pev|y=Kk%C>Sj!@c7A${z#=bA(BaSns-#Z)ri}I*jLX~{ZUr~ahfX^g5mU9|I zpq8?-bsHCFar%>NHQbIa=&FiKx`U&c57Uum>5JyFb(_KQDz$wPgZ8*0gnw^yQGkT8 zPsXP?hOhZOlmzSuC zeo9dk{&N@>mB#Wg7#5{*v>fSJ2vCk;y@!VFDRT=dZ9P zXImPkk5$Q2|8N-PMM6~6pc#%?$W(ua6uyvNWPd+~aVO7T2$WS~Wwz=cW?r5u)pcHa zwqQr|4uA2A>jLa1dh0Ma`PejF7R|UA2=CcgZ)eP-j$07B3@4AmnV77HlA_cdj75&_ z8QJA)w+0O(OFuKtSH&;KMTtC?N!1DHxft1t+R+^i84@Je8mmIOk4l`fGyV1!nFNC6 zyBH_yEjSn#{`M9nHNJX_xRUO5-veVVoZJxgzC5`mG@rChhJB^|jY@1 z*Ko;sT8icZ$o)0r?^&eyH-s1DKg-!B%Iyj=GB<4oI@9ORR$;q^N2(8b6;29>SPoKPM(c!k7RXMPjJnK!$i2rM?#j+=9 zZIM{DMNA?m^_A$;o9{{N|GhGk7W@ahf%u!<AF7hnGE zERqb(3q8m*a+pNZ{{lzxp$QkL;>0Q zDzev>HFa!nC6Bqsly#+*BF&g8Mvi_MM==~jrkjXXkaQ{qI=cIAVftICRHS)!FeYfm z6tezOU%;_6bFYYl>jV+a#~OuwG)&n{yzmTMcZS&Nh5Wv`@EetYMLzXHa!a+JbwK?5 z{lQ-tC9=NeH$18Exc?U6wdt2B(|*RUw9akgAvO;o_?xtNTz*3^BPjqaSbZSanmO@| z)NSo*MVNKC!(DlZyl;)*>Mw7-k-t*-I!JjXoyV2l+4=rv_Sm|jyOq5eo7&E%{1`%V z2|KsBwlsW#w?}r-Z!eW*V$D`0yBzP13>jv6>`$h9@ZRR;TF zyW&D2-lznz07GXH4Wo}SGsjXK2Zo()stZc1UnHV5KThZ1s>T@#2k~}@FlBL-Z()X9 zyTE6K!2Y}l&N8oQ&LQ&fu$J%`s3dheHuWWw^iBcwBZdqBKYRsoieM3!Dg>rl`a;VF z+};T#=qJ&&-Ir*ELsLK{37`!H0BsnJn0!bj$ooejsn}*!c`icL=CKC)u6aNEd>F}X z^25Hp*nQ^UYbWWZZKlL|c`)*HS2(M=UvLI8*bCS1-u?y+7r`2QAuip& zr&;6U4@$T)wWGFT+OcX$LP=fkL;=SUaX3(oHzv=X!%|WE7tr7-PR%yP@6C@boPSXO zS_!socKfx!O{W>GTTgcqbi4?DJ+sjqJao{KCWs_v!v6MYzfDg@BRc~sRj@4&nmyDQ zK^(vdc~t;TpwancnAl{+X=hJce^QiO@Hm5`A#zy5kiM(Ri& z)+~J8%jJLglGnmXc4LK zMsNX-)!^4vR`csBYlsiv4$6%HcYw16xWnQCz#Z6%!55ljFjr{Rw0)2CzZf%Zna*3C_CSsQHs=R_|c>O$rl=`IE2+V@vWVmUBs0_$RPp}mI1qAN=q8(+xm*;h& z(svB=I_ANy9F-CN$sFNDNy!!INjcc+mXi_MA1vLS=yBgDCZ{qvCLCHeDy*+;0bAU^ zM^~qQa46-!%!c+6SsdSubblwyI?!>qG=XHKd0#w~P{c;X#FyypGL&CjFhiIm|03)q zs3Opz8Xb-Pm>~Cyn*CwCI=66K&t%8OO_0!OR!*v?gg4lFldeaj=N3%}{Wu}47(@Tk zLY)6z_PT66`xz!0v$fzO@-Hp=!-SAMnZ{yg<^1~UDvT7usYNyZ`L~RM{W6G5Zk$xc zhJ%;R%H#H_9iz}#NtedTO7HwuhN0EF2QI1X8Hli5W}YMVj$Sl@f!I+y5oszhQOanJ2PBi8>yCa7JVZfYAWDYg3rx93?i3p-F-B(hCsF`RH1ayFN z06&dLs9lV_Hb=$<@-Ozzp3;mBD?x#Vs5!}ZCLmt8P-*d{xZ6YP%QIaD%TylmT`#Tk zDuHLV9{1O)V}WRI3jL{R=0T#@8~Wf?UrfMk7T z;3LpUd8j<^8U_-&BuutVE8-f=bY|{SF*8;Gel(HteQc`%CR9ngOO6iV z5C%U2wJtB&M?z0*=t0ST6-szZ>e!+3Db+&Eb|Hg=E>miy$Ltf_v2ku6bA(P_fuE;} zw^)7j9O%z?>|R!olyX;yBMu$bCV^t7Ol~t+3y6GHmrJlz&wEPSt^*wccVl;=Iqfko zL3{zTIW}-plF)}W1f=pkx4KxQZY~QyYL*Q$Ii1(o>02(`mOwl<9*T`s#_Yi!TlGl$_QCrcNUh(o3xDyT%16>nZb7pEw9QCH?8)7yLJK2sdF6f7^tD zzQ6c%4SY?9nZbzH=|`0{0u$CX@xje8ReY$3nZLxTsNQ?TsR%W5*SgNoOc0Op$oRBN zg9QN!h>-7je_+2~Dga51HP}>;D;SIWWlIi)-Ux@Hwk{y9+dIYDPwoI8D-O%J6=U6L z)JweLKh%P|N2?xH#%wOw61^1ek!OefHXa2Js(>6BsQXwY*AcC|R{P7x64z*#N(&im zQ7Z;AV`dnQMjG3@$MkwbBcJbX%(kLx{7l}%sO8>o?4`OBv+xY-kM2Gy6-VVDixB-P z6iFpShH%%tWe{cVn{7LSun870R8~N)iKZsrhZr#{gFNQaO}y;tOXL8zD?}dym9WUh zdA1Fu%QGhP1r2JP%WO7Q{Uq8tIs9{z8K4o;#5-f&JEka*Oud|r9V3fZ*gG8oi^&}- z@CXNfCG6DdG!o}NH~!_zKPD*9}210I~E~xq7#e5i3J@3{{OMG@UA>yJ$yY zN?$Z7yzq+4!JPEaO&dwx)JXgXiWhmO^@oAQ}n%9z`7$?7OwJf2HisBLv6g zA%v8RK~<1}#^mbSNeO?N2OT55N7N^KmHDh({wed>8YKR=F6)Ki^sPT!*3fxP)1>p% zFP>KwL=vu4smo$a-3SZCA%^+cnuP5XNd;}s2uyM#Z*V+jz|)hzLsKM2Ltd<|1#=dj z9n=3oXWr2Hr16I`(q?rZtr7V_bo}cqswAHg-7FS@eEl|XgwRSTz5uG=?QfZn_g^v} zjsHRBqx0`&KJtIdd?14If6IKZ{*d{A!2B)qvHFM1r=@XUP*50*h0*kLCKiv~B>3Of z>e(9k_tLgC3}!wv1wU9&#S?wnA!}mWv%~(}MNR#+i@Fq}m9^}` zy<3onT;rHc31OVZ@HYjvo*!dt^TgX;s4sLklD+>rw(D($gm&c^5vCmBxxVD9l=ZPDmsyLUy@E0@l#1{F#`UpL}Fw3+m!n$uXmprMN5_`X2O(AIu6gwmG5`y z9Q3Vzr^PbgeuIvRXDfym@ovepz18(`lWM-RNNW8Og=k<}`x`&|z$V|W@elKwrI1eU zg?sIQg|~-V6(%4l(jltkDand5uM{VT1U+jZQjMecI@!h6Qc;C_gf-O<&O>L}b#x*R zWOUl0_SwOy`7Yd5Lm^6W`P~;8W&-BbSG^DNA9^1KWsm{a%v%&N?8%xRUll&kciMB| zV%X~GgNzi!s*YUhb@ZS-A}PF5oH&h7F@I2;W>07anV=gL~5cFSE zoMP4?o~C|-+#mXHMn;kk^R%7DWSaesJGxE>V7UfHd)Q8M<&zgi-%qPnAgmD)R6TpCf*p;g7E#9|X`6xfnY`Z6)tYrf46 z=%A1>u3wWn1TShXNI#Y7LeLQX&|Fr7M7kqrW=mrOA*#HS2mSu*^E=BldF!N3P)Q5^ z>y}Z;ZPQFomMQ1(?q@YGB}7V>Hs~#)K-F`hcvJxNc-lo@}x{77X$jsGhzx+ zuyOY)#X?OluVSA%!2@6-CAy==76v`aGuHH~uR#1t+30%lniR!8I*s`6ceZA^7b)A@Swo&G-FxnZv z1V9;4rlKSt9bYvDlzVD^{D*Q6?d|4kxkryf`LA-1UI_X>$~}rV|61-j#(?;r0TCaT zG$qNu14QXl9X!m^ft-Jods5|J%ROmN8)gYE{1K9jP_F3;(~(D z)(rl;)jBIB@6)TpCujD5SMEvm`VZwEYp1`;J>&0xmwRN|;{Go8u<5D%#fQS=#l+L+ z1dF2fb~Iuob08+Cxvw8)o|cV09i$tC6$%AgUA53Y9lt~95u@AuHOe>M<1}8yZU%Fw zO`q2tF9;nJwFw6FX?I7*ruVUy)zbeJ4_y!;sr?|ym1!2hI89YDb1%;88Hs1D*8nun zW_?>+?zB7ePT!NfcI&i42x1(m$E{kX;hmaV6XKn9)EB(JN6~FTNKt9fgNE1Ki);z@ zLRs;Si_j)I#6WV*4QsJb~93mR=Q64+{zACZzCdCwbH!q!G^1=2O5XqgOS$dfR zH}o^MeSC|+~sDmkyQWyp= z`;4-0w_YQ7m=)>T{bcIN4Ln9t85hYVtk(OKoTJ~#+}5${(dL~&4))?BJO>orgK8Js zetmx^sMYwGL$HOm^n@#m19I-%x%mVqi-LHv6Ifsw>vW%co!P$nH9vj)z*2pY*4(DL zFs(G(nJom`6lYN=pl<8^Co`Lc@8bF2^ptewsIUFc>sZU~AAH_Ld2ypVOa?~GM>$js zyDOD)CM9q|E`}tqn=FRsIf<?lW^qEAlB|>T8ro}D*#`j>H_%E{JTRu zi|B71uP@1ezXU!_Zuz9HmJT9BbyJl|*f1>`zoY;gD%~N>J zknYgkiosm0RnJJ$f|@g~Wmsat;nTlz4dA8PQh0)7_Ivn`c@py=sHj-ql-oQE8V^S9 z&%7MmLz{c65uXR)4+CuKyyro6zsvq$?NnLWOkS;t1JQn+zttX-l$X;Ya}JOwz^!et zMHLZl6cb(=tXbC6ym|W7(eGSU2!JLk_36U$VE%pWmL#GMcn3>eNK&5Q`Gx=|xvpaT z{)ZNL3Z9MSw+bzOGZ3+_*QczU?AaDM1qz<8^bTsZ;fQ>6^W(;`Q^NYdB8b##TtE1B zXMHka$(N&PcjgEf!94{3;Q(R{k=l`k)PHpVAN#oHVNk|Ii^>9YFe`7{$8bijh5kIDjV8=zCSuCJxp-r}Qo{>}hXpQvjaS z9|JtTy#t_9Gt6t51r~4u*~Gn;S>B)n^r@5vpikB6nWYD@>qpUBAAZuJiRDYyCFokH z5WEi1f@bayHUs+{PQO?c3i)T7L?P=>uU{bPPj9k9buhXHB~8}$I@z75vjGg~%Sp87 zNwn)pv{QS7-HDb3z<_GYwkKcZKd%gEt|=bq$N+}y^of`raX|R<+JZ@U9!PZ_NOm4b z)Lx3ePNxQ1FavnNC#6qQpt|7hOjh4CfnLkid@c}gCn-7G*iVRv|cks|{ubPp# z@kc?w6w*`ziOXfIFe_@#Gc2egEW`a5iUIw^i3llIM^a;PE}x&YF<^+D#o*A2_)lD4 zM^KSJ%F6Iwb5Zomd1SiU69J35+81C0=aBT32tZqnT0R)9CQ(`vOALJiJPF}Wvl|8u zlwj(Q^FejDiVQ2veir1W10uZWX(J$;&T!x((LyQ|Xi3sw>}Z!$Q3O#@tr9hp$EgVz z^NWHo>ZD>XdnUI0RJLEepfhN?Z_5G16P7%1Y3xjD3{E?hqGm)Ji0L;)CuR&ZQy5!W zV!El<4BaePkqk_Xe)m)ycN(g{_}ZDI0fOuq#GbH@8K9%$Wed#0ct3(OC<1G)xc{o| z0p7xY8yOWjuMjU|-}4wffk-EgopW=u#D3oyKGW>uhh)n-^O>^HZD#{EKTfOBe9>`r_Qw+o3Gr_9PdfIR%yfGUs|b zx357`E$6TCgpnC}@~%}^_qC$(gz(=gD)iUbI*WLI%@7p=hdBr>=tr2A(l9)SQ$OCg zX?9P-ulqqfrQCjS#XFuB&Pe+iv0ww*89#) z7vzDx18Q$kPEK2(O~FprAWPFLtyf^=_5)Tkj)+0l2D<0T=fGnSC3nPUq|U&MP8Umm zSLOALj7`o@|D?T{;Jzha^7~(Jxg0B>{w?##bQ#+>K1EBq`5&D(GKjL|6RTxAhg?U9 z%HES6k)L2Mu=Mb;20)9knmq%?*p5I@5~{%$X~sc$N#>?YU6HXrCqHlI`oN%Ke7^pH z;Bz3V#`ib!`6q!ip$b*1o}?OA#PxmJTDv#fW9OUf1+AX^uQU?%Bn!!bNb`g}43Bow`g;mIr+D5R)t$iqZm zqEO)z?TFU)5(R#c(Mt@9Bsu&THijCGmFyn;*pb9ClsF{ziL$vWaWZ*!AZ)Tmx!y}V z^sP5XyK8*GMRg*lD-S+{B#M!~wtg8_gz+?!WqzBcxot=%3kg#qc*K`}U6Qa$)Ua#{_S~@xejXYYx2L)*bT6eD} zkOm(VC`2(^q@zx*%UG-Jpzdozrl>cDQslmjxKYB*;k3kzV&<7)2Z%yy=$IE@GpQ$M zw=iUVn}!c+%Cv*d$Ty9Y3S0~3D~hMua%C18ltE>|)HH3;Ok<3bKdW1SAt$;zA+wFf z2bR^p*QSS+ULmI6hO00lHZ+q;As7-z))ZIz-V+x?S~zLyPe~?C|KZHlg29MM$c$Mq zHb&(0q-h}8fKmxiSjs_4PyMFxOT09CET33UqapXY1?r$2iDxJ^^8#6&`=&xoBqhSF z%d}d=vfUWNYK2TBg{{jp)R?+mjVT^~@93DOogHeI&Ek$`VIC{Z)WMdo_|SuS!+UO{ z*pB$ZeQDT`AWGk=s*tH;fIMA1U^ZTGF#Lt_9@pLSaoe)g7SWQM_t6OPGbh4Jsgf?? zcY-pXKn_GQitggQ4k(pHY73Hk21>{Rp6>5R^B9?1^xM$ljzb?}CLCJr@nJnUKUXbX zA(-G=Z~i(Md1fDk3aO&8T?h`S%z=i#URjv`c+DaC7RuIlymvm!dHMEwl|cw;;=)_? z8LJ?xL|I(@9hz912xW;J6#JXxS+Ns++T}IyxRcMvWUF3wr@TammkI8D()LFEHYG0` ztC`AU#Aa~xPtGph4quqSk{Z7bd24cMLK~$_ zog~a_l#^OBAs47Zd}5EZylYP#cNB2PPlQ}#tIi?{G5*!z%O;fK&Lm!h6p*+>7zt5_ zpFQf69R@=#&TUTNN7j*oZ9RfMrZ$g?F_fjNLR%coFWH31%3qB=Bhmn$tB8n{+%Y?E z+kEls>Xc1s2JFWbLfPIT)uQWn+UM>4p{M1|<)a>!2jWZ2eY8GLnT`o>@4HV^T|<3a z^v|IUr-O^4o0oG#?Gdxqr=}+pZ^9EzGxtEH!aCkO8LzG1nZvB3FA};7FWVsr`){A= zmbKzrJJm5fCB9JjVqRe4i#>n3@g*`8<2q>}voNv7K`+1y6itZX^7IvuS<@i3)&qI+ zTqL^3?%lm0zt@5AqiD6)g%cK;ec!H2XX`FGBL`W(r%%blt=9Sl%>x}y(oTNT(e#RO z;u0-kSnD1GiC4%O+5jbR@Oi&S(~58^2qk6vb8`PS6d#;O$0;lJv9v&3Zg61R9@MlS zxb3O44e97XoN0I3lVt#&90zA+#DmSwA+GjSxiw&w_HJ)%+2XeHflJ$$Ev(T|b5kR& z>&R3Xz>;sgC9^*#le~qCw2->UP_Zx2tVruUxll^2v@D5|CJC`Gs&>8l>`Lsxt!$Mn ztxFOpZS+KJBaI>$ZZ~u;w^BMz;N}7@{T%J8QLPo3W+8B&m+c-k_0HGr_6dKxE2BQ5 z{mZchRSi9%F>{OGxLXnrQy?4o(CsgvS5-})m|rx}rxnkM^JPS1>5)Ru~(!UN6^Mjg#xLz>!Dl+jvR3JMQy z(!FQqXxq#8Wqt*lKgtxPc+wbK=`4A)x4At&75{YZ`Dz+hjkwTu1T`neaV^sK zMr0i$#E`*FE|@YXzV>MA2tQ%YWtBZbAdRxqeDRyjqHh~dsdlcFF!X?Jnm9L7(dHGZf3u=XoysulPTGy}b3oH-G1;il{_g!{s12U6-MotiKl z(ZcWi2Ce4i zta#m3x244fnB+9eHE`w$S11UY(C7DaVj1PSI+lw~p1sxRb42lsU4Cjkr{+am49W*C zuDpMiSgn?8OfTaCHP~lWgaFaBXsZ+i$h1J?7Qy!Ca?JklYStB+R zj;upd2ie~tpi1CV^U;brMp!Y71W|QMz`ch;VV+nTF{jx$yOFg(nrrn|54Cg#UPh&r zKbz51VK4HQ3Of%2(Yc=r?P*HgFtplR9fsR)L9ljpXP^Ai*BoKSE8`$Xj#U+5=J*U+ z4Q1UU?IH1^0#rkYOIG`EK6F=c9uzvN;>QI#YR~OTl*=qtjIra{)M5v;5WGID$ti6yVj5<4Cxfd#s=-p^~nHhtzJyScgje#vlsf7|_* ztXp2+pp6R&Mz^g;=kxAeA$TShm>&oHOrO^LKrOgaKV{e3iCKH{Dh!&STRfkT_85z| zMc=yI%0e2@X3!K=)RmeCwany??ws?4Yl#=*&}On0%W4YYdw};&rnOWwMS^)+W*tzT z&iNv|KVy)S52e0&osy9`{!GF$F&Yoz#mA$w@lRcgkqsbn_WYcS=6Th(%woR`ZV+}E zW)WeN0>7!>?X(`W*fbS@@47F8{wyNo(sb&A4!mzS8c(BrEImJVT(|LYDS+|TXy(+q zO_{;9*D4RLmmQ|6&|1o3^xV+?Qo0Nu$Ikh{qq*Yc*-~u4E|j1LMS+s=p)B$jhP5o= zfZbBAz-~%Q2P|``t5%8B0j1Wx(5dIf4bOyo#?Pn2Uu9AjWwe7e`3Koj7Ws^x+1f_x zOs`*%QnF51GJY<5FuHUyPu63BjN=G7<@&4C*l6Bh^wxehwOSMHV6+kC6xsf#cxuI@ z>gp3wMjTgExj=(BTF>lqo=TOB{$klBC+B^IKw=eiqmPqL@`1#PNj&o9j5tYysR^C| zM>U;6pH5D+mx1qov(F>IUNTfCG%rCR0u8DK|AQX8IpR=MUb2mYj{NXaXe>!|5+u3N^lZbk7ho;FN;hc~< z#EfQwWz4epj8QUGrhn?%xGTbN+cGUmV-_z0>aV6o;(@3|ZCiEPp(1XbuNqVgNKne7 zapjA_WF2!cQpt>Q%^=7^%EWXIdxDWidY4T}ufsPm1CxF>n<=fBsW4RT+{=*X5Ra@m zZ{%Y47#?<8hr!^=R4+T`)9Qsg31V`j8uv|EGy6?hiYh9eP^IkIvQCLCm%Q;U|B*hf z##R;$zf&C!`^`>g&l0wPN%|m=rPr1xrDjV9joI&Fz9i)|WG$sg~^;A6~ z-BPZ4@D4>0+D55OQNZVe!?7uWEt{m!J^RlNXQ%y5WKQglV=3;H+Ptr~*gi}xK+`O^ z46VPcXj-(whs&nSokI>~Z9-&uch3zeJe!};AM0)|f|%Eh#ppm_FB!`tTPyb=1|5jr zW}l8Aa2g(I5yqn1u>AxNs%@;(vN>r`-n~n7gB-XTg1XhSIw3kV(0$-7L3iIgl+$a6 zoxP^KGc-BM@dZx!nmcWTMR*TO=$}jRK|Dl=$)AotVSmf^&doR!qtLLam(lY(B7CNr zWaL*3;9T?+w6MkA-=a^4iES^bOK;b{V~xiX;uWfx zXwPmw&+-4d+b27)9DEC^P8jc9k&%)14(bVe)#O`ON<;WFVVA_SlJ$khP*%kG%P9Ic zZE(U%&sNkpZ+-W_Y~qEl4>}00E*>4Ka<~;PaR%k^s3^5Ao>btAtBLoD#tG| zl13L(GwwaK-M!wwI32*&HTXNWVvjiPp7v^@*FIWG-O_a=LOW|iL|rd`riS^VayT^r z5|l`a=pc3Spp=$-rPbS$g=aP!^5_Z|3u%8<%n;SZ}WM zrZpGKHXH^e|U=8S9Ca<-JC$!I6J3<8dk?`k(Bsa1}r~xn@Nr-s7hOlnzLLa z(1gt4{jiMz|KR|6rY>Uz?yyBHRv@kIO=1Mjl4fB}$*0U#FyPm66rh#%CXlLs@-bu2 z721rSIQbdyvwYW&e@(sZX^7y|8Shy`{7B$OM3^TIZ`foUye{?+D zPE38XdBGJ#LE?T1+A6mT{#+{|P^U_8hxHwiS5WK?2DVm@XITdff(fqx{St-(~1gy^No)+_T3R;IISlP%7!f1ibz zU#^YcpQ8Pdg`6*wNl}b6`Na3-B{8|6R+eln+CGmCmH`(^gE!l&)yiv322_YYhLxJ) zRz1?z1pX0)Vy=`*5AgldTeMa*JsaEITbf%N%9dV4mNy#4C_dhw(6yVpeZFhJ>GeiJ z*LHm{O?`H|yO~&k!#m=}`hkQD$L#5o#DI~x3d_Z%V@3F=&uK^($4;9aJkEtD_2_2l zMEXNqHf;CZKy{G^!Ck_Xs^Es}4!0Vo!)C*04SKGo)b2oJ6#J?HMmrdo-IGPMiB)=$ z4ABWdP%5<-2WmmiSXDK*>vQs98y`EU?$7iS)gnbpE0GLes!f`_vJpLDpGNEzOdTnY zLHSUO?ob;bg|A@Q^kPnwL-vLR1H5a|vVp9IN*oE!a|*c-@&V*@Ipyok(g!R%6JCuh z$kDo8hgBO|8`n8VeZim<#ZUR3*0X2dr7ZpU^P*X@?ayR?{i<&J zNAkfm{VB6~<4QTI$~xOm`d*lQX1>L(r3IU8Je${hSJ<#0iYcO&pKT$SC zT($}!^&V)6Gf&VgAP}7McO34SEB`a}SeF;hf;7Un9eqWKKyA45^1QlzP+_?FUien6 z0|bH<2DBmmf=?F)7k8vzW)gRKW>(0r$2p^`G}bsrkvbJzxjHFWW?e!eZz&B2NeNk6 z#JsWgv2}-oYN~#zw%+B4fIb6Yc0O-RaxTuVBZdx|Pg097Wt{XQ(W_#u_|Nla#{1vh zzUGhdDlyQ>3qJAle4NQkA+ojLLeiLTK=@!e=W(t>eWMW;&?rcYd`~rr3imPU;ESmf zDd-W9J|>(o^#xig$|o^67~Y-aF;pEl(@&J|`+CGd5`McM_b0kRjNFoQuG&%ENVXB! z`NU&aB$`ohub*YXo?PfyIAoirEVSg4#EinIH26_XlF0g0@S*}-wxepU`b?^lvyoEI zKG*sPfs>4DaFPPv;U&fO!nl0_ZRKZ#-2a)6n%2OPn4I zhQm@AjcaeA`c9ObQ~h88p3bpsSODu!lDgdHtEP z(;1kOOcyjUnTY5I6Tb(VjV6L19JjoiPHb!vS)Hvi{5e5#%V+jc0(Guz)<<0}>*??= z+92e#Rn7TVQ~QOt=1q?cXYtxQ6?@a+Hx%3ODtedIn7Hz+kZv-F!y!ZfTW20?eg zeDM?3ia2++0@s`wQ6c538~#XE>}c4ya9B$Aj$e4{GbIJ65^N4H1i7hU%*mJhou{oX z0BIbdPuwXp@zN-wE1jN4G#ArlpL}9ujvEpc%gTK(Z2HFEIJ^w`a~bBxml&pDQE&oA zm*F*^lcGYCqk;ut>`Q5)wvV!HsDZvI&%U~1{LRMRq`~5&q``IvigGi$S5~xrN2T_^S#iiyCdrtFOWYHFRbNn-`7LkP(00PHg4O-Be@BmD z@yn2D5iDMQ1jnM{@OW?!TQJGJ4>e+yJ}CTAgsqL#%1> zqm7Sx<=4sX*A=j$YEOy;ioZr=vEn^eFfII)NDsm=8{h1tov|)GTbnqF^ec2Jx)pn2 z2orr_kepyC$V>ciihB$F2DMs=VTj8&L~ZtM5pY4X%}jA-9<+MVpAVdTXmyFVz(=t> zl&0T(@w;r!Cm;}1WF^$BtX167@jkvT6xv}qEMg<1w;Vo(3zJh)xN3BGHLC@}&}>ydblSi;(Ye)p5~;0g<9u1><74<{pcUZw-? zH?8>U=PTMLLGdj&V-Hy$PTbx zQICpLMNXS3nI$4w1~R``Ge80&KflzV5ncfxK;Rq*|NfT`1y_$+=D=mm>J|zr85uf9 zH;o+TCl0RDD?9gvtz+N{2Y(@e+w;ZxA-V|7<~C}gXFkEl)<_2d7?hCSl?1^F-@;cC zaMLeGIhZ3gHS+Q7VbW)Gs>%eRKn_c~W8PXI6g@fiRE^|j3a7*Ja#oVyD7{LtO;1gA?{mgwK6h~$Y-S^O;p#T5Qe2wo_aX-k>4sg~QfW~~Sx2GhbrfxRe7>QcFwD;8TQx%}OVgwjQOHD;@oD3HoN?)VKJV;f>ut{|>ej*RngBkZh}-iS!%2>~ue^;*P;6eXCmkLNpP zS?BdqM4Iq6w0L-GY=?j;!PS9o_TcpzV{dPDjV$2@;0Bh}6pPZijx)0h^=Dmn$0dTx zh+Dn4gE~jJy||VhoO~nKn0LqjAPSJoqPJg@UWtP64bos}J@%n**MjC^S-p!U)#W33 zRT9@yln?&wHEkykMBPi5TP52lJnHq3u2p%otn)1J&k15nV{vPJxYN9v-lGQZ+uJY^ zx%=f}RxIHcG2Zzog}N2%v`&ac!AF%`Bi{;Wm%4}O4Wa$!!mLLn*Zm*-al%LE7fNW~ z!ZNa}hV;15$-ULAxV>A?t*E(!y@GXfyoJlDh3}*%A~K>iYR@2R1gKwhd3iaJ-ruu% zL4o%T+u}Y62F5*pU)sKl7sTIsNu$GhNqzPPb5wADT&&$Jz~|D?%LbF)Y^`=aogxM8 z~*c3TVW9LcCB<-8H8HZcZ=X%Q7=nzRDG1^e+)YDSKWC;{~i{f zDJoX5)~KLGCxsS!yW@Al{V>b(-n;58C-U?qCZ5482Ti2%{0J50svTruW6QVJ`Jct~ z!Lh~rvs^uaZMFBsmmMH*x{EH5{HFN6rhNt#yn*5>NNwK|s2@=EVBgN2TQL{6G<;@G zj`}H3fTE3nZBQ4)la5W3EqfX-VzFDj*CKq`Q!}GILu%z*%*wiOsA9!vU2RaCv|>C) zG|bwpJXZre@FNxu)t6O&HOF`UNF0ZR9QvmFT(x@2{9@!YEJ@8kTulaYC!}JZXgXhW zJ>{B0ESfR4h{L`)QSzy5uD~ihwUWUs2BlXqXQWIRX8j$<@fn5R=Ot{nj`zFHKe2Nv zX}Qf9;$U`1L6S0DHn5vkwtuq1mX1_fNr_s8TN&_}$JB)h7nAM_6<5h82!`;thk5nU z8UjpWbQU_QdHaY>I5-+BSK^~N+cWc<~Kj!X!HD*SclU6gmDtOG5a;iCxxQx-tc`0*@|_f<8k|1oJ_nq!Jl~peyv~bKt$Z4)uFM(ko~eqIKGUidj{C z>1pRDrin1b?V@?UDn%_R=~$+$(Z(6V^{5Z$G;VNkb$U2De%^lD_mUXZx5Z0~M;qWQ zA!BbLHrln~#$uO}^wCHCu9rpQnU0J(ZF~iCyW@JqvM@Ls)3ss^PP)A{sbM%9@q!C% z@<+I~eZc>4rYX?+LF-*ow=8++EaBp^AWR{qWg1*Pr`i}v^E3Z$E{4n|)QL*&PEzp7Xg+8qJW1J@g3rX00JzsiG zckSy$WaRom5Qn;Hs59GbG=#jnFg)>ufxB_(^HktVvikc6wFSW1W|F*j@fPyc&N02+ zs_Iv@{`nL$EdBuJOwjO4Sf|{)PHSQ@jp2Qy{?xm&uVLwLrgc$ZZK~JPeZP|kGha%} zX5t<8?YP*GhA&7mZ;X1?2_TS{0s_l=himxqdzE@XfY83GxpH1s)*T4%qEaXIu#_63 znDi6EPvV1x(AGvq|G)|M1vbH4;x=zN;LE7|_h*VHb|#i9Q($;-S`KJd|m zT)h?w>4X03STONHNh!<2T-^}7z~IfCJx9MGW&JoqkuWnyQ*r6n-jE}nAdT+}-()IJ zS?x!Zi8unNDB_IoDU^_D;)SiKy-ylP2QO({j*hfA-wm6ra^eyEm|tsYdD=X`Kjz#) z>r+fqy#Q4ktRrxpDl#<$d(_9{5;axJeZ3u5{66T9BpecE+ ztJ6}5rS|gUm_vJ{h5`4I*ZhEcy_HpS4Go+=kjs$4ddD(j#402)uJn#i z34ihGJ2QQbW=FHwILPHY>Gz$kVq_xTmJ@x1^X@uU6E292ELd~`CPPq0erdf>@yQ3| zyoGdFvLr@cU-ho~f4vE}yx^WVYwhr81WdgrLXLHFV_)N+e$d*_b}rY#SHk*%`SxnO zA$y(<6yx^p1RoOQw8IX-@5=Doa>HMN>_6ZT? zp~HGF5VN;IB;sUN<4@UM(UqtYKzFm$#CZ?R;~(H{65Hl=PcBR4If3wa88ijNIb*1Ga8 zjf%pL9@a-e%wg-!*Rb{@XK8&se3rk6_E+i_)Qr_D?La1jlZw4~iyY{@F7BJ<-lx&k z<5~1B+!$m`+jhOGB6|}u5QVnjSV`WXx6YO8X>-E$5BfGJ6EiG*;WIq9GB&q$=pNjD z`M0IXKgZGbO4}DC`Iy=YE=7uYd4MW!gy*B3&RfoFbp6D@>8lLp>BqgY_Jz@HrZ%!r zgioGTtY~KL<2;=k^CD3zkA9o6k)OQXE~aaZ=h_)reSjulre5?u(ZUX{r*U4Pr=5H6 za{?$nFVKnV)pM5>5Wn%VDHJ^K;D=$27uPcnABZR&Fj2g$hYOg_ASh2$KiRk`lo!M& z9TU(ys3+PxL@Md?$cY!f@ljwm{*w;T!z}@XPb;XdNAPo^&I|7sNc=l^I_dL4&M*+e z_u2hsfj)-GSPeY%%Ws@bkR4LtoV^uy#*AQ%PHJ-I45%Z>dbaCRWy^kAZ7cII?JPANwlF zv>#5t$m+AOTEV;nAu`Z*IALM`yqwknuKPe;W6UgX8=7<#Hj{s@Yi2y+K0n?^Ef7jF z3Kj#m0}Yakj|OAP#n_`*)Lox0Y{piGrjqak=+v5y8>g~B3H%c5vR3#k!n zb;T5d7P(a$jke{TxzRVh`J={RrPg@^iL!6kEsi(VD&L8!!z|Vel|$8>BQ^u zPpiLGEZ~&+Cct#)BzGMf|3_;b; zstSH9l7&k*b+ngbglB;RX5}-@Tvp$+0a7vEDCqK6U_*pM0O=>yDCk;Bcz~`8Caf>P z=?-FRM-`3;>ok;S%Bmhwv#laqdgrpUVJ%AD<)u0KQ1#xdeB7_GlDv;(=_-0)Ca=z?Z=bywgfWW3#muF80iLa;;1QKAM|F3qo&i}S1H za{g+${Cu@swgL14>fjZ~Kr{u8yi>Z7waALnf2A%;a&CAMMUOqpBL((gJ{LMistja9 z?H#W4C$6_^dAV&bT^k{tyVPx+2BaY`K{S|VblQJZN36qG=L~ep{E61qbeY@IQ)}$# zgScJ{P!~&zkWghf#o$B5V4OaLpTyPgPboVd?Oh&Sy% z9K#G*Mdq#c_Yitvm#}~F7kE{LKW|6Z!rm|TJf;Q2rOBFtkh@vszbo|L3);uC-k$|1 z44M72ar;lm6Al=YM{o_MeZC@b%5Ws#=M^Ewx}&}4tP8jFj-PDp9NzILbz5hGUlm?OeA@vdtUumSVWbbU=N*iqD8D=X1&Q7x9PxmF^V> zTgX`vI05KI$qIm8WY_>a24^+EW8gL)ON9RXWC?5sDYr~zb0{vwqoqxln3!MrbF*g+ zj?L^tdfN*Gr5D*LLwMd@{y4u78Fcq0@yFFu8L$r(a7TBEfHo!EA+?iDv3m%NtMin64~upvJ(zYC-Uy-l28VmgXWKb z{UViFsDc?GWi&S2{@q#qNB36`Igjtx7tiNEPn3a4H+OZa?&Q~|(K`+{0*RG4%)vVY z@}mvl(deb`uQynYnljG#9|VqT&B~v8A|U(UerG~%`9fEbob}3JwE2_uJ9AD-9*H!> zmg9uKG8piev5c{+Vf0mpJ!+8Y%Q(N@UshxHZcH<@sI+K}67TL5l8jE-`p{8__8?@Z zzLintxnmfVMm@AmPV+o7s>ZgC*Zmst5q1nkE1EKm9Iz~o>NjHJtBFQ?PS*0qPMuMa zTdZ$qjt=_pr#H}&Cy~rLsjG!qOB&$|YOMQ`H!QIB&f?zS)$jLQkefp6F7zg2u8MM~6ioJoD}F^6Q)c`t?Sgz|hqnI$f4 z%n2SkxY*Oip021tOv7Yssg+3~XivT9vl{FnLDu^x0dRTwM*h z18Q-e_eR@PEAS#@*PoxvtOC%ETXxR8ZOZGkA^QtPE$;nyChYttcsJ2=KYG`>V7OGC zAA>M2n`{?a+NKIiH>%+ZdJx-vSqEGBRVg?}3MJrn?(DB+tD0)De0>vP5s-nm<4s0e zO0p4D*A`kC>3jTAseKe)BN*_+pt@YL9h+Vk361r}=EVH8%s|Uo8RKOx5`-rQDbDM* zI9z0scS_bdH?FKfqoA_u2Jr>eEE&J_d-`#EE?e{ijrm7jr3?<+PeyVrzqsOsiM9#~ zkL)qV-wx?0YmDnucwkE+WaAMo?CVTABuyq4zG5SnWdFoQfN_09a{njT2wd|&uo3sI z)+LKmjO>oq1x}ltFE;dT{dhLi^U@qt&xQEU-7W&94$q;AB9aqCxe8~RMdj$EFcQBm zyKXZ)E$~8k)qK<6i7X-&BFvll)(%-~%iTu+2HK9*(P*D`0DrEgQo}Ae%T?K|P96fh zDadj=CzQY=q7bLD{}a7b^Vc)0Rzs_FfKgT_NRijpWWgE|GtN?wCA ztj0`Z0HhXuz1u}2s`H<AVFL!q_hIBtO~rl~d0)ef z2Rz8JIo@UKA_t~a(c4NfV7Y`xh}Q1zgm1&;S*D~pdVQ?;=CrOo23KNb{*wf68Pd0D&RaAwpFy{DK2Q6kePp%+D-w$HX=lwY zmg?OF_F=~zYcTDF*c|=Jrp~&0*AUi9nZRLh)k>GkVWUC*cj^l3j@8w5&5ad0W96j_ zJamLa0k=M~AgO`DgWqV1oxRql_@};vm*IEPWVN@xZk3*l*c92OA|I$+1ut!ENDol{ zw6tynXD~m-*@2Q2iBcP#5xY8cm)&o&Q?`H^u%jFe87X1uilYmBGbinAOs1wE9^cDp=W|jTpsMBKw!`1*yn0zAkAVorRM6$ZdOC~aT_Pk zXcG->V!eSnyF)LGCo=oRh%RZ05_0a7p&|9;2(nvkmU*H7GE>WUKi>bD*2h8tXnnMS zkFfDiv!P8(0{IB!ETbok(UZtNSYduYtXNh;4JrOYR$w+Zj@P8pCdns*IUg6HxxYC& z)1cn2k1cYx{?u9J;aYp~a&Q~|>`X9*3ILg^yM%NgtE@$}qUJ;wY zqAF3U7|M>K$>mSr$Jm{7?h;dKrxc>Ewt%RMdR*fwFD>l-UUnG~x#aj01 zez01|Q}pOM9VrOjYdp^o&4wlFklP%0^H0qyA(&G`_EE}n6^FDyaQT@h0mpaX;x^;3 zRBGLRer>JJztiKUZ3`4EEqrWvnU|*GwB2D%pK~^ZUwc5l{_S9@vLF%B2Kwa&yTC9Z zoY*)xiKzc}R6ZF8sHhNgvL%`KH5iL7I#9q_0+Y-_TnNs~^h5myrmC|$O9r{KgDHtr#B|w{-rg5cHI}@;?JB74 zr8PCUT2C#O(If=OOWb)*Xj+VWH3GKAOG3U>U|7pehgV1I(}fAOhV2L3Sjo>mc<4Df z8{INFrK~`75FyVCo8FnSo&-9L(%T;x2V3sdqc?b3zp{>jjUw8w5wh5u}`hEkK9onWucq z378b(8Z{kyGx|YafwT~1JBW^-Uoh1Jfr;mo7VpHG*198DrZEN6Jh}OsUm_q6Je%IM z&d#Wc2tbp>QwV_Lb2&JscRGxA-e;zn@lb$7HyJ=cdZ4Rwf99Q~S<1F~f8OhMQAUTd za)bUx5PFIEk&$1jnuR}1Bh#-KqQr)%jmm{2PsSOyhc&#pyLJA?H*irRtDfz{RoUj&=VzEy6HH0++>~dqnCSl@wJ&{&O*rP)-`%xV6 z$x#RQoWPCUk;oD;VPKAQJJ`!YZT;bVsq1mTsQR$X7DrfoNt77-v8cLQ>>v6*?XKDN zg0qce5w*lB7ZH$lG8MvKBQ|!jIYM1?XuV}K^Z`COHT_V z$^6{6L=->BWe`Zy*<{kn-O#a&nT}LFa~8K&h9{CGraK|;IC@|)5knmL2duJ-q&QKH z_UZVWkyk^zi`?+Yxw4!jYMtWm#W2Sw3iG1u1gTNQ8F^vq15`P=;i-P^P<5JWrpHLU z+7TTAd0KOF?*lN}!Z*@LCU@Kn;+qAZuBUp`{FNva(zZM$cE76Jd~vLI%|a5#2T7nE z(?rxI-;%;montFu^flX}!qDAhizWAI+M;seTio);YG5c_jWlPgdnEXJg?!`%n=4BR z|BVb(jT?>!Mm5-Kz%l@ZpET41U93dFF*-1c2YCRNMJx^4P>+A?ExZpIxG>oK@kkSf zhV`8IhpQ)Ng=y(QXMt?=`K{ry9aDSdVI^{em>x;BocMZ{xC^;E*CEk^f=EI)Y0WGJ zlVs)!E)@62cEOkJ-IK%9dGtt^n0f_VixxTt^K~aBp;8m41~V7I+i%ItB~=KcJ#i~v zX*R2z_4z+j>q|&jiHH8AYzv`K0nx=PSLNGUp6&%Rx00(#%?UuW&XUHq=xx@R6K>6~ zFH3He*O)(2m$cFe?VR1;6=|qS-ng!>UX=c1!I=sNvpi)yr*5y2T9$Y;$D$y(czldf z8BcAUU++}k6#DJh(kW5G%)W&aEOFt2CpvcFhDNElZ_Wj&r0>S%&D%9F-grM^c)t4Y z=X-oF`s{BYbGNH^&1_QSg%>WdUCBej61H5jNDkljdYQ4ip{vWR2PD+An>XPMag!=! znc8}QwHbGdaH#sY*qteS%43af6ex8`5SnZL4@{)P`<5B^bDFxj+>A@kj-^uvCji{J z0KlDVxzCv}xpP~n)gQm#bKIWqG}t8Sd}wm^R1^9&IeGV)VDAfB9x|LWO;Gk*xzO`A zqSGHoQYn+Y*+I6l&5}|UB9Kt$Wj}qYi=~}>CWf+rG!I23_(!n#-1wLFixyg!hR9o& z`T5qgzE}X^fc3WhK6}$YSMRbLdtB~0?77<^trh)b*a7f6P-nY^HWik8!cgIEQ-iNL zo*e!7N8b{IeJd6?kS8rjf37G9$mBhj<3Ws#Ut*^sgu@e zu9-DaF`A^$83ePx$<}8_UN(~v8^Y=z{5Y5VM*+Jy8kQ5 zQ+ean*x2-jk-4e){=@S-ySRlUTG<}3Bw%_)2G@k&TSb4}0WG#&=q`e0^EWU;8pBxw z`n5P-9gP*L(+PLuV10VrD#Dtf<}gJ-PDg78$nD&C0A|rr^s9L^VfAVr>A#vswSajv zn>AHlyYYpGQy~ZMXz-o%Yo6yW7MTOc^VBfxcV{9*CbA01oDCKTAaQo71~{e(P>EFi z+B(5%I`N|yDP|ae+dO=aPE-!VyD|r)b_NsxScjGHHO15Nn&P4Mf8~1OUsF8btpL}v zy!6WTl%7N#x(l~@>TCNRZ^8rS%`LR$v2)?8d-LPs=5R|3cbK4ZwT?XZlV(DrK#V~n zcK~s}T`Y8#%>@MGMD4Cq6+>Bx;{kHlZOHng(5a{XZB1+oLs?Vv$LW;v&{yl|D#ShB zY*cag=%|Eh5sg|+I~2(D0FKbPS>}GNtmP6J12<IznpIfFm^4 zjyGt#t;B~Jq11d8(*Dq+?MYa{V>iE8B#9ZJsTKD_Y+m@)I(iUl8?rT|aL~R%t)GU* zQ{E2UL@rOfjZ|@I5NOKlt>~SSED{Z+%KV(3^JRxEAU!j$WOvzj;E_GV{VcUwQ}z!3 z3HX&nIPalf;J z1WcM3{UUI3u4rMdv3;tCcPigQ;!`!_EYgn%k`t2Z>@r(J&^E2rpRTBT!pD9A%N(&; z0j?62Iz-y{+3LX!YcE{Xiq8pV)>J{P=m;Z;w?I#3YZLn6^=oHB2X9%~M7C!G&hzPL z-Q3~EU9B6ncO?#owNEB*uaMd?O)k6P z)o9=Z_&=QKqT|f%E;+ZgZK-GNiOQyv+=dhXZDVZR60!VuR)zA{nEF-s81&Zq15&36 zli$>-cHQL12hoSYh`8!&=R*{hg%6t@gw)7ilp1SR6H&2+oq5b~ZW{B~@qi?tdy z8(# z?1RovK|(V5Sgqr}$@1iLUAju_dd48t624>6uJ)?=<%HONeALC8KC5G@?^Xq2%11Q# zb_X-_;cSx8$qJ1w1N#__E?Ih+S{KN^Xmsi?Ig?q$CVW=I#-?^*^n$3`d2j4Kj2o`2 zabl)5t$t$W$z|SD;AAjqFMBxcHq2c%zDIp~<;paC%VZ?n4YU24g_+V}QHO#f_8oEJ znkfj_*+EtsHIF->0;S(&{uGtS?^+2lVwfhw@Lk9e1BWwWNjqP*_9xF|LEum15T2sI z(;U=kb)&7#Rv5$G_F{I=i9muSR$nsk2)W(B6A1R7zI^&#IxGMjISOjVVlop`Iq{XhMK!$Binn+lq?@$d|8(B2|e; z>g!?vV&%*d(KaR^01A)Zcl&kAM9CnSFnjNXPddyb(s4{e- zLw|H2Tbr)*&Xv>Sz8yUw-sQb`O>OTDvLk4u0#cns$g_Ju>V(h-@4eAggiUT#vSZhx1*}+Gvhf;rV!D|kqPCM&(sp}x;LlE=~*%D z4XLdDzHJTLQ0@5mG*5Jh6!=E(;p-nQLR-)^|p!4Ei{S)E{W6oIj> zS5$oPexcaqK~=)1Wb5oS)&uC44^Wm_IXThlxDhAj7Ml%UR!p8@*cE%BRS9L;l~m}a z#Wy^OuoO>VMc&MshFVeof6y(JOGPrUCjx=h)B~%vSXh`Z?Maa1Nb0C&eC{7U4x+^h zrey5D!9kkUtUmu0-zGBFKUNQ>8%n?A9fU)AC7MftWusrgl~5wpw;0fbMtIEdg`cin z!eGA#r}a19;_;({rVsp{0wG327ZCkBzeWEIj&sL@b8iKu3e2cY(W~R zYpcOBeQfqfvBQhlWYfWNlNmfsk$-t^(Y28ZP*U6*r_pTnDG{_A;ZMN^tQrU*J6t zMY&cPXkE3yKXiUA_rL4>CbPeFK2jTeN>-63&8yHS{-@A4`djETv}ZJBnxk8dvHpdoSl_|? zji%hE{5Lcut^ZFnMe=_@Q|ytl1)-vPFkaCVa2_;5xy2E)W&c0pDQTk=wP`q24H*^- zh*%*TTZ~k@yZhy~-@T7V*O8e#NVD_L|5W-hDoh=4g8jd>K7#E3qV*+{M*yw={!gt> zDf+fAmMrvt(fUzpKXCq=)~^AyzCB)!<=?|HhH!e{!e;#{^E0^qmig{}fXsJN#!%^F3q_OM$)Mjt z-#B7iYhPYJ4x4~dGY_5@m}Na#9PsBUk@mP_L0wq=Q||UOtSc zQ7r0wvB#9MreGYx zwRUyHRAfrxX=A4`-i>gaFc135KC<2lq!IJEiDI~I*El?8+;X|z-=+=m5OfgxY3 z$3WPTpB|f@Bu`*D`#}QI0Kh$ZF2BBI`mjrb_K9+4)H(u9y)l5rsucAwuszgOI-mm( z$pp5}WqIE>2yL00nT6s8qf zrQ5mzut$_zQ5fe={x2aJLJ#d?#c)J-q!JMfZa=V~DcL-HY{aN2j%S>GZK|LR zR}`=BchG5`WJ~v+qwPfH@}oyB;`HGaAxAC3-_4h1I8b zbWYW$)r?}yHK)6w3g4C%UGAg4NpvV2xKKRFojT3ElKW#XpGe;N zyLW|Gobg@-Q>RdA5*-%V=e(*9=_~Q<;QLNYVQaoRV%@=?2Z>b>hqSj__|QmNAO*1m z9`v#DbMWn$;b36w*+)TKxx;mFB;BXP*Sxw@%kdZ_tJbL5W~Jx0aC*tJVmQRVRww>I z3GX1aN4ZRVoDqW=Sx;OQbJi;(7ABf!>Ll((EARXoA`ZlRT1#YhhC}&_JluG+$5ja9 zX)7*v)aWR5v}MfxSTAw~){8;nY1g@OhY~9SN-lPk+C9S6OFZ2EI8Irm*zQ3(A=lnY zlS)hLSyBFnvygXAQ=8TbLtStxFs!CirwPBKEl~c6iH*lML*JMPm#Q;o><;&G${)Za z=7@d}T4?>Bgn|fd_^v+vXQ7~9@}msT+5_xa!UsbB>BCv3{4@FQ=%isCCVHRP>xzna*x-2d2eXrV1|B@QNw7Rbx? z!~D^}Vwv-D(0RUnhsR;LlHz{Qi~G(Qabr|&U3+mX6cOt(IJT)Cfq<2<#GcpI3ASxC zAM{(tp)Qx&$wN>Le1mP2n^(hX*rNo-7y2>AzpRCE#$u^|vlbLztp(!W)%;QdSQC`nS`;D4u6wtdm4fR+$J>n?f~ zDELVwB31k)b$DU4oSn1BUeDf?`W|mII{3pTicNJz4YAjD5DFInQQ3QK2Z{ItNwI!) z6;S?Y3z>aw3yF8mdrNk;n2hzLD=TAK&B*-Fm6--ipfFF12S%!TxB?kodp}to)UONg z`Y3XNunYay9Xyx$0Vnh@dii*MYeb#IMhrWxyC3y{iy)fu>M9^j04IV5u0lNUJfJAa$_|;Q* zwF-c`5C`X*C$T`H3a}yDQXqzOHA{B$6WQ%Lq-9J-Yshx{XEv-T5JLhyh1Xt@*Se6) z>Wa#A<-1Bk9h6MlwpQ=f<`z_-fYOUPL0f`b-SB?p5Ql`SpS;K3u;|rV0N4C?Yav#= z{9hrVrRd)wp>kX<_PWH?mi1y|eHFM}=G24qkEG5m8zK5RGC-E-&FS`=jauYhp>i$` zCCim?eZmGTR1t4U_dJ>gy6cd;{mFx;*x9%3D0b6T9GYL5?(bzQ$y+7ODwnv>n#YkHIQlx@ZeOX|pP zpqG3^Dd$-0(e0L_azARUH%T-|n1{w`2euirwbp24Z51B*-S?JjlWegYH4$RqVw+qLR`j4wRXMtwHdFuQfYDcvw5< z{i9=~XJE8#^tEH8Yh5I)>b!InphVE6ha%PAQJ{IA z|0oIs!fS2SLat@0sItonjroVM6RaJb`2 zxAER+3^HiESedwW=HyIRW2?AXdhHgWy&2PB|Dhpudfu-w>=X+h%1aUaa+Z#GnD#;# zKl%O&>HvjHrT8#l(C__=$61@n`jY78-Tk!`orFEVXQK>Eu9EZR*CCe`P;(j^0~?kb z3RuFO;$~*qsw|tbX9`bSKJbN`Rer>!ApE5=tfNT1DWxwqTO<}1_lNi%%ZsWnAktK{ zb0Bc0#7z_AJM)44m1SsjeFT*(Fi9i%fXk9P2DmKrRxPo0;=^sF;o%7_YN=rwCni?; z4J(sS^*b4J%w@o$KPTa{Ix8CWZ7olF!^$*J5z~zURp=HF7n> z!J%RWdmI*#+<0@1$uNVI96snFL6<+XQwgmfN5PN~&}kv?hCcqU&JeO+A0wk#0`qBe z>6xa;rZ8&Q^{Ozj!TY&#?B@4fX9@zbBF(v+xj(Qw7touzp<>F0C#WD9ayD4fVgPQ- zjUV8)EW`X)x5adMI@MIEe9d0fJ}Oq{#{kGhs}=9#vi;qa;+;o`SIvcd{D8WnK5|*B z7Rv6#-gFTUJ?TM=pH>e6jJcxES56J+;#?&rZ?#VWBE^AU3!+gJtNdc=Z<11dQ>2Y3 zRllT|iwM8v=VsF~MfC^Got4sInpJl2oI;t=r>`)j#IZvPoD!)K7dGgstPRno$KmrV zJb2J_#zL!dG|-&Y{+2&awpDD0)yuzx@&4_DrU-A9^c5}Vcpc=L@fpH0bkGYQaho74 zXD5=YB31(1EK*Jgqhaj^iy(ez@g$_**U>8D+9@n-+G(}fI1p-3#vF4IpV}#r*uG$m zY0+@IT%JiRY=l6b_a6{D7q72f7udbketerXV==Q#j>z#F}k0u49G4=Iw#&dlX}9jnU9?^xlqu5J?dGI&CNfe63n zigAK%xSMZKuB?V|DVcD$qjEW0UkL^qhxQjp9tl5i8>YN9xxTb~n}= zI3-dfrzd}lhnpd1ZB^la^`saZB4rsnuk4{9KHVnjFmtzhXu@xG1P7P3dNQ?pwf%ZD zJ%)(9`2g1%Oazg_(;0>aJ7ELyAw!l0vPpIQC|$E(IF^+u64C)cbAgefWMy30!zuJF ziu?m@vy!Kqd+E1^$0Km+x1)8qAGD0;7DKq$Wh|2$;VA2_twK#LFmlFo_PsGdNagzw zF_j~uj{*$l*z<}BpiL`|9`cLDd&~`L>zhqKg+N6c^OyLAzV!|%S z?|rRbUav2hjuKB=mo$(B^(OTT;jtA9Y6H9C)O)Pbe3c?*183Y;pe>~);Y%}dO-DxA z1aboQ&Z8kv6X{IycTJ=bsiO!f$YG?Sd)WXh_Y+6Ga8HMP`EvqQKqRt)1{rWtW~baU4y|d;B&n58v{vPZr*}^F1`}ox;0dVmb!lnzO5 zjm72(u*_%zs#3OlJBEVy(5iymd)GrlXoJp@lT%0Ll^_hYyO7CjuH1@g(H4(;Eg>Vd zrqf(-^z~ubhdnU~EGU@*qP;yAz>KBE!&mzldrvYA?jH_+f_8ODp6b6RKnEFEI%!SlY-}g_#;p4;EQR5Xz4R0jr>^`k;)FRSSTg_f zGpw&vDt{uS4>=@c zXHr%+<^9a#>WD}wW;{d{#?ceao_vw!54Dxt&Z|Mf!#%kic0svKpAirS&(s?5z=}IX zAu*k6H8+hPE;r1UO1@RfxdqEYIjvUuopFdNM>2Ds3K=JTTT;*e==tIYK#bkMcn69V z3>pr-L#qc~7yg9Vw%b+LXds8OEl4AkfZEvS@ZC%Cz2q(A=b+$=nzROh5`&OZ&s1}i zV>=fA>pvn;_AXLy^`hkK9d$@l#Wxf2FJ}pYiuLxE#n6YfK$Ql{1!{iRpbSX#zgajk zqN_d$`C^n(4dsgh`mK^`>!X1x6)hj^&x2#1ND9B=gnNR|J`^qpI^`Ic$)$cnEd!qq z)y%o)#1_!V(o$1%TD~VYGv6%A*xF82UkP}8{qiegYb-{N22Z7cNT0dRS(jYYq(l#h zKOLzyE2WzD_ovB#Yb$a4;^Y>$_ouv5fhPjTg!JMjPh4&@jFCpE)dteY2v} zyTr9P;LtECl7gKKL7Ap){5$<*EQvZv{E-U0J0rxgEy@5T4|28?(U;wocOm3N(`rCn zA-P5Ki(YAUf$l3&Vmm=-QBdauh07EruXs0>!(-x~Opi3ok+4)KAjg;a>#u&+jeypb zd7nhNWJwMN8X+bs!tJS3Ap^JtI4S1D!u*BV`?u8B?JA6q2_1M0DAlBS)r!e=XroL< zGBA@;^AaK<{3C5!U*AY48Q`{0D-@ybt}&%iogmv1*xgC5bVrE|Vh)h(*aB}8KWo`N z6v7J8e~^b#*mjy!KuM+|n0v;dk`2a`0x^WPtLM)qjm9)(xECBfmPb8uYx1NJNrG!s zm>tnpa+>yGElF7`r-)*6>?judF@ams{8>2wx5h2MW20T}*t2AbQ-%4q974WG7;GC< zT^?e?kmR9#!I2Bw_qG=rd)n{-!YaIq5fT5h0o+jpb1U!M8NAvmcQu6bU~xZD0S548~_|V6}L8g~1*1cY4Y&D+InONQti^W-FuhJNOX!%C2*v*+G1-IiY7-}nh8k}&J zx`-OC%g>oZ_Ws-Iq&#Y_vsmay`Q;!aDANLz+HeeSnv^6K6qbfWK|V`E_OSP!?080l zoxiM0lLEer57DKYjxVQakgaiU>`jE+Ufi%dZnimj!l&Q8gI_)D`8?i4l%62C@2P`o z{2Y!HM6F?vLwI5U27sPV%Bj10k`C!3Xem1+KAV&G=JP{ zUsbx`X`(7c`!Q?l7w&$eDvTg2uY&GETH?Fu)P6yiCcw!A4(Oy2SiC ze1-VZfDg@|aP zMIuZ0$;7q%I8a7OS;VM%d@bxoRmeRiAeuOJ_Pbcjx1YX-73y7qyJ=vbJk8kS9qp$k z9bp+g!tVNBKvPUU!x0~e6OD;H~Dh?#!3|*BO8vEI9Xk_A~R$X1wOr^G{wu)AER2mWbOnzMI zVz6I{suU=5OF}7FFdP$~HElFFG;vUGY6Ogkx&q$oL8)Q|$QG(e-Kmc4JZ;UcQj^UW z-Qm)%hNR?M-J>>O_)_6Yk4)*2Z!Ys+D}aVDL`f29%I@C}#tJgkXilF+Q1%)H7k9PX z1v+`;>oL2!tZvWJiIf#}wXE$Ln${ISoGQm5GuN6~xGUm~^3ED?SxblioW3nEA6ZuL zcj(OE=Nc`Uj3pmtC(+TZBqfvlnFw8JT?d1m?#!iDj$fiK>?}H5vUDU^YCeso!W{g8 zS_WxAky$+#c0kc8mI43pFjts#PYcsUK~zLr4*$`IoZcUx zRiAf{r8RKB>mP?B0Lx49&+a~HUxYuq^Ql_$*vJCY&V%5yBE>_y!M}fqMOy}z)HL;5 z8F%ltGBEHA25PMo5C%NVelH?3M$mJ}E_?R}J?PyTbi)4JWLx$)5MRk7TxL|cly&Qn zeyja};c6xa2bD;>^D`x&@FHV8x769zNe)6Z&9M5oW0+o`g6I}Ap^LU zawF*tyM+U^758_f+zTf{q1zM>&TE3SDaJCl1!>)0s){BgiAF$;FjW(GE-#x!lz*ok!>|uz7i|trs}F3jOzl&$H}n zGwj{9;WC`8m(3~N&>zjPfxnw!A>?8$fo9k}`fyv0vE3lt1fou{=iX&F=Po^~a;y1e z&T>?DFGC$Nl~uCJ*M!d}AmOuRlK)!ca`~?s7yUnLTRVqr) zpz#VaPqN)@%mSK;-ut3ucJ6YXa=q`p|9%h@#e>RKpjezG?$aH4Qy;pwEwtr5%!=DN6or>8l{Xd0|fPEMyJI>N%eT6_sf>2>h%%9_yTl$_544rWbj!*^S` zp8lXGRX$aqfQyZT3C>l2ZalJiDU~@fSAEwle(@viX^`nYw+DHVtfsaR=`q#q0xqRJ z9*cUwMI=Ce=_7UZCN}lGNX4B9dM9Q9MSO=EyzFEC)h(c3p=mZ**4;P)oz2}#0YeZM zD@mwmDK7Wfc4fl7zGI2=FnmFjp=K z(!)rh(t`&vjrNI)RrH)EfkJb6cu#M5bt6xaQnOse)u5(kE7y;!#YW|WOZB`+(P@sn zNV^1IkKxFEvjg|ZU=%E818}+>Qls+_QW_bv7V@mrSSLhCFrt$Lqsv+lLG0)?B};f} zttvM%%l5+l-JPixLkEciM3#cMHG))yJCyIcC<6YK>_+tXmex|+`xtcl{V3;ZWxcu_ zT%t|8Cp z-KS{wT1VmCuOq0*f)Xl*Yd=p;{iG;`cPFM$lU)RM%`a~?4a@bpDEf$+W2DHMzn&My z3zZT3G?%>oF()C|(>0%m}T+K(_L^%){w* zCkpatjf<{?ig)BmS$9F2KOXYI64x8=DU>@FStq z^HJ=MI1=#uAm3zUFn@cOz2eMRJq8t=_}N+nq7C?Tn4CnzacLLG=F?qA=w(x0n9^_I zXRf-u$PuK1p9C=HhP)1XyvQ542E4L@1&v4YIVzd6*2*MtDLpSGLbW zX@>KLOr$+Igr#(|@j(~x3u3~mN?(-zTv&*x>LqYT+Yt7sAKaRyh&-sUdu2c6;PCSI z%TALTx_%s&b@P%szumkKAMDhQiq7NWP5L=>`5DqOmy37jp8WOj&}GTtJuv*HW-y6j zJ@+u^F3k|1a$dI5KkiV)Eliwflo2_EGWX#8=UmN8&KP~~=1+IMXX9espvTnjn-OtNXzr7 z9?*HRiJpU|w(m}vb65*HhVi%*ZztK&Isv zjG=#Tnr$NH<0keJNLj@7f=Wpu2YoVX+QK&rWwHyF{^OE)C*LT$hLG$1FU_~uF&BqK z--uD-$=wHc%Bb*UUm(Rt4xs(HQs^PHLqvgxG-o#?YB?(kJUv&6JcNAe_rbtFa&CS- z7dWN9@z0glbAjtrMkw&;m+)*7YXrteLPaURUeq_e{P(q@ssCI7P8<>iu2#Ag7r9Uz z8N4gJ)c&7-lzGv#*b}+`@RMxuY`&DZmCT%_Ne`ByogtC!P=H|+QNbp~p_mQis((U@ z;h2gdS4s#RDRdLZ(rx~f3pyQ52zQQTh6`V%`Ee6*IDinoQq$B4Qk5o7Cg?7hX$O>t zP#9;E2zV$GVDe57@uEpzPuK(*4wyRqHlIcQx<&{OT!HxA>;5P{9ATW!>P5~Lc^*P( zocEzQ9}(X5$@LISHdeFsh~`q;{Be)hn_VZN{c&C3%Lnch7vT@w&Q6Reg7D98o*Fp4 zDJjYnxZwNnoSrA(3)aI9{(ne&%ji0GEn7IYW9FDKj+vS5m?36nW@ct)W@d_+nH@7T zGsetJZ~Nrj?jHTR&wbzcepJ=c)T$+wq^hy^oTXCK40dW3b9jIOOSN{(S>xcbq$p0A# z5XY>01kC3IM=4QFlfot-kbo++OMW7lqDMRa0YW_2*2V^(EQf5;6`%V9#1L8YY}Z2| zlu?^qHvbzJe)+F&iO_;=>m8hcI?v%~H%qhQ0IC%a{t%38^H-Gt?b*WbE|ALFWt$TM zLWw5Y`L9$kz`zvYqt7P&(|A2K1|xs2shTzPDq8!`>2Cms#qnlc;TBDzU z&vXneD&pP(#SLYf3}=^nu?tR#8$;NpkIi+dr-2CSgV?0fG)$rGB!?51y3^xaN6fXi zzif4wlsr=6l(mS9dkO(EUef26pL=bD9?K_OIkJS&d1S?!^2Nj?7NJbVF5`GJ@j|P#3ANs5{LOiaaF^V@q7bClXk^I8N>C# zvgwq%QSuCtk+SLhx%`@NdXp3f=D0)^BL;wxXx%Z(o(9OPO{o+2M5OC2P#gvXC>Whr zNlBmwc##3T5^P6R$OM^^L@4niljJoKtHblBLjhsRa>Z4Ni%H`Ww`KuG0l*j{#gA-| z=R?Gxl6@3VS)xA_=!HreCZ?Z`Ax{vG7a7_8VlYi#EYN!$Gfarji&HF+TPz-)`~yi# zfjq)EabAr)EZF%$Rzx6d_9}Yh8~!}tt(_QZq_5+>ki3I?2r&=->TrmVqX3g?y3mY{ z%l+s+LfBs_exvAxYww<{Q|i%U_#8x(;=^dOsrUm%QIvle4qNwQA{2jR4WG+-W?enU zZ-B=uei+U>>K`Y{d45$={Vc%q@I@<0TAwq)069e;uOQOPA2Oo@O*d+*o zLefbYSsUzjODSCortK{wEvB-o#$*$svQt7p1S>n%H!d{wQ!miX;LVinpP|qE5<_e> z@a^N!Xv#YtiDaVdB(@ePlwUy2PcR+5D5Ii!L{E?5MG!~KOVibptRcgnUvTD5I4!Hh z^~D53cQF6kn-IXKr(a(63Y88r$_JmyAtJig5Hu+?817oNh@Vrdo~O1Owbyyuv-P z+V#hYDbQA8P2DI?*-qUU+NailaC!Xl2)tQ&a&@dBb;k^(BszE7ZTdthN%cVOAA>Sj zL&`KMO~gBBF+ai!hm^Sqw~I~a&mwqzVN=7s_;V9e!Dw}Af4iFBiO)N5^>GXAs%j|qDbU3?rOR&+n{ zl7f`zo^t@qj$z(k9`|d1oRD#od1rn|kHNBB{7&sfdMNEp{>O;lysC@E*n~(!m03Z< zEf>jB&1`+_iq(bMSF0y8EUGv~nt}3r!G6>`&oe%nv9ZNxm$b&~=5UtRZYb)Na=HBJ zdDl)W`(vWEdM@Ru{)&VvPd}}6W4-hH*98LkRPm)XH+ zYmE?`E%px(RNV_|`hYNUII35Ln!_)~`-JLia}tXUbnUnS?Ds**<2MI94l_0Innr61 zo;y@a(82Yda^R(vK=Hj60qE9YhY(c90QA-V3IqYQMFl)JtX3#}E6-y&(3-~jicd=` zizpu$AB2_K5ma=0gvNnp60W|L-ole-GAiJx1Ss!BG`gM51aPj}@#|^;;PemF;^eh0 z;xGGx+2VwJy%6-WwCo<$@uU0-;H9w8NZjQ1au*b@sh)~$8r4OI3g080J5)!M(e?g{ zP0QuQdTKjUojc~c9c{-6eFy|PH=~u{CB#|+L*dbi{>Wq;Iu2b&BFEm74-lHRb%2@< z-|*ZV>jVA(=NA9=?fv6>r#NbfHTolXsdYtY^zYM|{yv?e`Xg#&a=Xpz?7rsgP*O6_ zKrPu^EZ2Y}H#3yq%CkEEYKIAfSr5Hm%?6TAe87wYos!tUPd3L=w#9=urD!? z^E~*>-IzbxO8P!Gjk)x8F+L{v``hpX^Vi9KS?nVq=Eaw^1}+QylyZ(_0~}uX2~umL z(8pv_qimQ}I(RSfnGM5vv4rRGM`B(bb_%coxIP~4U}arm6Wk=tH_~z<)bqn2L4B^y z8FFt&Uf@F;4Qnio=tqC4FxVwAGkWc63nC08(56Ai?68y$6cP}0wHBW1FZ5d1CMqGg z9vE#Ney)R?W-9iDIv0JLVcqxTU|PgHG!L%i%^0kXhoD+d3KV0TBM(e|G!a2jAiZ&EdD z=5cgmn$dz*ZewIKKVz~+59{JsG?eMZ-6VW0**QN@!Rcn#t>U8CEPQNPId7piUF5*= zu3O)+he2D7ond0c0VQU+s{b|Z>V&Ex@8@70+|j7YeX=m0QNG$1(+~_=#q@6!NNs*zt^_ z(+o$fHI_9$SL~uhlz+@;0!T~fc+~+5AWLXKF(&l!9$U~E&8Ek{M++87_SM!=>+0Sd z8$hYbcN#u5g7T-M?#2k{XnA80!w@ymba?=sK}(uyY0Fq~$p97{Wf8WDJOH&a*7H69 zZK7kHcJa}}s@sOM6E7*(Bkhg=vV@UW9f;`81_x`9SU_>Lo133X6RX|{0m?+*?B66= zLw~7{=m`fKp@UU6y?-tgqDm*#Jjaqc0unsbTuOc-Y9H~~xmpysw#<@S@1AX29(cI5 zyknCj$S;$;B0?(57)Y9|9a|u=DU^=H`2d5A~G-@abZqP!CU-4=M;1nCj3S-K1{zcjasa z(@qTIAjtSzv{VwX2!#heJExTgK8B9TvWZ9@KXJNz3rzgkv^(xne%0`iQRGg$9s?lK5XKu^Vu=i$Bh0Gs{BOt<_;RL&G z+8;}Kq;TU{U$W z^v1X1bSV{dtI*H%*Me8Z(`&h)R;!>b}esflA32a+JG<$Wh3&3q_KUC&emk$TuP` zMK^1-9DWMsY_KH?#tWPI0s&6Xc8|#{AUG2hJ=GjU^{r$YPH*t!CgqY&->EcxAIUm1 z$s^sHq5Wj%Gkv?@wLtVKRaCRI_FDY1)VGzDJ;n^ahDH-O$eMP!7DqI|ejFRH=TG{s z@8)VhOX^T2onY7FXMP^9c#F4U7i1BAIdCIB;$6nQUu&jI@5-f@8vRPb$eA+vX>um@ zr9ja;{K$}(pZ1azgAq*5aS#HwC*H-@7`gBzhJc4TM}LxZ675<73llWS2izFhn4D_> z{SyYqt-p{D?%1wF{ihVMp=_CK_?>-789d<0ZcP3>p0DZSrItYSjvY`DzQS(oK0-b* z5;-y#KY0r$Nv#0^tKgw_2c^W7WS4|IkYj`1{jbfDsX|==U$it;YX`ICAS~H&goHRo znEzJBuN+hbr-ReO?(bls4{;nAmLVv^ML z!eh8|_55N2?wxD?77up0gL}BgjVi(UIgNJ0^~xy?CJzcJ<)vNd{S^^Btk3Yd>P0h$ zux>R=$fU^yV!N-=IOhAFuzWMo4CNpwiU#_3k}zY8fH0TLadE{`+OfRE$HwC8#={4z znW2rsna&tV0SeuA1I)L2B-yxT@o6jG7UxLRDR<2q%YmvQV$o3%zjW`Ceu0C%YdwTM z7%)Ws6R^27@gs%quM|@H7|NmK39IG34GnEx*%}90r-~qo?b!(}l3-(zN5iz}<=W zrl!^#ulA<)2Uiy_7q^9c^gVERGWbPDzh`Xsdpo_S-O8oHCAZ}33DWC4O851LW7^xL z(Zj@@3teVzW`W?Z)JggwBBWbaqxo~RV+$Faof{8Uf0M}K3kVxWAznm-|#Y)YWok_?`EuVg=lh# zJ2gGp+M7F@J07O*jQDs)#oblOqF>HwyjaRlt~?z(p2J{PI9;gs7dB|#m%&P>JRZb} zHMjJH3YmsAuxv7Q*hQ~yksW_j+_Ojyp9#^aw54=vJcsS7t*&pSCPX-DdL2aKAp+y{ zusSOTKo|EPB5+s425Iln{@+l0dlU4BK1cGK)e zW(ztP3@?j%?!myKqOO`#Uqx1^N@9so5x~bv>Q?a$NevJH&7$PS$i&CDp;n9(b4`Fo z7C)eQdl9iLnJLulQJ{~apVmaTB9xf!$)HS1Ly%Ycoj9m}izID^c+9TydI3oogXvCv zdWWtb!kERjFKmGFU3lA4Q)6RoZv0`8y;A|%iN*Lw;^?wgAre<^Zu8w^b+G| zChbVaKJi++pJC}CFYoPNQbi_hBNei3C@?_JoOx;aSiBVY-;ds|D1J|V9vg10Z~s(~ z1OBXHF5c3t@LfGlZhZo%{L4V?#gMPLH*4RU9T4A`|`s=?Jv^v$--tUT`&%(qHM1X!EHrgYf=qAD9oZ4bzb}g(pzwO3#!|x27h)9TL z!fzMshJ3QHlm0%asU|U42l!OM%;Xjo&D8_h5--VwP1Djtlu3=oNQnOR$<@>S2n|c8 zren3YWk7aM)r?2O?SM|?MV}UljPRrhlFlVQ^NNJf-Fpc=G0OM+0SwKn5yFfhXy2bo zI#n?Z;axoxzK=s+LtORWcz7Gy{q$Oj+Aq*>&T5BFCr$1}{8n;l4;B5=V8;TaJPRh? zM1nG)^_*T)I}`M>GD>MAESbD~@CYll4f2fKla(`#QdVtI&98dD?>P=W7BgR3KQ-W+ zC7)C;NI{JhhU_JU!2gB zv_d9PgY+7kI2?x@MiK+5oM=pXzhBwA9pGlviBnsJp)7|cQe1w7HB7);vnMiIHb_D=tZiH`w0p?hz~ejL=qAL-NV~^2^yk`@@NGFIQ&(amB&NF zR!jvAJba+=5Fd^QdlK0i0y~?Q*G1O_+xj*@+Qj#h%@d!&CVV2o(ygGK>8Ky2ySVJ$ z60c13cq96cS67(URC2W*E44lDQ#iYft&{8O*L~c^Ss*i**==Fht1; zWR_0%*2Zr8ZU@d3ay4jv#mAYO-#w4b4Ty!ztSGk2OIeG33kBb|D6_|~+A)sneItIV zBqwJw#s6)8&vMM@6~}|Ln}~@boDmdr`{jE+Z=wTkx@hP((ST*W`lR~GPjU8u#biMk zPp6}Qf9bUoL>X5OJ#pJ7Npm4YI`s!moM5M4yg(jhFDwhg3nl%5n6r~RuUx^It$-Xw z?o@;8S>_iJ{%rAUIbZIoWp&k~?aQoTD5fbcF*kVBYb=oqWJ~92|8<+b68D=z?{42n zkPPX@letp&HwV2hc1&YBy{g0LL$Qb}vT!sebNq{!+KC*=<*uqhCO1^hkgsEMcuL{l zs_1A0+(a;#yz*(!?Pmhy(8@G6hRXrK z`f;c2Vc%RyRs%*<*$>tkjx`}3ZugHcJA}aMhgNg#{SE}fLR$_$izVX5rX%QbbC@4c zMvPadzR8_ul+SQ9YMnRn`RH&gw9##pr>{Bv4%fT2ESqV~92w_I6iYF-w_L5Y8#r2# zHyTTMed>2K192Lk`;C3==$dgIIm>d6^2D5hO%Z&I2UAUaq>R0AP+C>nk-sJAQ7Qj2FHuH4TA?I!H6KlsE@#mYZ!AqdS!#Biq~$wODDFPeh9TEwaIh3V$9aVL zyF+%%8qz4-B8vp+^X>kOl@?$LHNvwOY~U)$(HxisIFQ9aA9I4k7FHeZI@6t_X>rWh z1Zcp_q!XJ`(j@>V#{G;uJQsjlr7S5`CvPC5aRU?vSU!mf`k*(1`zH~+X)B1bSZ8zv zb+H#+hIFnjZ$?v8sGtMAZ(ZO8PoD!{bqlEDHk=viQV8fU>N4c>Pjz#WdBOHg$)5?X zQyOGPT>(yfu(zS-N84~3-V4!Q$@p!2WwPHi1Nco?CV%@X@@i^?D#~bn6Ud>h6s1wB zB2~&xLK5{mU25hVw{X-{r#QybRhnoCLLBzpoMUKnEhLAm%se&NoU2Xj*!lnhl_u;# zx8`d3PvYTg#JC3GT)fvVd7b*f*HG?P5WgC-xc|(!@Nw)0Iq`;Xdd9Im;OMGAtIPzd zBeA~?uf@{jdVVw$TnwvFrUV}PHDaVp>6_ct0-K+jbe$KZQ>+5rOtP8&@x%Jf#ks&T*ASu`y8SMIBCNvG|Qny zIB8$Ng|`U+@<>t7yGRdxowOOdSOG3a!0SFsh?)I3$!8JpRZs=o&;^BZ_q->U4o6&E z-#aANf-(mdil3{l9@rhe(&Gw})R4X340S&5JiTSbj$lZe)p|s)1On5~ck>DLpS&DB zoxR;PRwganzrAa&Ju3m;#s|7WH4O=#vVwpsAUJ`O;ul{0o*5Wf)yfHrqjT?q$=-8z z!)Winc5Lz+LP+llH6UjV5WUF2-Q)_Vyz^g1r=Iu!u4G-3O)C7x$n2%$raLdljHc+me4oPXvg1q=^ zk5cM?5#*`x;;8)qh4U*ymUeGz?&xfPyxNmZ#wlI((l(iLh#$-9_#Ut|>;>CsNeiv{k>3E($^esuSbZQ61CpYp(E3$Axz*DxtD$U}=tFn6)lm3qe{`Wg8c zkHYCh%W`odgZMv#YwAm)cBtpo?gSJ=cVXyszExG4-}s4C0rXm!G;5>z1E_!RLdKn zdICRs4rT?E+{hEL15HJ}uPgOI8Lyt#0p^eF*Lj#?E&~7%Mqv;8xnM z%#T+%A1UCs@^vzulM(iDIt~0?2Fx*Z*6GxQlATry5%+PL51k;d5sd0h7rRt+Ilhgv zdyWLxMlR8Bm+3WxvmQ4|5R9|i&YXWSYRAYXP(=0_zf=>-wi``E{Eb~}@R@zU0@G(a zCe4$CM;O>-_J?X|@|7D~Qscc6%K}clq(e)EZP7gaDIRDhj4U5J5?eljEFmn*1=fD28qY-PVQEmg~UWAQ3Dx@1HJ zi3PbHeLAK_Wi7T@sH4Yt_UjihFRJ0!sNSQ%M$x|dmQXeM%S;}W?ymKj7qYgZKz^L)LdO-p|P46 ze}5(DP^LUmeL@WF7%&kLg6Hc|kRlkd}JTik&mASD|?kL?@Y$!PXZ^ZLOYqs}F2n zyv0{Xk!?()9n&Jx5~7$MrO6Xy-H0pmJ;)82+85@W=**CRse-Cmvzp=S0;hi?T63DA z4fTpAq*xx7t5J>DWsy2w(+Wv+gS<9J>8B}3=X*^?C!tEg_D zq_~=QxtX4}3<$(`tB$FcgR6IF-h__*P7MJQkqdNvH=>djF5xgzuPt49cL~ePUX|1` z#jVK7hd1q-u|ch2sk&3_^T-FX;GNg5lVH=6jcL-6`aLpHTsxZfXnBc#w68?|^?#n$E*N!7@0L^_!on4}TwObe*`W$+lMI2Fv%_uaQT z;u(7Johf4I1%W^j$AXC!b8bS8ED`=0Zr?G(IY>y+n~ zlA6I!41{Z`2#3AZXGK!qy0TXd753V7!R6Tk@AOZrkT>!;LU)D+|j`Fb5hQ4qk=X;g2LWGiPQbM zoLbA=KFLPkFDHg^8RM$o9b42}m(&U~B#hcWyV)%-H4hR$=e!ger3783eYq|fD%%cE zT_qp?IrQq8E)%wS!-ic^)oqhG4}=CrRHv9k{LjOvqqDRpOed zISmSQuyT0`!8$dN#kgsHDORXMVE`4Cc|#jftXP6Db%}MA9vnGkmsxj^zdp#A09T}m zgmYj-N&W+u?f~m1J(%sUJaY%}%<03UOFEL2KK6mqV786u1A)8$kbUCAb_>v3C&qF|T& z4NfF0>-)8vai8f)xVdK5t4+Gn1GSpzuh_O|BW@jbwbF5NXqbxk$o)jg**a9QO|7b`WP?U^ zqDuC;zao~;SV!rSS>mpEO6;-Nx6wookfQn!jsn`(Ro`RFNnhFG_^TKtjI2-KDIKIAk&yf1&p!?Mm0HxFG$sBmhC z0N{U#Y^En4+8e-G_m8tHQbp@*8iD$`CDtDYhy{S&^j9@1dOxDE_uPW1XGt2P3;m;b z2itT;?-lxQsG z02V$7Bqs`LH}JhWDa$G?YSLq_|s?rJu~9(rWXCXsjYe( zqJ(VzF^p!=aYHxEKVP@t#K|I(pX;sX#ECOhhY$VCTtT0NQV=Rj#@cvO+M0!oM$UmI z;BkTg`JqPfOx3n7j9kEU9965e#~VG!>FyRf+AWhl+%P+WW^pm4oj`E`G|-VEJ29bd zD(0e$j4*8GC~bB|-OdC~zK;0YYY555++;;uA6v6M#B`A?PQHpNPe4i!7w8%ySy)GaGr|R?Yi9KE`2)3g=+&J1Bn9b-NBp9U^5I%I zNQRS0&6+)US`IhCJk#Ns)b6)GRJ`4dFsW|47s04iP)cNDC(}6WS_zyckp#zd29cgr zd@?6hm@Lf_hPzjgT*0WF>144r3?+(G-u^ifX;ip;;%rzl_F-j^7x2C`@xS&( z1#|^zBkgMpEf^Jd=*Y`(AMX7oQns)|;R^S{5Da@U%kI}VUoh`U-Jp2d_-a$7Y~BDr za*h(?pZGjO3d;h|+xCD;Ra4~Na4;`ci-#tEV||lOnQMSdy)TkpFl35S*laV_DUs1B zG2oRZ`No{_cA7LruZoyQJ#$f+5uG|}5bFs&-Dg)9yno#T!x8wMNk&%^bN9166EQnY z)>psaC0(PCVuQFnjn)vQ==Wz~_iE);i?3Yo$L5Io-M<5V;}Bi`tpgDKtpoT8{?P$a zy4v!!)4uO@cEB-TYuhtS_$CFzOjdR~&P^th+Bm#E?%y5vz0Qp}@bh0Cc&V>yJOO?} zYPz?#b+)ye$s+G$C3=j5Oj;Ue`JH=Oie|kqtuVU5+r42Jc*X}zV0qyba;34X*OGpUH1-<&a5R%ia-lja@S4^Yk?HaFXy+X|!Qi8_5zwVtECTOfrj5~S7=%kTM^u0Vi?`f@QGqu2H4fRrRjtz5*7)B9jry6xjK52~{HT<|aDV}mcz*|bUzev0VOfO&)bv$5d z1X3rkuk&g`GYM>1Dl5*Vzwc(-Xq;;0$csY1IqSzbz8t$zvYkE&l`n^&* zQmn0(;8qNgoga8BhU}<3=<_o3r0c%3`600$j9|QO&yc*IMIAqTIpSG0J3@Yz{Po$A z-svz6jfK6Fnd7vca)nz0*lFn6br}XKHA^dp_>%FN{#5 zPL?CcqdT@tG`z8nB6d6+6COdZTi{b__8PVh4W4N!^ylXW-LU==^GwOVw&q2+C5K_2 zPDE687Nq5VB#?<3{YpC6;1oTc2JJPq^*wDAZtpF>+d?MZzO;KxXupS~bK2-V|JS%+ zb*tyt#&Gq-_?!}PV{#`CM66O(%>)CNIORmGxFr+xo%434E!Hljlevm;o0X_`1HM?Y z`G*=%S_t`}2IPIH0a|~n0dD_;8sNsVA9Y3emm1*oFEwDN;=iZ?GI46i=<)LbSLCNJ zV6~#rmMyjnr}U#Gz_D?S9>FG(8hoE>*N*XlTDiZ_!h3%X=LZ5BCnWI)>2Lp|1`z&# zQ3LM&QUl=str}qTw;Is-b9gf8k~DyQ@Ffv*(^gV1bw7IZM4*;&zYd-~tplI^dL4^h zPAy@Ne7?^h^7LkOf^}aCc+~tkdT=!L#e6c{&cbw2+%`~F)I{FV&Be*i!{)WxEx|Zx zgm#Y*EA7?O?)Fl4`=UXCW)5UkHh9~`1&czXe}#3Ku(~tktu$0LA{Bxi@Fi9DvGZgT z-&NVX?fh%n8qM4w#=)9#J2??LfnsU5vb&l)Q0>_9$|xuALJiC>8otgIKYK95Vmu~Z z^ckY`b2Fs7u2h7q56>_9W+jMLB4KRyaOM7;sYj zwza#z6qQaTGyUn3_=3%OCziMyaP(%2HE|uP@%o^I7`H$T`1Zd0ajgbu&}@Xr=p17Z zq8XVUp_ouvxyAo11%&-SN&ySW|C=9+7NM7+})`jsp5Qa`*%e!Tl66m(6m(GH}zS%7=?>?(k?` zr|E^+i9c0CFZ%A81Z3?+jZLq6sksh|6@Cz`8dJni+h?S|IQ(u_9F$KtS51)2>NIuB zWuUfbTQ_VKiypT7HE$;rF`eCxr3xU+&kzT|Z~0ylfHhe1<^0!T9HrSdknBVI^7s@?KHFd1JKb?;By+#NW zOn&>6)`dN6eM;+Xfj&r_C4qLwPm!mYWH=pNmabSCRx?nZtA8!q<9R1ixe$O$+gCxN zO8!*^&7(`+O{31RMPv2RPxUIhfy2B0X6D>MZCr49EL$i07=RF9@0p-^mdsCGp7me&cH}$y zHnI7#R7y&f=VsCQR0qQ+CbNhu7D^SlXkQ{T<(hdlgv2tPq14D9lqw(&U9t6&G5}Eb z3#E!%LM)S`+ig*)GDnUo;#lh@aMMPSyudxzLn|c;-65`GrF^fvSMxzpDM}6wt7bv) zeoBWkzK#odV$qxXUU5m~UHGon0ODLJXTm_947s|W%1#RQR-*D@ z4t5N3bCIpkbkyG9E(?6SsH3dHLdX@rLZ!y9?>f=SrGaT~&|;mztrLE#MG+&_fojvN zBw(Y}fmmiuduhFxXTHd0O{MC}zap4c*K(@@pQR7R9j5+DUVQ<0%TUf+Qyls-tzv3p zn>Dqzcr&eX^vwL+I$MAER4V;MC(o2|f;3zF=yAWWVB+q}E+HvhbZGKhfO#)e>+19D zviVm+?yuyD)o}S-ys2JPjhD?T-s(3ts;s&JK2rr#Yp2e4HGw(fq;6k|CO)M*?2q%3YN&o=NX z(>UdA9=nFTlTY&&OQ~nO^CxX;&ZWN@mkc#(`uyfROsaL?sBu6IPcN*D6~EUyIs<0a z|I~vn?$Wu42MS-5Xx(dq~mJ0&N2p`>bwta+SiETUC^gx6{N;rD=81uwsapqY=DVLQ?i)e!|TBk*tkbH??pf*z$?LIRPfp?6(O`%DnE}&XwT9c`(m9LPU}7Z$&8#MV2G7HxJBezc0jUGM`G~Zm z&SGK?;TeENPg&AY3!EP50c{Cb$-v}%lEqM>6fuJZ~(5_3N!2vTPqz=$V1 z0T}UpoZ1h5-2zi*9l(eu(Zbs(mIlJ66Ez@hm+dqH$n<0?0GV$1!LP3{ZkG|Tyi(iD zrvb6+3IfRV&JQx(9K&ZEK+R{{0jT-k*Z^uC*q$(2C?dZ;l&mw!&c)5k!ER0FSib0( z+{qNMcC_r~5FSxJLrnYOOvMjO%gvzO`{AE$n202@G@****GnUu#}!Uxh6k-e`nSJb zseah|?01>kw>!!;pVH!rM^u1YlA{3!%HUHEU#q1nLZiw!VgFIIt$0j7DDl5Yff5) zV&B@VU`HpS1b6-$+DM2$(q?+>@nZ@b4=3A?;YzJ@pWQu<-|gtr?Lq5djcw;S_uHSh z-2gz^i+H=0Y`uT-(&Iu{YhtDR*#8@+ey;w{oI3Y-_5Z@DtGX}R0X}B0dE$y#0ad&r zk-QJ@OOuCUvY#}CpMEZ)*ljn}YkWh-_|69p}(j~BjPixrMi907AJKvH`JdL2JD4B15FC1Q8y6>ATk0ByU@5LrM z>yrM#?<^a}Z+4IG0okHj*BjKib%f3^HSJ#%G_vvp@7?=Mmh<;6Gb&AY5pjII#7*>Ge19a;;x+8rGw3c zzI%an>{m9I9crQ)1}j}t_avbbz6?$#RS6t1V4YwNP(AeUrAQgmkhLdYtA8WO50n*F86CfxHS6F!thNt zgnvycjnA96d90+wEdVY%H_obBN=mSFcESM$^tDXy_&_<)F7(NU|3n<-9yuskY3f?i zHYYxhv!RH*boY0yHD^ZpE;%IT_8$0x4P0^B(qzPBzE01ocw?v$Qftm`42*H41rCdc zJXUHZ5#4yDwm@m!2b&HtqVO*^UGr}?{rGP-eQ$fU*Iu6T0W-&$zCrQ}RlFbLrFe#* zE#qrpqEoE250);VB!*s1ivglO@gS+Nv_M`e%h1?#8T635HyGNojX&cR~%t2%}W}S*R)N zJUKSIs&Sq8Jx`8J*O0MFk+k?a6GnN6`1eOMNud?ifKb~QoRri^aUC|JAICoe)dRY7 z?YA4m5=PA^3J!J`D-KB!b5lp5y4wTPzOf!`=&#wh&3tEx7?TTVAY*|YQ(c{fH@2C{ zW23U1nLK30qHkSDrXwvM;7a^@&Fom2bOj@NT&;Fx9W9~Ue|Dp41`~2zE%kKkv23iR$w|-pErZ=S4VKB}9H>VCbFdQ;jOLAK=FuRd)EB={O z(Qj7s*Q5$1qXY3J5~!}8+O!k$v)lp)6eqy6iUcO@6LJ+lz?=%J8T~BG_V_Gv=QQSz z6rkHIR2Ns;$9T6|cvt>;f(d}54lkDC{YL^Cwu#FSCF$Ss!b81B3Eb zc^rQj_J7fREL$yN?QA`_Xa7kS{$IRs*X^Nypw2%M!vHCO`nu@fRp)yc7~emJ(d@bf zhtlLcidx53Jjd{UZnmKkHooJ1a*2ETv5|7a`xLK6aUo~f?g6Z)q%tH?dThZ^_}YMB z9(7aLi1BLeH%wjNkkh%L5nAA@3fDPwm?AM{H?`-wutLzhlcgJ#5A6D6QN6slag921 zYQ#<@d>S9;Shn|*jCpcpQ=(lHixYF_hS?d_Lc`2&V6mRr8K9hOlViBCS(88W_ghZS z*WU#R9g+j~l-Fgo-|a^#yG>8wiiC!~&sAZqsFpY{#jY6sQ02E{-}WtE`^H^r-pUBm zwr!M<#y$-X#9=;`dD2*n6PO`28@%4jDo9`(Y0I!qhAI_^Jh66ht@7+6GoM#+^Hd&DD z`2aZ}7*52%eDYfkKVMLt5f>q!tw}C0PVDevHovP!kp2n?74$$dW{?Uhh)3WA9Zo7T zck#rKfBnp$a zfeO*w&-VQ%$mbF19=gT&iR4E$J4;0X`rpH}7GO9YJKq%pRDmqpIMfL)ij$8NmwR!p z#bj}awMOoL%5Q%wYX2d>rCEE3dO-cNUeI-0EuqBp4?gc@)*k;ktplVYO03DVdMx$; zFD%wsUiyuGnXr_wXm=TcREF1X{i(<{E;`_{a1p+2X1Cfnb}iuH`JgiI9Zi4^wr%^F zP2~nVQqy;inNt6x5#)dmGY%NT`{(CHM-K>ReAM($xwdIMiE+usqZe(-!G5Q@xnH!T za`uPK;HdS1YZ>Fgm%8$^=e=j?aYVt$3==&@&q09oaf$rwYlmejj$O<=1{ZNLF@g5))Qs+X`$88mqt@JT_iqy^%<5)hg+PoWx8zg<% zQM_8T!RR-1zx5^uvT5*tWp8l&?j$F-?xSJFBj=IFLl+PL4TeHM!6QeA`9_Y8i_%7H zKB(o%x9D93q~oV;if3*d`^}sOklfnU`Bl0d8uCCT(2EjG@!2o93D^d78>$xtpS~C6 z97oA{n)NRzd%A^(Ik5OC6K@nQ4Dkh?_n`7sYFON9cc(>%6D(@2p}TgGOgt`zmst;Qc9#p zagZ9efS;Ri-m)pta~Ro-1r4-?kSdwm-jN#MtwT^MSr!gmVFI3jqK)Ld){wBk-$v^HH-a_H@x=9T@i786+yn9!OO6#S}L=adXvv`tv)NXVHUr# zWwbd&tQ!sG{KRu)s!XK1B{IhDJSRyPZPmEb?q&mod@KUHVe#8dGQ6;W@$xi2HOA;Q ztN`ZZSiWqo?I)iR!O+5qlVYGl{^lC+7UH!%XV-C4qQ#wTr)cWSn9NL_Bo!Zi1UvU< zqc=uorMI4t8Z+b1>x%J7qZI;+1SSG$+$e*FV}%MAw?xLaMt&$=r)SWIi3ApW*v!n5 z7=Z-jz^Fsx5`owtiotrjU<-jo0IL;g=jw=qn?XeYw_d^R6DUUxfqwsj14?VbD;Jaq z9s%s}x#z0Ook*hCd6HCm1NuO8(G3R-9c>X6J{;O!%EG%9B-It@{uiV4@@8bRkP+4@ z(u}gzuhwlcA`rSSbFk|ewMtMt;R3Cn&LYV${o`a94-Ne|^r3hytfR!Ys$tJ!$rc0U zv?&9?&*Be-y1?#-DQCdX5)TLcRt!wltvkgnpe+-MO}aJcGx#r3lp7e#2buWZbvF85JzO_^ENoPRc_VLJa@BK*ZK!D|Qx&vYCN0na3b?hF$`QFx<} zy|W3$j=o8m)Qw_lq(`p|o-QamAa83VMGsNn$^*I`Z(Hsc#2f#^aIRZAuwNQsNSgd& zHlSY$;f$6%MUEW(gq9qE^an2vm`;~NH{8OWH({_nI^X9QI1Vcy!0yRg?x9Ww;b8KG z1;%dC=M#jTF&tPRTLbo}PWM-THp&M-+8|Ibg#D^b_#SN37v5yOVS@mPX8X#a>2^G! zY)c$}wnFUDoWmaod(w8^ctJ9}Pqpy{;a`1y5b|O#`Ds;k<>>*d6NeJ5={HQ!Ud4`# z4s9`{cEmw-#*)IpbXnmZw_C8f&{Bqg+Cs6hQ%3&+W}}QYBkBQeUc-ff-YwBr#%m-1 zfZdr$(7Sh$$k4l{0EmR}7jUyLApft#MY}SdQrInE=a&uGEurZ6tdem-OLD{d)te}% z1=gF~7W>qjNYIx_AQ1{yF_*&$RWYB^9wapENf0Z%FyfIWPg@b{>R_NIifkcJQkLrR zP*Sc;e5IsB60`&)%g`w)fA!QDW^NwPP+}nrr4YaJaqc_OcKSWyjGk+If?snaZ@ApP zj7Rf&neX_4d_1|r3c?f;<`X-H)>l*Aj{MdFSu21#+PnyYung?5u9|&$a=Re5zKD=z$@=;xqW0+ z(-L31Dnj3p6@6)}05m+@I1a5?N})JD;S);1{oU3bY(BWc!ESgqrmYcVH>FN>}W1uH>dEbt;+9FRIe%nB^kz&FB$&8rcLyB-WF}4g+9tO;NBQ zP1RlyrSPOP11K~nY5HdNFtVm-42*O~(u@IQ_2F=siiX01*G=8RJ{)Sm;@B8%>0~LZ zhO3}S3^25O{FhN1pj zNss$pYC*Qr(IMAw^<2!YHq7Xse_Yy=jH4J1xI7OJ$%hPNKH z>xDapK4R&4esnA=Ex&%IP5G3jqY_1%d{VTlI{NXD?uWwYdz_vJvhbL!z~y`F-qWzK z$zBw|R0vyf4QYm^esHp%05HiuOgZ*07F0Mi# zoWi*^a`;;5?ys!cO4|@CSyDFE!<7mPLrqld6nrzogv~X$(J1=}V*|YgZyf_Y9opWy z{B*>@x_lDi6#VxCc%xB=mVh}P2&K|6H`GK&2MR}{AS+n|y->L43I6<@C4q+u2Je}* zDzQHbc=wP@-||7dmZ_D~i7y%;nO=3>UriGO~sUBYF$2fWk9ji*F+uOVhyYLw)ex!i6a!Ai zUi0Q)G$1qxCwj}?77Hj=QP{$J%?0Qrg0+?P)dvyp=->!6z|gX&_|K;>ruh(fJl&-j zc)XAE5%72h64P)P1`B)cDh>RNTWocjlT(BM*Y+?d}`C@tkf|DU2KrTE?mY0i-4X4|J zjcwc8z`(#GeOaDP0_N{OA7{9bY|+@yA@8;jD`aYw8p2SXPddp>gDlhb7;qIbtQDAl z9LQufNtt!d z)&pA58DRcm5ciAQ9t-cxM#oFmM;w+rSK-_V2|-0aNwjw>8#j?9H-0211q0sWu4oal z!}p+DqU+_Y)YS0D3Bn&HMU;r{-aHjmD|LCwuk*8(xb3f+mQ#qv6sP4N=zJC9P~H=je6VXc z^($yD)vt$j*=m4_w-UBs`$^$fgs9H z9gaYi>#y0`-+^EFjnkm5y42j`K2I|}C-H5XPWl_LYf{4Csysyq4C-Ldq1tn_j5chL zY!cdEBRf+XWKuwCXIZL(HiRK(S#xh;{mQs z)SA~>;M5xF;2b2MH%XT;!GhUBsrtz-KT`Gk+RfeFz$UeJW@D`8CfHX|NLMDPUj+v6 zC|l%QLN*Z~=)oBTTyXqIPW~n?+;lZmTS$NhxMos3yjTW2!&tx-`Ev>32r$IlKK$#Q zV7GjJFYepBxABk=;Q2Yk8b16PixiF@a5-VEQ8oKkY!1|&vpYYJ{oJ;rmi{y0cm_N_ z?pt*-U}hj1$?4_LvscpPN)Ljb_Re4|8W`w)IoEZ+^LW;!b2EOnpSfA@FHqPra=S*XpW-CzA5lPWqF9#xpx#t?un%JpE;2 z<)z(gogkc~PW|03@ojMJk^^PR25a&^v3bU*b&V;Lad27FQ1RB19AEaw_XQ1QMwyJq zk_42K=2=vRD{0uKqlg^S!UIlZ=I9v?ny6lh$Vc0Xao#psENSE7e=merlh8HIzvv3+ zBaH4}zuvm)njnxQh%r$ZwQQJ}Ij0|a(o|p%xFPFTd>P1no3SZ=;~c5HNuQt@FcR>N z+~GT9-#QzfQxhxBAHA_697X1R#nB{xlPTwEwNq6_0DXWdV%anD&29bxiGR2K_s{Lw z@A8?VH>;}6r4!?B*+Bp(>7PV>M*EzImu)G3uz5bu*t`Z_bk~1i^DO?t<_(4;UG(lI zR!Sk$8)v6AkQCp{B=0r7zJK$X7k_9`>4gucA`hlfi_KmZEYF>vtNi1P-4j{b*$M+{ zR_XfZ8)C-;vg~nN=3z&m1cn`^%&G!PShrjH75y#{dR6;WK>{x~IF* z4OP~*_MEJtCZ?jSzbeO0lFgeV^O}yAN)h&BO^CF^O-#gp(&8J$y*2 zYtvLQmj3R%_(*d@(B$20ZB>x9xN*aw5GoM?!l2MQInAB^zlY@Uv;PgrgLgBPF)QeY z`~#9_fsY5LI925Vj#9Qz&^o}?*Jb2F9@j?UvspZ7=gDQ8zOhU!#h zyU{AD28mxyPXG6!45Rs8Ucuc5vMCl<5!4*EA%v*eQ}H18&dl+CuA#4r+6ZU4Fov>2 z*O0#mfaFF{06vdE9l+;l@gNGN$Y;5lYd=3b>MIqRiE9n)_-X+vBYlx5O}R-hXCecNohS^?)Zo7qA^N2Hi^p9~X*dIu%u7MG&*6O`Q7O613J zZ`Qv!*P~LNhKC$cf*+Fuc^;sS_l`SmrNSzcoL3~?hhu{JweV?{=yhbD`PK?34l6d; zmG%4dn2o||n*;64Lk-5YOaXfqyx3XLOw6J<^g{bf3p%#Z@0yTL+g4Q}j>_~Y;H;cc zTrupnVI=Ur{FgT=Zodq&Z-2a56gfE3nG|0C`eLMN{H6u2HEZ4E>$z`@_PTXI>RlX zFVO#~;!Zr7iW~jl#^lAC%#&_|E4-U@iOk8d$4-NAx;C1Ip6cCpw=ZMa>JW=u#%_cD zuda88Lh6Kehts}LIqYBDm2kZ2Zuh8K85yjpfwH<$-P%4jL#E?4o7GIRI^PCVTwVhAN<7>do zX7)_s#AgJ4(sb0Ed!?L*Bn;t*tg~C*0Mx@{&swy(o$pOx)2cwj`s8 z;9x{o^%nbtdoDOv`Dg700eWF~yS^2_sty&pUkfnMHv>YxvAyF1B`y2XNZLzYD52~< zx>KXAx2GQj3)^Y9qLMzN9SiEG=8Dw**xJ}z9euhxxxC$>Ovj2)e)W!#!^r8;8|l0s zp6Y~a?OqI;+`M$AOgh3Br!b?+zRZi*S{Hix7_a{6s0X=CJQIWo#TX-K^QE!OR#qPlMC~jmK)vb&kRjGH59rW_YF&+?dLjqg-ij#8f7-x|1BxU9$ zYhhT9Pt2qATqzTNFhYfD*^h-aem#9gbgLoM1CLvhSMY$~5vt6sCOvpTI^}wa;E~x0 zO}@^bTks3Pg9o3pydyTznR+JGnR*U57Jy?Zl@ZtRuun=T_W1H@Z#3=~mYf6DsPXG{ zO|L+s4wUX?-=_0x50|RMyq)0Yk2)7FZ$-JyLT{}y7Mm4>HIEfSA@w^eIJ~re% zDI2EiwrcwPlZwo{_G=wuofe%Rlr;>`N*u*6qAuW;J*Y5a%;`k#Iw#p3rDwksvs1HCe@!37Y zn%tjZO{mYXCb+d{Sd;xTtSSB(*0co}d*BvwKXisO;xMElIR4;$n*f65u`Q7QA?F>i zwr=WwvZSLMDOAUq^~UhSJBLLIFF=xmux|Hg4D}12YA*lWrCNi^sK%isfMq)F0T4@y zYXFvss~*5I6&Xc15)0LlHd^gdIRWmSFMtPQ9O0U-dcV(PGCF8b50?MI&mT?19N-Jn zzNu>)JyQS0?^j#r<5*vCll3<3`$wAY?%%OE_pU!);`!^DLe)xBq<~e0#anLMXTD;=7_(q;>Wl@2sUg0-n)K z?=V92EgaB_5#E+H@-g)q!ge;9W^4_3fJ8UjZg|~&Ldu{`yQ<5#R(jd-KhnR(C>eOq zwjBHV^2@U85U6NMrL8Bi!r9bNg;};(6-`;YAu zbY%l!>B7`+NuK6H=fC;IKF}4D(t?hX#J0D}rfy4eBON{JY_>24!F~K>$@|iA0W|=7 zVAvfG(J+zV{nK#1s z%3z0Is|{G^K`{Y}zt(wTwkhd{qZ%)hD+!90#%KM0>F4j7^oSURT_`h`XoxcmC@Swx zlCIM=`e3fpU2QU+eLsH7zsK5@Ax?i_o*gbYSo%8;OZUFQN3}t~ligorwQDa4R|g*P zK4Uou9|3U`?(uyN@} zU0TrgmWwqi-eLLj8@_!Pb2po=G79=2pTTDA9F_E8`ic)Vx_4!I!e(Yl#!}{DC%iJr zGO1+rvU)s;Ih}HJE|Ipry#jz46TW;PPUKLHB-+;brCkyaPCiz9$JJCLjDDTz$QbDG zS?GpQ)qhe;v)d7==E}K5nRV(o@Z|a^(Bc#MIfKwi9J_wr!}s4GN|H=Y)QeeejnE$t z3g^TrN7SHAqyzb1;K8Y!e8=M&M0u6Ur)v8$wt^~f0(y}Je#rxW2?u7M&Z|TR>J`^1i5I1RC!%rwyE|3Ik^;{zboLP?I60LFuqM<7DVIv8V=6 zBYy;h2r;M0<4sW~f9~cqv#lL{}7Jd7cjs`xJK=!=*g^&|I9@~96!=Yd&o4C6UYDT!~ z4qLhp5pD*HaP{*RO^W zP47gsv?bipcqVDnaOXwh=Ge0B-FMRW64K}O*MCwHF7R1yD%+sDwfE zm&Xo`M_-(-ExZ9eRNE0Bm8Rv+bSOSm4|pb9mrdXAX?RrIBE7zRYa&;+aA$&(Y#)u{ z{EoYMK7u2P%T$iu?WK({bv??69*c}t^ONpRPUWDBP707kUI8CX@YPt|Kr?2yuh(y1 zy=={;iPYi{Pe-PUwu36r|ZYQEhfX*M{I;;Uz7Xhs3A_IEe1n+LBz#{UTNx+_S*{I zp7$Gy?CR+;_oa4faJ$a>hNqV1AF?hCrf@apd@^m~F=f;AFX%y!y3r`&9E%n<;*GYc}gUIP(9DN=eNARN(^?q zPJX%-S{e(P9v4A1p)tRB7Fyh;o?f2+xtFn8O?cCmvBw_pIM}~#HCQqEQN2cWB<<(3 zwBqXT8~4Ht)0dXgnE`TXlvZ0;1EvvWlf{xvWD$h*HX^ItQ>Dbbv zSF5~_9Si9w-(~?Z8qu5oO5D5-;QBJn^%L209_MqLrxj;R($bN0c0N5w&}czxWpkO;;XOki1ssw5V=2Odp+IF7G}Zzx_o>+~|90 zJXYOh`uGK1ui)-HO{Kj@9uQ1OZfh8iythvNcEo%fZ+zyQnY{kNyv~O^pzRt^y=m^~ z45NZ5!6QaqLbgcKi)03pRuKuEC( z5K{DWkoZeT5&RD!#pHVewlxmuZoAZO)$bqtAAXL`KRD;Kc6SbWWaGWXb5$Pf=I~nX zp5}NIGjVX*oZJ6;!rams`9x!9@g;t->DP3Y+j$|=hy(1 zq?4CwbQ#v47Q0OSTW)^VMZ^DKfx`*U`da^49OEj4^HJ!%khxk0QZ!-h6G~utjG`>& zfor(_J(uF%R&e)$E|EhaJ8g|i_8eUR-C@EFY{2i<&mj!!P{zNbG0v91dF?mc0o2Kv z_22(|6esLI{pX`NwSoy`gk$nIs8(c>t7VsMY3u!)5y!atwR$V^i%#aC<)})tI*5rl zGh0SDNrlz6_d6O2yl%mTP2Y~Fy3eg|UISYxdy@{hTit1xMZ4htsH^IHV}Ze}J|pK^ z_wju3KJzpacqr#Ny?y)UCZG$DAi9^vn^*!8X2F+zmjauIsChGR(A&phBi)p2lyyhZ-{(>h4S%N=j?{1-H_k`XMMK6wM5*hR`V{YbAYG;OuIT7Yt*PsvF?;b`_+a&4&V2oV$$uuJXXS()YRb>l>9<)pLj2s{l(f{y!{9+W%rnel$(Y zu8wp64@(l}4)pG~=vC6VNaA}BDdIZl5Ecm$;E##B+H_{ zuoKrdHtJ%U4Tcz=f*TL*$cv|&HtB!xs|Ohqhj$OOW1Y}j&D|$VY=6h06{da&k4Dy{ zDED)wkg9XWdWz1u?z8>M!sSI3EC6R)%~6q_&A%f1+X>ek&DwkEk$1(&siDozwRatVKeF?>0n z(PJ{I2VOvPo@rq+g8ny2((MmP66!CKse7>G@=TKa{2!7e4?vQP zvXL3ov)|?J5p9%$R$UAlHjT)(2~+8RU#z?Fth(oAFQqx@_zwtZ2yC9^EKz%{H1o|2APfjo%puE)tc|3p(5G{-i=*R)oR+CU;*{i=avy8wWuk+v zpikm~NvlU9v0XDGp^KwO0xk-wd2L`9_z}UtE|(RF6&Q9*Yl`6K#xxy=GbT#Plmrvo*<09v+|#Kpz;+sEtgql6bp zi|vAJDFp0d_KzpbX!?)RM4VC8S<%8suWXS1q|5^9NFQfhU!}gAacgS~drXoow^raF0JD|u|2fy*+kAxolmUuc-eTiixvtcQ|s9!)Pp9Tsm& zS^*+pXVscr7SC`1jauRBZ9aVsRTUD6>`tvXTAaLH#QiT-4^5*#+mVxK4)m?}T+mXp z_Hq!NA5b5hnvoa}@*UIoWU0)D_+;A}s8{hETf-f+8zM;RR5cNt03-1iz-ToD7?Upm zqZz{MDLK<#Tx4#QXF_17LMk*s2&DBZXcIpLexva^E@zP;Kln5;UO6Dw_!?LE?%_M? z@|7n;_{$82B~m~D>zN1W{mcXOd*%VMJ@Wv|o_TpG=5$o3Iy!ttlkP&8q~%e&$tyW!NMn@ zgj9sgj~HaL#p1Fmq*&L??DNo{B1g2Ws)f5wwa90zN@W%=T%e0~1hZC}pUH?vDW9Ly zYaXJuhVQFk5%}<@xGAu(Dsr8i-f3}Wyca@oyqaPQfD#?(e5-y{elTUw=;+Q zTumH1G8H;unbNlX{H-zK&jl3Hbh`keV$5?2E$a|Sp)KKd0x7hIvp*@c=%lRY6k3~e z=K`Pb)BmCu&(eQ<|DPYoZ^XwZ3VZ+=VAh!Wk+WW14yy*0Ui25e5f_{V|JDFZGLU;@ z*uOMN<-Mv3eWEw~pqKHp$28RSlB{1crBJrc{Yh-+zxaw9)JFe<24H|1fiSzCWaFLV zOQhct16_r8P9$;{afz0)M!rgV(OSpf?KJf7C(+}DdT*wM&b>w?%cHKyTcgYdd_V)xG3%iIOViAAxvGnkkj6MHe zaa8l-=Y}tija}W#FyYQ^qv>Hj+5qOR(l+;Y;q4Nt*k7>qwB@w{$^~qTDJ4w4V;w)F zv~&d)?K3Uk=y%=uW--Lsog3He<@j5~$l{~^AM$7MQYVHR_!C|_1>g+&0LOw#r}fLl z^Zk>usA4Z^3F$Ew1BV4j6~1|ei+pmQBR)<&`Nh}H3{5d}QUJQb_EfMHbJ>zb8RSD* zywJ>^a)LDYMoM3W>wqE6E2;Sn&Q5RPmJ88G`LY4r7a#4C*I{i{kV%w-2RL-?XT~Nf zU}t~d$P51JlDzCl3)Ejj(`9{&8n}M)+rkM#Jw^ibTf1eAs{qip6rC6|# zuyd&t8TiFbxteR2))J@hQ2g^g8K>&6^R-h43RDTI`1^+^_S2XQ*h;1`#5Gc{pY*1! zar>e}Wj=&rJ?j^$35_w{H>a>=?4kxVU~HRrV2vX)Fw`OUQ5nI$;p|>=H}V|(+T*a0 zHeTnrn&D9w*CEt)B7|~(lQg9Jc+jW2nZq~FKiB~4RP=On-AR-Ti=M>&!kDaBpMs&l zJ(Zz&XA_il55iVemflnHwzXe*qpDiXr^L%^)N*Bi6uinu+Diu!%RXIZ+z}P7{VXD! zW!Ca((&KjDY#dgBzNr!|7b!WYpZsWU)F#Ms+RWk* zXlZeSx!64^mc0;d1l&M-DJ$LaWh&NJDTb!6Mbdsj*liTni*$s*{zS8C(jE&Dii(Jy zJ#23dCEkGsDMy&UNn}xZSaA-3Ig54Lke5*FtXL0nO$^3ZfR9s(ETRR<&t8_#GcITN z$uWIQ#A^}NWCze!s0lepcbuph~ zDR<*Iwp3Q_pa<7CEKI6Qtv4AKHQXFVJwH{wvf`C8C9;N{qmPGks{{zRf8iZQS#YDj znzC}$^T?|bz@YunU8S;r%qLpIt-V4#3MI^KB{l&kyyF%Mwh=j$XG1+4TgpX+)a6dZ z*y%fzvRWif$c_fJay1HcVR%|OaQD1e!q3J!9!Z@bugJD7$I&8HgH)*)f>zXtMrt5|;#LyF2(*vM+?Yg{C45I{L{}wf@0P(+jG+zO;JIq7;4ETp7kIfx z=3YTQqT3Q&K1#@vxK-p3w8hh0ktMJQdnd%0du={?GSFok0&gs=oWGO;eQ?5tRp~f1 ztvEH?r!K(sDF_3I;t1;iuX!&UdvrIeBbBv4hR)m+NC0z&IR??qt8fl-U(u%7y9gG*deA`3X8k)pal1pVKyE7b1@2Sn{ zp9iXp#_1@>XrmIUTTAN0n?sXz;o1V1MtI82F5hfkPleHnXEbsU-INsB83Qu}uE77b zEM`)CLf~qr5PLzECQtZ{LAUP0M1(PqYu|_A3&fkq-68vvj_1#2Be`dD;{tralVwkO zxc37a{Whe{KZWAFdw;SBBm#seVq?ZU8uJan7CbGP8(M*Gh1r<5gQhpkmX!g=L@LP| zY=QWvlpjC`D+5u`?J5Q-F;Mc+hPppEwCw{YFTcGA>NIeMb@WSmprtzA+cZQ9*w;_n`Duy0-n5870Ei zYS!mt#Fv4xS=(kP1UEJJ71#wbZN|92E{5I?tVb4yqrwrOJ^jQCl^;}(Y(yN2cf2qM zMoFcj5!j)LvI|b#j6t;lj0B$&xmB@Pizwyiw%{bhU1rAVc|5D74Z44mnMMrA3XKkLYgh z+387#-HAeBj$!o)IO!hh*Ag2-Gm}`h@GYJO5U&6f;fvQjpbM|K?CGaA&e(bMEF|GrhvPzR3~s4 z=ws<(wCIXBR2u)hc&|XT>;_m%9fhJc-UBFNe60f~RUruk^%ZC?mcq;R!)Mctljpyp zcH8Ek9W$?A&p>ne%wWVrWD67#hc+`83jYl9YO4X&vc^eO7>t1U3iwh0<_{!ZYXq-A za|6+-kR-3}7?iRf13rMEIcvEA*804V9Aq(e0JN9T_MwQ^OuT`qo5_CwivnC!A-mnG zO~8_g>45Ut&}%;0e*#5p3~62k;%GdYU@ctAsDY9wa}Fa$lRC`ZwsM6;j);sphSOND zPf=kDjbty_n8?&+-Ym3%m3>IT>@5VpB#k46vpJYw3dK8m^x6;9S(@0hzRf}71M1w2 z?NY@YAY5zVqYi89PwM&Iv&)c+xPvOje**a(TmjE3N@S{|xD|RA%1sc%SzhSU)SR{p z`E5Qj&IWYTj$jOHvvT(eyXc?$);jMh(xbp$u;UXtzx)ybn9(XN?~H&mg^Q z3nDQb0#ZaqE;f&$c&9U|+;}nU3>Y>{fMxzB1=<3&YI}nV?4U^>)+Wp8Nc5D`5#Z=# zB)=fB1?(58zW15C*0)Q?u9`)CJJa=~Z=$iI_w*@uUw0srsq$^Px(PNWBIV)-B(HD`;KpZOVw$zyV(ksg3mbMbLSkO3LSaT@Z>G%%pN{v2(1iuT z%P+-ynq}XRv1fXjxdit#_kgL^Kwx9wQ)g6^v8E|Xq_3k0v3A|dLx&x}Z=JA%dM?oe z!<*qt0>f+I)sgS?Y)DS@aOLpXy|z7!C%Pa@W!;M zl4}ljn9M}-Tu-34;1KUruvE@Nsggm>r_e!g>PRriv1nrhmi0ZM^)HC?;9-!Q3JuQ< z(H341AW?Wsj2Qr54ktnI8_nJ5sHmOpc~Zzq*)fA5;Q& z5!_EM+a%eokq3tzVwGvgNBdC)p3eb`)1Wj8CYPTvESE#&;K;l3+yQG;?9gJF^FqkyhE~dYr4G8;^~IRoibeOGb9bDG-J} z&qAZaBpInu1ZUJY{sOSPMVo-9mI}iOMZ$A}H4w>@*B>@B_e{rHf!X`^` zbM~Q<*>tQxlnO#kV*Tb_Ns>dNh`}O~ z|9#7Vaq4^ipejQDHV8MON#r*IW9&>swafNYHs;1w-@#A>r|S$8c_fxfTpJvc3=)HK zG3YH8HL$?Y?sYLaCVKT!VzEf9=FvGM`X!odoIa$rkIvzk@;C-g2zhW>!a@P>uxN=8 z_=O~ry9Z(4VKGjjnyRArAq$}jV<}DDeCIfu-gJTS#!oh|CkXAgDG_a&Zt>+g5tiy>J2;fZyOrk}AU#6sr*0E4$F=0dl zSYiLO{ds|ZTJZmBp*;8L+Vm{EVtb|fW23YG=Gcxi+Ncogw2FrQJ8`7xDI;Y&v1~O8 zEbXVqv}&U}Y$Qa%4(@6sR%gX`@yTq1DgCjdfen4j%ro-d4SWj9xu0Lx8so;Ns7OxQ zyosP_7l#a^K`WGJ9T2GAynv{2KX89mpjn^pAvNgR@e=h5 zB3W|mPuC+u+L-CNOY9#=m1`Pct65b!8nf~XL|3RqNT+BUK`!K-IHn={QV`aJ_>+6| zVt-c>+?2LLj0^phoIaYt5a!?>x^4V?B4&GSKbGKYp8KT7Hssh2;zT=It7lx1LitDFMf_E|#)%22Uc)pS4(DahnqTeQ5%rAcx;>O@+!-%gI5%!)sCdKY7ad z|3?qnU2}gCAI_2ur&uZ-eaIUJX?rnlE*C0|x^P347lyrX-}g7&v(wiacK0`DbJN!> ztvt>}in{B}qTD>tv2@(bTix6f3qo8=X!&v3mF;o-!F&Y=O8!cSJ1Q%b=g@9Rhs)v6 z4D;M?N7gKB$PrT9&K_~j7zN@Jfkf_wyvJiq}{ z)`5yAF~oDk$%jlQN%7@uJ%Fv{M&UVqNR^&$FGrnbED;4t=;EjLpsrsH&7aF7AEt}I_C2sCd?rp@O~W| zQbvc5$GjDfMyT+flkF{zmISKY`RDnNn0-4A5{HW;n`v;~^{g?EfIm0@)M`S2-dN_+ z6Fm^W7ypo-qpOCXP~pbaNT9ZBncTp_SWu`dNW3dl;Ko%dT;j$h@Jnvjat^9sc4U}} zR^yN@HmaoDr#z~JVz$i3o;@q71V$%hKzkz?GW9yyvEofnazNF+`)&^Flkv^oh`9da zP4!MrTZ#~O-oLGb9%m47O205Ba@<{V)-wa&!4YA-ueF!4AHCVA^uAt9?vfk5v?KiJ zUDJaJCeaP*@V!P5ZAZx-_PyXcdP($gig4BJY`wSx;ms+=3GdQt8;_d7iRqvR2ykpo zd38GpdA35np6{0iXSwHGlN_hwF4}K6*60I!4|LQhtF3i+MAnF_W$Fpq7~6bsI`2at zz&1fni>bkNYW8U8p=d748gAK;Jy$^K$T%hLT?Ls~dh%2GG!DYh{-%j#dB3&X;4E&0+jBh2wyJtWDIn;c^B_2(qr`s+!Nq#vI}`M!{1bvU~ugE4O$EctT%a`HTDBHk+M zoUN)zgQ^&lb>xVa?=X%iiB@ry6g|$k>!$ieNZLZxnd92?5wUOfCy|@ zDSN@rG_l*qlGJos)e`XLzRS7@^&S!+gQxKiQKiDV;+Aobxn+$-C{d(5B>x_Gqp^r} z)ad*Ias`!lGD|>8GWWt5lZ0fBlLyUtU!%lFsKOxPw$B1#YAR3cVhM@gqf}cY=Q`U= zqNC@7b8nzuzfDD*nZ(0(i;5iXA)(20K|>2a&M?Q>IYD9}DcWo3)*I|MU~|-NCMha? zQg0^t(8G?_2c1k0cNbPZ5bhV|90~gqMHAgIiguM1V~*9MdfElD+~ZKU6%Qc>ZMTUG#GlBB@6a$0@dwRx?=-3f5af}5MxAgGzbn;uNb(H@mQJu^ zYp>B|;P5S#WRAPI*A?y)K^AtfS{KA60@?4Ob<#D|FWmjL%GA{&v-s2nV+Ez>zT{+1 zBkYg6v{n0He{uIN)v2?+UL=h;7c=CkGyVfOkYv|Jp^k=)2Fui*O&$sEkDFy3F3Fr6 z_(T=J5I7Rr;m_fK(Doo=Can@PA+U6|C(z?Bu8!o1tbdiTIw}A1-FR6m8f>^A@?)W| zYAZTDUzxxj_Qxo9lzpAd`dGDo!LU%4iL&j?FUc$uEqbnMky`zNx|vszD*b}=7LhCy zhKoe_dwAt?{etj+14 zS|R*>wk`~!eIBwm0n=^D;0`ndU}gAwp$GpTtr>bJ(^6%IRES+#J3$Tq0InT`e}Fet zreGtQ7v&Kg$}&?%ljVl30vI*R6-uH!C=$vPN-Xg-WrdDN3TU#V=1Uby0ySiXbPKN{ zfzy$nnygUVzD{BN{fc(s)sxY{k!R}IY=}$Ak+`TWzo%(pDrfbwf zoLh5bYi#UYD0mcx0vX7zel!!o?q4^vUBV5D4sQ=r>BHt3f1CBBlg@#w8WrU^CP{>- zf9mW;x{mG@(GvK50Q&Wr`gYg>iR`gqxejhn+>vi!6ROl|7P5Yx2|AJxfU~a~;Ay8D zTccQ_IHcHF`a$&BgUmym4*X^)oSzKA3e`j!FS(yjvYCny#wrL71T{he9WI`hgN7h3 zxE@ri9u>3M&3M2~FbP*nN>~aDo3rK;w3CfMIR-}^GMvu}o}1m2PSf~AWPFbr<4d6K zt}TV7J1Y9Z-YrNIv}P#sK@C|FZO@eq5{cf#izp}DvZ#<}QcJeL3a*^JY=&Z(W98$g z>Q*QjmmTbuV6=(Y5-1tZq!Ab!&Mty$&pK*2B>EkHyB$j zPWG;wsDzbpt8#NVycE5HD8>ay2ZtsFT9D|&&}QOG0dd$p*lN;TJ%-e91+YM_bL-n% zG=b;Vn{YG9uKe}%*2z(vZqSy>9adg)WX^ugC z4E)+TPE!DU<(#G&Pt9@}GGYAqsp`ZN+$o0mur*XtR$mKydy7!wG_}azEWxvXM7kCa z_3mBPEbac$5t{(QpqSlx$?$B+&z`Ez-djvHA9MdEWJZw#2h(APAs=&$3gi+I?59MQ zcIv(N^XLd>Y>o4_5N7qvHY=dyJCZ?F^Q$u){TyD%*w}9mQB=gKXBwDBKY0yXcK4_gvn+ELFk2XSnWk9fDv+EbO5pB*8iM-ZbUCEfpxmU7`fXki#oU#x&-0;A1Wfifo?C{VqD zYByJE3{ZDRb^y_I6H}7LAh}R`0Hwj24m>T4apgJ!Eeydf;s7ilc*Gf)5cIAmt2D4a z8@T7x|8)TO9s#)4k^wOW-hj<<4%z~G?ecqIEtHdh0GO@YJb?~yxZF))5TVwvIlUo1 zCI)`WASQ5g+dO8B#fpK~L6Q_6(6dKW3o`#&2rCb3kUS7n06n%K$xZg7k#liO-D4npqwrm5fT$9f)(D`DG z1t_{d_i~V7mudvR>q|2sj74%unF#|fhQlCGlIgi_MFtkVV25Bso7dS!Z$<_}xFTK& z&~!_rqawoba9EU5k0;rH-ZK=qOL{bXT+L~>)b;t~qR7_7 z{a@<}!9y1W6MOyf8AKguLL0vVgUo&AHMDvqE`~*pUe6s=?MNi1y?lfJNQ-iHwVfYf zl_`VUMksq&Z2gN*8PEWgsE6aN@!ar+vc?xTP)pER^UUra-f7hfa+T_I$}XVW;4%zT zG;{;Dv}h15#NMkfVN;=KdUb_WRX{ghz!mLvnaB}M*B|ojst&1XRfl2db?X6Hn2^DV zu;$ZMdVE^_R}nUU9Uq*Rr?Yg?3z%kHPZX0^B|QaHmtx}Ku6Z5>q2CQD2#iM;)Us!N z+an8Z*if9`6#-cVfjQPfM3C3w(Hq;ylOKQrSCh~2wz&^rVxKP_H! zV_V7DuY6(@*WG1EeA;;J_9AD)kwWb0EW$T6B8iX&+HhLTa$4E=zTjKy(}keg2zK`a z&STr}FqN0txQ4_zQ90GUKiM+Z#xBq4F_MOxT* z1wZ}o-?sg3@K?Po0oTWVpQl)hlaIgqK}qQS3DOk38eRSA*QX}${99jVTca!Y-?U74 zl=0O!tyT7=-*Q|j`h0)1W9VrT)YLAF4Q?-2nl6SaVzCpzl_?-?U4$uq*3|1Jyt(`? zd6PBA4$t&mW!ab0P9)l1S$&dJ*!K44cI;cb%Cr?SbK=Ok@j4-$`2e|U$St!8)H*Fm zu41E7Fk8CpMRe&%wLYb@^Qm(cXXo5I1~?`X z7`2OyO$BjT^_BcmP~pa`IU&c2PNlQuCizxE(e7HN$(8iFTG~Rd*cPyvs;1ZyAS8vk zbNTrd9ycUdYc%sWI;x}lFUGVF+*-$fjd1zL_K*LCgMK*L{rOH5f?*aZwm@Pa`D$>^ zsFYT{tG+k5=+XdNC+Pp->@B0}2)3IWy*Kd;}_uTvL8}A$M2i4WJs%Li>FuHrqHP?c+Mnhlj#?E?n`wIQ)L8K6ywF~*1 z=eeGSly<=G7jnvoG7)_Fn{FLOQl_j`6-dY9z_~a+Jzzo|US~Z(v~z+jLqP`sc}olu zd|qgNem@3n%Z?8EPOTu0+v@Yt02+Ud-}-jo`k5OG0MX-k5d-Y2S>E)~Px?o&4K#`lmW7CHJt z^HpQ?k;)?~@Qhe0(HTI>q2YwOsDaWAKEa-(mFS3 z#21v#hzdeq39hmBZ}rhZjj5N$Sw*-OTB=+|;x^Gj~{v~A(DXj%U=s}7q^$U7fo|X#$uL8UA z2^Jj$@5^#^Yhtq6=B;CGjYFBqe1%xNord#KncG_Kz-2cK&bd3Z+?$$Tp$Kq$QUwVK z)COL84;wf2`P`DO`-;^&R#}B|NVa$$=&^K1idBa&@+Chl|9)KOW+HM6BD~I@h84)q zzvW2{8L`Bk2kGb~vqnjGu}|b|BV$q8BQ~Pm*Ot}s?hG1<&}rlj=lL5hCV_?Q3p@ev0#3Tv(O@QYC7;G<6M_(fZZyP+tJ5K2`GU>3LU(!5 zs0fzwl}k5%0At(1ZBR3!uuT0iW5LEPe{nX-8;7BoqH5Ne=p{f1NF0 z-CJCB`2C_(es_h<&D6B?1igH`2VI)CZEO^=@fa{JX~fGO(U`A6EpKB&PbYlhAh>It z0DBo!$9$w4Ee7NH9zLW<`XNo=$brX6>_IyqDELFtSgygMXv@hM7K4c3vSnHx7UrT| z1jd_;eg5iA#(w|&CS#|lxwF##95fSq=;GXm5Lp|V4QPeIG&D&nOSQc} zblF#Yr_Wy~@oM#nWc1XU@9dqFu}3zX6cJd)ZeI2&X_CVS4yJ1zFG;&^p!>_6%aG*G zyh^v7eFaJatt2$@cG7c*HY6S2zhvxn0$(3cSFyUnDjG|1jD`ngjvk9<_-7)HdYOd>%uIv}5M@|h&_;sTcy7O*3 zK46CFnRAN)A#%cc#+ExA_=91rfnCUjQWKwn&9H-zJ{CIzEMV7@oUkeU8iK+rENlu6 z%6*5;ic|!ow%4NX#)K}!&%H;~_p1qz;um_v8T>$Uo)!>x$`OS;4q`{Ze0KVt8vzi! zXXa}Uj2H4k5)XgDo&WaCNy+>$RLUIq-$d+y9|>%BZTaR94JTh93irlFk31smXX2V* z?B{!q$TfO|FDD`u{FTgnJ>*R-3g=-$OJ_e0tMHT4VgT~JH@C*FpHIdTX*N~NiNb~u z8{-zLJ-kA449-6jfi9(3Y@o6+^sn^d1&=l@>F?(A`E?@b8GT)9f5{O*%buM?*DeuD zqlZdSzL+Yf;Oq;d8twhgoN=iGA{rZ?eKc>wHf0%AKLYvL2-e?Rsjp1O zU+tS7Si7Zfa)DljFS&K&42e@Y#5XtzqI5?NRWkIp{}|1cEZAZ;3wFBq3r?3xOl%Nk zeb8N-FULLp$bc+i`l;UF5LG7#{|J@MgnypwvMHccewz1X3St z`REKDr1fSp&bQJD_Ip>rOX2ZJoe&-ZLG2)De*ZMG<#D6}K}ylT2>)Svf296gFV%hV zpu@8Lb7BP@wSqd6&zj7b`aC@oe|D^nSwPOGo_Tm^o4_*6kWoZ+22@L&dPCz8%f*!J z@J6t?ej%U?bD0rqF{U*e*^Pq&#!?+Y;~Ka`XB}$tr$o43-zjk+^VDj&EPh9^5@F8j zencS>*W_6#64yfWH;IgNm;)w9*@; zYH*R(2`bVbjl#8Rx>fb&OReXVm?+YiEa$*O8rL+zHERfy$e&8Q$H5?33}BG`Fx47J zb})$dI2h!|Td`~GsM(H4PX`Yt7xfj}wWuR|k%hQZh13EZ!_IJW(mcIk{B%=%R}@Q| z(f92uwj^Q4&xnGMYB6HioxeZoC#9LDj8j+~MSaaZfWw%&PtNEwcBT1+-h(TJb)LQK zo6VHQobuDLn7eMO0rk9Tu>|$Sdf3>NEPtBhMq9VjiR?qE)R`P(TZnuzNzb)t58KOz z(Eb`xJZ)p;ty+eCX{kaIG=j)6hBL3-GNtiyF_m$~Z#2f!a=H^jG)AXJsV++6QYQ^baZ&W7cuD`xCPM%=}KBW{0$27o~i8@dIng?3n>-M9W&H_2Veu>_Vhr&;Uu zc@?Q|AA?h&`jKT~^QAlOevH6XGO?W7ZoZ(71T$obdq?Z>(9z1zaYPp%B4P;eu#xlx z`vlt|6d_2YjhqL@Le-2TeQ38=C^BPR%+)*Zw~n1>MZhxDHpghm?C@fvWj60Ds`RPS zd9k*^h0^PV!N$cP*hMhvg%3l~)!BXvR)PZ4>S~*ad&4N-?pE z$J2rGFx2a2BD^xYM~r+6ZKsYngBMCL#{z#M7HuyPUbB$Aom|=Y7s{@6eV-|6f3`|J zNtAIHlq4I^fGW_Pm?{XQCtlB(nvy2Yn89VkZ+ifvJ>-2?PuvxvteF)oNTGO7g$FKa zr669x^7MIUbUHY%!iZUv?A5qMsJ`wxl0$r z6F>cyE--*v5sjnxq~hGR2)8f8S>i5v#F2R0QjDbBF65PT$#jW@DD*mTy)}&2&|&sm zaA048rwF#BkGSCgBgwU9JSFbG-BY!szS!(U0@v$WB@@FPPlV$uOuY6ksSTgzYQfVB zo^eJvlp1bwhZkz`l-n(@kL#~;hg&l2?|)z`{whv5KlFj5m{`B9gs(h&Z^;h%TPmxZ zp!VQnX>(U|1B_6*kLGLVZkf+gQD+fF<=ou`SgN}4{h!rHqhJ1lJ;Hda<{ zLy#Pd)Q2G>A(WLRccqPCcbYl|!YcD$(aRtaG3`4_>v|DXa{@g??Cfo(?nvb$OkvjwUhg%P_J7&x4 zbpS*;@Y>@Lo>c)p#?Ak0J-4h^Z64p&Jd#4I!PQY6JlR9C*&T1u^b)Cg@D5Rz;Gy?i zwxO9L0x1qCuea9x^|nP)O5s9pPA9}EAOI(O6=KphtkjYVp~M}B@mQ^6!NPZ5 zO@!N5nesL79XZxyO-#|$<8P4FIH=38R@qmMXSd*^>nJRp%vxql2KRrm%-yFe7#V|q zFw&RDvou;ZnRUBoQJvVwFnUfH%`xE36<3ap3B#qHEq{cWYufn58$1f7`6uErJM(ED+ zP+xGeN?8Wpsdv-B)-gE>A)vI7w1L$zF)I!CeVc;(hSER1JOG|N&qfW#03TLx*|?=q zCg0J5A)zi0KFn!HYW3xj* zjwkyzsKAwTiEM*Y=-wN_l>_q_gj-h#o*Ad~HXNN^?k@0mriRA9=l%uI7$C#5k-?iX z&=bE83Yva{`B6Rd*R(?r5=!wO*zu7;N31&pH#-ET*O6!#8G_+zKKv6l8pV6}k=AX7 z;v6Sey4hcASC)=6feo#!v#J+>(CHt2Y{ILK3Qt*_b$v^1!DXgSSGk)joDi@N$NMYF z9OI0Pj_9#gLA6g^tC6&>`tz)QBeBOzTu;r5wiH4Ma)lN@(K521aWM$UJbgqG zrjj-z*@nfEHWG2(6zwdOeKPR~D6?M+x751{7vh|#1k$q0TYWw-&c@$#?3D!-VuP=Q#8jLit8;3Ed@V5ZeVB8CI@J_##{G zf|lG7!E}#E0iLn?10hnWZkDfsDXil0ttrV3M^b^&zfU-Be~PF4OJ9WkfZ6aGPsxJPOrVJhRe?3y2g7rtKy7At@CRy^x03PAT$0Gmmg%alHz73I#v1GMVU6Aq7nv&4reD z7WVnbMUmfBR*$R0Ep^woEcq5V8~ab+DDJv|hvodan1>b)ar=iB*HSxkz{B+AiZ!LP z#TTdR_eHMiZMT8l;0sZWCctmz`n^q*XykIN@ zFs#!=%-r^j&Qi%le>D`5^wcO2@IBC_4-)fesQe^H@FyzKGw*#*U^DEdL*C@cQXRe! z@mwKKl^RXRT=?a=5X~^HISzISP8{<17CY6DzPd?L?}A#!diGwQ zX^}wImwbfZ}9ceM`Ud-+OLVG#MBB_y{0 z%?o_1h)!`=zq@xXEJMomT?^!(IVUs3y#)vY?YrQV=1^+ul_f{^)u?uSiMq!Y2o+?E zMeg4E6swk7TA6vdGv<5AQLO$^W;D4P^mE!7Z7v}rgT-<3>ZlLrep-cri9){}r8w2V zp{N8-4RK!j0&*XB3PQ44@r(WxWr&u5qG#KKvzwiloySG~DFgoEau+B6<*V1DB+KQ< z_OgGNNfI`!*v)b%BoD=}0BqX5ck5^BFSCw_;-v~IN!LK+e_EP&SrVspi@`dqElD&^9^XuxMJF;7jh~H^ zLJxI#A40(>u3rg5Mp{?2Pp;Vx^S(jEflG0Q-W49C(41xoYO4;*)U#puM^W)ta8YqB z=}LZ#oX9#DC7A`fYqZ0`nX}_c?P7q^dqr)Z@IL=~T3F-PpMqUpT5xlOMNOULG94$M zK8{|bY5VP7D#H2x27ilZ@uDbDSj{`a%V;9i*VyZ&i0e_U*oeAtz|M-nPXNB7Tz=@S zv*hNr^GO@;0|b4Vi-EPu;#1`fy8XQJY~%4d0fWR-{u8pVhSdKzYPe6pfsM+fVNvk^ ztrvKtoc6nxJvLN&KlR6=y5Ja0)j{SFz)vbYy6MtA18dC&)tG!LGyJFn9J}lsd|-dr zuUX^I_L6rJ_R&_m$c0I0n$VzF%o9b=a>O@Ru+KkT!5E)4qQoT0d^Y7a0|EXcf?HvL z0m-3*x6}tdxvDf@6rbgjI15DqpE7y|(Db@XN7wp@lfPC4&gZsv=kHf0gp^+{jq%rN zn*)OH9KPj!mLKvFYv}^Jg6n4uou04rsxs_nMJXgw&Njpa+()dT?_93|wd`nAMo2q# z;a?h(dP)U3&@`Kas(2hHbU1Kx1XBcgk;Hyhe1N2Wino3_8)HAq%jW-SjM9hM64qIJ zLt!n0dNzwTtRL?=@Pp{Y_!+{NQjr*vJA0U1rFTg!W~0h;cY{1qD`d`b1jT+V5;ML* zCrAZa9Sitn+Qd6*nzJep79m2?zy7M#VG>lX484~mCehV%sgZXcye}E>&X;>}7xQlS zLY=j6t#zxdo%llI^|^Mur(xWcyNqB9o@$-r5P-scjeTaHIzXDLE4=rmec|fn@^trM z^C4HkIA8wQZo<##gLRgbC zRFy=j$^)tH5am<6Of^S{3bRtFO)XK)v{Em<5b{W02-A2}qK~Fk78M>@!cL@~RF&bH zZGZG9vpp&c?54_qHS7WV0~K+rl&wL3rt4kS8QFnlSG~OZAf}ssk*!Z=C+X4K-6?KP zgiVzkRDWSmw! z6Rw=x+pRkjwE4A2)X4hDi$|Mtzwo--UDmBNp@B2uX>_om7D-RUru)i{&1y~Z~g67DL>-!+CRp>j2Aa~!6ujy zdJXapoDEd{9uHo2gS9vtxM;uu-xI7C8+ffE;`IF^_QFt%Y^Pk3z(-Sq&m5<=uM1wD zM}BJduXcGhkzhL^I(r5~gri%}?4-Hle9MG5mTcp@S^Z-12xya?cCH#5p}fb%$>$Ae4Ao>+}oNHpHA2!CHS zWn}f~oMU|FoeGFML*Mz1dzzI$vy%!tz=p7g8e&T{;O9G`Sa_U0@Wlb1@D3mmu0&V5 zsc?kpuRhYIjIpZ1LU`ah)vgI-4&q4DUnQ@A|8#+`Rg8|+xDXE-r_M3i$`BTmL+2Eo zDlypvJmB@ag%Fy?cyI@sHcnnuVmA1B^h5)+-u1`8pS=qs!eo#2B5XRx?G)icc_@9G zq{pKdPTvYkE0+iDq3gsr8^l$^kum-kFx?#fgNLnCEx6Xj@u>&}1h{q1g>QijtK;*Q&yq2RKJ3 z@2|4gF4$4bUdp29iLRQ~rr!6cd6`kF2ecPkP2D8i$=4(+E}ZSdz~tD z8>rUX-)hT+3*b6ql4Jeo(dktB^z906z3La&K0&*lAnl})yD+KI z`;Z=^PI+cP`zyBf=+mxA>FcbD2(AN;>2PB9hoUoMJ$SSGv8sNI`O8RX)Tu%b;na8% z*EU}tu2YWbHd~m7)-w_G3(Wnkb^@GehDoq}`H24SrjZW3X{&80wFry9(4$PtsMpB4EKaYg*wrX=#{+kR~Ojfx(8+Ze|*L)5-+ z`xAy^o zxC^@)4D;Zf;~YrQLV2`1l@5M>vTVM}(`PwdgfU=1E~U;pG~Z^NRC`{3>~b+pYBt9Fy}c zyiFVvS-#9e9Ey?%u33)hGDMgci8GO!FJ5_0l;PP_ue877?f+&{qQUy``mgjp=O??T zhcAb_&9d*MT$?BNDt?RbJuC;r;thF$NM|_gXsxkp31AFSd=J>mI_&Fhiiu!CPW!h+XlfAzN@!}+0)h;B&gWl=#v zA6NrW;lwW(i^doc{5IG{{mz$CV)wxAOW6W%72m);jMds<(%0ctfg$)ZykT=(svfCj z3#+vx-jsY%T#^GJn42^L7*c7x3fa*;VCmH4dn?8`;XxQE9wsFY9PhNvj)vYfx#E4}2(TVp1O@Dz`hC zwfWB86Jxyh^3wkG74MVypFJX$c%UvE&?zK#6Q}b1r!N$=*`2-b6770F>;&(0_0FO5 z&3zFK+WMLlaYhAMZ6Q9AP4@}3Pd7H1>zk9rEFaMFP#XUKpfnSWHjXE50J!-=3(RTiafF;9XP7uU2c*V7s;;0VnY7Q*4eF6;6Av9HzBi2N ziIp4XEj_qz2$k+y#ap<2jU%Vy?D(76?r%iL`MUCL*WB}tvUh8MtD7oBqcQ?1l~{M6 z0b`)Td}5fjX|KuP2P081&uQKL*4>kO>q4%+b*Iz8UA4Ea(IU8u5O^dlFnC)z&4}I1 zjf}tEqc&rW@had4NS@n``j3o6-9f^=2EVaN`x2_(t<;aare?4(RNS&nOUiG4n^Z-r z_-&2w>oMdZRUfseuB)A_u3p~ES5V0m+MpEwE5?6INn}G>|7A~C*jtItX1ar0aEjhJ zD8>biIMUlQ>+?SdP=_>PUbL|sj9~(3l)n@xJpnZDnRBwRDlKsPKzAy*PwPbNU!T^r zeVoh4;BV>08(#sNt~-5zh*6$s+%_HRhxbs!6M_J-auPv6{Xz4p^Re#nYEtPmCCRJE zW$pE(KK7qH;IjGWa(lM*OA>l{Lg64p1|JIp4;cD^pqCn7KIY#p51eGP(-lI89Ue@6YGTwIk_Lh60?X(-f*DZjqe>=zbw@BkYSD|J&QRONY(6$wu+*8b zjS#*FRHd^9n*J$SACK@+mw-~{tR`w{X??Nxnz1a?5FP>;7kQ3o6}VoB95uLJ86T-} z-VgJ$nk12&5<`RfD^$B$>CBX9twvM6j$$>KLW6|L9}@jsJ{rx5Is4W0y2`PdJ*9jd z|5jt@U(;Wzx>0KLTZz?dyTnU0?b^~iLPZAJ9t`p>9Sp)_1O}PJ1cTUvLD1i^e-YX8 z&SMTOW=*tnQP=n%WT^kY$xz1wd$h8R7AmLi!L>6P!YrcV-gm9ON_YRkg_6PllMB7e zv6bq=mIy0YAUuo|zoHVHnL%zvO`uD{kd%I}D8=098$5OOT}BYzjg~GY;}tm#w67jk zFyKi}FuPx)e6yD2SLTZ|a2cgz5Zzfy#3R+$X%;+VNHry8NhK=|XT%wpS|mwB98L&h zC}!C6z!=OSjD389=K{u_w3TBkjXM4xo2lG>EB~zEpCZGnRsF*zqBdPc>eNe$6 zQ2i$sbtG1qx{OcxMsMf)=CF+f7n~hJgd(J{M178@GN&jJuYI=Z6+-l1v1ba*%38>(UV z7}nea0E8nQ-O@mo7024(PzQJt*kM=r1)6yi6{JtlTmcLPX#K89P{TgexIGB+pBOCC zHwwxeemruZ87|)(@}-MVN?=#vvZD#b2M7Dpg*yDGqV?cucwlUGQz)48uEc%~Dt&V3r#=;bLRB&pgdKq`a>XMDz!`y6-A9uDDJ;bsYI+Zz9l1G$l) zKFz&{8w2W3dZyml1ZQ+x#|9QXrrofH8?;IiLr-MaxY z7_Vd~^7rx9+-afbxDBr`wTDF~+-{vGIANGr)9G9YARpi}7Pa1&xu>W=b&q@SEC87nFQ^RHw^3NGwXM_js6SA3&n()9$d->*ogM`!aNlxoRs-U;1e82h z+JWA-X(GdaXgbb_agpMAP&;C-z6Krc&=*>kp+|OS7GDo2F!5WszFcpfzT6qK9^AjS z5u=0dS9do+ubxSPE-Lv+8L$8LF8aPQaz@5FWLSg@KvHxFkNg_Zj}|n`31Fzkp#F#x zN3J@`?1HQ_6|$#{zMlKUN-i?okvB4A+m-?gA{EWtA{?D5T^U^E^l#E@Nf922IWojC zSno+RMy3&E@=6`eU5e1hFf*#TH(okxmL9hK1cuY37 zTSG}MCNaz{rnJ*B%n~s2N9TSoaFs#}nm67isVrw;pmCe`MYEMw4PrG%a4E!Q$%B2K zw&HKb^orU$x7YO;+5ED;3J@z01rTsA6;x6>aRH#tKuYf&GL4;>CY+ewp-X0Am%KsnZ0;zPItg7;k)5$eo+XNq=K+)zQIW ztU!H5UD?pV{I;zuoBWB?ke2SvW-CTCr!=WLbT(DkzECx@BN`r6aI_vd@hU1w*SAX`ED@LS+4XafhP1rOim5k(>dMesbc})z&etcM<1L6F8%SoKNZkroy zc&!lrTOFXw;dhw!E8|HAAWyN}LWCd26FsRb@+3$15P!1ShP;b!?#S>r+(t3`_mHm3Vv zU>1B@1bl1x7uE!T?;1kr*b=XSil^chM6~}6S?}DJ?EJ>XIvJm^%`^O+^RBzGAjDOZcJG}gNqKE%!Y8(U}@J!fIb zWFBPfWiBbeKpir!Ivz6wQfI@=nhD(BsM-t~;6VlUhg)R2Fs3N<#&=S4fmF~_Wg3#p z-FA-<^Zg&f;XyU@VNLmUS)Lqu{tuE)R}AY_q*Sw3%|hD_#jVVXR@`6u^1CwHF0ktDjZD!OKPaV~tuRKOM6OQ6PD3sNIA z4}LW`;z~>urbZ?dnmltL$WvxBu8Xxc4M2+XqL-zn?FVW>+a>{UWfCup`?Y5EW#Y zey^bNSs$2^olbX#_I|m314XSVD#*eNQN3NK)1oE7W>?OIchcgqz-gD32f;NlK_`6) zUeWhLcwqYO8dsy2ah0QHzw@0=quJ7OGFf0B#qSSc1Dv5nVH?t);~opFGVbm7y$3ZG zC>H>Fjac?74nSg%=EvMvpPd*wGXI(2sfIKLw$k#J0;4Js%Af@#n`N3-n#%PrG`Fy> zf!BWyl?~Vn3|olwFrEqZ8l|eDf@A20QK4N7J?Sopl85Rtl;|d==kmDT?fe;Km(-y` z>^SRI5p0cG<&nY8UdOF6Qnd;^)mP2iRJF3oA^3R_jjRiK?6+fy&(=!o_sBSubIqSL z+A#onGD)Rrb@rsBNhXcFM!!YzgI=iairurvrP(wdx5Of)ExNwV;Z!6xx#1k$?fVK_ zvjp}CV5`LV+=8Vm#STJALQB6!doK%1f&>PsJTB6a8snV=Q&V=an{f@2ds0=2X#{xa z_qqR}V2RUz-LgwNJ~hkg@O!29#P^Nf6Cj?DzW_NjX86aZs^Yfy>d@b#Stc+o>_}9U zbFZ&>wvk#Uvc*KX|INs}TE73HBf}mR2)#c32O(MeqMMbQQoQ=TCRJpWlA=ZW zXkwp9g}d)lmfNpDM$J?4_L%Z4o1*$&&GI|KMCteiqyxandJ_78%h0Dm9oDLnbiC zr{wP~!JSOMl7E;V98W>SIRXu7Sd*xIL&!2~qWB1Oh5W@nA46VDCfUhwzWmNOVNe@E zPR3MrO0n4;AtW_P##BuxQYS+!amJ!uIpM;N(sFn1diD9_OpB(%Ul|zxo#(1*zVqWD zd%;=`vdcp{4jN_rKu8ui3@i}I^haUNpN@)376>#I=)(pNULUp$l(0#Sc5)qS2d{g@ z&8cuiLyG~3xod3>aqj#k#`$iX#JC1Kp0eKaQ7C%+NFMC!=d^CPv`^b{o)-u5|4^Y2 zY3cYJ5q)M2m1`W%nSS&5t-ih#!<~jI?1{e}9>^UGPbsP`PC#O1OE)3Jsb-N$T_o2pUBcf#T^iXTYBzLgnix=|X!YW-6D#!|}WKnb7GcUmt_iB$L_(4M=8!86*h##jpC zn*QU$bPBR@V^Df@qyT{4^AAbk;Wv?iF+o}@+Kd7JB@VYYFWp4&dOhiw>+X;5+;ucB z;4m9YR>!XUt_OB8r`c5l;7C-LuC)H<<*&Ci+v&DkKTZDb zG;$Oo2URJ$>5%gN=#E>3rv#@mE0r)fzU$vquOr>o($5>m+m1N_P`Zwq!b(q5W2?V0yau!X8S;W{PXGI1T zZhzZk3`iW1gqk$jNhfYy@`{GG{z#s>6CLq>IlK87zrfQC>Av9VdyLVL_ch=DAx|V*O+lQQ0T z=^>tFyS_7yZy8Cdl}x=Bt74i^?**@hxM?gq)-<#{%;AHTi^OR}t*&AY6}k<)#m2_? z5j7w)%=5CXA#Y>Ei?%h?NM`KFqUOkbK;MIe=ZMq&$gpXuMYH}W0O{c}wgiY1zGJ_? zL-;IH(oK4jTY4$e6m5VU9|OBaRjo*Ta;f+YI|6SCn>EF5w8&6#wdO-_Ad`T`GT~%U zeVO$E10Nn%G|x<}&ZYADR_S&ywsDzl8kd)ag@4j-Ew1}E&gY;tM01lMQ%C6rW@D)W zV_0GmU7z+f_lM_^zUT1Xc}qM{X+`q6Q@4)8&PZ~^ck zgTss6ZB_$n!=V8ERXy+RD2I{n!HpMfrq#-;*I8vZys`XJt576-5rcn%8^Z@7Lf~_i zG#BN+3l7#>&=1xAIYP_c!!7w3C9c@z086W2*TDoexnOV6s0E^GJ{5MTp}QBLls)UF*Ei!VY&G(3 zJ122p`*^+V(?bk0b6_DpfjO=;ztEyaMnG8QJpchy$IkT|efRD)qrM;A1K61kbvP3x zE6hdm@c%6&f>7clPH=H|7GHuWJ=#1S1ES7=40r2FYwQU;12W=K?3F-l!T(pT^a~Wm zR(AOMuDTG=(#&lksTvHDEZ8-%jqG~;EW`5TGKlw^QmrD?&-+a&D-M;JGP%vX$CM;Y zv0uh-t?XVuG}D)Z656pGUr@oQ8{+2PFY*sP5&8z%zw|_$-jLV3n{1>`1qQBiy?TkY z3&8`Y7TJ~(N+t<`eX$^uqwFEp&d5mR7z0zWL=ctN4+cS7zkOPiMWy*=o{WErirxaL zit;b2Vuf6)YwtoAqY7-?06vI|`uO_$6sjdZ(8@+3I5V+EKM?3jvD-#7Qq0&n!zE>S zwKSG#au)Q#ekS%&!V?01<>XN8`7Xa}Y!gM+Yfesw?IqNW{aL5G7imdO0w4z=)lwWt z@b?WxLgpx;e?=yRGSW1~HL}xO&m_ z+J-KwNcV0U5wj%E@mJZm7X|A2FXKFCtMo!x*e$ossosj27xvjKl;9cD*ZKfX)v$i? z<2YCZeG#YCesEJ~)OHkxaz!%EYPj8Gi%|S>PLmUzd(G}*|4kTJuhLoXuqv;@ zqvN9o(+2oP!_5QK5TY04Vxqz7?6h~c!jHC4S3?X}O%jeY3Bbicg{w=ZZ=dV4rWLMf z8^ed$qT}m<0YA8-{>7j1z+$e+J-6UZlgrJJFmp`tgwMgHHGNDAhaI=BvE>d`3=-<9 zewelbSJV;y&x$&d1CU(>BAM5*rC}VtAFNjAhx6AAi?C`BEnq~Y4lM$3BITV^eFA+* zE?A+$ecCezFzw0h3KuXnDiXq7(|Oy*zfXv?jmwNkwvA7A%C(I{3}^i`GS<)kX;dT| z?3$iEmcCClO^bwDF;i9a)2Oprp@N}x>`31s(VxwI5rRgjD#(i0f+16 z04zUMgNXIHfl;f>KXw{^5J8br6~MVh)^uo&|GZ&)Tsk;5sz6afaIL91JCT}7$UhD+ zTA-TXHx6ID0_>Y64$(ASkS<@uuiYKDUX#V}*n(>MZVKaec&Gvfspgg?6-TDw5!#Af zX(32wg-(9o)YdHbteM99OAgopPv(3Rl9`^O8g>^3j`CPk-4+c`%yPfh%q{EII9%RH z+hD>#RI(Z)WKv+p6#KB2zFH|h))CW zURRDn&}WP)zIuoKV~=d82sc3%X(TNz>P)8!+?hp{lXfTyG$`2tAILgSlBWcQ`mU+P zNtye1;v^?r5*SXXQ;3t&?!4_@2Xo4G4b6OHmdrNkO=I}_Mzf`3g2^;mb1=i!RrbcP z{eENE+`m;z>Cl=?isfgf9@b1Z&;qYP7Y^-rxKq7+zb_+@%ux&E9i z^=as@*6{W>osc|OYuNQqt)WgBSWF0b1~-r;U_MPy6f8LW=m8cS)_VLU)fV5FHqAGt zO&h^nbZs)IE&VH0LljK;!^ts5<0^yo9D({jE9Rce4_{Wb1{^j#8I%Y|)_VRHj;t@A zi>(0o#Rj-+TFTK{ftj;WtQtcw&sGZN*}%*ht<@WImQ-l1ROTnlDqczgCeJV_XXD=*O#N%#D7aqLGnoA#Xhj|)Fvuh(7{ne7@-7_=!eaymLCzxZVMG)$tw{=D zpigYLRc2X0Xp#!FmpqWFBb(8I(IE4CE8$$6>dISfjG*PzfdXUa%O4}wMx|on-&I_2 za6DO$s7jY&nT?!oaG)AiApX;tPQwn&nw>fn2W2A=3(b5bYQ?DMOu{$^*B!tJU;uYS z?@BOSBjt`)@q&jC+{AX^8bTC%F{Hl-^GsSB04P#0E%h^kF3g+je%qRwvD*1mZQNKd zI|njroXxpr1g%0c@5HUfd=AnyE6rZu$>Bn`EQfe&jN(AxW)1*)EDIvwHs(mqhHPt7RbP_y{u_I zY8$YOSZCHg3xTt+!`=uaF#X=wA&iLIozAe_8}s3d-bgs&)?OhhzdU{XQF|;h@TZoMDy!#Xyw#qZUs!i;gd3PS}R`^kP46+{G?Xa?;Fcp6+ECf~h_I4P$K1V_EE6+i}1p zM+~J+m@^GzWp(9VWUI+v728?m`|C%zxsOkisXvs2^_YL<LdL_gxgT@aN2JsQBZpE z`b*pJ^Z=zhbtw8-XU#8zAD{nU7^ortL(pd|=>~;Z>)1jsi_IsQ7H}<)HK1&~qD6~0 zHQk7H`%izMT2Ofom2Aqa#UJU0UXJIOWK0sTgl)lvvZLGO!af#Bz1lu)8sZQ~#&xo$ zavy~mPzkp1`fJ!vGuBv_v_7O%z%oru&7OOOfA`PX5Ico^ve8|z52^W-h9%NQj$|hq zL%%%HwS4?_HA|cO^0(eU#f(2>;)r4$aLeX_v7{3M%_kBo)bXg%qc!$SWNt>9&e;(r zpUqIeavmwWnX18`50OiVQ41WuKBP+a)2WJu?%!wl_Q!MW<*yFShQ?(mea>tJE6UoS z$TTO|J;*#(FV=kG>Yot}BhOO7qr-)Ww$M& zE?9{dIpv-*{x67AR1gx22b?)JkxBq>esKs>SUA!$x&f!_iQq_O{#`?NtMsBRko_xz z|5w6u?T-Bb%g1EyG)ydOwq?EIoQ~*JiAt}<2qq47Dx}qcLAWAdSqDk&8U^!}qVd#+ zl*1&%exH7`Jwiuf{UROu*p14&4ROBO)}DE|`x}Qe*?QM#6=I>LmQP#bv-_8ZE+>hN zH-{Yrkdz^@_p9t)h(6#!*8U_yM6U1(nwOtJPyfj;B~1U~mxh^qP~0PnfAh=BIL?3B z>WE!ieC3DuH)9=j!d{Nzly*{s5AMwse80tMlqb;L0%=Ru>hEDKmUf>S z`FqDg=raH3@A0Oshy>M+)n5$;gBN#h+UeR?JW;i8R1$@C|0_OeZGkx4T{J+vI({Ch z^J9@F6;+@$va7bn7#v3%W#BU(%EiEMzj1*X*4XH@2;DVZo)|cdn(G3LJOtRU)*1=M zybj1M0%s%a=x-h<^`ZarKw)kie1ZUtZSh5C|NJH)WOuY?gVW@(`^5RmK@YF7REknFlpLf%u#DRvpbV#-AW&a`oC{<}rJ56};;{!2fIK^2{(M}AUQZ#_bQ?zhAt z$&Rv{N;n+_mO!2qto3jDL2wyu4$&l00{*dLLJ`nEujxA{s{dZik<;~U4z4LIVnll0AqL3AT?VYN>tD)KhnmD5pQJB z(#9dQ)3tOxJ1CqRy)l4kFo*Yl!<18hFr~zQ#FUx;4W|6^FHD)HbB{U@wA!@FEO?qQ zkDdpq8{Y$LteCs-sqj_g))f@@sL)0y7ypY=68wu&;{O|^l>dKFN^TmQ=Yar6-UuUQ zF5-Y*2V8`Wbv;$mG%NS~Vo%-=3;f7i@XZy2Nlv7C>^gT+lJ!gwF-E37Cb~o

eVCLWmpi3*EfvXy!<%Yscp=VEK??S(m-{^NctzeL1oF}F6CjeR9KTB%N%M`xjJe8J&w4r6zC8x?_8*aVusd7)2bHniGixn zw!ueCS|)q46MK6c&zd)K>FqdD2+4h{27c}};RKLNn6SD%TQi|9RZx;Ig=Pl(Kim4% z22nXD)OYN>UY3?%D`UmnpoVV)$+`0S%uN7v8TOx|OAKeitK$w5jr@a}OsnUS`e2GL z%arV9->OuoKM$8}^8So2@(8h9%0I)uG;5Y=*_+nD(E;nmXzmS>nVsbB(?_go?hC== zxgNV9hFUi+i(P5wHJsFkQX3I+$GndX>MasV6-`rYuSz#uxsw~PuGArBcl>=98&5$% zh0Py_3Xct$lvpV$P>pX<)UZf=L?GN1QmPRg)EjlC6yJJS4rNt~gi2_CNI->QFA^Nl zYb6pKX@pgK*p(^NFH~pM-Tw=+9-cS?dnhl_0OS=h7qvLDpFZOqvA5A=+=DOQ&dnXY5^tr&%lR7nSF^av6A)d^0uqgcOoZm=B@-%_P#V{|9YZ$d*>s>7$@06Tk?g`Y+_a%O9G%nKj!RfJz4 zck9&(I)+kM4Ni+)R1NOsI@Ibq*y=g}=Gx!t-q-5hyUO#+yJfSDXEU>91B7RTpmm*? zcO9ff+bQW?)^keLu~SA~T?vZ^>8C{FS77zJNzZl0KU1vudE7qYfIh+30;|PcgS5H` ze5KDlm7wWBF9}cFNDkG=-vh&jT;N8J2iQA@@XWB*X=-`LmN`+@(Hc#Tm7aWe0Zy~j z7CS$J*vL8avCUnzZI;{Zd_j~av7an^B)OOWTgVDJ<7|285;`)oLNf{fUAaWe`22Hm z8VT=e#SdrEnQ{peF>@7>lkgh7B4<$nY@p%;buG1gE=n!+C0jEIByVf3vuFj)pFZy0 zfZ}GwF7>W2nPeR}b-b596DK)Eyax^wa@y~Iu7QS763w|^5wQ!tBPg+@>o#zIa~kq_ ze4HOXdIXkSPycN6eCLKx3$A!L2jMjc{T+e>0ef@c3836*!_%?IY~SB-xa=DaZvk-l z8h7lZ4;0hklE?c?-cpta4^{K)yNbDefapwqHm>C)#h9`){Pvw+$GZip;*im9@etCY z5-`!4J3#@@e>2fi_%bE4)}yhaHmZ?^e#1UUA@_fB(2DFaPr_FkpaQmIuYb=m#pV$i z|mMS7iAk41hC;4Msuu+YCI-Z1}+17`gpt<>16Qf$5n(Ry5 z_8;cG%~*pZKL}F@=pgH#!bZ>rk#oAGbv4v1z}&>HzvH7-sf|vbNoFVYu}u6Y(Ay28 zEmV}6SwP{?A6rjkL{gmYtFe(gB0b3Tl?`TnZApL4?{{54>U1^oWs!ri0NHpv8z3A1 z4k*M$>1N*CvuxkovkKoVwFH2c9N+m^Cs;7m0ObdaBnd2CoSLcn3em226IqGTrctLa z4bAOOOB;$xp*T78QEirR{B?sN881h>aCY}NTSB!;sQU=BEk?8o%xGNLfNZ>p0g#Qq zcL8MM1IGZ&+MIiH&pHkU+_OS)0ZT0pE1*TD+K+Y+7pJnmT7RQ%wnM2s10SqD8M55n z@RPHP+w;Lo{;GOo^jEhDx#y&I>6`tyIh&jP$+=qh%ft}dDTdE0GJt=U4&a{!m}Kd$ z0VY|=?0`wu?wd&#%oW;LapF0&N!ZN~+d04}E1eZE$}$1mwz|{+qb%FGH@7X0qt$o# zm05s)mJZ;b1$5#&26U3e40N&|33LMLNE+J*B58%8U`MxidHc2#h*3PD1>fFc--i+*+rp2H=*3$X^~>+noQV8&9J7TQ`1CnG@Cd!?Y>-2UFDo?U&YJ zkndqXhx7mvbY6J}=CuD}3+T2bLKMWi8_xmQHJ6L$AuioD(!rnNP zoD~<2KCoK{@KE(Qe*&$~;t2%tXW>|4kcY9Ya==>;jg_tGzylnVNC zw~N!^GbK-Ss0%yUP=NFWuN`wirQ(Qi(LiX)Oz5^Wow!Z`=!OKkkzL zz`jcJ=WZvD)YKja%oPju-Hi)o&)Gux?qt<=lbX2NMa~_G_E+;4wiNMd{6Qv9x{~Bl zo%lruu63zO_^XqjYJ(e**RPqUA?0Q}{I{f^)Q1(C($5ylzbh~Q6dA$~QEQ9*`i=Ly z{8JzW`7)f3_l$H-gidyfA>PyWXnLkizvoW5xw%y-mV??$cHG z>FKh6))2>t^5m9*<|WZ9-RbIj>#@v>zNYi{Xx5RVu=f|QyVSHVYp?Z&ie?rUM?7P< z;4yk_s5w+e4m>dtpKi$Qs+P8^Sq7^e8zbv>hC=+`OtX%49M^ExXfG6!Nt>e=QSi}T zGke{LX+Kxe=bkluOD`U2RF7v_PkA_5FQM@o>DsGuy_n#lRuyvk?+vrCPd50<1}Y+} z46DX+XPZWjQh01GwZ2(ek!ht0F$qsORpK6?Ul1^H4T`tJp5}{wATME{#D2n#>uXqB zC+kVaDAIh04ydEI!mBxMs3{;!WZU^PKU3z>{L4FSDt|GrX2;3Aq0Gg+amz7obvp0U zeCR26hI;vt^0y_Y?lZ-eswT-Ztc+rMCcEee%+t@DZW^+CAvC^lf&1o8e2ueiVg=P_T;^OQ2Q+B- zsu_d)!K!+m(~~o?ioz(wP3n5G)9~zO-l$t%^Cwi%MVhBBMAEGD)x8AXgd7Da2 z;N5u8#?cCIOTNc$f}hkI6QGTw7Bmz(bWYicC-O&=4I_#=3!{=}Njzch8 zSwaNPgA0!M}jFMnAaDEx(e(u~Q0fV%F(Oxw7nZJMWrdk zEPaNaw<^S>Spvt^6gY6L&epCln_u2wl4|7K5RyvU@D0wO>hW{$EJ$q1qMb6eX{=z) z*_sbJLDo1FjOh98mL>D+x1VxYF4)OeS(93y1?+?F_FT0#8GClt(W3J3>8lUh^RVb+ z0QH>odBmfKS=@HOcn0h$7LN$nEL{w>tr zModpcvPfX{?AsTtFH^l3VhNG(GkjW>f+CW{jFA+aOch{Q*b87IN&Z?L13%<=|5RaO zDS~_Wm7fyjY!7s3XBiUZ%uqIfA%DE$6c8EoM121R2QAt!^eZPSQ4Y)Y`@GI9k{P&Q zyd1*`7?A`>CHhbb1Wtj$IKkEqi1KW0T>|(}GJHrbJ%+)3#O6&Fft)-T2W2z~MWQc$ zA`mDbR1hfW|2R1sSVNFlL86*=;MEfg_YZ(Yq~a*tAQH&i$Rfr2G0Y4PMahnZIOT*H z9N?Jfw@I|j4(SKoOMOnq(o0Q*w_VpJNYwQXuK`X*#m*Ho^%k@RPG%OaOE5lL%oa2C z5MeFI_#NU(P<{r)nNT&am2IBU28yQT&PLYo)UCkPc!M9DFCO0Y{=@UD$=Pt~a$l{} zYd96s-HtNJm+C`vm&9e~Vy*tVDyxF=)90=nNeTTB67;W^6YVdc>5_V6l+%n4#8#53 zU>LqZNN{lP&=KjFuH-j2NT7@rT@qrj`!F1p=cB>8Xf|#A)I~vrAfVBSJW&M-Q4p27 zh})8-IB3N66}$9!{E7pGnNlCJU_!_Pq9R9kdku4Be^M|Mb!_AMNup~#VNHtGE=tqn zv!xA3pwuqnVJf7JW`Kl6#BtT-2;_iv>8Z9CDi&hMYC8P8uE?~lbM z1}&u^!vg4;3DT2wVx{Pqe^*J!NFSvH>5EHL=r$ZUa%AE=dk?~sBv!G`SzdkoOA1JT%zUN``1h$KY1fr@uZDSf5doFsJ-B}GMGPtDBldI zfe5}jE?+?I>8wpr z)f(K-pqpN<0@MNKers!TXX~7Ln)*CuF<%C!z1&zxc0c1_6HnQhNz9_o63^~RiePQy zSFcH+bQc7}iPkmf9RU`z5AGy~x53##;+{Eqe+=q{D}B9PYtMGkA_1QV zN_dar{qQA-Z&FeB42PNJTXbP42gc%AFs~G5Oblw34gl+3v&JW1Kr?ns+Cb&1nh5Pj}Nc zOAIZZs<&G@t^?9Gm`)YzbUp;|1$Xb9EA~WXL7Scl>=i+-1H8wD!C)CURF00^&77<- zqu1p4kDZ1+oG;MJRo#3GW)1Fy30-^s?#sBaue*|9UX?}XPLiz?zoWV4!k6S-D80X zUefppCZP0fKS|eQmbZA0bgFf)@4xlBG~FyiM|`1MzlJ4wsiVV@O}*}go09n(f-9Sz zB%z?JR6+=XrnQx3d*9f)2|JmBS3X(O>JiOc&$x@hA+XgJhH&R^-#MTaO^{8QNCbTyb%uZINpt1x?Dn{~b91=-F(U9#nTQHp`YxpCoassU;Hts@tf5d%eo5||ZRAe7x+utD7z zV!$9%wXeZURe6iU@G3Iw^isLY#`#`Sn`J77mQ+)RxiIJoKC!m5clp#oN%ZQ#JgZv1@b-o3|S2?B#9J9rPDJktfD zPK(Fj)h|Fq&X11xp72{xHcbQ=8LCVgHC~o&2V(%4t*-zSEyiv?h{}9L0x2|6Kb;6p zTcxRlCgxzd9uDz>Yn$!afL!cItXkv|N3>@Y<(WfP_rgZKsoD1Q*ERP9A>wg^ZU`>j zyIu43nolJ}GVu$gxs0iHw6<&t8^?|t(w2((xvD`XXf)jl?X)yo#~Esp)&f8d-`>RA z%xeC_ zPJ$-9&Zg}Qwpog$Ez98f_~(6Gy@3_R9Ye50&?&3Lb1C_D4qhGnJ%J-~N4 zJsz%GltXRZB_3w{xK4NS?hiojsoIyIgdv9vLPu(d_YvW02jaZa-E$QeB<8O721S+J zTS@tDPbDy2IzP)2)y>56aI~KyPE&_a6LsfsXx#fF8-+z9cF;ue-UgP;VUmN0x%o*2 zes~o=g4;s$kFupeeB5eHgSuj~49#4VEtvhzxs6Xau@vb@cG|FdZ9(J~0~Uo>4`TBOa}azp>9aQJDBHntjR_!>?!;kLG_WR84*` ztw;T8R|n-0&gO;Osjyf2I^*QB2PHH^z!NO67~NjVEFqDQidikE5RLylX+ZH{aHp67 z1CPEV2XC0uI50&?0fXy43obiD;B$QLCvs{51yQ9BD%7z|hxO(Si+*Ke#O8?ph zS6%>O0)ECo4jOsJdQNk@yUavFw4=)c0{+2iB`(+0su8>6kLGdfeF>L>)rq)M?dI&Q zC;a^eS12xX{}9`CvB5irY11>k;>opc&D_?IL9wF|O9fhK8WjrX`KIniS9?-W*FMUiY@>7xV?)Pq5B9uOo(LRrF(Ls*+m%ZC(E z?sM}l8Sa=#qQn@c2eZ)uLO_2mJWP|=z1SzEOlljt9BxN!wKUh`Lm?S?HvKf5^wY3@| zg_v1Nkc5q`rHn^AH&-BI^qno9s6Q9!d8be!wBCh@3?@(p%bU&K^m_ENc^O<;NQJI1 zqLXZ)7Uo1_fzbTq;DqqabEM(DGE3kZ%OIl#!C=jVWb;?E$sp&C)BX)7Qu(6B%Zbu( z^>x%Kbw=~$hD&0aK=ZN%&|;NZdkm=J!df#hbFDRMX&z3Vu$&@_j3&u%4S}}_VIomC zHZnCyCKL1Rj(^Z@C}5@5Zm6F{NAjT+;;>E@w+1{XWa$AdoPv}x!&$;t^bzhdQ=p3G zLAZ-2sJa4&V3VdQ18t0})i9`9mJ(3L0f8JxH96>GT$TGse+>AEsR0B2gOAi%4pM`# z`|0vkpj-v$fB}D)sL%VXZa)?((F!%`ea*3*)$ZBDq_`tR+I}e)Dn<5oJz%RtW60jM z961+;LUEGB31zb{Vb*QJ!U|L&FGPip?^3+iv3Mcx;;2vHQoP_5P{eL{m8#{Z1K#95 z{4%=8yIc0%=oO&sDMqV=1nn<-)kcA0jXhPb<0TfV%C@eW?Pxygw^w%)pVq;k><-Gj z-e;KzlYMu$Fbo(VwYYjuGQ%zaB|kq-#3S}GmH@OY6ZM#O6|}s*TI<90)Bf}KZCl+g zXB!amU+&PpG1i+}%ZO-KMJ6E3l)tc=B}C|5on)@}<*qv!yMAl%>z_l^`KX_Y;=jKFAvFLKKx_l9ltlwA{5u*&LJD zJ965~VrgMTtRYH5B$YIJ`+c>lhmzSY>vTsLVF+z7ye%zfNwO=-w1aN_4#i!n&ikPH()I);ozKXWL;Sf?qp3s9QBH8iU!fk%dWNWz~7D z6TtmPb9oE3pt&S?|kv&et&9$xW*Lt z{kPsm3U|(*=hFn{AfEM@fx0ocS2>;v`Or7+M492^L zt~kzVNl?#;o?XVZ?m?Q{(D{xb2cF)QbrK8{5ZPeA#}llI8;NpwK#?7+$atS1nGr!i z-m{Nnw}?tx_MoVFOOeQ%IXplhgK~Kwpm|HmdT&Yb!6SJIua%#g%7afP6tid&1EnXG ztz4K_Q$nr)AwNoCvHK7$qoU#ihqVZhLKV)9D~`zij#N^IksH$@;r8^2Di*1mDWFc8k6^Ulo{YbosdrT$ zE0TGfaU+?!5XV$FS`DK>h)bddxj&(E-clc)ObwmPUK!6!{6%FVygH#1&!(r&ea$eg zJ{9Mf9Q%0sz_T2(87LAS=l1H8vQ-zz88Rl~98umolCjS*zt0~;xRHKgY_WDOEYLD% zN!wjjAL!buueBAUH`B{!{>YF_AB|;b;NRjsbb#4!Ef~tD<64C^b3)Q>`CgXHUdxcN zWL9)nTo8ouL2Xp9FiKq9E3hEU7Cyf_6<*zX>t4VXCU5C?YYn5GH9nFUkAS);1;1;c zy(YfySG%^-YP*T{?+gaPxfl?ZWwtZ)QMpOaQ^Au=47#*WL^CK%V`%wZoys z9r{)XI{qmOG_C5jNbTIg7Ru#us3_JgPny+mCFN>L7jbmF zoPTEA%~>IJmZXqo2u`?`NS&!Vc(>8d-OwH<$+~=7_|8?b=qHz~SqG*aWt8!=o+}H< znTV-4Jn@sIMc4*7GB=|Y>ZUR&UW`^Fi9(hPRzs($`BAA%PqsW}rN|-iagNl=1(k7Z zne41eNP7kc2RE)#z(UW@3tcu-_$5;?GY|YL9QNJDz9rA8ugZf6()W3VDkes1xN#=b*;1wMwN4_N9QIxr)QMhqQsf-dbg|RHUCAHT1CGNRE4pdxti0_}=C5tBJ?(yTc1zozzT{2Ir&zpQ8Rv7Ynj}P?zaGvwAxkM=Wba$w$6Z*BNRJe5 z5Y<`7@iV(tHa6v13|@XT#={%AkKUO*nV(%9#pXhs?0 z7cUTjj`5G9^jXNZ4$;Nj_t-dcW7oaH##{vdS+RF|O?s9r!$XfETtwt<8S>p*G5d2( zlHxn3Jr(wq!_S#kpfopCQxN z_UJoPjt89b*gv_ut5k_dyTkzk{$gx#8B(P5@`dZU(fj9b%mUE~6QU2-d|&yiJD)W_ zUF`6Ad(h_d1cVxHZTs;PlZ)Jz&xA{lxq*m~&d&3S>~E|!9k9r@(E;IzMe zx!N^8VL`L<)@DvV_{-29ZbnnyKjVp-JF4LTX*tnfX*tLLo|a$k#z$b-_+I)OQT;P5 zUm;Ji?t~zcoLQ+Rib!`k?6?{{tl)3hRTigrZo*x9Oi(qV(%k=?!&Dk~An3)3)(_s> z7cPxcH8OYZl4+5~r8%RqHU=S>W#f5Pv1gFtQa!xPPybV89dHszydiQ$hlT~2_V{(k zJoda1-*#u&mmXliNK+*dZ4z;roOHcmZSoMRtcqn7-btA4Uh!I? zO~7BSJ7+WI7_rF{6V{J?69j(404C{zGUGO!e(<^G&RK06lW99h^0+PYH#gsHd5T7HjAfxyd6a@uLtIE|4@m~(Cg3u&giH8Xxo znuE1;&gfpBpD%Bp=p`pu#BotloXqsld7g>t!#ANyC*U(fl$Cv}uYRR*M8r8T3|QUO zN$4sP!k8iu`3WirWa)lI-1L>CCCZuw1?432@%g{c(xK?zvUJ=3Doc<1J4<)@FS2xs zKUuoq|1e8G{uTo59%j9oFAeT8WcTC#@Nr|wZ|~6WIff$QOOWW?>WywpITE_zxBgp& z#2hVIO5^+Y60W?9F>T1fBfJW-Y;u%DtLiX6N$(@3?`G|gYNGeR3gM&zFcgT}nebWy zn=q5v#x=zaE7k_j2hzw%k{FE=Aryf(@ghTunRR;>fv9Rjq~llrm92yIi2^3*zm?Qh zzw01RF-iJIvwwBz%U?@~lpnmg>16#-~HDP^m!=qR;vMvd7T*>dD22yLV~NF5T$&OTAHDPeQD3vs4^=J&W^DKi%3 z>0!1CN)}hp8&mlEQOUR@V5mFa4uWs)h@maii-%;of_6qGrbL>wZH>@&pk_v@Gs66mc6q@cFuo-^IuW`&6Ta->P9^xrs4{6sk`8)) zBj#Q_2BtsBEqei@w@E3##ZxY!FDJnqYGMv2YuM!x6Ct*D@XHT(F-(0oQpn+PJ?hw( zy&6i^+`7~1JfFQQZ4LIPZa6&0rFAf>G3nasxs-+xU4{#P%Wbsl3Gamz0@GekE48+R zP7Aq{Cy4xNDP5b+ciz%Ky%4fb@Gq87ewQ}z?~jOUIR&=KS_I{G6dmGy zKQnelfokJ_6k4+{Z6qmCnnSS0QB?b6jTcM3UicBWRts$I>$*gteQ|q=z&1rgwm^CA zMm{LghaxgdQwP5RW#<4tO}#-GNG9R6C`f($Aa>k=rcfJ>_q8bNjuL9DEf)eaa@JSb zL*)kup&wpGErmoZgV;!<(1g;E#g$^0Vhio#mJ&jfp)I8ohOev^&aLFK{8wx}7BQit zX&%4mWzll>Yu)T+?v!bGEVimwEf=+SSOXMe^(_64ZIeNb;ZAy4uA_PtA~(Ip3nSWVkr zGs}%js+7BTQ8gw6R_Yyo+uyrf=}oqvEJEtv`NU5A7-}zJmx}IY5(3L3cb*r_5xB1l z=yYVmn;F?nH&*yQ5d_s^6ts{C6*xv|e;}CBOIi|r+zO?nTB3&qYyEyUtWxz6dj9GsUn$_$(T0+h@OnZ|!pD(+(E{pKF#!7YA^5i`zb8V9QA=@Z?6(vofo z5QF{aaYQiiw{+)Sv>q<1oq zw+maCF-pX)#>9H9^Df644ht{GDoCl$N=JTta8=yNC~9FQko^-1lIYSmD7ljX+}chC zaBD!x3vwH$5H)1MF+*jeXVPa*eI~MMt<~<%;>hwtbLl3rP|4KvIahCD_F|;gx1n zH!)5#*VAtlsvQ2QLqASPp?cF`t7s8iANq;Omz9pG3z3xe(Q;RHuFVBntz|H(mv2sv ziT{(hzZ&+7Op4N2DSS-nh}0~yLc#_^_(zzJV=DDXE+t16_Pa7}1#HX%Ylx#KR~YZa zeZb3ia8HBv3DvzpT^ApTpeK?5+bCUr#^1VKzINfo)_z_RlF3=88p$CO8b2*6e^ZJ- z|FsCFfqdy^_xW?ovR^>@1iu481tSk;L$-{EPE;yQ?R0J{SY{IGg@in{`GrI#b7{Zf z**)=7Hv;b)_ky-kZ;~|QMis`7s<(v`hc!W(+?U>OLpcJAh^*qbz}l{;Z!y6Hz}HdF ze+|BzGnLtiKkUnz+iyBj5A%7!s@}F|tm>J>e0=bOjW_IMi;+eNHdP=qi<<-RS854> zzdkAg_zR@r4S$)x;je)={N)PZFRTu8rF{)o5`EtyrEHtch)|F$<)F+7+|^g7S6jDk zRmDVD#vJ;Q+R`4bq}pJum&T^xPkhFj(9}3WHmTdmtTBe_`#-b>Yxaczz9qN<;4hMD z0CELs0QhUU%0h+7>O(oud{PUvu#KwUlG^O|tc^u{sV`H0}`$sWXl>-1T?Kj|+04U}t z-x!$sBv7S#4F$r5Cy^@;p4J;UwxhYmt664^-6%V2U+PG1nOe80P`-yO#HFwn&*v)Y z`F`RoQ8od&k+tiF193yioRK;?*h!l*sj~#~NS|QpJ|%com)BI8OM%5k24U`*?ISB$ zSy5-xRDNNfAng z39IMGgK@jC?r~gVhTO96i)V%1GVcz@wfYiMFXV%tvNX#9JucPaS9Ez*Uf z+r?0H!Z_iRAzMmKJqvQ0>=KxEByQMe)@E24i(HHX)NI}{vYY(@Pa&}mt8wyb9eutO zgl0y?<+RU%!G{&14x#X~7<($jq&#MfY_$G?8Z03LI8D6?*x6b?ZoewkMAPK3BCk}N zBH6$PrJH$}hjLV^MQmvHghefP>j)#0*O^voHEr?+ldR>Q`|DhVhc?7WMm!7mc#SOM z(&t(?#KcW3we>1W5Z_Ja5RjbaPNqkN6Rg@2ejq-dBoHBh@r%Jgg$V2r_qfG!>Gp^e zZ3$sBWxSH6Sencc6j=dIDgzw5eIiAl`g2;TAa}MC6(Q!f#z5t)A1MBrXRO^rq=jfwDnS|5Ua@bR= z#E3@jTYo?fps#qIwx_q1!FZ0tuw$as7_XGC>vSf3g{_t5NX`aZDMN}>AfiZ!~4qcmvHU=aOd8fnD6nA2VeY{47%3? zM*E^&JK86gIV2a}c@H&`^5bhXn3Ag&{&x97@sR$)udi54{V-ScT+`79jrwt@>C|#q z3~Bw^(!~c_W*ot5+)hBczD~z?2Vdy-Fsd!jG@_0{KQ87PPI(%Ov}U2u%keVtRGlhG z-XzK?asktozv4U`stTdc+T&UuyEF2AF0yYr!;*0DiE$q2@T_LLz+l~S0NLf7ZY!iQ zmWcT#Zsk6i@H&GJn)gz-HOwDWNhlJxw|4rTOa|p~#M0dTnn!gcWUEJ5R6GEN2H}w+ zeLLSMwpv8#$~kNo^_HPovu28f6cpU8g{cZguBrv0C_osQ%hRzM|R98Z+MM(W4Y=#!`j5%`D{t zT=QCKcDy;JRa?#BegyKH^p%>-1LGTM_Mba@Vgc%{)`~XXShMLwVzr*DrkW-5QHP_* zx$f~jrrN{vQ5d!Es|{-`jOFYyk>Wsr>s!E)4CDi z>CO?^7SN--O=@zeG-`ywWs2|5?$~h%_f^JwMZ?sN*a&GQ zRsLsJLxmwe>5MX0-$)?}atMJ{8{k8Xcu3PQ8BGUNb-Y!LX`4%a?8xGX-P zU1OL8ktREI=cWb>DB>nrUw?3TRi%pu`;{zp3--*-t-rL4ow3-S1 zdq8x{8VfNBPDfX&dYdfJK#U24urUCPCP_BPpKlY_wn=>tgD?_NFc{!? zcB_>AHc~ip*>59-^EOf}z(@(bjTFnR9AF*NBtHMk;X z2+)~BX+K4e5@v|0so4}zGkd3efmF>5%GmN|E(Idq#$uph8wg=(bS{<(EaPtZAV}~+B~t<5FbEvnvq~m> zSsGyxfX*g-9an)W^`^hij;THcK`Oq5Qf#*ynhJ0uBEZ9C`-!rBvkipA=;yjj!xa9% zn|ut(mH+>rd^FMjaq_)AGyeLq7!v<^KK%XM`16ZVlhz#&&4(E|R5wgbo`prAVTgl> zoBi`t!OEnuAVNZbFSxQ~0_NTSWcrE2AZ$ay64~Ry86p&dLEPi17pxr$_$AmnWeH?z z;#0)BFxPsfbIB2%<;?oMO~@=@LITymgp?zq)dEUnDo}3|!4I^p>GdlVEM%lOZ15G} zfI9o(Z92z+7TqexphD@9X|U zfQu`OfYLQ!ze8vkM^zLGm`IKSz+n(L0265+m`LG>KyUkR6Uh;nNPnLl3w;uW0?aW) zp(wTqNx9^Blq*yX=_LQZPG}Am2Apk3;3xY(`2z#LZ%?{F$QnDgVoo8j_~W|%d+Dws z;CYfR<)_*AS3x$Kr@BI+Qxembio+hH!2iSF1$mhegNX`XWJNjA9}fEMPBG<5zZz}X>^Yd(5^;=C6vGED++Y?^v_!UO2YPfHKRN58m=Tgvi{=riWv)WQTt4 zMr_aO<4ClcC5kRK_R8)eCzMPLDasQ>Ja=F2z%mueQ zx=DUJ@cB(rc?d`&f|gFo-j7g7 z_-ZN;%hD{R*3(_yg2^I)}%Z=UvT3PMc_XLCq9!$*?VgR*iWxWApvg>px2| zxx)1aKN5acFu@cZooTH?kyU(`Ah9VQJ5xp~S+2HN8LxQ9Qhj3m|9(|Qv+U zR$jV>OVRxS^a1kK?>X}2e%I36 zzw=q;d+HQe^R%BdM|X9Mlh_}vKPm}1!=!2W%(UtcC-Akf(L@k~^K8a%FMd|f$9A*~ zahaeY&6c`QI_Q(>e1%v(lqP!wqu}HwNqjI(HS1Hx-cmjy=1o$)gW&sp+;#5bd)3|5 zB?5BL$!n-8-|bGJzF)VRFz@2wMd-ze+CmF;laWB8#F$AUojT{o z{I1+tto0s&G>Z2us9==;qWIo<^yQwN$h|0N;~u=E3rZZCrPm}+kqVnN;!-Kh+4 zr!_Vct|zI?C!ydLhN#tfRG)tFw&RGH(JtGze8GVn|Gc2JjbX<|o$M*+~7sMycniT@&B=hl+0K}FE?*%}nXJ=`7 z^@l#hsHpR!kpD;`E1e^`v)4S)>CBEtRWzrtJ`&83s-2Y|Zfh{1mxip8uxFaAX%zYj zD@nj?3MvWfIH9lSP3q=^@JX^u!v3A~k4;>-qF$%CW1M_s|EN8HgYZENpU(gucN`uj%3pC~SGCrDT8~?gFMbf=TBK5mR#>3kf~$L-N`_bNiK5AZ3?r|Fhi z@&Zj|DB6+_L3L>;EoOv znD8dOXD{fk)TCnjcw0SA<{G-Va!gGOQ5a)Z>V(QgkMZqp6ltrfl;G4?Z6o1igW-|u z+7qZQ;)oU5q)H^Kg*f^0u1Z=f4szyc196(>(iuoJQHZ7OnCj-V*^^mLW>Re`+KMl# ztdhh5$rhH%oM7;=?5{`q{4 zJXjZX_vKU5`u6IxHR-#c$Pv+1eJKgAG4-BJF7|-i3syadcj<#BnIFRtob3&)4r?85 zqb?H0yWG)nWj|eWX>ZYGyIjR^5Q>sObovwNlW{A~c45_qq8YE< z8ljX*o)FV|wwWYuc#+z)1y|{Pp__U$Yh%d5GYaWtnAmi*iIfn`;i3dBQ8L%(Y4RZG_9HDU|dy#K^a90`{ecF=K7PbJ7qA?)oL(8QsSh! zn#gpNCyhO^{v3Dcp7^^%J{k<8ZKt7EVib5S!;dtE(scFrc!O)9gJ)T-mnCH2V<=>+oCvv@0N)MPFU55 z>mwRAF7GE=CMX^x_U09CG2h2IXx!0fm3%ih?E=$ilW%i}7&#GO8_6~SiT>Wmdx369 zFM8dX&sguQkM8!8`v&?6QKgd`j}MKdgWvw<;al`On2)S6xR}y-Z87$w>=-hQH7B!0 zK1TTMAAMg)+%5dT|6e!SxmWrT@vpxl(SDmvMWg+;dK4DiiT(kHb&QBL-p{ot323DQ z_IR>05E0$aP^t5CB}0GtpqJUBt_BxvgT5^D&Ll&-rbiv{(QOuhh3Srfk8YBob`$)Y zyBLF}%=aBI>!;WOv)%zP>p?RBv!3reVAexTZSi3x5H3-XIsnb|v#rmLqeG4*i3SCn zdb4$3F=*a>z>P#Wj6uER=j&XckQdnZ=__yuGnS`=J8n~;@>hjhD4-@Wh|*B> zcN~}ah}6l=thNY}gk|&%FxOWNBqQTC92T4KZfYOV>|r$({k?wtG|cd~?aAD9NmUgz z9Fkc*&W|v-N?4~im5X-xG_vJ*D7kddWMI~UoH9?oj7vTF8E~If((0F3g>Ovd;bL<* zQk@No8x9$S_7}y&Gt(XGe}=X|9` z1%;rbNw3Zd_J!6TNCgwhNSWJ@&%xc(quS!HWD5%m{TQrkC%KjD^EN$wtd?i}F<4CW z(|~xFSEDXCRpElsSk!*{Z~eh+#pi``-YTU>O+SC_p~yXLL;1>v6yl8o(T@rm-hG7Z@^1LLlXQ6m7my+%D9@(ii z@a6szTM+&BRUvzW-N>tOT_Wjh?8ZfwDm$|G*`W*_v{>zjn6<@rH=A`wnnTiQ#@j4N z{0dklh#QD$^lW}%%K1w`ThMaypKZYa6nO@3!wpRDJnNt=H({B958rYtts}db+$b5D zGA(*fJsIKQ-*yOQ|9UM822IfbjbCrxR+IKG8Y}9<4aiOd;ZYWOdBPxD3XTl_T{jRd zsw*Q?iqc}+H5O9>V%n(x$4%Zl&)oWauwu^6mMsgkCvz#LdYMQXW~@ z_>2B$Vh9a)sWV*pt*xKDC!1c}UF{vc8r{^^GC!fHI8Kc(H_^y+)JjT?e4B&YT+k=sgD@DHUa|BV|irc4lj`}&T@gro)-Nb6$!JB}8m zw7Mv2b)X0+eU?>N9plaqvC)S;=+1=WY1vvP>V1jb5%k*qL)Y*7XMT{NGZ_|RXO1UM zM{&e+&GK^ZjhFi+kDcthXvM6C!63oSvh3ciK|w_^jVLWDe6)LVGV+5o*_P(LciYF;Gf^(Lh$=8#1ifpzi;Oh*e4ujg@9@x&B#p{={-A$0H#^1Jx zfq{|RHW@%ljv7!WL`_3^KlUNa>gdIW!u*a8f`z@t2Nw)hQU6L0=94^t;Qli`c=Rvn z!6h8{AtFRFwO@YSAJTW+E1X@OY(Lx-Q-7if=;Fz41BrXr{^;a+d-382aW7st`&O`H z7(sS|8?I)^^*cOQmUt$Nqs_SG%~M-zPWy%Lys_NAfVWbRUX&EV(JCd|?@}N4tfAw| ze_!j18h9pUnXlwigG&DSTX#bL)}6+w} z_CDVng10n2E=IZDg6)Ju$KtYN`4@Q|F*%byXXL)$X~A@aUW3;vALIP_F?8n+k~NT z)pN`0u3{eaDXcCq|H5p(ZsOZmzKJqYL`>=<0?=}FWT1G)2P&P`PJ%u(T+r3UhY1rB zJ=!Ce*bf#TS~!mO$1x`;YJAzie}8S@3sgWPkfCCBFbLr`)P#sD!VMScA`vaPbefzE z`9zF|s0JKyL_0>l+_O*VSzNT%Fc0>iCxx39NeDNs%?6t`HgNG_ze zVW~hXOqmBWnRUh~`WiY;494u2$AH^UA(W^4KJ&>^@4dA^KYK<<@q>@tA^ZK@lNFZny5OY znZxeb)3$C6d7nfu8Jwlrm-z~O95hMV;%;bVCRGR7r&=-)vT1_M-<8B+86!YUJqcB|J20 z_-^Db_BA=^5n=a7a+!W0Kb}J6X7`2>qKlhPkOSHyoXh* zEPcF-m80n8R>k~qMF+(~!w`$7;2*{t_x@X8m)Kaow*8(|Kw61LnkdNPDW7%UH$=>2 z@@^^CvQ7kkwOIvaD`Xlflb{8XnOncnTypMW<3I)$_w$tWlZiRfG_&HzQDv3@_J%Pj zomDS_tR#K40iHTNQHkcTb58j0hQ@9uAx23FD?Yi!d}%*yot$f1+Br;u`XmdQ&m;!Z z@?$_pm|043-W@u(#lEeBl4AZ&C!V!RI?97F6eqDsypYNY8Na0CV$b(gklHx++#-oA zk?#b28=F9>JVsF>uk43h$}qBU!Gmw3P=kaljZR?orVA+gp6LWV(O6k(&D0^Q;ZD;K z@l;GhdEbmlh`EI?IMzbY@MPh$dY@udp14KeT^>1xGjYV}PFl!R=wV+A=sC56{#wvI zI!CqDtrtv&T9QRapiCJy_=W)4f~6nX%x__-{im=T+y5;rE&eVnMI?B#*Xk4Gt_&uS z-56X{C(C>Xp~6BRKXz#(8UgTC359H2geDS%xo;G80C-XG5%~&o|E;B86k|3sB>XGeKEVCWFF5@Oq?3*l!f|S&9nK%=Z7ExG( zgkixEcH>JTO?jenm8C{_l1mDE)5VmYx(s5chmWPgcXU|og&YCwnC%Z6r7%rW3MO?} z!uD%i`OKw(`$xDlB}tH6nr$ShoSt}92VGoiUlUtC5dGGv1i`;`YCcH|QJdQb1VE=g z;c;yKp;LDj0i8OS_bN_5o&(}^$Q5Wqip~ImG47gmASP!0;28iDIFfYw8?esFSvDKrug#vB(ah<>eezL&_z6w?6 z`~BA{B)$lFO4cI<;(8hEufj136O|xQ72|@VkdG2JnEi19$lps>gInisnIFG-eO%8h z(igF&;j#v`xe?tQsN9R86oVo5d+kErJb=7R+`V=or(e5}Hk8c48(abFZlA*YW!aCo zCMfwbH_AG%skp5m;q7LaTizoqVP?X7n<^9zFV-C54w*}Hzmar^N3i7Qo3{sQT-P;!FhVmbm1Fpxh21%I32 z2Eq?3UkZsRw5HL+$L%9`4jRz&p6u)%LCMJ(0;A;QOoygb^C8=ieN?|MG$naP+0A(^ zAY11}zf0W-k-vR|Ag^WQEOFv&qMRUUotvG%yl=LZtd^tx$kgq=%HL7paIHw@91r2j zz}z}@jr&}|&iVa`m=VyTU|8waS{v)0AX-1ChSVH=V~Yem0+ z%TOwDrt-QVtzYo<1FWzx624_rnGrOnF=dUu7DDqO6oDMvaTbt+OJsb_!S%l8;H+M2 zpk+V}^y9-^p_D!ykc9gt%9D!vgkBMX*=7dhhBLQ8dmndDh zAs&$FNq@<7K+l^1dcH^fRnOZlw1RRN3Ij5|{V$mg)J6He19j0UcAzc_=D-#sQUJvg z4xJI(+~(e-vBqGRQL?9YD6{m%?8q}pK+I$$*g-7p!N*R}hW~x+Du!0{F?vm|z>a!1 zQ(3KGHLNzE%%fr_YM9F29%oj?G1o23>3%v@Rx)4#qH#}13t?FP3v=1{d$gtUP4{)S z+y{fVqi(RGEOu(b*hEQP9@gW`$84t-b10O%Cs@bB_PnQ2QZ%o zpk&c{Ig-hAnq_lWDjQI|uUjN1R2=rmEsS$2L>}?7%-PNgrXC{!6g^E!16zuNp$tmO z-2N%Q?La?QLV~zEp1(s-Ajl5@98hKn8TL3=>yv_N8}W%a7o)9()NO_QX%kcL zM>#(I1K27r^?fef+i83=B(nILTO3LwY>WHLO1aSh|MwhSo)V8jN3BzAUL_vCic_c1 z@pDqE@PBO2Yu)Ye=sZn)bL%jA0+GMTML;S&s5}r@Hw~wJdSGu3g87$}E2nr=)yErMVXNhX(Ha5T|mX)&)gX?+$nb+bBf zk9woua`4E0j@Y9!vuN}`me_IY)DIZN2O~20!FfG zg^)U=IasL!`5&~qyiF%YJ0xCp_^Wp3<`6;bk>7?e#pS_;)2jrxImvE(lz7SsdsXlB zKnHDSur*s!I88D zW!n0m3{MXEh7E%B-gSb+SW$YDmm{^iGi(9t6MPs)B3e&NSqkw1U2wT8d3ZWqBw{_* z8*#z8vtG>jg$tPDqoYA2S1=GZ@pB!rRM)Qb4-66`)FO3$*ZoO#!x*y?{O6a}Y+Z5G z!-u-yoT9@cqPLb~=+|4iGAHkI2a>5>sOXQg-=o*su8@${rmDDn|D?~AY}$LjLGr*Q zKa8kGtCX_nW~v|IWavIEz0b}O$|Hg^Erc!GchJ1Ew1zMO19C93roRV$OF1mdL`|hs zVjk8bYx5bQ=D2akX3?!N)W($Q`uk#_D23pVAuu1`^C9NTX7W-7;r@p8>KPCO+){&w zN-_G9hY);(Qw}*tn#9DR9xW-YSx5N-i+x>-kZExv+Q{EWn{uhiY43aw30bQY6n<1$+M32#SJPLfo79=LR;w*UbBbxy9L>2SEZQ(V z?U#0O^ETxw8288t%E`9_vLn;^4NI17tn|gZ=^hzb0-Q9L*O52BLjgDMOR+$fDfL&7 zqlrqZrui(cRdBTl zZmR$m38|1=c1jMU?ElU)z|r^@&%kKxFVDcu%@>uC;A$yqI8EU7lff3+9Z$v#vq3a6_O11U55`B3 z-olaG)qgB*5@qM--|(0jr`A!O6e^%}#MKY=nI*>jY~5bCTc_~XH2a5fV75S@mh^+(-mEG4CjZ0-TS@n-0ws)x{>08;-?YQ$CK0m_%<-eFf^k8Y3 zH{WtM84Lc211d%M8xClincRjC&zG(H2?G{>3uhzmY_v8UCZ}lf(C!2H9q3+0vC{ux zV67-7#PwP^L@;Q^USX6|vwUkrbCWcPxj%H1%`+8fpdgkOo;=4Wtx{CJbJh7u7Ctnj&S-Xxu~hs^VQIYJzAP+!e3 z*1|FLuhwDA@77_+U#-I>-(>usQ1kvT!M9!CZ6RvC>i^iy5~$p|{)g zCT_@9N}Z^#VBTN#5IgC}{=mh5tfA$hgE4qaeM=DjE-}&7pLTXjpMLJ0e@ay!(PCW{ zkBG1K!N#s7geaYs-gzUK|32n$C3nUPB26HFcln=5#VwFaTXoEl8aCb6>2nY0J4hU0 z`V0y6d-{xH^E!R@1F^dhc%426_x;QC8DbbV<%a^u`@z^s`aMs02TbfA@YInW8Z(;F zc2v8r7EOGRC+k|LX-;Nn`DVyzj!Ytq6%Z4~@6t2i(?cUi98gOyBK(Y8jv51v(%Ts+ zhvA0KXwFp|O&Y(SAQ5Je$5Re|w0Qh}bd>&52*OW0UB3DybW?^sipd@XauJ?O72(8N z2=p+!hw%8+tx6P<-tf}0M80lt8Mcz_8Ni3fG#iWzzomf4U!~Ye$oH~!>)@F1T{!gs zairH$1RxJ5Bred7v~-r4DN#eZ?eSZ&tg=E>1|t+O#UUUV$P7_-L628p4{F>UXOTXG z&)aU$Whk8#rO(%t0y3nEat)zvj4TS*wr`K+dzydGpXU>P&!1;%ZkV+8!Bx2-2Tc_k z(}@%m&4W+nzMMjB5TG>>nU%8W4GvH(CYOKKAX|jWD4c&CK?m^v??%wWM*m?1-OT^j z2wKRwkY!m)aQst6&4;mSBjcx)yEl(zsJ#q#IdGf`Bg!5dw*RLA^tj_Q=Q?rViEKr+ z`kvfS`%WZNwyH2l2h~}w2?34z(=Pg#dA+}uQ@XMw*V6d)r-w(u$3ItS%=y}xlP^cH?d z^PsOK@bT3{P<{kFgtKz4SQ#4{6s{8ic}g}eyUeN&*Gt%1MGmzEgu=eAHCt0xmXkzmZHO5mr%`)R|57oEgL8vifQOcp{=k;g zCMNz&e%PgJ;6iINh=tlQIn}RYHcwjRcVDW(4CqVMTpiO7(>i`ROJ#?Aio`9@f`@fv zTJNF8`Iycvl*g#|%`)KHS6E~@iy78#DNpiyjC!(JkCdkef~Ox(z`S)dq+DR6^%Rb0 zY`~FdRtT1^p5lhu1Bo7Fu4D?uE$_MW z44=jtGrUCMgUHR^jWzG>RL}{t))oDZfZ8;o+br>5nJe+E0H9t9g)vqDD9|cT|ftflxhopo)l zvfcfW15&-9hh*Mpm#xf&GMBA&6;-Yu^-x<)dU(w#sAHUfmiwb0^_vj{Xt^;?K+C;i z#d|)x1k?^WDX<*_v)MoUQLl_3U^dHk{92@%aAV~QE-ME5QAvOGqkc1jRO-Mu0Kv%b z0}nYgF159UaX3W8$QMZUyV0imR_OPxupPO5)@hoNng}EH^xG*CQ&;Ra5cdbH^ z<77S)t8COsVmUH=S*oQ2)6lFHATakWXs!0kuEg?HT$p^1^dj+t^v)eQ|a6yTtDS= zDnc%0E(;fnv}!?iI+QLlOrd0tB}?5xUu4#M_jdR>ZC(Q$>$8etEtWbQbE%_&N?v{p zTFFm!b3F}n;?pvq2(+q>pL4g{=N}EsXUC2JQOCvk2Q+)C{Uk1a&f8V-rwZOH5DYoiED zh*S*=!UwS7Hx9x_PwxW;53v5rO!iAitd?p@)tY8kXcHdTW}fqh#s5V!Xhj$C|3)+D z3^a;B=armKmQYY6Y^9wy=hsdJjHM)ZeD?-1U#CLEEwmN}dB zLwQgvclqM{=i7Sq@(z~L`NjSEr5VF-t!BL%FVffx_Evdr z>y}ETqh>oAo-%23M-D6b+LdVw$Bsq*uxy85F`%5K_0hxV%iw*!m~cQk|{cpG5>+UL`Y{rX*?^TD#Ubz3w4zLj=;>{zeiDGfre`;TR@)IOLtE_o_C zDEzQ$UGk-kp#*Qs+`rCm2qC_40<6+CaKtw;E%>jkmH9BD@)OoU^6z~dY=Z%?=j<8&V&#Gq62I|mg15chJ z))M@DgpzvpWgPJfFBB&s`R^1I(wnQ- zEpwMVQK`puoWlNM-$2^FK^=<5_tAXbG#SesnZC|~+Yrz*oc*DSvmgvEZdNStV^?ut z)0DEbC4T)8n-+TRUWxKQ8SR z61U}&pclnKx3wD{maI>Av&O~`IPBFxf7Uq!cii?k(G`qRS95o-5HSsZ18g?!>M+u&2O?+@Z=7irIdJ=m(hdjTO zY%3Q2_={LO9A3mFl%oER;dMgzzYnidiXEI4|2Df;U--xDnwR#d?a1`I#*h^J5@ybz zbdhw}UmDcD`%m)C7p_seL(IsFB-tNyFX~1qZ6q3KNgmcMD8xi%0_KfhVm70wLy(qk zlF5$P<6g|0>H~b`W)+X$m(woI<>H+s$8Ai?=ZiFo_JF#AZ-XD}(6I&Bb7W0V($L&I zcUs~9+~j)UPlEYSg+MO*R48EG0i62l(W%?*8F%>3gzX?6Kl8sZY4cbk*5FS2waXaL zXl_Jv-vd&=3EF5R_6;ZGX{%C>cgg>tXsuX z@f-&7T)YLS9T|TH>v+mfA$R|DVXqH@nUI@Dz$BaWS3^sS(c0UgxkObp-Z;{Hv||rs zVakpHGO$o|%6^VhhqA>gmDhPy*=!VQIfqgKz{#ngkg{H^V^*%lDi|94owMFm3&Xi( zGj=dUU@OY`%40~d32<`mee6^5kDklBnC#k`KDl!oCo?7RgPLLf2IX8H;tj&SzX#D$ zT;g$chWI`Ep7rdg6Trr3IL2kWkgS$vc0z;&RzXkpb@%=Ft@ zj!c6u(ll2xkZuuhJdc4QnT;rby1kkea^2Oo?+w({`?ddhvrcxg>mGvYIcV!#FScK^ z_t`jaBXaB6w*27!1n%->RislE?tHeJ5@dY)iejwD=5;)7`^R{k={?_ zEvIY8a#>|8jb5#!aC}7R?jy%B+y4Szf$LI%xAZF{cb7jLacoik(ZEU zY|A_m-GCA__JbwEE=G;9Dh*JLI~+ZXJZ7WPXuJlU>$j2SN4q*6?vgBa_cgm7>kxW- zoB!uCXJ19Hz39J+Uh}`_uKnu2|AoWV(*JiHE~Pkt!=2Ann)(xmd(}+(Tl8|5DEuvV zt@q_`x$8@of8?&`=Kn2sEu#bEu3goE-1TzS@7%RQxoaN2Dd=nNntSNa-1XkkYwp@g zmWj){h4&vcT-Ko$sPhrIdI6b_kw2~kjYuU?Kkh)TBde=%H*--9`0YTbeNjvvjG(Li zJR(rk>E*XW7>_BkB8egiLsU@>fea8I*?7~=Ops)|qe!cHDKVi9zT2hqa12hU@U+X6 zv9<#P@vB*I>mron*wI(*J2VmM;CCIg$u95*4flfZHw_o$eLI32_ld+u84fJT;P^i< zA!k*u($?Tr+K#@0aI=7M@0rXiHKZNTz)#OWfIIf*1Ax~B{&-(NBH8Gb>UB{re?^8g z1IUp0$c2>ULXBj#3b}{}D%57WNlWrF$m(1HJ9gBY;A-+R&vU*G49X%RCUp1vMnkXt za9x-GDXI(z-F5p8Rpd33r#6VLX?$Po&M(3h1k9ty5Zvz~Y3}kjg4LAw$>S9TSE0)(RNZNq6dtFm5czFAE@OT#SJX!66U~SLsbjNg%V{YMhLL$O4Mu^S z(~;e@fCBXWTCP&rQ0t)cuNUV~7>U|F#eFEf@4KAqt5OQ=Dxn9v*zs|^fu&z&2JB_U z+8tPsf7$|R1_qvonWZMhrF`qya%b0a9JwCF#kLR@(OzT>@Zwf^am!0{?Vn1=7A{^j zT1OTx`=;v~%NKr09vIHNj@MRmTq#!=E!d7shhF7<)}k2>*F2Qdhe!GM;w&-!--O`p z*I@Tauac@Sb}lydOH|8-;(TQ;Rs7oj@d^JqSzNi? zw!C2M02Sl9X7a1jJl298>Z8)Me!&vtr;^X}cBs_#@pJ8Ms6T@F%JK3vpqZYYmrwhD zX@BDSaXGLk$YS%{TG&Ni>s9LvBLai*)&ra=Oj`pTJEf@gtm#Z%;57_!aE31|>_{w! z3vD4jcyA+-#~92+&QU_-dnBd{OtnS9=3~1iK0e}+VmlHe3g*MBe9VQ3bp>*{`xUDy1jm7RbdOPNhR*hn_oO^y-Py5<+MP1mmIjj*&WzgftB}uLBOPzRmfyTRM15KcM6Or?;qP7Di;zY^q2375@|^L{#UMUEJD9tup>mb|G|%w`fvDgmv^<~_^PHtv+C6-P0^)^m4Z%SB{tlV z6Dvad?G#NT;-iTX(N%D-yPCXA0bLrGYJQ(+uu(qQHUnAH89Q50gk66ZN9LcFt_vLR zt{!Jx!)FvHi!r;%<&r{2yU5H)vTR6Ae_A+y-5o-dLwe?77vhvHPY3;+zeT`Q&wfY1 zu#(AJ|0M!e7Lx&|N;-2J7Nwl6>3pP2Zcuxl>D$IRv5)on$}Ry;FOiyYcCi5;3X9Vw84sJdO#`3WaiKj=Olf({6Q`FD@RmzsUaRPt(J_^y(> zB{#b@%9xb!KTFd><4dOY#uMPlVtE8tnvf;4KhV@bD442ok}^1x)w;f-;$Vv^S!ZO( zb&gkoAU8k*9Y?VyXH<{a#X&Ou#dODyj+4ur?9lh-FHjpBi!-J2$CIk7=$IpsaZ{!Q zJj?NjMkf&*Z{h$ogs-Mz37U~1LrA5m@<{r^$3h|{QrHnuL)H5l5-RGfm?U3o5+lC+ z4c8W(uEahcor%c)PDF0V5j7(Bf-dfJPf+ehp@teHG`m2OJPO00wpcWP)|N+U_^FSG zN~pXqkLqJ-5D~Qy++HqFIhGdHih5v0aSUC0C#(&?+NP88D8v9OTq{494hh9qOsbhn zCm@WL(EptskyX>wwhklpvpgZFa}8+g`!q2>#M#i7!u~@?{Z27kw~QqOiF9qUJ9osQ z#n6}Z-q+g$Hpm%?p3}$R*K+C1(K@C$6P3G)~}MR|>0Mr=dj_iEt&awd32Xp2F5m&h8u z_G5@os9r})E5QCU}QPZ<2!yzrw zTOh+b8`>3|=rwXRPk4n6JFaAIAY*-{=75cjT{%Gf3h93u*V(CX8ZD`pep2IY@AS*YBX6bV*WV5~ zdTrxt=`a8KKF>@AjDIS0!g#5l8?dugn|-hoJ0Rh=XEdJ)e2gC$9_KWPu!Ts~(}EEp ztll21OuXiB3?lz)4hKrcE>QoUr29--&HqBWAI~u9*I*OD^gb4a?dZCb9c?%D=f4rv z=o`+r;2xA%e!J@VgK~~xOE(DKCQE@Q_KAimDvM%+w}iB=U$mI?1hUhGIq9WoGO2^H zG1EGQhYz*=K(LSEiLxb)WkoBD$4kblTL5At0?r$O1)Fml^p^;$BI#s&w=8?oZjk{D z#wj=q+Ao@S!dbJwL?a{|g*P-Bve1_>^>T&O7c(rE4_1b?Z!D?~n{&?D;X@8{MGM;n zp}fU6wj*(^wOq`btY+wJbKX7HS_g}*>W6+2k!%u89~pyD_(UtE<@t{c&Ov_@z;Fr; z{O=h~S`6+gs)3_TN|QhUX3`&5r990`+PXboiI~F5&mzngmU0QZ8r75%5#vfr%dLlj zCVh$ZAO{`h!WCBXW>wcG!;TOmD=4lL_AMh@)VADrU7oaY8x%Y(YTZZbXQPBDuV0|r z^xrmP?oI~LNX_0ILO-Wrg$vl^0G*i-r2gV z8Xc^S(SzC8pyeo2p8*L3Zi@{Vu0cwJEt0E;)&t+Y5NZ5p#m_N_O_x*w`}I9@He1+f znbVRU=I5?3+y=BAYwK$_Kci88c3w|8dzH@FbES@r%KMY}K?LR}=euNSk*1*L`f2Rt zY!}==u%;qGGY-^vD|MqN@l-fifG{0}C}VrP9}h->$L{hAAE5499DGaS&DG~rHByF( z@Frk>6Ff)?L|L@4_B$Qp6DAse$VU;Scy*5S(YK>1WMQa@vC1Aw571zh1g&IT@FEq? zX{HI;{qRKXi(E5rdWhUD-%tjJgK9AJP2y*>`n=LK@U(Ov(80p^kuD5?{*+_Kx)poJA5L+0y=hJ(aAxyS8-okIc~7 zpkBttdh(O6G#PHt_AVYh4$jAh-_~QUFzk@T+|9k-1 zL(F*W?zU^en?rF(j|hAjm%7ECyiedgil`NxCS1nVZOs67o4d?v5=xetHYX*P^5?IU z_Eo!`ra1B^$}q1bYh`3dRSRj^CQH@2`z4jg)1wq^TG-TFVkgD?uzZih`xhj#iSx0? z$~QjuFcrk)EBd@jn#eIJVj!VoLWGRqP(NSxRE-XFfL~$Vv{Y*jCeaIvm5aR5sq^j( zce-02-cKtAag!JLBp5VtL!OcOgR%3j6D!u6`Hvr$4*I_{7uw`Dn`)v z&M!gsa=|W>S#m9z#R2g_6#F=8>_(?AHS?XTSU$SiD1H6TC2%f{KYvLON6A2xI-%zk zmUvA#qsdoUj4QqGi~1uqp%O)hh%zsdbq^&S$FP+-y=_egs`rOg`O*SRpq@UrQ z)GN78Up)tzuPK)Noti?IO`&6UWK|==9$j;B@(>)fS%_pqSPh%{e5Zs6{@} z?EAs_z}>Q<5t;=7z;%?xA!DF9Xm=&iswQhqC(;#W*Y66VBDd|?fm}(~?Z1_Jq+O^v zjpp^*DM8EH6U5Q6@b5o-TxrmH$*-|3YIYxFadfP-RCdGd{U$V!cNeN!qBprgi+84S z&zkf_uTX=5TajEYL0fw%9(s}U)69sQSAnz69nt5Hz2}Tk-mbRyFL(QggJ|OwqX?k2 z6@r7To&;ui@B0t_9EK z?`tWCcw;8Jjlsvie%N|?Kbmm&psZ1&P_1$$_moJ16z@8F2xHp2AR&Fs5sL3<`{fdo z+}e`Yj5p3G2QM-_)0IzR_(5u0ua8D!`kMGF&EcWOcnhmz&xN&AoQqG52wXsTX@~&oPAk{CZKAvx@~>l(So2MOL3GjE6jyk7Cz; z%bUfs%oX{e2i;A6*fmOIo=(J*OK}*SQuh7ce?PXD>Ip){wl;xqgQFp=I)N~jvmuUh z12t_MdAmqMUqh?nEDIRzH0~iaxH`ec`-q@I{%T=_mGope*<>dB;1h~Ee2vZ}YC|4d z=c@m1@!)8ME#^#SgcDNlyu|A!W4sVKBul?NUJ^U%OuX%SUA?rMi_T#Xg0-Kn%aE-> z@0oO1Q>cx3rSyXP!>rV{E;v8hylz0$(r3Q7B=jChMqdz}{whcOgd36P@eSXMo+bfQ z21JQ2y%bofH(|fZx?*-fMf?OC@OlW8itP|b;t&vjOGUD zofG8K`o1ZPSR&i2zOPrG18xS%XNeolX%~WX-BXlmfboQ9x);m?FZIg8lX8g-O5;Rd z2u|kOfcJjZj!))ft`r~3NL{2lk{LJ0yj3r8&B#=i(Bq#ZRLVQKDFarysQ|0EOMz9H z^T4Xf&%ml9b$pTO4_|hVP&0rPVKTr9;UZu~?kuok+6?nHg`5L6Rir}Pb;3PJ-?iY> zbW5f{Z{-#ZRcVO{7w1C-|4>mnVG!Alpqh4KVh|%^m{}Ne-Ms{41w|wE$AK~Ba983; z*i417t73xA)N$&_ZayqruE6AkZmHj2gTt5@hSWjqPULo{`CSjAk$i6ByNNhNP+T1@ zhNAQ`S_?=VTcG7hzs7psxV1M;qnvq7=-zyayt~HrRLy(w5Z#zJm1REqT-R7s;#^re z{WCOZdEPX*F4Hmcj=x02y-<|$ox@r}-1<+`D*_u8rj{k{s&$DuvZ{3rOU;xT*er*?7mR{9 z2%DFpl}pp7w|_qEpHJZHR=2nlFD%LJsh2`&ptK0qalH=<6nmVEvSS-2Ih$T^E`Ew_ z-=pzM@)wwB5Z@A!f>cB+WQtj)gUKhI=nD+-Rqk}UYuIuVwV1=l z2EPGJFCRuYyjvdOH#7ob`H*b0S~}b(Y*Defwyn09=14o~P@#o6rwECM_>a-g47xGR zqP+{vkMUHhy)zWHjZsIb7u!8`8ASd+zO&R{ddB9-&wTd&2!jc7{OP7|`ZbJ^n;D~$-nfw3JC zGfiP#gw(A~9%T5?!=rLLT%L z70iokf+59y9RlUtZ8!<0(iQE|D)*1Bx2<5&R^F8nK=L>)b>obr-iPnqVM5%ZW*!$kA6yzs=B277l)gdu*_Z#bV!v_ft4;LbTmxz zXL8iKZ6AF)$nR*sDnccsJ!TNIN_M+-Czhz_91U9V8nH>XW;Hk;NY%uUl&rkd7}9rq zzxooOGdn6SuS@9GP7a@A0QmLiG|7@Ip4rr$D~8E9r7GUp^)W%QK#f{A-fU1z`3XDj zp)r6SBtDEBHa{4Z%KQy))MdfOds_v@r`Ubg_|kgQY^x*Tg91dL!tSGyYQk57oT7+N zm*^oqN1^?C@~}~?2GTBp>flOtB_ae$c2~0u=>zCFH}Wj5qP81ZO&16<=?oZ>x9aSvO~XUJt;B8A|GUhk!skj2AP6=fBT(N08D-T zGUf7;5O09rzzrl+p>_|y6*+Y$b0BVh__LxMMx>$b60mP?R|C6wzY(V?arh*#nWGm0 zTRfHa>t0^z0^B6SwT6uR60l4C;fg4o>5JC>?+fO>Cz|pPHOxiQ9rO*KO5Rr^O1T;& zT9DUi^%cje!9tX0&{-?21oLe&>>!K925}%f zir~~eMaz_7ia1Whg+xAb1RX^uRt&LD#l?&iK}`Gyqis?Nx4f7A^DY@ZtqTDq8(0FI}xTGE*fHd z&>cH7E)JjYUXjy2kF@(s7CGx@1-O>>yzR*-4l@%T%JrR+X^%K&A^Jt6UM*(7=BS4~ z1@B@TjnV&)wYLn4tLwgf8+UhtySqCCcXtTx1b3IB4ISe zima8TPIyd*Ca(??wUTFV-h&y=;@6E#%Z6Mvfni?p;x{IbSQ?qQ86tjT0Kyw&W$ zV}T&+Xaak+$yijnvdIKRef_{!s3dLn87qj! z*)4|in%9$`$49@=N>1u9w6rLpZ1*QhERCt6rDWZZP`sEM(FKWu;9 z#|RnyBCGX6r3xR4xs)zok70vmb5#KCpbW6huk%7(;fJxo2mVy8J<0}8c0MN1a!~+n zcxk5$@Xpm4SUQ&?q6q$L*J(C5=JVV}LJtcSuprAKD1bT~a4Q|VDS#@K0QadZpGz44 z@6>4gi|i6U2fP@2>XjmvQ-MnCn~7&V`Ll8*8D`mU4gI9Co@6T<37xIH2S~4Re1)ta z5%m)4^{^v@h=!417Wf7ss~y?rF;=x1yAvLXW^KGXzOpC2ef5*b5=+ajMKpo34AqDd z?z4pU)Z5~G%ZFd4RX^sx)Mrji6)9B~-E{K^B#S^1Tv6uC9H?XD;X7su`}Fnoe) zLXBTozZyvb2J)B$myJb}%OxeJvz31TZom=aLNT4HK0Tyc)6fBG-_<$zZG|(wa?^+E7vCtF?%H}HWlH4Jt z>v#F?5yiMvC`9dQWRGMqlnbVzDFHZ9Fc>Vy6HdhN{@SdG;w|8OX9ktmWLg+j#nO3= zoW;8Q2Ok+Z2To2eUTH=_Re!4VXC|piNyuo}HE6;CEydhozwpF_2$e`D;qrgC0nhhk{3u@1aWiKgtT%+PvaV0IugMDOq>K&;VeFJ;H5_u zsA>wb3;cpEy#LJ;Z|bUNK?pe#H@PgBg}iq4AedXtub5qeL+~_iDz#n^i5SCY_}Kc8 zx#WDhxo8XhHf@0}?@;cvREw0!$*of=gntO2xFZjjqkblrF_`AB2llK%BwC&;yE9E9 z4gs^Fe45@OR4J`Mr~Spqp|<4n5J9=_XoeG>etj(Po1Ai2R z>(A$Vq#t5XrVX`k5K&|o88d-+igD*SGXwDH6?@;r zuJs9q&6Rto18*1j^crX0+{d>2>*?hfmEwF{&*8>B1blko?f!XsF${B2cvt+{b>SE_ z?1pMWq!;Y~pUN=oe(vTO?T~pTlK2SxWT9-Fd1|5Q&ql_xm3?f%e!ShX6@#DXyP|q= zchNB|m-bWM5fH?hnKn3y$Kl!ZV z4s+a9-wjp-ff;%u>tC{9Xlir(^M~1%&eLqL`bXz!;y8)G0$m(pj?2{{q`GR>8?JI= zx--BX4(tPP`@9#2_hJexJX(b$gE$z=wgs(fgoJoMZiOucn7fPIx1B zax6u!kQLA9L3AR`%<11WH$WSLk2|PhI-MlVy#)c2foUe25UQBTI5hs>LWPO4ueDel9=E#3(k7u9Ykb?@%ucL$`@ zXlD~-VIKhkaGvK$>WrS}i|QEs5A0Qa%oQFFmd6X7KmRYigua`y`G)l2^&>4gqx0KJ ziua~MLn*MSV7OpjX6}P62Blz8a>y3brsAAFuujbb+=ZTHrUYy(^p{SH&Kl0JIm$^F zr2hCGcM@8XvB*!pM7NmGfax|HjC4z})2zF2^@pqd;1Z{DFn=l6vu8(E6l?qX`IU-Rp zd$tQmRD&2ECiYTSmb}3a1}me}x0WOqT*T&hmNqqt@ew~!DesffX`xr{TgX;l^7zr^F&Q{sLfX;*@8LS!)^+5DSP$QLPZ{w5J#5s#p z{As-rTJdq^)jaRt_AW9Sh`V%p$|oET^)3H6Fsf)!-lZXhLu_qejidI#{e#-C$vBflXyHgz>|V1rIKGT8rGS>w7T1 z`?TC5D}iW5ApgNBw3BUT4VfH(`ce2h;lq=Y4ede`VRT9Y>oa*waYKUvyx(f!sz)XE z>zYhi!i$Pih#5^X{3%~DJn*DfoD~Ca%uM1EB1TdhUrg^j97M6cp#x%LiTVZ z#ugO%akZnDbl4xe05r-RY1vTb4@q0o?P&(igtgUNHcXhCYK^@^=JoKue;sIDe%S(5 zNxVTm0;NcGBzSD}hjFDyhWv)$N&w6L4Z@Y-9NzbXjV}RIvp)g{7u?Y0cZ0D@zT_x> z6b^nQ1qJiqBHU|SLpmBqG190Dqt=ygRTJk?Og8fo;wv6ZAzP5VNZ%8;KzY%o`M-fQ zV`o7aKhW160h93kcK%tThLd*wU#I#b zIyHbW^Lrt_coDvqRq4ZTgjxlf7Mc$R^EaAI&yXvZe9i!0uv6JncwaDC?gW+aEzl3y zp(&xjTOxHsV;e-W&U$xLgtijbfDGlIsIvD6P&d$i|I~1^k_~#l>|ya06wBbCF6jZG zmUUGc_@d>Yrk}Rnkf8E#g!>9#gTQhtS34v zsk=@XNkk#ti^aU5cG{TJXKR6-V7@s^L?QSJl7vYVa2ODG?$ z;{YW;Su&q(=q>h@BvP8`leqVjdUl8LI;zG>E4g}qs`>CpVUT&zSYgmXW`Y#dbVylJ zbTXI!6RW}^12^z?xtr86S?oG?v zYyFZp{r7ihU}I^?`r@I}L*}jVG^6>TW4O!kDeVFQPUOf|w;#;r0TwY2VGqP;R9x_J zriB^12dO6Hlk;%ED`WIxLubFX-iPJ}PTNU0-Mzib=9(P0Xl!FHelBu^${C+g?vz9hp_MV+VJpra@=B2e2zAX*th^Rpv~Hpb zNnr!QZknT!%|hEez*6NppfvYvq9%uCe30VQ1i~$u!(mxVe0ls$En8iuZ@vbFW@dK1 z%p#SL#im`&-gX=X@$a}Z(bz)nR`x!E3o_QjUZNs4xXtt^d@=)!Y(ZkrpaVci?zVi> zVm=}V)jlzOiyd$8_o{>4Na}4=_RH;4JL)zlB(QR};GmhFw&3`}488H(=;DhBOGiFC zsqo>aIq-PH`Cg;+^qki+;vwt9E$JVV#ZHf~=@TLpeSkBeuwh#J5hj?y*D-fpgdTgo zQb+@~bagN$`NBeM|Iu0!w37Y^*-V7-G)*Ax5K;{^F*FjoBAL8~Bu zx3?~{lDvDuvI^u2X%A%S8wmgk?eIr9LZCoCVJ;7T>G!K>9K?DvUs!KR$+7WRH|RoN zP|vnsm8&NjPG=XOF&k#z#Ka3xnZDn#Lt`M6K%L?{8RMP|q+jNW!O+R{usn!T0OCRq z0Z;RKAHBqvKR$uFLhFE`I{OntI-AmC1cy-d@ZpT{ z0YS6LQgUr}tGDKYU18aic4JvzcJkb3Ia)nD$J)awCchw0r*z<@@%e1$Dt#w&Vx3~3 z7vAek?n|dqUdsfno9(vOtLO{&L6*m-qc&etk|r5hbDUXmGUVm&+nxX$l=sF z&u8&#fBL5X#%HB|F}f1;CDcSz0p~t9^V;)AMmE^51mS>8Lc8nI@y6btcZ<8idb~9p zQ`;Ha*SVcFxRO|?(C`R(P=PQWe1kgU9!qpC#%nWvdxW-?Ej;sr-QdfrBOghcs`CaH zfM;0LfZcuq1-xlGtWws*k*^T$(a{u=kM-6JCYqvcUnJOkprc8aXV&Q{%MCmmo};|8 zK?~Px)}QFW`HeryhADGM$t;J4O5AWDH@G;Wvjf9(bjfRJn^^{Y=vS?>+Aiq8% z9+|i<$;D^Zh?gcZQ84y+yCOna|Ksc%YW;EcTYbmPxx2ZI0I{_-fxek^2!?m=iRv1Sf5u-J^6x3#7iMs=`7|PKKWgjRvGL)iId|qwS z)<*Nwdrb1FZsgGp*#b!XdDuXrA<{ojRKw(|S_N7GHVcM*4=e3 zy61~1C&DB;X1!UaycJ?%U4y2G$k^YnYax_>S%!SZ7{mzgUtu}IyKVF{?68;0QD?8{ zb>g;3uu`wttrys+jNf&1mEJflj2=g$GWYIaSsTm>DpnaE^U6`DH`dK^kTLsmy8Y?tieSdHQokv zD5Hia`*VG!vnC*;3p&cgQ_&nn{iiTMge&(4n=!iNG$vg? z&(6-gx`QYt3sO@PrzFdUp>lSM!WukmA?NACVh9qm$JsZDIay+F{wmn`VN$ zl1(h~JZOG6u-a7aRUyGexPR{biUf{(e3>L)B+s^- zM1e>$m!cs{x3v37Tu7p<#g8xp9YtksAeECwIWCY8PmVqth0kuS%HrBpD=voCq74w; z$l!=%1nGc&U=#{d70yHV&{|b9rI9>LGScXTFjDh0nx#Px+MU!y+bqWbNy;ooflkuh z$c2xPe1Jb+i(vGGv^I7cn4V#f^IO2q1t%4*0NMZeD4pE#D$Qq-&S=NAM%*Ne>CBrW9{uBs#k4qP z3EwAyfd-W4{D63I1W|;VFbf9XZ{-dXUz}GD*tKBD2-u3B=}M!;1+b9vxEY*~R);md zCb`T?-oKMF^+>fqo6RQ>Xs(@-+CY6Z$uR_)Yfs2PpRH|Ejn_ig8-Po_*rp@0C&6V& z^`-02!kUV_S4YIeT0XLe5*^+nM2+aH4Hjx}~l~P+lgyFvL(BCdd%Qu_HBM#1FTsAjzl? zh&$QPp!ZCwin`BL?Hae9@mHv$Xhn&^@=Tf-5&|rRu5vK29r=13!D8uE1$mMPmIHG9 z;q$7#??8?PIIzd(GJb2D4nRf)yK?$g`x=nNC)2 z)9_>92pNdUM6m(67>*R#OfEEO3i(Vhd0_b{2P{Fdz|t>6Azuk5Lo=r$Nm3ak1-y=s zRDwy;%t02-?&icOEW4o>(9DPJ_~fnCgSyg90#A>wYlom9Tx3*@z%AV*n*g!EoFH2c z*PS4{2m}_&YG9eJ1Qxdn-HAe4-nLXJEC)yCgzDuzURYiIml->fpd665;rj{X zZBU&`ezJ$RsobQCZIpQ+6bQ($*UUFaQ_~%4xeK^>s^9z*ydnDu1aB~1O3c}#9_j8X zkqQofz?`9H0UK}s2l&Pk9nCw%>E+V7p}k~)XaDKZn!qC@0xx{@#EbqyL_D>jP%+-1pWH zC~ISR-y>ThBxD(m$JtQrEr5XJMklVX5-rF}K>mNK!^8?UZ#?Wk{z?QmVSdjLIGM@+ zK^ijo;6EDt|1S+iYN}^d6mG9%9?n5HMnIR=gX%WYp&=hWbhp9Fg!wDLBwm(~gwA_| zex36xiR+u5eM8Be(A;jZp8tw#!!AfhB6G5-n{g>@I^BpQL5RuafU7)7 zsMNmq|3wkvs{^EY@)y=?h0KDPcqb-5^w_NN_mG$y+i--OdxceAOUXt`skZUm&4bX% z^f!5B+u;G=O&?|K{BHZHVNA@t*L3a0QQw6PF9_*pCa5ypm^(f2zZpU>uLi{xtoeoE({cp^Z_^s}sS7Bnal z0i^I4WGXu6yXx{U=-2zGRCtnS;g8?9Sb=+&eXo54d9W`&z=|%*7Z$-s( zlyz`41*izPhN%5Txc?Zc3Wo|vx#>8&Sil-y7(daXds=|ePNv<3n6TU;ZSE2F5C@GWsU(;Pu{5H`LX^grCs{4rBUJwU;8On1 zPppH}rVZ93fnvH0=wV}N+gRlU!c1726<<0Z78ip}O)usPNBDs1*ZgDN<|2XuBYG#1 zqca;0g6=VZ5?#Px`3~(_d^-W&NmgoiccNS}hz)*dTMmoVbcE@-+%uCUOt%QR{|{># zSk1}=>aD+JRB9%)+`um}+?{xOI8sQ~1vQI(Sm+&h1X^&Fr(H)a$DjVDSk}aV>N3h` zv{kIz%}&(nl?1&hl-hiE##&-HX}p666?mv)JY#CMO;8(?PMf>x;0l{TbHW7B@Dsf{ z5zMz)B}7Vs{%wmBUe(V2O~(}TQDI_pmjZjz;-IoHN!$^eR0w4>^Bq`^TGZBt7KCY) zmTD&x+Rn`+(*8;3jR3xq zR{>n{_H&&5_t$<=aAsT0bmP?vPmzm)a1J&!e@3zYfz#}inu zAf?bW8K?WH1}bK+{OcWgk zo{dWGjVL<}@s8G;DQ9ghkBfOL)2NDs1}4_SOZ%7B_HV7-PyOGGsi@ z!Qu5fIa^SWry{TGtLt1{8s(e_aek7Nat zJcP;Jx#H>|Lxv1n67}g=#U@CIWbl6CX!sw6!wSoP2!}G%fF1@E0<7}0vfj%!X|RZ1 zyi5)6Ox~pQheF~IhJ%0O*~Ur!#k0Ly4{zfk$s&N)m1>p2LkBxf{EKFj;G0j+YvhO~ zFZ-cT+?5s@8_{Ka$0kI|l%MIBT9_!wMPj}i!y&DFPfiz?@V>~Wq>Jl+UwHD-#T7Ki zuvp4`>pEe@5fouwTv2Km5;NZCvexCef`*5)y8z+=!=V^c&%feHUA6|H#ArSGX3#*gT6Y!hYW zZ(Yy-LJZv|exn$?&IApe(jPvKkH%)+6?4v7@x5ovh%?nd{r*EVHqj&75=zE^1X>wW zPnku2$8bhT`1!==Y>M`iHXs`iz4OzOL`~B&w%9a>a~vvw?&4u0RT~^>HWT$2geL;*=3$GcdK-}MDhr>InztixJ{8_#uf6GARkH%#Y6IV*CZcT)= z&Rg9&>=~L8p^i-BePNDJHvwGOOj;(`McJlUiC03YZ>ExK5LfNNSgHOL&gWgXaFMIa z)jJ5A*3P)?m&N4A6zuo4noMzQR^HPYR%dQjCUYECcP#p0q@oMbt12TKIllNZ-+z3M z`DVlKm6x^mDR>c5-z~(jY&rygL8KR{wkP8@qoXCr+MVf4fLq5SJ5}U4xt#;C@K$o* z3IFAgH$#pbS#Z#EI6TX}eR|$4$BGFFe!~IA#2DLv1@C*UPr|K5I$K6YSH!udrWq;} zX_QbIIO&9hB7&syq=>sE1@cInH0yFVdDpD(jhjIUQ#JAz_~7`3Hd)AGkU>9ngOMvh z!x<3WCzSj@;C@^FA^T7R!BU`Klq%_-_DBs@Lvdb#+`h`&?gClJ|)CvOynT_a+ z;O5j1Vdi&CRo1fFU4?m7Fg3;^G^(6?*)Z7%8EN%*i{NsTTH4#p+f1nXQ^LT-O?-mC zM$$B=PDULO(uPNEH>Fa?a#49U8e~_GmcA4ic}3@31tXM4NzYyH)*Q@QfCS@iRvQs8 z*>~wqp0+9|MH*suK)G8nEB?g;hELUxMUSv{dzOnXY^rBe7Kej%B~yGhp9F!BMnTWHQZ5kE=;Qsm#5Yegv%(L1N&wLuK6I{R z@QbR}MbtOAzt{_1bFR$GY>^*!;{JpH;yU0nicW__Kq}GSyMTXwDdQFcI-cts1NxDP zoey>(>mYxO15Q4(mkf<^>NPGL&3p)SoiUK>ewq2h^VowPZ5!Xs~tjQp`IT@g@r; z3C05_1f!)Nl1*Zke`zSt)-|A#^W^3t{bTn+^IjPlMNx9-31ORGkF> zRGkCsWge6H=L?;{%t%rC@{v{%I_51MUk(p{g~xl`gj>%Y|$aFx_!&zV7W>8!ARw*MIUKbw399 ze(0u8CCbhBUg8Vgf?D{XlXKo>R&V(%Sqk1u8w0yw%Tk}5|qh7Bs1)-Pl^*q zJVazR!tue=IwfwJ&nOdlN|A>D1Z`K^dhh`XJ!f8tcHlsffYTj(NH=?ZCk_jkGND8r zW&XH1m%9hsTM@{fFdGk?bNG@QZ1A*@Y!5tCK;BlFH` zE6dT3RVJ1ri&0;kt5d9S=O~@}Peb&pjJwIFDGoCCLfr)AxpY+HBwcJtZ#q{+>Dv$M!g`(Ez*%?7D#P0H`hTB5|wA$Go=keVLdRUrbR zWSiRpqzy|ji$z;~gl~r+kT0L`D$G4Xo7y>S%=a4BNodkL08zT^_{_xmjB zM}4&fVf%Kw6hgpJ*T^q2d$Vbak4YjzNH>Yp?SkT8WAIwwYS*(V>L)5!i9J45w}hez zJZ8`rLNx^0d5h&V1saiL3v$ANbm!@dA*g<4KT|Ky!(PAceGq%jZ}~0mb8R<-m4Z+K zBE>_-#3ZbZ-+3{g`gC<`g)wV?5=5OXi8A$UF;SWyqJre0Qf(`UA(s3G$1Qr3C;gIB z@J1?>R@Y~@T>OE3#mZMwGk`$RJt<@RGzdM6|T%h|)WBHc2=?GJakUTh=$TXm&b}gDP;_=5#uS;`vv&2RY`?MrazB?|H z-3hdzYib6k|7xR`zu45!iVgh6FJ!I#gjCFg?usM7e>Zut7QEL9U+kqp({Kpo-u3n_ zhz;6s5agv1ByJEntjwKh)i-iH%ZuEAobH}SeZLO|vS>_XL~415_@PXgD1b|ekj+>c zVV)-BXLqKcY*|-SLwp5M5)(mCW=WTtxW0oh5mUYoj!ImJ)Ogg6)KMN>c+pU2KesKI zQaStkO$8f#k`GlP`mzi^y9qcF+#C6j=H9pLvaX~N#D+%io3KZles+Vr6$yZ7_COBt zk7uQ<$Mg}E%UyvUkgo57)N-MK5GW)slH13~Sx^vqb$7`B(L=Fy+HzQ{8rb#~^#iH2 zzb+?dah-f&F_gj-(HnQA9D)DHN9P{ylj^+nr8~vwK14%nA(7ZCE=a{0{F`#)3kU-a z{HI9k&eTgJaP~*kOxf)KRKJ23h;JaQ$^S+5AKnPvy!}!A$9CcAU4NOaz9Z|L1Itc- zo2_d8)TsU!zgvzfitSGM?+jY4>fsOVhb);kc~_#n*!j#4|Casnb`Af#?EgBqSzAwR z8koGFECb^7c-4cFGT*hn2w{?Nd^Uk#=xN_}X!}cO)a=_owf|>kGXjGKOFL5kA6uV% z)k0qH=IaTc`{v}_?k#*?JYQ}~wYn&ckuVA3QTR<)f*i+A0_;2y*A*Zk9DO^b)->lK zGnO@CpfD)HhjE(!iZTCQkRlqO0sXfa^FqQ)!PllS)|XvtL%A<}WXf=fL#b<5?w2n9 zuQC83;fTLN%*rn;B~kJk*9L!wmj-!NnK>-D-S>&uq$o%LXKg@ELjp>?Bk@yq2rSeM7g@I^vSfm0-B52Hca9dHmf$ zO>0y9Bzf0j@$R9PAl40A9TKjzGd?}@vNomJNx_<=_e7;}KJef(V16WT%3!7-@GYc% zS_uuDEW!IF1`Siwg9(XwZ#Z0M9g+S#q;y2i3jKBc%7_GKdik#$z6V6i7$-D@&nBEu^^|{ zmw%$e0E2rRe+T#YjPxW;zS9k<4M;eOhv^UrS=fwUSW0b_;IyluOA;d*j~9CS?@zLQ zn~gvBW_Pi&pm$k}qAjy%kfvE{jE`{D{v;=cFV~~i-$q-Yx6WZ`l)Y5mijTbO;TtO(ve`WuMO!2SkSE3m)u^xoe9aIF;|tdyI-_E*FrIuh+H zR(nEWvoe%#iKFLbOnDk~rA}A>d|L2Q5ohwJ`siLQqG<@gF4(Ie!IK=HVh7NtS2@MU z@}aSAdM{o%C+rvYgRY#48Ia8f@kG|LAb}$Hfk2et6pR=)9|AZOr!Sx;jpMpaE5M_| zvl16~_Yo*q5+bO&Wj3w9TOBY*QD|^V=!AlF?38?hgooFw4kfa1IsA>mofJwz)Q4&?^WNNGic}0!90VCOK%Al# zSkL-ni;|}PS6lS#U$&_1MtNe7Sx&+~CJU&hxEHgl44pbA7a9qr-J21J(16D*&I7*o zggHVc-==xgpR!VGi4bQ3`xYUFM$2&4=u{3B5JC%GmJJW6wUPdBpuv(D=+bd1NqTC< zfx-IJu7f&6`bCtObt24Mn^ElB4)Q39qh+`Oilb@q@dKEv2 zv&yIJqG(*>TsfV{BiGT>Q5QCsOo+JiI{1maG;a)%GkCuuBI#bs<%IQs%KhCsj2wOs z>!rMh^#p(>qE_Y~iEj%G>7Nz@Kh%vc#b;BF$ZZTBiLswkJTCdOSW={^0WOv@IerhV zxR#j>95?TO)gm~bk1xs>H*&z=D=Y(_sdgGDv6p%@q=@t-%`y~Z*RkQ&p}wv%uE5rz zMr|{ujT?$DZgpHKOHRd1sdy?0T|A=~aOXw)l}Q5tQ@QB++fE$Gk02yszCK03YzpE& z94YI#9zg$$_3kEiVUX$X9ZPJj+Ry|%P%rWN?vKp-WFs0W26-49MvoVx_5%{OS3=G* z3TF(BpdaD8;tO5T_0wU%9e1K-=_I87(@x}~`8PXp!6}x0 zfyt1DzZy&s1V==tWe_0|Y}blRI0v$>=iN?pATvpgRFF+2Q^Xz|t5*;JP4-DWuf9#; zLL!itLse_mvfpur20Dr2=|Cqj1LzuN!jJ<69tb6{@TiZnjUw#ZZhUb^)UjcW_DhkmZ~(PJna>)=?J{EY{Hu zB47!p1s2x#vOxo+?Z@PSqE0LmDC$nwfub(OES`K6=F7@aA@8z zLMITLx6%h<^U9Max0TAUaGsdi3P33b^c$zG{z$p-HXw^HwsEBO?lPF*Me+qKcYWRA&plDu$x zd(PR+CLJ*vd6@{^ULK`CfxXW)HJ;Htzsa0>X=alaf=hEWm+!;W9CNY+Bt4kI&!!-f z7)0$Q{B^JV_M=?WHK}>#wDE$VWWAESePNkWmG*-Z%An+rlplu_3Gf>`xzjdC;t2Ql z5VUwOuhoxYCBjCHhk=!z;`;Chiji^nd>`omCLA#MZ=(|~;xp1sf2PAeb-3P#ln#z_Fu8o-g?9KB|AIe(L8o(e z5nBR#EvYD$4KhqBgb@yaZ{I4}KaDqQ-oJUYG~KQ73fOJV0ms}FpY@KJhMbvqbJ9TI$+Z$T8 z{0}-zy*&S_vc{q1>5IR7sR$gkb`{FbPLJkXk+IUQEq8XuSZx zeo&7%^3#tX6}`I!8V&<$hS-*&_J3oe0jt zDv<8WD*$##utJGw!9yUM+W=9iG-DFS?wV|0+7$%p`^W<3|H{#mYi1xTRGEL71Q;;F zz0dgxsk!9C0^h-S-IYWv^UHytg9ZBQ_=jfy_t{Y&lv~CI-|Va@d17qEt)}rto?#G0=EZyc}_RYkL5;1~?PJ-hsgw zc@!23$c-=vV4fot9kNHF$3oQ>FmD+LsGc|BU^wkR%CyE9qI@*a_3!EHpDpYGWdJP> ziXctJ0GViAXx|~5dT4AHt;xh9Vk&dFazM@*W$nH(Q9|jV$~T}`@+0^Tncz-2<$+iz zR+%Dayy&V=gRt!bQQw+hll&4T)hwC=%b|HX_LSlZ;Ak@D)~Z=#G2ey&7b(C>ji4H7 z3M~gEyXScfOBb4FWs;)||B~zyUepunT*&9>aiDcfY0&TK5b`Lr^Z8z9&t56;j=*$5 zBJnhnzsGw@aNt<5e`7-sT!@KNB)}l%+ZSg6WqE#gSIsL{~T=MU|{hDL68N zTO?>Q`f!pZz6I&&*+*_!26IZsRau~eU6oAn?qEyaV7d&1?Xu6H9KJYUNO9Btu8(X& z1}rb24TsyndD`j2Xq=8R96)O-EfM^ig`GJzbQ@Vgdv`VcRI-H=L_F#q0DFoXtZaoc z*KyIpsUTQwaV!w+0!IOXx}5PFz0k#+pJ!})z?ciq)~BRz&fz{;m8vQ+6%XCfRsfdM zh$oEvbj-hy0E3i>-NEdy$z2%Och=^1AF@k6>m9fofThtjoID9u+i7goc`(&G8x+DS z!XlL4(tVibiVI>q?q#Mnm&XY+#1+M!>xDB;&kOR%9_97jeTRTy|AK&-@S6R{kbJ@S z0`5N22I$}!bbe-7U>9@8?L zxlpJWW+K(&57x`9s@#ENyt6HlULWH_wcv@tucNqh)DAC6pEeyp zM6S{F(#!Hf`puaZ3^L7&%_b{Dkh!+W&@!o&mSF?c1H7xT_zVwcz~vuk@7mqj_M@Pm z?$)PQ>Qc5zLui!=y?C9X&Nrg%h@YNp%d*_?FgNBQ|UZDpRdZ~B<-nwONO_lf!SdJpsd@ za-3$%V4pt=(+ZOVE;@gtJ*=%0_iBQtgjD|?T5@R9fVw%^sh^5;!02U;#sHjG_b2wO zByY{cw=1wZS5O6u;0+~99RbEOEPV^eH#~p|%Sr9y!nCOhD)R<38uJ9!`N;O-wOyU7!eEB$x7$FDoD@!cPTcTG@tu;bp7p}KRl1_p;aU2R|e?e!B= zT)qN_$#d`zs{<2Q`aaIFU??@Ni@ZHhq4~$QqA2Y_3EI5-2ayJz1SKFlMXX6c^P6)7!#{Y?WAma?!N znZ2%N44xPj0`qCbcbyv7zW!OP2oDin8&e>(A=ZDdd=ZT|hQY0jkx|{}JNt`lnj0{+}S; zMCH0n^0LFfoOsx7V;6QITA&jjK<;CfP4^_Love6iHWh~{sAquKGf<7S1dQSTJzK_s z!T<~84FWojhANSSk+!2+_8&p~LSPVI9T>###If#c$Rx>)S78Uui*M*p7Dj=|9<554 zr5atIz!3>zZ{!ZNut=c*I_=g#r`-sx74eKz1FL~Iis69Q4I4)H0CD%rLR&wN8@8*( z!3p9Asv>(HH_Zb?W2ir$rb3TQ;fojMIfW_-yPVgeCOe-qp}4KEscDoug}R(GLG~$} zBkI6ADzpK=0d8Wct=|G86S&0rk1r12craWylzuKOi5IjHpof_Mb3@+TDSY5x5EXJg zXCgt!;*Ej@erHl`p;IVKTmQrS4e)-z4PwCC)Yj{7od=UE8C?{VhYEl8KDQ6gOJ$;o z=vPFn>gA{c6p||+es;Orz2C7MjD0SM?h08YUFWUJ?xHYnsq+%4-9^3*G01b^6}^tN zCjb?X4E)L@KMYO#6_lbI+k!%d1*u(}Qi`B4w*Xx5L7Q8P_QL8%a_cS`Qd#>AI9%L( zKp-J$3ECHk*ctStJ=xws442X$P;`f?yXD!y+k`+tg=hX~kr>;%rQ=l%ry5!tb4FAtjA5E&U!LbjPdBPe2$80-Vf$y(2V zQA@J>M+EWbJV;O&#|H}<^)X(4xdhBPHd|u32G^O!)0<>P;@Exs9GsKpABK2r#cPbU zvPyYVKp45223d-_@Dt!7AcLM*rmeqv#z5V1&v10|O+o_xs|0 z#r_W#faEK=95X2C2I~Bdqs!ZEZF|>X3bh!nncjnNYEkks>s_I8ezDG$nV9Z|RaMX{l}a~V_Q;YaKA(~j$eW%Se2py5 z5D<#~I)v|>QaDFa4Q;Cxhv)vL6b?*%wGq-xecDKi{&;xH%LczcN&GIyFC?JWRo`|5)9T*izxNWZ&}h`4{QbGgp#RAPy!l=!dV11rBZ;z0{R#2U69WVL zIF>*_*@gkwR-i}(wiTcPOYrSi) zU1xv$?62zlI6tPVXP)Z*2Qzi`ecdhbqX4b{Nu=Ho_CzHGsTEuV_CXdlO zHB&wUtul!s*HR@$Kx;!=puBJ5^JLW`9jpgLCal2y=TaPjd(+Zi#Lp>8n@RKh&+HDS zTHv$Yf7Swmfxx&K_-uFYPwsfWO@5Ltzbeak^KtG_bzJ{%l|S}ROj&gfOc|JuYUgSJ zVL5)(bzqf$@NbnLEd2kH2Mhn=g<#=7=YZJ+znLoktL%m5#BJ~F_)!M>ah#Rp2PZnD z*#!*|S7Jsv;~vmVVH)b<#Za=CsGf6;#1e8ZblV!uxA}Y;TDOE32snDYx<3AJ5gX29eYp6UMfq!KcSWJD8BFyn$CkS#4E`P0D1V zVZvik7MwR7Oyrp^ecT_@gllj!JrP(c#`st=Tx+SU3TI~G48di9dC|h7vI|q$1LOlZ zSL{f{w1A3Of=|&I{xYA3XA~a^XA}?fj6b`|tA7-rlt+kMN@)~*jFW`rf&QUSjh~3X zVAI)P(EE*5e3v0s!MOnKESERb+lSPU`53u=E}Kfh)+QO|D(*Lm3HsQMYB|60u@o^L ze~?*vkeZA?x2Igg44cgVn%!{5#1Uoc&qut+Xr zbSS3TH=_x+c?P#od$ZPJK05ss(IAT>U5z z=9vJ=r?8fAAyy}Gc@d?VRtJP)X!Gi_{2KBi^ELq z{~{{^xLq>mO*P5ckCqFs-pudfC%&eUF?9VEwK5&7k&%}ITO@7Nnb z=GxCn6XrL4(p%AbDl2Z5U=Ed1nBl(4F??AgA513{ohW>Z72cf;3|zDo^j;I%o%HYSJz`C}R`Gy0-C_Knwj7$HN(Bon>RF|jauoKRg%cO1AoG&dY z>ubeRsTnA0#Xhts5?w?xs-{a(#bOXe+~eZ$H?O+#YVO%&iZKNdmQI0heO@`a|FW(n z5W;Bo3=*4%K);s7enZqCUO`Vy76fjeNCzE4B;;?d2uyMz6JK53-bo6k#RwS*y8O2mIgyYnqy1~M#hLqdScm~I zV~JJm(?6JiCzajLM>SQ4gRc4>&{RefkUuiG2jfkXsfhZ3EAS~>7(F+08;J{dvYB&@ zImd?4ska|9g6g^&HDH$a{dahDU4C5l7A0eGh&Ho&gM6Ec`RoTZ%{#WjWpod^GJkHW zBkh&!M%Y2BDQUvdVbd3x(*r&?q=jX%FQ9xu@!2DSh9D!W8puW+O;dE ziwTK_rZFcAa-|L;-_@=Wik4Gj`FOkTD7@Bijzr~8`{z&dFAll9euyAzN=QeJ`)MhH zP!`?OaH+$I%T{gC)zwfm9-KX17ylgWz*Lt8AZA;-vtS?G+W1XS})_Tz@T-Bg)>7$90(=km8d-(x}PpO;kWvB_))&PUjxrE_^J+qhY?Bx#WAB@?H&CZebdp{oU*hYYk;E{tnIXacE zFC<5zR4HufwB3*ndMwXJ?O#$2C{J3SBC}K9uDu!?9+fAZ-dr}!yj~{X=G`h=9^YO% z8y*GUBHw^dt20`1=b6B_n&&roN2SDHsEAh^v-;0Y?0sI1<6hK=QJT<1 z?wSmH4BN8x)F{N1i@xIr;BO`Eo&(Bt_H+*<4@YEig z>^iolbV%G`VT3!`5Hyd7WVJUaC%kUZbbUNex+c?Y?{^(#eLqsrIkYEshRD3k*&&^) z)-G1=t!%(1_NTsJuLDWFcgCZz0*&7 z!z`oXR;i`3Jt<}Vse-6H=EL6Z_)Ft_?>cN-_xnOv~UGd}g4`;Zucx{#K|75Y3vHa6w$Bb{s2bN6nP0pr6 zD)k1CrL@1%EDGkSML{$vPwljG7g)yKY26iL)G>4xGU-I*BP{KEoAQ=Rdk=05wEi<$ z@nOyprQry_MYrSk7vQ=Epe#e_9;eYz(C^)Ut5%R#X=VPURzTc>)d~dRf2b9;_?SbF zmn-t@8nf?Wy21Ghhtsx|CguplM7Uj#c_I5~Asv(8X9b3#ZCaoja@Em%_|IPO&4woW z`d?UIw(~es{&dAJaMb-<$bvTfuaJeR?~AZJg1lM|;j@~oDkoaR;BV`O?i{^C%vxAN zp1)*^spb^)YV-CaC!RndWv{QdM62H-zltaB(@hEf4Xk?H*m6{$LVq{}f8YM2TxbUP zplO)bgq82{jlIN4yDBl*S#1S@iQ&V|#r7G9SX&Y92%?EZ2A1Jt8q+H87l(f->}YIJ z*w~VPOYHA<{%eW-;-uPFXj1$`kZ!D};sQ*ugornRC>1G}e z8IMxT^`FJ>a_fWDmfd+mTdtAVVTC#_POrJSg!24>z2KT-x+J$=Ac#+1&-P1JuarIa zd!jCyeQ4JIq_E@Qu1)|s`4!e%z}0u}SXGS{@4-l?qCen31E;^$hC|wXs?*o+9jDYh24$o;6{ieS> zQzlziEL_s(xIr;r2MgTg$t&ZH6iBRcIs+2~?&-)lq1iDj6SxE@f-`YGh)f^6{W12j zmU_GT04LlHa$n;&xamF#yh9WETxH7a+9!Aw`o9|>TU(#dq^97^|0FQ9{!U;V|9b*M z4V=Ju11B(^|CPXa{PzR~vGYF?7zkk{rQifc2{?fv_a}it3l%GjB`#E)2Ty_jIH(8{ zb0&8P`TWFMA0Ke*ot!g9e+aq#Nc|NiMz-3)1ZrEM5Fw9&h*8s9Ke~XR_85w*F)Kcc z%==#FLe|3q5DivI%>Jp8jFK<-i#d+#X-GTw^@XmI2pO4=R{ov3DEupR;rw^%LW9HM z$oSLkoGiVIsS)+}sc*%~lVYOVs|nMbXYTCjkp*q{(jM$(rEHeNWzhl}f)!^>XH18D zV%!>3A0yC~uOjEicR3U%R`40E$w|}hcMUc5uaEv%=%pEtD3ubt<37R4tw%&UfiUu& zKfb}UlD*e-K!7CJ5&AoHF`HJx+T#t$j@M?#G1iR$-Gxi3jkPJH=2!t~GHsq7>rX)& zbGcUZ1 zEx}dQ(2mX7h%((>Q~!`llKzrQEYtvfn174uC-A+)3`^0>%E!O;Ez(NEm9d&*zrf6D z6a(e;L;JffxRJ!1Wsq#Iqzvo|`N<)pOlRXEHZw||O)`*lng~okoxJBH2Rj=z``171lBR#yB@)d4ZkJ5{yInH+A9hJ{36%;|s)xL1 zr*o6~`i90gkFDedS**Sh)tFz9hAXKq@WrV7N(#{IP~;JoXZUjQr2Kl9y2 zf@MW003$R%Lu^NbHQbw;^AgZ#b6G<$UTJ#s3Zj{ESt+>%N4RB(yJm`Id2|6I+*qdV zfvl`phI4m5R2GNIlO9y9ng>519OufSUzXDQKsDY)?f*z~G!<66!oKD6M$;ATOK>@z z0CDJ~oTt0bM$v-3PeS^IzPYPd4WwKEYvGIfzJv-D?fkMc{37Pd%3^OFXsEZ|K6+-@SQ8rn%$h`r!K{fk63m+PGycJvvIfDdX=WSDnu-R& ztZ6|X%$lkO!K|rn5X_pIBEhVwZ4k_wx+cM_DMTC0n#f7OtZB~}%$jaKfLYVgem1-d zm^CpbVkB}=nM~kIPa7*W`K0$-vVMA^VGW<53DL?`&?oxp;LbiRP(MB^MsMVdkaCa# z1PlH1oZtw{&4&o^6wA*%_>YPrheQkhY6h@T4Z47>ej(d5k+4*x?@SGFe77nKyQcXH z45)Z_22MxD5vs2CHuKw9xxx4g*Yu$%QMcT?i^Qzm9@mk~bfAyXsbwfEbS*_AOx@u3 z0iNG>134_VtCDUNH|KQbSz~rj6PNs$VzkYP-B<#yLi1x%FFW&3ziH`W-%xuS!zR$I-s`o;njbZU3-3srlxPt1u(YhCRNOKM-3DZC$GMD&zZgUS(IL<>Wdg3fzK~Rc++(;%VbsR*~e!&uHhsi*Id(J;w2AQ9h*J^7g40 zL6&8f9(dKN>$6y)3SrKrvAf0Txzi%3*5Ohmi>=Ie_-;YB-DY&nHbH2!>1!9^98?UO z$0jC!ngWZ2PQlQ{66WqzG8633-Aoh+H&!{5Kxr`~s=C6umE|zW~MLXL%FBu}9TZ z9Ae3HsM_D$45Y(ZC^;lzbt3E=gkpF_q>kPb7gL(&-FZi^h=}jNhRhKY_h8_Xp`(A} zz|GUcg<1hj;BHtOxr6UOLymm;7D~BRlK#FA zo!tD^*{>8Kg}TBT`v*@>cjE6iAtLmJK|T1KBNasN66(ToC91$rB-ycVYA=%{IN$>+ zudAp;8iT1ht_Q_D!!qV?!+lq0)$0Uv#h7vsSl=RlRlbWlkloW=2?>>Z&oBMy9Eg?r zYtsLyzo5nJ>bixh@;gdSD<#*8n+)(6K{P&knv!V1u8|&bz86F!Xx%hu5(gv7L2^Eg zl|-kFrs-3xcV5voN6o~{k^KD=^ZX%3Sl7hY%!6Mt4*mJo?bJ3l*O4k3bj2sLSc!D7 z(^!c)kyfnbIce!NO>=toMkA`q>LrS!fJil}!iKq;Nu2S{lZ`b&fWUK8Ylqjjx9vUY zc9)h8a(1SlKxn(Dw;riea*gIf`P zrf}h*C_=d+@?+EJWP&}?x$geC5|8l4;a6zL1fPN7??T471{!2IT3_!@L02Jpve*8M-UKD1GDf}3TCy$NM?@;d*K z({ejCy-VmKiGiDO=4%Sv@5ZsH$R?nojflg|Dh5}q)3Ty(Un6IjAO1&N%jIHXTENAt z&1vB;@rvb`&F62@>M4{X=u(-}sQOo43mZ@bLvCNK3~g(zLGrBo6!BNpx}T+tsPMP{ z%|;onL58 zvs`1D;j&bjV>PJ3G_~ymct~pk5B#;@VXYiIRGj=FtbKn7t5z(yI`t=&YE0Uf;B@(4 zXqDv@xqx&+axE5ViS;T9x3-Q>SGADz_zd03AOA1ZrnaUX!zJyc{Rd^GN~qkQv6uKq zZ0llZ_!F41(g9ZfFjfZPZFV!0^LQx2GJ~nldB1^^U<;=_PC6wlgx_-NbCtgD<1v+= z5*)(^&>o#9UzwGJN@!miWG^tqe=8#Rt@{e1u1}*;m$jQ-^qwhQAx;&FFz`#Mf`c6 z@r-*p6j{v9I`GNHFI-)(HLSUzT#VQGE>xDcsUcJhJjkTm*Y$k}Xf&K9e~VFSJF*p> zN1xQRxOosgA(V(rg#D8rg8|9rGBwblvHhc#BfjT*71+r@OuRi_lk{ski=W*90e(QnOyiyMpq^h|shuU8)__6I}}x_@imI zHRaR(r6g>jYZ)tx16>T6RdP&$^YsPYX0e;@ndjf?IFJtrdxEC-1&HqsB8x(>^p|-S zc%hC@k-ICsM^Jsw)B|78yM=dFy?HFf+&h_NyF%@7sZ`)H-IGQ=tM=G74*3Q(k1HVE zZl>U15MBbljpY(BfonOEcF_J)EeBOnASW_mw3}3xzaSP*+8%~U+$&!=rf0-!B!uAr zLZ@b`<(y9G<3}`Awfnsl|K(h_KkejIrs;o~JgZ4o11mhB--$)umZZ*p1AOBRy&>=M zcUwAJ3Q&sW8iPSJAMIgiKkx_4J3fDzgUdvzsCB3pIC?E}k(l}FQ!SwbTtMqNVdk=e zsQq~l5tBL}IQud`={eD*N(`EDG`UT-Q?=$F29_#~eWt z&RTlmXRJBOcFC_!hBQJv3g_q*m-d-hKZ6!e$22ncf9@Y5g(o8nZOX}4Jef~)H~-8( zV{Tx;1(gx+g#OIWzP48|$qcYjaOxO08O zv|K%6KGxl_Rj_?TV)Z!O#CQ{x`y+62w$tC8@uokVix3$$Pa-U%)GcJ`_mQ^5YtnuZ zefQ+7Al_>p;|*KJY=yxC{|m;Iu|sy|by^~2@BX27k4MHCiPR%jleetk(gT zq@zN3!jwC6_=FU;Oi>Kaw{i56Wq7lxxMI`5^trfVqriE~&(DnAwIuG$=&f1cn9lmi`z6$s6h0w5hLm1 zmiC!L|(L|yfY4Td8(q+$2}IsK}iORCl|t0ItL%fdq3!@hDml|lneYPigZ z9jn`D-fFn|y9P{+6JumF8l&-hZunFJ=Q377Z-r1(G+T7HIMXhWWd z3eypnNUNf$(7+M0HprH=0DcmA+g2zor9v{xo&^unRPl=N>*vJmr%igFWo{=CEcgmm zT=w7#(#+iqDELTY3g*2UPS|;}b(#jBJ(n1KgAF}2rqgL>R-!snfvQug{HD~5$bx#t zb;-%efEVbFnRLuKL@y(Knpb-W_yJ4A{ntI;zKOmZYMSjR81wARiVD5CN?+)15SP`jcsEHGL&f>?QV@8cwEi5Mp81^ zzp6NWMGjcJ8QQ(K+)=f_g4nwcC;t+}Y^_#wP2``$?{J>qr3w9%(#byiy?CEeXx1Ko zX2w)tbl+hnSxUib`*+L^`u!87;G5&6v)p^b3I6`zOyZMzF03nwfXjF-fLPeVf7uL^ zm3TVOtQQ1y$v8u-!e?B&_-}%CE{GGzxb)+0x)^=9WRLg-Kw)A0VN``LkN6kmc| z?FV1Zxonz0r4BGE?v@;K45rKq(UHaB;a!Rw8100IH;GJ@wx^F&ba5$PdIET;Y2F?> zP`LP>(30978_6Dx27laKeR!K1y%?SJe6rMxh;|-5oe2tb#_B6TbGLgj>D*>!%JF+j z)drv%y06}Ee*LL9*p!GpxM}vV2Noyq0&l#32@N{zY;vB}S~+;-otvLJk=r)y{s&e87zfI$W#OlU!rvQ9+j~o{RgP9sH&9cPPOdlujcr?sVFc zF%%T-^eQ;WAW!${F5=&}ZMRPBww_PweC}O80O!F^$ovjFTTAqSqZ})}xB4J~JC)s~ zrgZ`5E<$i~Qkv&$vHu2$O$(4)Gog@FuMSm-56V4v!O&0h1l41oJ;v^@Hj`^uoqdPI zxWWr|+kFx|vWD32NMAg(h71mS#Jg2u?!nPL?ewO)ZEGQ;a&<<)2LLFq|OqP9&Fm>4u}BPdMJhZZ8@mT+{OBs|!J5c0erM@2c9ylk;$ zYX991tm|B+4%@#DBB{Xt4ZL7?MTv>y;(uXgJ+=B>+9){DzI!PtZw|6MiN7X(LumIj z0;G)1us}SjbW!UPyUXPuf>i*cYai6}i&Vg`$oroLI=_KHWgP@Rc)m+57PAa;7p_;w z5{T`uP?0AkXjDF#IF*f-OlYf4mYZS=rBC}ANvALa3b$vg<*R-IE5<)xXc#A$7b6`& zdARcHr$21u^$;ECf)~L*NaqK)^1J;W`*%>Dd3h)w1? zW9h_?>L>1=aTne#U-xn+VrskPH=x|sMq;{)O3`z#F5Xo|6Zjv3_cHnD9j}37K_IY^ zpCFd@*`D1TWqG!CXWY$@*q<+UdGWDm^2&HUj&28};-!>;BUeod+Ao)$>lyVX}eumkG1Mqd{QH`in!sDcGnl`EmxB0LQ%?Jcwi!|8jvy@ zIeznh+hb&@u`4^5QG)^=x_^~WZ1B=i-;Dj2} zl?n8Ls@=6DLgd{>daC&noR3pX<}~Hhvd0w`?+1hSpKp6U0T;E6I>g@-?Hav_(|>M+ zxPN##Nc-L+OyvB6KJ>Ntj`NUl(iQLxAZ?qO$V=kXsarWRzv@_;7R+S|)-jp)d9rcR zj)+362CXtZLPpdZCJEJ=T-V>F_UJA;UL*8G#EVQFjQqH5WB15nz;6i<3=A6_KOBs@Y;t+QWu5Wz4jUsOCBGUu zvg(u^66~HO_3|wheudHYdwxIh?vWX|H#h*dDquR`)XLAt=ldg*3s9)EbDu!%eJd0F zdWiZ)Z=doInm60Tt-sf*^p&=PUEROn_y5Q^eElWL zQ92_?uR`XEA5%m#WF9EQDs< zAyb`%q6hExE{5=YtORup6Se*_S6_9Z-csdtG>k|`tV=eID)fOz*tOHlzBM3$x0zvI ze>q^(7Ld@_4xT12|Cw6;naVgO?z)?QJgZ5I{8Xu#a9u54`$0Ws!=%qM6N|6K+(CDt zb25TRQ?G^jSlfKK;5jNa9JohsRZ+#;{mS=Zx74XN3ru_|tv@wh_R}U2ZK~A;Zt8;k zT{<(asmGq;(*$*al{?SxuRT8`a!0`UrZ0Zrbna{+Z&FuedmURu@OvZZxPii}e zis?*mC3AE$Ue$}8zk2a-(6O+Auf0ibRVp&B%>`?Qr?u&P)HhGwu?;j)&RkiHkJfLj z`FN{Bx@EJN##?!lKG>|6Mn~y*@I1yBYE!Z_9?R0tNHtIl;#BBs(y&US3%wFU#BEL@ z9Gu(}jEANKKfBz%SLn}TDkP+qUm{`VKW7?%ORk}g7=FtlI{(>ywl%|6c6Nz+x7W0d z;dF~%>1g0I7|_15jAJ0^84NPf0!xMMD@-=jm6xW8wOfTpbuakGE+wq1v3P!3vSEt- zOBum7JU93FSw7nGVT->JuSB~LcYK4NIR-v7=aE*jYA0fBbL<^!+6zmCq*+;u;C7f^$Z_Wa|TYDrW&S*-r@(2&22HF+4qi(wxY~J+@#De(3 z_F?t=7l&-R?MKYYxJTR(=bVObW=eAwMwc&@yE{fCIzi9g@Awc=L3*FbylH3i`h zN@z3lx`Z3I>Qn`KVxm3>kHLZPuWh7xwAgeRX$Q^rMk#lle^l)I@=T+!YFGUiuM6;J z|J!SWQyU;vuxZ_N^)TK^iX+9*HrjBN>Gst7b*6Lk6lK-dt9O;8LC9-%RnY3zH~A4^ z`1J6^#bcpz$`+^I!c8q{y&q?FD7^4fhsZc1t2rONY@_G%tnyG~&C912`8dHx;QPDp z6>D?DLR=acZyL={fTlzAMY<~wC|Dp4q!T^0SH?B;WTK5VmO;qNxML3Y4LNz_Y^vC* zh47wwO~@$>*Z0sd>H9^*R!@kfXFEf_1&i1&cqJg zoqNg$trLQe6IWYv4d0C4`sT-j2)=Q7t$%OnR7LN2eAsPR*y-q2?2$;}E{*pRg??_Y zwDWLtV|cg`+Py2>2ZQ-cq1-_)L8F#8_xlY35e1;bTls0B6OxjICw-rys_4euy?9yW zGYzdReZ&*fn^i7n^AQKy3ow0TK)d);0aslF!QxAv_w z`zG#zyy9!xMhfUWGnQQ|U&8tN;q`7m2+oSJ4|dyVgT)<`Vu{54G>vtTv*e($SbcRO zQI-gIS)Dwl*t^0HqJ=a)%$xp^z$fh(E8UI|`s^sxu{!3q$S$`Pj-lD=qwCX*?zczb zmnFnO#OyvmL3p7A<4migeE(DbCIRC4;E)=9W8_vsdR+Yqz89j0 z8ur;&w~;JntUQ?F>ht?fk;s<2yIw87E^eN~(?%|BKrG19qgHdr#^j{5Q+vG*CYBm` zYw+x_)MjI+0QRN_#pxxE@2lf1r5lGE%)3CR>!To?wT+3VgO@wIi)qk|#VlOl6kCK< zRDc^ZHi=M5G6vPiXRT_zKn`_B+dOD%5gti|AKd#Gjyctm4Y z2Sw0Z=Ueu@uuc0$He?!QQX%^TE`B`;X#7;;iqLC!_bHAw=^{V#(WwCcU{SQzjn-23 z0O))kesJ%2KYQiYD9B1JzJsqD{kTw%6%@H~YbYu!z}lyP@m;&Kv3bR*yW!%RBzsRjns!fNdmFw{i1pig;_`MI@D_iSw?Hyv_ zxtDKrP=Q-lwUfyZ#~q@>I)|i}IWmc9NcuXuU@PUNzRWYeC<+Iamw8y$BBDzX$88yh ztsG&VoS<2_b}t-XxXS{5tLKqdMi~5a84oPP&S*K>PMWUh6ox>kP9gs9LVVQ1AefJn zxbOXW^&wC}VV;snY%K(k>mE&sogxbR^C+EAQI}yf%)Z4|P{(%09G}Qtnu6fPNSWZT zPVhY4GKJuO#ghmm9>Xn=>NHiOdlk93W_)=D;AF_b5^lKl_ z2wiY_hhXXKdx+xVdX!(CW}<8zrJfghHS7tDD!yj9G$P!)qg}{vo~0cMQ`jIVcm%-; zwq1=YOKRUIoU_%OXA?07%XYxpw6X9HR&~NcoSat@i6#YMEbr5eWW?D^N`vGN6sD3G zAa`cT*vZJ3INXvWS4t(voghs-7oP+>2%PIl?7~P4IG;&P2nBdIZELFu$XRxHIQg)y z(n4u}o2BK+t4$=Tw|@{qyk0E1du1JE_H0r+ATPb1B3w zSldO-ikjFp4SVR=Rl$1H^m|D8u`P}XJE7!W(0)Gnx$h0QLG1pr(+w1^cGDQuP=qr^ z56d8AisMe))pR<)oW0eJn3ImUX(mj*)Dgr4?TN140biY9+peHTYOS4#OB2=wrodfhge zCvsmrCp!FNr+?>&!?p9GnkQXR9PnKpD~#Dwi`n@&I^!Jb5i*8Ea>oj0jnq2V=8AOX zjh0z~6kH6;9q*Q^-xslCeZD2btd2~O&39a5Q*Ya5OM*O^=iOFk zmYaWxYh{i-cTDXlC|95eBy`SelXndhy`T0BWS7unyU>;L=;)NFL_4W#9jKW03r)>D z14YmGX*ZdIcOa%Z0s5b5KyY zJxy~zgl!uivCF4%gPW`kEbsx+Y?e%fu*cW`;2K zspvwk(c}B3#dfU};K>eJ4Pf42f^85cNE`p}ipeW>_7`Sswo>}!puI`NDBGiMYr!Nw?Qf=~kJLqX*_z2HOL2W@J339yBv=~T+&zTIYGvn#S&fylpR!3A;!_kGB9rH`t!bPs4y?$ItfwzWQK~LyTM<9dMC6gz zA8#Wf5a&FGbM?3FIu~!2s@ANy1X%G=+~iPDdR{DN^*y{gH*ZuD1-}cpRu{hgu92LK zqJB7{s(Q;Vfbi+15fKz&-%oRQudZ0jWthJw?8dr-qbvqOkZ3iHoxm~uB3c)9?X90`x_N9t@czL z4;#LN_6H*!gU5|iq1TRFSnjsLU&e%=vQYT)g6xrP`e@pWTH2j4hBE=XfI8ndv1Ch3 z9*^$s(el@DzLaQa^%N3>`k4Z+K)ja_DQ})?ts~6@L2$JbcDuG1>jw-}z;@D#0=$h5 z<%d=#(ka|wl9v8UJ-e43_P1lBhalp?4TR59;6DNP+Hr*nCfzv>mjM?rF0u{$%?AHa zwesSZHED@0`~_WNXx~Y;Gm(g|Y4rnO?zcjtz7?IVN6ARN;(oDBytu@Xd!PqSRn^05 zOXVnLf+WpR3U_Z$1~3(g;g&DG$uL7QDf8VOPLI&eNiHLwzZN-} z0A4#lL*v>)?Tx`CK;e7rqbf4BJjE-6i@#n43QnI(!^ZRIro+AQ>bOD%H!lEme~d8r z?#1)x=8_7z72$ELXjq#~u!46GSHuY(v}>pO_60W?+=uv%ASBIoEop?#h2RR{HrWi9 zPa-rIr49-;Zaa5}dB%3XxbXwNaAvUea)9y8Lyz5-dg@jf|DXcAwaz-8G4YI?a>hAc zrVJcTR}{B{oc2567~Bh}b8nJ+I8C523XO;sMkf3fbjck_V<(g2LMWEN#H0c+?Z?e1l3AxF~V(xbvOY)XLmyr;WXQ>Y)HtV+9QFE8Vp5 z9(nEX9yGU#;2l{5j(~A#HRO^LPnjkcRg^au9tbNzqKY? zT`o=!xlX>=gguQb86qMc{X{E5+1=bsWy`nfjtLq;>aezA2uatTJ{|4uVYJ--eHk+)IEpIh90RI%>l424eT4I|dF)29kyVfWJhLjrKR#t=tVP!1Cy+Z90N zAzOPid}gdMs171;*fv-ez0F!AO(r3JVdHoZl^lfsrGXJtDfv3=5!&}5v~4n1{~09J zy>!ncc(YhO?qPZ#oio60lbt)czvnr09P6~1?gTE@98i#;?c)F~#xH;{=AOdb&`=*9 zVL1d%Ke)^3z&kwnUI+EV#_-sly-u%(BsNprUGMd6A6?j(CKAg25>PARrFmy37I}7h zUHM7t%H3keN+r*A)Rfur;LC}}7Voc@vF6W67w4U=!5daBV;L<45fF>o-;@IY9zi=# z>5q@c-q)!G>z6s{>-|`c(*j#dqlSDW*Mhl@AcYGro7`uN*9Y-Ok`40CeFcrUw@~$+ z%nNF!k+*)ic!dW=oZUA)=`iZ&!_X@)o94UsUVAammpF?iB-ewR;E$$H1vkAUhJl&C zy=GoFaq7Juq@RMc=Uz9dPf0w#J_Tjgc|C}Or~lnUo0(TM@X9{vCrORDS2XZSo3Brj zX?0$G;FbSB9!7&#x`0<^)OqcISGvr+MuWHA0dK1{_xk7IW5k`+oA|+^yz5i+OUBRl zY2R7O#@C!mS~a&1JoOgdUQpzqS6V$>tt*QpPJ=w{=*#vW=&dp}a=UO7Jolhqxdi<^ zZ>CO^k}5;2#4a-yq2YeD=w7CGD_#weU-bbc*JmY>r#6gXM9J^BhpJ!r<>{Ou6OU$D>grU zXT$Z2O-AJt)7$>>D^7COsK%wY4<0 z^7Sc`9_Qj=3qPCW%c5d>T1=hB(>ui1-aKr z?r!cCnw_5A_id$9!`2Yyl@4_7#c%=j)K1*^++lvr6cUY3Pnvpf_{R8V(?hZGf`eOb zzbmPI#r=qqHg}&n&JBluyHna_b4{(C$2GI&UR!AOuDbqlOOjLPg@kmoqw-Afj6lG} zm@(Yr$<$PAIrd&$=b}M~V9b8w_KWRKx@Y!*5Gixl^k^>i;~sz(X%G0JQuLMEdB^jj zj5~Pt?O71J;|=Z$6WL-D$d=;|MuBXHY&NJlRtff>{Fz+4jZc$9=-V-)2$&? z0X|HwJcPvrEFjVRStzLs^9Gfxt+RDDzA<#hVCiQ3z@~9=-Q?aSeA7GsBSG!$WzrLG zPten(jF2Wo>CF&|h~o0?bdrxe-&b50tFiknP;q{H9_ztO&Bx1u(_DSSOQDf^>9zAz z-fBpGKNhg6b-UiscxlyvRPvL{q>Ci2-3xn`ucEwqd>k6GY)n8!JbCJDIRmT2f}oa# z77+oOpyF1XI!eT-kzzz|4U)CWyS~JJPhca5!hgv=1Xc(zHe~ZD$Q#!CcCu zo*Z2v@fq{oDMsV@RH3tVLAyrHLd6;4sHhDCu9O_WoRl>psf6emb-HxX&}12AgKZQ_%@mzuNxGWdl| z#-qCyzDrKf<;L-aCr5i8Z)?VR5os?}2Y^yw?$xA~lm>}rxP0Xhm<{8?&)Dqf24{@l z>8<=owX3M0KZ~qdKZ`4&*JuwCxPan1XU;6K{n4sEAg8$~XN)GsVTL$rZ?FWcMn}~; zC9NMBuhpDJTeT?m^?h~RC);%tLM$5kWG;J9`TR4gV%5^?GsmnW7xo8FZlr{DZafM* zZOD~!Y>%I{QP_IrjuKb4WcP^@iQDC{fw**RET%oN{)CB-jx%3N9D?=zqD_9*;nSa{ z*Pzq+{t(O7wvcN9R;x~L`H0BTb(7pW1+#2G5bFxjT8>YYA2w0aiw@ggW`9HB9=BE( zJ9Sd4Mc#f;q#VTbb12xHxpLGcz4?Hf!!!R)&*xYnjxy`3>zN~`5W2nyQUEPc&P|bc z%oLeit4;D8E<*0do~XR+Pg%A&fi%Llr)zrY-(cQ+#KOl~p$NX{G3>>%6F3AjidkbL z)w-Ofp4BAtx;WLQAWIJtF|NMciG3pzV?1^9S#U&(|H53?Fvt)3A z#tGM}o6PYfw2KQrJ=C2UCSIuWN?qikt1zv(KIu?3%DS)=P;1NdVt};$0il> zz87_6x~PUS+;C7tOq}LfNgh>?HB;tj0U^LMPAr{8>A0Px;R6kl9nGkz3nTmGi8w+m zbmPZ$bhMd&?x;l z(5sneK+C`G4a_jV@F^O>7$=kspiNvyi;;rDW<_RIoM>TNaBzG274x9++&!IO`( zsCznB`<_pZQ!#!JR{Cwg4u1Y0uz1;$742@RojS5t%$DxY7q^x4;d;fMkmf6Nw9i`dD+ zEehrKkOL-XO+y)s`gU6*bC|J8PKHoN#B>$cP|G6Om9N^xZea}xSWIOSr!yy8?XYH_ zJbv!(s3+$=(5RfYrB%YyY90Dgng>`Q6{9Nr>?zoLfgGQIcJ60BGZ4s_n!@cL8%cL2 z1mAM|p3G*bhPSR0UO&Y*-(_PXuSYCZ!I(r_|KpR@e6n)yNgEg{hqjL~L zLu&kb`^cHh*xZyqIomCSnlNpdB}+q_RL*PHP$}D*jk2#QQ$501s~=~}7tjbx_b7f+ zZPB`45&dcY6A|WmHQ8!z2`Bq1qL`xU<##LWqzVywR_R~r^W}*x6NRIuTyWP~TqeEu zd<|F-t4*RgS`Ewyom(kFa?eHb)VhR6C4N6DEbR3ak-|`u6XUqnx|WSZkqYARDE;&r zjz)XT13#;)U!%Fe<21xkCKL_7!r4ikWdu(-j~e&C8NjTd$j% zn;EYPsoy#H)-UYkU(Jhg(G4KdU6prLf}9J)NEO+g;gs`Rr@EmK|AD2p(a zrGZkR|6c%0K(xP0ewaC~rqPRu3vEhRC&zA6A;<`_3RiAwoCZUPA_n8O7KNE_rWl(~ zM!;z}nbu3lMdObsi!fy=ciQbDU$_YwBNCbp zS=$a4n=L7}&|q!T2)~(I-D;N6Jrr$mL|P@CttqnFbeV^mu1tlY-R-W@35G*PHW@~G zJcx}|+>G$N7z!P)ZKBmm2r)`khM{0jpR`8sfb7HdSI7+h{iUU;vhHL+FX_(vO~;?`!D8yKI%LfQLmcqoVAbX}s2A*@UOGoaYM@on zss%+3cZoNG7Y+z=L>7Chtif!P%=&xZm-j!u{Jle`m2GUY+NR42%M== zfQk*1igO|fJ8M71I-5mzTd-j}U+4m}nh2K2_XiuZKWcYm-k4ZnG@Z)Xa)iOu#!%+D z7IX({t1Jh_lo$cUmnT(L$kTREhp@NQ(yF_~rDWOf7MoE|656?=k%cBsNO>c57)+oR z%c&~L0C>b|)=h+T?svt3F<93OH-iaJ+mY0$jG@?Oo0MNkJ4c=7o>y7<(Amx{U*Xk~ z=}jC&pNvP0?XIz?%|WFD&03D<)$K+G%DzPSd>VM1yb`AVQnWX{p1B2+rX11DmRpGS zg10-(h6?LAY2bXI(5ckfVtm#m2Xl+gCsunniC8=6SL|TA$u(i2+hj5tpzORsii#|9 zt}g1%TE{#p-whmSJnZ^SNNhS{KX;H+8k@>Q>2PQ}Xy9rQ(^G}l4J}#dS7PVD=TVGAbb%V*HEF9SumnMIsM!YI zZ!1Er>5P;%1TtF1Mzpi&QK!R~?oTdUKsAPgEb3BnCK+rBK$Tx-EeOEoofuiK)q zoUgq$!KK4WQ>)ZKDwkDd)bo1_0vcv^fC{4;6(>b|W3mzl1rnDRPB7yo%}AC@pbx7W zjQj+a@e$_u!*JIYWyvm;Z05z@P5}7TLvdK@ccQt`=ut9?Nmg7;u$7bKsvjmh9-Rkp zhHj*&>eyUIRHyN}GqD>*Rg<+&b?SagmRQ1P9IVXZsv}JECQbbzge~U%K3C&>ceBN9 zgcvZrWroivW+&4e>Pp>wrVC?y>~1=Ei(|t@>TXR-F9ISS^Io2eS1m-yC=dcEEn0CE z3zKz>axyVtdQ9n>o!O$5#=(#yO*6NcQNw8kORK&d8t6zH$Mb5N1if`J$Dlbj7Kx@6 zP3!)IcZOKHfS9OA=t0@MnR$VB&?Lsig{c}b&UQWcoSlZYqQj}4&1)t%mx&; z>rva$SdAzO-9=z%77?ynH8V@Ljgo89rNNla%0VtIS91epd3oJ(^BKv^Rx_URDcj?nkuD{67TNC1E%FeJ z=|zto5UovXOkpu&WU)IML--o1JF<$ERtp)fbWCHQkTE!plv3N6Y1^)6r+Hnqt6}L( zN*jVGm)6x)x8IMMc*`sXh1MQS>x~nF7<^`k!XQd5bHpi`ZgR7}v};!Ru;|NtwX~~& z)%JIIw_o;Glb$qa>w+qd)`gU#xYpRd?=RFzL;-(+4GpG~BilYn(ovYe!MMk@WzPsy zUr#%g*)LrKQM$v~{x$ItrB58erpOu98javyw42CXUh+&XXq$P}odC?~h}ps_;}ojI zF2I#mBu??7$Bh^BQH{^11;A%2)+`E)6+AR@+=kiOgBj~EEomIJ9HaoDe;u1Kv~Dp> zS=wgNH#|-4uaj0ZTJx+SCBR-=n1lN`8+Yre=+8I9;Y@6Y1FyrlOTXKhl>8)LM}#Eo}_mA7Rhs|0@- zB{|2fs+AY(6M8yTGwW_lt@~^?k7U%` zx{U#%xkYyc91z>Zng-UYKH9O?Au^$(&ctl&0_&E=Dr8rqWi+lzLG3kTpe|t&t{uCE zMrf+4W2oJ!w!@y`LJZa#17uW=Y}@jtzDBMaL>O{~MD~=d2iwXnjHmW|nlIG6)xu1G zwY=?m2H<;7ke3BoObU+2cPeSq1WD0m4~B&GeA`0x9V|;z7R0E-f<~9SoFPPO)NVH> zL{A%!7C5+B;Z(0p3A1TuBJL!-rHF`Njo3EjGCRi>RCQWA2AegV?MOAXQ6LGUp*A;L z7{KwV%pgpbg?ydTMXHwD2icH#?pFJ z!}L>Wugi=x?x{K=2vLxNFdQy5YQ0R7Aq4EZ8f{1NVun#Gt=P;(nVFcubf@YBxI(+_GIYy;TYY2-L0-eil`3E;;mwk29T7erCsof_PmE>&6BCnDs+`qHx2Bec z-&@OViy0Emw%0cLO+q46o6Uw)N2{okSe7%ZH|o`R%OiW*GIAJf<-5IwAeM#F>20jS z6WS2QvY5_x+p&%T7-6kuw&@S80OI<*Rz$uX$`DgG_JC=jD=Dtp91%f8Qg-rkz}3X2 zD-wihX~^ZMYePS$JJw zRyrvdCLgpGGS^GIO4+hLJWq8Zv~qswr;@$w_A^{)j1{uob+*$$P4H2M*dofyl)o96 z-F~>lQGh~t(I5Esq|={fb7YjxcXIaz7Y$baq`w=YeVa4d8=*E>yHybA7Q;b2LIU78 z$~mUAX3?4%589J$zJ`#sh{+wL?Tm49x%Y3OL(igFJ#fb226U~=PH0yNrs+q$u+_!qE zkx47k!*IGI`MAgUtFj>(bp;~eu4xVngrqE#n@}xEhelDwm`hn+q&Ts4 zcvwgBO%!dpG2q$dHD0ek%)%i9BZT`ppEf|DkX5P}z8VA@73y`>#O&_kj-IU&bW|e- zTV?&Y*Mr3oC36BkK}F49Z0H_9x-c)1z^X~rE!?xX94Yu3r@Sq-NS%z~hCO6dQck%vyY)ghcwE=|9ju3zAzKX-eHC&FEp8Ej zz6zTWq^_4|gwt8=vL&p8kdGzAAqXc&9kWL`vqJH3%Zp&g9m5c|n_6`j zDAZU3#L?UlEsNR?WoIjUF^#6tr1l$Wj+Q7a%oM>(BRdJwBr@ANKwDjdnQb=xA|KCu zL7zzk<1$@6^fuc{wWl>b6OB;qrqLpX17f*Q>TV~&<)rI57#w=JjtS(#>dyv@G#~f6 zQP{9>FJY$aY*VcS98M*KpKdBua}00j^b(j{E9xx6-5FzN^Ep&j8nkTB(H@t1RSqD$ ztmIe$PaCV&ySbEWSv&0tjWtWVF-}CINbcE1heza4n$Pl9Pl?A$vn&X=w2NhF7Rxyy zq8o7&lFou8bG|#RmKDiqsR+%BF`1h?)$PcuRXiSVHq<&rie8SzD8Sd&42i(&6`XB% zhNKx&{tRlt(!#bl3a#`JvJ~c-1Iu#D10hn@8KE%Mb>@!L9kzF##w%EXdzwhisZiC| zi}`GWv+NEv6*N-^o@`Z{lG0x%-$R^v6vC`_wv zN{600ZmX3n7HFKA0C0+alZGo+TZbcKvxNiFQU)QI1QLoF5ou8CvrlF&qj zoo$I88_VWAV0v^Cv?wy0<2o*}9S2?T)=U}=)_I^K5jjU?o5f(ahfiFAAu&7dvXwPu zSgozk>w2=xMy+DLMTS)*0DoL}2xuI8lX;vk*SY+Cl2Hx_qfYuO zg5i4tu=Vy>Yjh38JDXfhTQdz}F>|GqW~aT!a8zmLJ3j7W+IoVT*0@B-D6(=TB8s&< z%Tsi*)H-U`su`k^XFFN!Ofm;&I8mDhdVvI5>F_zR$1bhq5(A_uhZ17ks%BHOo7P&o z(@6_AtGd0tomf9={+nbTSWXyq@g|cV**-X*Pwlvj3nBqepPP_Rw((E7~3*A_V zGRSO6h{iS>YKo1=y`X1gau;_OFpCc@h}HwwEqxI4W6-EzdxG6DR$65A8Jiz0r=t}j z11FkyeP+$y?o-+&6StLy2tC?O0qr?kX9~bUS+2jnF zx){pLgQ{Z|;y|ye5zNk(RneJ^0Pe|cVyjOvYiIevPNv;_SEQ+oPC$TWvXW3Uf;lXE z76s9*F}5ZR*B~UHCb(oM!g|Hn{a~Z*q$M8oYj3~)AB^%kEL|uv3ZZT`tq6nx3*8*n zAP$P}UW6N2N+O{sz6md4JDXBG|Z%Mz!7^qZ7PBmC9*Jh8+$yypVNCA9X zL&_3SMzdbGA0+9#!>w4WM{^j|hX)AOPCEtNTDqbYT+ZH#UfiPybby~PZMT>6eF5hL!rV@U2DCO zFieGntf)Wj8G2O8bh#jifuHZzbh6KW|k;aZ+(-8*$#LG%^|W|l1d+*A)}l^ zDI;v!BvR+-uCA7xEp3-NF$R-Bhk?q3E^RkfPjrHO6x3F_kti)#NX;z9lYwpZ=lw3) z*k+K0>JIqp%r_>jzAjfo)NNNXJlNU;&Bwx7fcV>Jmh2c@3K|`t(aEsCSVepZG5QO zKEg7?Eo`HW*8&*Q$f%l`*~*}6FP&946?cX`SM_e(o^X9~h`XDX#5VfW$WtkwNEY&2hq4sN_{h7`Xvt$Wf#fkB)Z#X zY@(7OxoVB!B3nYMGBzc)RkCo`6g5M}t=Xc?3~ayFJO;m&bQb}z%MT1u-7%Y1)!Rad z?DdNX>DHK0Kwv(_UN(%Q*dETN{HD_nI_+_aL_KD)u= zs|gc%6>d7*)-yDZn)7pLPIo>twR=4W~ILFp}neK(;c0x zMPM31EV~;3kzv1@uht&I1)D^Z+&P10bjv4Yb&HbJ+VGql$*C2mn!|KQa06yLN%KNm z>g{9EIkvSBppdQneYGW)envQ~FP1m1vfa=cGSD`Dxydd5mokrfNM){@e+<b*+m{^&)*|H%xjrArN2rFxJi4~KosoI3K*hPkJ ztz4)NLDj@+L4r_V6ludnIu9{Q=uX=0e!DYJ_^#^rOmhhWuy*9Gm2etNN~bs8O^t|j z8)i6)L7AEurNa9^DJwp%CV#zY|SUlekWrbNvZ1uWJSsABTWVy`@p-8D*bBXUN*vf(J3dA*_%jY1(QFa^1 zU9SbXvn-Va!YE3WwrgprB)xSrASnuNGyRra3MRGo;I#$fy~cLDY9~q4)ZrJ~!0KAu zHgn{5V-BOOVo1S8?KqL4ZM#%2TzdWS3=g?3&$Vka39z*d=dFf`+w<-a!W=Cs7o}uN zDe&)`JlJo*z%1Psb|Vn~^)^=Infcz&+FgcPhbdXfA=_UW++w0HLY9&@>}KdL5<|;o z{%#~d)~E;LC|Z}mn7WByG!w%U?J?XS*g7<@Dd8zRH6EAChE$l9GGt|5@i$H}qFY$$ zI!q(Ag4CV}9W-EUZ>_YgaS~F6AqX8>u9mhq>hw^0ZP`XyGicSeLGT8xsihj_&{SR$R5NEr->vE5$+l3UUQd*lFz0T~ioDF7;_^z1Gg%!;x5(kTc*a1cY|( zexEiCt$PhBj+=QGfvf?JXox~<_^M2L=oo=$x|1aC+V4_SyH3jjFP2zIfO*nxY<_1= z77)Ihgr$K&aDkga6RhTa-$>N{fa%Tsv_Dx1Ox9;dIqq7}rhw{x2tjW|xX|B~+{VATViJm5sNkz?Dt@F z*Z)69=ds-=5Jb@rVu1jWmgF2nh8;QQ%-0W|-5l9-+%#47?zKc)2LY)l^DOd(PvQ4B zO=bAbx$bXFq&ainan8bO^7lR&CHug!e)f*P#_Z+m-giup*@Ms(4@n-;RAQAoGDjQ% z3X@}+FgC^TOQgpL$kK7K70C*m_UNj$w(Uh^xv8O=$th6TEOB2W|lC0lW;MW`Yv}Ui!4)7yFC)Xf9#l zD33#;G^$#ekMPclacI!uL1_m>1^iCzJ2*f|L{&&q)Ex@ShY`m-h*fhQ0>OyT3I}*|pb;_^jmbO^7wrL_nTpc1soN z?ByEUot(xTIzH>w{R+qKC#|TG1C} zxc-YX_KOK(EkpyB;NP)#L&C+0S=TCJ_|zdJdmL0+)#SSh#F^;ml(};7EeWD8-l$d@ zVq)<7eJP(t5+Io9+W%~`U&!D%hS|!C;a*+ebTY+t#cmRJV#V*wpU|b!#PX69Nls>W z^HZ=drCY=Es5oi^uK`r-e5%F+9guuBKi9XKR!}LeYhZ|Wm~vjPff9`ij{`~n=0rPk zDqq)W$qO;Qh)pd(TYY|*Z00g?h_E0|fV`XhckWkmf2B&$n1lm^ z`0YCM!20$uwWwl4`wIa1D&vS_pZeAY(yB2`1{=1^;RS+6X*9_L4wo00W;(#KC<^_g z1QhAckC1OJ600ft#?Q7SG`w*Mn`@c+K+$LCq6FX-4ZEawy_-IKf81M&uGPdkvt0Yf zCXmqUIY>DC6ByjS-mqUthshbNQfdj+`hmANPO6_3bDvW7g@q13!Y@B{NED4JwRVmO z==mD)Th-AoUnur9RcB0XwE508;llcnzx(Jp#;-!9l~c*;AcvbMTf5bNo>5Di;d7R% zet=NI9}A|itzr0jvrI~}Ro6z~?C?5naun%njqdSbD?%`e)Nm`xsLZx+KLdAlEcX%< z%QE_w5KyI12?TH+1I3XYA)g_O<4_EJogC`++XdXG`)!^SUY{4XTDXgaO25J4I#FtL zBLgK4Jh)8Q3=+Gq1Uh8QnICD3H}r}eZi$_RL1=`ZaUEpv+v)kq1rT_4lJGOoD^=oQ zK;qosq9|BeysO{nd!h0~)p8DOy-b<(&Xia5+O}QC^i~8pE%eX2?n{}l+8X|7iR0&* zD(hKtb?%owa^9mm^!bRwZ}{R~S6h1w_ic{-pfJf)p7XdggIL9fKPj!;oJ@XO6#n@A zj_WrOLENtG%w`o|$YZUAYFHtSpJe)=ll{)%S1OC))YDNaC)QHWhVwB4X5pBg{p^%TE0JWt0CT3!}@KS(r*Oi$k&d!Bs=|t zM8|XKWaGE=&^Ws~)#RR7=(04aR?=vXM#KbsN7yGnTHWC30w21|Ma%ncesmq}gyk!) zau~z&`#qnDmP%pvSx*MlHypH?@u0`3sTJ2?U%k4zv^xn9(3jLgGE)bA3a=1(?8WTPkNWw;BnK}kSIiQS_7?8M2y<9hbZQxR9ieX;z2rli2|!Xcby$2uE9On0p8^T ztH`fyVvEISDO~DTqTj~j70lr02PFc%`GZ$E=YuqEYXUez;j%*}(gWr6#FFHRCgE*S zobgH^?6qb_ZH~Fdb#O;K{20pV(%MsxKUGD_owVR}BN5~EpC}~lJABip?~fY4ixA+A2_FdWd*^oG z{9rSSk5n>d#Zfl8#^b`yGes)XwK3vfyGRE&Igd$WMc{!|LXi$8r^)G>UuctWq-+g0$D z9rWwZwgts2BHXnUj3KWO%)OKYyRTHAg#+D9@aae17W)lrztfSb9g5CyIr8%GEk&(k z12QS)U!Tka>5+DG?)i>8dmn(y4ACI_bA%7&$3*5<8q7A>sX(TWv7YK$)S5x5uTpkZ ze;SHg=B>CkI|>$Z*YTbo+58NIUn;j^d@ai8QBd}x8n+;NX&l*1KZmuD0}&K`Z<%(g zMuFmoDM_pR&4W!Ls9nWOCR_~MdZ?gCRy3r9hRE}fXRCnw&3`R#bL5Z%{!;N>WRIW2 z^XKQ0J@VRo7tJsd7x#c&<&H99`vJdKsUjb6UMnn~O(X#O>uW&5nbpxK;N*AhLBFe~ z+HV*wyAllrLKq5_V|`euw+BPLTL1m>LTdh1RStR@VV8JkJd2}6Eypu#!djWS`F$1> zowc=F!85-Ohq?ODVx~z7G{4pS2^3202!a|3J5eXT(MbY8yF(6 zGaS&?I(*?|*VN=tf`i2!kkw|W1D*WVwQop$>b=cAAV1kw6uExQOcS4@wgd$PomHFt zPXCW(N42{HTvQB^wgwvMj||{WQn$3n>ptKe$~tkt3uJ<^pebJvU)OC2n3cgj@Y6=^ zfE&+5dNG;#=tvX}pq#gQyx+Iy|x!T#J8$1?^^*Nf=q14<$5SrTyaHl zdQM1BrVE#4EwjLv8rNsjTRGEymTT-(!e+YHAbl}dP0SwQ$)ao@%58njh!ARjmVg;j z=8ezHdNxC;W{6F{FL!t6+PzV43aR0wMsVH?eD`YG&lu)4UoH1Cr;WsFUvQQL0>h(Z zrsY&n7`ywkuw98Q{f@w(OFnmuF72hoDg+*%@dxIfR?ep`vtp|@ULDL`>*17|@age= zKC*yUrJN$f!=l0ag1jG2b{83*)(6>(3K?kqZQj@e_Cv&Nn+mE=yFRfH@I>)wSYPeN z>H@xUS7354>U-S*XftNN+(}517RY$Ic@e&?75se-p!t?;ab<*eaoPD^EC=SH#cyv%)VO zJBpEY3Dpel)RRkBOI;8T@9K5Ixh+x*z%Az9DoGp}uB8=r=F3-@yWS7cyd(t~OD{(c z-F)XZi45w2sl9DSJED_U<=c7peuBgORDjeUWe7Fs_`sxCZTd7!}DXQxqZHZKO2PbWs$_B&;p$Lhz_S#F1l5_uS zPYQ6{Eo;f8oltYVolp)WfW2Swf#5W&_M#t(YNl`;&9T0b3r+Cn3Q?%fI8Xd zk!Ca{6cR}=so39XJ1v-uTmIvEEI^Ttgiq+kRDA$GzaHO-24w9s*F{Ap?~~sWds_?m zN8jUhUgK4je523`>m`z;EdCdK{NQ)_YHlK=r@zJeg`6w|^aeD09mlK+#{u%Utw};l zsvPR+0)=2O&yi$BQp75EJ2G9#YYF#}`tkE69^k~LC>d+ro(BhnzC^@G{RNF{&8%@R z->v*}F-bg%sdMpmQv{t2Bduh9nWiP5gr_rL*`)<=zBph-SUS$KCrLY!-fVq(zuFNc z!sj*igMTx9rQMBTF7@=e_J=`#&wGuX(jC4pBd+Q5zP# zcTPvfJaC(F-X8hSXP=y{zc_ZK+XYpI$-T3+n9K|s_Fz2X99-qTt8flu+C>};D5(QM z&tF&4s9JuEVt;(j_U@*1n^p~7J7afy8PG?e>%>#Z8COuy z`^+yB!<|r9+V3>bzqyg=S+9?DGJ-2`EX`S$ac@_ z#30@TBUQoy=f>Pt2<$HEYyx#3!v@Z&%3ZGQNzU!pw`u-*iPQrU%ztna%jfqCeIE4t z+6N(WuF?wFh5y7xCP=mTK7u9e@Y91vP_S9|b9>y7(C#L-uO)rnN(Y+=d+*|5L2KQ{ zuTYErSZSNgf3?84qDf)aG=XGOXVV&MRNDG=;iCIjTqv`H0RuJj(Pb+!Y`Wuey_E1~ z@>GZZJZC5`FYB|jjtagaM#cfz!hPOtG0k~z0Ixlj#086hKqaEMhUCKG_kW+ki-g^J z@!a_C-w-l7{fxQ6(0{X@PeQ(0vo){_D^BDns&P1Vpap2>6D zx&mo;{XVnHz(G#%vYjU;;IoVqg_L0-5_|^z37*z-3A3<|DK?}x=aj~xX0eTk?Knj> zJQEKIO^Duq@A&-w9stvFig@9unf&`HdzRLCl51pbVKVC^q=IEQHkFb$J#vFrOGJ?=8=7iZwa`&6)^9QgZy0S# z^bibn4@8;?sW3smnxW_qM{{pX#en%_J4|Gb3sJA&XMZ1>U;vSN`*;6EoR`XL{0&#> zyyO~9-(hNHj6NOJk?MSpydIOI?@&ZRrY-My9F-0$Z%M_x{djqtGhc~4NjHbvR zCWi3`ZO&5xZeRlJv%Ftl(vqIF0ry5Kx}=XmffSZ%>pmmbt?hRgB3Da6c50fpmp;}wuk#c0{w8n9CzuQ zH;9mLg}XPu98aBqUDo`^{r*Bq`w=U^ocAoO>) zET!?!8As`W(A$ zda@gGRkw-s>*P&m7UP-KDr>8Hl7)*YVOVv`cLy)>DCDPJx{p@m6)WiEfR3-cO26tk zn6vRx^>?eqbZvtiwM2S{JR#$^(z`)PSbHDd9;;Fc-+oeTBBJx<69N6eUtu3b5=H?h4cL`$R^`Ylf5 zNk`=o)AsD-hC$Ktsz7YlowJ55F8e#QIq=@=?;7j+km2j6t3G$su5NkiVcTWgM6gQR?Hi`Slm{b{( z(N2bE5}BUiqlcce+-96Od)4A{ZiP`-YKz2}P_jL8I77nbQKccaj``gBW27^ly+20& zq#)4o?VLL6d4df~0Sz$>cNzha?4`AYk)RpkzGJokkn>eJwNbwkM0g@F>Zn@>VYdu- zh*9@nS?)%cU=$Bgt9?l5w+GI@XLCFZVSIe`UuI7|8+G=h>y0)ysfse#IgI4M9zwEn zv@NQ$+Ddt-{kz@|2 z8KbM>0O!DmV9uE*`1=f2R!l10F|V>B>yhd>N`KE3T((+lb)iRj)NJIB^J`OyqPvok$D4^xcX&2Fq9g4aKhzitK>|ONR9d`KFZvPE zDVsD|Hf(zvB0K-I6^4m=_!M3|S0VIseaDI;E45_UpzLaQzfZk8Vd~pUeE8(y1i)S# z2MD4W{wzU|(l0uZ7!j-_j*GYSLDn53al!?&aFHM677%lRJOlWaK=M3~3F1EKz^W~e z5r`Y!f|HMlydzXQ%p-#g46U#N_#$Mh$2NW+&{sxQdkBt*c2eO1_3#L|=7)X!!ksK{ zoNM&qPYi>#!yrjrM)Bqt9p_>1mwnA09=-HS5LCA`i-+ki8V&vqL0B5DF<}}*@@xqB z!N~!vn7?OLv?er*^!KdTO&HK2eK$UTSQ>pt)CNB^o&&=!tN}rS7(eL+@LTphFeCyf zHZI@NS%|kza56&D68%xWg|jWek-i*Ktf4un0rpD1sY?%LVpWruFDac#i6RkYM>q6( z*??Hp#8;lzCY~uFSTe#9*l@J!8VfWa{b@CGA5GW+qc6LTmo&Sp+jh6u+51QOC5Wcxkr2-Vr#ifgVoWEj?@ zS+zgx198B+9(uL2PRaI z^i+#CbzhQ5FDzs~(|EGRfSD^n0{4?Cj1q*Q$$dYE7Z3H~0h(I_j&N;UHIvJ(GtkMc;wU|z6_o7 z=BlQ>2ziB(;UwNLUFDlQp8B_#{sw%12#PFi{GoDJ+8=()?|8Q@S@!lYXqs(cWe~_V zF7|x#4fDF=lB0VJl_G##ZwC5J_-4*PfD}yP{S6-|BoN>7Z0atWxYe;gUOXd(TXTV+C6nZD0!4GEjih&{P~aE3oqsl4{?*KW zRb13Moi2Hzu0Ay6uje{3r}d==;&|cIt@q8X4}@z%%c(kT9UoY40VJH82Np1-8$+O^ zGXH@gtE~i@VSx42zSj!3IzYgAR8^!?eW}wJ`w{9f1r3(map4sU)21i@ccHbUR2{9i zx2$krzOBsXYbe{_Gx3*v=h0!3fTws&vP1Ftmz`y5d=<2K$PqchPWZII1(^$O#u&4R zRY-xHkWz?F2`~2@>ljjVuGGwZvC{Gh-Vv?=dB83Hkse(omIck5URmvHO@L z8^5Zkw~>0et$G>io-hS0Gw^+6sHF>}p==bopJKec+N9vjb&x)WsPQ91K+||?={%ij=NRtX;Mq!60_jLc-r`oKXBX)8Z$ENx% z4`6nSl$uXF_+3*jV5pV|@2HdUou(hXe`(sdo8+q7g|9HXVH7Lb?Csh7>5yQ#^5E~p z#{l@4>>@T3pR8v*c&#Za17vTrrMu7SkBTAT`?)%^&tyEp0718K_wH$0_*3hrY{I`{ zA%{2Mj!WrWhEDyMeV&GeWEtYsPLrn2Zr*|yB{ZN!x@O^H=ofG1l_{w#s~Ev=(3K74 z78foeOV_g-9O`rDFcvMKM8M;wxUCtVL&EY$M9qP;2#QYIWSZ;x<{}W6!UuLA*5@h3 zy}ypfEaPa4y)(fz3@?zT-Jd@3dVKE(B3C33Yof>yrU^TdmA9CU+&=*_PFw8rixs}T z6g8ulyk3`HKJT4DS@)$h{yfPqeT8^dm?+}!Mf2s%5kA*DGt|R={qmGP>ecdn=dCOq zc)x&{0*0xnqPRQw0{hMZckQ2V`+$x75(zrjNuf){E4smEo;Afxqyag@_M4Drork&kDa+0YcMw|R2 zhSdSye5VqJ;Uwh=mN4%;LH!QH_C>+rIZUPiE7LPU@(mcP_qoxq({7Ty@~5|faD#3x zuexkOTr4~u{#v2NZVNrLL4_-4mh(JE5go|Y-^d$6PNoP@K^;Y@WOK;w>e9t@=8Ze zl2ZAS130w6*^^}$Q3TTdVk=;5nE=T;Od2;?v8?3eH-)*q4B&WzX^ge`e6y z6Al(@<(M|GSMROOOZ+o;o}5g)G5KJOUgpeeI4wvT*70DRCK4-)DE9*l)`Nk>dhsR+4 zx83@XtKS&|?6p<9h8Qi$2D6;d(q*>Ui)nI3=+LsKG|i+70;H51+am*O&01Gt>)5uE z-j|TcQwxFbDfj^1D?8*r|^#WwYtw65_6O(RL4O5=A;AyS;aE$pY+sV zH5udj9Xq4T54^vuv?RBs#*VxF_t`bABE3rT6y+4X3k6bnvy73*VcF!sy#z?LIndir zaHJ@%+Wk@0Qm$9wc{Z44@^rQd>puy{w*Vt~anVtafrR$kOQ2rEr|fH~f3(DE>CwtT_5>RvoY^~v2t(t1u(w9<^wK#i zoG;#;v7bs?qFk1=rwS6miu**Ts0!^Or~X2!F=zklV=Z^=93MueswcuPu)vJP9;SAOaR9ly9S*PrO;* zPM-PtNH^4@7ssCG(*Z8wg?+_axw8AK4y`-3E-!EaHb_5+8#nO^&Ti5lZQpAMUTWXy zw47YlPMg%@`Bh{Clt1!(a;g5s9Y?Va-7Q_}C-iOff{?8FB7@Y_V014(1glsXNeesu5UcX)Zn5F1o` zjzXsJWck*_zLn!q$*w{vf)9RmWy*u5^Dty7VJ=^vj3r~#o%go|EB>fSqeERKOngOq z%2DKf5*fdVC(Wf>^OY#nP0htCME4)Vew<~3lsXxTZS?V!htLHmURJWCT*f5p!&A|= z!oudx&*}yQ0_K2=Dwtz=51~DC97F0l_@yun+FYPi-FKB>+%Oy{9^KR|bxPT_@$SG8 z-ACigfQ}fI;kw`CYwghHPK^fA;WYBG@D`1UE5cjWoLi&;8;*q9zd+V?<=Xw`g4h$k zW{(!ET#RU$7zpQFjoNM7o~VAV{S*E(^3T=s6lf`V=1X6`ZgE1ry7NCzVU#}#FW#02 zaNM$BT%b$%i4Bn*nDo*d{3qqWgdtJC%4~V4XVtzhqkZaU{Uk)ST88?pQ#G{3A`x5^$XMWDJ2Lyfh*{pO3;F;~$`w9Afr|9Vm7yUv|FMa>}t zKp^!^i$rAHvHSBP*!IP$EN4DaCZ0j-d4z1m`-NN+CrOT)iYM57IdX7GidW>XR!>+t zRwwVAnI^Yh{hv;kJh8VV`@YC*bqjeW!3aNuzV|4sO#lyQ=1Srz+52x~SL%W}mn~S* zM+jp`hz`A{RtSZ&QKz(RR{gTc#~@CVq^mw*5KDg5Fo5E!sOd7e3X2{u+Iz&aa1^-% z&8};$JD73rIf|1;O(Q>|MZ{T?Rc`kYwQUh9{gov9 zDexw0bs0Y4%WI*|lAtogKe6gBdsQsN#@xMyLg4obVQo4Vn1V~*D2~E3ECNO=8%k8x z{npyjt<=g(l|Dr10~b6Qf`}E1eci$3vM)n>?j-sllHbI!-4g0HeC)f}MVc)>~i^#YEwK&bw@Jl>J-kN@O@6LAmP zve>{P#<%5YxX-8V}K|jyfk0#yaDq11HB*ol>$-I^>@F6`dM$-jNoY@M6O%-p^_5)KwnDsa zYd4!&mm}lL>32Oxw5<{W_R(F767OT(@TE}Y>kh8<{6@ZCbK~PwFl!V)DO6}GLNP)U z@+KhzUIv3)RK<~3=>R(0HYFUr8adQafUMOi@WN4Z>o%Bhh9~goH>`r?&ZjHwa^wKN zz=uAjGICwaXeKt>FB#7HDK}e-69Q0+-%dr*lE}CnyY$cH(Ko!LXx38%^2F_;dX}HE z0z2amh~8Elpt*H7CQgeuJ8v=IeP?yjIsGUYGg`cZPTl(N{Hr_D?TpcLPzQibC-HVFD{-z}Q&TXne z1vT*7kEvO^`ZG>$b-)}8tm}S$`u=KK4yX89)hZ}$#kR2G-z|7imQtczs5o!+f4?{r zx}jL@-$&DMy{7;8u+t8geM=!fsImqR_>)=Kn4@t1@uxt-8Q|~4XvYF(*V%5GSDFV^ z@|?{7cxvG+W>JfHlEG+`2JIS=%~#hB=!cKwM~y9j;YXjn7TbdH?b!qR*&zDW1*}<- z54nY-$_C(QJ0yh4M26rHMz=O`sE#W#Wi{)e95<=+WAu#M|Re*|$g095mdc$|#4 zYr>+@X%TCX-$|e-Dx*zZU%%#}Jl;(;-QSssd2xQhU8;+P8~KVSwbv88>gD*;?zl-D zwyf!FPwC7%5`b;YT0o^`PMSn*#TPi@%Ek;xgfx>#+PBcqS(iWK<$}Kr|K3|cx&u4akmdx z;Z|HsRbdgdK8p$tT_aSEHym77MPySjd}gyUyL6>0?bDNy$$;qoGh<3hTYPR)Po%9}>Q_q3w-$1zIfGr5h|E=-tXYcZ0{1PF`2Pt11 z(6CS-dyc{;90pUIUypXP>ABGUIJC^QZd~oc`_4qO;G&NTx)|0Sg)|gZm02KZ?`guv zYlOEce*8XA*vrta=~=h-7nRr|d}K7-FnpgK2U}yT9X8@G-E-D@>OZ2B3ObQ18}Y zBxH)?t8c-x>qsUYl;3599fmQ2E#2CusSwr-di&ah24!Av7D!-NXPG0;%t$4bIad-* z7;FzpUgv-u)!72HU^q&A_L{so!g)$+A!3S4hJYX-*G1Kdmanupgclg@xyUgcNZ8Ii zWlWWgh(Fsl<{*(n2Z(Xq(~>D*pa=oHOfjnpNkRh}8&bzh@7myI7jn~*B$GAhK-r~) zcfGInXm8%B5fO0l^+hcsV5pIQ1OsuE1jhEoEy?j*e6Jf4&xMT= zzHOS!-wg-J?gMfE=1Q8S`ykvtq)t$0ChM7^t->OxEC zh|`%nK|1S$GsM|=A_+?%tJL!RdpQVL3dP*EpiTIMbDR4(MaWDyB(%Y7yX~S15^zd> zzj}FjojXr->Zis8CCW$hrsORM%?#oE>0}aPhHS3?&W_EkoU~^J0Q1LZu;MC(@Ij+( zB$Que=S5`lqWMugdvqY9TtP5yzYrh3uAskGu-ZMn3-0h;r&7a~6dV*A?u2uU*>L%L zuAEiU*)(q)$Q4ut3%aV`Ym~ycaHtR;uJJ+PH_im_v>K1!KfAcUmzj z%>0wux1WX%4)tQbLcN_}cw7ASbDxOuwZs+si8{_}}Hq^_E6 z1KFb=jh{mHuaLEem&Ve9hO$DeIn;U){o$5*w{&f>ta=?NzMwLnr}LPlwBk8MPX-#y zaFbh*5)(lPuwFXEPKdn9kMeY#Nr#Fo3N$qv4=)1AI2~M1ckq<5kv`vONChqV|2c-+ zB$3qyZAU1L$KRi9aT|ybIOb)Za1HMrojOhaD`1Z|8|ibApZoB6 z^S?8Sy&6l?v0A)%><(q+X?P?#uK=hpH69$cH|f@~fuq_ifPiQ@5US*$M;%eY{!I z{)hrLU6&7vTF4DaP=0Mg_to9`RxKr=4J5Y;8hw|)bG(kczbIcSW{P`G1)$1g2Z7CV zc`hJ82YiXeEkjf7RRpt2ut>QF!9O7wj~~|9+MXZnt9=L~>u(LxT9w3j(H5S3@_IHvy9c6YXWO02`0(F$7fV zn~-B)?Zu0SCJ2I@yaO&}0Jh$6Fbf6{g7QUfkcXRhJihXAlxFP-xj46g8goSO>Cp!R zkDOnjDSeB)$i|wnPx03~M+!I{d>jH8Ps5<50~m3(uTUpWa`*OgQ|s74mF*qF3gXsl z@k3pDkWAPgyGuY)8~HTYHXirTRzX@%Y_x7k3sc8x4g3&U;`MWy?UGO9WK2g{$;wzx7$K|>pEL_ zJ0DE2PSRrkpdu98yPiqcQ)Nj&Y}}FCr_0S#FO=brOL+n_M|f@uFh>z6eYD2hCkH`h z@=ewRB6wG?KXCj6ZrGJu9i^ z+ULjvEQN9ydo#_t0$P%H#v@yKsh#>Fo;8Pcb65mpa3N?r4{3)+Ox)a6(-2s1?$3U= z5f$s(t*QZw4a0*CLF$g3JDx%=f5bA4IFj9_&*Oo}pepZRAa`u95}ssYyZL4k#cq2R zyh6`+cO20;dJA6=wqMx4l+@0z#3W4+TQ`Ix(6HxDzN2sQeCW_JO_S$#(Hx##d3O|< zl>dJ}B+SqQT>jp}g*J%XAP@NdJO~uFA_SLx$Dnllh(hN2^5WUa{EUvzAlVwFSgF4a zc10F>YM%K4vTt+;AkBmzkx-e_(HQ>A$o?XmeCS!aKbwjoA-~19H+b?1F5%R%LLm^D zmRmP}Js5AwY9J%%82YB6`Cc{P))e`96eLm%Rdf$BY7SB4VHR1h1q9pYm5z$bCsoL( zUP;js2pfNu;;+FG)SHVY1qm>M6baWjMGhDSnS+IKaqw0Z{Dq;CS&$9Je+ck#gSSeK zdn0dV-^17|{&jaHyRIT>dS6d*b$d|@bc@jpS$R`p27}FLz(}=Vo(D6-(|4S#R907S zx>vnuTe43C@JIYX#5d?1;)G1j<28Uu#sroC_NH^s_OGYVru4-W`av2GIDi*plaB+h zl`)HIypD$~xexco2{#4qeQDO=n1Nnn&)%W{7SHtkA_E8}^XHWIqK@X*Gy#L^C;P5F z%z0*|q8zMXo!QuSJRgp?V8IFE9mj1&P}4M_6OL~goF=K~c_d~oS4v}k;M1}n6e-bo zoO7&7wngiee{D{-DhE1y%pB-8^PD3#$vDjxn?RLf&P#YT?@*5v0RPi_lO^mhb27p4 zCBLGR_1JEg)D%=1~4!u#@UwigK| zX?Y~$qNhOg4;bu0R>54of^2zyn}cm5phv!&do(*VzHZb85<}AiE{_!U8Z)|ZC{gFd z*QVGqc8TWSZczt*w47%Q3ai6Y3IVCfyo4;%Hs`_zK>C1y5T5_(UE-2v9F5; zL$+ofjA3e;&nE+&U;n?bJmjqqJ|{Hq%Lvjc>=S zXyL#f4{|R&Qzdv*XbWMwf0F)jFhptOzG$;?$rJ%Tjbq+T6LkX;O4apRNIZIXvg-x} z^z<6h1UtMlXk|P;r*JWE+tX{H)M$hL5+b3+AqHn09xVW^@5htL5HWre03r&$EQKQx z9ayNu>ZY=gP68gF3XDbT6iltp>jwGlezME`j;X^X6Ep|CMG3y0j|8n7?&_FF|I`_; zm&I{LTN!wZ9m70PY6+H=Ki0axEtOr0oeRW5hQ`1F)FJ#cE;Vo`V){`jMO-@OkbU^} znHo1LdL&qvke5d4A7gM&_~-^82(Y_c*A)7B!MepDpWYgQD3VvXs4UX2u4Fe7de?U0 z@p{n<&W0~nHq=_eZ<~3WmX$WvufNwt@4RDXV z*3}9r8)bU{wW7gvd;EjYAB%|fYI*g~AxE_fETya2*hgsz(@F&+!2I#We+0B-$i>uN z!v0pXk$k>6^eU6vw98X>Ub;~}-b~^{<`#^w#<#<#^7RnTwf(cNpH64d?QA)q4@Q_o zwWPJGE|5`h*WQBn`@D)X-K)mfy8V6}>Rl6Mqs7d#$<>IMED~^w=ME{s)beQ0&OaCg zKi(wB8(e=Yn6M)C`#ddESn=pI=Ca!_dX0i;_N>ONHb|!$tIZ3FG=%8*>2%GV!ob*tDK` zFt9rtHsXgnM1aSwJdH6Ogc&;_ujNz6H~PjEE5EThao6}qsXnpdZYvnLrR1&0MsvJI zi1Z+z`1nV-QZANtW+$L+aS$D65q^Noz%_C9pkZ$GzO!f%!U4voT_yM~KQ-3wUoOx+ zUT?kk^4-OJ3se^tu+=EoY+6DrkqG@eqFIO=MH4NG#+taTfJJmRV}UjceUL&ywff+C zm^DLl=Gbwh=-pwKcn16N(i6jR6Q$sDzL~In>tk@u@LHSZ`08jhdtigo4^vmvgn*ULQ$qguOSko z+9NI?l(%#HIP$GbbyY0@08ygvyRH%`&rxsn4N3Jr*k^ju=!4adl`}({hl!K(Hb`U+ zQZ!H8T5o=5B16r0JAAB3YREYY9MG8JRRGS04@)tg`Ph8^^i1)l_u*Ga?Hd5e9w84e{crN zB!a>SOkYTlLKW#MZh3j#2IgIZ^%8GdL=@&A%$TsUS5j0uuU8i!c4Wcr8&KcU=$NBA zWMhJ$I?qzj*+*ioma!)!CBHW_kYB7dOODy>Z}GA6)=R2GcPPY~>zaBR`M88KSYYv1 z-B<&nJCa~}nCmxM&45$N_W)wf?kx{;kIn+jpS)Y(Th0b4BLY|z;Js<2qwNlHMz<41^Yk5#i!J>mcgbDU(O-I%ZOKOWwbhC% z8A#bXcihp_EUfnRI$Q*0)xF>KpdK8jT1wb?CUPO_BN$pzS+A2R<6`*a`w-*Bv}s^{ z>5n>m2$t%do&<|?jf>SzW)K%qn zTVB~X;Y~)mUZTh$(R*_p7unif&8=R0qOgmKGl$tK^F}rkAdI8?ACr2Ja6DCdB3tF& z+QNY@R;?Kg1~HaEw)5R12pSs?E0CF5U54k#IO#Ay8Y*K93Dc+l#uR;Tl0!~*{>uDkUZ6W#V z2;3Yuh&s{tCvT*a2A#*!aHX*G(+g%+HjyQc(+2Ur#|2;Hj+|5wmy)y2jHFa*C(F(E zghx270@Nz>Jf}J_FpDw{Bco}ubgME#YPp%H`>v+x6e0@I1LGEPnnQiI5rH;*-MFpn0YkhLvsmzy1ncGNcc+Q1r1DAmD$BAa<-BaplZuyt z(B=B+mqq9k97)A^Sg>0mUQ(L^Pu)-f-qAXa!&P~+gVUYO#Jhsex>ARKBKo@?n&F9r zSYnm&&KszN?9K`(?$I|+OqbH}VLQ5z3j3t2fZ_+FP#Ozo6BBlV1R9v#+YKXJeUg1l zJ}&J}&TaIab$oas<(CdeZ4p1!`q)+YT4YQKjH&vl0R{-pSJz%K5bc=?fIaBzp?a`& zBRHtyglD*34ii<`RE>oSRe%$Cf}=yU9?bPNfftBG%A_H9OnXvJ6jEO+sOELS zXL5Su?v%q|>Ra=8#-(!aB@Ur5rRIs0%x(Tf`SV| z$0YS1|OlH^COR+=sAih^!Tvhk|bEtLIrsdBNH>? z>f3=#4Sq<&6@AnY9*7s2w-wNSUp|A&tR{Y;pSFk6+j~_nZ}OJoYd2K9p%Q~4V$8S$ z&Ngogjot8^hDD}3hHF()>Rcr9L12%V3Qa1{E&N)Mpucy5q2f_Cgz1U5dJa>A9!lM3 zhwTy63TWQ~DX5{*@qVZG@D){<7VMAr%FJlW6Xu@w6-ATn6q^zqBB=27Sm(6`S=I@j z1&sn)_YViPpHTr8TWaR{xD3EEZI(J5gM!Y{Xl_h^z`GQ12&BVAdN@0P*RMUTUpH}T z?nP1j{Q3PG{M)ba_}gE89lu3wRrEi|1vk1?8}Vv5G{$8wI@ zFIhJ(=ik3|(Nt9Zk7XEtLB`|HGJxr2=>Dy$&GO~nDyFI^|ET$jAGbr&{HXdAH2n+m z_isyfEPociPRfUV`H@Fm)t`8c+XYPXC8p z2OXNnsQ{cm;DW&PjM0dC<4#b}12{>uRT3dciwg0FJv z+TZ^1kFUyUsIz40hb5bT`^z`*`gN(gtgZU&TRWoTF#r6H;|NB42SwzcMU|CZ%BXiiOke#Z!u#2E?$p}v+7i2MCp4|@Oe kJO8iK(LZCu|1CI3Yv>DbPvN%f<{y9L@!Mbi6P*+gFr$l+Yi;K9(^^Z?{pZG$fYUGlB>ZxwO%7%FU@`S?WH7PnbE|=gW<%&}Xtjl-5@1o9IJQZ4D!|B7(*4_ERB?cnq)fwXTq_?i( zP54!3-xtzjckzQz>;y!{ahNr-oAEx>kpq}DAYD^*asq*eLr`V zAC5K2RJxillP)a$mmnVcujn3w>X&Qom)4DuRYB){O(U~M+XNn(uL4Id*Ll5(ZUJEL zum$wbO~Ou3Cj3BghF#P7%x0}EhBZ#0)y67jZ)EJ@DRryxYnRPS``U#eeUuLU?U4@Z4R!5;l(s+tk!Sq3~dgm9YNz2@NzceIv5#nWh3o96`AV#i%5}}G1GlGWJ>$4Q)mwQ5B zF%jQ~d(V{Ekrny%eo9o*Jqn%-N`?~NWgQ2r>c1@#rqeV837(5F+&+up_Z9FbQm?g{ zYE)u68deger&EJqNbc4K-y{G-ILRUBT^OKv73Oob{HBp86}Jo2JQJRVLj~JI(^*N~ z>T?De%|ll4)4g!Q{a(f=h1E+_wS=lFJq*m&_*BUk2dMT3N?onMeUoFhEO-=d&0q%)X6KO=nx zJoHum>8(&W`zgpD6;mv2u)hvRI=E2=H{Wugc(eQum*qJfvIR4k9y3;tj6>S>sQ(>? z?3yZ*RSQ?iIyFAuSrMhpWzwmdos?<|--}X?WpdeMR>EM{d6rk7TH~BOUzw^-TqiC< z^NXAN&F!FZyQ6%Jz&K1K@c=ORk%rdK0H@rtLWkOVf8ZCW>ic(meG^%I`~#%Mj+Nfw zML4cfWy;!7NU(i=$EZd_lh87?R?m8$19JC89(GlOSGS@Ld;5=reWw>kf8}Z}zU&g1 zuN6V56u>9KwibQ8nyIY7uB6{u28V>@f8?GcqV(2k!XDaG=mO8{N2{PBJ&|s)MpK;H zz%65|$?j1KlPatd|C&_&RUjF772Tg)zvS1T=(|_4N?ig25j}|gHA4aKFvMeR{m%{+ zkNL*WQ7`276u%1I%)A(%G^tML%{gv2a|B1FMHzCd@Ba*$< zdlYlI(#`gfmdb%HDw4AK04cD(6m(S@o~@DG01#5WMdfrsr-8h=y!{EKA*HSaIt(mv zZjQpOtRd9u@#tbOsqPt-y+ROq!z6Cr9Wdb*m{A^AWRf3F3OY{b76`q}f0Ba7z2BGq zXn*wA-cvMZC0&OOSS?!2--&lxxbhUJs<5k@yR zfZ!tz%=pfYT$t_A9aK9+o5JqFfWOndz)L`gM|YOv$m|aWt8foRY`ubqTUZ|88Q$@A z*LBwRo|>*akub_$)fFAss}?$#!rU|A?CMA>>qPwGbZ-1w!tH4fon;7RfSkUC^0sSX z8&d6roWU>eS>2?$z>|RGvt%BQEhHsYap=HLvK6RCd&a10;_$tgL_FGRq;&OO6)7{D zGhFmpkc;WDDu67)CWjxnbz*s3pty-1d1N|X9a0mHGKaA$K(B$%!at}0JlKkPPR9pP z?{?@5@$vHTO+ev6-@Yy2e}s1g=Q_H4jC`vQt=30NN6R|As~DSMcE5~5i7(0*($Px` ziK}q$C9W|9DFz4RNnehiMadBqLtyNx#6qcef&q_uTy3Qns~SDu`M*3~hF#y>P$cpD z0m+ag{2~D#t6T~d>;{WYGc#5Ap}DZ>af7lt8}5+#Av7C^Fm^r!Lhh5DNhiSc_9k|iZ7`4b6!RcY9mAbo(Gtd2P6x1?W!HHBt zFm1p^FUQi+6yp08(;`ss-oda!%Z;Qif?>+Mo&|8$FVL*Qce_?cI@Tw%@6G%^(G{&vJWUtrHrWfa~n;tp=2 zbr2Ye2#!jlEQpj~nu%a7sBsk~tFd2D=oV>G&aT}L(A(`@g$#|GGo^LGcTdX8BCWe4 z(fsQ;Qbv8mi=QN*JzWLhzgl?-cJ>SrY;|dJ|FOKVuA~Q{ZDIJ0aMjMD{D7eg4;E_A zcDqhcj~;d6ho?&tzvL#QL&tD>Har^OW-P@6p146Iv``1t$NF0?GDvt2r8jImh8PnK zod68|ECuO3dT1OIRn}*Ar>OoTT!{W+m76nWnOpYP@ z*K(JAN#lF`*e^4*}NDXGm8B-r6OKi!w!`qkfB=!Q4FYkiMLQGwruNp z;UhafQ5yZGQDT$6yAzMyU2oYw*U`L;F^Mp5{CI&Jw!S$$^c~+Z)fjFE*}h%H8#Uu+ z`i0#h{gx*>?%N}p@n5~B9=CB&_p}LATHcGY&I1)g*asECJ4hOtBW+G7?Xw9|hYPz# zW(~Q-ZUU!m{)LaG$M}t-6v?reAh|h-rCylsYH}k-m;D8v>HyDgZ(Fds{ghI|q(*j) zPnqaABa_@w2w_-uwCPB9t&Xj<*`%gHW^FH=jfWIBqCbgQ3RvVrWrytSy=Fjm`%OP0 zZ|-FfQ)KudiaB!T0`Fum*r4M%y4(=g`Ae4X{a`xIUKBg`2%f!HJnudz`GZRk6{@K# z$Ia#bXwvwde3_;;ji0x?>m%2~cK!f$p!3uH*9YNcp|3Byah*j|zmdg!TlRbPHD|x5 zJUf~AFeqxl`Y_wWz{(VgFMUA)!%R@Re;PQbY42w=HB)emG9GD9dd}#FBPA}c*`fM!{WXc&ed^b@9 zH9D_?QdfVEKk@fjJV6g?9ktHW4-&T3^)m^r+dM>~&glkMVLu6kkaW3tr5^t{{WhLg zohKIaDk)!&9nn}TMOZ`_?T;_P8Y>uTAW{`F25IqTo3T-kP%W58yNCB>fxm?_|H$=? zH%x2Gh{fbH&*`_A8LjkSh0XIl$#xL%i{I~)VFviGgz}H)tJJHRbN8l%E#v_%;b7%g zFyF#;v1%(6E>)SZL^W$nw8)+Nb6vMqQNs2eEtIKJxtjpP``=FexyyAcC}Bp@rpi=m zTv-{@hr#z2SCURY^6;MP`t6r6BI?Yh2D@;xSe#YNH~-|Rz;Q|{gEgc9PYTz(+q*+6 zpC{7{ycPl>(rlSuw$!_?w(iWFyBtSKxMSyX(wy}Ko;TNvzyOB0 zpbK24l=n!Qs5LQP>8P>V9>w`x#CcuBxn0CL@wVCFNuDUt_?v2u8`4#n177kDuF#P> zrOte#Clz$Z$hNdHIkUrpqP5C|pdgR|H`f3u#TpMT`Gn8Da z@wJgHZPvKB)+{b+>?FdZcM6h>9w=Hti;Ni5BB8@^N-)f5E{7s-!w+Nd;tiLVUJk=% zBv{k7aJV{f18Ir$gPec6!7{e*z#aYC(iBq?wBSG!fw{=+L@QNC3;e*Vp+vF zHH0qz!`3*Lc%gxyPbeNETtkFVc4F`fYjJO%Jmgj-CpLtgAsBAdHk~h-@LqOe) zV<=1GBo;vpe0q{A>$yW*#4_YJIm{-}P*mhndC$E#st*OD;fD+Znqfw+JxtlXxXR=PIvwl@D9a7$LJC_ z9~$>!%Co{1;Xms8ZH(Gx%YLRQsQ-n6({FB#h8j$>y*8V(=EMl!vt?so@sVr+D| z5FlbCVdg{|4i_5_b?B|_cNY!~7fYiCo!AE)Qax!1gc$?86;sJ_+6@@$&B_M)wSn#j z1S#~UbrLZzE-$6yOSv7jL&+3rWs(N0`m;s^`kEb%$REY@iaBFTRIo8)ZfPMssLi{h z@Klj-E!n zd5=n~l(?I4-~RnG?Hdqcr^wAFaY`ER%}4%x!+g>Eoi4BR5hR71Uro~y!I+~i&yt+zZB2xx4s-JXit z-4k^9HPB)=KXp3tZ6HYd=Y0+FU@7^%;s$pvl|>+ouCd+xJ@AP)a4h7b){i^XSEd&P z5t@50y5<}tRaD86vWjY}d0&weG^2WJWUj)Tsx>w3fpYP|yg^NRNR=Dz`7Ib;Z{!`; zEF#ub%PK+c-3j^pvOusOyR5PKWq(aK2_tl0%sTh-#*PSs$*7RNO0fQU4DL7{-SmcDw}+G*fbX~dhrlKT=3k2$El`_yeTD+}h&tEJf&^-)}UdJfx* zkr*En0-Dyp?z;wO(y6RUQ>N#sJl~msYqR<5OC@nnBwSfns)p!b99|!U{K7@hJ#1sd zH*8v)4~a;ecF9l_doWd^U+~xRK2Nai5S`MH6g$N=`p6;;X2l|N&9ZQ~C=#h(3|lvW zK`ae$>5}4T)b(1^VX3FX5|g!Qn(KcDm%r}mr6_g2@~AD%>rnwKB_3EjQx2F$)ANiN z41ffdtd0>zgN0%K;!o5#CX8}b5Y4_AtTz>vVG8)u3Xjm~alG!NZ#EsSwpFm7T-a4t(h@=?U0pHQ?bH7|BQlP>hN60j zBnm8w#(B74u)@Wmc{pBzH)U*=ejKVhnso94t*ylP0Tn^!AOT!Dk_ufI z;t#3~R|Z5L#jN3_A0!K^Ve}G$nghA2bktqSwxn9s6X?S^alQuNCkz(7$3 zk~~8%ZY>n&4fdc+!iICD5i+ydsDULCyt(u+$DZ;n!(p_IoaHbA3Imbie-GrDZpMds z6y9r21pH1j+9B^G^#gTgNP+Ch7#4VbvY#N8L+2M`n_+rQoX<&78OXF#3D7l@3CUd*2SR_J1m7>i4vc2KtpR` z68%J0SZ|f%u@dydq=d%jvC$M_V}bn65#L_lmzDHO~yTw%SHj; zn$-<)8Te2>^;?RpyRM+JL(**n!j|N_gQJ`MKF}~&CU_XI^MT(5zUr!{q9)Fsn3>uo ztjE4;!>zL5L*6s?gDx(WxB*jLpsZ2BzXDmN5tAF4E=$0pmS%^K4ZXF7o3AkJ>a&Bw z#}Ctdnc86hiV-IcY&387^Wg#e8pX`TdGk%Kmt_`;tB)&d!_3EamXg-7Jb8QCe;B^l zI0x2lncrM4essG1(K&k>B6`A`X9XM9;9rGocfeBLK>!K7-hk1j_prG|hG-UFp+B5z zcV1$R5|5`yJ*Oh}@j>=E5!_0W!DNk0lFbc`*TH0cLeY`Q@@{(dzt+RT?9Q#l)n(ah zHQf>Pg~pz%W;k-{9?&dGv9rOIY4@*@NUL*glB=dXUTz#OZr|fb-2S{2JzCw!`JPI5 zZdHlbD82epw)z~rsbB32V~*GL_1*odjO&V3`b72-+Dmtw2VC=har`09Ok8^N>x*5s z9FZ#l{@#(;CD!nd9Tc4{v|Sw)8Ro~4VOfWld2u4$<#A4P2I?3Q83C}=M)6V941z2& z_OyS4Fbq`j@&{?u?o+IPAgnRn3+>X`*Nmh;=2`l#ue|2{1VJiEtq(ug9?~U^O1;&2 zr`dx24{;O7Sgj6eehhbd6*613ZId$!fWQn+mVIDKiIkV-wUEzrWMv{B_{gT9uF1d> zb(Y#R?>8B-f2FeoXlm6^*Jj3o*;w3Y@^eZTbEHAums%f;?m#0pWSL?|jE$|^IRUNg zJ>FgpuI+ak9_Z@)m-@y2d=~eQdj%O#lGaX%u5i(0p1E+;2VQ@-Wc&eUyti*^AMBZq zSC<;0L)QB9X|D%3qWGK&vq>XU;UW<}`+gRM?NP#sn!Tu8BiplVie+s6U~*(f`$4)3 zAp=Tv-$$vgo5Yiwz~%NKaFwaV-V(@wpbwAjD zpBz8CKLM}OarNPo$ADLZWmW`T2-{u?3T$Jt1`Tawi3=+X7g{5x2rz8JoZ3?B11%l= zUPDy~xhQ^#C5qz27Hk1mQGA_{i&T1s`1^oL=w`bFbq7C;zK3J5G?${9%dw3(Ve89v zGDuIljs)4aTZK+5>vP-0tcHt;&Xjy%k)AYim+g!fg>A7#SMxIQ1%dA8EqqcK;=MbruDko!wbZ6|M}TT>pZSt~Q~+ z?UB=uZD(6|K)*z2hWo4XxB5DBBene|x8zAfSN3w;Yr4K;Ahe*3W8Y?b_Mw8NS_l=_~MRp+3-7#^#L*(S`FKFWEo+Kxc zQs|#ZrZoycvSl!6erNQU+h7+Hn3GNxqOFt8b?^u>LY|gD?K^U?3iQ6-D$tpQ>~ z`7HY_5qOZNOXx0keuD-t>RbNYA4E_&jkWTz{qS48QASn~#=H1B3b4#nPhaWX>URnM zQnav+;PIXN_v3d==k^AJ$ZaJXZUVQf=gG72yC4&W{rDYBL0MuvKzjQ?gtTn#6oLXOu%=s#yR>LvjCK9nI;>o+6DuC`h(sEy2%7$TmGnI3P4WtI`75 ziriYtc2?~g?s3s^zkBmoGaV-pTu~sn0`WDLSI?pP(K2*_lo}q8gN55Jb}^*Y1Bj#Xe_VGvXgRBZ5uA z1y@QSusxkmj%;r~0EKgEfku{4!QT>Z>l4I6L|*#-Fnr@GTK)A1expIvPv{3CCAdZ$ z_yB4Y1&K=J8&tA+(wNiJSxXkWQi7}K-D9l`eg~+uWL0*Eclu!8z>>e`!{^I#wFZ(ZmQcKnPyY2$WqlQ`9$*UiB|5cs~5NiBxc}N9?22*ENu?xX5?2Ti;z({;bN* zsCtJ0JTiZZQtur;plv(-j8}inASTn^dBtn7k<_x}&LF#Qn^ihFxcO2%n>p6MO*q=X zrhhk!*xZs$2>d|Tr;G2JB7=7qBkeqt#lfIEqzRo9pQkI^?{iasY^NRaFna)(!pn3UE2f&l(!4wCi(mpg*XZ~RQhWaYw9L=TeeX*H2=$jJ_&lG@xF~2i;2|Ko$hUW zGM8jDDjz_ZrA^62;wE6AM zKBi}S_sJx-*LUu9-LJebTfvBbf}weDCSrVJRW;b8G!@Xw1ypQ&L<9Ou1l7-+k zVVv27z5V5S>3QbsPn2Z4i4fW8`BI@U8GEH zCV(j+q)kx%Y(n-6V{+xRh{#KmoiAjuNRz$Gx-wDOvKnS35ufiL-PHbG+x5^&Y<7>~ zG=@C*R(et#Ce$&@SddCaz;5d*@Rl!R)RC=vNx^bp}GU=_Y9CY(@%** zyudTZE?DPXU=#23F1L;M0+1ax_B7!B0rzbb^8!CCcm5l_u}HZ+f}7}z{zTOEVEXqx z+{x&E0%~bv+;xwq+7>UfhLcg@>L^F;;7NUIDKop^>h$j?2hmtqkC&ffyny^?w1n2lIM?7-G)FZeqME6g%db;psDF z=#a2ivwTde%7}MAaf;ak@@0G~<#9<)7yTMcGTzcv>HP5tPus|jpn*&K}@(M$&(IUD>DC z#RG=@H_I3iUM7>MlLp32&pEv6T)`RBY-Qk{uP0Z4E13rH3GUFn>OKcWnKtrE4dfHv zrY4)c$RoWmW4u@!NNxYgg0MU9)-HJw7yNdrwYJ=l%2SwB^dz(NK%3mP^FM3IZ=7QV4 z3(ZCK!~0Ddz#0k6wpN5d%(jL|WFdQG-MVMZxo6eCEGN9^s5RMPNS>H|N}WWZbgADH6eQgb?Nz=M>LsZ>kW3-;arRb4qVMxqW9qpS-Ou0A+qPdy zb(d`gy1G%~FsA4EAE#XHqR?8@pukLsvn>&1&2H^Nbuk|uNkZWXKI18G(bibey!RmU ztSc%x`paf(8K#B^A-?dVoFJaSdyK44GSPskp{PKV%_F_AljJS52kJ76bVQFDFkz7p z3h4D22uYSV#Q{KV{lj1ChsGL8M@G1sKHwV&ss{m@xELAXEB%Qd^1hhirYwkJy@5Y3{>yqa>MPumu zjf7IUT$0G<8wx~?BpLVQbhso2T1fM>jL`4d8#0FSjlfv;11$O3Q4bL^tB+J;#*%MO zg|fYMdh;hf{mzjjN_Zq(EDKUoli`R_71e}Q>ZNq5P!h_LxH{`+%5BO}!A1PI6>^IC zZzHe}g9Y^pdc!(DP=>U-g(%jOvs9~v)f)Dbv!(PqCyNO;CKmS-1B*J{U;{sZy|H1? z24BMB*OYYcA*O{1XWzt$c|-2@CoYoAklyJRB2sxrD)_M=87GHc)9x~$jC&7gGE2(G z3`z2_XczH?)ch0%Wzj)2MmN zHpK_dIOH``8`25l@Q;VEL5b@smLw0#aKnQSKD_}$* zkDWYsDo0e;X~sBE4zEhY40tnU0(%ls2xcZf{7C}8j@#*vLaF|eywaU9uEVFy*B*02 zcAa3vsuc(-j|SYQ9h*2epyE}R(BW=va|?he^Va^qMsyCKiP~3`bO@F8f?6WG6LC&7 zmz%-b`H0d`~u{CSHLo44|qPZ^R zKbl_bK9%YU6K?~Perv8ehF-!TA#%bu?Sw2(LO$QjZ~nXP8M3~1!G1qJ?3TBA2nP+kbOOYaRfzg+k{>o5YJjiQSHZ=!@SsPFc? z|4*6joH8=gUc>#ObTE5&!Ud#Nx5s)`gtvbqIBM}-XStZ>aP-S}e5S~*p&Q@v!aOK& z@AlRpyboE$^G%~wyHcbXE?jdw4G*K8d!2co^hRXKHV z4wMdpWTc;p57^8?egbv9sF9=l?jnsS=0;KQG_gUXIV`uBAcW?(LoOsavOekOW!f}p zjX6P4lRJZ01V~$PVTZ#(4JMD9#OMB_hdo!cKc##xmldxq;>-y>lyC+Ozm~9|f0M zDfG$3Q}>l0Iv6&M)}^}zx+1(lDB15#pLG?z4z9aO3OMFK2!BgTyObQ(8fP^01Xd)T2?hjdPjjD}RrH#6okyqJo`I zfNco&Hola$a$Jv?sqJpwW-3Z}Gdu8>&f4O+t;MX)jiLT{Cq}WYAyl?(T$)~p$N;*X z^FGFZ@c4Sw(KD?QE+Uo#dkXP$M4?Gh?X( z`Aq^())#t(6(U=-kAciD=nl);tQ*p${BIlw>F3k;!!W2BLx za`9_f&y434oPq*-UEYU*lRXK8b16j2moV&sywQq{wSO&F!6xx8T-E;ZN9wYFi1*#i4>4vHh*+n;0!ZUJcr} zepc%~ZS&o31>VWEzj4S-9JRY<+!;(*wB)~Vu-YyRONqZLuni|5_F9`%ypp+x3P`0V zhMX&5HHuVhwcptB5`n^QKi%8DF!U;Tb>T0&hfp!avP6X$b7sO>=5UFwhRgP-wz#hR z(Y>fPP^v?K5pE*y;!?>+n9mU2NgRzkUPA<)^J)c#@E)!tK8R0%uD3>632J$MRdszm zc|T&!jc7H5GZBWim{}wQcGkAG&U}9iZ?dA0%TDW|q!&H7HY2D<(qt}(E5JcsQub+h z_*Rk==m;l3j4Hk(j`m$eM1Riix&b8Wcs}$L(L7J)zu~Y2GH9RK^_G?od;7Mx6*~;) zOnImOlEe_nhyPOtMwoVf{QYOMg5L&TVRaY#Z8E|ifQXs>BVsFmiI}f*ouIIyj_a|J z1VtavEK^nTI$l zX=v!TE~CDRvM|y6)vvYIWi?q`h%q7Ky12Yps~vZGq9R4?snN4PHJ*a4%C55PiZ*Q6 zEYSId4=RZb%+sLdF@Q1*5ib3grnW8IGDehKOe&{3zWSR|uYm>G-C=T&H-UOLi(oD) z8<2#4LK<#9NZm+g)|FqbMv!3^7ZesDCk5SSjJ1V%KH9g>>uqsf^`WQgj`C+x0=bH~ zX_Z;su_KliYeRaK57v@8e{Mn)hU7b9DUo6|LO>ege4%j%-)yvlYEOCQQaJn66> z@QH|Z5i7SFwP-90y>uio|7s-TAIjSDi-W=Gx1X}5L9UYpYx#~=7$92b^$T7*+*tX} zGmR3R=<63eFIg=Itvv=&x-TR7HdCOO%P8u$Uz@+!$x}y+6pv!2TX*WYr^Za&WSO+m z`7PtPeg}h}Lo$N=8mrecf-&2Jnj`0g)x#wN`}lfZj62^Rs|tlH(SF&saDVk&XlpDy zE)n-ynAduso02_lI&yAcYElO$O;w)}GXFdR@-H=*DzY&s4+135&pr+ZN)D^*Ju2A= zhFvOVhxP1j9EISCHs!E>bD1AdQU0aBqFtCwq5A)%*9bsxu7Zghn#)ugU}xu0Sqjg8 z@VN4PgG9w=FBUZ*tkLTxHq|k|D%SJcTdMMDaT>Nr+Nk@znK4R8!P$4`0mb-T+gQ`l z1H`EvJ$-&y16)C1TwCpM6si2?!FD~`ny&r>8}G5HEV%9LZJgO?lUO;3ErPykeBSd1 zI;gKNFkDB|SL%VCRvl;yA+nmgU)?$eOY$*Ekav;4JYcB0*!|0mQG%?06=1`LjZg-l zr4dSTO;`^2LebsNGM1~C`o?g^>QW13kdxHxhAL9`uKQ$ea7iu)k;e)+aqEJi3KFRE zoQ@=pDD{Mx>d^#Tb%q^f05O<9%Ajc14& z*dE0c5&NrVItLgE@n440@o6X?pV`wJAbV;#EcGr|#*1ZMz&ZbiQBF48|6&vsw84jH zrzqngy_m!z#&1o3W|Xx-KLQHxDZ%4J%&)62;c$AJN2_l!7kQrYFUXnbg$sXFwp^+# zGJQ$x#KFv#Oi%{V8OIg`i7!E9;S=`#QIO!-dDU`q_BPvhf8S`?C(d#j5ZUL&{Y8gj zvKy+3U$V)iUL;C9?swV_@V5-mpKf2RVpPz-$piG{42ss6(&Rt%#HqQ?2isB;5{vR% zEs<3bGGt@2CD14C+JZyLsD^W-w;4s5wv z(Lj(vRo*^=G-ogs5U`(a68Rr)!hgU^Xd83kT)efva!6a8FFLVaGH5qHR`1V>TIPG5 z=O*kIzbhdEVOUq3f*ht~ULnF}KkXI(mu>aP&7Re|%wnKkvRP-s65Xmd-7a??Dty!1 zM2R|nvQ(+Up%7Ajn4+yEG6&Qz+@yYO)4O@BLE-cC6U|G|4}9ClF6h-o~Z z9p%lv| zv;B98aWLFv-3SEkYyVoRw9SdQ_S=~e-L>Ls+`tP$!XRf*U0C||9Th;+l)<4Q2y^1a z%c13R0JG3Qv`|y&0En7W%K*>Xqyi|3eV|#&c>h%|J9}~G1vE1H!WQ41C7^eTYfZK@ zM129Sc4UaW`Oc>Jo%JkcGN!^+&p4G3jrSItT+a8-Db^r092MADtli!U;4^D`u0g7H zf%E|r3>a_Np$*KIx+)&KT0>$r?*VQJrdPg>cq16MZ$jDK$#chY*%2RtmE4IHMaxr2 zFS3ic8l2wNMMDc;&Qc0RVRl}3q+;G2!M@HtTJ+iC%lXrfuM>`J7X;f~D5it)~Kt zCjh;)wEDT7q*#Xi`j)x;3Jo!N>dIBe7(?*F&s}Hl>xOs2(4b~S;Q!#6(TAZq01V5( zl5{1R&)6ny|0uo1*ojQeK*G(5)_iMdk5MHwQeJf&K5p2KT+Fn@H88vG7H+7TJ>?~n zR5MgSn>Oe|V#f%-YV|#4D;;makjl=YmYgYO4E>DJcg{;Dt7d4PByUuOsBYbetj5(S zkU@V2Qy`@VY{EM-e!0=r&Sq>M&|c9~GPFPYa~K6a*Sd4!({mUJGw9omg^qcjpGgky ze;|*Pi!c&P)8@$-btqz5X#L0;bK_)LPInbZ0q9U|%UA#dK#C0PQy>iiYBao`gg8>( zq;G%u4zr%s^D>KO)N%J-e^GfHXe%PDeht<34Y4V}LBxnn zsJHip;C*EX#%qX+!babre||@KbX@29b*+~cTk_mP7z1+bE3k3hnxaSM}CY`)0Eq4eS#z#r94x3Bi&UNrC=)b&TotDxzrUYz;qAogXeq zv_y3eck0A<(=lSFigMCcdS7wt10n)yLk;QVpz=1qE5MQYiq4Z<>%|C1k&eub$ELNJ zmVnLal7?))57j7&h`(R- z)K4@jvYlj@dJk0{f_Shnmjn6{;xHE^8VS@ZtpPGh)T;?SWY;l*b>Wo@mEAN44A&8o z&3q+BR0L@x4trD#(mpY*>LHidYE_mY^olj$vwrC^ut_*K%PDhzEv(xWh#7OcFs-y) zFOI_o{wl}ZMeqnFlYEv4rY?>}f(pj;P$+3rbjjuS?FI@YUNS-F=#w}yb&0G>&AJ!M*0m~acDInQ;3o7Ill12xZZo|j1pY>dF7XlQJc)2Rp!6?j{lk0 z%!HG+qO!;fFcWn)5T2V7_1pZ^o8ax3EuO{e*O?FJpcD|kHb?~_GGvvU!@rS-DPlvg zO-LXbJ|~oUSMv`waYuJ;kfkR_sdwHuQnfwN2D(yx2EqFP7%liSyaL&Oy%5--zP`Zv zpxyY*S=r>9N}xa(?(@U^4!C)dzLEK=tfzeRE=qmKM4M>jxj9!$mCSqE7G==4$jt38 z%3?1NIH?bCZHKZUETS4ZIME9K(Ld3mk>?4akoPd*=E?i;YksU@Z1oVJmiPk;aLF@6R^-3Up z-#6uk^6sH6UmnaJeQ$m8lKXiCiLI**7avL_O_VRl4e3R=Y|;))_3DQW16>NP&CVWb z)uk3fMAaq+-}0CF-~!BNW})N&$x`fJ%%?H#i4|+}rbKJHG zxaiBxf81{a?Y4-8;+5>c>9;RF~&}7eEPutbj!$6kfl;4g-M?2@smzG`lqJ98IB)_}7o4 zqogMQ_C0a#*)zw?Sp0GhS@x13PMC6A>rDRGxyp5@3;3)^D{pSnE;sC^*V_5sR|u@q zDQEFgMtQICzWs2(wnza{n*&a}1$>xWTU<>m;^oW;z zCzaqNtFHTEet|dhtgDG96B0Oq)Xk!z&i1t8D{}YQKsoFFH|XQlF?;?G`Ly;>s^H>J zDS_{A^lqgbGzl(n5?H6qS5Ozq$gQTqhC@CMJ2svBWB3v!?n+MnE%WtX26|`bBaN2 zyJ{t9Om3Vs+tPpgcf&Mw2U(02O>qZlnSltW09#qb7JQ1HZ=qsuOlffMLM-{Cq)cRx z=@ltOYz-zvZ4;C#LeYwYPSI)kM!sn1%1Y&aI4{|)HV?QC5Cy@jC1~Tad_3vfd%vND zF{{}8qLzgZ`4c_$xyrZB?8?&Lc3u=K@AXyZ+c}IBqFRouO^L&c#h6Y;v^JyylhI+l zaRR*{TM)ekhMenGGDEy&be#dol6jb${ExznW=8Z!^EhyM@SBh6wkTj z0o4&|KZdWgke{?X1oRq5a+(LbM0_0#y9BJ}V4e%ae9q1JyxtXNd`$07HU$9@?$1;K zdRF7|sh$ZZU%6$>cuEWi6Ear?*}E?3%t=8%VNo~=#AVTPbQ>|uB{it7=iHr!dgFhh%~$~YT7IJDr#jtoga2{JcHsL$8=wrP07Sl6&YP6OK@ zA5M2RO&wh=12FBoBLPi)`jz4mD85GULfHgZI8GlL#o4^HM z*h-mnR+!4E9ux}4Z7AY&6YuZJWU4$&Z%6P6l@vi5_piNi$SMg%r+vI{q>@?f((iq$133P&7fp9W1M(keP!8TCzoA6Q?z2t&yfynk>i(t0m zQN!`m83N>kY{Zkc;`QzhxJsq%^rV6I+Z(iPOg&OIr}BU&T+6(N^PVXQX1Gh=VnxnE zJpOQ(wvQE@72!1;jA~$di`k4g15V6mZDwA}Zf7WafRgY59gN)_Fr0ZViFx-LUV3Zf z88>d{zJ86Yr|wQ)-rr$(R>o_%JCMhn&+?utvDR`PHmAJMI6>OWye)awaM-xctAC$N z=V|)RWpoKKl8MEF_)#B1D}<&GGfd0Gn(on}FYg?*m;QQ*Xc2np8aF~nKO#;R3Kz^% z2WP}Pt&v~Zw(9l1bAN0a=BdTe6%)qjv) zW`weQT_LqXpRKqina_>5wux`SPhGNZxgvUGn`C*6!8D6)rt5$h8$7@i~Soj-6BIyKE+P4&k@AgAztJ|Q z+sE1&c>1T$#s8*Z{(-_i|SMyjqPy_nEO|*!R@1vNd2oye4M%f@G|F*>^ zeW}kn3lFeI$v?uKi617cmN~)I{&a0<|2Ai~m=va?UC<1dHK>-J&x2@(cjFfDl3Q;5 z=O16}cY4sU->RuJ{X*r*$;6jJQ3c+e${qwxCQoeP6ABz5PWj@oW52liQWSi4X1VHl zt@=^%KNngQJ!0;=dH*a>BF8%Uf`_B!3d)R#6^6oLXYKxJv-_jK`E+kJ2z1B^Jy2(M z1o+7gJQcpk#>#+*1PE2cCizfM^|3~uJ**V@KcCY0c32EbF9VRqp}jpHWSs6HkdYcq zJg;hxvKR$-kB;G5=$U(hJ52mUZ!mH$Qd4 z(QW)Gd}5i=K2Cd&3T!;y0wrSp?i{(Wzm1G0{o$uKS^~T=Zrbhk?l?zRNBj(#wCUxcTgvqU- zcVjagXP&JR?DON|&g?#%IYPQS@~D3BQCarA{N@wcg143R)(p<8c(@P z&zTEPxeL#kb7SRmf77)Qzy@Vm3(V*GBu~kDPkkjZU@;RAT>(=svxERvCXteOj%ImE zW_e6!dB|t$PG)*yE%yK$l=B+kfb~hqEQJf5;e}ZCw)?jjn88LUK!^o6QBuWYTE#w@>b}2m7U~kCSjKS(#p$0zC25 zNH}Tpm(-6@gq>C&DrPhn1LyBTw?on+wHBulx57unuSP0K_~jy7FGB!kELIhSxiAsr z&~q{VAMCwjaHie5w%f6j?%1|%J007$JGRxa)v?pD*|9seZQI&U-gnM5=UU&?R~xl= z)&6s5)TlhEC;!HEj^jLT&AHEM1~?nyf9+BFNr$>L>A_V?j^G>s3YBA>r39qIYGn0O z3_Yzv)fF_=GROlYsC3rV%CKV^>eSKE` z(&=ubmU#{+&!xQ1|H@tbJx;XCk`Mlf5N}_lh zAV!R1_9gHqZcnAu_#_HU%{S$B=U9cLt)jeEv|L0xzj%lh)myL=SR};O80K}*IZ0@j zZwgvCYIl=Wh&ya<-0(h5j(ViyIc$Sy3RkOLthWc{Ypn%1);!35STApq(gT5sLH3Vs zlrYadvhs_!Avn*)%mgtgZ8&0ZX4M zE53BL{C~+Z5g|dbhPQ#V;!Jo})@Z5peYlvYcs4q}0BK43T!rDv|cHa0r)l2z8}pm8WuAf>Of7?yHb#6FZKkW_x9Du{;e927Hn>r%vy=-7LmcC zpfGqZ;A`?0S)luS8}Hw-Ua~b4iV}ugP1p!T1 zsndN1M#q?~mF&`yNy009D`y`DHQ_b!ZB8elhgMzAtXP>HNpNJw>6V>_9TLwnw)Jg3K5ToD^C3qO8{Q zqE>`4yY@(m=;Bq}@JX2aof~LtdMHMD?PbEWFtbUm<`{F&I4Kgt^`aGNI0kfWpV|ne zc>^}gks+19@pAJ@Ri}0|<=orxMl0eQo6s}(ruFm~&84Iij5ioXlM4&EKQ@XV8CbKX zU^L{>%11IJ{*esHFx*hwq8kUxFC0_#%K(xg+x{M{(@$gE_|2U#l_;yt8y;0G3QLP; zrAJ4DiTUP0a1V0!x@I4l&;v9`w7hGiJ#@^P7v^J%y-0L}%?WIFQ6?S&2dWSt5$f;J zajcT0t19~o_p=WEEP55GFM`_yFhu=AKM<13p^kIELO59Tn4MNKFMT`;&(WAty)~K> zmH2BOL~bRARi*a+d}J4Z;y)%V6m^v5Ph6SZBUD=r<)C?~egO^bW^($NfNS76B`0($KCDBu|4QOW+d#v9|`H{Sev zVmJS3-m}C1PxGE;w`N5ULC#cFrksq!5{eA!)9lVM;IcL!{9}?tIdYJoZ$#cSB)VJ| z#oFb)7kAvK^av#$bu>bXTC-E9d0^bKi>-GMxQ-8ypQqL63ZZF=k5MZVNB_ z(W9@oy43x8U3d)%m+-*k*=8Swwf!I5sTr8^0DzzYFf0rA2rX2@EUtGrlDYR|G% zeqsHv8FyujCXfU4!8AgYbl8f)HE{MxuxmdW0%Mtsjg~^W0{l8y&FPKl+4fXsf?H{X zpagA4CO-bP4bzd4{hD9c5g6w5CSz73pIrbZDFws5JyhP|ojJ-TQOc*G#dIb8Q*zFL z!Y4i3)-o?jwoivEs2n8qxzk{&7i9H1OQ!aS2mEr<3HT79%8jSB;K-AnLKApfIg_SL zbNQ>x;#ZQ$Q6LT)xeH7Adf$UjJBa|5(|l4V%PCy(imXH!w}y5LuXSB%U_*MRZ>5o z6!;62tojHxfCr`PnJM#VEx)xthMk&=a*j7-IDKR+$X0^{>4Ji$kVVV}aGMFi$?%&Ae+gZnS@-_P zV`$KY*p2_lV+hg_QBiSaVln(t{@C&y`4+~VIRTeNkn11GCam;?D^HM?!zU^nMDUea zSjpS>`|m(SVK5&mIJn`4<^Jccn$!Ms?<%WSr&c`bWt;Xh2*7sHct4`Xuo6TzYN&gW z=@F3{-unaF?U%C2j_0E!zC!-E?BO zAN1(4Vtx~8ovszM(Rp5JFCfXxA;qCQ*)A;U(3*|tPmeM|yLN$KLvcAaUxa_g1uN=@ zqibhDdo&2qH#cuQaBMs@tuJNRUQ;LwCPz;Fu!0e8)=H3R0`JtKPej@XWZOs(Y7Jvw zSL(k9QQjoaEc~Grmw>5Va?||v`%gp#V^O|weJ4*;D`e0)qLu>1*t)!=^YP9Wbz>#F z`mdHbEyrr#a`U)*N0V8ibjtwH(~A+n@U+c#3?QNSGywe5!z>`b7#N@9n;S+La=#@| zALh1s5~RZry{R zk4N4--CpvnVSyj7xrDezM#{|G=TDfGDAvDa%i; zBgeIE&$DnF8XK)ca!ozwi?w-W8cB)tZ zERbo05&u?=-YrRWgRrGn0oe_4^6=)TB_m!EsWAHtUqS`W6K^nCQhp0*%BT_DuEWDT zVs3fE6l^!xpwwkLX+_`!@H_on`6!Xuew4^+0DUp%)sGSx%14Q8-U6_xT>#j$^ZJlG zE!Fn1eDuf;#!=_>O_|3^1n}P7JpuSHjlV;*f!hha7s`w`G&f!!p!Z9o)nfRBe*IWV ztkpfg;b894=rmBUdOZV(ANYjDT_Zd7cHZSpnJd=)ICQpXyMrt|FqBvcb| z*6{~=L#zNX6z^#~=V?6aX*?rOD=iN7B0vm9qB&|@W%Q>QD#_{(F%;g=H>=W4J@$`J zJRQZ~9mQWA#h-Du9$~@`06wv)22e?1|M`hvLz>iU3tcO&@(VqocJigo7B|>Ofy;=u^HKDlSF*$ziXgm`bH(A^@bJYT#?(OBhc53 z`B{1|qU`bj%+r(yTs6m>UGN74~e{N!|>~R#qzs0^ogPCK-9dGx81LA06pd| ziJ`WGpcw4FRGzy5a3WYd=Jr?WJ5W7KBkXyBZ^isJQ$$$=nzg;b7>t0r7t@Z^hVYHy zSd3!Xbn!U7&={zUuoP3icpC6|Ts&tRWeocC3=h)6K+2v?d#^heN`0XltfKr`kX9k# z$gyN_GkKmmSczw0v$&BW`zv{WFVIL3tp zcm0`wA(j@0^ps>+?cOn?Ck|CSLZVV9><02?08RzJpOsc57$XIIFOaHsZ7j~3bhFt+ zA|iBWdn`^+JESe6BNZ8JO<8VV4jIPp7@=(X;rO*2*czq`((V?v3AoH57MFLPsUy7MXrQB7F8ae|{4{vl{GhcxbIW({IIE{9?^Wb$hQEE=1v8B4BF zeJna!3n5xv-NcKnci<2$ra%3eall{S9-JLr{yc-ux;^kYpG&OZ%gpR}xsR`SvY2%^ zp5!}EDmh*|bh}sa*5hHM9(R#FK+OLqIFVNOUvQ$!FaHWBGJ_)PlNG!F{(nVbD+-DV zX7}n^3H2@{E4SuYo*k9adlJ5()RLB;Ie#Pi6U_s%*5P+8`^~JZTr6&jn=uV1oT_y! zAjB8!jn4PUQ%ey7(REajPaU%BQ19ZDXej&wK^JtZ-?s7YO7PxekRV@vMqW-%z&S?# z__rj{h<@b1=xcei+lmA63z}j3#O{NnNsFPtzPUQV@xA4h)U>ienyM*?k|O?BUCX$+ zEbLZW`B}xi(~k(aDG8K|v+T{no&ZPI8x~JWeI51`%L)qs5`9*T1As)S$W(h*n&tul zVvjvC$xL6Wu7`sl7l!*3Ku;{X7K}Pn*;2X3>(oU$L<|XqUy(b#LC(b;Cm?G^%~l3* ze#@7)tgQzl)}~(Yr;en^8KfNgLtr#X<%^|?;lr>V_G{6q_hLDyU&WqLyhqidU~>n1C_; zLLVuga^vpbogZsDgl=$HA9~7V5_`rdno19Mphkbq5x?k?TEAbdhe{Hm98n!%dGnLg ztGIlJXer&2nL@w+bi;G6HOgu0v6qz>e z4ju|m97A7isov0#9f_z$L@G`lhKv1iTTJL*58qyTO}$W<5w4W@9W~eRFIBE?YqP#D zqHsN(xEAB_5K)vnr=rn>t)O0CL9#IF(O4ZtTqM@9zKjunRoZ7T!i~||Fs~y2=0f{` zi>b!-hu%m;nB{*`K7qdiI`_xu9X^pq0mKtIz0iM+bbzD~`UXBC9a8!H6f(lUF+Noc zCs;#(Di>JXU#eUJK{fxl%9VCf+y0j>*CJiL8{6@R^Si2MgHeJ<>{ePJPF3zHCi(2d zYy^)k;oamRY%?IQh`m#J>P;YIudRpHT`6~he(fv}7&QNr79Mm*;FK$-AO=Z^I8-4+ zbJM4QmbcbjJb>nH;uC7(Eo3dlymY9{pLmbaoj7aZcVp=8M}C0ho7Q@K4|EJ){^q~y zjgHrIi2oP8QP4lu8+rXrZ^Y%0t^5)4ko+0)fS~?GZ^ZWZdZV9?e|EYE`_|Nzc94t> zCq6Hx-PKwES`C7!%zxkL^5_A1qm>)YXa51rxDU!a=fl%|s9?#y_YGdbHr`nB-Uc(p znoNIJf%h*t50vCb&XZMJe91(jIX-PDN7CF~ahGcb5$hee}ockiofp0P>`4a0q-NNA@mlm4t3IYi(^^=Gc#et1z*2%*Cw|6GY3Rh5?`(oh zlCzwiF3{W>-v%uOW|v%)QwtK>@x#7`Lc8;Ze1}G=wm}B1VkdX;*<=H(w4JhKV>Ido zxAFNB8CiG)%LwpBorlfY`^t(PBGPVn1) z8U2aC;fidU3TW{7(Hx&gpc(?N_+@vG@Ah!d{VRXCn667rVdia&B6DBf4JNxzIsI5F ziDN-=GQMnBO-@mWs&SY{wWab2rPwb-nGK&OT54utqF8UJG>%sQYg8Hg$?|5*pk7Yu zNQpkyJ7|9im5fS)&HknJBQ#>dEg)oZX%K99DGwZ4kfYSp}0$ctcm=KeW9Oy{Z zNxYc8fsq)P$^IBBfmW~A-%_LSN`FIlif!%Ra{QAYoRE%A<+4`$Fs+5Fhyh&{Q z*M|9H@RK0>;}$=g1?6%q$XNO$xO`M^)l%?Ztpi^HO)ii%j9!w6l^XppCs!{K?HcFq zurW2XDw}N3Av!Ga-rr8M{KNxHv{>Stw${d>KaL`X!4r7qO%;DQw+xLazDOPZK@U-6 zgkVUSYoU7=GTVK8O_l=N55~6;qrgg{PMQ{8%ab+2UT=ZiylTvQw6+Xgpp}Yk$H)b7QoRh5gfI} zanQsUwpi@16%(y2n6J(!{GG19w)Cle6a^L$)EA+H+?=n{9)lSDlX1;=v)qo8uyS@# ztxwHAlSGYYjN!)qbQtYF-sMGD{!fQdzhhNEd4j$KJ%SPlZwFV27)zy5lK%iA-AhHi z*ROW-l{-ZMNz`}tCrNal{|8C*wI2z_y20?DkVMl5pZ*(3lp$9BACg37MZUR5K3UvZ zA)w(g*h<7Tv1$w(am-cqu1XAD_9v>HTCGM6!?wkS7h}6$5;Hd4d4n)wDr+k^+kv=r zV8@P(XhA4*_ULGCcA>PN`1?LcB6#smh_})RfLMK9Ssl`uR{JZ4s;xrL;7u$83bCHtE9lL>z3&d|4p`mU zz!9%EbzD7!Mo+JFzd+crh1j_R*vs{?yX~m)u9rum?t>; z9N9&jFt<4CoD3Gwg)+)?%QNJp0=jYpGcKvCceqvE?AJCnMntm6$^1Vg;?K?Kh&9*E zScw#iS5};TEw>H9Xz@Y1DKhQXKlTo$>+K3Tpsls}#=qoSHI6ydg#|Kb19|))%8G7D z+ZDG*Ju2deyRqhVgMq7ENofBHY{FrI{$wtbJGwiDb-P&ZZ`f^&Egw}YoHms)KWr}c zqb?n8);F&FHhsj`q?lR4!bQ8lRM^UrN}7=hQS}FJU#rQM5#X4HWGp6t`$@n5lg;{G z@-lx9Vk1)~!1mLybzM`&MN_9sy?mB=1|j(?ZGL zBEqMuoe^{HWYUHZapvP67xIW-46LuIsS}r zutVMjfe!Chvplik$uBsrbj(?mifJMHrl9Z#u<(b#um|w48mJZW==BEvIyXN?otaN5 z!?-)3?3-}xyAu_JWBeLZjxnI3m&~&}aS7v84{Yf(!oD}xKMoymo!xbv$>Vz;doho2 z&gJ&3f6_@H5{#LZ$eP82+Y7YGjHo&*W)R~vd+Q6vO|ML9%0PouL;_xtGWl^z4ydQgQQ zOwG4?K$_)W4oI_*JWv15QhqOz@gG>qS#$uF@=|SprTmIe8#nGbXP#&U&6r8#BwqF@ zbL)4^i&5F%zXn(k37^-?W!B_$ z4$$TkmCm&RwE6l0Z9YTVQ@C>8X8>VyVGE~q^+lIRl_&#y)w@2XX>j{H%Ze7(D_A|U z#GVAt-}5!69C^*cn+Lvp|1UvoW`zHL#OB{2HtfnDhz!~|Mun8 zlq6M%6Y;wHpCOi=D~UU^`MFSndyzo9D*oP86roZ_9P%&M+|&EHR$x);e(37>=g1|xz(_! zo=9C2_sczXUgwK{Xp7<>c z1BY&|c52KI3*Y$^VrcC^YnlA$rmL=q%CFN+ohj?IVbxctO$Juo06;ceJuMM_aTsZn zfDFoO7J%JYlfr0q0{r9{YhYvvA98MP)P?lvHBRWlHkPqMDGecVCRRlufFY=h0x)K8 zAP-snH+vBmt7#ifS}ktm=$iYLM!c3Kw_DZ{LeQyzJ2e7SeKR#URDC%s;9-6)AZT4X zI8KyvRa>vv=kDlo`=I2~e)XUA(#RYIo%$*}g`1|}Lt%%$E;V-s;CZL^L@y1A(XL;j zd%Z)yv^+ovyH!gMDZOrdk$}(u)h`*ipqh4Y?DMDAnr+n1PbhdeJczk37S`I>0lHWA z0^;fk?4>0hW%Ij@>&8h(R)t9NVlD%Hl*Wzj(Z#_@8FDy)y9!G1bg7c&aWB2MF+X_P z+wl0q{PQ0KNh^(hjI`Z+d?Ev-a|apqN1S>cY{0}19G!med3l%Nf)VXBVV}IS+L7L3 zm1rq_wL~{{TQ>XoPL%oH2*^G^C}M2`-m7&G;W5PfI0jIxmKYN-bT^8vFC?8W6+(44 z@|5E8t4>(}{M?0~MgJdxWeUsI;r<#VG5xzBiJ)+3$E|Rb@`(l$iQuT{(;b%Ywn??W zXpFV!Ht(4npt5$e^&N+A4v_uO5{wrKJ_UMme3_!y$P;w$1H?RH_7j^7c17Fyt0n0% zvu)O_(eBH{T=vKAn()T*folDqhmAome{v?42Zd3K@rT<9jg$F4zMs4<+i$Z^fkAgb z=h5Ab;>{xdba)91y-;e-#n~2$K0V`kU8OfetYN2#wTQD%+&^UF4>OGs|CBH6vRU{w zDFr5U8X&lWiUCIpQ7@qHR{}PPW4-VMw${TOVV*W=N$^#tR!C`9LXst;7|Sh)Y$c;G zZ257GaJT3mq9IA1UmxE>M<+Sn@3{!GiZQ&K;#2)wV8`Yro^$Y2!kZ22%>&lM0!C_XQr-jN9iv6 z)_y5GhaE>2bdct_7sEGp5ylvr&_K|b{Mr%oyP8~(2p1M9A~_OD;~^3ck0qGZCQuR1GS4kt*n{-nSNtL zgOqYJcODIA^=pfM-EIuJ^&(tec}w*R>Z+Qzx^33-m1flQWxVRERpi)B0Y87~nWy8H zkL2ZSe*7cPp$Vf=tbtcUTV z)Z?!Te$L;BJg~xJsDq4XNQO`)ctL-TF$jCWAhd7XkS{h6W?gy48jv*a}{}$Wn)~0JQce31!ls-Gceoa!p-%# zPLK!j;P?gXr_3)9h}981_0(d9v#S(~DRq2FjDpU1v)*QA+5T7&$zM_?)>)y|xErG4 z*yVtwN=nzxa#@cbvN!?Tct(0YMp8=`h8E`0)x`v2M08I>_lm}tkJri4n z*cC!vo+^8M_@#z4P#>d2?Ot5O{gvS>h|wvdT@5fYJhmC$1kqrOM*Q*#5Lt88QXIpt z<-Of7J8m%w`L*i{d}MQvUplF-212zPrGdIJM!3L>UrDejA@0)5ZFztarVUg>znq>uWjc)lRexK;%enHhJakI!eb`Fmq=AnxiQ4U5XVhoo&tVw#S-tpmDM-zn z$t&9z4l>@=AcqlHZy@K$YMWVk-YI4*F?|9hs<$u$6TQk((K1YLqn(-TVhMSPL+yGs zf1=@?5q9c-su@3@lue|MP#p|dCPX3qO+_x=Jizi3@>cX{yuPjp|AO<%nSa^h?k z%T<|%KSJSEqy&cRXLhbdbqi8eE=V!W$xL=wNx>Kx&aiqwla#gozN4xV4Z<#hlIF&c z2a2}jEEu8^*p}dI;+ggY#dnrKm`UUl*r>3Iz9nyB1@KHorODf#8paF!tgwoLBX44j z%&(P#RZJWh{T+t9?2Lrpi(zO{jCQp)5^(N55t*)% zJ?#ec3l{~B0$R?<7VIfvO3ho3XrT8RPCZ9B9EHK9BIJTiO_`e({%ZA`F=x7tH{6!w z7X!+EiRdc}tyDFwruCW@$4^&Ez+Xy7^O^`dcvn_>Di|!uHI<&_wQ77bzi}1i7cIn? zmr?5a3O3bT3<@e5>$8c-(}&e(028S(1b#EtAE2B=rcp||TS9}}#?Eu)LDbX;w@;TJ zs*Ik0e>;78yT+SxR`6u}>Q;)3njzg~!A`oKfyt8pJL)oSN9!_gp&cc#{?ybEmg~ze z;Jx@rr*$C{SN#1hNH7~spi4UUd0)|a-+1_z`V~jLJBkgXL(OJ5*alWJdJ{8EE?0j+ zj4}h1_rz4ATr3oZZ@jIojWe6^X~xCjA}D6Toa2O!&O#ZDUHHx|dVq7FV;8;HkPKSH zVbY1?0uE6^3v%*aftfKU+YFe1FwO0)xMPj#044!5lYX6y_(7KHz3$t38r^sYIidl< zYZEbOYkXPoBnOmJmq1mx54jTtDMXibUF?DqodR0rv%N!jR^pv@@6=a=(MJ@u=w5qq z3n=#_HC`!C+HWk%VH7h7I$%%@j*}~^7I2|IU}Y`E9}HbJgb=}li&%{AzX6ucbsR)}}dJi;AflZUg;JC&GFI;CJ-PBtLZ{CDk!q#4TK z=037~U=(e|Nd%j?*E~BSHUw6KxOeq2w`+BG5R<%ggd~Sa+V~m%&)WREMInRm$JpPs z>WEljq46(YA{$)CcJut2ak>Myma}Yqhk3g(QllKXWAb3d9WHHam=91NcIZYFxT~*B z$~q~_xT=Q4At+@nopo#}lDT?kFRdIouLXil?WyILG@|jtC*6g|6wDrZxGZv3*%KU; zH#F)r#rLOjSCx5_QBO0^o9oP6jK&K@JPz)(H0nYOK9**;AIq=f1zHID=5MJkVhaaGO)QFEi%|jSEf?{piEz8J7of$RP7U zI5u2av?qhpkB<7b!s)VTcW2!nON@`D%HzkDc{@N8bNaP61Vmpn|~@KB%eRjt_d%Gz^@n$b9t3z*wX4 zkdfFQ53vIwbSD`Zef z5IY~!bi6fF04r8V`};JZmG zS-}O#sJbe!>`?^qb=W|rGbqY1a)U0Y=Mp?`*X7(<+r^jm$Ghh!eo>DkXg}kx9*_ev zXx8%457=25`t-{L0{6tSB)D3aj1zZpm?UQI^5~gjI2wJr@piT0eufmae zpvXXxsdn=-6r<5`+Ye?CoHFWSj1$`5FtAJus}9mVWnz7Ls<^E3qFFyA7DKZY z*TGQY>ySK{X<;dToK`czaX6HT{ie@1a|Ujql#pWrY&uwA3K=sAg^k|WMN@scAYCQp zJf2i$%zr486*6R0;!G2tOs%*d#6+UtO>;WZRbh_bsuhXeMA>K?Pfh_+D4Q%b2{4A+ zsGm!ou&Sb6RT?WeR;=wB8$Hz9-*e=5aHwJsHG%gLinv62KIjG_wsV>k95JfLYY$S} zB%KwcmX)vnZ0V&))2m_Xla5-#x7?IcA?BhgJ1)JSUm^B>f2PebHhsB!?8D2UVlawl_&=0W+G|{fubY{s%lvBux1_Ei(rLW7N*ickVR)0+Mp^jIbG(lhXt>zZJsO}YI}m{h^*4vp`lDo_N*n088)>(} ztY`34ckw1-Tsv*M-qv zBRUYa)_rWyQ<`d;w7t`RZ(UvpDLM0e-2>i5^Eifx!oklCMt%R1*O#^Ba6M$=dbM9P z%@7LrtG2-(#NErukobk8zw14osG>B|^{HVA^x^rJ)Qgx(bNnZ$SW+Jjo0zu?3z0*F z0`M-rYUZ`pD9N#sxfer6cG0`JEAFX?rkiQDb~Lazn~=CEowCodrJ~ks?qpc`;Wy1f z8y(~3X?9)Mv5%BODSu2FG#yJ_jF(m|zkY|q8 zG}E7%ER1Lb+u8P*SfG2gFi-znBg>KA-{vjfJhWdqPS?1_)ctWt15|kX9WhdKV14$h z!20FA#(_FecfkW!2eMP?Pi?3N^Gmp}YRFLheyCwPeCAN)xYTd3lWW~a5Jh4`f-=0j z)hpdv5+;zwxx0WaFYU?eM575bf*BBWJQgq;&=b`d!6d8q#V9m3;!gdk8r+MV^X>X! z>Z>JoU$kNKP-HO&zjx#&#giuD>T^`9U2Ob-kr3jQEI(pka9ick9H3ZftiAqR*Z3w$ zxY^`aYpZpRj3xW;hOc|NzYbr6!8+S2e;K}RQoDI~{~W%CqRhN+zj^GL|C8AZ`E-n4 zBR-Pyvldxw7<^u{@3JUG%l|RTmP1A8}8sO{!IDx1(mTPYPpPjjg(SDoI zbhVr|O&}QuPD_`%RnphMmONc(7pf@iIPG_+aXD;zaL`$?29W9tmMs(V37@ezL7Vd8$Y${BMz zJ)L?T3+gpkdS?)1%xQobRF)O9dcyUI8-)&@8)8||`14$tz~tgDxAt}rJOO;^S~7G$ zeMLHM=lP~1+MXsSH=VU|g5Le~ts&i7%SR{52ABq0>B*?(r>j2Cr|q{#w2FA@BhJr7 zsE@z;kigEo0XV_iDa?p%Ny$W&f54Aw|E|`gX_PP82M3fmJRX)?)&9Tc&C;+mve8b8 z)ri?(ttuHE!%nQxW-YB47h-WxM#@o6yvFNCJFl>P&U5IQbwS=;$q9RoGor_ipVupG z@w|<(OI7E7vI|uGjCdGIQxMu|ONCg!&CYbAnxNo_Ki7lfjJx=rU6+WrFxXYm53f!T zk~$mQ+*5)023{IAuq*(46X*h%Z0fWF9uxvo{NU4y^|?_QU?m9F_sVeE@$3rel+uqo&_}4 zZ2@&ZTW{cuwSCAmN>Lv=-{us7i;eS2edaVKA0le7)_8eTfJ?Idv^7r=(Zz-59UYpQ zr%xRSdqw1nOy1S;_HBnxYy104NCbmgXb8*e9CIX^brg-P$uk zuz6O$4}ur;j=GY*fPP^!`QyK8VKOmc($=Lk6jkgW35vux>(WNvWHGU^xvW*tFaK&5 z$S6aLTJY6^j2_*JO0d!!pR4dIl3c`|Br!>o5^@7yklP{3kOp5qsY!Yu@vwEJ2uPS^ z+a@}~;S72W24`;yKn`L?M^I0DBGbIwz9 zp81+D)}YAJI02&{nd;!_LgDW|t{WENFYlXy4}0yPD#(u+{`z2Fjzl{Y8%$E|Ni^ue zDXpllP5hPQz(UJ8!GHsuYPmHBi>WZv*NLVPtl4z;7mMs!I<4-s@%uh^!Mp;>USVIP0Ox!8Tx3lcp3+J&+*f8&o(9#S;UR{ppK7i6LiI6&0H3)w zl-40QKn(w7L}2Na2vq&-wxXVKoZ5q!c-!tsx*?ji5zk#ium|BX*N)R@HekJAR~nl) zJMzsEr#IIlQcYX>H`1mPKMGksZ3}Zq+}T*d#AH80jKnFNfb5>HRUDUv#+XmE{)`BW z3ISKPxB=0SfnG9mLw!rJL9o+B2f^WzajEvBG<4{J7RKaClK?tY3eYYm*BpWUWv679 z(4uG=&=08Vd2FxM&&wi(336xjR|eGB56Ptits&287Z zq>r4Ie4ugELySte2nw9;V@?fZ_d;#}h^dL(RzrGC=d%cPTw4PVP9$Aw;Z1=>L;2hY}H5OgVPKIUfNHYS+pI1=_k_ zF-HysTKVG>iy zgE4vJy-1$DtmH3?8En*DO?POA9QG`e+!Ew9wJi-P$;F8R)hPCZb``wP2$dDpjDWA*y}Ki{^u<6MWwxI(!2EeGp*7rtO^Sc4~0y`IqAOA*BPy1@WIlk-!~u#NzSbvqD>>V76^s}s-VC{ z)A>uOh;XXig2)PpZAOusZZZb_7#j}?>}@$Hv#Ae`xw)tdj=B4IJbXN!J{~V0kGG3D z9dGe+RXhdDws6BGy6n3FEu7ovX6_?A2d^2vJ>Z}J6&ZNG-X7oXn-*&8qWS5P#&RRr z%U~LuS3Q3bo$wEH!+c}dqA8l{1Lg=XIP*7OodCjbR|bRy^`-Ei%CSp8j!NvEKZ#@~ z)`g8X^4>pgzCB)U-X+ziDoJ(XC%bqr6V@^5#_adbyY6ujoxo-+5njH2&W_R281m57 zRjwvx>UEs(J1#qLU(580co{7O$?hb^#@AL3*7?vVVNkVbH_s|j5TgBc;^eO!ga(_6YPLm2_+|Ll5!Vu zLsAJuYD;kqL4uJ-WjwS#0g{W^0iLl>hQ*F1;*;uG4CF##W29adq#~xJxQBp4-RFLz zs{TdcFc$Y&DcZ2fA`=CNTGdf-tbr7GXB!y^F1Dh8jI`;$k0fHblZ4ighn{BhF7m_ z$SflF48PMlrkz!g`Eof1c?C|&yslLJSp5g;in52uSPd1VlPb}|Teu^q!% zui&Vhkc~z;4|)u5HiQ7mJDD9WZLp86ytrf?xY%{WnM5Q9eCCTTnm+sKU*XnyJkGf! zkU%fb`Pg8kwA5E<`DBbQT^|T2TX9Jo{I0^=Zb1g%Kg$b+zki?HlD&+SB%UUCyq-Sv zJq2G=*X#mnYj0sde8b+}cpus&pB3fy(zFJC@zNoEBPv~y@a%e9c_i?Syy$MkW z*vL}DJl7r|X-qeJW8lOl^00KnH5b)-I>cHJ2li-RtX9AoSz$K}84E1w)%4U4B=ZaT1vB@A-K}cB?9v;0!Np}-N-h0Bc>P}txi~LPQZ&2!?wfH7)xx$1@w;_mH;V>qL{A3oCtti+&g)fg)w5Ph zWDC9S02bt%#p@Q@}4tPz?InI`b za!zYn&D?%Bn5f)7wve~FvbnQq>qH@jTTka*eUdej?15~I9y8YJ%2 zxnApT0Sx}3t&+UE?RtX9+m3NG-i6li$X|JXYJbO_4B!fasA57PZB(=YzES-ZkZtXG zG7152hh)vA0?L=5&E@89@{TzGWME6*9LWYTSUJJ{7)>I7Qqlsrd2Loji^~=Oj>Bgb z9xv%)I)xiqOeNBFta?LhcZ(18_mHBt0KT-B^+H)mBw2LY+@dA2;*8BC*A!ePM z!gt5B@^eyixAhTsghHZ6R11&w&6(+sqeZH3LI1;8NeM zVQowDatd;%1Vs@G`f?1#e{|_8;zQL!JWsjb*BD$6WABQ>(o^0V>bvZHd1tWM%}&Su zZN2kXd+X!N!^O)tAE#wZr2(8B_eyhthxM$t9IVOPYxjf~V6=u;FK(lEyVv>2CG&B! z=mC2v${#jnfg(b1K6#Hu?S!Ab3vjd$6org%5lEXy*e#*vq?dy-$%)c!9!EWbs%7P8 zL(U01;s`6b-usqPvF3>;`wkAeDywO;)kw|fwBjx`#LIkq%d|;lRD9F%3Qs}YK`|5_ z%fH*-J2O^-+GZw~Y1K`!rtEJDkWbyYwlQAz2A|m}Lv`Rws2CMf(z$ zHpgG+-TT|r|K0EJr+&gGz|`M;E*M}PqEkLdH{632ce6gW+8UIY5t}X9;GTu&)7si@ zieo52jlBnNopGM}NM`)sZdvbR7UAT~q^@<@xStn$1-B2cm42~bU|Yc?3aC+mi33mq*{2+v`q8c<#<~Y(l=`df4h_u4xZ=2 zOP{~*&|T+!`OQ9AU8ZQ~ttW|LhY1I#EWDQHcA<@Ml-dZKF~#+eRv1OhO-vrS3&Cn% zRaz~iLOy9AGJ@lr_{VkT2E3;vdBJbh!uVR&1Dv0jy~%{2C$1@&{f1CNQqfdM!!Y-n zDyi`NnP2g9X$Z>(D-qc4s~p6zexuHMBPUa5lbxJ~{>Ths0MSXJ{M=Up43~ox47kXo zGn!#WNjFM9Ra>CKW#NHwniV*I0$g=EW%$kOjRD?;mMdwgFWqkZT$-vx;;0O*73q_S zRhkIo4#N{NEa?A*L5onPBR4o;eiNS&VR_4+OgyhO()Q}HT`9#M%J|RpptT z_C?vGMV;_DqHhLyW7Np!qCf34}S1z8)tFxG9zej>w2c=dG^_O-y>e? zb#!)z%H{d7XUus2xR>F}r#a>g2O8MUDSM%HW}#c}K%YDgX*S2vGyXM(WXTAz#PCa9 zvk;JVA>hfKTNkp$`~KSo(XZy|$pxo-<43Lp7Z*++f^Ml|Pk z-SaR#PdZ&Xe)Dvb1|KzAeiWz*f={l#rz{MZN|GMWo2h4D$ful`IQo%o`; zO1~GTWG+HTD$dzw6=-C_G!mLe)%(g^U`3{UkTTV&W}N+*>z8=ah`?pVY6lUAZB_2C zx@JPG>?scmS%%UL@S&0x({eCHBwfv<%*l)V@G*V~*YDdX=pu>Ga&5*dK~GUQMwuW% zqs)1k017T;q0n6TM}&_H(1f%$;Qhrv(0)wNl#{w} zH9f$cji56|cwB#VMv%LrQ}!NDJX(~09%^#$i0?vbNsocGUo7tM!X~cfb#{Q%M;Zep(|DxK&wc|EM0;OCu5|$p$5XPZH~R@(76W0V`VZJu@xdQ_ijD!l}jCj*WwktGA*CtrD5OuS71F_-z(ko z`j@NIEkqw_c%OrT<5%gq*JuDJh&4_Dg?5;aX96@o9jK?cac?XQv2BuK)0Y4lKdLT8 z#Qe==#nK~9GS%I+>i{Ob5jWPhq+aJJQDLE)+w}IlkFh_5p~#9CP+WDpPY2DJtcT`H zQPzDFd0CXIknBlNVXPyKvE#Zc&ysCObT$k3NfMpqTmug(y+n*7Xt=mZrS+}P%FLQS}vLRxW0M%xG63OP6<>3FrYxp{$ftS_$CzLV0_=SJrd z6tlaz`Q*(yezhzy#wp%at~)(r)Ayx$S8=6+VW&pecXD$m2=84;{zp5sk4gIKl-V;k zCpm%cCNl);a-13s?_{X-8~mtK>m)@1C8dTxoDXd94bPN2TZT6cJHw1FlyxN+TTwW! z0<_N$*+{M>OenGz(!p-xRy4}J$zj^1CZ*#%7W2hO9{PWQ4YiYg@(oy*$p22Xj!u)h zuL4i{9yDS{VyvqHMzvVbl{kgk^G6VC#O|+UlrqJf1w8NYuC4|l;8T=hSe5X$NrG`3_vZ=rxrk$+ABlsaJ^V zHB>46MnLZRO5gpJI^+-?=6{9QEq+ueI`9*OrgWZC1Ii$sD}Yf0w4(D^;@II%U%N4CM+e2D1<}O@qTuDzE3rs zpBMhs;7C5ZsIm2r@aG z!$M)Z2Wfuy}X0yIY`>~6VRWF2B$UPLsw_b#S7S7;0b z&sR?tL6SWKn3?boTx_1K2~6o;;23Zm;5;rcjVQml7p7N(l~$c}k`dzdf9 z8^Eyv{%tIMdOO-V0UR|R-^S93K}Ub0H9|&u-Iluupts=;s?lPsxVw4u=-~OfbML!u z2OYbYk^xI<*oD4{)3YZVVvcv4_ui|(guy;-u8dB(z%iXpi0F>+{4r+VQ|!pg)=s5> zRRJiD%m57%pfw_ttNkXC*xTMHC0Rr#(Ub}U&5(kTm^%kC2Tl?;e3Vq}49P^&k)J?u z!~tlK0IiXjB9%9ZL^H$lRXT{c6bxvF%ovJYv69^2ptvH0w$Mpy4cizT=E93M0}T=j zMI*7FrEd}m-AKDGSNVmmWIKgx&-KIk{q6=qf`+;$PLj5|hZ^HA4kN8;JBXDB1w~qZ z!gT~?^6NX9;R}zm@T=Lm5$ZyJ=@-rDIIw#be|*7(>_s$(Q^!$(g!CShp7vCyoa@JanCzj*9$ z#6-7TxIvGZd~BcpwA^p_xI&qrPWd&tLz{$dnK<)VYpbSBgl}iF#uJi{bgRbG3IslJ za(lC;l-FG$YU05F;r9m%@(z+Psfk_W1y`^OsK+xis9zU}-;Ct_fv`=K-3I)qxIp-X zX>z3Wqsypnun*gP)}qQ@fi7*n)CfE+-5+6(ab46Bde2jDeVxgNU9H80aE_X_Bv3Uf z#*7fdy%a;QoQO{vZZ^wOlWH!nrL=IbpfqMK@74Xb0&nl&?&6v+3y-?SWTIRYc!*{I zCYnC|^crf8cpfhAml$1q?o*f(gvxRZ8L?%)B^cT!65qs`=p$CF3d$eGWa+1y!0ugm zTt?yb**?b8Z$a&GyHv}?MheLsSvB^#q)!$8+}@{d*cZ;8+|ph=945&;q7swd(>15{FXNvQaoXK))|}(Wp#NLdrIc|9 zKsS_?CC#40ayGsMi@1M18yS1HhXwU2V^v+swt28@hWhLDlIi?|+Kx0BNm}C5PNTp> zPbqpnUjh!qYwxtnDJNZ1l=Nlv)CSg~#>k}@^X9b7`kgk~T?@X1gnPB%RghErvB;@7 zQTM^$s>@2_dhC|l-SX3v%)#x@RwSQ`S3()CI(!Tzi*yGCz`tdT5Iy4c4x>@-0jj(v z{S<5bx>hjVq*3DuVaXx}KTxrWs87V#@Kw$Imya<7EH?s@gXVMMd3-&e2&G4>f#n;2 zp>3rLLgoxOuSjw_+XAag$N-RzfDA<6g7c_JKl**6-Srfp-yLbsR=Ip>mdm=Hi+(Uz z(hxoTA#Tl6Odqkw#3_7dzwnAv|Z;j>fFpF_uDOM=t%d6m(iQ%tgxyL zpcv3xkh?{L-{G)*XGHx{Bkl(zPBV71kc`sTDItRIQKAWVH{Y9@(>Q*GCMM>zji_aG z&?k+^G4>>is8%(Cs59yR$T;M(;>bl1d9N5L%1QvmOi=h^Z7yQSV`(|f*K!#+9=J>| zKk%_ebU&HS#C!ab%-*MjEso`eO+`eZpoGV^>t+xM zV@ft%OZld+B2wY1V3@>o+#-=UHNZq0Z=NT^iOuq$X!g^fF3pkI;>uMLa3$0>Q_S1J zf$qQ(Y7Cy%tCmPosDt@~zH+)G&(>`4+KGLJo;(pwD*fK2xF9l(O_RZFNh)~@c)y*o zvUpQwlKAse4ojoe(KL9GX}zOCgVv(*1}dw|5L8ii8nRE1oN4TtQW-mC@yL8tfqF?@ zVomI*lWslL7PJ~Tg;}A@Ga77+^TfjHNz#HXAy?H-0{t#VTG2Cj$8P6pI{7&CL%r^kuW|u!c(k0NRvzcf<`b2 zhPI@N_AVXd8GUcv443aN$XrGq+JZ|NobO7{b)0=Zw7$^zrqVV_EQ7Mj)|nI}&P5kF zFb<SphS%?=pDsavA9jN{8g|q$(iO(!Zm>1H*wj!UfU`@n7_$LbU7T3R zq(#SH>UelN$KCq*8$%9?oM>o2a9M@0Mf}IC8AI9_uz_k?Ggp8Aa$D%VFXHgrN~hV3T? z%fI3bTa-{uoxZb45gV&)^lIel3G&~0kdOpp>jwadq2z2VuT2tn@5FZr1yK~`Q4o*V zfp@Y4qbd~lBV!8^?1CI&0DgDqQUDK;+Z*IEnBG3XE*}O0<$Y@v_fsmTt6r#5c{_P$ z13~=3dhcYldwASMA+Z(m?2lbT$Emkb%O|+=I5!Po3O(q;{> z{psBM&2Xmk?aIZvCrAUd%+4Z?mJEo1u!v#lXcx4oqX%Kma|LhGLN zk$XV!)dh=TlQ2&j%9!wdxD)w%)vx7bV}a^?1*X_$lBEEq5*_CZsfydv<%(*#E|k}f zjam#5kra-tIwvdMD7=6HL46-3#X|KP8dKX+OaUv@c#~Om< zm<)AQe19@X6u^5V)uk|XZh=wO9jyGztKnoZ!f0+k8cy+D%~S=KpNpt53Y!757z@E6 z1yr&(+^H2;DU-4YVv}Qr%ahijV!_v==cw?-;49?AQ0#IiJJa{w6@z>{GHk=JL+Ryz z<6|)5Nz2;d7-;i;Z+xtn(SXY#gt6}Ac(N<#XEVcR%pS(i!o&gI;6jQ9C4~@EIWU~s zU%0{xyR0_LL8qi-7j`UIDGdGII2pta&JvK5_5X*H1;k5z8Tp%&?U_fpr*n>oiRA>y z3sZoIP9S(rz^}dz3ExR`EdFHd0sn;k#cG00iEb^gk}%BW{r?S=U9kQEWnaxx*)hNS zQHfd1QojFKmcFwnXrwi@{X~%JWzuplIKg{e2Ws@=6pS+HebnO5zfhSyrb9wNXNjqN!qB7^|zfhSw^KVo}3bweQ0~66|92@OJ2T~2?wxUKV%vIz7 znp9Lal6GeK+qm(t79B-DDf8sAnu=Pk+EOMqz6Dx^oFVV-aHK|n~c$wqcv3mfh8fp7Tn!pu7 zpL8=ayS3W$JZAiBO;Vh!$p>{mX^zV8_#^UZb`UY}Te8;kTDo75xVH5RZoOX_B}m+p zmp^kTwX`BAkK^n`O=A>V`x%~S$xzThEPva=5miEf0WHXJgBb`!l-i6%8IQ3UP9ckD z5xMMcCoX(a&eQ#{D@L~dyoW{;8c}~SPR&~QMTERKu&E~gR)sH*StJSnTlXiBJdVyG zY}mkxPai1IV8^9Y{xWPPIj4i+0`2M_@AY!d!b(&r__D(SOH_-(0x$G_|6pQJq*3e> zRC-%bH`?`cs6($BkqBUSBttRyKKkL5#EBWxbA6MCIR@%72{)WcpUKGb$2bSgEAyOU z7qv@VC>t`#k(vVJUB}}5pClQgS35(paOH{>$r*N7oX3?4esbg)VJcv~?5E4RI?ZiuuY33yE)5=I!-Xcrcp>%x&{0dk(<3SgxqA$+dk_QOBcvO} zBUor0p3Zk}Yz9;$pd~srQ zt_Pa*PcKh2IRg473I$u@C+pLk5H4%XbD0x@_9rLz>!*)KT;giWAnxe^O}Y}RZe`~z zC-zeUhkT>VBjA6z2kuk&8!)GJwSL2vsbD#M&MO?S@0WwODEKY%>g4$gk5`4@ZYK+P zYICoW9O1N+QB~~#X;E`ZDkD~3XEr#O`YU{#>k`L&jzjbU8)~UJrk9t#xNHJ%O8@!( z_!zA!nfAo&nQL*^$j_-}c8pF~uZ0T|bK!z+2*$!)g^IN9TB|E=@g*v!v7vKBo;wtr zTVrF}R{B+sXA@B@bY~M6e`m7?Nfvp^?t-w^qIuU=RehW{An6h*XGgE-jV@uXEB>ZS z1YYO|y0i4>pjxLaxhto=XlBi*ur+gz|KD_}?I}jRlK1R}9Hve&S6DLqEas|po z4@KUi@8mz6!K$Hu<<@e!djQM>CMKj>+UdYLKC-$JY-g~1_vt!$!ULiuNYiKiUf|98 z!ry6k(>h{G0fPRkTvAy^zh1&(@KerO(7SRh9W&M`JF`(q$8r^a+kW4#UVtV28h; zVudTtZVZDR5f$4rQ;o6E_&4uyEzdJXM8TPx(qa0X9lwHW-XM<;I(*;iI`44(-gL*z ziu*TO!fP}7cPi;-Y`3qEo(F!QmJzRj8s=Y`(Nv{MG_Cnk+7mnj;*KVIBgQet^jj7w z5g0&f*>eXXCj7-Wt*H68VYKizs{bCR?M=I&J%L)$*D7*imf!KDtcb~t#h;B7{=CoU zS00{Fc7Br4end{4s)05cU>JF-uP-*?TF$x=)lcI%(RAXBHd+fC^_K_TQQSbpbR*IW zj31rSy@isJy55pViEp7K$h)`E{B1P4#%{8b?d2lbeP@B0C6`KQORsBw8}SBqvMNqH zxcSaB5D72v;uUhf(=-Zd>fhnP+D2}9p>677Fqr+S&b0GPtwfaqdqyRwjGu}QNcRUqmSXWeIuij(W>7wFmWT3lUWk+RpbCOJYX>vrHRe z^TOCse`T3A7K%m<7cnggl?W_`FiEW&wfT70xmWz{Buiy_rX|DasUh)^rH8 z-@S=Ot6BE{7LBAjDEIB+uPG>_Wf&Kxq7cHm5OvXjdeM=tzh1nWH&8#qv5>U@9_wC)EN2#Js3{Wk&E9o z%l3UpOv4oSh-~8z)If&veoK6=_8Zi z)&@K)_d#3y?R)q6k(ds^mhA=pF69;KmVNEQW88`7ngm4w{aOyzUwa9+7~z9dy=ZPArjH z-ohi<{C^i_uEWtezFxHpEp=$xVOwD3fYW`++LAjS=rf}fo*zjLjt+dF(FKNgG&(I9 zPaAytJQl4%OI_D9%O>1T3_Y2So6?68gL#sx7~D=wXnxwu;5M%Tmw4L=9!n_|p4(a= z(pZ@RA`N>Pnf`D=83kpQ8g;U}tWB_&&o085aqb%)H6iq{aHCxpX>)x}3HO^2Oj;7z zx}wDTf-otIB$-A5;c2>JM;um@01VSO%Tz*WAp!5$xi3k9 z4{!v_i01mZwb%&9G_I-`7S1d(k5Ei?uE2?4#5AI+W#&6X#hPro(Q27QYdKTJT_%dQ z=Z){Jl8(M?G1}f+SQh*;PXFn?j)|HOl9u;72L>S{oP}9M+@vH5ek$!I$MRZtJRt{< z1en3L0s)VN$Q?%|ReslnolFU4?%0*C%?2nf|5rO``X*-9as0RR4Z{4lfW+U}AR)8D z{kMUGu(dB|x9@@&lZECH#-8~3wtdS=b9-AZI|c$9H3lMwa7i1X-rQPs?z&CJ9ne8? zBLXS@5DCc4wLM@5b+aD|ZWI(I0bp4U{kT;&(OB}wSF^6zG3`(v(AtU`df0sd^lYu7 zFAAEhw5tAAa1y3v7qAyK*1t?&Z1ZkY@U7}tf5|WTUmP4Epo6n%?)rcV1)&VuXRJCe zt8TsXCwwy|QrsDn^LiqzaXIE}2_bLD1Lm~TY}rthD{1HEq3r=+Qx!l&)!P}~y9ub| z*ekT5E5O8SL`u_HU-j_vaPeyFFl*y)?A9V$TKaT~pk@qqBH?bQyfv zdZ^5+oB1|H5WSP4$g(;K9UO%7B62z8A^h=4f~%3BAZU=AK+4XD>nvqbUM2xqIyiN& zt$SglVRgbRH7>LYydE>W7 z8OL!_dLHKznHW&JmSpmu$d4dZFEwA4PdF@c&aYglwDDXJ>jQ^BW>aUU(JcWs4<$R_ zF*E*eFhC{r-!PyKt|bSb*y-Ly3u^NxQC5RXZs%%c1N%gWr|g2X~FD+3xIXD{dH(?_0?9Jb~uiINPS8)%l zyHmiA&3Ez4Rs}FV_D-xO;1HdTao<%yIgV=hN-yeS zqcjcy#JY+7rh(1Y#8B=yTn2%f$*HXRCsl<39bF}x=T0#vfz&e_B*$-x&F2=t&yaJ~ zUIu=84Y`sFD9}-&{S8KJ~&x9Z2u@)CKN})i!~mp zPxp69@>q5rC?q*KaeIk9JzPMY3vsu>6y}H={!hICQ@<`LK|M*j#8Xnhv4(&uTt#S# z6wGJWJAZN~Q1WFZWWt74!(fXcpEbsOFeO@Spd6#d1e9ZR2Z3@-(@}~VR8Yj4FCn!BGm_GUJMW3q`m2z{<*|aXyiHVimMuh9(Vv z;!kN^h6UvS?Pq=`sYJ4D)xq4)^1#1_?(sqf`ZkPd`_~hk^FNxvKd*&fTh`li9 zW1cW&6c=3YdSHouMtT)WjWO*=)at@3E+}Cv7Q~U)AEH6OpBuZdAS|sgvGt0U;;UWt z$T7|W{R#48(b*SG2s+rpj9--!yDW|szoSleq+vr9YTt+c6p9*6T?MxNS9<|w>o+8s zENgZ9HeA>-dAgf9bIc?dSN;AKJf8Gc-WVC?>T+KcEY}`EH}|H95T41caVpa#fpWO2 z&&FDFLDt~x4-i?~a}_DnD{7BUE4la$UkA!XqU@M@${pXsow4f87Ut`YQsuNF4sHhg z6i(iionB3q=Ulo2Wh!nX+#kV<`NAanW#s;pF{0U|i)!2VQS$(Yq&m)ku}T3)LqkiE zv4`_+J;vd<=DOmEAn>zKMTCjf%Et%@cwf6re8BrAmtXn6YHW8ovr`$o$7bgv=bWEh zBq*G*Txl^7pZ>k=584uQk`S&UBr!5lwCQ!G3FZ1%)<#Msl>04fV{Dq-&@~M6@2rh2 z;ld4GLzB9j00^osPD%2a=6)y1N6HNZ81isarUN||_9HI4VR{zWo3Bv@7g|;VU2%hk z)TgcTKNhbJJ~P0688MAt%nidWjYh^H9zm1~t?jLJw)4RJkd@lnNcUdObc?%7>lAhQ zlgpuGe3b_s82SA9v1?ez^VGGMV=&j8)`=RkbNMIBBm%aI$Hiu=wbtE_UAwF1-;RfO zpUvs=3Jl@WJ5cl4t2OGYg#jeZ(dFW;Sbm15e>Tk^(#eNb_fS|MGmL;L@F25p4E`)O z5Nn1O`5)3?OXyN=vMt2i8!2C?i!}Gqp=k-4fE#9-Zv%2Zs-5-!W7q7k-}0_<1cnA1 ztT4}UcD5$RhEr2?;|$hCh0qq#LKKY}52x~%SnxplS+CGGaMR4-3cDa#)T;g}-Ms^} zMFe+`^@^HwTKPz7jr#lhqa$gbpE)u2s4Ldb|^a*rQqmx->NQ`m`L zAUI2ZsJ-NqmHx0Gw>fx_E=~eFZU3N*5>l7J+lUW2exQ%9G6yL>b^yNjt~jF|boZ)j zJwtEridJCWzEIUCgLm9XMci}Dg{oMF^t$*vZ{p7Zf8d{B$=d`apdju^Rb1IdL{%Jp zQ6+H*evlvcpeUauarlT!dL4>?ns-i+sc>Q$+GC%Vmxo8-;>wG*hX&Y;5lO)F+v|*~ zDW>)akuK*&8Q4##f-8s7o8J$9P`Ksmk&Mb z;(3cFDNsb&Prz-XV6!{B{_*F~l!{~$PW5IkvjLs*;P4NZ$FG;7C$(o)w{ zV&dX~e05eT236_>&bn`PfxKg9S0FnRxEoVf5k7vXYz{4sBx4S3`nH6=EyZuk(A#o= zBm>w9{t3KiP2C*ftrEQKf5#u%m5eigC!8)s1q6WiX;GJD6a#!9UbCapqB(o8EN;g6W-W9SuQ$d4myWEGJZ|xWryH1C==op`t(uEb?~j|&EsR~({$AI=Sj-LYhpSz9s}JKVwt^}z&2!C zUaf@|xVO>J1~_W$yp5nR92=~CYA_XN}#rt_I-e5t^t;P?5XUxL>sWY`kj@3-%Z$zOBDRe#f^Qn;DZFqbO zQ#p_*%%oL>GnM~E6lR*=h(cwJfqSii8f^*9BCzhHw@@f|hOqh|X7FlLvxG(sSa%Ya z56Goi{HyLH&hocrBE_-tLe>=Fwg!jx?h~d z1K#pUd;U7#E9j5Y(@}ehQB@u!E{lfG zxDW;qO*HGq>vgJ33Imf*e)HK_N2>0OeIG>$Sg9aeKFUZj4i<}y<#kB5$tUAVdA|R^ z5_w;_2X2z;jAB>v%q&`qvaj?g zd7%#-JhN=BD0QkZkb?CoQhM2 zcj6k?AxiJZ?dldRA}7^3Q63PIc*N5K)tf5)b^0zW`Q3%u0iQrR!>9wdtb#hUv73c- zU}TBTt#$yuX;3wKYcyt44c|$wCfiJC2UxH&k>nZ0{D~J3&+`2)!qX?E(I`G=8W>v| ze~5ShiZbv{mvz}j@(%S!wqR1*5!t+~@kEi@ZwoJLIwITIC;T!=_QbUGHZ!(`rcd}s zD;bC;R)}o9gQIPh(2QPsSxQ_!Wvg`RA?Q+aw7nO8z$nv#2C!QN{oYo+=*417zWT2` z6aHiCf0JhtJ82Tp{`~(?tJ1aqr&a}*`M;=DS-kwKR>en?q}|q8wK|K1Yl-lbHh7u9 zP7})YW$l`2`}Dyjracv_$L&$uDan`jy&bPB3=Fo0wk=N_ ziG5oe!9_=I38U|Z>2Kzc@VNLtBhbDOi-hy_&HKdAByLa06e{T8x)@u&gSvV+>=(PU zkMMsbC%;j_J-@Q7gE_l(O}&dQvm^K2=Crv;-hw>@<%n{9r_2&{Qs?*ly}19-Qp7+V{U4PO%g`j z)&YA}FiZ2%wCHsRcrgxt_zc-CS%d!QYh4G!xHRl|SGMcvT*C`!N(L6I>{-nj$A5cN z)4}3#X2CdYDg+j*G!+7iRrbEI6_kE+UrM2OKQRT?qa2&6EH`UV=Q13+#9lMfDz=gV z4E!R5frN%IbAzrYh;z3_oGtSVYQ$U}art9^F#)O^bPY-GdwbiETGpgyhJe_qJPwg1 ztmf)JJt?X*%qSWx`Y|cS+$YWW(fuAJ{dqXi#stwcE%uxMSM=1;LY_*Z1*elm?UX2T zo@F?No_*jbef`bdjq}6p%FN78*Is5D-0EDo{~?4g_R-mQ9RaY+a2Yi^z?}X z^LMz*GPU76?eR`-C0cmeVYA^KG^!Hy9pS_j{nIIj;RlTg$}<66Ry|+(!Lb?^(vBbc zFWcN#3=m;A>g#(f1l7x+R{Amu@jIW{<_p_udpzc{I+a>k;{c(eBJ~o!l z#ua*YH9oj70t|`bJH_aUzpzK-QwZ0B;m*LQ;F9}dzrV1Uo;cEtsvlmeih3;(@Lszo zn0|`9kuea?^WG#i306Ufgs5k9Ae2?V5|}Ft{9KP18aV|%Q{D*4hQ(g}#eGs`57$!{ zQp;vc=xn$l<%SvI`aHrs)DIkY%Zp<@<=`2c{p4 zo~d|`VRrQ`8JW#VcHUmU0x1iRmyhUy-fI%@lU{FlT>kU&`4-aVkz;#vu8>Cm`v)oPk z8&heN5gz=4AQ32XO_;^QI7X0c`YR~rz(>IJR?xY${dZ7IiSqA)PMcD5BqxKmrli7W zGkoXTg{`sY`QU*T>bQ6zxUbK^m{?luqM8`}(i@}dCKES+y<|_s(GD&Wd-+#R%yS|f zwS5b;g4*EdqNFczw?bol=x36@PlJBBQC6fOY7mI-p!d}1>h=H4^WirSV#m~?!Z zScGSLGFQtBCy7oz1&r|lTf0;kX@gJmUo_V2a-garY|USNvZQiL=46Bb)ocm9{h0#j_rJ6iF#$5P`^B5LcPpZ@;5iaeip z(485n9Z>6D5{@0hfJkn}xDJ_F1Jd&mRVTZ;j zVQLs}N?7*Pm&oa8VbfoqyoLL!zw>7qlt_m1CHCf!OJe}WHJ;ph`IzD2@9W2WXsA?y zJtgg{cY96M>Y&kCbPs*VU+bYQsD8dbgYS>J{;H9|wt`>3d`Gs<<%sSBR;m8mVv$|0 z!H=jtM|Q1udE_uL9;&s%JQB$*uGsq^L`OyyWx5MO%L~ue(*BRd5`z6DQFS=(2=T0J z__xJE(qn8v(!J9);*4UC0lG(=(Rv(Y1jEv6tU*|k1VSG@MFV=K362GJ=5(-*m?Dl1 z)|@~H!q^984s8=2z5x1kS;LUnQU(6I3Wke+5p;DC6c4=ij$s9{v8hi6Mnsy5jx&FK z0tZq2uzR-Yx2y901Aabb58!k1zJ4EC$+NmbxgRRL=b*t1;e=98A1zX)(R4^!s;?WKf@gh)d#>azC;y(cdS zJC4qW?Mu&KrQy}M54irT1O~rLH~)Ry>_NhXQUPyHQWznuhO-A39Fu_pem`jUgX*_2 z(eJYR;QI8+jHAnQGm!K)JP{h2DvH^URYZCeq(6W)I2EKgJ{&2C*B4$@g3@gC z9BhwibTR5Y+@9^GV#gK~6w8Tg8kKecE^#MzcF_qk?2h}ZUy%^Wx?&eUdI_|#;ke`n zGL@#KOQPQK=N8ggY1DP$Jay+D3DvJi9M;^QR zBsnC-2cF$AfC{|Nxux=c!nl0=WOZ##mUrMa;>~HMpI&)_Vl^d|x)7iC0+aFaZB36% z`q;uX1DO*1UlGKAl4_U_4^0Z%W-}!<+4?hVs#}b(I+O`*Z~7*5hXs=WWR1FeSlN}> zakTw6y6Bu?T%F;;?*GP9ospq|g`cd3Y3)m|hg(8a-hbA^>@AmK#VjH_q6+MH8y5hG zyvu$y+nuPnRT24UE>8id(=_)ToxL#n^2rv16NV}xE<442eha?4lP!v)Y+n9>d3TM4 zn|Ax#B%wHq%-|xj*!fg%oQKJ`S?BEp9plZDEWDoW7R&n0UN{5%#M{Tumr)dM&=n_% znFSqb1K1AE!XzJxTeRrs+A#-F4Np9LyKh&IJbj%zT$T!3jw^Q&7$y@f$GCK>u1FSn zs*<#--VHLuuR+DH!Njk?%>>9N;tcxN=bl|cnj}ZIO|WIr^gp52wa-AZDr2WxK5fZa zG?Z^W*GD<87~HOKE;#xhwFp4b%7`S8C7=o$^=B3-;jhcb;CXbS=UV?LYl?@HW~=ju z$_{X)j}PxNl@sF9*~TxsRO5UK54MRynwdtfIK-5)RaKA8%LmU1}t83tLGNY8uWi~IZQ8QBysI4P$V$+CePW9^M z%5cEDw?bP#_zo_9>ZGi@t?f93Q^8s+t){BtGH6i4FcU)RF#))Io2x*9#_7)j3N?kle|y=iX6o_0*FrbB=iJ zvZur>Z~)V4qA|?2>X1B(al8>%4zbDHwe>)()#)J~;8)>bQP}f!e?dCBXRuh`e9Gx z1Uf;>%ft1 z?Ky(GYjP#SxLZUiD109%`nd+fBmHe_Hu?A7q2k7(B4MGYo&!>xxio7I8VKb`cJM2> zzj2O%VJ*VExZ3qWjb50pay7B|df& zsUIhqrEz^~qBn=}khX6Krm477jSwCnfQ2>j&@{Nk*rZjkEE8!$4j~uZ7F^FAjqTrq z1u`m_QHcHV0<)Cn@@~*|d5AEfVYkey`8HO(jo;KJr0fJxTJXZ_PSme8uXbF@9D|GZawkR&Wae*m zzI||+8@{s=u{np~{qFKh3{dLw%LGv2@?<_00G{qTw)%av^)k6^wADW8@8uGse6*Dr z^I0g(b$C42O!dc)v}N%{wLN+@;6n4yLaV;0W=!`GT>FDJB4?(oh{<7Grv|vF{j+G) z0v~E#A?fXd&_oUs|D++iP#?eEx*Y4_A)YUOz@uI*zH<@*;lvYPm^S5sYbX-)5!_b- zB{y_Ij6Cz0bWGYw8d^{Nnt@(RKx)W8tBdb3lMM+dBh+S9HC|;Y>S}gLRY90LXG1Wn z8n4__@^_??h{xgF!t>JRS<)G^{;A*H=;EsqdDC!;JFDtc2D$a_ao6fVHPTl~tzY9} z44#ZfO}+!Iv-z0L;;CNWv}sdqR=tL6ys@ON0_uaIQo3_n3C!WtH5zh8$xH>MkWGn` zG`1fAP-aeMjE_D4Sqs0ki<7iMAhLoS4xrm8Qd+40Llm>ty+8{INub4e@w}E5Lal7; z{3~?LLY(akXahm6ba#n94}_Y<5C-PrHYV*g0g9KXi_3^%U zF!8=vA5gnYmzxOuF7xeqAoe8p+~5t`rJ$H}3cyq8XM9JK5BPUS96nwex>esZ#WYGv z)*B10>?nk96@bwTLAN@T2YpPgV%?$R&&hB7LI9ja)%`sw+Eb9>sm(K<%ZgyD1@h+S4o+;5-h``^ZIYS7)u=>lLuX z?he8yc$}VkCR*gl{Q8nk!Q`F)-Bz>B7&);0XiZTf>QnvnS6j_% z5wNWW=JcPo8cD>rwwf+eU|Ws=-{q?VGts6W_fmiX7_T>kzJw>y-lSTJ+ajFkMx}%A!+?du#>lI+Ox%Qf_V$UPd z?{UEOS7)w#EwD3}xHoI_@v|72)mvw7Xgh0x>YuV2$QL|fU|CH$*en^Br$#x$3okvw zYpl`^Nz8Un?7zxtuFqx0noodbH7f7^RaQf%dGb$L&8KavWA@rhU}vt3$L)-LaHlsy znI}z?Z3pqd^;yGxUe@$!`~a@^*%QTAhkrVAyR>#<~ytsL8APrrRd%HC3j+{p#qDr%Ga~H(gL@ZO()FgBUa#Ga+*)0E$|m8)1j^IdwQ` zK(Knk%p(gV^N_&o`#^UPLKW-1E%)r;0$Dm^X%;3V>m#MBjgwnU8hsKs6CaPDBTbwr zhNIibWi{2PsoVuB^zf4oc#3P`Pwt430bH+XoxpVLKK;w72P{T*B#qTV%n6q0(>@*l2mXTSPm9*$=cGG`>qw;wBzRr6&mkq z=y>(1`AjK2`<1<>NeY~tsoILv~9!r`Wg!U*r)iDRk$Q2$G zE3U9xwwTTzJeV<1i>H(2^JV&v7}w<=dM8si`)ivH$Wfyxfg$~P%7KLR**;^7(*Z!H93o>DvFG`=EQtK1sFk_64gb5eqWi>2g zabiG=!wx1z2mk#$$MehkcW6vm0|7;Ynz5gB4#Gy!Q>mFM!*!f)o$EG37O0t23@X^X z`J^L6EniBoWT@6mBZ6zr+cLQFiiRf}9{u3#wL;Xim3gEGEmF8h+bUH1=fcmgf ztO=-)b6MrMz-oC$g1R`HFG#e^u3yOd5gt*qcxz$W}e5 zW}ct_-R%TR*hLL>4x#a?l`J!|)abxMQ@(dEuEaiMKAl$QU7y4R21@m`3Lb(E{VJ9{ zg^smLk1qB4oNa$Z`7<~A1esEqJq@o;XCJ2H`8)T8y2SPj-(@St;%L_>N+DFJ(NrnT z!cmU9?7~p`PyJtb&3-tHFb-r_MlT%%=uZ~rS7H=jNfqtLlV#E6FBn0l*s7UW@s^K( z7ihxijDqO5e|aPoJrKz(0@df;gA3!7WwAg!NcgU5bCGF`lO2|NWi_^ z+S>Fr`gRHZ&OPPCsZdSq#Z$swAvfRK!^*zeELIt{kiFy~1+5O4FYOBRNT{5mZdG>A z2>>WQxJ4}bkGAg|4^!Qui)kvN*x%D>;ag8q0Vkj8Plj2`la69oGSy9bm*Lg-5>aEb z*h$?M(m4v($|WubA1=xj&#H}sc>N*9XF;LNDCMGj?cfvi8Sn;t?9?N3=#zaQRQq7;U3ysM0t*_t2M4xz!Ybr-+D$bI1r% zMUs3%kvHAMBauYi(_LKIrD5O}6@(Ga@HaLcwtr9iCBxs8a#I1ZAfMJ6PwMS8>S>L5 zGt#apNkrpawP?0tU>cGGV-Ce$!QVUTLN6I;^H1!gxVChc4$A0&FJQ3+o*E)#cdNc& z^!G#HtB$|ZjrY{fG;=`BO!vAL)vVcan})O45~Tlr*Mw&Kwo2u!s@&63Ih|&c3*=-- z_eZh&kYQ0p-rNi1#ONZA&%Cxf#8e;T?n5*;{~UJZl)evneh8VtuI5RM9;7Vy_`E}G z=hXby3B5*tpYZ49_kUjg1Ljx1)1UYH`>EdFmjIp}O8s_;VbSdajve9~0P$vMmsgY4 zfAyR2JcdY4^8rsyP=5QRKXLszP47qJpO5h8q8|8#9=%Z>y{}m(%V-~L8MAsO%2H=b z{|{?#8BkZVr3vGZ;O_43PH>0dZo%E%A-GF$cXxMpcXtmS9D>aud2fH+GvD0qAM>Nu zUbU)f)hXHMoc%miwTFAeo%Et4Bw`O!l&D7qiHIkd3r=Qvn9bGW4jHs5W^v|Xi6-`l zXSvdc>~lhH)aX#`;Kl+~TmWMBn1`3>s~a4CEYTz&M?WVGy5xqNr*JCA)cN$EY_peI zO+tzv31J-3(c;7sfKp_W$RT+l4%L?4Qbs#1M=}lp9bao-E!@m?C|ho8{4lw zs=*rzB%&Lt|ES~7>;NbQd#)euWm%$#0OK0pI>tx~aO=PbD2Za1X^KO7F$I_`jXPI} zX1jvysnAa$&|yB~ef3VVfkR^Xp*M1*Mq`0}V)+-#!&Z;BACriOcQ=<;Z)%5_Zf`*N zDI|UNV{C{_ivAf8OElI0m(dR{nz5<259Af(}lqYg2+eJS`@GU z>e_#LFcXF%j6;^-qXTN4#{M>|f0ttP25gUFmuIHA^`oOTH#w3M$^P63oP`G;y#ctX z5Ra}9DRGBBM(R(0;T0ClYe$)V3|ROGa%5h}jqo0Yoe$tfs)hylKt5tLq1@$>mb!{| zOK!X<`SlnAxN}PLEn93KL*{X@TVU}49yr`^q}xhm)#ULRb=G~KL&_F#yp6x&H4$OX z^gf5AFhoDql}dz*44b^*8SClh&!R5}rqeuq8Rt>pknqqlMW0o>@MQQfVB9J7g+r8ip{|B5JZpHSYv0 zWg$e0`(|hqZs|Q)aL+4|F7J7&Xno|63g?&;Ic2uGrFAJQQ#Kn{)6F}`a?Y6ZbxvXw zUeKdZbk#smvkwc+7K>{utRBsBFx2eVk^)tFJ1(FyEW z#cfW2URNj)}!2~dy{P%y#1 zSB)|!HJk*^C@M)#N8T9oQvvG1ERniMOTD;)JRNOs2|$qqC_m)mIadK+sVk<4qvWGw zXcFY3<+eGNxn^7nfj>g+&0@83wKPZ)C~iChm_lo_qvVKYv`9qLB}_U?<*;UmDw^cb z_cE;A8qL}5d+lV}64FMh^@V zmuh(r$2m3jz^WJ9w^56JDH7#87T`7>mbyv=S!PrvAD)gH|H+~JbY)t4;m|&z)amd# z-aZrUDxT^1Y{L#umNITqW?|ROg(q(E%sOXon#nAu4EJ*c zcneRw4{a{7HboL!yrU{tve!I*RX*OJve`{{%2>`c@F;Y?4-%1zpTG^BkBN^vtMU<$ zBoaUZ!X_Y<+RF?NC3RxQ`W9J)4V%5M(t1+^5Z3^~2!q-b(KU0Z(pqgdRpA<*>^IcC ztg|@GeIq*2s>@f&U zoG-R+3fMZ(Jn^Ch zE02}AN}#sofCQY?UIcx{qBIyxB?J|dytg06MheLlo|IX+4vJ`@kgD?IqhbeYJxFFZ z8W(v!5mGdoa#z@RD!#3pY(C>u9MLPSEN4m3Dx^vo{K3dU>eJ#=t}dxsuR@(Lz{Y7# zDV09laNhcB?9=2$N*tP6)jhZp(K2e2Buv_BoSg3(K*MR>44X}gR%Heip9mB)AmnL-cz3ZqRd=?J8Q%31` z{-{dznxqne%I;n4tqUNVlJF|k%L#&;CXc&d;T>DgRsNX_+ucq@7`RYNBI{5~h?T@z zV$D&uxI3*d+`Uc#r%+1;%ZQbi+rPuJ%6BOYd60_Q2FPF#YMJp`R5?Zqz*aEVnPNJ| zpH;t~DX&n)Wc1Ji158LC3h0WuHkpH#Uk}|MQ3Fx8Oo}EC(FS8^QZvSXf3KOv6qBLc z3I>Qu@jIin^&?HDo&ZV?RN7Zc!PeF-Cv3ti`km1@e;S_DP4YWC7oY~XEO=l>2b3G2 z171--do7Rr@Ft~(jn)>?phw`H@&w50dj{i-Ijm#oc-wtezH4*_?yo(l!?3IzEMx_-j@UR!Uu--o8JDpQh5N73E!j)I zCrkCO^^Z;Af6K~^U5CAwn}ARk-u&U=KXPLJ<8-X1OKe<68g2PX(ZHiX22zLKc2Z^R zn(O}h>bn=Hk0VfmqsG~RJ;-iW{Uh~DNiqMOWn3x50fGENHV#=SmYPE3@N{>gxo{8! zT|T12wV>Y`o+xT!kVLfx5*0smiLldIG1B&EyRZFkJ^uq3FJPoK)2 z41##KH%YUHO(jV1kI!dS!%PK*1_k`jBX}a-B`w0r*`AOB=5)$sUQgvApHe@=02`t5 zGv}21t(6Mi`9>>Yt2~q|%gI}lMLvv4f};t5jJ9ZPqL_?K9C)FQ77O1cM$b#9$3OV3 zrIX<}UM<{tR`2YFhttz_9<@EHfXt(9NpLgewK|v3b{?dugoWD-R5d>)o(2KV!{~)y z7-&MtZhcIoj^5oJbA=PKCQ}AMLEn<}UES5pk!x8{#Xg==MP(f*R7VxL4!pQXJ9cmTaHp8=_EvvMxebQ6+mU8ra$q}W*W zNYJ56GB4oX_)1Q-0e;KfwN@60*1bumy+k6(zIHb7+;dWp_A1%b%3Wz_GsXTTn(BdK z<6OCw@dEG0Z#Hswz?{(okK~ZmEAT5&UgMWD7)AY2^^ZC{zM`{aPT+8a4QiQSI3RC} z=P-&Hyg3hg&Q2vaN%0jjn#9u?jAIty&m=RKmqaj{Bs2RmveWkH6BvxEFRJNJW5YNX zsUADKOod7|JuhtN$#4!N^y`+B476!ZC-j@9k!VTSk_t5O7dkpZPt#&_C#P8%Sfj~$ ze$+#!+8V&v)ZpL@jZu<=Ub1y8+?|4OgofQ$>R04J?{$ODWI=^a^qdy}3!RYl)uR zO1p=V%_?RQ^$dQrhq3Jq%mo2y2ZQkpUdV{&f}h=kflNpG6C>NeH>&3oCqy|cXx}h7 znubIDp?$$#R1YTyL^B{sKz7L2t^IQ9D~91&`TWIK(7`8O6Q3BtW7n((`fulsS;`uu z4SimVZ!9spE4&Uq5Q(d7wrRnr4SoG6bioOK8EYx*-uoI(a zu0nN8+yIjf&xidD&&Jf4jHH@AlxEvY)zAW8y_oK1)xYav5S0N`)G199Kj}+Z?*?30 zNew91;=JY=skLJYEW6>*$zU@#B(BGOg}bwREa}@>^tHiN$wEU?8ja4AI)+%DpRc1s zdjzzf)s%+Pl+5doR)S95w|?gf?7rhmmm3zT-FGl~jF?-KQm*a31TLSY$+{a>2) zFodP6Z7DPSh{~|MJIG&wgpC(=vAQnDOGj9}9a5FkkZ%U_YD-0z9w&5_D=M$ndP-BZ z_m*ZVSGA9#9n!FVzHqJ1%N+p>sX72zvZ`}raj9fw^Q*z7w2}R>Z@yx6bNlo_#ma^) zl3GN$p@TB{PI)cu#f`6Ib#ve1kBknLShy$aQf_LByeMS-=4GYT&^fSFt~PdW_!6{G zU;1k0J%+~qbTI-ps8HtKjrOxpRa+O@ajuNF8vMLaHDmi%a9*Km$2L@Xz5JaYaj_pj z_GLMx0xs01I&~-Ds?&GI3(%F>pZ`jgKRTMx+mk^%R4~A8hLOCKZ+6{TDLR?om9c6E z10qX^_Fb>AoT8Y2tn#1ep6{^M=c|Y(p?(b2jp_WbtT*KGT6qq7B=)-+bwzLY89&?L z?=G5Mz*o4t-2E;gmk3*|#Ryvs!w6f1okE^29G;NSsBe9;b3DH=0%shkrNu#8%rFpt zHK~KkHzw4>)vZVgv_Hna3t(N0$oliFw`fzcn^j*c=FUbj1&2L-3&sC}j$n)aT5&(m zr|z_`hT1ZB-^EAl!z;@!8;;K^DJb>IBI)NSn(Sv73+8i(tu8#o@pM6cqs#$sQkQEh zc)KrlbGgIgu`&}dhBd_{!^B7LwMGVeqRa!2uE&sdU^nO50`tRDd;aY(*~g*0i}QX9 z=g7t*Mf1B;Wawo0LLI(np5q}ssLk5`7{lk6{l3aB@DTGNc)EPjgR zeS4XxRff*(zdK)Qdrli0I&E7^4B9{P(c;x|>(bi%wyE_+XM?YvQuN7Nr1o?#;k|FH zZG`xn5hePr9mhKqGyS-q@=1k%e;GtfdHO9mW(zgF8qM%oSv`Zmc>XfMNReUqR^avJ zy!9srevs>wU(BOA?Moc#;J5>_^jzGg?ha4<-p3Rv(k8hqE?*68Ul3nzXOnE@ouX+? z^)4x+lDH=+)Wvj=#{GUZP+gIiP^CQ|{=RwL*xNWiU|*aH@MYns@%7l*f5(5TJYHGY zyVZY&4zNP23d^~FDC1*)v%keCAI6}885W-tYtbVqWD^&)$!FC(!;IK zu0~&IjpSDnse$!KS<2citz;+k)pqia&?~j&J;uI!fz?oM8j-r9PYTIA2feg&xrVsE zhWn9esjV{C>lJu3=B5nodx~5lX;V7f>ypGJdzN%Dr+Ope(S=JKBarB@Um= z*#qI^+(~L|`cs=vX9Efrt4-}wT_ZBV!reIt^fz!dF;?iM!+^CGH2NAIhiK_xH21g} z|4KdqyG+l#Z{AP4UtYYQAE)4kC+OK9R{St@40V1^&~H`Z`@GHu-f(_h(t75?D(!*& z2EGzFMvrUB$~JQb_?5vIo?4Lt=V^v+;g`h2-WHAQc1JL2`)1Vqb#J(&p6_uFamxp< zDwp7E#}Ub9xmik#Ox57Z!nuv=)`fS=iM|=U4G1(S=FLd4EJBC-DMv)>7onUG$2ene zweJx8d5uOCx6FB8cMZ|TKY8jvcz*^VA~IVefT+=adQ^WfJ(9A~QI`$h<8f%|nqKF5 zoI2<%YH^|y+%=csK;>Og-MitKPeG?>?Jd^`?(na?-AVMh$eN6r7zp6bK?W_TJW z;m}u6S7-=#R7eZzAD9Tn6d3Bz;hl37=@?9lUIQ7JCk-60cc*%4lx&s#Q-OxYX74M) z#Qd5G9?dIn1$Thkm_fZBij4xLqHy%_B(Nm1|M#V(=Q6-MtWWN5F3qb;5M}Ld90Hlf zu4(S$>QNJ8LBDAp{@w(~fU3hWy^<`=M4A`NaG_QmQ0;!f**b)MY00|QeN&-Rg6=n!{X$vYr z|15tekNNj&F>tJvc0QNOp-GQJdU15rYJBWnuEn9yrfP0uGK%yda|1W^SSIzwIh<<= zR=Nq6BM>Z4vHyd?@A>YPl76!wFcWgrLNntPXZxjT^_$dOnG3=hb><0}?B6Rn%q{9I zSp!1bOmhsuAydhtqKvBgIU-k|X z9MCb5O2pCK9C3%uLgE|S{oapI;Fnz`)3#pKdZ%b!A_&&-og6adSuO35K?Wo@8h?TW z7y)+J+fXNcve*3Xg@PT!0XSTl4->quw{;##07X7d!9P(X*PeO{Xdk~$5QC@P&{Wk!Bm)w?vN?<4jXQpw&pMU$ZW3qws!>W=p7d!9IGBswuHp0WY?WX^ zywl5EBI^;rlA2?zqcUxj>xt#92=#zfQ}0c(3bpCSaMNUV~gbf0mP|{E6F*9`LJY zutOE43*pz;#$CmuT57g*tFUR=HxyFIlfr7XQFsaKh3$wJbAWHy>jhqNME&J7}3zqQ_RW z@a+*6uX3X$OO^PNHP5;6J7q=oF=4p4!-^{LjW;MVwGs1^wbl1(^`A1*%q^#EJG$nTHucD!iTTe`WC!SXz45B=;pfD)7{#^9_}{G41zB#(8@k+D3dNX zj3M^-n_aBCpMY0@#0*YG&-s=yeN&?C%Xht(LihRE2;Pz7VuAi$hdvnAIw&oT2j03uL9CT>%+HEdJ;i7;NuF>hm;2}+ao~2dbFA8iqBs?Pv zsO~&VeRMnXeR-Ji-eN1`okc-1j}&VqQ!M~{c_-u@oK~B6(TdFFFhXWY4>K!{_AlwLBY`I7 zfQu^VP6}2G509e1UL;{3mTRH$E!%fkK-9$iQQ=A&Rtx7(5!XUd62z%_@vs4AR>>c~ zq$dz|0fqBE1M->(Ne9$)XTvbvAgm8ds(B-(PmVp{mnQ5v}0c|HGfeLtO8IL~|NT8iGh+gphA zO&5SwWBl#ZBgYjsv19g=f-SlzCvRCRGbl>`#tjQ#JpM3N0gN2Tdn}G9M~dk|QMgGj znIS)ps%IV7n>~mmGy631GWzuMGRDp4NDgP^4OPE&>3Dgg=q|7^>Iylu7MuYsCM?k- zuczubLKg|yUXl5aSv#uitv3mOb2=V2TZDTKnkZc?$3??TQ!Xa<2QaFpl5hh@ZEHi% zZ8lq#%`=Q#SQy52Ntg5j)wh{`rkRg34hvkZ6eO!mZG4LoF@;Zb3LpO@QIijZsO!G; z9!>Z=K}d0$+k78qQ*T$hMY<}&dWgfH=nB3$6N&Ufo$9a>n_m&Zl$hWxHjLQ}$s-sQ zq*4@Q(b9Z=psre;j5(JWlo-}wa#pu|4Jh9uA@&hgFXv^UMpSS18<<)-8;BUw8P+QK ztrjCSO_^^@4NjSZBXE#~kT#rHqdv8FDZfd>wBm>3^@HPGxEzXRAef%?ijsSaHFTNi z1X(^lP=uc089KNf6H$0P^apEk{%~5taiHs`WStj#w<)bJg-`H;$@!lf35~89`U%JO zIn9D;;*K0}Yo#-xoram*fSZL9N1%%VaUI{Y7^7qpi;qWV!;wRoH8<*~R1tmI_K@>a_1xA)Ct^|+Go2D&IViOoHI_mxg`@rQ z50RO`zC@bOyLgR;@`m@-q~X04!L;i2MlOIPY`YJ&!x^Ba8ACRb*qHWjPKJGIs({nF zU0Y$?L7V#?&Ra|B(cJoa?Mhy!QYgUk&^=&tAQT1}RWF1Jk7Lz`D(hW9h=Xx>)%Hnm z^GZrI*=76(;vsIGVSw8ZEtcM9Y`Aw41H2uYVjtS%ZWHou`Cd{4$7wPt@g$e=NJw3j zCd+O)G5RZ=-Qo%mZf7o(W7Ib$kPL&w2>v5LrfE*=8GuRamK&H0D1U}P&zSMx$iDId zzv|MO+dd=JQTtft_9_C*5jmD=7O~S#fcgg*a?vFO*^JLaOI*)32g1Wm}-%gYwA&~!flz% z+s8Ai;T`Xw$s78rB^)ZM%;8LniZLR#!l#s$6Am?*)lZx>LYII;oeen=Efeh%k!KVMhN8Le%Q^`B ze#4nhK_*`2+Zd)g&oYnK z>(Wq5){|4%@qmwy=$mJzbtW3$ pz4*fiYe;#OXI`~K1<~;KPuH!{X#Jc7KAyG1 zS#@6%t1kBJ-d-OFEd1Sj-?8Y$r1+@4aVGp%qTG(F8o$xb-K~d#y~~-K0Ua1UUlkLO>=yWR1cqb z-InCaxs{Om7GeC9_I2Pv;_A6*v6^Us#OeyKRKoq8UT5NXz-p-ZC%BJ?ibcYVAs@vEJ)GU8F~E2m>pNxzV423k3IG(Ve`+RVQvr zQSm#;^%~#hst$J{5m7{mOL0Z>sdq)H)xhd#tZb)^nWSD;@aJVMm?r&6uwr-8_%wvw zEgDAdiNcNc@x4={oK-PmJBR&PW2;sS^9T3g4bFbzi)wOfy3`lCZ9LdZfW9Ec9M*io zXRK(8q9xgX|1B?dkfASBYK|oKtfaJn`Mc#%O_c9-_;*UHb!US&R(aW1kF!oERUzdJ z=}k`%Ge4as7%}rjocbm8-<hAkT@_jJ_Ro{76FVj>@-(0H3s>Z^d=_oF!d7ML zU`Wfn#>wW%S{2yw^I@GtiMbl%IoGEU75SpvQ;j87x(!Z?unh_lE=_;0T#VdSxi-|X zDo(88_~N=gdh|__siYARWsXpBp4d;?+YHet8Ig6UXWAl|K*I#O* z{#I*Y<=kD2T9dY6WpWdo0gQ3W=Ch31G7P#p5nAcAHW8Y?5WqBsk;m2)&uCuPg>BGp z>Vj!7Z=A#=3gb*^(7!Ea;3#eo&pwr=PVC{ywUYpca)eccEP-6cl>+2P2gNb9GDy5F zjP2v+t7w^2Uqgsa$>TgNgw5?MN0__k(3l<)HDjNdePF0g{55VZ-Cf8r{j$c9*sVYP z$dVJ$A@_hsU~I)GnasYeOl&CK5JSomp74-IXe=}tnfB!WA zHH?zheVsJA&mEUEyU(3Lae$lH68R}2=$PH_Fu_#AmuWe;3~E4a8+>v7%0$k9w5*yS zEC;ObQ!SW^(+8{{%G&12_9E=6TE`y209;a*?*QE%GQ%?PjdnlpaY6g1UwPpEi)SWu z*;TADwV??At4-B>cT=rPlw zUr!~9VDYk%AQXUdvY0b=F`NdlO}(qt%SPp}0NQDlVR>cISgn~CqS^SruxsntB7bQ% zoIf-hPqVd#FSH86f6#ASy-qCx;Q$^Epl~{W@NjOn8%#hCPlW&Qa{zn5sl7kiM+Sg$ z_Tl|zn+tPggPiZ*xoNT80M{Cg{_FX}FJk2wtj0e|%?JB8>3n zpHF~TDfK5qlx+v>k~Z=$5X`>};a}2_j@|ShW$1%Hw2;58i^ z;DX8ZAwS(8T`8ny3XM`Ed1Mxy$k4;A+MrPnFY!WqURJ?#!|nb+*A%@r=2oN@OVk}arlu-VlL za9a_b5pI;Fz^IYr!8$5tN$=6%|QsTDv@ z>Z{fs&&S6ygrDn~>=VJ`h)qjBj8Ar>E2?uw>7ID)LXkEp;Pk9FwJPA`$f^8HUTE?~FeHaN@*TN4-vv)u6urWdQ$= zTPZPT&+FU&b+Wvv2AE#+XrU$>;6?=m$8IQ|?Xt{U_cwt!0?-?b>FH;s)&AviYv+(=vkkV#u0WIcu+4^Zpj`^2D>jL+d>sMp!>Dn@1vU z5^ZwidC<8v;ulO z(Ztk|=#vC~JmlT8551%ryiU}-A9DDtjjew!w~%(!DbIY3+2W0|M)1&0fSotZ6zuQv z5ly;z>I0m}-NifkI*#9g^re^Mck}f-GWmc&_;nh?XcuFd&5v!?@pp<<|HOTp+NAXR zMe^3j3*(e`tRGP?16wqj4Fr-}TizXQT`it~SCd+qiF>~d9rPtUj2*llOf2jzyb7h| zVvvtiMytNT(_fNBdnObZ9gKtx#%%1q5RbPrK&zBA!68*)@z>hVW*Jsjwd>YE8ONiM zg`}WhlVF-HqjfH320Gw+zhceqyBaX@p0W$8FNPv0J1e}9>kXQ}}bJ}#}c zE2`|raX*%7E^Zw{tFL}Q85+Yii7zLn|70xisFCm(r<5?^DK1|9e1G2gPhYSu}v>Exc)Ly8FzKyPWX-&L> z(6y8pM!}zd9`)-Rsb)`PxLl$?l@NtNYPBK z6C>9KZW`WZpUTRmlW+Y@`qt?R-?hd2RtIW-Gd8i&P7s8ig6@7Z)^s}bt;=!8Q)Z5J zsWU3b+_q0uznyd^63L^qhIP*a8$<#<8RHGfv@7=M0|pev&mp-eqOC2>qHlE_HlM80v>gjPoz^ zi;c&)3R$vNGId3!K!>r!?=^x_ASpflO*&oEbE2(%iQ&usx$=cTqm{qQ?wH2enN65b)8Ll<ijO*Lb)!LeQ`flnrMRDD``}24^l|w=|s5*UKur3u}5CK9?z^1D+H~PF+0! zcu~ps4VcY_(7i`P&AThTr!};fC90j}rOG_nlspILLZ?+G%)kt(F)K%|W?|(ADP*!s ztxjK2|5U9j0i$pU!OyZey1O##*LaOm1cCFd5rISA#X`=AINzUpV$2di!l1SIDdO#tfzl&e&}-^=Rt7PNL46b^&E^Ea1RbC?l;|1KjXfHAi8T>kaR=SgK`q z-XS+ZP{2ujlSq12+I*pM$@ZeEE(RvQL+!5EP&2-%p^uhNd5*J4&k#Ttknnj!AU;iv zJ*Ty;ola$ZzsGXgaW>1aR+X{>p)mvH*Eb*!@97OH~p5v7AI4>4;fg$1ne_>1E6#B z&|cGyk;Y)kf_1N0R(m7=PHt`%@;N2Wr4XCI!EF@PYE-`qsS*+=p?m1>2QF5aeyhz6 za|kRqTurb&Qt#Q^!9tg~*DmmGCPCmY4X;AOBe4+gUSGHYu$p9;>ZO%%k0=p_{{5;H z4cLplix)L7HZ_If*Q4-UX^C?b{mIC^;PSLl36NeVhlhhD zo1|zL-|;*`AZ(!85o{!*ZfA^Pi0DH0lNsjR&6vTcQj2b7ObiWn{-0tLGORQ(D5Xel z@d&io(H}zEIs~s+8`5e-V9k1Z@dVKuU0eaChn<8fcxT1_w@aQoM<#8ti+CqtqmNpr zP`0OOC!ri6&sNMdvG7z}K2o90@CkD1RyY=i)>gUIRE$?qbEe+2^^;`IvtYT>W9J~@ z0W3>tHpZZhxIFX$x*$pKlau>08zfD67P4Q@{jDI;AzKH*Hr$soa$w&bxOkbK`f4l!+QGCXo)pmN+bIx<*t?R7e64>AfeO|v~KMR!Fb@f~DQ z*gBM%WKkTzFz!L;v>7+<5L%F>Q-)+*5;~Mk!MZw%HiIP}9)1y@UVlnzXvvQ0QDp3^ zL@43*_`LL3U$5FaTtp`Fn<@+OkqKX*+;LgGHeuBuQ~X=LlLqHbihXG@uz3H~&pHwU8K2jeE?~AeZ8OgOI zkPZXj3E>=iv6Z{ie|_08+YfNz=ABS4wpTApml^zYGVdMncJ5RwBS-!W=43A;M7|p@ z)ycB@|6%c6ucGEd7R~4pN4DVm?L!U&)S_K<7?NT&oIJ3A_Yq$KK=!n;xoLSzURfc4 z@ymztj>XY&ihpPX^Jnp$2k@&n0CJZEK<)~F@ZUr4>FGsL{6n|7njE-(;p~`N&UtnD zaI}l5T~cB)b6d{*H2xk&kUTb9hSSDOzf9RqGRCUv2-2 zRE%u%)YSN&LhhOWL&zQMHx`$e+!J{}S*?-fB4C|IJbSowBY{{J$s`{lHHg&Jj+BJw z=%mY6qQ;Ql!YD?1GtlF2tuQvCU;s$Q5SEJa+StgQPzR(C9k3FHSvA`1%^!iR#1C*v z01u2E0AQC{1)%OvS|6yp;0MYMbn$_*n*-wL1l)!x?Y>KZbckv=jh~uoMAp8?*@2yb zw7U?#00UyhOy4Cu;O`QyjB;c#@$cE=F{;NJZQirnrZ$`YgVPA;1^Cl$$<45!c$&`{ zH~^I0aR-31`!xfgb(EAZmLs{gr+QLoRx7xThBKn;A8OTyTDbC^b_5%%rm0T9*QyK@ z{ngo7x_Q8HJ2%>L1h3M%i)VABJi!cM;*0H-=2Oh>cj9xo0Z_3u+kv=mW>AnpZZFn? z$u2Mdfwj|M>TTME1K{`l+%3{^q4a_I>cG0k^8k+1$N#|E)tLbBJ6a^i>99zbRJs!z ze{dBANCp6q{|9R~9sywOnk*n006<>G7Y@jbMLbzu`l%(o?D=E0QIu8@nYL;xvKJ3A z@P=@Hgrnt-9hPJ)iFgkB$Yp$N3oB8W0DIV?W}#%yWowFwdkrmEso0X00M3_I>-$*c zywgVPC@gJs?9tL0ryf`_SQlN^){FWbr_pY6N9`&5X2w>RFF?prX9XzatLA7DgSMa8 zenp!$2e8*;`F7z$<6kzI>4f?9cvewJIa`k`(l6FWvttr zViMJAyJgm{20|6Sg(zM}Av!<<$7Cezj+cH|{(x z<F$LEK7>2Q#NdiuOSGvgv>1bLGJ79 zv(J^e41J?P=l#YRsmx&=$#6IxD*oqB1K$R$SU8tAy67>{a>`j z3EmG?6$0o(RH)TFQ?=sp*OyOqufguD=zJmB3|R8#3`E+9^nL?G$wp@W z{%mmrrvd*7Ywtq;Pgwgvg|r!3d8pO|88Hi)4~Bu*JHzVgQma4f#A$ny7}c8?f9e34 zXMg?$SA9&(`>7bMvdd@KoLmW^qWu&P$^0i%lgR!V63^$eN_NTKg?Q*>tHjha=5yRfYWrWKWS-s9EvEx7s6sEmjZu zk-}??ZH}y6kp({+!j*@Lt2v5rVG>$`uZvX2rCyg`zFl)S7g;l_;llE2`C#}vsbg{_ zwZzaQsu^W1(VNTkd(Hd`@>jDTI8uCKS?O>}x-K;qsTrYh6Jysf? z&YFTwNja@x&-oKNG=nDvM_YHM4-!>DOG8Mb%*%Jy2O_vdnj5QY+|EO$@YvnO$Tg=M z3G^!sY}~r@VapR2jdf3blRz-;X?$l9TKmA~2f|CO05Pi}m|_$mpQbsA(Xg@=$*5o7 z76E|Vr7(#h+Tt1Yf8{Z9Dgd;-HZop{C=CD@mTWV~48SiTH z5}ll4Od06UC70^sM%(nwOiM`Na4pQ{SygErekS|NFKzLrw0kg|G-~iZT|wkAdOt^8 zi*hq9uq+^rd9WMClB`$`8*NWvimuxNORiQRnqlgSQ|czFtz)kH4y>%$$qss4gx2jr zluyW%o&CVBW`Sv_zDqHw|wsH&Um5hg2@g5+IJuTK=?Wk1h;Ny_%eA%JUeh@o$hq% z>v-VGlvXm}^2v5xzfkw4omLAFJk);+MfTil?^TexN$mAWN9M-pv_F}XlzNXg0I!|!+b86C;NxO3hFO?PB zAq82!B!_D?8^yGN{4eKCC0}8GDfFbV?Sk2|p8{ja>nq9vhynIHTSfRyqvjG97{Igy zL@rryRMzLYE5}08+?z|5!g1N};#Xu%6{A6yGV~n@w9xJTwF~MG@XPdvTmeAa02H4t z=(IBBJ@Wq!+yiz_VN}YppkGFdG{apo{hdJn;!U)}{vjJ+nt5w}y{QG9{-XOktgZiI z20pf1ebm0`cRJW?O89T+ic0_=qg7?Fytkh2(tRgkp%H#r`JD;WIKda2f%-aa#r4cr zPaExmNhwJ;t9}KTSV)OWmk_am8z8EMbjw0;t7AgAWFTC*L_LU3}3O^l1Yv`PJ{oXmOH0-=K5W zCs!ztrf1tW_zxlL)E3Pqt`RI?@7S%pWEwL~kRu@I&k`VTA}n6H;l+)BMZzLJ#3~*{ zXcZHbj`oKu2xs>U`ok4ubhEpTeQ*Wqh3rMy$Rc#gOy?l2-O2iT5O?c|}}cS@taiV7CaFX_Kx>%lA(R7sf5z>LFr!pe4ZNy8PXy1k^SN?)_DuqClo zHOyXADCFKVBeviM%(}l|cKt||IUcMM*r7i2h~0`zl|L?oc4RJ7VFrWMS5{_t%B%iG za&PEIwXBRvgQ%_yuE~F>q-+a5ajMY4;0)%d#Qqbc^%JWoi$I!manBfjM<8~66-}3t zcA2>z^!Rrb<=$O*B<-pb-RmK@9RsI_PHQ;>2|4h(QokPjlvZ`+9t0F~`8k2sP<%|p?Sl?m>ca(@%wAA zK0^Bm&|<$3n#r98>GtF;1{mY+6}^MiS)xqdunrUasx6$o&28w#*S*_nRcQXbcM@IZ zlLn*uj)qzJ2Fs-NdTQAG8@}du78H+dxGiBXXaFP>{Hh@oyapg(0m2C&M1jD|fb#7( z2@!hpzxY0Wmk4^y5dj1NK#&P~++q3E7YYQ^CN3@D`yB`rSTqd=STr0E`~e{Y1{eYa z=CiIeSzo>w77!LTKQr*OX-ww=4OA>N!7M8jq7eodG9JHS1feiQ0F>WR#3$bn5HFCw z8-HkE1;Nbhpl5g4@&~NM!`GC;eVDG_AM=&kd}>F0JjX8h37Qle0PlmS2NgJ~gdk)B zMq6_N5=IVTW+?gX+Z7>AU@?(z?gbo>r`s7)YVP)iB>oHnQeJ9CKsB0%oGejc?i$OS z=MVI~P&JbN+Rc<+WdBA9H~We^-Pt(6Fun$z-*fZ`ahrA`AEq-KonlEG=$a*tNq_-f za=%eM9uw&n)3BuAxt57MJZyo>pj)7;R!0)ee- zwhT=J4pb@sQ)^>)=t8MZH8^X5)-GCh(FhGj3N#V(hh;*Fb6JoHQdsn9N013r34Bx= z{YQ`c_v;f8FLU*5L_yB?Mxfm_EK@ZFMi4?wR`f@<065Z+!U%)zVasVACx8pE3=wcL z8UgxdwBdQVD`=@{XynhypI5CE?))fCVrhrsO z07!IoAOYA>2{iCy3U+m%M%Yn#w1I01RrPHsjQBd5&rk9fwb@6MKI+rW@qIL=nxm>{ zEVKt?(1Qr)?joneWcr^c3b6I+dT6ofj`uj{X#pX!(zG>}7oQWp zr5DLX>XzUOt%G6BL7^#{6N?6zQXPOpm%!Ntnl2OP+7l{~^%u`1i}~`>VC2^8h5Yd2 zAu9ga?W{{NOuPz{gAi>R?hC@>7s3(g0;UPZvWX~cWZF(F2#m6cm;?0rGDHq2&_$S{ z8|V%s5^(kH<@Uqn!@7imKmz2a@Ezp#n~~bX1y;+xiURaNGckw=LRK`KbFW`A5tW>{ zsQ)q{S29J|+xL2Q{#8&yjQ$M>5LWXi)Nc9d-%`PSV83=>1h&3`tHOa&F}lDpurs*8 zIUGqUjA!g|h$V9`Nd&eyz%l`1t+mPAN%|K!{xA+lScb{mL$J=e7g}L04px9viywLy zIP5e*Ee@22lMTfTE*IyQ@B{S?lRbfE7M4XgbBkEK6onNH9dy!~795z#jf>qC08ztf z`V)Dr(xX*O0CK=%Fxg>IB!JTdhTo6VWWyA}VN$~|*OVpEL*B=o-;!(FBl9(6c>Npxds_wTYgpQ(b8DHg_CE&|S6YyolK5Tne z6}NNZ6aTZvZo5a=Crt_h0gr%(-?h6?$aR`X$kq23&<=3ccEz=bqXCaJ7iA_nBtJ2a zY}C<+lmd^ePTGTOCQniC&A!$*@ToX-L)Xmo)Ak*=Yd@H&I0tQYK#rIlH~UJP=%W#6 zCLY;HN0pHo0p<&)#0ECK#P*4wU$`1%76=p!M^{}1 zgbF$LONVb?Z3dVU9N6QW6eSI(1b=}*eGxhva02j*4lk<&N-hvMlU9g>fvIz7@CCtiGrdK1_h8o;Ku8X;l>RC}P>zp?QM@rT?odwS2UMG=r;^kgKSYwsO&cAPJaAjUTsbxe1E>F0;~?Uk14~Df^!e_#o53;cho1A-j zP(fcf*9NauIoL8G$ z((+{>CdxGBK4c1GumrV1AkvGNUm#jFBQV_B{y;D?=u>Yn@=Foump5|WwnMj8@|^wR z)Rb7XM8*YJiHJBcY_s9ShJE6NxHvHcW@%On6a(Ug@SFYOh5UrHL|mh6vt=@&5t3*e z@|@Q3#szPKP;p{_2cMOi3NRm=0_yL-PfCG1N1}KhX(mhb9~_VxC83@`7BLO(Fl;tW zSP#QhvyxT#jgss(Nu+%PRd0bkRm~P+lE9@~?`-?NlaCv?;HAVW88LoOl889J!zLJe zC~fjyFLK`rl|w$xCLXZ!eDmLTVkjAD9%B=%kqO;*`kIGZGE$sIHbOCQ(FRXgGLo#8 zMivGu?bzXDGeWfYh$vs)n}qCchNk+8qEAe|ye!dJRw4>j^%buHONp9D8Cx;*BjWRQ z;dMw=n2$C-Q5;sI?i~**mrS!(OE9UxqPy@fm&{g6vj#4S8nt~MqH2taJeNtlaRqP@ zRAY$3@l<0D%>Hs0(h`_qEMfjc6qU1{5h3RoFboDx=% zGr0K-VAbFfL0yd3Dhj|C-(r)(Gmo3m)Wr~`$>p00&2hYE>F@KNIIT~)SbW8ewN z);IYFJzc2*Gb)Eb$1DX9_B9@fY+d>k0({ksv*#T91p&Tm0-e!c5O})p;Zi(`?5Gj2 z(i9SoNo`zCFX20!rMZitajAES@o=!;H&WKKAv8pM7rFXLe?Qpz17{iio}gtw5pEz@ zw1C}Ap*kcB_8adDIcy27GK!Bz-Wjn^;ROV0T2i>sdD~pj%5n%MutG4T#`SP!YLLN@ zA@QbqdU~*15a?*u2~`2NyNC+62GpmTk06RlN}`%a@-mP-@CEksZhlU!d|jnMA&`t zNRNdTtqMpu#|)%^6-5lhgr!6TV+K=LLW#x(vwh{^Pmg?_NrlQyN)?Sq_&hV5?K74! zq~n}>`j)C+1XwvpAq=rlBJ@vObfY*yOGtTMI2)#M-592E(;V^a~c*5Zu9W#O* z;KLSOs~E~I1gl~cNS#kI;g#uXhq`5qh#}@l&_94l7W#`w?YUzRg)HVN?TC)3*GQs@ z7o=n>Q0xCuGMcktjBRe2YCzgF2$edKhCW3C)~UEa@j9Ky$o+1f8%T3lAvp zK;Z|@NfQBEh26s5rILlFwIEEcDM6EffV> z#Zbf@dCXx3OcJEsHqQ{A0upgP5)N@bW}u(}#SJLo&b_cBqI?~(t-;jeqDcdkVoCE< zK%o#zQl%413Zh4lcYK*-x}&fg?u@zXIEuLo+?ZY9#_R$&W|v|d3Wo$?H=Z&gz315v z+_?YKkF-nRNzL^?9bA@vgmlfBEv(Aca}?Pay&pLHe&Fo;fwS)i&c2@{n9m3rYBJrx zEJFCc@ImXttKO+BFweV`;8~YDT^Z!{y z0K01xqCGg~A)H>@@5P(>LIS6NPB_C4t8Am z>~5qj^2q!m+VjIz&!_nq@GD{d+ui;o`T7*cZ?22NS=J|u6_o&bfEadp^0aq$dUw+D z$aw2>O!rNdgw=!e*Tin>#WOmu$mYjx)&pVD`>kb?x=N zl4+NU9sLKn@m`2_oots|yys$XLUQ8M%N?&>xG1>v*yO)qfVU)er=7}A*{jS@@X{he zVv#4_F$3B&ZyzoS8iPuBQZc@##wLutq?1*pmpfFuirXwr?GBEr@Us=gnWyHIUHX^` z+eq;Ntpnc5AmDKN+9S@RHFZNv7$>mW{3{CG@87>X6sBv)O7YhWDQp`()uWqx3?JK1 zQwv9oB;bvWe=k_*S0pN z>*L;`#3!Qz4) zcq`#2Sb(?0%t8Q<@hr_)oAP*&G^Jqw^%g+|5r(z`HAnDwoun!bvUE>@vZ1{(03|c; z+?=NOYhT|~&Z73)5lw>1FD|X&MmFkQxVNv?U($Gd(kg;dTh9=r3R;Mww#uEaI`q7q zryhki8nKpT4YCxDI?ReM8TU3PtN~lUZEE{Z@ZWOaHT8Uh^SHIDgD85Iucd!`%onS0 zl6&{ouVkBu2R#l{KmBUkr__EzL#rA+wpbP+OlJ89@xR%?e$9DNLtbki3?HLk6|B96 z6n}8%9uRU3ydEbb#KxunST8D7E2mwG)(oY43?lj^x0e^Adk>)eD3!NHH*YKzcAiII zZ2c`GaKt*3-J1je0kwb1{JOAaOJOl=M&>m13Oe;3e62eU`SlO5(Kps>55w9X{fIH* zkdgp|W=jFzv$vHW;P{~UmR4bBqpQp^&GBkdDdE_6#Xsh(F^!F_LnEd;z_Z18GEE5L5>ku*p$Jx_%XmK&@@g8M0J{=PU zLyeFm4PCJ`{pFx(=?Vf07rn6BQ0(k+sTra!GkkWiJo}kTIzx%|3o-j>pKC*yZS6ao zw9+pOg@Rr$HG8N)UoDY;zP#8kkPUXeJ`xvzi{eERuwa}}XVk}e$n0c%dJxX!Je&tB zylsj|sImZ4=;Xq!PrzO14YqPDonDKy6YZY8jjIQ#=WBlHcl14ghxj z;xe>t1*=dCqYw7YWk z8@L#=j%~cQ+q-SgE`U2ZF&c|QYs~PwVMETVT1YYME?ifVQ7@xtr(hm@rjXvZhqL4H z`yNBq9(~~_md27FpH1RJe^;++)%f0v!%AEoE9wCRZLNEgN-xIpGpP<@?ULCNzT?ZC zTEhl8Bt4%gHO{kDfCR9&>dS;J49a)FR^8{z7rRa?H~OG4}C$))7Cjaa>B~V({9FcHDBrUb}KxYNTp zWHrBxXUM{w)`P28xnGUaT5Yv&zy5=5Wel1XL$rC5)6l=yH}`YCLoBUTz=Iw5=lLu1 zNC^8npU69tUwa$>CMWqoc@wE)8l z(u3EC*v_@9zZW)PLCGHdNZPuifjFDD$Raqu-1!93>desIqRDf{QM)>S>4jzb>QZ0? z&d`UG8=aQ3euWW8gR06z8sYE9@rA7#JQ^aaa-qwPr2@>x29f%8Pi=81)rZ)(+eqfUD91W)Js&@$z~XCM@$>D5%2#F-{30`dv6}b3 z2=qBLy?fQDFnh7r#~&ntZk>)XRGzy+8-*WbK=q`yQxhxCSYNx{-g+F)-wD~YZp3&+ zSj4kUAKnlj2^Se^!&vg^EOJM%K~QF5dGGKlu^y%28b?JxtmK)?eA%R{^+V;~xXi?J z46qqg16XQLNc9o6gqd!>)T#iHSyVU{kaG2-(g-GMn2|hgi@^qWu zQd%5oL>;Bw^nO*&2;Me?+24|7kTgZ0+R8@fT45}J@233t7 z0Z6J4T@2j@tBDi9#l4LVbiI$CyHn`4GT&yxyRu*4*z1zEL0TJ4`>k&dZAN^~R)Fk=)ZA7dj5-38x*|)30 zzwZ&t@y8qThw+?BDMz1j)w34pT26ri@Kot&0G{gFZnR?dk_28Vrbd_0d=ZnpV%KB5 zx0+I@by{7F*h6rTp*-xyR;Y)M|0M9Ey4Z(@oXXHOC%TFYwYOOfq+c*(DxIiQTXxa} z{9G?`8ohgPjogMSz14ZBnKam%k?FHcPI9G4$?D8(HMMyOrQ1&gsb%juWB$&@D$B86Ah= zBu+Z(UglIPPQ(+Uvm)PYZd}2kOKeW}EC2x$Px1h8UpJco?(3(=CjbccB>(_|Jsq5D zR`_H_)(^_F+Ov=o#LWSCugBLNS<~d)N`}8LolY!$vrXQgZsdLx&MnO>9m2RP-IpLJ z)@~ScK&7_|5xyDE`!(BA73;oldgR1Ne8iacZTX$A;d6##_yy2T zJAVTsbxN!2(6ACkF5%_!(b;2T@XwE%I_4jMPTS*a>ov`+e^IUlwumIZul?Vlz{Je{ z4F%>WA);RC?DJ^Gh`J+tno_3ZA;vwywrM=9?9r3pU7_|Fr&FO-&79XK3P(H zqc+glsJH<-61w88S(3VcUbZZZrh-U_`*=t%Gu&t&jW;ipIkoH-GjiP}p0b80dfhu8 zIJNNFz-ZgSFDv2t(4j%mx?rzStE0Eo*d|Gv_Aru#&vxmG9*#aUdQh)pzcC>-oBs0o`8vijwY!;!g=w`4fl2K< zD?*%pO_Oz=a%@iwQg*}R0_M8^gREx_Yfl%sZ=|DgvftM^Lo`(PGY^wKlWIF~pC2v; zGWTrWAx8gz9I5XoAC{_hOo1MD5gRcU`v(zYJ5R4Nxo3?Y zb%!itSNd0%>!98`+HR96RK3Hj#ma0`!ciKgKBXLC=d3l<^P{VfQ{YU2&&FDwz#fxfdVdox( zs!!Rxd7z==!Eexx!Yc5u4?5IZm`x-Hqr(ZYUQ=0!Pll&qu~VB2_n=xZO)9@mnh-25 z?krg%wPnH!lQd1;CtOEUuh;Aqjp!meu`aejK3XU+ZCR$Qw4mv=OJ$|YBciK-?j=GM z{7jp%*2NyQgF2$U;ly3vLAnKPdre?T)2X7QdL%ob=Fl17@cQ#+*TWTz+on@WuKmep zX;JUctEwNoYuh3umb{m`iSQka`fQp%TO!T6~PRl|0dTRG9Y z?d5%Mm)10h_3VY8WTww5agWfSwj@)`Au2X6W2-#48%pq-Gh<4IN|$EtK1CNA;j#kn z*Lygkn)cNT{f=_xO~kqrSSL6P{Avtoin`NmOq=W0R(2B8%6QbXXYE=&5so=WD{G=x}f#VsL`o@VC;oZiVmT-7?h6X|N7F=fN zZX{ReRLXDd(02z?=WSeq5EPu!n290DL$=H=+`(Hx^INpEAwiql4;v>kSTUMH?5AU~ z9ADo2uIzYO0}%hUQBB|__a_bEziC8$IN8Ie!N;FeWLh7;R=-NMTHPxhwqZ;v=tNfe zBFpXIU*t}_@^A*oW{n0*HuHIwje$}0^XPL z=aiF(uUvL+WrN)k{Nbyduqd`P%`XGRUYZUewloWuG(eRaV7Mt9c+d_||XWD`>6bUOnuEP}p}XmF8uPSEb@W`qE(W`VWJ&tmf1D;(HpN z9~vj{uNkH^^##j>HktOm|Ij~dy@H|i6odVe8s2VTR$^$^aTM>bdSig#c2hB zwO`Mj%MPnlTwi2#pGij?r%#e@zpHpR;n3{|Wd4^t2gH(Qa~9t$j;8=2AnhG`3(dBr zaW?}8o5c#eAFispVe3PQ&i}|Gg)d(HM;_?~#_-_SDkIHnLv(TLoPe=K&K}_N4 zwME6R#ta#PI47OL&BR58;i#1Dcr=IpQncT~KFbwkmLzYFX|~$4++5Vu(==*2Tw%*F zwDm+SQpZy#+b1NvF<^pxAs?FL@)n0L$CsKBD%JjSdRCgc;mXKUpVHTuJLD#LLb@HJ zs2y5~`D@tRxAk`Cv6e@(^=s!yRC6wnL8|#HgM=1> z@`|Kzp+iCWy?5Ir5{ExJerV{k#1z^l+8mjr9i2$U;TjyCFH&8s^;Pv0QmZqwfr-Z+ zS$Z7v5p5xYzF**$z^!D@!$0M#q~5LV?y=}#4n$3&blvW*{rUZ`EL6Im!DuKY#?`iE zL(%DY8xmAKOq&5@D1Qb)_`Cz$`BkNT-|&NS+{#4t4t?FWTB7GqagD@Q|46N8dC8Ee zYe|S^yq}i&q&?3;*<2O+@4Az5_JK$+goppqom>a!hp*$n6r=v9>ZJR#>g2DI!MT=G zLX(cO)5eq8&#B+CoY&qMz}Y$Z<+pn+=T7(-GIs9RiG}V3tLCf9E_&AsFD$?L!X~+H z9+hh_s-$4e?T14tts}bHRmtbEqtqgl4k1+vYO$8xbF^ROVAHUYp-@fpTsp*m|4rIs z*%`}QnR6Uhgiyn;*&uWU9+_pR|BU+^?5up3&%e?GSwxbjw8!)_isdr}yU5q%{3!yQG$6V|4nn5qec`Tm23D>C6@tRN#V504$&6PvgKXdv4nx~H`=vZs@s;@0`UX|YM2*`?^4gzd3eR^AlNr70{ls z`s@9k$>l&eSA|Gd6&;Vr8}udTpyoxR>hpC!v`-g9h7-flZ7O$4<=LVKTw^%Ka6 zg2Equ>^@GdA7B3XjOiWI@vEarv9_|98w#)C9WOu>{TTJN{6C4J#Lq<0=+Ad#l7jha z^Sv;!Io7%i-(E$(G{vcV#ElrN($QgKm%T5m#_S znl_Ra(~0vBmre+jqQosdn1KnYxQPbD5d~!cnM7m%P)NY4+e`{c(=ohVHCX^oK#_4t zt}`fvxzvxR0zqXt@I8Vmk;y%R8b9n6cK~uga%eDF-;11=$sjo~ja-TegN5FJ6bn0V zcwImn5w*>ALKC?I*Q6Mll?%OmQ?3v>5-Keex0ynoQk)7?wKNQ_%m^wy3lp_9A`|X9 ztpr~}1De+*zmu5VC100vQV%5CKM6Of7mpp1j*`|sW5{H|(hFz22dztF!QvV|I0+Yz zJE^CP^~dW-l#XIrOae8j*E?csIH+{Yw=uw=UDQ8`=PadabnuSbr)>V1h;M}AoRzfyHF=3oAI6v&>0%vkun+G@Xlrs>u% z8+`vIKDfskHjry%|Eh5+>1?l$=;lh!8^x?;Yy#0TAc^y5=sPoqcV> zUHPvZT?eKPAUU}956MBvVQp5z&djBIB<&HIy&ts>!v|uG*l)cf-V%gap5v>scvB|A zx!!u0VnW2^p7jO~IJgfV&`v+;>$D1^w8Zn>y>xntNW-%JKF)*jG2tdD|7V7TE!Jf^ z{rjLK1LLb79qw@zmFYpUk)QO_4Z^?p?@K&=;5$z}({~|LRFPPV=kTJ~28fQInt)WC zO9zmen-1d#*p7%_fmEHu!gHz){G6)8ss;QpMx;$Mt##sG5$8owd{A@;V?cm`TBk5< zgPg3^p8rz?(OapT;`wxSN3+hXW#B=)5kCEekf5SeXW%dPW8@$9qvtdGvEUE;G2=N= z=lRTjJpRLe1itJ5-9PNdntx+I`uv0aIP(7@`?2r8*^ffhQ2G*o*pFU+u^+!=o&fAe zZmLKqx?dU0s>RbDWLm3#5_ByTK!T3v9!Su+tpS9`0V0i<8Wela>RBiKgeoF^k5$J7 zM|d(~{V$EbLan|Wzg>TSB#6=b3HuGhP((|HXhOYi;Nv~@D)Sas+Yggg`6k^aqG6fk z>KeHZab|2^1FG9@6tQZEU~DM6xq>$cU@&50qeO2sJ{WN+n)SN_dV{&Ix{kj@RJ{xA znxn3H2M%^*v00s|Q*x_77nHy)wf++Gc_iac7H@Hp$WwH`O)+eP6{d35t&?W@&9e+5 z3ZP(iID?W&1*s2(AB-#;A@oc-h?KP5+{$MpVxMLIKqH77lg1FO!2rp(8l;6$!q<0N zML%g{P+aS~RL*JF$*eo=A6dGHN&Iv1 zy{KD@^Ih?6(!eAJ#ZO`vj_arMRdj#H&@nXGPI0BEQ~n7;6_6cWP;d6oN&{XFPvP=#$T> zpMSC*3i~OOb}YP5*?uDm8!}sFi$Im@D)9*sFUlyT__!%}|0?xk&)BvwkFiFbZ6>e7 z{CaD34r7%m%O@@6lqUiGdtI5L`~o|_6R$(lfRo0Qt_|eXMM2VC!4&6~?2_9!QJhx@f}+(Jy~@A_m?-NX568btE!#W9VKqHm*$Ill~RlH`qkF+QAD58 z@p5(CI$uJ3c7I@CMI%G2EP4{+!>+B)=1-Ixs^AaePv%;qb-+n@l`{pMT^qJIBK3jaX$!L0o2(lT0gW>+~j<>y8>FPnSm9oYB$1^jy@2;xEhJ2eT~T+}|T zngbK7K(Zy*Oj=xsm0B{pxLq}&x;d(txRpjSJ5!(43{wS1Y*`<31xJk)`Wn9ieY}-s z^1B?O{IRTxS0yag_N^6>?+~k_6>yMsoK)0@I75Qf)NT8IEsdy|lZcwT+R5Xbjl|Zx zO?Hw9=2uUT=#7WGS0}V#N<-{;t3HgaHbkOo7Fb`FsjkkXulAy{BGMcYNuMHLL4622 znshz?6}6RESVbgrMWdYh&gmer>4@;v>@^i8ad0sG1e(e9Tz&p*5OV{3 zY!#F)%Ccrr=F1iasvi6~$W7J748a!*ivMKmKD z9p3Yj0kI3?th7KpJ7x1#XJCagV^4Ur&UJ{H8ke~Aw*n0x_Ixo-F*Y;i5qNBs!>6LI z6?vQv5%BXdA8svG4}B5x(OxcCCu7|-WT3n((*N+Wy|LV_CupVw2~OgV8|7t_JVm}> zC^ndJM}%>wt=HYvCV4AP%9|q;dzB4rStW)PFfcg_#GrG6Vu6C*br$t}!;qRRV!41A z5ioIEKd-x82$r;~^+d4G=sCS zYPsr%Sc+h&>U*%rvC3h0TD;CR>D$^i3$zOqqA;ENkf%@^C_#c$oyU;RiHD$d_U9gY zj$HKY#-NA(B}aaj?^{o!0w^&89P3MbcnMm5Hlo-08h zA<5;78@O;1vxiQu6_z-I3NTZsFcRc(k%2DI*-i} zWqsv~hIQH;`AfbE!S>DU%{Ng6j|+1S5mYUv{9joq3h{g?wbkR|3esz1%H4h!YKd8I z{VrSUAI?vCitDg~S{h#1{{5CjiClH!x0Lay7V(;EwK$d_SXNz*x3ifY4q6DAI&L*B z5Q;p+Jtkgdv0+GGL^L37@F@~EZ?jiQmy?S#1{#(;yWUYLG7y$LL=KsFQ;isrpK@?e*Y++dv-8_+FEE z1;e;{PU!~7&FIYkC3=zUNxwoelQ}JR>0oLEtpDH)>Of#}{~<}L z1D})%irp&^mQWrg{ajg|ju1Z*8ioc+V?i*k2Fjt|#&;T6b+A6+07!Xj0MB8#o$Uk-(*xgQ!PL5+%99+I0wD_RDbNklLAv2 zqCQS~j@|z%Uw5Fy@+|T0Yxcp~*P=vrkKamq1}Zjsr2fm@56eGjxoquKTqczcoD+2( z78`CV&CfL_wrW!b44G4VbXfeF1)i*L9~RGUE4j96mx4QWuF^VnxW(~s%&gZiHRWf_ zr$XBbq84i7sqiLRDoRMF-V>fDc6=?09a9G$$%GP@FhOr+?61 z+S$*IrR6T{(zuNPGoK6EiFG@zN3&jHj}D$+Gt-mx$^)=u#xo}Cm|Erj+tPOemmqY_Ev_A3JRFY6iY`_y6Nx3iaBG)qVH3t6s%qDX>z8HQ$5jJ zO^?)#;1ANhUZe#iY8b({NJvUE8oUHUG}le4JbMoCsL7f%p$O^A4WS6NFtg%2UZ}`h z1Duffe(UsQ*HcCcm??tm=Nf#?NC1O|cKaeaV0g!8?FVfL0St=CEvE5iFq?0vxf7Al zAik&C=dc@S1&r4pHuh*kZlI?Wsf+AGaNo@%4dPpMkWvB{gtA*C5jqAKL!NjjLl*nF zTb>8{MRht5P}cI-`u*on4th$rKcOzVNafQC$fs~*!?_|KwzjAzYEP(*iZBg5@m6bq z8s7cENN#T+^^d>N#O6Jyd9@yng|4?KY1ez{by`TGWihU4| zB}GV7+Bmrm7TE>09xfxtA(pa5MD+;?xVy-+AyHYY1Y))UCoeNh!7 z48eGibOkW-ISZQtWnC|l@luFe054C8woGU5X2LtLp0No9aGSmsVQLbj5rd_D>{Y@U zXK-dqkeosSNO>rqFE}wMxeaa=lw1cV`A)q658t#%mpS}Z-Ai@w{+Wsf8=Qt$B{YMw zY&bnhRVEW6LtTcLcz_pj2^^FJ0!`U8K{bXy4*gLhC=EgZJ2EzV0wq!qgqhVVIcmgD z`#up$eq<0y^fPNoJ{pFp5veK6FQYt*VfbaJRs`-(E>NU++@IW_N`tRijL-%0FT5DG zj!`^a`tR}C=;D+E`$BIEOVEBALGr3zT2QHe3B6|9OagIhX$eFscVs1@gV69&(B$ZS z3}pQYm@axdU>wXKd9Yr+5N%fkP$-LRJd&-!ves*;UjIGlO!Qt9tM}~rz;mV+*Ab*6 z;*yC}JS~S*2kqOo0i_Hzg-Mgd!b$@|Zd2L;i6Cp|Ax; z)g-a>B9bP7sM{bq`_R!d5=e^h%gYcz3Bz&L=7pX=wjR@gc?XDQfqbuW1}fga6sh zL0>vE8g(N@pnW(*2wvWIghFeJ})^Dd|bTLh6bb++x(VL1jSbhqUF zSH!+BM5r#LQs*?3LpRR=RRo$JPAo3awT}G^TcnEgtC<9!>Rh26oJuM2q(&i0D@ zh&pYV0o4Us8JY;AX&6LD0GU$iy&djHUn1Cqy~qHAk+K{vSPIxt>5SOLY*6E&-M^+` zc4P%}8|H;!1|H;LZ;`>if1C$`ZEF*r9Y)6^coa6MDNnS@bSCL+$aR#?UnjK8?t9B!Rs5u>#yaqp!Lq6TNi+1- z2oeSiY=Z9$RD4+9Wh?&`AZqW!THc+C8xPF7>?%S7{~0zZmi?qzyiVXo1Z?tu+eH7( zhAR6fPHjW({YxMYSa_xA&%z9bzYqVc^w&rjSQ`iew(?gLu-qTVUuGT3zpUE-ZUkcC zp2xv|&0Bvyg?~(Ah*H^*yYsu~DpCM8wf}qeI-!99aOx%98O)Iucc&(amj5{v$Fmi% z(LURPfJq&OFu*J(VB7ELmKUxQ@SbOIKM#5GyHhVXuM<=zbURaAbBYf?o?h2F33-=q zNLzbJSW{jn7cHYF3`wX9uJN+lephCrxMcl6{cCHLfAHIXB z+R`lycsX^3is05IQ^eLTKgvLP;Ev#@A+tcZ8{89KB&^#xiY;ES%D=Bjl0oU1x?n-w z-eh~X^~9*1JaX}ki)NV4TaSigBbCA~|cO}*y5zKHLE(*C1f&CGq;I4U- zw>DbwlW5cj$q~oic}r__RTygB0uy0CavRBUewm_1xB>>k4b6b8nfh*W6Yq7`OD_{6 zp2xsg>fZi5Z93cS0~We>Mi--;6tp6PU=``j%U*JbdxUA8Wq{oFSV&mrl`QRgNE8w7 z0PN!reO@zy!Z1NOlk5Jz%+Iib5WQtu_E)whl?{)U`G=v#qETMP1xw>)M?HoF5E6FV z7%~3h5vUv_MY8xxYF(5nPCO zC_#Btbh7_p=R~xoS)zcjq6S8qk^Wo!^Te`bt)RRf7R0ki6sI;(%r~TS+j#+vYohkO zxsD&z_c&z{;9;PrU%e(Q3`$%UHBb*Z$1gs=foxVu>{EOaJxOKPSL{G0+Re&GL<~v9 zDGox!t+#N8^KqUU#z;G@V=Tjgn#Xjadff7Vs*86I4V%MCddG`tQILubUJjoTHH~OO z5%^|rT#F?3A#hz%gKsr%Cg6!}W@2blHhM->Byge7Sq=lty~XD9E8DcbI8T<1owN~| zPw59MzUEsO+(1JW(MW)63M52KqPo^iR%zwHFv*vkI$x8(B+LM{R=MM|UoUBoZ^&NRr*zd<|0T#r&n$&M9tX>R~#5P5$%D z8nJ~A6aWb7YPor&M$rAZ-X>1p`$#x{AYE3&0j)q zb}wrdt0q0TK33MH^@(Ublx`Muk0y7e3=>jW=hnUacyLu-7N^#FKXWJT)`{_WPEtjb6Xt%&Co@P#J8xY35q;h+xq0^5K4bv|y7yAl^()==#LIo3|4u zSc{(R>s~&yPWOb6TIoQkv>?VqURc?7gD*`Fy1PyjXd^QCb$f|EM?6#N5g+zFU-&0Q z7rc)b{yrCEq|lX{E_*}@!N&u?9KrP~77xiqXk7FwNR3jwK~N3o$IadN3Kklj-eO7nn-dAs!y~0Vqvzc_~OrCK-ngWpL|onmEBz0~;fZ@uywN*X_vP z>-!m%$<}-WlyinP`1%C6e=<6LBQ@m;^Z#~Pcc=E%9>Inh@t_OI=o6U3TP| zYcH&X+26{}h~d~;X>2~i1|4(Lk)p=(YN3ewE78|0^FwkYqAoFDZ-WzX_LSH<-$LC5 zW507lyrdK2=B8X8v4P!sGK*eBg!aYIZDfq!sK@eFgL-UV4d78jZkdKACiM7}cn@{uN6b=0$WaI#zGlvdaK zW^g*3my5;0rpuOT``)OPZlCH!sWku19z^=ym@2#)Cx!9#~BL(Di$abKa_M3p5wzx z&?>aOu%!AGEM8^3|9qSR4GQn0*2^(yMPVXt7d~o}lnFwd#!s@!sOe5Krz13XS;4)h z4k@T&HHr1Q>*$G59z=?-e1JNB2DNrwdX+ z5OC{FB+XiYQX>6jO9^;Le^ucCo;LzOI1`xk*8l8x@U1c68NjHLC< z2+gO?;S#B(&S7@t-(Ha|rOrVGlb$YMO}ooJH%h@OQUY#+i4tYhIr4uwi1K4SIYwOv zRiy-ogMm4Uz#KGw(o=!l@KPztPU+rxSJHg%ZMR;RI5xN5#u!Be0mA1EQ~lYUneu^u zDY|DvP$K^(us3G2KaCr(MYAwE;3Oo33S15oDYw!anlRpnNO|%YT$Z6rjL-k6i zuc$p;pq!Qt6R33M|GuJFCYA#sk+i0UC;0LySCXVPf zs`_jJs^2D*N)shf-pt=`{ZdaYaA?nyUR+=38A7Z&<3ue9*81RMb@Rmv5MSWGaPj7Y z>tCir<~9B`KVpK$_Ox$INlyTxiJ}KX)DdsNd1RpoMyN@>?%f`df{Cw@59$Rm31HpW zr*S1k^(SL0gGJeOp?C7oTLk(<%7V~12#}gdZCwYxgx8}7*DIq`%I{K|Og;v<&;G)S zNcN}4o34Np|A@&5A++@z@T70~rQV;fwT#;f->pTDrwsb2T!l@69(g-KfgIUFiW*3T zi6ki^Q)(=#4%(gxCk^*6*nxu=QQ)jV+RiNr3DFk0!6BVzyL(pK!haChag;3v20%B6;@bM>M1mEHZLWr+jcO;LPV&8NEr?tZ2o6-Fs8=95jv`y zJEkHpl4xCH(99Oi1s{@VTVqgstEJwzoPaIG$?Pgv0Vi;(OPbdwmlC40L6buCCLHP6 z?&)_0BBS#Im>j5H1s1rW0)m{%FyI_AJgiVkB@7h1fq1$O&))dbD0AUnpljb3Pj~R4 zKb}sT<6}WUW8C|4w(l#Vh)aH)>qxx!EGelf5#9w@tOPfM&g4el=ledP6Wob^B#ZXp z*9vbs!Y5vH_n%c->o;-kU1^<}Ma=TSy&J$RGdD6t;Jv3^{o1?2GaJt7c%|Sf4YJ%2 zS;OMJhsPrg5>;;bZKkg8Q>53E5FI+CW_$);LLu+NQHMrbnQ$M>SA{3C;dzC`G&{lz)!`vxEbKuBa(*hSM?|GCZQp-Ga!#)E%)0}2Znhkm3fih_9WWDrMA(L+PahH63)J$D8KTrrdkV(s_!Nwz2*yN# zP`FZ1Hqh9p-NEwxgf@YKoB>fFeiFUL2B{$1Goas%g=PZ9jpytv%9I!62>pY6Zj%iS znak%6eY_9QjiO*2e}5VR}VtZyv||6Gt$wL zg^z~MBVH#1>Yd4;SrE!Z&_(ns3yVy03`v+7iDke#0hGCM;48ccUXr&4NaPq zh)kp6L4YN~Tnr4YY)68ypl~Za*{F=->NNtfpiCO#iTc9*BZ@J-*{J*jI5FU1>-1r# z%fEY@NGP#^!~C*RTgT4wXm(~#T#Q~nXCfDjPO*5QBp-XYXjv-0D z+RKdz<#G>}c> zM#wuqOj=TD>;I$eEu-=Xy0uLR7MwtE4;I|r-QC^Y-6g@@-QC@S6Wrb1U4lDIC+|7m ze6!BXn)x$-`l;^PU3ArAK|givd*7rI3Tf=JxjfSjs5V0awNZ`|e9nd0b#(3b+EbP{$5wC&K*pT3p@9w z)i;_CqS3-MDp)pLC2W~d?u19XW5y#H&n1~W`kj()bCc`EHtVL@zb2DHxg+%8<0VOJ z{19=qSoY&aAMarMD6u9U^|_bgl@%BIt0``0XuN?{!lu!*r4;X1<)3jw5+4HQwt_Ry z`EBZWodfy|&sa9e@Teiktj*aM7hPK18*A$Neu=^Mf~Far5)b96f9(>{-i<%; zy}A15gLC4zIvJp6s)X1x+|~8EPIq%Uc)Wjsj@P8#N)fcOS^cpVG&*hA@XfG>j)JHc zM?Sv|GuBpwJhMVRcu&K7H$W|bYpKfL{h6A5flYjs5-4LzVOztPX&lbjT3RZY5Fjh5 zhwux=SEopJn?Wtcm@q~3i6xZdrL_kivSC{y3%bl2EpU{Gi1~tGX%H(a`Ee^HF7!%` z%5)d4cVt#(Oh0ry3%XZE*=->>-sT&%1X`+}6xTx8+JvAqnN{i5D| z+-LGfEb3YHjQHz3Bce58(F>>AC872cm9sCH1^j7$?%9pBi$|ON2;dMnRpdP|xr5y` z_V>&wRwzf^Q2m*w_3PHxc(^t`?)RVXYcur_snvL!G*m>?rB84Nwu?8h^-Ol>v3`D9 zxJcsM!Y+6ZCWU!F4O*6(ry=+rSiOl5Vb?td%~DcVPi-F_^m}~w4t*}C5uH+R;r>%V zYp1&2_M_c`{6r5k`_s9WKOls6=OZ-&>j*sp1zd2ny1NdVMhOt&>mr<&=Kc!dVZU&_ z`n)_&A{<2@$>>QWT%)U>i(eco8*;V!>{D#fpA!w>f3>OgL-=TK{cv#gyrF;e@O?Nw zxM7yjp>(Qu?0yQjypHGnEe=f&`qk1p-$+EI?wlcdaTJ43ieA3put~D6&R99nC=iDk z>?KhkugpJp_S@XAUR3jNFV1Mb-jZ9Ta<1Cn8`QGK#5Le)I3;BiH3qQF?i!i0I0rd- zsuDOc$#yMBDKW_gj1C?Tf3AYI^_t3nt&(!4P}AQOUR6=R_QE|)({RK9POAU}7lWCv zc=Tgg!JAM(n0QJNn*BWg2xk#nKp3JDLhe3KuJH&4!_)m~X}^a>ewN0*<|EdZMFy># z7LQDZw0cpyC(2LV6fWMh{Dt8XUuM?p(XYNz&rrc=XsTCeq%(td&*?%Rer(t{B%+mt z|74MbOoK{F1hMeF0I2W zN^NUb0oJ<6Ii#;=QdVHon0H);_QUg0IoLYupAi4#^j4Qnp+z%pHsNtKv4JfF&tzDx7;`ZQm@8Eog$c#~)`Sh$8*fs3gHGjDjwlOcXbRakoH zpf(+^QeAKcgRAZEs5AR=8r-&8z8b9+i`tS|tw1=@!#8($u^rUNtE(zQ_jA2Rf+zL^ zdMQ3AT@-r8?B@ILooaBWkrphgXS=)o)5FuPF5k9iubaoV&szn#5oRbK#a4#)!8LWB zqK-%vk6K@4bStp3a0TX~vSS+`R4_q_j5QPoHnr7iIESncBdQ zM89EcQ!9tYseU@&E#M4>Lail4FvTEyWh{{vsZ?BQt1?S3Sf)yYQg4XZ8W}}}x0YBn zK7|nCo>{gGbm@dS;CdN`yU-Mr8z9gW8eNNOeg0|avu!CS!}6V+Gch& zD|uB4ZE|C3RgX{c+0mXNm2DSxF+)l!3dyyw$Ou7^Gr`gJl;y8Fe*ec;0|<5Wa3!`s zvmxAt<-{viKX+Bc@q5zbzsT#~m|>PRR6VB6GKn$uvdGMwiwu4J{Z}7PNdK^KTl{kR z@qYe%zkBHb=;M9KMA|su!NKaGK>3MrjydmbN!})ko7KYpOwQ1)GSxjAIU#MHQLKKf zZGl5kTn3=zHPyh&*iEV;F#X=7>Gg*eokLRY4#)*3b?bgu{t2z`f=NZlL;G9l0z&e! zYP;MJTG9#2lI{nr=cJy1UVwms51su@s+>5aq=k;VWLGxRr@HcRxxS8O`TKWDG$B47ASc0<=i69&;xb7L`S&a{ab zC$`-S_IlN3C2MD4ZiVD_#oz@dIQ;w%K}*jC9tw%Fxh#*koSZg(M7e#M zbU!-k00X`kSKmz8Un4j%&B@4Ti48y0%wSyVm(p+aY8bW$_Ybh8&lkyz6@VEnX7gzK zdiBv6_@^9>-9^=bt&Na?$k4V03T6|nAH3FCX5QU4A+WY@bsahaRgn%P8wdr{E5zISyfQl#ubmP|WMCvuMyHzP%^x(oEh+)>hBB*PMn>aJrPW zZa@{n&0w1&leoiu`mtnZEtH_jZ+|P*K$aV!-CC6v;T@taE#_{T{W?V@tWdk{t98g)KhhKBthT}HkzyO_EAl!Qix%j+((S` zLkLHW*v)~o+Uu4es`z~Ycy&@r5LTnq3cr;Y*-`nZY~BXx++wa$<`Si7a^2zfE5a zKK1k%{VR^o1jO;sc#}iFY!VVgIF9BmiO$avO zpb1IWxft03QRI)^58D`djlR!(V!U-wF-PawG08KP8QYj;So}pY<3j zIaJ+N2JLAbx0>mwAubQ+!MRNsvGX?_$o$IK`d#K)m+{NH4Lr`{Z;;a6q*0j*cNt|x zaE0ScJyx83Ri~yP!-TO*I}Slb@E*q|>m!4apf-0eo*=UAEtmQDO^tq00sLuemaZD2 zIQD|8(o#Z$zIOB|%qgLH-!mRwoXFVaa!6{NNQJF#e*%-iz^_Ali_GAs8YAwP&W9(X zanZ@$xj1Hg@8&`G6hz4xFO1zRqJ|`^f?uXcM&c{co&LD@sX}{J!|tnXcyTvPuH3|A zQkNbXp|g6D6X?#|6T~q6QK;DrFo8{^oG{KhsVX9qG^430a^kQSw;grj9uKCqjShIB zD&Em>!;J-}=k1EtGY=I(XA%x*>Dt>!n^Zt1D!z(4& zU9B)h=lzfx`tWA_-uw#pov9-tUOLPA*!V8-GM^-oL1T(E#~Gjcb3k!*1;^g5Rk~t4 z6W;7)m#{mE2jKAH#I6zK{@3AU{C;fIuJOg9MSapgAR!~rJxxA@g+uOA*K)}b2cs$d z`wLp9NVuE0(z7wsXtMmZMwq91vcsW#T-DCr_op^`yh|VL$^8A;s3g5t1`eNxIGV#3 z1yeMg;o6{LEtaJhL01L3Y)0Cj&ct@d2H)rWNlVDVDNbUj#0;Ry)??&>?fd-kW- zF$LeJL<#UyUvNg9G<^2z|Gs{3YR%&b9Us7gPVV|4owldmswL9qkkunCwE2+L|FlX2 zWhL7@F>^?zg_;rm=*bj~a&0aCy#41bD&t8o?tn9#hXNc26$`(!BXP7py@>6w`chD*XJdcCa?cn+I~6~6jQuA+Qd5uZ{;vsbe)k<5bgT=tuJO}(-Z&R z1;-Dr@c2Xc;~*{#+c3XgvrII6xcI3{D=SoYS@RB8253m!w=c0t_R#pXWlrTQtC9r9 z6}^B!3e9FxVcUgFVoiFSTT$Hk%RSn9;8OYsc2lKNJCd*DdEyh9#W>l(=8Vs`SL)~V zDzO*VQS@Eg1>HukQHR74^c`DsUSl8K+IN9h>O1sp+v!c0xfR`rE_GjJpI{nnm^&b% zw2PsOjNx75gtl)}!B^O4QKh^K82hdNaaBdOu~0W;^Tz$joUIH*QQ*%>DZJVfiYka2 z`44Piy+ZJluep&FH46UDyjU3IU~TU|#-kxk$RI?@x@Az8NpjeZ0Y_q}v7;6B zKQB;k)F0g!<;xq2TXhQF`Bj}5REmL#VKq|7XleJk&$}XbvVJh5JJ^+%QEgDN!y6c$ zRm;;4kR#0A378~Y%5qW6YZBrifnM|NN{AqSb8gQ%$FBq>bOYNHnmt*jM09~U>i+O6R?F%! z+Mgw>x9HcOHCALxUW?uhLR!^3|E_}a=-;4yAZS%KRrj-e^PFx#7m%{}D*YCSsk-2E&p`X{x_;|IqOmIN!tkwsH+Z!!FJ76U?c>F~xgIqvpXObe zn(sqh3gKb&Do|yX)EG<0Y71GN$!kaQtUvymSrjkO2AVT3o~w`DP&e;|MflEQ zR-(~a-Nf|ys`p0WUVY%qRCFhtMPC}H##1iga`XQwm&sHpzPNF@DQKH`s|ZSle9ai;OYR4p1J0dHK^ojQ#-WU3X2QS8mKZcxBeoz+Vigu zms#-`L*i`$WwL)sn95pmeV(e4NY5O%dKlT9#H#i%td7cB&1|vkPU`^LaJhJFuAv6K$+L)t)x&E zIyzIx;kI+X*;C0@U<@^Tohi=xiGD1Ix;{eVZ7HZ`uL)~Sh3p|fyI~sl7vHbSi@I8m z8!f>Qr8RmbC)YM2p3lvb^7r~uScdt(H}Pyw?4{CnFUWnZ))*>?7)jqu?j8q?B&R8I!FjUA-%Law@@qb}$(=S1_O$pnFwJK=5 z=0sukscwA5D{knu-yhYrfp6vd;++i_w>d$ZGw!ey#{)VMv?pC;+7^M2Y8oc!F$YX_0q=uR0m5${0E}4=o zyNfLeJNNxHt%@KG-qZKOf7hP4N6>KaTxOBQYFAFRT%U+-2VD4b8H+rEVW9oIz~&v3 zy9`a!+$_ud$*~mpDSO9Qz1{ub)#OgK$%@0*gJdRGcTBSBkU6V(n%SKP6I5W^IWT7Q zM792_dW;XIHS08~uO?a@!WZb|1$_~T5wqUMB5Sw%CCYN5{I=o(&#M#Cjh(E&OD1^CKtvlluJ@(eb`~w4g3`KRqy8T*q&7`R!PjlY@7QXA$@K z*#&R#^=#BLzKp3+)ASclY2+D$?ir-p4h`ZxR43)~Q?-H-Piii5JC)~BRLlIKmS@XZ zF^Ss?ZVY(4A|eWgj%Q1c-MiTMFU;OX%pwCAQ4wr}0jtUhdz|#AY}r8Fi}1@^e|{eB z!1R!+g*Fscp?zgBKY=E)!mwFc^%Snl6HvKzo9(KK7voS*>5rSFfSV_imxf23T;hhU zVtF)clK}D&H1%6D_=Yd2vuBzqg$9IXx z`#Br&h7J)Zu$noxh*l{8$(@Hw{xIpM3#4Z4IUwvEa5kxf!? zwS7uNXV9v#AVgl@_<3>L3$1E2t84SPF04@sgsge%nrq);xh|`%_&ah8O%{rE&wi5E z3$V33bWR5&|F^~}w-%Ic^+k237O&1GZ_UUidIwAwR0B5HwQ?tWlJzfZVOF+}kQ;)D z{CQF}QFS>4I(hbVR)#87{suK6#+<-*O%lxyns{bjoYD6ZH92m*zOd`g+)ZQ0f=c!c?xD6s*=ZtJLO%Mu^ySs+Z*>8O1C!a}T4c(K3R4CS|(36JcXaC7aHUzo7)_!0z{~ zMOE5&7Xt)It9vO7m$Du~wD|xy!df(va4A00jacY&Uo#C*L~Vh#md9e(K>I3B%6`=_6=Lh%OSMSZVi_OvJ;GSl`pKIsq7)C4-!?!Pv8xx5zJj?n zuYqn7-0?EW|IuX~GbwWcEY`i=T=W9|S*#~!T8C;f!P($dcjZ7K9UwfRP+70^XZ+rSsT(mB>{L>Ey7#Rl+_B^+J0Ca z^mXC$6>|Ngn3vEsWFhMyTaC3leaGp*X%!N11=UBd83t5YEq|ePZDz&yuYPdNw&Bs; z-D?zJmG8EkRD0c%&F_;OW6o6u>DO5Oo*J8I)i$o%%s>)| zqfD9kJqUXMNf{SDlQee#w@S=#Z`B=KL!_`=V?Q(!D7Kmy^w8HDO)|CTr>y(~)$rH& z>`A~*4HRmJ!p@KOfYHG2i1x4PakFi~(X4QY^~#0Gbee_YT(BzJj?Pjc%F`dm0C-_60UQ5evzI2HlCN$nAl&|sEz_Ig_~se zetAx+YM$Hr-7FuxJoQUVQC%LClHU$RXMC+pm+;IyTNVENci3VIJNHTn7&QAP@_lD^ zxVzTzUT3_nnRyp7C!$p`c>doLeJn`09S?%?4vj=dS<21bU|dIbRum0^ht@e+uHil4 z1R;@&SRc=UOc_&j{(nRpJc<qa<7K)i@b`eJvZ@ z@L`KTQI}Ah+^@+atJD!+M&)eX`_obMun1IORc*#^5ulW)o%BaSQqAB^Oy@K>-Le_1 zYRx7I`Rd{sh0TtcsHc~703IaY&P99Aa(tj;$c-ihr`{kQPnRlegc^lXvu@3SV+ne` zQ0>av{78eFZ>A8V=Bb4zfub00+W!G+ag`E|pOaLWgC=_Ee|x#!X3r}o^8)vZxPX~GPGTT1xhWBr5MI!<3L@}=uV;LrUEWe=T?F4 zcEPV`RIcV0P1a9Sg%(0_5_KeDUh?-sy5NK;gyAHrPdsq8(k6IPsCUsp2;BG(4|ksaVI~s1@Z)sd{?u#J3e-WtOXPr+l6$9)0+^v#5zJJ* zfx!uM?v!^Pz^Ya|foE2r1!h^$bzZ)A2v?dc;WH7l`~XY>cgCl*&k+(ZtuK1?$@_U3 zY*o$3ZN|(#8?S-_tb{YVUxg0E(*xK}b*RMq-*wZwj0C+}6#Bh^4fZSugxJEl`~OsL(wP#5DC_lv}MJm}y1U{+!2Gp@*pFwbP}*@ae_5?tW1tN#HFp zFilH0k5G>I$nh?9SaTV=)fL=tKaV-gfJ+2x?$v-xBx)AKQ)g6~SVQ1?RjzXh(n?l} zT9*k5Fs3-Pfx3`H-J5i#%xEx?Sn?I4EO&Tz(Hl?L`iR+;+6Sk(!-bra(g(#*V@yQV zQyW*>c?Et|!9RlA`2_cgY3}5p=A?`vFcwTQD#3Cb@LZQRl-OfAYfLCi9fId1hu;xb z3`|c!OEN}r{|&%ZVj@A=os(87aeOJ5&35sYa$ymZgV=l{B#{D&(pyJ2R%(;pY zGcvrcZx~5o>Kd#Csx)zdz%}v}=xQcYin>H-&CPf4Ne(&=o&~0LnZsF0A?ntGXAq+z z_51ln5P>04u-z%yf^S?2*$)!e2&Tz*$jel4pMZ897FUijQR#zFl_asgT`;SIUJ2m_ z!Ch=pj(wc-j6jin^6zw)e23%GIEx(Gfl#FhPk78I1@>-1cQ3D+S{x(G+^Bg>METik0A z6v6$H%Nj$e-`AS{QS=a_yx--N*v~o7D!67(aitP>I|Vl=P0KPKaIe4dDDMBdj1k?F zN}TN!yl_sXDE1Cs`$exT^YZME-~-&ccOO#fu&{`GyUd3=%6n9y6nYP5{*k088gXOhI4xdg=SD) z#J(bylHNiEz=A*c8bQxodmQEq+8N61{ny9ml+t5G3yjFdSa2eyTdrY`Nh)$BI*x@fS zuZ(bq;9l-u>Qv-9q+aS&;5vn-So_LNp)$3;uCwmbenIehIRBR1wwfOOd$BDzeK5Uk zRR&e!{HEF2Nh#a3kXC(|?AjO4I7sbHlQ?Arnd)6aiWUOYZX_baHf@nnGLS@`%~vXcPH;Bqj$S^ zZ~lHo)yEEPVSTTNY$0*PGFirhTb2z{ruOe>F;y$yhtzH3m;XahCv=U-j0uUO`7P~) z^H%4EOld8g^_$>SS=87`t871<-<~ckgK@b^khltu9jXVvh3DxdPK+*y&0OSB0yK3| zFLOY=^Gj*~{Fn4|QpDW=U&zMc66dhrKN>Ywh{sK)xrXwLg(^nnm#}M5kY|8Qo>x@W|u9pqu;6tyD12nJ^A)KX};+Jh7EF#ooC ze?5+JOfi=KOnRMNDBf;2J{({goU5S75L}8ovMFR`=`1|fONF>VYq#+kC@dBWnO9T( zjQ}>@y;UM_oHU2hDZ};|UpbePf+(g_gK^QQcI zk}4tgCty<6WO zL-hL~?d4AqENkP&Fo!g8m8PGmA&w^zp9yhlcOR`&TDL2)%X9F zDnB(5cD5ok#()zW&UtkESE~Gpk@G7o%A>(fzVCm-SSqP3{%#yjlsUS#%|-Qw3E2=PsNtzrzh}d{*Z?Z*)dPf$Qjn1Gt!Xv^U$PxI5bchZdm-l+9&joY!j_WOH%H&Jv6aDS5!ZUj9H@z(wOQAv| z!Wy7xd5N}=^EZR#1G^ugEJ9C4Xb_CwLQ+u3E#)M>uu{%SdO_##kG(9+8%p@{>Bmtm z9gxQd68`0}1ps;-J;bV$N`hqupe)GRUnpzw7s|%`CAC|?#W9eDoWS?bl6`P=@ES+a zHNs;cQ#pZIQ~#-2{#MvqT888-q|4#04$a#%8l3rldckP_#IY+tr%JisE1S$Y%|&9E z8xK&|JXnCoLf8X5Hhc_0lL5^J7l5*(f1zyTFQB_=F$f^FOxR0tuY5%JyA>F%*35R( zyc1Emno>Z`as!}dx%bQIZsw3iOK6;XxAv+`ZTmh&$#tA^xgG>Ob-u#8C&^JsI7V|Yf0Ppbv`-Nl41-8=!63Yx{Aaa7kdK!SV zzmBqzO*^o2dR3IbGG~)3eoT-{F7O=!Q8|t)c>tufpXP<0+UCPzj$5E2^`JR?&eCz% zh&@#*K9z9&ImpOR1xr#Gi{QckO^G<1Wy?nLb|JHaq5Xk#3cP`MpUqFzHnk2J*R@}Y z*46kAz+=Bc3Px6)vIxx|c|88i4Vtz;E-b;)t`@JEJ5mj=Dkj3vSi*e}3(W{ZIv>mE4m{0N%CoKgLRy(8ZU>yilA zQOUF&N1RvTe-y!<%FLR)6y!?1bQ zv$#yE5wTsVBorgXvw%@(j>U~phnc&BKcz($n-*d+${$ii7K_;PJsx5lWZtgfRypOS z%*0^YCX=J>gr~%J>v(ZGakH+TVmpypWR>oCEMp0c`T(R=M6`Z{2Q*O z!%A73oGCJ_p=gCHCaj=lJT7dnpjmNLAf|WQ{NeWcsENiMd^m~zmb%rS4AD;2O(H=D z>SGU%OQAMxZ!(#oDq_>x{e`=g+tN=*B9~!9Y1KEMbILDU=5HV6A3 zJ>HWvJ)A&-(`wnM%?G=;?WI^+?Z*1?4ITbWn=iL1APRbS=KkSRx8BwH@MWUcW9Z_& zg&PrZ890~ambiSsgX7tFy3oDc5_EZ~zMLaEJ_iC*Cy33c+AbH`@J#DYBrn_UDhE>w z;NEXvY|gH_;e_;~k27Bx$QoDbMYnZJ@1@I@8!V}x- z0aFjde;_g=h1jF+?7kW1L4S0)5+JU)<>!=9S$y;x#XB1ek z3ySe(&!X8E+E9tE2$LrU2^~p$+EaxKE{PfC3>*?))`8vi>Qnayu9<1%2wjoEOP!g0 zFWr*^>eR}ar>bl9s~2HIwOEU|rbo;&agPsLo$=#+n=#eS^p1jMuO#7yBl#;)!rVut zc2+5(pf+CKGyGQzwJ`IP%KXE_UoreV=*UAKx6{)e@#kHYswrN&Nh0rTD#nw1zppQ< z-7|1~+z@Vh5fe}1vzJ<{cHE_Ok#P%Gy*kECY4zi#4!-SQbcz4V{xwsbQ2Z6DlW)us ze$%Xt;rjn8MGm|#XjWN$C8$#qd6#c*>ln_zEBf3YwQwBd5o#EZbO~pL8Y*jE_@kRQ zulbv(vyc@n`1_r~A`qC7Fi2zO+HIuZ zy6J%eS0ERgu`UhXI*NN_dU`7B^wK#KM`Hwk(c(EbD{^XrS7opH`GD33C1$Iw3^~oZ zJAsZbIK9a91mPo;W9@pdQ!M!72tg6o&L^{rsi=;*#>-z&poL)SQ#mru1R77>4aHm-GZw*j31dYjuI{S__?K1>(Ifq-JcoCpz1j3n!u zp&IJ~8Z=8cL_JBb)hV;>HyS($vivSh4^IaB)?5MoWec{2T8r`Vs_%N)-)lXSu-a&U z>lnCQs=1C@rFhYRR^@@{DuXQ^pwjv2+rvnfz>jh=Q|;hhJU6JVZ(la__NyRUo~5pL zRGLLh8k59oK(3DciPd%?jQJ`@BV70C7jq1@X+=sd>v0fehw%AV!=SJ)lJ6;m<*&VT z^{@Hymd#hLF0o>2i!N(6oH@O%6w+sECa%9kS!LZc~|skK-Z@ z+NFI6W!cPVr7S&*_QPUuU$LnyOxhVovF?@BRk7R^O)^+KdR^L7sH1t2mKxWZvk(O` z>G;^#Sko4$jKU-eWY;UCH$=pn8O5Zwh$ox#PvLOP78eVQ{7wE3t9b5jdIT8Eg^P#l z!o(f9PFif-kx9*Qdee4n3whb=Jd3QVK3?%~34z&n#CqZ4?bufN(CcCg{I3_()Tm#d zeR*}gTkzftC67A`)!!MOGki4T(OzFRKi*@9)Tuy9R^6UH_~J|=lTZa$EW^NheO zE|;B5X4dyU6h|3SXEHB$k9c*|&!jTKHL{p1BQ%|;*kWH$Xy~Vj$$zr7<`6VD+ z3#K)kVxk8{PpyztnoHB1pp&Yab=2W=4YrmX>IrI~+@>akm(XsD!Zf~vY~ed9$B*2gwuLegjd$BQcbp{U^p|vnpuNI z-N0h)8_lkKYB27?tg@#sJya9WFoj!*I65VK=12mmq~wDwN_VEeirwBydo4iV1uC4J zV7?Zx@RklrpF}@#E;fUkjUqT&EegZ?Ba)>ZM$*PiWTx0{HDTP`9&b)lZDuZ~N_n-( zyJzTmEUTTiF7i7XJc3aLbgFL=cOaZN%T@b(;Le?HnJ@6ae9GkRBT)2WrH$r5Ew#Fs!}LRI@xW9t(%HH{8O|c9Z6You z{L$ytyVjCvh0YONK~Q`1nB{lmVWDdFa8Mh`SmhZnJ>{-AkZtE#hlW+i+YnmDH0rpa z;kpO^#4^Iz0gEo!_R2R~I(Pi3*Nw-uP|82MA%n8>aw)~vAnB&Cm%Emrr%%46e^WVW zR2Bx!c@wA;(1cm@gV8i1)PI0wy$P&rUz!HV&+^7KsdrNy;i%%OZ}9tQXPp;A)tBD= zpA!t?S9lESdZx=lR01QL>Nd;jRhLM?S=>+^b@f!~Qq)G$SA3?aeQIuJ+b2(JK%F7Z z(dHk&G1?3xzCM2HWbV|HMaLkchd=GnUxeusZ+%7u*{^E)WS7d877sf%4W837dyFrb zqldLb;P<%P;C1ULF?%YQD3n^Z6eQPmaR?sFA& zw3@n1fVH$3BKGGTaJto|Q`!?ce`3QIgP-4Frp+ut?2@GM}k+WBTj|Xqx;4 z_s-pl+~vFOVt#aHhVJXm(Lxyz*e$Z0!$SElc9S`u)3%$24_CMZL6fEZnm zOl8xB9@p!LOT9C)-vrd|QiEuM(WpNB{2}tncdE&~GE}nT8e5#eP z&x#4uLu)I>6tpvcVe9eu=X&*A4nhx@_@`#KtgbV2mC1fo^nQrXx9rXY|AtQQJ;N+W zP*C_1AeTxM7lHr(dN;_l4j>@QQvWnV8RAVcKdI7$uPoJfuuCmCACl9f|Cv;(X0Q5? zg11p8`rBP!()zNFy*}G}w7dicTPIb8<=#J>@-}Zf-2UUIR8Z=PB6BJK;ird+|HV(^ zS^jVMDV)Lb&RBZQ3CEeO>KDpa_f($1Q9XV*O}gl7Yh>qp=RjUKvtD5Df0P^6y8gQs z9%iXCL>0#W!ly(Ysy?cm^uZ)7o}^>t=ah>DB+!@w2GkM@KLBZt%3ND!aGFZDzt$fb z<_=1$LU72~Ssl zWdZ-&7oR{ycuMHE@A{v0Au)Lv-7Z{=ZNF3$D>-9R%^fs&|MbCATAXO#?2!ZRU!=*HG;4icS)7(*ws}{s>9D&h%-4>zX7Gd-+Y}vkeD7I3t`snP z3wyl^M|kW=n@TgIbqBt! zsqyLg0ip^jyT76e5m^fxkH4Y{;{PG4h;r)pk`gkdS~85Q!I;$ZR7uD@!9BD6h-qY< zL~^uSajkmihC6obH915sJw{2-RL&7`LsRNjf72O~v4Ni;CanfxSe-saiVQNYT;s(^ zvX7b|B4H(H>+<>4*3tU5#4T7*ZzQqL2AdA-;~eZV4=zwg>u6a9_1U57 zd<=<%+#^T%b1zjK&&t(#8bJkyMNR}!(O0x5-(m3@>t_{nhZ0H%$Ck;w^i`J~$6J&N zfG>5TtZK{Av)t)t2n%J?r9%J2|2)un$lsdUzdsVnO*f2GqgFcnJ%t3VFz9n$NJauo z1yx-v_AsOt%P;M@*;S6U&l-+fOBGnHf96guws+x)e#gsC*_(Qx@|0IzGU@WDS;b?H zznqK0jQb^tQYp*#MXu2F;yF`7C@@F9-xU~)>-YT>{F#@fg?PTCIeNFC($R?E4a&6C z9$-10@(9UrTGj_mOmA-cy~ebKeHzTtHSSF2hATRnPo+lpm@qDPn(rWj9v z(vl;y&+iyT+OlU8e@uct6esbya-lV8PQ44PP?3+iIKkCvu4qL@q9zRG_Y(M~PZ*zr z3JGHhMxo%G3QS{jcmY2RIX>UF7h~e&kC_8IujqsMdY+y)#gWh7o+1QG12Rrn)GQ^U zGVtO?b*#Y-COZe!2^D>E#P_AVpTAiKhOrL27Jm=q@*YFJGHP!A1LepdynhuulXO9c zb~L(P-R0fs(Z0JxabcKs$2|L-YS}x+@=hRDi5K=GCXFdj?=z`*PdZ=!Fvm@Xfbpv^ z=yaY=fuGql5}&k_?D>kNP?LrN|S3L>s|ApM-RU2VJ3H zP91c@=dwBC@8BwGaYjEWnbj#kll)7v{Hg9%C?GXB`pF?RTlXa)v|wrl_>*CR<+ch{ zAcnUjrNgv9ijck`1uQ!%m7kX{rqhy?2@fGw-0u}vG#!_zuA)pmXqBDc$ywWMDWhD2 zmA^SoSl0))Xm{PlYYrWkQ6njA#@-yzXgP>V7bSnPuRb=8)m-3LI>>gW9pdAOIvaF}f`KK*3MkmvNCI>CEN@=@_F zCQ}TO=}Srl^ko{qr|N5>-LP0Fp=bKL>~YlNFipqiI!=jrk1*%(0tsKvild{kwWcafH+0np&LaI2FLO@V7@=E@Xlq``3!^9S)NsJ5=PM z4GVo{1>;=l)9YFE;xM~lW@~ci?CD$wO{P%sap6(Dfpa6vD%rN1f%X806;H^*)$O|R z=&I{y_R~TxoR@NEeqD~8gFjqOedD|CMz?MC0_F*oOjm)H==vsSLa1@&eFCwlz zpEn){mc?Y||GSDxsKz2b<|{w`gbpJnj{oZF;vP@}-(#ki$W#t)V1r9U1 z4Srr-v-ppS%F2v~4{bc}G4UoqgYl3#MbvVl86!P?`TtZ=@pOJ8AMBJk{9Q#eBzaL27x6YK`hVT8zq5DhoPhXiGO9bIMgD@} zUR|&F`LeATj(;$ki=|e3z=d13i=;ta2+vUeD8BHP)Vpu@$u#Hr{E#yR~ z??O5Nc5K_vhK}fG2zzWDWZ_qPz-6>A+3$FNvHZXH>lM(KL=I5m;K#-;L=c1s?w~{x zQ=)jkMS(3QYByb?;t`vFvilOHu>ha^^WXc69dSu%A$_xbys-nyRT=CHm-x+h5OHT* zZ=J@z(dLtQ4=*qiKu&C=pu+awZ!TL`+6U}u=IC`4Vl~NGMSbtF?`%vcPw4%@s3&&m zT?LrDZ&cdyf`NE%$$WEPb?eI6_8oo8wUG`p{wvvQ_8ojf&j|9%+)vv~SE>+aZ&(K+ zozSI$gpxo@hwIfANZMMi8$YQbw&kflDF!5ev)T}oHTK}h`mZ+!3mUHZ;WjW$TcbNL zU*ErpI*7i+Gy)6?`QMvLVX*TLtW~3X1upV5u0sLaFqQzN?S=^OR5B23ffVZAc$N92 zpRgNZUK2BwW-81WCeIM7_vlvpGS(fzWLAkfHK;xxP1ipw$vq|>Gk`NT8LtXJqxq+X z_JOjAB;036h1Y0x=mMIRED;0nDw9B0G*-pJ7o9Nh4 z!nv!B8Azx$gYew6UkNq#Gq-1p;ajgBs3U+-(bJ77{D z1lM>mQ_X#}h<9s=D7sgo>$#PXqw5)yW_P!U2X_Hltm`>d%{>%21L<-8gOBj;oGN1k!@&-J|Huvni zi?ru5>$&M+Lw1Ma#kykb{ju#!LdTDhW{9%Zz>y81{n2Q_CV6*$8b_i;TUd2>O4Mbk z{k1-{bs1xgsHR4_DKk6kX&0NBmHQdjFZYedh+*S5x}$3G&hR@zk< z^mUOvFYO`MP1d^D&EGVek<**=9X(e@6CAbgW_7}%l(Vapc_-%SE>J%fR4ehG-K#sD z<|aLl4?6+^fI|T|aJaqE@jfrZ6#|-&OU9IaCx;zLy{>A>}XK`+x)J zUM6OplnR~I5QF;fNX6c9d~N2DAZ;CZn-g{3au+ z91g2twFD;s=S>{~63kUofc9C3!D})!%`;mKFF`JJC+$ID)#97>Pf-{A2!FSCfm^@8 z{B?w)Sr>e|s*8Uqv@o)B#eK2kdhH%|h+{5@I?2BBOqulxLx{sVX6PCZh%9NojhNOlwy08@*wz(ej74CWT{&>pk7R+zFPrA+Ao-g1DLN*_#=Vs zbR7h@DGv}?b{YX9OZ7TP4j>A^d}x(FCPKsi58~cBsE%k^9}N&RxI4k!CAbq@gS)%C z2X}XOcXtRb!QFzp1qkkM$T{bpbL-t(?^pHe)%#;l^>p`Ivu9TCslC3{>+4r>A9%=_ zlGp}g#%X4z#k=>EX1rDzr?x6ba#uy7owiaj?4y>$Y4jX1bSu~D_6=%cdw-!g`%WcQ ztM^5{<}&GjEb2LZf|CRakbDQ0>e_Y#bAuM`#4CVBy}LGGQIC6ywCL+18YwI*Zm|yQ zg~1;wRw>84&~o4G0F$z5J1(ncbjZ741N)sU1-sr#eKe5>@;KpchODL7)a(^kY@X;b zX2vY7W@pB^8h18~IJRF$t*q8}Yb)H)Eb5%F=cc z$}i8_W_`^oU+k#0JucVx+f6=RbGz2V-f%8PbIW58tB%@p!+6Jw{^INT8KFhDyRy)X zPbqO^8h+e*kB-qvkX5%VmL$LQq(>+zyNQ0Qd(+NLlEp6Ru@F$-OICJeh&O_+2lm{aL*Q^zo#r1rzT$gT!>;~(_1 zM?7o4FL`prtAcrO*XY9$1TAUB^2(Gbd)!hyQ_pva&oj*xCF@k-s&-5ogb~X_5^;zbHO3%CJ1FUzLwj>55KL{AntX?z7<4Fq{YX zV7nC)U2i1==Ci~2@~V;Wg_sYhkgOGzq^Av3NEWTr%H~xcW>;YND5A?09bN zn6u88Uvt3DVEIM<>ju3}zhvGFeZE>(MBJjnuhF&yuIM;NG;-0AjmS&aJD@@`ZM0*< z$DQQq%l2ePj*a9tH%a4i$F6cuK0$T4N^SJf5a1{slR-CKEb7OilwrnFL zT6e`~c`*uJwCc$zpWEM)2v5|-I7#~0#wUjl9CU^;lYt-q@Ok;=ts6esoS>0yBp1f= zMJX*apRQ9B|BrhptNQ=9J(P!%r`6L;neTfj4~+j`t$uMffyK(U5ormBi?ED-8P~cx z`kgEf+pL|cf3p+`?JV&ACf%OshR8>!+$=$Nnl@Km99?&5dr zv=Lz%ZFPwLWQs-py}{-|mU7yaJI^2k{;gI^p}E2myj7miMoyZ(+?~byjo95HRti=K zozLpZbc1T{BX08YGL?-|@kE2Rb@tBg@GY|Om5;M%dJ-J#uqORu4(HpQf8H~4Rn$Ls zcZLeeCq;zJ6Vb90O8+4Gc{bK3)902{C0g+Pi(A7w>a_NZEY5LI8#>`eWyO3}+RQY( zF!I3LBt3lUH$%q4O9umbzl2)&_?4O>+oq{f0@@eG7tdSumAS6W&ZI1x>>o)eeK#Av zNl{nu#*>EnnQy1HLrHFN%gmh>DU)WGD(z_n71C`|;nfHekTIIf!^GzG;f4XAp{L!T zO7Y#JN@B<9Xg1MkCzh9`0rKr(5u-4QWjct_Z1uKS;9RW3>R8j8g0Xo9oJB`Ay>-hKQ|X9!1j{S460YsXqVR@`yc^?NZNTNaBIyEs{#`;Vg@3W9_jfiy_m%#!AkcwFgjx7_&-A9d2%>B z<3*|&U*zT>B#8JA183HQ3f4qj-c#iI46j*#(XC#L{d0TDoPjkM>|2X_%kPlY4=Uf$ zyb(-^Az5PNx>840%qs0(1*SAIM9ZaXqUv%^z5i}DF;wZn4{$DQTu2eGmA^#S^3Se) zo8R3iUqkF3KCFh$*}113(eYbk$r8aEkfrD1GF`zxElKR~Xs%@@N3=!MK)2N17Foxl zq{)lfRLUjM_{9C2{l)%YEJn=4Ov2dQGFku6G)5_x?=(i?V$c2*IE9Y9B6x8YH<2hz zW>>oH@Q3N4L)i7%`yVSc_tyC6Rh!tEGn5y7-+V3Q?ys1R&fPwpqljeK=k+?l#*{zhjJxyg`mZYj(*xu3rHkh37^OI@2 zus2!*`%CN#M9tk*mg<%*7(B!SP`QO^K|y)gdZq1mWiURaWCk;<{EN8A?Iv+~PlJEw zlOHh8K%(cWwEx3`c9v+=5!RD7EJc*<+=<<(^Ah{nd$vv=D;n(>5B4u#(XNs9h%|Ub z*oEHJTie#^M9zQXDRSYa(4J>#aqD>PjU_GnH%(FEf6x>)x3nZk4MVu2OoJ_C=d|#5 zOjmoq{4-}Q!>n)IzO){_@L6O=@Q3FMKtr=yt;L=_?Re8zou6y|>$zOlV{2PdwEcau zfjm`Q5pQy_UZ<-CLpWdDYeW*#Q8JF&Yl&4&Oy5R!MmnZ!hCB(neKk3&glxsu7tXW0-}6V(jk{E5*u=3St^^*0dc}2(*~54>G$TU8beQvC7hn5 zwTd&YHbo44LszY|&Tg5!9k2 z+Jhh%IA}u%ltuR!(BeqjU|I~b?oVGez(2HOS1pXPZ~W1h)Wo#f0oG!7S55F~g%TIe z`r4*r%rgpBBV$PY4B8w3aMF6|=IHei;PB~sxo^icEfe|X*~ay`j_RN2sJ^bj5bniB z*=&q0tbaIGM%~#29JJ%F))}h2sE@*DsyN7-Sr)kbK0maAhCsATP;q{teV%ecZ|%_n z0p*r^w)FL3XorPI%ud#&y7L9Y9b)i|1Y?0eM1X{X1u`ON0sbV3jJ|_qIUZIAB^UFJvxF_=VM$BJ9u*JqCAs?=dp6=uw^rJi<)}G>ATM__3^VObld}j97=sK-2%@&g#9l>T7vXV!a)W)SlpRR+|2ph zsKiNyQaHC99)eB1Kn=B_shJ^2N0d3O3O(TJyUmwYb;!IFJTK59f!h~<;6%paC2qydW*a*8V5!bBLH?bs@U`3H%`qIcv>^Z3IoonY zJ{&(T+`}sW=-;Sm;6G5)Q4MEukw7Bs6w+gN>x3SHu@O`yhF~g<#Xi*>dY2!sw-3jc zQ3jG7(Qbo`v#_gmoYs}b$Ha^q!X~%}&ii6?;4!(;bIMl&N%~Z};h3e4-E168s}qnM zorT~{(2A>GRZ2G!+&$G~0z@J;WVv-HwBWww?b9e##E_BZAvTnY!o#N$i^7U7MGs7% zUlmII!3j)$H5V1=pEi?K^1PPQh!c7Hg~h|^*ux}-#z2RlN2UV@IQIqNySb{)Cc|p0 zMGFhcfKVjy7b(6YTf24uc}zgSY5t@DkGI%g797p>kt&@(l)+xwPmm&&RGchBGrka6 zfj8usqe8Q~Qs1=oTZX=Ct45b%NGl>Ws&SPqRA8EgQVi^u#e!jQgHMEldM3F!r%@R$ z@o#af=VUGMeD+M8G<7wcAwQ)me_L~3Nzq2}`|=l|N9**bmy)J=Uau=O_D|R!D^@y# z@QIY6yNn)d(iT-$IW*4o1CqA@r%Jh2+Qj@ZVyQ!8OTnQgEtWi|isXU09rRSIy4SAx z!&rF4$up;tWU={?Vr5BjKhj7;>}%XtcX6yEa=n6V$JnfRkw+OcJ<>4SWt|%uB;_cm z4J&Z5pb$p%$?Z5vM0ptmJ+fe1x;%waLX+48O85H7kT;0TvcjY`q2Nx3&smnLYL_%J z*Rc0*im6ykE=Md4$!*fbL?ZXRnMik$oMH^BPCTfcWe1MoxvmIeI!jma^g&Io#Nxkx z^=ZSAD$lN%n+{hTR5OK!`Bw&IB$J0~@AR?Y!Gz~tL^?gX`tfGT#pgzoUmWL0le6p{ z?m!bEUtfOo;uau~of<+>Tu~yBm5V8eCbxy28A9nN1ss4z{K_JP$^^2A`EM5ZvJ4wf zK!aEss;@dEpImQ_J~oB8*mL^c$*lOt>~DR{s3Wi8ocEnbDkM|3U! z3r_(#j}J!WB-!VN?`suaeK8U-i)K%pR@o{)<;+o_H;>f$t<}B1WYq;b%w^b00EruFQXlraPSrhGY|VDXu2kVrnfwB18BN~J3aur-*7u# zZva(+U2^gny%B(1_{2e11j)!t`~~bC9vZg6uWNE{&n9MNKnByU4xAdK)Z1*zE z2COu*GsW)VQ!2F)0dXj8rXW=l@<_Tq6sPxYZRid7l2+5F^5_2pk}~T& zLwwu+zgfyy!$Y%1Tf4q@w6>oOJC)5k6>|0LU|}sf(anfKcHW>+#fjjoryJe=4Eco} zTYw9BaZLNID6L)VAkq0f3*Jc*9ms9R$$HOi4~?70eleeR!h;qHp9s$tzUiLqXTGWN z_>KC!#MRCbAwCH(fyLbZ!o@r2O*MDaQrxH<>OtCJU~ZXI&Nwe$GC^XFDqzl-ipaln z?8IQUcGR@Jb4b06Dqj?CJZ!Ul6ONpAV#E@HlU&wR1n{}=Q(^x6lSjrN?D!~+Rsbsz zzNL^g<(AL+qa9Ja%KYoNh4aAhUT`J^hX)5%En~L%<+Noxs4}>2q~wUuZ1~9c$X8|L^>&lj{BmJeb;qW@UG|4TIs*Qs7b@FB><~B zeaEU0(&5aH1N_ArZ&n7xgdB3<(+KBy%;S-<;5 zX|hcwX6@_TX*XI3G0C-^<9Ug(qxQ9)m`Pr*q6>NB;Wx@A&2q*>@)r<|Go$;7Voi%S zm>R0>wDQ(cXWOKNXI1bm(R`!8 zJk@k|6ku$i)ph+p${`opsEUf~_~X@3pz!t6%EyM^#GsE~cKZT_cqf`T)+Pv|kM1_| zI-swi-YnX1Y5O{W`o+=49{WZuD_)JiDh>n$B>7gGO zmvJT6qozsp`MZs%cXf1o6Zs@s10(W(TnxC^U)r)nM~U%hd+v=KpMR5TP23e)ug5J@ zWu8<0pYxDWAgZ`qwL7S^Z- zx4}Qc%564|YcDCC6Yms_W{%S4WVV;ZjbA6Zzo3F}7-Re6*m9=IzDf8*e#qnqaCDqK zTT+Ll6_@0rr4>gZ-ucjaPWwZh8T6*NL{7G4PO{C^Kt9;LH+j75E`dHn=`vMb^zNYp z{>G0!z3@>%lihcPp{Q|qXjJ_t&aC_P!H)W_i3y2)!?QR<^_;CY3GBguYr|BZ8iGmL z@u+%Vs{}1;2K25%Un)O6GaxVl!uqZ)+4w{_7FB6*J^qBdv$sioy)^d3XniYi#-3by zP*?BZz&vKpL~uk`+FlTF>>hM91S^C&F&oRcaKguvabxTKy)X3xW^sOM?&5cMc9jw? z(1-%4oj;akUtk@R3(BjABYrYRxChv=(pW-P#r>e#dGO|jde*C1?zY2o75F2`8F$%H zl;{-VpBbpFB)bF~$`{)SNd_C0gn3^AKXmE9mi^fvpa+FYnE_&jo3}OC;E(!K?g77`HOS4V-sws#tCye{L~4p;XS4A&CM-^Bq>_lxD${Le7*Uje;qe zf+?Y#sc{JEPui{=3{DgZmPy*K0*tf})U;2ioTHIyyiP&MY0!!4Aqnccw=6$PX;n*0 zxKefPmDUj7|FZOTi1Jvv_l3NpW$ivscFdwHU==)VV{BXsfd!SksR z6dNPo@T^oIA0iT)#rY%lK?;Fhw}6?cc=nd9>4=a2`3LnG0c3butU`uW=Av)7>UCk` z{b73hCSz@w4Te4jxav&dnb2rJ5!U)8;ffT*uAyAga!d%?)PWjhU8yU;=dTTO0fr_g ztPvFKQFkh?@xD7}F9lqY(PvUs>HWOy{X7uMM1P>h2t8k#(UikD1;an7q7XKSMs0vW zcSQYCw<(jv2CC2Rw^bP&5xb0J%M{J3luH3BZBrReEN~8y}w!VtGxJw<_r3bW)!ip z%neIo^;CxC$VuZEvSlRZ#6uJHIVoeK9vl_)p|8E>3i^LvjVzwTv&s%XY0{nH} z2ttB&dKY^pyeRty1{|s4N7x?&r~6Zoz`HjOlSxOpYJcjXXG{*w)=QlC!(a}Ya!~D? zu|RYUNOj;w8bu*Zd;Jv78Pm)eQ^HFccJ-?o)Iyr}V^6jT-!ViS;y?p}F}@ijjcU zh%IM5opNGOiGcqE9kDUPtHxS@b-+qZ&OEi?0}D?x$-@LI_!`i^c$ ze_iH#IMvIGFVtb&7~a0HifyQP-`90-?p0f1@w|;7BMwmFh6eKQ=fP6Sr8j))b4jiX zZe1C(1f!X$XlN2JO>6=gFvC+1KGm#g{Sm&$SKZ)Ot%X_NQz<+sf%N#&)GV%dUFY-% zq;o8jH!HH&OYSlgtw(uOg6L+t_*w1p8sb_0RZ2M5VHcFy>&@$ib|@4+Iz3rXTowJv z4AE~mNci)wfK9GbH$-wvzl&fr&ci~xRT<$v!RVy(XWLBI^u%QzQrSUrgfN>p?x+k9 z_v~MSWgpc9e04g&p8;vKwR+~4qJPvczpn}~sPJsxD< zAebw&ghX$&c;8YyU8W;)w;u{Apk<)oGH58?E`(WH)HE5KEH^&)5G?eRmjH!W+g{qb z+Bv?rsBgeS^SwEHe>Z!z^LFuec(8SI(V=DJmkke4yE?7FKv7!?&>28-B99e(7>|_x z0?|JF%YSNYRXa)c8m9f%#2CKPWFL!QzZ?xzYrpBQ-3UAgmm@_sve_18xi3 zW{3`4D_lbx$B*D2wj_OaP>F-RzYMDCHoD8Ptk$I0c^!g6$A>;&L?M#=dNPKx+~hqV z($Uiznfs!G;c^mubN0Y`WTN^({nt%))Y#UKY;V@V%e_AHlvg4_W61V`>KFHi7hN}h z$Qu7l2CQ@2I?T_>=mG>-@SrR?AA%i9yQ&aGtt|0xi%^~~WLa;)>;pe3)6JxnYK+qI z>eqHVedxM51QC_FkH_Fv&uaDkR&?ZZzX@iofpJKSf<;%X#?%PsyO#+SyE7ha&7td& zhf~Kj1QQhz{82LkYC^VOX3ly9n^!U{pK7WbdNNo`Y$DH=h+(Tce~Xq0?oTDAG-;i9 z0W!flc-@pY+gT-j;U@D1=xSsxcSw7ow0#~tTRK!onZ|`^O?;X!{mMT35$}bzH2cG+ z`&z#ld{@7kaxqGMsyV8xc86MVyRW3}ZJpm+p04))-1=&MPJcDyL*}6tQEqe$6Y3u| zflo2-h$BSg-qV10gHlFgL`i+~5MljR3;kIN$)cH8jdLywcC zP8GU2i803fv8>y$kWf{W6+xvC_09oJ{m=~;1%4RDGkzMZnlHevsAGEahbQCtTbI{; zrhn^^{j~X~kK07CQ|WwF)%dT^yioveC0U~iP` zVNIN|&@d5DpIM@?ZBsR?=@ds3qNyEPtSW6W?yIbNmD*8GJrPCrzhUCuvdhM*@4uY> zxvd^OxgkjAjx)atszDrXkrnjvd9(Ztws=4fmr9c-gsNc;kNq)1eJ^)v;$!;9TsT~) zY#v<^LH>XNxPhU~FLFFf-)~t@J*_!=*XsqJ!O4qqzf;|4;Y}ZqktmwR#qT!`pMTo@ zMHwOIq;^@J``q0>_=oiq|2E9_Nqg_H_4TM{hh|dshph$FC|!WVQLjX9>jNZ%ruupF zeMzh}gAGbbo^HbM2ZwU>mFN(JyrsBt%lK^mZ-)CHCd&-g$TDzXO4___Lke(_){9&5 zQ0Zx4+q>4aUN^Fnn=!tZ`xYs%82J*tF~!&6sp=_0{~XqfOlVnh1BW0S+(P}B?7-f7 z_6YHjf>50-oCz`J#(?DWXYPfMwmvOdE5RUp8lEk4;J9)!UHb|cwmwkE=d_41WGX!% ze|C^@eoAlpERQjuV?&zaPT|7G9Ok&{+2Ptc!AVknm64!!6l{0Fh7&&_nr~@B^Dq9a zc#aoHNZcMo*AiWL8ZvO3jLJQT()5K|GU}Qw9v8;x`&EDeyucgXT-OlY+84`(Oa zOBE`N*hlhmA52WJGsG3OKr%)mf9_B$Y+=EThRoT(j(+qwC@o<4+}_@yvyru-eIXiR zA0$1(Bd)ETf38<=Sideq~R-~EO7PG=rUesF7UdE6A?C4zYG4jP6u zJp)bKUX^3#=9D)`85a_+)>i^VPNkUUsQmCTfhMu0+xhpdGx{-my8Rb;khvj%;`qYf zzczUZmS}a{WMa}j6Nz!@^!QRN1dXf^=2J-sb~0#%)SqzbPzXWZl(FMWCmWHtbzF_5 zQ>wNQ$HbeL0W{Lqc%s7w%Fg<% zXb+(oEaH>RiwKKR@{#?tcjP?>Zf%vo5E<6_mP;B;i~OC z-IEb{+jB;Gl*|?cb1KP-{^*pc$3JE{&y_KGnS!1$iVHURX)Y2hYIdN!U)x+ZXbw%0pnuUb1=twT?bkEz3U~Hq1M|!r?Zu(sd3<|r6-mW=TZ#lZIV)w(k(P2 zxSX6=xHGZiPl&I+yz4l@)TSa zPx(&wObFMKtx?reC7(AlS&?`HJSvqq>3F0i-48@^!)=j|B$gfcEn_TZkf`UWhZa*_0I=H=gP`L zx2QGPdEr+fKQgT6@_R^>YG{e#g9uj=#5D>UDdU^=&A*XZLC+USseh=FPHpf}PiHa% zsbe#m0oAtpSpZR^j`oP26@sm6u5_qzPh|d~L@5`~J^$&)IRTvwdw7{3q0Dt)dyu`L`dod$$Zg}CZF$}Uh zGFuwU=y`CKxd`gRuIJe@0+sFGCS{_C$N#&hvJ!*{dJ3!V$aYDUtHA?>Ylhfu?z?i0hqOR_|>-+VyRJ=Att|ILrBqk~?mX0w zqjr5e2(rqa&$)75_b6J6nI|oWmV2Ms`=5j?ZD=<{_xaS|rPnE&8|dKC{t3KHPh$XFU27^p}bfkig2oL`LbL-U!gg=9H0se}-DS zzZb1dOUD=C zQ0RV4qRy}nfVi4<=J`W$3*B)><(=Kn@Wh3!8ZFKP!N}>E5fv!wzeXt2j%41d_?wRX z-p+jI`s(*)p?aUMb!T2JQx?h2a@`M7V#ufx#RW9=M5)LJL$|MOG43~T9Z6mz423lC zofSNP=r2Pl-EtA0%n(>Xa`}AV!OUB6ixVZvlUE+zCp1j!F}_oHi}J9y!`Kdo1fchN zbt!qiu`)ITP1?D1$&A4rdT6f)v&b_&&~VDm8H_&t9;4N_6&bEs)7bgqU%9gl`$Oo1 z(>}YW$&(Wp8cjwRj32<%k&7bRma!oKd_iq922 zY@<1Ox(=eKj@6JO1 z`H(l8Zk|{UTSWeY5t)%w63l>R=JX?4ltF(=L?Y%zdSst2z;G)y9u^<(kw&e4c{;2s zz^HG*W7C9t<(r6qDxK!Zt3z|GyXfC})x)bsN6y6jgcM41AYt$##>@8&=Y$;AxCHJd z{+X)9x_=YJFK}|d#6VKR^|1+^cbZyV)m{-_MK;_hFpG_3xV*YdB_?k&InxuuT+2`j zJD@&Q0z)lp{+K9SLX1D^RmvQZZgh9hF>g*p40fOB8)Ue^lT`<%IBl*Y&Rs|3gTsj< z3(|)fPQiy=3qf$5{|6NnPcdsW!^9Hl={2=6gqVfrkw#rEE`+h9yKnuAnvbcJ|YGM zmIm-UW?WgW<}}7fPOy(xHDKccE&PCHKic_FFj`YQBK($*7Vt>() zmgDY3R9dJb9xU;j$jgrpAi1Ht5zSqw^}_~;!gk2Qv}s_IDO!oh5|AUK+~Rr(UXOg@ zRG1QB(#%ov3#f{&CWM1(@&#;<2?zQn?MrTl6^Kj2q{i<0#SA}>29{*~5rvP!S<(?b zWt(xn2;BF<;3Z^Pt%jPa9$s%sDI4D<>@Jh5EggMQ#_q%K1muS z7yd7lV2_v_KnC_8UjHHkGN@pj-oPR^+tt63fq18o=91G&wB-d|tD%K@iwA$c3V%^{ z$cDCmAOoU9#z6tu-?@JL*yR?A%Dh`vqbIAY36(+FiyrE6OX+U^U*G_yY1z31(Etem z5D>#RDw{%?sUNvcT|t9LD6W?Hzk>oq*{)RL-ZJ4sKGA!5fZc9Lw98UL))8Ab$t`$ zRF^<1=&2hMAx1PD;v!wZXAou6mTrFWW-BLhyl-7NIylo@>U%zXy?rd3hY58v`;AoQ z@f8vHmaO)LsoVbG&2(tm{bo2fMDVl*8Iz_=y`eb{=qMKw(U0`#UzC+x@XMLR--AVN zcnAjb-L9wW3+_t=F1wBXfEHwkY;o)M`VR4ZQ**AwgiBkySKRJv=SN?eEb6=|MCHcU zVj^@5Dfl1e{R0Q+ohm4JbaAH)_^x%^IscyZa{#mc9iHs8@z(}|(5ytFa&^u?!tFg~ z$T8QvI$fyC_<;(ls{ZvBIyMktO8qlul76lTuv&rGjH^1jvqIa6^yOn~T7%E?*Gj%4 zph7iK=J-_-l)n$C8b++#ZTXd_(j)L)UkJmP_aePt_rl%Y?#}woafhXo!_hT9BVvRG ztUud%-CP`C;7=5%RB_+g!7AmUXM>OQ?iIt=VMA`LryWYGyg_@oD%R0K$8V>%QsEkA zB^5H|s_$FU3|LtNws*T&p;Rqr>K^!0FlF~{)(p9Y%`u%DCpUlB(yo5XS(jCs%fC=` zfbxmBBbkG|pWNrQ^?L1%86o0||GrJAJ4b~O=uEam0%f(ye|-uK zc1eD_JJ>rL*f7s2MU_^|p+!tpTZXS-DEnx%+x=BS^RxrhA92Sv%y*O{rNFS@fGA2r&~x^tkQL~xgssOjXfNbA>YgPRqI3V>u}n%R=c zJAvDheW8lZR}D5{p&?fI!AOI0npqsz>vQ`1k2mnk^yncV{E5ZXYVX&p{JpZ8?q2p= z=n*N43B_pwiWou{4tN(<>zh6j5h;4{y7gBN$3Dvmna{g|Qxwa46&J{%H!7(-?a{et zL%tb8MRmUFAQi21DMn^GA_v8W4*}p!Zsg6)azkqgT=q~oycAdJt_|Ubz4T^doX!1J>ACkoVYCX$9{1rB-|jgaIk(NA|{Ag-tbGi~Oyi+3(Q~ z)JDRZnDKr}J2`0W9H%{b2atH!K&v`z7+;z~&nf*%G<(kQRm_0|h3R#9Y;#Hshl9ts zE$CWWw9sD_g8B@O5<}fq;FRTO^TbIM6NSCfqz^_pNw0$HwjCdeUFSsGyzKGG|<9V3qPI z)!_Jnnaa*g^qaItATEvogeLl&ax$d}diZM)yqopu#1?zN0i( z(YT&2r!7%A2(~H8O@jMr!5ZuH*63QU{lLBTo?roD*rA%GocA z7=ELWE4OXWmWtfqgy&OA&Px{Owam`T1I{@Xzv~eaqJ;R%(r=RaswW8+>S~0LTw(ZS z!PYp4M>V^L#@<9XEJGGb)TPBydq1Gl*IEx( zMXJ}yudt389b^v-+A)meWEgzn#}a8Osp6 zltPh&v08()n!^tsxOHnzfrP6D7ca~slgphNy%FIhyKljgEc>VKlQEr6CoXs9&!OGo zZx4=C+5BtXX^Ondp5TvETCalenWM!H4+)q@n!MQ2?&F0H4~}%%6MM6*o^`m6bUNl- z%Ut#n%OKka0Z#AwlW%m{jH#G)8IHUY`oK;n#-HmPH6gk4Ovtdjzin9P|YN%hn3hBIQ5x>BqABW^akB+h8*gW4nRCZ@>K ztdw`;Cujz8uK5@J$`o9Ehr4Ru z;VvUSN5l3>@55o|;+Lt|Gwr9`oo&3s7Eee{?EJf&U5^yAh&zUPprB|KP*Bv+;M%x} z5~C=oFHFn(#og_)ySr?@SGWFY|+N=8nPb(eXlPpgLz{Xol{rqloq{OZcF zASH2Uqd{$c?C0h1N`(|2vfCih=qHTJkb1Cu z_jWn!tPbe4?I__&@gwm_7&cp~BdaNP=+UDEo77k%%&1?BqltK9$98I5{0MlN!lkNE zM6wz9zYEa&Icb_jA|_AuK6Be3#;T!0ZYx9#9r0J134NW)q;T`k2=CBPgIoUAZGJ!W zfM<8BnoE$vAdY4kK3t#|0$^U~W&q|@+4>HS2?F34x{t*-GTNL4037qFcn8N+(wRy@ zDwG4e0?G3rQr@Alt`O`G=1Qi)>xZxBPr$HFz>fuj4nsSi4yGFI=oS55UD#uY&{JuuYIyD>iA`GFc{$wSj%D0NnLYT#Q#n!?AT+Cpa;Nv0G=ST%4eRLGgWCyo9+s3ZF zUCzYO%bCoJ9jx4`O1__BCKKE;{*=gmV%OS7Qdp@P#LxVjAGDX5+(_AzH!FflVdYs} zF|MZOHZ}8z%OTEvS;DY8JYo&!H`i>0zWF;)msDVPP+L)$XV~Fu#VPr_gu{tM2DL++ zN2NqTz&vGvwlTIGeA=7S)e+%W@^?6+)d}BGm+PLbO^zHv>gz=%WR7QGqT1%JT{7!DPc>W)4>eq`fmF#5Yf(9KVu`nTf#1Tt;bleH#;mnWCT{W+B=zfz|X^FlOr_MX%HI}|Z5V@;0b?uOP+~OrG zvv?}{MyRwVI_pl+7X8S9L(214y_v5U!YvO@AWaTa;ujrs#&{NdmqO)^U@+&npK9Ka zZ{|rk|@~t1$U3=`)TXt7|Zw=UO z-l)Nl*V`gn9-D-8{%}knWly>&eo)@`Mi6`}!Ij&G$d%RY7Li z^qwX*{y?2DA7O+zK4kcj;@v`;2DGHt2afi#u5C(Xc3sQk2TKQv^FUFGf zbDH&{@!qZ81*&>`fc;L_hoPITX1D3dq4U}8IfCcUGCYr;^=uVP9iQ_`5G<)6m1rM! zL=Ojt_`>FYb?|?!;_E(T@R3Hc>^25FWv7zgv}S^*eZsk&#lLkxd<0wa+R_A1z6VfJ z-a4tG*rCrMx=2f~j7y^8w(d0uQ^6yq5PZeFO)XTax$ec>dZ9L(tRv1i{KaXkX1yO? zY#!#1_xe#dYZMzn#C(YmGYwC58qv<3ct@Cb~^@fw*k$RHAEpD9r+ai znIazOiK!J3s$-lS`PKeYRZ@ivsNP+D=w1$@;Ge&L)kP5mtN-W^6$CL+#wn4~EI|YE z0yT#$l*Uhsg#B4g64n&7@#85mAeewQ3Y+@y(=w)rpV9h0gsc^6+r&e`#SD`|Fo$Y_ zUd0ZR3WAZ?hfaO?d-k>L<34e$WB`QNE^;nNUV7>LiQ#|QiCFlFlDj=g2&jD@#CC7? zAyfa^Y8V(lrd$x@R^U>?bbpOS(0`48oAh{o{Np|wAoXqpzFu#ddZ!E-3;*Y2==bmO zukJh6SqM?EKfhlbeLgVkuQ#JK;;_-b95nUu1c;JT$bhdl`!Q7WZ`Z1lKo9Q&)DMg> z&>xL)qafHPK|=xkG@vIblwryr0;V6L$^+ry4ySzn=fkkXVlK!YX&yWrFbY#Xs%uWB zKKR%<*_0{tawtd4;BoT{CNK#r8rhV<_yW?%O)BVK+X*xH&lx~cviBLW zK2vA`QY8E#V4Vi>D%zyY12eg5?TVKBJJkREkdx#H#Eka#@&A43?hkKllrFB4 z{2jj@slTD~k3jopAQi+?y~hd=Xn!N;(AfNj^WRbPH%J)>MusGGd6C^Z9|VAq`P;`I z_iz6dOR>OJ0Hc}UU*kWUg#Je;h13)&SI0xc{bwA#UmkeD4F4m_eT|%QiQWV6{Z(QA z@9V3H0&R+*LI1uylHa>FT0s72pK9jEPsc2csJw`r>$|5`In^D9s1}n-_Wla#Z`rE= z(KprQ*2!7s4~8HquM`5~Wcf^~6%3Iu;$+I7stE|mL!2x*H2D82qQxB5*1R97^H< z-Q2qlh^A;%D2aF7SIBF?XBy;1tVcr_GHFndPp>2&jFU6xzaQiz-``sFcEHOjRZ|o) zL1h?lq0s`Q5f4G^(toPT zB!%&?r`}z4#okJVz_LejK@7rz8g)%|o!idr?20xOfe4fVCswheBC&@qj*BOrq&ZU* zn<34B3DxW^1zs=z5OjB$!HWfnl5MFz+G7)jGNz>1mh zi3wLqO^b;Zey@~z$`!{2;10<0$)F5%Us9Xi7kW@d??}f=6{m1O zxgokY0#%$a+Rc@=^#jGJ;Q{X;#)wVOF^w zuKI2BZ1#xG|3%zeM^&{iZodf9-Q7qx(w%}d!Xl*`q&q}9B&4OgyFt23y1S&ik-QVP z`|NY}J?9s9ym!3+tTA144#r{(hs!5E&o?&2ihjhm2BC@MCu_+JOwr81Wfm9T!6)K8 zf-5Ua>*$Uo?28u(TVo+$;{uKkfiMU3hAqX^)xu{q)%ST_<~K@=C6et(4P7$*UOJwG zwwaw9TR947aor#B{75QEQDHbqZ&IDgWlMNGDNNSeqcbpBW`*EBS6kV?Du!%3IB-|N*p=eGIKhT4i}hi~d={}I`1O)_8ug*F8q-`3GzyFaw{h|)Th zyR(DxB1>ChH_&|r!5eXLTWg)Jm^R^dYj54^p}d8^mGS`rUdXrcaUzoY{(7RUhpWXy z4RJyTK!0`P9cpx#T8ZhcPr#hYktsVpbwi>1vcw^7hDd}qU~RbK_x5jXMtipOta;jz zpOg&*NmT1~H_1v8+-errV~h*;z&!THAi|SRsF+-;`Rq4t+Z=>EPF~cNz+c1#Bg9Y8 zC(H-5oyqz29dD64zugZPb=p>I_daE|Njd8xv!XJuGYm_8)h&-{y6Umr$w*bnfk>0-*~p$#*Mxd2PpCpt7Qwgk6uQ>a=G3PZjf#wHeEmO?+LRx9KVevl z_vH53i zgZ5``!%?y3G34`{+rX8TxBSj+L@RS`P{>!X;C=n!pt%Cp*4NT%?K*ohYpk+z+UrATo69(=xE#DU)p?n zaCLP*t;4gV?S6-4dM<3NT?;`MN~?#(S143c8dQ$xPxTiqNn@>8HzNFU;&*Nj`z+?iXn~b;Z!i z3(qs7Qsu;v7GX_4N|X8tmrN=nNYW~sYh@|vr+iC_(?Hzh=jFhC?M-3wob*r>L>k&l zD*Z}&B7gi!dOYd;U15NvN0Q-hNslE-?(d{0OZhqJxoY{9^qko|Cp}0S-DHsZhwcaC zJmWJ0^~Wmd@@^OyqYnuZ*Jvv7$mdew&ppl;0{^+k8K?4BkF!12_3-eC9KGKX{-1iB zI0k>~apG_O`yQul`R^WQt=HyN?*FpKsWeH~Px~KxoWftpc4FqrfAu(bMtz@Co&kL8 z0QiA))i?A&%7goFQXXFZO1cK9gW0c}cvUI}(?9k2A%4X?U;pcv#|MacUhyUVOU&Z} z#5{~uXkGsr^T_0@hJgRunCHdsm`A*jC6^#4YVExxnuCvm-{$eQGw1$75xiAUvx0{H zM#YY1303j9NU+u`B-%*$>U=(P#>y%x%jhWu7(<#@7}ypiUof;5JE^xQK3|3ZYn3zm z3X^s4?^RB_JSg+IcYjnlO}hW6azd_XQKb~sBjzTKijRug&eMHp^RNbvyyctXP3)N7 zV-8M)eKUO{&=MS!mMnL__~pnk@j@YQ@5No+jM=nk$`=3RZZR=WdxOjw80gvC&bK+( ze;irC2=_E1^Q>Q`-9)p=OEu2a-Sp%*y=mb>V$!#XOzv{K!PoHyKRylofmDQZgw?Y+ zU~;}k2y3Wkc01=p=BT-S=NFE zxBWp%Q2b3U`Me_1>K^OT>v(NLRyEAb{TlVKBm?$>DeWPm5}tR4x)QaUQ;QVF5)fS^ z{sI@>DdCqHOygR`$e#Vm@4cHcM0I?28;|!#XT%P+z&&@Qk49Dd57Qe4WT=YpShkPk zRwfa}rC{G98bOE|VP1nSImm{a--&i~zXN)z7KO9qb*VH-`W&ihPeZo`+8E16vD}QG!BcISB(lqXt zc5DKD+vQEn&)-P{eSW&~C;=9|^?b3p@urL62T*RfUnWMNcYJnCz1oVvVo6!q?lt@> z{@@N_D;y*}&0PucQ~pe1+k4Mu zqS%z6R&2|1X4uuO$pt#&Ft$v#NeDU$Q^ZLT2)Pksjp!ueFZBLS7L6@DMujk{Gx}MV zP*sDx>s2+g;0V5$Ay&6{-#{$Q4_>1@iz-!#$tK}p@#T`@fCCy`cClku6fbW63t?ZW zLS0QUN<`6Wps6YenDHNmfTrp^S|MP@gK0fCRcoG`s)m4T57`8`_K3PQv=2T= zq_Mvo&{Pd!tOlB@UC&Kb)`e0-P<_q&{$hqkz_9072X4=6Td1;HOcWc*p9RPr95&(D zFBBS$S#S4#)s$iQd^8)qq_h0dRc-KlY`?Er$NIZRvUNyz4Nc%h(|&!ejLQ!_!1cm- zoVm;$I71Gop>{SgKAZFNST`weeuE%R%YSaUbX zZQ3xAlGda) z$4MUi3OMrbt3(tH#>xRdKH4m-qoF_?gt#o*SS_)(1WR26eP=1KM3Ag`Y&fM$+r zcVPla?`Occ$061Ys!b(u6bEFY^KL8(GnE8pqFc--gkz?B9lJ>^AU*=@YJpJ2i4f}B z2-i;Oy^74I)g^zX=C6l<)qK(AC8PZCX5vcWf_O0CVKy6h9B{Q=RQG`&zQdm@8<>vK z+?%QQn&VLFtKNKc&7iXFEJtbJx{H|>^3+`I^RFlE$?t|1pij|?WuDsmQK0JRbeAt8 zTl)|fmj$T*UU2oi$>%3*^YELW*!GT4aKAk`XPau#2x0Cn{=>H(tp6YS)($G_|G~Ew z2fg{jx8{T(d9Yta%C|8Y~?z4s#H$7qNz3U_^bg@co-j)W}*ml(;t z9=~tczHqDn;=wG$qy4Z|J0#cW9S9J4v5YGn-p0d>1)NcDKt9 z7v+j)o?q`DrE@gf%|$dl{LDM1pZbd$@Nzv{PEwi?MS785pz%vSN^()8MbXKPE$iNH zZy$tbgygBIuMzR_HvsTI_ZryKn`?qN**H(lQs#Ssc&H~Xl~?3Kq_r1Jy@(NleCnsC zfg+b{@(%BvOE)qH;i20}mx=Y=Eq)5HV#g$TqTkr!xDZKh@@b%Q3BV=hzB2sNv}Sd+ z8Tl_fYd0#S|E*^|7DZG$eS|`+PC3!nbH+Cd1sK--Sl@Mt#i;A~2s=9pd>KmHRP)Di zI<3Lxjb1deTom-PQg4Dm9x$|Kn&&E@(+=kiHiU#!NikrK8HD)I%H>A?^eM`$P&1~b z>?9on?m!Q-MBMx=rI1pK=LSDRWa z)CNgI7l$|v(~d6?bLUPve;?2#XK@^b%XSJ3iH-v;M60>)5ZIC#NzQ;W zwrj=7*kw10vdd5JWpyFo6Q$)m5j-yT8b$D1KIQ{Dh;zvf_mnS37;nEQLv9-qt~1cO zTZq@Ps{zZ-2O5v7$os3PDEwWIzhp2B82YEHy}EPvWzi}eyOrkxBC{JShP&!`y5;Mb zf3JCgUB^KNYE;n*l7`lL5&1d7{9l?^VSV-$R6cs^?dh}!k`}&AS6RR!lt$zXAh5nX7V>4#(k2579*6XASxW!F|O^NJKxY7#)&>;~39d5jN zGf;||rANU+hj+%~VjpFDJ=9#rTxtHd!Z6|J*Glyz1O~Tfp zJkj|`d3|xdz=$#d1cb|qcAl#c0z+Xr32;kECIr}MZW}t+)%~#)fIs< zfET{~6E6%x4Ed4##?&;ze9ApE{7$Q0>si4L{QsW;bbXJyqXaH2kE0lH@COeeM}H zcyt^2mj#?XLwGavV8L`jWyf&9C>qoGsa-#>fkFWdL0ey0cZWho$BS^C`s!utQ(^KY ztz}APqg38v=C-E7ngw?&50vgWOXI#C=+%Y2E89;dQx#U|Er{Lk-V1UC3Cw@B#eM6b z&{WQRaO>v$)5}YHS^K^OD)H@DjDNdyEyLJ?H>N~vdMHrgvGY)Z%Dtg^gbRE}pf$Yv z<7yutYt;}RT|K7aQ!x{{-ix(UN-)T-Lo%P2*(F+F;ON78U*H^Hz~a<=Y4gLQ&oB>{ z&kJZ*S44BLMuC@%Ug;MJo>KUzr7zk5OV6By+aqG|A`CLI6CCXsZVZPkE!Csx-jOnk zlrz1SBPTO^pN%(DtAB#wx7VD-Y-kui26L zJ8A7Sy2Xk?{1HCKzI1Y~l;yO|*IXj5Xd*~R8z&Haa z5!>SoF!E_WS60ILvh;qr%zDI{>4sd4Tbfc;PGJ@(Q%pfVQJS#)0u7h`9cUOa-mT)? z7Y_FdKjq&G)YCsfLyqGv&OYh;>=fAs{6MfhgP*k&H|_1S(w0z>hg)n^`=hx2?Z&DR zZ$yfjX;jpN#3$=9bd93)wWK&u&e{z@5oPXlQ5RdY9mc9-;@L&T$4sTvu&Efrd7ir| zm&(>3-$?CmZLKa)AJ^eBQbTAER$!>N8%?%X;Y~7_1Wmq9oI$T3tm|}&g0?qOwL_e( zU>RgLF6t@vm4i$io1P!+3C-8qLoWDEI)%503v|3<*$T##-(16S2*b!^Hs_$;6U|cV z4OJ(5At`E&C>a^oXyJ6=?@ihy#FE6hVe;6{K?@3neM>e@)EB8%!pQF%T&&l_ke03K z#^ae5aMF9jQ>lV12~tleATFg>3S-&Mq(^;54XN7xh8p@G=~3k)dixWomD(r-Yo&|- zYZ1}!cM;JYXERwTCnL^|#krITHwk4y9`yj#sN~DvFvCg!GyM4*Gc@u1rA1R?$MT$%|GPYKZ z7WmQRgbYS;(+H(7O*l6^PIBwHBDU8Cox&>rfeCdp)5 z?Ym5$9U>w06C0Y*po?SSx74AFUMeY?8)besLidDEpx$sZ>kzyPCY5~bz1@quJ|huN zZ7S^)ON;^Ow-b3Ry_!4CS3yh3!=UZfks76Qq>O2t@Dx2&CA;7WMx2n_qwoqUop|>K zu}dp4sD3i6&L)dt=Pf%oKX{B0>J5oBMI=%avl^yTvUwFAXRo2UalT9O=eA*FD*}XHqK7lBPCjW3 z5gf4ntnx%8TamLXH-a{HrV7~$n924l4X6|@s|;~3auY?`X3QpCW2KO=ou86A=oaRG z$B{#hzYhpq4yWV5@PqgMfbd;5+IhYVe7^L5zO0M6%^<&He-_2AUf-yIUAcsSa|cIx z0N1of+#cIcrJhTg%hmd-fkY?anHi|9_M=v2c|%Mp!()jAokCwwNm-8&Do`!infso^ z-X=@;{ZSlPk{|QLn6!&N8 zJJ?Y82J_$_l;-e8(?3aH_J5YXrl^4Q^$F)?;|_VQrx|Vm^)zNnK>G4h{~>+pRR2Z# zzRYis5xeG~4wWMo_aIB?)r%l<6Ecp3b$NFI3vq#cL8iZu>cE)c|I>Augsh4hNeuKT zPZ~(a%aYvek+Fqn$9QIC#BM16qKMSZz~^Rg_Y@XSX5ITkPA}DP{(}xp3A}UR?I|p6 z%}0++v0UOboSj~(>?MsJAwhw$n+-WN)Pmela6c5FZ+ftsaZQ*m>(%!B0C%3`eXByEK8+0 z(0VW_t>uyTM}Oo&*RJhP9P-n-U`N$PFy4oL|E(*}b3oPP12js(U38h%D8yIgCH$B9 zOImo0PnpuLS=y`0pA~!>;g9( z$vlpMza7zGz{4oJllmzzI@uG!CuGI$W}p<922I+ zT9MBO)P!otcYyrgPXpwC9NV+}?*!yOyw!7QuNWX&8Ez(XWz3liR5A*C4a{LZ;Z?;D zm#YD9AQ_dte%xpDH@^y>4DhQIevPHdJa8TAMGrMC0fS6dp}Aj&=_$F57|0SZdrSeX zpR)qE07B0$0JRxV&dV49tbp2)XDa~X*$OBs0`4Fd$MN^$^HkKG>wjm+juUT9AvUnO zuBQKHAn)Ymdbg>^z@|zWc<0xhcQ4)re^p%zywq-Wb?Vr{P$8V}3}x$S1MKPdeWl6( zSD=i?Tn4bGTaNU%q8)r;RY7diU zX+9JWfVT?e0eI_<0}XHj1%S7vPXKr;xWmgY5(O|UpJ7uX8=BmKjSxG#lz~l`UAY^@ zV1*qla-;0drYIOyikFc){%;dlS?ic?yhp=6hz}-LC!Cc!5$3Xr0Up4ZgrcwWLm<$~ z^#(?tI%PDH-SEbB@x}z3UL43UIjB+cF*nfA4W6!7q)b~8e z(yYS2X%NKgfy)In>r2bcV>VTVqMJ~#DI|04?+gbzt+JV$MdQ>)0$ue9V(e z_M8xM*DgvZFr2i>qLzCFKdkb>jW#mzEzVs{Ou-Mf4WGB04Su@;D}!3B)^4o3{mHpp z1q^rr>&c&8cj%%6_@TCs2n&e@1E^ewhuBjbk*`!t9oX@-(Q!i17pixy3b`s6@Q-pg zKDtCyWL`5TYO{p{&9%-4gJQT4{2uR(kM+R&ccwhcuR#vPRRMaZYGa-<_(Y7+z>VO- zJ5vp0@nhSbhT-?m7c6Gq+^LZka>)tNyY&|8i9$L7b0C*LSV$JLS9-&ZIy}DTa;M)V z;mWW{X2r7Ug5sp3k2nibdmd(;ZaaCZh@YSAnG=4q{LBf<-vF1_t$%RBa!${juvfk| zdENg26y~k_S5Ww+;61A^@u6sF)E9|AJc5nZ|FYZmixh4&V$I@0BOn9f)YSH#%k;Nt zP)-Lgp14iwmbN_zSD|@l1Z_XENMv2iG($AKZF3npeSjB78mqa zCeaP$aT8;6rUNsqdI|Z#0u<)qIWby@@FlGP?MdC>;$YB)xQ7d@w}}?whU=?NlOj1` zebe9szOf&bnfnb{WEqsAF87Ihv1<)h-}DC!^33WZ(2PpbkBrj&V3%X3A}1*R&gyl1 zdTw_y=%;OyimgTOMI-K}+P)!Z<6z_-eLmKuhBfrQfNO=?RS>miK%YwZ_zO&tb{|Mr|!oT>&u1f1t+v+Z|zq|8V>p1V>G2B_mFkJ*j47| zm2O)?;xR`t7LQaP%OmK@w(rU})Yy3q-CtB}TqtpSw3R2aRNyo)_1!$6#C2d0{eTJv zH}k^W<|5GojS1fpJV;g)+M9F%m>KYwH&Wm4Z})>0S(j zPbO#-g8|P2W|d+wwOzeAT>_NLhn~F(l(%9LcSNnW4+xw@e`WUVEmh)1)(oas?epO! zCfK#lLrUbu-_~yL_kqtuz@P;Cd2Pyi`?@JGzhdfg*o`I9f6TpOwy3H)bO3FWlapf! z=YY%}S%l5nT^&Gx5At}~Iz4}GrK7N5z(*~d(p|(boX~paI{Cjax-RGfb6i&yTuvPE zwtxt~nfkIIL}%xbLBZ`QMTRW_8>Wka;_z%%=pv7@(uc0t9|XDwZ&QN)noD|x1A&P2 z@bEv3G`8|#STRSA#&_yaN`&crHx zkWsK;*ae?7uTI9WJ~<%q)fZFv8Q10D@)MK?hd=W8KBhGgu$p!12J3k-s*}Xz$I14U z%;1k8{*?Ty(M-q~^Kd8ipI*guJOHLqGm|d=MkMk17~*8L{;`eJ?QGpNvrc!YPuK$? z4meFfqGgDDjeYcm_#}SebFaKQ|XSKHSq1G z{6sBYycF)|5@<`pK^TMZYxU$7NQ`x3{EDF!pBkh+(zZCt)Oa#Y^jnr zM5WKN;YHg%vZB@|seQ}y4pO9>Kr=0N$*2$sBN}Y;&yCx?wGk%1WO z@KOGQ^1m05#x^t_(XYTUn93pnufi=Mn_JE;NZ?G|NQ51c3BRoCACTn5-yunpe?XFQ z3IHT&strJrcYi^W0{|qs06r*xU3l`3tljT9Yj1bH-dP;KLAZFI|0f~Y{0AW^3J{Ww zJeeayCI@ZI?ObwTjZI5q!a z|Hy^vo|b)z>(kJI!3J0`QV2Fy?+ewL@)-Re+0`Oc)$Vy~U#c0E!YnjdT;N15mlbhX zD=l_>?pBxR6*78$uOPAiv4YeE|KF@2HMwWfPU0ogAZWuH#>LJ2&t{O|9~)olyTK0O z!trjt9QQqz@V1sXYAr;~4DBqaN*W^~+*Vj3;-!tTv;(>H>urY4RNdid*b=<%f}({K zdY@96U6#53u2Q)Je4!zFK|O!Na$t$;r2_F_-K%zFq#kX3wjfK+G@x!ILW983=*EYp z?=Mx*0ZyCQlLJN^M20S0uPw2jKm|v11;J!_`8HS6TN~)ghP6r%(+3@+UY1omnD3r2mzm3+Fiv z{keoRMf!@6LW)qSq&?*{e@2Tt8c){Ip-dOuuZ?0}6R-W)J1lBSX7J}WO7|tbiZN)= zpR_4M4l!OGR=+?s13ljJ1Y$CO&@7jRQj|{IJEZA*7`{Zsq_SplhQXl$wo^8_=H(~D z*w}>IwE?mz9D6Gtj9(eLq0HYh^c@K|pLrCuzTCAKu6!}Z(af{_hEGxf9USV2{z7p| ztfm#yaIh)e{Gs{+1*XT;h&3^v8|*#%XZ>bBhKT8dNa=8RattOxur<-@nI!S!ctWJ- z04aI*o0J^>MM~!G`0u=AeA~~}{egMPfxw3-WJ22vV#|Hvfqt!Ec?_c#6eu+*!&TXH zeJU$<{{yfio}TT9BOed=6c_90CV#o6q>L32s^y1-XZM0}DBQuvKH+i!#zRKkO{7Lq42^hbar6TmS~^1SGXn#4`-W_&k#2`8<-ud-yz(G!Mkk zGdO@Szd~09?w?O9KFe)3#O;LaRO+8ZTX<7%ryj{(4Ri+4ds29U`?8l z(IV!qWCsh8bj6*kD4)+6+2ba|9op{~Ei(FzH*YvUfY__-_$oQ*-n|h~oR7X%lX@Hc z`SP%H>Gn1;6(Jn@VvNB{uolc^i~MAQF6x{Q_K#_#(ZSfAah}tK)`5ZSM)+KaE1x{1 zBKPvY^5(Q*>B7ik@Ln1GOi8)M;;_m8!8j7r|FLl-b?22;^zST~tc}C+L-;?mek=`` z_oae^L>+92PU9!S++KgphN~4f9A_2-#O3E|KwM610pgM&<5^s`J&Q|)XCh4KnFwnu zl1-Qd0~Lv^bf7Cj$dtf&!t013g4NJgi&{H8T|WS|FB3%->aO4xhbN*bun&FOov(;F z8I@(K(V~+pu_C|2r98*X?*V~H7xWx68`c6c(#8T1n3_({^jFvODpK<_fQ03$52pS= zOqpG9W!b7#8O&T6j$o<#s)4msLDksW^l(-lmp~Vt6zuOb%sC|4}-mgv=HZasXH1? z%rSF6v6|1=ZRa1REPy_CA`Br)mM+f*q>r zuvQkhIAuNRcfq{|#FiVYHYGnUQ*>%*yp(*;U^dl;(KvAX@V6FoXVuOHZchNWcay^6 z4MMebAqSAQsZ<3q;*MD9JS1{00_i-q?CFn<`~|JtO44&BI&GoEq32I{l*T;q5IKW0 zXV$d|7>fckXIQyrZUzh6TeQm>#cTFH9loIsJ;zc}jVy=aF_YhVEdk0%op_01KIUDB zgt{!9O>7Rbz%$)ZmmM*8V81Ayj!+Ma6p=`nQY+JPN^=60U0oa&1A(#IdOMIH^BZ3R znlLTJD|cR18a4UA+{OOmlR|T|75n{hk=GFyaJFXI)6WC73KjYayvMse&2V%=B2K0o z`J#;dH#?AGtG)im-huT99DOhIk9-luc27E)*lxxIx?pf4BzEqINlG& ziJ_6o+4M)|b97wCmRUThxvSSn#E`6P=u_TXCH!8HLXY}S$kTsZkosRCPjb3`=i`Ec zCh>MEeBA?Z*Q>kzvMy~Su+TSxz_jt-7o=`=N&YSJR6Ft?kf)vY?;GykG_&6a4m($A zs-0ks&U91uJFU+%(;`;#x(ftvgp=JaggaKVM|W<-6^Un+kqR}=dLu@|BD^q)8eF|Y zwY)yf>47DY&3+n!nzdEVY+Et}y3U*jdHe_4Bp1--cO3(ay=hR5q8^3`GUT7%U=Paz zvrURoD6eOsQRi9Sw>ch=IF@&__rR&}7{N$;L5$L#ze)}QpG29c3iW2uX_JrBDS033 zp{LwGlH2iPP%74g1&xwZf(`s3Su6rX9?YLqege#co59zat#vyn4|c4xQ_RMz@vqeq z&X{YQh6{h;$2!J-IfrhWg=#3@RU+|)AF~cm_^Nu^CNWqujKp2LtY~V_!ldSQUcIEH zHh*d+4!EoY>99%E@FcMmZa0N#EDhnz0Q6LSs(sSrxmC2r`(c2lvhOJd<*^xGx-B(n zm+d35e#w>=f?6-0enF|3i7Sc1g5rQtM)E>wY9Du<&^KsnuP;sr;pH4!khdS_^QU0& z#*uUpT{Qpo*6Zz=ee;^z#FzRc|9Ff<6P)GCi9|^Yr7upwQ2CwWU$!l3dPy3zwB|U^ht%o0JVLt8#x!b}QAy=}E|F@g2XgX%Kvtgr8)E;1t;aosm%2 zOrN%c>GUlnp}Pr~_u)%LdseN`{j?V`4%vUG*3G#(G>FMyod*{S0q9ahu60#WD~OUl z_o7$*BmEnm4a@_z%n~n_qJAy@@>R_^Z*?PIqcPo=Sd}s!O(X9woLdY?ynbE3 zFG!fs=_UAJ9=b6YIK})R?)M$ZNlNwZ^6akt3<_Bn`c9X15xU3alZVFZPb&!EuTy{T z{6atP{8GAc9V;8$-i3aR{aihVZ{N^e(0FY`*rg-Y<0t6ap0$~PKl8@j`LrF|6$Uly zmv%MopX7KM=S(K{k(0RLorW_(CGVR@sK`A{P$)F{ZkCDE={sM2^EYR=1%D3wj4V?F z9rA!kp1jvh@P-)=mh+lAZ`(I03t-ztfA_~%VP`DY|4|@k5@6qJPBewrsO)rE?GN!K z%Lyt_C~`R(?%opH^T(%Ko@oDF8^`62?;fW?KxInf*6lejh7=9jX09l#M{{3ko_&du z6I8PhiE_-0)CDDOH(WICx4j36_GIyovN$}#Kb6HPIi)&Ys0&-Ox~*OYQV+*)-u{ed z+Im>2LvDpneOWLdKFEL)^jS>A`mw&n?dBXFdHbD=YT{k}8=kj6)+b2RzuhkigEm$d zG|~A@4+Qnfj_fp2`m%3ytKpy%oMQ`Q^5eKSk}0Td7c|xwPu?&14LVWH{v-+cp2y5} zV4F%-^Zjma^H3*#W?wKrz6$e_y6GJeI0@g1679=-6MN!%0WY&Y!^q92{;%l+q8S;m z60jopTquI>Z6PuAzV_!N^MNZum%7mNGb|SkyD)bnaHrVGF7(o#f$v80_mK+MM`rDR ztcTetHDmqs^ww(Gg6nuv(^)#Rg39iTo^kvt89g523@XTswd~CT1;dj~(slFx>?(|z z{j}4P;X*~M?zkr(3lja=0GG5}s%}Bsgy+(ig>r)PYY~V{vJitlWK4?~K5H9jmgo3a zG_0(j2USCyb9mEy6Ch`8`x5X+RR7+pB7qd2_>*0SJz&5}SbNW}6dCNQU(ehsBM#Hk>^^SI82%hV# z+05fbL7X%^xCKVg@M6ngeONWnBLY8BGaL=dGO)Fxu;Ggn&xdrdoD}ILqf>_B;c!uv zLIE;*BI)SXR{~fyD!nkS^sk!=Y7*gZc8^digJw*W3)Q)3j^``p(qXx{pQHOuDMYFy z-{@b_JrY&fb9DdJf#ymIi0(JO|2?|z)oS;|A+NxZluMnEGt3}&z?D~<{%lzixo#Yl zo_l1IYBNi_&aU@_Wxi^VW`(vQ4KWhev7%3s&g-&bbJ0lDlDAo=Pym1Pc))P_uprUO z`{}y3o$kfZ_!-alJX_qz+c4V%-DAF33<*Q$_-o)Ff(7_T7)3dO@3H6@wYL_Yj_qEj zmE6r}t{pt5@tZYlj^Q#C=vgq?&?{l!E1`>CD&%RnffHb_v-rO?h_U0Z=PN`_D6PGM zUS-oBIWjQqwjD*m&&L3BbR4kr_IkMue^J@H1o5hs``qK zmZIHH8Xq}sN#6YeqAek+nD0aFbz`Uz#`Wo)jC<6DeQ=D2NU2llR@|c5xnoVKL5KB( zwb6VbB#qxJ+rq=tOIbA8jR_ruQ*b|&G+UB<9>{t8GC=U}#j=}91vJNiEC<7OnkdFh zm2!eQ19rek01vy??W>?1ZGSnft9E3CEXKTqT$d|x8djBmyBsX_dpUTx=c_$zBh0aW zboxdM8NHWaYoq##_IsO+NB?oOqzv&DbAG*8tPtmTKrX)<^#SB^pHo)3EJ&2}OHRz4 zPH~OM?MGXK91jm6m$Dbczo&&x(JEw##D69FQGW~vH$4vrSAK&scYXgy+q{=2=Xp3d zq@`E{Nc2rXi%I0Ui3NWo`u=|<`mL(0l|3Vfy8`pQBOi~nsg~W>08RWb=UX=seEn$) zNc7#?^2k$Gn@C+5D~dr5E-_9NMnpg|of&2CKJ}c$4XQeFntdyGprF^wu2K@P|N7J`y^j9mLGM%m!CC>qM190$JblBKm zv9w(X5KB8@E*oe{k=7(5TxDruPV4lgGYDq6MCS?L!b5 z_aTvt68;Z9U5l3jCkEKk;>M0MZ%-UeDDM&~8Kh=W3%bb%Ps@5KS~*plEw<=G;^~6f z86{Ff>s{b`-!AurqSIG`v+)`_E(ldU)eB;LZY!1a-t7*)eT7=nQmi~(VuWyEloO@{qHW}V zGrzvUx%iOQN~7iCW@$RhE8XkgkU%g*5xWtFx*=bEzWF)N22*KX?1-=+ei+myxV(Yf z5Dn*JX4yewIZ!t~-H4!yNR3*s@NG8E-%sA-;(d_{(TC-l`Mkkw^Az@YfdXwl3&wr3 zTZd__W%<2E5PA$X%L7Z%B4!v0kcvt&7uso?a@mb8ckI{f%flxIccDk_K@aAL_5`?5|@+$I7o( zrFS5~+dCbDizZ73t))kqDC&vSd{0nDU_s*C%E$v-s+7JiKjtPbG<;802#xdeVe~iLP ziH#5)eEjuoIZC>->W_Cbn|B8_e2bQChL(IuE5TOkKb##Q!|t}T3z5#&py*1_%_ETY zk8j5qK2&JfBgW&Iy6w6qXw09y*<`(!Fa7n(IM1v{@|*454api5@i9 zkqvMq^SC=%N9|qBDrOi)RCP!r(VH}-F7=LvR9IHHS*9z2O-}V+j^+svdT1`# zNn#hncxxtTa5=B6py_WxKey3<1$}*Nh&2=R5Tl>G$fBfQb7BxRfNwzw6I=YOuL|E# z8mr4*k5^V{02}%UazTgrlIh<>K+G3XlNSAxwR@=27@JBOFR6dDAWT;3NYWwYSX4p((h!t(DRh3F6IxKlSt!?c35>A-4QBdPlFCUF z?#!HnOy+Z8B!W!#CcPBdB`3boq1(Z{+C*tQ^V6=s~oKsFZ?Jn9U^bV#TvwTFm8S120@c!H%L%A10&1=1UbVrh<~0 zxo9Nh-x9=1h^f78+7nX` zFUAY6jePA${mvII~k1&c73tM^K%EQLznZkHE%mV3$#la@mn`jdCak>h$R(^#|I{Wj^h znc$~yqEtL30hKf8F;isC&cM8aV9E-ICPQ`N| zJ?omkOt+I#zl@#sM*lYeN5sI4!A@Ca z3^=;?A{Mcz+r~=j{4Vj$3hr;J%ukM-GxEo5vQDhzGjM&K*J6r^4#@A7`LU7*TW`z> zu=T;M0-BAbhnq3)t3vTjve^_Cxh&?D95=o*K58%CWUqPOwls4cU-p(ZaczUImnPo? z*?-=JK2NI($V0zkph^kD92Td%Fg8;|6rWLaSE4J(*x_-pUT#V1kF5w?TKh3TaC&TH zm%@2ar~Jlz0!h?1dzzSR2w|#+Fn|5^%7sTq!$V%xQG4S}C0)H<{(4w0_E^B4Pl?_W z%Mm=!aeo^j9nxaYr2+v<*nS3=F5Zl&ZPV+L-aj4aUBH39_{*SvcA(7wgSr|psONth z)Gc!^yZv}+kuBot8&%p(`vRLq!0^>z_A^EVng?9rCG?K~SM+c^qI_pg;}iWJ(R%op zxRA53B;cQqmLjC94S5CX{Ks$tLXM!l!ETfH3mT?gaDetMPaD6Sj^gPH=*_%!kX~wLcNJrE)SzrGUr3G zXWHw$$5l5)vN33uPyE7J2k=ouTq@G9t8XH<+rlda`}9h+Nk|q54}OBJbrzg}bZfvt z7;y&RTm4KB_Smk1&Q2K4Qfi?ti61o+8`OBnR!_axzYdOaqDy{Jv|>$D*Rf+*wFgCA z!=^hNtbq-wK@QozyI3$)cKy*sCQvOyMFWnodp@0}WU{?;@#6=D;QdG3u#s@eDd+i7 zpZqQs;wZzok54bO>C%N8P-i0=VX|dJ(e&kmYQkuxsGZJImga|nxPz}l?#+({mImZXlsH_nkOEi2_Zx!mGz2DmVq~?1 zaBrxx)San|6E{x}495rPM*_PZQ+vH)B@xDqal1B;L`P27btgqIrv31q1QS`Jz>9Jwh?5nJDxOo``CSK@^hv4JKi0|ON=F|aMtq_?*wJfNT_5c)A8 zRW3xd*bvIn6@*RK1$s}PNtN>|Fv47$D-X){=&GyKU6`1%N-S2ox+E)FO>iY~r%xt{ zY&b){fhh6d(g6a~j(^;U;{%Z zeR>KxC=bBlbN#ln`l;b1KlMqFvcQ)VwYB`PCe(L+LeQ2uy{yex^KI+f*2a>%fx}*$ zmUOhy&VQ4^CI-Kr{gCq*quXfr6mx(E_SVb~9IQG>XUWun&$SkkYiC>Z=;ct6CqLwO zDeun&xZSe;m!IC~Weji~hLE&;Y*P|w^k!0oH27%mXj|u^wwU_PoNt7*Gcrcb``LTu zNfiUO5cJMIOL7h_^hh7h45sl;;PQQ9(DFTXA`Yd3pcqMaed*&Jc!j3joOw@CS8t~XBn zHMfBh{0A$O71aJEkL_X8LEAoU0#^aMsiv#YOxlYjdx^Zb#azle*D#QjQW-d16-XID$b4@Q#$aE>Ce$3Q zbn%_maO`c5y3uL?y_q$u!|Fz$MzT}bRUyyueQ@K`W0JC?s}C}xR*IfsDNA!S`d1l; zm9Op7_8$gk6D>q@&q%p(BUkAzs<)qa4?Y9C2gA%2aU#h0aLVnZ!AvDe$6#C$BOUFp zZMXF#7))+N+fPSEtXtme@OwOUunL4ok^3DIdr< zT>G)u*z~Y*=5`vEXyPL#7ej^s_9gNW(--V0ddsEFO{~g z_p8ZALfrtLDTLio0@I>4YZ6zRN>4y>DFk*ljX`NMM>i;{cJ@T=<0zvQ!^sb|qpeB^C?I1^OW zNoQDu4@Rb>kgImM;>1Lws(gFC1`9hE7W^60Huhl;uXcQN#)!~`C52UTTU6cp{VsS% z%q3e7)5{P>BS`4icguPaxioO~sm?82-@`{bf$5%K1%6&r=g?^4V; zO$}T14xz#h5KNZRBcqiI$i;{{q&oY1CsE0e*NWE+?e<_IQ@B5bgcTf?8-G7on+g-x zM{<$Z-;g*XCFrOA*><`Bvc?YXWc)^~?a79qvK4YiS)W)!@N>FKC9H%Xc(E$>y=wkq z49E)1Ldwjxq8?Z-*3wZ8B*mUZZAErj23y66Xp>#;-I~cD2bHTT9<)%%bMXb+Y>=@? z^?6sk{7A9IPHwfYZ~=TwGV7Xg@Y8oC&L7RPCO!8V!VJm!j;eP~3c36A+U8EQRwX#L z81xxtq9$xD(y!yk*@8PI!4)oav#r$lNq$=1v&bdrSQq*RcJ~?$0n+#A;B_+8#wX$b#4uNncnWoEDkS zh=^p9h4;n7r^MHN8GWB)e}3_N`8|+Ysr3w)a}bo3RZ^)Hw;6W`qO=_ktp;8!OMw@e zeBgz^Ms_`$dor<0%Ki&`aP-!RIU3cJ_Z0Pb-4`)s=yzF?D6$U| zPvPn>9ZfWQ7iUsFpxf&>*UEnPT$*!IyawKsco5zjCSzYeyBh2i*(wfA6M@T;U;Uhp ztz{;gfvxr7G6S3XR1&;5^TzmhkJA0Yz_vXYI$}R073v9=-t(wqV(L&eeR23;b zsS*9j+k?yEX_jj@P|{a-{vzPf)G5Y2?Gba_%fQJhB#ALS48DEL6yS*vIp7KLJm87U z3E+t_Gu%sPMTeKK!$hL@I>5klVCWW3J5o-zalIArUK_~ z;;UT+dVBsg3YDl=zA2EOhQn=#9Bj02CtwQ_5X8q=HVuVib43-MWU8H|-xQ{^Wy(Vct#}h>#t}_PqmaMbiGW+ zTh9}BPnk)X1Ui`44qDIkh6DQ(saif;ENH$6Dw^r_>OxtUcV!s8k#|#ZWjkqi`_Rk0 zx{V_O+HYyAyjWQOw06zSaRUvEq&P~#FLk!YIemHf5@QnKMG}DQl{pbIpqp8Dr`~*5 zyn4@7WT}(S6)}*zHMPG1mo3Gsti{1xXFJ06;H6cTs6w7hGy;QXr1Rw> zJ?2t@B16=+f+OAUFhf3GVI|+#$HTI|O%kC%6T75AN;~LU4!R?yh-@JWuy@ z&rHw%{c={VyK3Dee4x(W`*-cr@Ikh_aYH0PcQgMvC~vHJbX&DH@Ii{oeQ>l$4v~k%a~7e)9aW| zCy_5x%+%KdJ`diHhFv>9yld9$CI3931(CyWtL0gv$2SdNy9$&uW0Zr`@~Dz6kk&Vg z9J@Lxn5`Tt-6|+CU5=G9+OQ)!Mh9>^mX~A#!{5JBBPmleC;7>9ITEYw*y?p-!}n6w!u9+L?cL&Y zsjV!e508`m7cS$qrRN5}=n;E2m>zfB99+orB!R=}khzaY3{G0WK|fds8sFnS>$&(@)(1EA~H_L&e*?&Z%60+O6zZQXx9|8^+AVep{fbB@;p`uTZ;Cl z>R9fX#>ISAfIO@slElfnH99gUgQ-ILp&93SG;RKQ&x2jA~uQ^?HG>1 zLH^voVQ~m%7F}LRtjI!hLEnsEIeS)!J`u9AZYU+-dFVa9X3P7Mh0!H+BC~%-VOzT1 zRe9G2al^OVj#8#E)#-Y5fY7N+tu;w3~A?AUk0b3@fAOY z8s_ti!ObN47HUzrZL*d4p|JqzyL{@_Q*EAWZIb%msyALmNVm8@p}nuoMc8nNNho71 zsCwd+ekrNT-5TkOspBi~GR9JZI9W&;c}dz=^p|3RW0VlNMwgO+M2@-XVJo{61*Qz2 z3yuAdbDW!2g7uqKMHE}n*vO&v_X{c!BvM{2xU6em$P0+4M%Tr`!BdXIh%2mZ?rP5K zzf?jxF^X;6)s)*4I7^6{{n79f^@~SN&Jg{fCK?QEx(m-xMO-5uA+e)2eOBR&2kID1J^nTcRIz{SRr@cwp5wW!sZk$WX)+ufVGGk^_%_I#>txWze6bgNVF^J2+Th45LMD1w1J`@8DSnYxymPn;4|Bs8Et? zY6BVLxh+SK>1Ft9H-tvJ%XlQma-u{KQsDP4f-VmgAlbRdR<0eWhSw*~K4UbgVQP2r zPik&5Cni?z;^5ZDcC~>B4tx!OAB}Q|I}$U<`W`AIm8^Ko&t(~W?tN)kc<&`}Uj@2u z34>+?FpjS%HPRnM%VOn6KaNOgda_AuyD8OzqU8Z`qP>m^eX!45#}C8QLpYg@PPM7J!7uME)|ZFA_tbiTO9_<;hqY2u5+tD1i|GlG0So^c1 zF_?=vfd7{r4KWL_qp2-Fzr-7go-t&08@PncdG}{S^C@}egrUdhbKah1qr{QOgOa8Sbp=`Bz`*A1#%FtP8AQmT%sooX!z)OTAvf^P@zaqNTO2O5rvd>E|D0v#lE7^GCGHb)?7(Z|1Zg3%MWW zv{k2^4k}yPGmafA2Q~BhFNW|G#T_dzVP(2jOPp#eKR8qICm-9-TKbIQsw< zUCWbktb9A5^eyLdNSv}nTodQ;t0~Xhi6=bu))F^VC0jM>y7|c}%0zif^R3QkGc~dO z+^i5us>e!p$^leKz$?X9L6{(Me@t;SN5qTYGH{cBBp1=L$m~`IS32Um?=)~zU6uVX z2OJSLSnz@XViKy5g7PqfqA+84z>o2IjbFi%tzmNghi%vM#9NfelagK+^Zlvz0fIk~ znL1Ouj%T#QoIJR1R!qU&swHz#x6ESIEJYDe0gGR!6wk$fd&LWG5w%-z1yRt2p63;^ zAZlW<-cC?F?Iq~s)%fx)@@F68#f91}Tu)HYaNi(KJ?eeK$d|1AUw=2RyPh2i)`?dVMhd?WGuXP|Kx4b1i+a zXEXZkkkTw$8_P+FVUu*@o@aik9~9w~JD!?P6HKUV8hF8P(n<-GWN^iKVpjViN1`if zL#@d+ewGxTKwMWUUDn*P;8r=heVeV4 zE+%GhYg!j^|Hkj}8YCQINjH{rz*&Lmtdd(ezD7c(%0x{61mdtfaDlmYh=GW2&`|DO z&8fSd#k;y#{ZFl=bRIMW5Jfk_I=?FH3x(yo;biO@i`t4ZoChS8iXG;t2*yREc@lh> zRgUtD5pyy}PDZUU?_Se%8Oo1e20hY#9VU7qL-DNWtTB9^uyuz(f8Zz}p11*zGnmDF zSY)ZU{meobkGX>mlqunRR3R_Dg{IOCMA-cH;>n@f1XN=3$? zD}sVVf$*c3X^fougf5beQf!E{FrIn8>IRxsUrj~7<^7y;DFn$7w-KD2nVpfngFymT zy>!?U#zTWAMTQiM#71en+rrtYR*L7yMp^22KK1igPDFiJlvD|=_f28FSNbkkfq1>3 zD$l$SQToGeY=bBaBPCtNimXw0K*-DwCYZze!7xzLghRxepU3AUy1P(Kf<_MxlFSDu( zl9*yB9anGRfjGOi1h%?!iZOO#-0((x8h6(4Zf}L3jBL0$kR&q0h**Um`xRIxF{H@* z$!=k zu|`QPHJKaAJ-|`iTN0x{f)`T!HLsM-*qd zF`G6xWArKOnFFP@dbY~Ju_uph!)}+>-!}*i59`1u(<&5FM{~$l@^Gf)_Y|A3VVvsj zFJB&?4T$VCzAu>#1-LeX@z{L)D!XGqb=G9+lF>|2z=$AYD*>7wBOU}HH9*rFRBSm3 z2d2V=Lnbhm9*>e^@&W?!lkK8GRd#N)O|zwNgUdib@j45QklIteY-3+8zm=j8C$^EJ z5JYu7zZaD9Ooa+W`Md&+(A-VUJXZN5ErdF^ZZf>`cvkYJIa>#vwq5!MOg=BNZ_@Cp zqgO)Lv6r1_Avyd0R00ZDz5N2DADN6jnGlmw#Ua_y=9+VJ$v(4;huZ50*qwv<;v7Mj z#pZ|Wk>q=)-=otn6^HBPF#P!2JPd{bs>br#1U~-H1$VCivGTN&Ld}Z4wrlI=>P}OHP?eNa}dq`p73O z`Xfy2JGA&HB}MW4SonluqOlSMYtn`;<)jab>WPUv2~3Bv-hx+wb-Ct2yQ~R; z`1~q`MBt&=2r9;!;)?axg*9wa?vF9esZy-vg@5J5eky@mute*a3Hdr(Y!_DPvzHrP&gJs~-s%={h-O){(yo zN*~_Llkhk5uTD8{L#NoC6t3Ka@i^YefusZF7vOm4M4wlkP9j~gFHz~lGc z+nkU11qF`%M{sRV>7KdGMGZiM%>Sg_c%&cp^Je-EV~+ft_Nr6kJ1tx@u!tMgrE0A8 znKIRB2aFq<>d>%7ra5cO(3;Mn%L8{XdEQIMbD$a8QVgW3(Y(+1;o%x=hqS!bX8P)0H*+qc+Lf{CvFX1g}S4;gXQG`5RAZT>4 zvR&Uw$`og4DBX!eXvotAG+9Hc@iz4i9w>> zs6{ztmi6j-bV-MpQ71!_a9#PNz{}y%A*-r03-#lZnk`Tb(?FAOlIV?`%HQ|0;7J7` zd!=>gVfVGIP$G+cCi{UeF8W*80DhvM23d@8S0ZO$H}`igV~8iHa$eLn{7|6! z{Nm@%;_;xoryOT8gTV0fDluH^+nAKxwO6kuugSXZ!L2)*DK;6#IH`OHkkDRi8PsEZ zEgTQ_g(sDg8`|o-;~zikdSutn{3|U2M=-7U(FXSy^|wZzbePk{7A~G{uCK+g=H%y~ zQ5E8>SbOg=`wCw(O{qu2`D{YqK{s)7Z<{yAn9q(hF*Rx=y) zHfC0g_uVo6NTufc>aDvrec($*o@PPczRTKdA6tI~PYN9bA(+JjBel01A(+VnW8rBh zvIx}WHim8e_0V8lehvh6()kehamaO64P9{9#3f&u_pJ8MgKDM2t6ItW#Ie`< z-CD)6*~-I0A*S3{i}3*sg!X3ivk#5mK9jkp{+#`=WH2S-jZsAAP8kZr(BP@1)o|+I zRtBsd!x35yx0ilpCXI&}IuXu|ho+&18yT$}a1)$)nctiVtRti*>P=UzDoSoQ^2#Fgnd3K?})EDl_b+uq`|9PD0mL+e-8Un zns?7x>aJT9v6*j5<4{J+dr0rT^!|WJCGkT;%E?#iv*q+v{JNOtUQ_%U9McG%A{TBn zHOmtx$}Go*>D*?6>x2EUcVV3$%n@=X;8m0t-}PTovvGPnQhUkeGc=W{TV8F9KX?zk z%zNFRsoiaGb$QLMr>u&N^gkS|J7%!Md240g2eg9bRsvf7_-Gm zD-_%0e^4HKqduV0%fairGW=%i6yv4*&AN#a*Mji+az81b0t%pHXHnkMy>4+;=N}_F zq?Ths54%8N5kT5ex1+09`k@C0e`rpPvSKyjf3V98?8fijfrIj7e_&lW&4V}gihBOs zmroVtM=;3u)Bh}(^7+^p+>AZ$TLCejxRLBH8T|wT5QiNsW)P_vUoY;=LJ&f?^&lC( zY&0mfNpI$M=^DK6(w3x_?+Cuk zs%j$W=QU0_fKXV{&_U%iPhC=RDri}Ff=s7fOMOLzp>zE3TSe3Iw^?YGM^M(*fSzdp za-h~vO{mWI===Q{zLNZe5PXw0lXHHmi*rGd91>c258~MlM>TY0(Cs!u#Hg;atE#Jzi1TP9bR!al(2~G)R-tw?d*n$hbaOGvv!GnYP8qj4hn|6_J91 zEHaT34@iqkIR)RwcGUJTa}Wv{hms4RK$CGuPmVDQd=s#qcxh+*EmVY)@$|yc_!Y-? zkptANb~4nBl9SXC(?)(-i>3a1PjAF`ie)FL_3xQV8;|LFtAn7^!%E5`$YLrp$P&P;|1h}kPt*Ll59^6&J7xEM=cVzPB#O_;2%hI(8R+F zioT3Aw9JH{&eKM*F3Zz#Bnj+77d=I^VwTD?m*w>ZN}w-6$19>wj+4Xw=-b2B%K}@ zfrAReoa)+b+Gl(^o?BQld)_VdvSHcY^|&z$5Yypx+d_U=lJk4l!l=6({FR;ArL*Qb zOUGYa7b|6?Q;c{P04G+5MR648gYYgJws_NHYHD@1MIv12fBj&aTxUhvy5yTFzersI zN&V4jL}UwP!-cRIX@tN5$&(NJMQ+AqWQo^o<|qATOpo&hO8ge(M=1E_i~D(K(aDG# zgO6LvOKT!c-5$&p(KMp^yOk_Cr$|zD#_P*kmFMyXE@na>dPFVVs`fLsbi60r*p3w8DVtqD;hy48Xs7x!HP4Smg?xiqU4>BaDK=Pa% z`{8a!ILheY(KOUJs6(#$Xl!fV`9^s2dWMclxZa=YP-{Pi3?ibK^-O^Z8f{&MPMyov zL^X~XSpRc-X>26BZ20v$olcWs@VGVMvFaRnVS_xH)J+Hsb)lO@P}7orY`pY$N#6JG zNfGG@^+G521{Kx4lDU0*c;tdna17so2}6r|m|g?9V99^r5@Ss~=-cZUYGvURI~MqZ z3k4aNE;$;^oI|xbwQNC~GGgBWJM=q3XC-kv3-0uoM^uhA1S81@L z^TDs|2y%WD>%x=!@uS}%8~H~bH2!T|^{sXQ$V5FM^YeS3SP5xHvmNZbTMaUhPqU0o zokrq33XYNQ;V-aEv=#RzP&kVaT#q=9c|b(Zu21)AVKRB|2sir4sXv0qztasI~Lo|4-F|x z8N?R6825!q&Zx@h^{6Fy#}d(=-Yt*^^0SaCI*3&CFYnO1ab+MZ=FK?-5%BmlAFu3V zXL3x&-2SunDj^}M$`4GxVKxxiRh2VQija`VQXaa2Ii`g{bRBx+m!B56Lm!+}lmDH~Z;-TZL9j_OlC(T0QQoqq?x3nM?TE+PLjYKhAvmB=6 z7fInxB>79Qxaz1R{GhlQ1?d<&i^sV4n2M09t3(=*CZrC{#sx7HwH$8f)gx4b;i@76 zuRcDmb3ar`Y7jhuV{gIKBKVlA-e5b_JACXU`xXO)6HX46G1U6E`R)laB(7#7FY}QzenDQCMZ7+(dVqr21RS*x`{v<9C$(z(7KtUKxB0 zW;H26m*j*TLLX9PhCHY!6<%mNWI6n>LcZ|D0*!X}bA@N-0OKrXdhhHZ<83;O=iEJW zC&HI(JelX=`CE(4p~^-%)UF`iC)NwA-3H)N;_b40vq1>;;~1%4BKiod>lfIwh10Iz zMdvYeq6b2O0ILM6TK<9VpuzvAF^gq zzPsf2Ysj$G-a7^tP2T9XYvWM0q{f*x9yyn7s&5yxr-uody?KK1gAyXqAw$(2O{lFg z;8X=yxfSkuq>~(DBlesYu_5U0u~_348VL)p^1P~Zq9n`wOF%;SItB1gx7ly8A%6o9 z7QU1N3E_$D8~)U4w3>5q3}U{mEO`bEgdqj9#pTC)qIal^b?X{@TO zh0yTr|B@K`;>YO0<+Wl=;DX?7Ava61bv-`@p;xJ)v@!UA2q`ZCn66J;>UPYOI+z|K zoB|i+@{;J9Wg*07vF6#zc=>XEZsB9v{L&rSOWnBJ1w-yw%mG>QMT-lcWpv>yyYO2e z-V!hViR!k`Kq=b)S?E9>3^n1ZT}JtRz)Dz!1Qf9*nKy14Mu&U?Bo~@{4KqRQ7rzIj zq?D3p6Ft{}BJxt|WnxYXZjl5|3o%V#VOIkda%EsaXny0LEZ;1=;WrD<4d9;+yDiZU zv?Pej0!tMNY>6UE5OT!g8Y!bsf4BQKw{Kvnh>*nG1zU|#FVuX*b$K1y#7Xzk?n1(* zrw-u)r3*;g;kLkF8EgAVYnqJO1UPpS)_^~EItMs+RmbTA-&@11Fj$T}DmA`WNY{ms zjo`x8G$j~*w<-@&=*q-1(q&?cYBiDktNWG6QFZX9EohSBT=XYgRece#G(P?Wx?j;{ z)yMPYlmNf(%Rl@&8$-aa0~jnz-LVq>g7o<#jU=?>Yg+DX58iU`H*D&UUx(@l_;r!3 z^L3hHFGM#LfeKFhLAMdIfUEa6It4s6X#u!;`}I+eft1`}>0-H(CTOT0e;wA|ywlH( z{x?UZZYii*nslp@d9MM_M4L9r@)?xBdmp>A1(DGsr1uq6OfIE{z~I`6={Ep2sipJx z?jJ-vdE$7ts03>=60BvctD&K0A^v_j%wkBnYXd519@|+-60DgBUSAH=$4r_%wL5C5$CLULO}V4 zkbeE%$2N^KIcORH?K^X{KL7F}KZPLKfpI-Qg-}Z{XPWpT08j8LQtWKSU~}E*@au%w z7gGaw+8McxaP1m*A{uPMULXK!mS&V$mIAwP+QsKrVkB*b@XsKKNhCZZy@Jpfs>>ymn03n6X1X zmmAzr{yctTmv zC(o7@5AaGy44-BC@|&9J@T<+s0+FhF45n`;el6B^uLo}b#Bf63J%o2E9+@Y@;sRJz(pJYTCx`eM4-S-p_3 z9#+PBVe~fAmvKs0R_=;Ex-&S1mr~!leK~S#h2v%Z|dx&-LFNSFd+F7 zOf@@rN;S@TAz@8&5R7%;{_zA+Z2c|jVqy-kIscSi3MMAuQ&2PQ z$sP~-53}D3iaMhh*%`ryO2{`xc?Wj1OKtIiSOtGc4MT)% z<{z3$wo%_(Z?F=6%Wt&#KqV%wvOo+iCYS2>%$mqXD#_v$gHG8dx!!4QH23Fz(@s;W zSWf?DZ*g_ke7zV7Gc(l1DQP=6k`WA;a#%8G`xd3X^Fhbc@|cUtGHzW*+LmV@ms0xeIVFiM zM%xn|Xm2`1kQ^jtYe~lTZ@?`1hF~~0e99%{m7#EIR@!k@RV#*phGlhZs_OFH9(#>5 zL`Y|)!ahk@z8y}+wlS}*DE$w!U9PVIzTOa4&sXlQT;D& zLHQrtf@-$}B0MQ9h)()cDud!Rl|412|LV$!4(&el*G=ZN6jDXq;a~qy9XCZ0|BLFN z;-X+^Ey*dcL`x2Vgi8{|j6QSNjLJ5D0(^hW`pKM81Iw0LJI4%{3#k;l};1>SIud#@$~+eQ*?^ zSK_dQOuU)(WjF+m>RtJ1ENvh&3YRvs;$$C}&BAtKl!kKvQ*2lE`YP?`Z}iO{T=E(O z7Eh&Wa}9}bgOvb=p+!KW_yx{$e&{P>7D1*KvUspm_ zHeLuQniF{@7T2XqE3?$8`9}p3Q3VpZmFU+B&r(3R%%>}oZq4JW2%?z{ORTX=b8V>i zN*sS;xZy}9mqN^?MRpFLOGg&sh-6s*HbH;^CWz;3&E`MOjy)bk8{*NZ#eyrlBC3HE zTJ*IRq;g56^u1)yoLPX9a{~;R%EhjPU-HcbusHuj7ntv#*W}WmIkVhJ4v|g(EzLS9 zK3=K_$>)zgr2kZLO>XW9V(!$F z>_@vOu8C2n;7V5)g<6Xs@APa3NQWQCfJAG{S}qPhqXhUz)th7ti*!Vjx)8KCSvHky zj~rr~e3fa(ybMpHxXhS7jj~ZakiCXBBTAx$_oi6f-atqDKcHjX8|Y{OfR2&#fS{qy zpGC*ffM{wJBB^6lGYk6|ln|nETFwFta%l9?63t-~^EfV{;VLZ~jHfmXmf=Qj^KB<7 zh2!pIL1TyFelxWMbpkd%_6xxy@eu_AUPp^%gQ_tir;DRiLfW6DXk^10 z3hJBHSNK-ws=gge{IW+RqgFX@2kmD1;Qw_-wdb@!L((kL@NNK6AguN#vY3|vC86II zTlyDVkruVyn0ni=R%N}wv+_}g99rSrKc_cnZ~jHT93!(^wImC{Hi~%-5c4FU+uLa} zP8~yf2waw797?l53GVWtf7Pz=Y~RXzze4kX$0GExMLi9tG>Twa`6P&C+(auPzvqe zJsgH-4p5NvMxI}KM)0o?5nJ3_=)jIiAB>FZ{nViI`Gg><@Mg?xXLs#`# zlpdSZyVFY~Y)kS}>-$|^?2#|!kl*;HTaK%B zI5WzE>lUc4eF;iF08m4q@g8lu_G{@+g~$Nvuf9cU@(){{3~bz{wKrM_bcjygu!rI5 z8uiI+$~7(SuAewXXLz%N}&a5*v@-d$dOUR=u`D>o#qf1tY4BctbAjy&u%HjlN z27=0ZbNXxf6_T@mKDtNB4ESP*5F}+4An^!@RPqH|$`8acUPE&!Sh3ywD-fxcTtI`q zwN}YZt)pJRb66r0Q~z#O9N-@T7&4A-HS{kSvTaK7Ui`P;9~jb-D@YuGA&UxGdr@GlrrARNl;ucu$$NdYkAyHNm!d?%F& zl~{kJt+I&GBgv8qDA_t+U|}(P)3hOPGd(N7OwYK+EU9ezRjFi#vYBGOgi5xG6_Gcb zf{61PYs58O5b|ux6v)&Q{}}p%U>%sLaj+gbIXeU;Os_;Du#Lrd@Q! zzLIs>7^ZKYIAW`{q<{6$#&XeY6(IHQquvwmL#Syg0gIOR{>HavSc;0>$>LG~Nf`o= zls^)eoe7Y*Z6(0mkjV^z;`q@`g%qr`2S%nGAaVKLkd!|XH@s!OZb@|YNP{l_HP&OU zhmzY#2#~md5YC(aBXNPJ_Uj{C)tmq!EQp2=DwF@SfH?xD1Lg%*zx-&mNv80S0DsJa+B6{a8Bo0X9leHht886t*s;S-YM zEAnVq#cX4AqV5VT=*J?&PNp;Mteh=E`-<(R%-ppGTrm9mTB2GnN5cla&2=^xj0Tp_ zp@yjE)D`s&Do1O?5n9emag~~F!}1?>zF6|fb7H&f=amDIb94d_N;72JS%tLc&9Rhv zx|gI&X3VTjQP{^JXVp-tk6vu9OK4 z{|M#SyF=X3cDUN;(p$aSx%9caztPK1F*S~-+wJSp({XHpcl}b1`CR<))1{AcA@`H# zf6Am~asSGsJ^c*+uS{B^`+e=4McGKb_?!V|z^_FwFq&n5Qg0mfxOXeXcq@Yw8}$B1 zHa4z^4-n~@|9c=U|B`?IMELrWGF2PU7u0?a-4|5%`$s4j0l8Jy{f{NLo+(%mA>Odx zVMP7eoA4d`c?=I%j_Jweq*S}6zXETTVhp+`n85G(rKUChTN*9AsVext^$oOe{D;1Q z593HVu&T-U9pt$*7CijtbN(&So`cSLHkdwN|3l}1@*39vJBtRJe)NCY?sG-}a|KCZ zpswfmXBh2OS4qG_@IwgvnenzA(ThO@<9re3IU`Jc3N|y}fP!%1Eskd1g}%gqRUvgZ zqXNJFYTGN8EeCsDq%1jUkt~@JEi1-CAI?;tI(T6rMWPAK6r_-e#s>`XXJvjerpzcr zlO2+gS0CsbF=jtxM=Z{tw4nKzff#ZvMARtn zPLkdyx8%T2x_a{3A|GJJ)@}m#3?1+l`b_#mA}>E36Na8SSza zw7#`HIE(*yxSFK;{d)p%F}}N6yI!^C?M5+=;d3w~*i0f#q`fWrn__Uw6_k+vc zj9KupB5+XWU;oDteftG4L_dc5XNaC^&M`)5e24h`rf2};w$+d)E54CQn}kM;OO*sZ zNJWg}xa(nh?34 z36(CsYLd!+YbI{$Z&`T@eg{K-Va<|iw-h&L(A-2PaCE=dm|8R5?;b*I;CyJkyt-o0 zD}ZHBxga)zbK9gup89G#uZFnZb$;yydjxAna95+1>v@5NMk!b2>ZGhx^ES}fy{2Qt zX0{zB=@+{buS__sjGhOU>#!SyYN6%V$P`$)x0DQOb6eQtEQp13)V=&rn~==FQd4$} zjW3b}-~reN8cZL4@nLtGwR!c$!Vo(*MTRh_^~tC1!DhMnLnO<4y7*zkVm#&vJLXg& zcKL7j+-zB$Kdh+3!c8ddF}RtfU&@_$hu-Z%FsdQF1|@BxWGC<*Xs(`xpRSNp7#`OG zB@GXm;WoQg$zb8AVau*T&K!Sc9NCdM*g!E0TR*sZ@n?pd(OXPN7Mi9@bP}*NG#8FJpQowQnZ* zv?e*tV?piNRhb~>rh*D7{!p>L!h^p6;ac z-cs%!PVP%nS@u7oTxf?`t=g!Z`?5N4X<0Q{1K!0i{@9|rGWvuHAp28^AwF?=a^Xr$ z(8pkMQ%pHT))8ZzAh92&8%UD~w6Q(I;Th_WpSO)Wg!4hvZEp1YGtm(WQJ^KlaL|M* zqXMI(Hz0+Wn{j5g%56j9r|?*;8_6_9l1DdRHzQCsB?ardzOJQPbzI>GSK^juTICL} z14EHm=FZW<;1J^_nMQLGS_pupQk5+W?lJ|xU~!6?(69}3)7oT|N2@j1L-C@q_C_C! zJw}ztUpFaJ{~vye(A<9wf)tVU#&+he;ihZU4CZm<@S?4ILgE-LX0sYg)W>6n1({SF z6Gj!pXgUN*Pk*s)j%BVsL;z0qCW-6YISPcKuxf#}c90TbuvhHloVHTyki+;K-5lrgXdm(Lm;v!UKrT$p&_f8ggzcca={3n15hYyrk1Q@9@K z%r^`Dz2Sh5pZ|vJ z12_W)Z_dCF;0$1-0Qn{z-`lKY#vcpeE$RGT3Vf(@T7t{!09)bo7I9i|HfV4QZ4+g% zWt7q56V%>A98(MK?jk=B>q{CGd-Cy=XDQhk~HXA9y$}PS-fq=U)FB)b|koP zqNYI6gRq-bKXCpOi-x^)J_Jeh#r(VYVmIYKNhfmHza^a)d`WO3*O?4&Jr7}@Dne?m z6cBj&G197yV0ryRUjr1uwVJ2Uh$#KTN8H!Q%OC|m_p>lzJ;@KVmS+EZ$Ah=-4#QKs z2P>Q89|1T`^rro$-c(@A+g0wH3haJUf$~;>3KX0JRA65@paP*(-?|-86!1Sq6MMsH zh&Y3=MbwL0IQ=!9FjPW}W8cnjlG2Tx1Gi{perf+AWSoI$2vgrwP<|>^3B?}e3Q5e$TyD~fIIZ#ILV~8l>KY65lr7Ial~F+9HQI@W*85X z$rfLBPEtaHziVLcQDBH=cVh}NG*6`qF*Gk73M2XssBjvvec_V<${_$$xLDrPsg4qq*jneoN7q(mB!7r25Tp8)r=U*mExj*gu zN2)1gpcvX@rN;tO08CG8^=oj7aHtq01w|?=etSDt{i-4d|H24viBraGyA(x)P=%?> zC$!)N?BazAR{RJdYt_4YO4u3}+1Za4+-OZUt3uv+%{ErW!uHE^GDFJtT{E)c;s`IL zn?9wPF@Th^0Hjns$&<-k+zay&ptt^2X>lwwEJg?W@&4#s2}3nNih;-Wg(5j^67_|e z-seQ~6(29l$pgoqx9+Bb{b|1nr6s-{oX0jMv+78=8Gzh57YPR%VE0>%qko|Jv$SS( zj3fiYT7J8}h`;%NOUkYuW7w^8ij~>)f0cdEo!w_*eEQJaGqze=T$~|9kvP*`)VB;@@$G|1JJa zthW76@o)Q>ln)tO=hQ#rUn|+g{~Z68w6p!c<6mHi^xe})=n)?HDG0qD=R(~`agPh8 z3E`H}cGD_}2uiO-7=RAn9lTo)Brwg-XR^*7R-f8W&$p*wG&p*bYT$rWo8@#PJMaww zNvj^&OV|XtI)xTq`E4G5%dd3AN0UeJg&Yio`BHA8_7%18m7-YVcfR*;rBYD5BxtNJ zg&=rlAVr=aEaAu*(Y3E;QZI7M2(q{L6eNY{)q4J0Sc#l|7TA8n#7mC)8MoOniI3T) zco|v7FU7=GC5Eta=%B}Y`uqV0aqv*JRH-MJ8_JgG6SY(eX8~!$I7@fgS^GEqKhEV$ z6+v#5QtA8`ONl|vEWwpOZTKKh$QDp?PC~?9=-28EiWB3ycZJQeK)c2^6?5_CamqVSPmoMU$B*Pl=kaom{b4e5zB2wjJv!hB>AP>;DLp`sb_~Llvez1amVu6({csx^^ zTw~>2I_p;|eP{D68UIXWSt){1_zt~hAjzAJ0EK+<<7>Hb(9805F6b_o?z7JBTyHl9 zZM&fMdqR8f4nX1B8=jCuQpI<+Az90qPeWKJ)gS&WUQw27QXPbyk`4hRZhwZ8s4vbf zYQ23QDI6a)V@B9U66VLR0Q`KGjGiA7eJgo7&A#k>91&s|JD+x^)AzQ5x}0gxoiI)*?O=Lg(f=lsOZgRmzG z1IXuDo4?5C!WA)45Y7H?pX-^jJ}!gdU0)Ho2JFrvT!8Hsm`lY(ZH^6j2-Fw{;d&;? zA)at^bmk3AS-q9TX_)-8EKX19Ga_N1jcX<-@Tct2`C~Xe!EWC+X58oGrs$NVZz=q) zMJ#S0TFsDKCwy3PkP>;pz!{s}!cQ=EgqVX?ZFbXQgFiUrd*WZh4xH^7=@Y!Cmp*U} zm8#JLWpU-x|5X+zZL*JTB5?UvRb1|%^}q~d_kUBldp>HFMBS7#!hp&}<5Fq<5?L-g zVvm;f4sylqnWpJY<>pLBuS!M|*3tm$ENc7z@%C0xadq9+F75$>1b0mc9)cDUEVu>Ngy8P( zZiOWwxVt;S3U_yh;99staf-a}XM68|xBpz6b5m{AoRh`6piwpY==~X*RaIXaL;vA* zj;zuMCm1cE8tL9Qk5hrE#jA81WqG@|O`mKc5J}ZI`EQExbJrR-2Yhl@9AOFF5n>>GA!j$GkDuM4Fk?ud;w-pUE z=Ws5oWmhEQb&0~~k0+JE2rQY*hZD(5A;U-c&e_b}i)c#1IpB*M>!yh1k?9%H2f6iy8i^QxJ42yi*42ytP%6>MR13J z`bii1+ls)2YWN|=iVj+`BzUJkCg(B={eaPDle6m|lk?;^AM<1W=IcP*TV0VGTs3%Q zPx-4H3nvgmqeFB>c;#SLI@Xp8eO+R?CK|0grSXaJu_1~|X&OC-P;*iZk3_i>W>FL)o!z2Jw!x`QLJV*Px@U>tNj zo3|y-UjO9IO@GVt=ib5-7w=CQ_+f(dx&Jjyz9ntyi}4Sw_}Ev1w!D7v=Vca2FxiwG z_+m=9G2abUlYCF~a1{}{ZYP?_ZF6!_B5UzrU#d1&hxu~Z|54R$%Y(m&NY3FU*uhW&@+_qG1shndaESCw=WqM_rN0Qn{ujJKpc;`RLBcvl z;8$C&f?oAi47yxi<5^3Yn$Wl3yC1vp>FVPBO?_UsaLu~!@Q15&{~;ZK2I`Pt5fO!U zNTL#wqTtd0_ys~IiK&sW$9dJ%OaJx@0RM|mIq9E1<=p1{uk)$Ww57|b9b?I5q@g{Z zjLz^&nqdgPKm1Vp?PZW@z}2Y(sYRy&$m9Fn;+QQ+x5acoOWXy`%7K3?aVfaIvi)0$ zJMwQOZYE!~^dIg|$)}4z1rjLrXZJDzi#f7& zeCb60K!3D;D6=v0?tc*8d3niI>AVcy*rl?FYs zo-)qQ*TniG*I+I|krod*Y~^FUgJlcl4EJlTWpHimVfWWR3(~&s*TgR*j`iY^W}0T9 zn}1dpz+CR~_-MW>E%Yy{T*{~S_4wmw<~^>pxR)%N%~hcH3(SD-p%l85UxK+_E7ms6 zib+fZt`{_}cyX6O&Tl+C7N75ygabV$5is5~o8xy0u3c&Q^E;ASJ>;A_p+9IA)8bJY zt>=*d=Bk;q{zRvw)5u;3OKAkI&KZ|=7F6M_30(XnO1n=>XC{HR{I=B-B83CRC(Qi|IYYDUJfpO`&qvWT)WxB8DQf#1C7*@s~GtTqfjs}1STYJ>c<+R*GA z&@&1D$52gr@V&|+>{(^#39+4ugfb_qa6gDM7gX|xUwW;MjQW-~O)FnaYJd1H3p1ga5judfl|YRCjq_n4>o6&T z=8c-AeI@u581de=eA8jbwXY44T5A>vXZoZhMz7X6cPn!|FK!vzU#IZ5H^Bg1J&79O z-^r3iH<1)<^CWLN{e3fdl;Zx)UwyK8tGl(wE-Az|O{;38jldumVhU9kK0Zqf+t1zR zz~>j(S*X|`S}K+D4q5ZrZ(UArr%a~tEH_ArIk9m;`_5lqPQy+yPQ1z;e@wr|O5i5V z4ma&OZMC)1MCAWr(m4W+lZeJg9}rNBCUvthWQem>>o7pI1~&qzm)d^)eMI6`q7}3S zy$o$Z&wST?Mj_Zecc06jyU%7b6|`=mR9DW*k#1JgZ2R@`hC@tw!AkXS`@yU>Ac41E zM+UGPWJC$Q;Peq%)}$@aTGq^qu>cSF>c0c01@7Kmn(INW(f=!=GV8n$+G_4yI?#l+ zr!PyO`hvR}v^@ z^dq+ye&V8_=Mn2)qub7mX_v{OkwGe_#|(G zPuUqV7khCwCX-y=u*wkyMXJbmQ$X9=iTzr9Q+VAsn!`!KS$OGY4%svl@pe-4Sr=co z1%(Lr`uo=)?KuqjjT$l&)nF+lJnj5tML~-^|5U%CT3k6*#4-<%^Vd#$4)Jm3_-p$- zc5t?Pgs?AVhxv96H^d-BYdB73nzld_RnRhiEyuW6_0%$TMmBi;cac|tlUV5F>?W-1 zPGZ{ZOpxnv(v4rlNs^O+_`GH{xX@L+t+aM*f~@_gdK5T77f1Hsy&N7La(Tg0dkX!< zq#xygsO5laf8*|=qbExjtqcl9E z=yV%p!62sOIbCYP7|!Z7Yv>28$1xyNa@nOWmU9Xztyi2*wRRJ}R9$obblUh>5gu23 zm0Cn>sD4crm$jKRQ~HAG4Zw*NYuW(Rm@lq$z+1#0Cp8^Y?xsh-97B$4)RJ%g0k6Ba z#F`JUTk=TFlDn%{Ox1JBb50(w8@YVF4l4hw8wwOb8}Ch2LU~1J+1Abl<`hmIUVK?z zvZ~is%Z`lT#f3e^OXn%s-Vz%5iNwD|AkzB(=&6&kUbtoO>Iqg4%+{m{idbH3so@W$ zJ&Q3E1JfGYoj#z(ox!a~@aX|akaf6^R(mJ}y0mRi1SVI9&VxPAJPKB-5Un-;Pc#09 zo8JjEH{%Q9A8q~)bo2ZFuWHP$_t%(O`3SCDKow>aHp1vOuEzhTa!i?B$irfPdio?a zE}axRH#|kjtTP6@H;}6BNh!0K8tP$*fDc~XnOmc{&&(l7#F@v8pY%1?0jW*0xz6K+Uc#B z)+22Jw}v6Y@i}>v0a04Tz23L7w{GYydcmD^N|b8TASJP3Mnjz$-m&-6{I%R~yHuRdL*G!UU+Ndl z)T=2BzA_XyGvaol2x$n=&pFRXgflZ<%4JQQBv1A0ylYFYjr92};&&nHKoxfM{d*3_t zUXG1klej2_h>SzMtIpB1$pRUp4gJ9lyU`d%I+-LP{k*q#A}?59lUy_*RIZuL-3g|3 zTRt|hmNJi0jh&(Tv_H}lD<8?#6N7gOVXYs7ITr~=K}q!j2_|}UB<8^c=9MvgDuR!G zM3b69-WpQpb^-!NuRWLMN2-Rua|6u3MvH#Lz1qj#=uIFK_~rd`Jj19*wig%J@G9HK z;)5K3&{I$kG80Ly>9*p74ng|y2zqikTJU+1wRSvPdOQ0B(YH9UoPSzR96)MzRC$>WG@FC3;K7ZNb8Kqr+Db%CiT`=ciuqJk1Wz}?p2;V09! zp;#- zfgMijAs2Etl_m;r1GHa)7~*znDuQ`8sU$o*HOf3AxLX4l=FqJnql&JXZsPW|_dU*&5jls+Faq%ntuP>p-V-wA>o>?9>=9b-JAu!Rhs#{`}+&q4R{gEMQHz%X=Vb9Ysw} zjx-~_f=@>ekT~2V-{mcvVFGx$bU$jujwLzKFg~V>htHD1WPVnrHIxte5#>#tDBJIX(=b!;E2u>X8jAwqacJ((*}4 zpmw#nKfshrG*qhJUX1yF4o9E*JKE3Crkx%N`8(JD(710h2lKxsNswQh{YjGipnFb| z&>H`hB+-O@PLkB2(|!1Q=xMkIXu5ydM9w^ixWO?$`%%nz#Lm1~Ha570KpyFK;gnpi zS7V-~kvaN5!u^Lr?6t6Lsa}$_Q|GBN9_V}&RRq1H(1FAwj^ybDgK@PU;l$2H6s~EF zwy03Gi=5+G?Q$*rqjqJ-|EJoeso@W+8n2i{_Z%$=0`(ZW&~Luf$qC*GAK_h@zD`kx zNA&v0LWo>4{GZv9)(lBMZ!DKp8y1^Ax{$Ho3ip-DRUBDl(1bsb=AQ|Fa<0CAB>dO$ zWB(a5c~1D}%dDfJMHPn(lK81-5`SSvq~y9Z3alie;gR6_N5FqW>+An9-!F*EvLW6> z`ak9SF|*NHBJ$V1#Zy&;>Ogi2=nZ$YNZ>~RwzK0BjuT9Nl&tZ-{ykLi(65b#&yJ6C zu@V?7LnQ6dT4oyGnVNq(K6qqfm9_^D&#s~e54yR{ohafkJ)J~LvHzO{)@%i7)9m`fSDbtos~;e0ABDoX7GV+~@pE(_i@+R;(|x zeM#YgD7^8@HA2(%2zbO6hu&&+{8@{k@LW^kBgC`(2u96g55_W$7I&KJI$TcsCaGsr zCpenl-Eq-~S)FVPT~g7Axw)+nkC5{-`>h5oNv;KxRwPyC@2$MvWfeQDDr?X*H8txm zrLF3nxP;oL$jYZsR^3uW6i3Kperx5@Su+-Z`GI0F#FV1>;;`QvV~FwW!u6YopBSuVl`))kE=R??iwHa~d%X@kWem8cw)iGJ z=*aPIBT|@;(R*5P3s_}QEn>k0TF&0~RX9pYFeP>FZ0jRvAo$C4`O!->7&ZSWX_bKFbj8 z?E1VZx%+4uakf{!=GB`yHIet9-hMT-L;FXvKY@atb=nRwwu?>jgM1oX;8jR}@wZPR zw1Tp+t`RuCSCajMGCCr(>;s)YWvr)vpuGwEjMfi?^M#%pUeFhz`^AN2n1au0_UjYQ z2HxrKZp%j&*?ewjViX;yk{S zO$_}(Ragn_jFyry#7%81(^Lwd<Zwuqzu=^QwsJK;5kfC}%r;10}nC|uV zY9RzwVOr`f%BOCuXK{=F`K4CpKNPMJS0q~T5Kq{wDwWp1G_IXusK&L6Zg81;LF#)3 zyn_=)H1UG<5AIAulIk|o_s!jC7!{sAY-Rb?$FIRt3_wlqnkw*VI8#=eo97So5IjNoty<)PiX4icb=+?tR3hmmU|=|F z5ur>)bvE(2!kfE_z@@lS3>gg}Re7$wgx`@~=!^uny3IiX-_eiq`Idtw@-AQ3NVDDH zY1OQFp)srC=S1OYm2Q(3{dR5!HrXwc@I$PYO{s5DkXtIwbYcXlASt(Ar z)174$(4F&SB=x;=qP~8x#A-u%rdq$t^{Uhg3@zZqIeWKOm*6WlTaoaAk76KcQ5?rh zg)SRhf6m=kAftm?lz4@oIGrc`h^{TZBxT^)q}}%VmAgokKDkBTSc4E1kC_uABg`2= z&tfX>e5;`$0j_<<#GnrASnMWm%-JQn4`08w&KLGR!KGye<~G7?PL=N`o3HpO(nh2+ zV81m{upsgtQAaN+RBbcxMR4a;$IZov{vvz*>Oe*Pn*!M>z&{pXU!+UM4u}#ve}~gM z`4J)7`A}X?iJzXe&HVrA@$HJ-d38mZWs9s+5RrVYLjxX3pL@!%ECz(D~$Mu$;QNHX6}wx*XlLOCF1MKjqiY_H>eP- zS}G$iODqvhvPIDtTe1zcV(iZC>)j8V6E{28=Lf|hLPqa6z8=6+15%s2#qf&0HH(GH z_)AY?GpbM|G|N#+A>h3*`Z-hjLBEu~wx@3w-RtGpU5tM;iE@T&M0PY|46(=gOT>Uy zMs>^_g&WzuWrD3DViLf6i5ahj`LUmSpJ@F{f?cCq8weD#cOyJhU*mb%u_k5?9^l&O zEgkF@{8-m(5gb3jef_PqA^{-R3I~P>gD}o4AD^ z4^fNEZlmc}pY#|#M+epbZ7WheBL$LkKMs(bq*C{-9@UDn9%v%E?RClUD*d(a$EcWLH zbc9`~zNO|rQ);Oo7xn#rVEJc?H`f1UJv)c5`eD(Z-psY0uL-rDDdkH*AsYs>@arBT z2tp09`G3&-W2OILKl`l<1#LtDGye(NfJ}HeL&pqxdjE~)N2#;Tef{A-l4_<755E2T z5hSk=5W2(S{xxTal41M*S1q`MrdELc(d?)1tM(!L)OSz~Sob-L6t)PBA(_-cjcG0x zP$5{|`B@0=dKQ9PW}xbDuI9gDNVTee$B=&hXADVnjpfDX3sldS7yeA8uP^+IO9d}Z z)OCVz)y{V(r=V0^LnvkAPXq}%ZHTKJ>LE*-g?h*ex1nfUx*$40#7I6gf&?Y$nm|dq zP+hnSstZF&x|b!-BwfIP0UjA&Ce(HYHMEVJ{fQt!dAp9#pb`@w)X+v54)o1aGF2w( z4qVyNS;?f`Nzusv!mrnW3lX5f9$+ji*<*_@Sqns<#FMj5#Gz3+9lt~%U&H6E&JRxb z?kXs28yeTn6#D@>X2kkAg!CF0ji@;=63WUQm{#dqGYU_j3s4sbsC&=Dn{U)_00qLw z*$uqLp@quE%4kuMNx$0V;e_c71-8GMY)r<^e}+>IwyNqLy+=8HOVdp>j=?N#XH1#Tjx z9BvJ2ybjM=JkGIpW+X40Y`o2MZJhDyQ@}IQ7(V=2kQC{3&E$9anNy1p0eieDH#6JTRZ>gt>I^eYUj?0Zw8_pZR+#F&o&yY`5LJNpAnnR zc6_SeD0;aDq1?@fwQrfR&JB)TFWw)79&xBF*EvCfn)>GXqE1ayav}T9<9XRMdW?jJGB&0cqMc7Z;+3&w(cNXM-Hh?q7kXp`qtM6TT|i&i|z(Yi1bspL~=CH0D2u zP2IoaO!oo*7H9H*N^jvlkSEh57F#_ox1IIcMRwOd|J3U>14m`dj!Kn5(fp3ukN%x# z`TeR5+9Dyw&-S^kmcdm`eJl9Hm$8ZLCdN5;OY=Wc^UDv%X32GZd1Gi;`|y{RWe#>4 z`$qwKd|jF3Irdp3KVE#iJCKjET$a;oPlPb($hiP|?YUz(5rWgbFS(XiY6jUUeU`0S zIRV-pJ;W@!Tt2yWk6sm!NAKv;Hnw)12a;e^wI?i!ZTO3>bN8Kc#w8%fYI_-u(P zIzCO%Ozq^X^Ak|ddyYu(W{;;7n5*eVwa3p2eBe=*n|C_jgL6&)Gl+Tv^;NUzn$-1`D~HX`la-7;}oFy+lwgsyv=vf1s} z48=rSxSrr4=42lXA|!nFcch zP*1pQ_#^XJhpE{?Mp%hocc`O~&d$cMC)R^V>^sxD98;I<$`H>J1z#l?m$HC8Dl>!F zW>0b9z6%7Hiz_1+pBQBK$u0+*l;&p6FdaW)k?89lmfco`Te=pkp#`F4ZkqfnGi2EQ z+iH1G?ql2vW#BKkTjO24y4lpm41Ou^0)BW9Mk*iZ`e5ujmU9zvuukYgrV%tDpT2tU zw7KhG1Lp>g${=ve4+Si+?)&&|jP5=0xgeix+#tr+6G2p$v0{BBo@VzJ6Ep%FGoD~G zn+l#JHc=n^!N>gqaihD)cr)UUSTQ~l*Vm4i;d)~}G;Smp1WrO3???8Y(pH2|WiYm2 z9}+Hn$OP>~u3bVDbjO~?PFVMzqy?Rz?@iJnKgSyip8V#8vhO{rL!i@B8wf5}8J@Dh zQ?LrCO=@VYz9{NU3;znAci(Oe2(O}pIVdnX1z$xhy}@o|!MN9W zYhNt9+sJm9j%g%F7`FT-?$(s@a{S|Nde~P%;?T#=*da;y%IIL~`tZOqj4tSrhe^LF zWSUqXzL+}$TKj6@-3Q^r9PW>sz{{W&O6k~NmHfXkaY#f~HTno25*I|u~08>wYiQj) z;Kx1uG?)0plgVRp{iHIv^!?EI`!tTp>D}8#n4pcO_2%WR$uS!?wnNbX zAFKF?wmpdpCMg>Qu_aG(_LRQd^)4}vp`>#VRfx_TQ)$cjg|tvBk|1R9d0gg`@B08w zy|RJT4t&R{NH1gsW-b4T%J}hhL9AxdZ0THZ3~JG0#TgTE2MZsRFqM z2cxlI<&`ekF8u35#;&wsEXa148uO+G^m(pl3*Z6c0Vi&6EJx4NpD>~fqR?AIUg8V_ z3_XsM*%Vi0Qy@$9LM0nF3Wn|uGrK!;%lbmSU~k~^@*`=J_T-(zAy~8*%ow-A9p6-U zDECc*qi>JW>V@-B>JHLjzkpR-8}d1idgFGPNzk1xWx$j7*dcYgwR63E9H>@MTL>12 zq6*-yX!B=IJ1YoA{-ltx)m&CIYRvuVtGWy`Ge`O&&t&a{R`okYWqufY)K~}QW)rHp zJv7>3j<>oVM#F5gwhF>&l!p`OjNTrW5Ed7B-_`5ESd(8keSqBro441R%*~$gDM(CD zhJbHYAKQH1o!43Me%fjR68DGEHM!{W)?qCfxfawjr3&f~MMFf9y;<-P!ChO~rTpl? z6u>s;!_U=F6n))$nro!hixlngZ2Ud>o85!{Aaj7@Cp$MD0x zp!ia9VJ<*$o>4Q)Xyt+RL9t^vvS+7B5bh1wFr&Efd-E3^|4ef5@a(*|$(R{|c8E99 zi?%>*@C72z&9VNI_*f{3fUp86Wm4`kg{$~6LA$HUi+hZ$Ex^JKBs$illO5!;mdwwZ z!0?E$$-PmlLs>I!cqnA9w{I*qaA36L2L zUgw1Hx+}QvO#D26d(~(u;Ya3*;P3Lb*0w>ROsoEmvUf0Dk8Xv38IH^CliW2moc=?b zOA0!0(QIDoT;L#~OEBN$GiuK1}bVmcAAXO9Jj>3lAa&P69(ILmPUX>zr zOT`-!Tj*hNFE2Zlyt1R+-r2{>O%~E0sypq8U{T&Q*(cyKjs@56z-hu`%FqWRa@N~p z?`T|r8-%$k%(>Nesw3ltz}~dYTSgunJ=qrVX!Ua^^6dL?!&QuhQ==pW-a@Wna+9BW zfqoVMD^k)WUUId1Y0~d^6J;813JIZ!?7mm3s0|; z7nE2h(NBJEEQ01HOrtF$TSp`LX^0C4%d6n1G)6NgU)!Vk%rMZqkJ2f$fVBFt)&5-@Uz# zSpDMoRLA6k(wDjA4S%bZ`020Kt^2k!UZ9hDi?{c?z@aJ?j=#_Z$GR7S$UEDS6!UczmnopsqK?X{3Q3Qi+2pQTZd$J0Rg2EWZ-Ci#+ovGfAaj z_{-Ezm3Jav&lccv7&?^8QJ*cyGILz6tI+aTF^g{+xi>R&nEQq)|K83kc-`HLbNPyQ zWa`+Y^cY>nhkNy~>8qLbUb0s)?F)yjz6-r$vYo>zCJm1koBfo>+tzub54Y4^tBm{( zp4n$8p{s4oO+3_Yv#k7=C!6Fo$9q3hbEn>udI7ba6QeK3b^_&_=8P3&`lmy_>V@&+ z&UaR*nEa|vV{K{&AEC#H%Fh_RPMfR!FzSU-I*m+aIrqqZ5pcQOeNfk@hoW`t0B&y_ z()AAht|3h+8S6sB=F6!2#3(nq)9PHis>)f5R9WlcGQZs+Su?Et9@9}c`ml)LPG}LL z@e1IcY2KnWdR=5i0c*fR5l{*C4ANbIhei2?IRG^ICHT47WoE5p_lAwpXm9X$klmG< z<;LN-J!Y{ZLY?=f=gv#9D|w!c7YYkZR_{y+{vjAdNYQm1CU&^>}*9r7Ufa2J<*Ow zj19xVOSDaRnaeCar%v>X-hgwjO7!Ccb8o#Pr5R&8LNCj_6}bBtw86)y1CM}@7Dtty zmck^AtqJ=Sgo8a3hiM_LZ(i1*ABV$n-iCyd?^6!3u=Q0=vI=I$s1#5ni@ znMzlZnSXYd-desmcb91T;()KkFgzk(lB#wp6jnQl@JKr|5_l1>aW7S(i?Gj7c?G@X znM?0lxb^kMu?We8?#uf=NR^}_ZZT9kzbJt2C_n=gVO$6;pVqyVEeHZ0q)H!iW+0b^ zTo;{%wGJZ_x)QioFU%Io&#p%&#XLom`fNVpUII+voHsh&u-WvZ8E$r&zR`y4{OL(sHceZHa0vzHz^Q;xTs9>i$6fuC<&6gdBBTJbEkwUB39U{7C+8 z0t6d1WFt&-R=Mw}FSdDXU>395K1B%%C>8NWHVp72u{f_g9e36qZjcT#L_WSLx{n$y zG`gKS3pDiFRO~6NiW)OpTkLmX1&)L~8dtI`K$a6BD(}b>XK?S1Oti|AXK;ycO|({X zPlYFG&K1%Tg&lF=ZiD8dJG*>Z)0-bOcjSFI?I&t+f(Bm_ zyt8lp)k?R>CtCM9u6$&}UG#k7yFdla85QAZ_ZvZ^&CBm`2b$;jUDnKt=dWCf@3q#c zar*`ih(Qj754153(c0v&HMi%asjh?)hdZNeo0InaN4GQ=2zn4y(^%%7mY-Rq(v&uP zw<;eT3A@TDTh+VV7TcOr_&vQ7*7>YX(6;Pr&tO_?;K(x`5JLk%mTAE}+w{5zmgxcK zKe0{|d7=@`(!8A zTtooreW4cw<}{z*V(>u36l*7U#jdu#zV#3%^>`EPMd++WxyheMumG5HB;RHj+cEHF zbvpa?b9Io#lV$rlt|2|*wq@vW=ZiP%@*Q+}vFcuD=-8^u3%Xf1D@btek!+k^A_Wnl zyBamCbg3^DB)SyJ?_F8`a`$ygzoZ|bOqbk|rSkg$L*0nP6T%beg^B%wNv2TzqIC%Q zL*r3L>d`itejWw)@d5i&gPVTXZSU~C^pKGC%4`B)Qvz051)SYZSVLkbQ?IL{sm&^*!t=+I{Uo4 z;E4`6LX_5%OpmO>O*j-9D_j8;?!5l6%4m0{m{TiH`L_LefZHhTA-LosgTVrJs>D)Kv^+ z{S-SuEQ^YSh6{|+dvBDcR&ax18B3-4aSHh6L-7^3h`iqX_{aUdeQ|JVf#3~mwK+Ha zhIFyBA+vW8^wCX6Rld&y6-81DGF+h#5nf3KeTbFS??5DPLyocwPI&hU3A*t~pW>QxW$aYpb%o73xY?m^9ep{~#R%epx~?!q`sCuqKNs~F$uy04 z30O+C`SLrAWO0UxS>W`u)GhtMd1}-m>!-A1WG_&}S+i4#D;-(Ae7;U$x6-zUs~5Kd zt1e0Hibk~l!PPR4fq+u(Ndw>)JCQn48H%UH`<97{@p_9KuXr@oYToT`0MqMe z9PuYS@0q_&`*KT-H_Ph7(nhW*>Anbry7Ke9Zw?4N)i<-eiJ!6u>X>$%b$IIn&ns$h zv!|{A=Z-jim3h)vf32ebF@2-*matS$wZ=|8RlpE*S)F?+01S zM+E`g_e&#K&1RJR2Wba83d{ta_X`)ZgVzef(U3Lw@puW4HvAkm0?4yc220}&GsVK4 zD6z))V94@kVj`HYzg)D>ZIOR;a5{F5O*48<8fwc)_?3**d!NCkANYA7&k@YMiGv)B zgk=-NSa4F`tmwELztC1|R~BoeD8nstX+?%m5+OVufTf1PJzubH$LiZgoY?pgei~$G zo_Dd|VZg4P8o5NTn>4ayeVWh5`uwwbYaStPh|`buEVxyl@QNh}Yiwpp(5FwGpd_AL zz^4gfAAR_R)D4)D-~-a;hRHs$v{_yY1FIwNi>%b>atC2PRB$TX(ll8FYTqoden4M4 zux{8s9ae;0JznSx(fPm)IzHwA2zwNiQx!cFakk=W!Y`k;9H4%v8_ih`rPV`{C0}Y0 zSo7jCJIRl&^Dg-9J}{DLp}r6~1p9`&!C`aAzGS-EF|m=0D;>lBOVCV&7NSnwG~-~j zI}9%R*L0*j^T*w`_fZOs@6{I;?Me8xvw^yz&g)~!$Eu^Z2X3Ed;77MKe5&)C%bR2C z2RyjHy1L#$SklA9EyWKo6=vAELq=D@M9Z~?5zS+!gIc?T3NgM>4(YO|GON0rtyb2n zuF47qX;Esf3CRn#UU^3&bxXIIoI+S`B!s^Pz2cX!sjmqk3+}A+ZstHi?hu8#t`Y07 z^yN5L>#$YB-Tt0U2SbDwlfL-U!i1SVfF1Gs#|dIkh=Ky~`2=y(=}5or2~(GqXt}i!N6-t!4BcPu z=S;S;SH5t%gc@jyA4ktpd#Okeiv}Qm$lh0XH_igJOLDB%l?t;YQT-n(!5Utf9pD|;iIZIz&{a!$GYo^c^`9QMn6vv{|ma%kW& z2}-MLAPVj=yPJN3r@$hW=1X|_+MiZ&{T-*C1kDH=9#XpGhTvg+$)oHBLpApe`AzO? zVJksu49~uwDF#QR{ztjdKkz*n@G(|_h1d0TE?lhkLkJ-IrTf-(61Z9Y7P4c`B5u@N zg>6juK(Xk$ona%Dgjz|GK=RvrQxu2YxSQ7!{eHE22DNFzj}D}`cQ9hu{8dbggZLSn z?y3!l=4SAUtkqA>mIkIa)oJ=vFA}i&99+0kw)Su9z;|Q35?@g4AE-wvDG%Zg;Y17= z(SF+DdaZ73W@;lJ69=PT3i!pKgwvR;d!j#VgR5|LS;B8cGlBI_?f^tv4J;2!*r9%hxuSCJngZE@d`(}fAn`~M0xjT199A2jPs z^w?IUiXa%c&JZGzy1~=kH4}|!EhBDM+#LTdS{V@>y2d67Q*RdEg=m1Nr`A+2DRNEfntzVUj(9s+UaYczn%!j-QO4WLk; z{cOoBn?>6hkIoK{UtQ-aQ^+uvHPVMYvR|cUXLy19#3W~^P_y$lr^yRRmUyK|2^2UX zmzn2d;;boL;W=RrZb!@~iwy6rHj=uG-!8rieX|>PXs*srdC6Pv_Ylq^fIDClUk`e5#3ilfX`(8z~(Mh8{O6y zo~~vNSxAl20^@Rg8>#}oJuXN-?x@q!U?UhCoUN_gJM5GXORNzFjCJu}EWLjzlFcZq z*k$JEqlLLHBZltr*Rop(bq19FjOfH#zs2&&Bx5ton$n$07?um%nl*B zPu|Fa_Z&}Xu-+)rp5Y!rO4k^k9$IG}4BknBF zh3o!6z65NfPsLOt!uahp%^vL5TiT+;k!q|+?C}BybuDl$*5Oyqx9J-u0QBJgQ?uOakqaU)A8 zkZPnNpS3CM+C;~6$@-yxFp-1`f%g;11IGEKuC9Q+!wOntTPpYvm3nq|=w|r}0W<_h z!)C8g7QM8z0+)^81S(6j4?~o|G`V!KVsnp9eHj{iz1!bGDkO0ey={%r;G6!1SN zP0G2rs>i4J-z0UFtBud87wnSEG*XVrV{aKL%TKE=rI#vpd5Wda$2=k{A9l5v zXWKF;9(w65mzYi3C8W2!yVKeuWUmkZOA?EAz~{?G{&6e7P+t*^Fn@*)-$oCO&=;Vq zMX*}%=>g(swoFiWkC?JBnd_uahz-c^VZ9u<&L2bur6@dz>nLO25Hw;oj4-1-9Hcb% z_9YuNPwh9qo71eJv2vc>hkm@%@lBYT7(qYrHDjKaenXe`gx*IrRYRB`XNu9_Db6%z zdHu6*8R0^d0=)s{jh*iGql%O#*5T`z_YzWVtW$HydT7}Ch>%l=A-0)WXk=ZO> zekC)A_mGP`srTppr41K=g3&8hrwg-fNEi9B!uZm0=HOFc1rF5=mYI%Y*4m~U9Nvvt zeb}2*`1iXmSZ_fRxi)CbiQ7Nqg%-#8PP^uh_Le`DkHd|EMC&5zOSwfMWZ>W2ZxB3n zJanB?vBj~8bwOe-#!Ab_D$Ov^fbxl26+8 z)G1#~@6@`{O@C@p_^#@7hS(7s@Cf=Zeqt47N6YV@y(?jKA64wF^ah!?8Kfy5BwnG6m_?GsQk%CB9_*K)M#av5O2clbm92XGf7$*vQMr}A1n z{V;D!U-w$)U)izw%?7ofze!5~!Qb(ZHUEk-=&Nf{fq8Yml9;#34)0mhPDivhiMp13 zFC>JzrcLYXgZu;1cwgEF2=JVEghy-2XKnQsCcS`j?n-Qy97{hyc6my%T3r%SwB(J{ zD;>EM@Acu1KbKtc?)<&<)N*k};6`5w;w1a)&&_oELo(o=~H2cusnVY8Fe zHo5qUlYsz8NfL1@`egE90cx>xtB*Z2|;We?y*wI;Gda<;U0R!q!z9_K5 zWEKcM+AC2-8s{4CRI`_XZ9lEKh8A9WM|(l&^G5yIN1OW*Tq^wz_u4m{~j z?Iy4EVi!9+@wL(&3t-m$T*Cmm#bigrP|(#yL1wxM?xKbEy!MCWbO5WwL#k6zb4fy3R8Ws8@O4mQl7la5&9 zrnj~^E}uq+3B98{s(4M~)TdChb4yo;{Z5yph95LCleMXmelheb#HptI${!W5aUN&t zzZFH}t`NQO(;@&hm3T+ii+q#vN?@KyXmHV`IbOPXjF8^fjVIKfCB9dKU5{VMy}kr5 zB1f$q$YemQJ+2u>6F8^Fb4H5Kcyrx=A2Q^*Tmd@X4_@Q5lUu!HNl{&-tit{&mUfW6 zGkrI&;dI<>5820q*SBZNi;`H53NC)GMYmDkiZwk5#)7+5WFlk@uC9JCrJL_^hTP`g z%Ju5;wA1RZCB9+QVcX=r4T1x66-tgEGFEctI4r*(a#uFRN{G@p+ec z@oucJ^uHu$M}{HOJ1_)!ctz3f8>b;Qm&Kmai5HAsxc7VD@Pj;$%j383V|CjYoa?>4 z8XYC8o(Q|c(+2!-6B7n+?jN6GESD)Xh6BGEV3_DBC{tlL_&ms8^=r}hzToqUI~m-U zHNZnFIb-_$E)w^8D6=UUWW$9ENBAzzmCFpI&x?yd_0jX-`_FPl6Mt+BQF0qsqU4td za2po~QACkleikk%!`7-J2t&A)(AQ2sW!LM`rQfwcCEN$c230k`)}bYWmFl>F$t#Z_Gg+sde`goXLogqpLUU9Xk1i z@G9>}o#H{l^#>i7 zu7b|fdXEmOA$AOi-VGkt!$e6pIrdEIMq&N;@d}YC>cf7my$aj-p&l2}#efI?ifbaQ zWhyqO1=a=zwm`nSz~uu`7sCF{ARl?-tThE8^V2P?qVT@52}J#|!a09+w`f^wmy$X=SMSj>Qyzm)-$WT-Nb?WxVpx@j5o zDyHia%a^q-$*4^sryVq?v(gS4%~!B>6&(t&=`TyzSm^O>{z7@)X-0z42aC~1ELi0S zG$@Zg7u=-Y?~C2~)o2VG4B)_g*yKs8z$ST1+sYN(mh2Z~*YXj|hClL2@FDuC^G8I$ z`jguY#TF(%q06uT2Pr_-zq{IiZ{2mpQmfA1{q+liB#eci@}T_m1AY%f5ZC_e?k_Wn z{J$Y6P1E}nyZ3@AH0VQpj{Rjuj*Y`m{*0I!-D{Bs)Xziy<$oXYCujcq;OLS3%%=;` z#*Fj~D7{=t^k=Su{`>HB0O2V=nKyvcFa&`7|8-|ucmMY;HLXbhl?#{t_njZ;ztTYV z|8w{M{XY-Ty61l%peJOCr!DW+U;XdD?hJtAmmxcrz5M%s4)s8jUHO0h=jrPZ%+}us z;z!S))AiK;O~UX?gB-};fSVlI{!cz8|4z28DbUp_pgTWJ^^E4%BKqaafjk8ynZNQO z{H;5FwEPcPya0&hugLTNhW%{wS@9ykMmt~Gx)JSs5v-|Mf0M&kY)_s4vRt4n!MFdp z%cp4bF>kap)NyS1uF5CMoX0gdO-z4 zXf!YghFlpTghj;4;MDPWXfHUf+9bLpgj)zX92SPdcw(wrwW%ytH9sJ#bz8`&(qhUb zy)x3YEvC7Enq3?n+fbM4W-2eOYQ?3;4zprIq-Sj^5f;gE5iJ$97t&0Zc6cadr+QN9 zRl|lh6g51Y0j#ft)K#Jpxth_ zSudm0u+wpQind+eXNyS`W&NU~@U4azlQ4Xf6tmS6|7qEQ#9l~AmB z&6sNo_N3zZ4S#6$0e6dWy)_@TM$Jqtb@)k-uTSJknNvp?B8yB<&Iq$rjc}eT`lWt4 zb8xKDX{r(}di~5|M{$fgnoqeWmXrQiE1t#yIPN7h%Sq2->a7Ez5WEiNK9if zXdu4aquN!0c2K%gAW3Kx1QjpwGYo}M2VqE3mPSyrbR3i*%f(1#^`r^@ftnH-RjRB_ zHN9rk3&Ku`s6a$nrC~P8EXA0t1U;(Er$U?5jrOEiFZNoZ+mhAMtWQIx*$P#2RcNc- zj>}pwHg*wq&g%j{7E1v#9hW+kuW5-_An{Hy6{ePwW=mn{tQ4Fq2L6B-!g+?El0i^h zVi!7NlBo};KIe_SBvCOGTNdON(xu(`Vxafzv@V6pxC!Bm4A0x9T(ldyKE%DjjBd1C zRW*^0E|UW`s`OwWWlf?NlO)VmGlLkTW()VmEZm7%m`Ae_iZs22SC#nDlu zR4PCWj&taU5YncrLg_Afo1&TH_ z6jFO$f=yh;D5`T5@)pD4cm~H4jU>WYDm4YJ2$riSgrz2lPz`iS^V)Je9SSq6CKXmF z7PVc`@8~>Ut2H#kAGDt4g~ySKKF)?@+-L}lSC#-D2_#?b2N9h}^=e7u%Ivh=8_vW! z+k?%8><_b$MR_nM;$k3^Eyi&iXi1~}1u0KTD$?v%W2+DmMAIJ*TjD}>s&&_QOI<7* z*Sgr!rX)>DUBi0wO}G2}8A)gbEmeX9<(UjJ7Dn zc9BgG&h)9y0G@`$bUG;)Rz$bGbo*fd3v)2ry*b`!ihNH}8ohReD)pv1tfu3@F;F8+ zJgpuz5hoR=f!XP>q&5{R7F)0P;8c{fQnO`ovECeW-D;^QL%wAOO$seY9tqWFfTwG2 zsAd^jk(vXa!7^cPgQ1n_ev>1hk-1b5N z7U~>Si14OBsZ5wGC$sj*aV1?TSStwT!x*<~SPiH9cx}-bn{}>)G{zC&tx9Dq*A-vw zV}#KjUSIFQ+hUPx!*HP5RZyDOP#A43ukU z0I>rY>qRCND{M!YAY`@Dmgdn=X!at}>@}?sHx}7MK{YcO$nz>@rE``=OY)eQn4^l* z?-kv8KdUj)jPILH+9@VV1+R@*bz-%qdbHq4igWGh(u}y2XnE6dRjn?niwqJWj*_bL zjI$baIpexhhB11xZjXk#D5j)F&n&YD(IN?ofmF3bTO-G#DY!MPTl1jWlZL`TL6cBo ztE(zY8C=6%CY5D1uJ;)$9dPQ(28yQW+gY_drDuInN$Sn9Qv6+VgLIChgzW`}lCl6evxme{m7 z4$6yYj8&#-Z1a@>L+eeU0HoKOCS#=El#P`*9xi-gQuYetVbEw)s}pzG==Z3WJ(!k; zY`rg391D)ZLSIcP%#!IR3jtolXv>N{lPzjBV}>lKN}{9al5MgzDlI~Td84WHryNxq z*ox7o`#ecD7ow3(#$zs+q{|_Y`>{;RmQ~VJ8|cO*;?}~E99wC5WsAP$Wg(g`D--ctY75ZvZ;$_OsLS|*O zrZrUP&xMe0WL%SM&AK60lf$rOulzzKh$S&GLae7w%5);)q&O7j71uNBM890AFB(G6 zP_Q}_kQ~=8b439S8f2|jLMp3pHPycGNec8DFW1B;nU7 z&rPI)D~V&W40K7W%qwtM>I}*piKq3++~<2-uNQE_5a#R6AtyLD~*srp@&q!zs;TqS0uRPDEsw1^K4$QkiadDVx?smS^htzGha0g_0B5stbR zc-{Xv+918Ii?FwO&%oKA$FwQL-A~6e&aD)aRom z(OCK&Rx8%5jBSas7tn?zho(Fmadf$~obW=SlTM3bBgAluGRsQO(Ca170G0uj2QsBM z{FO0CpgKiTu3kt<)Bvs^y0FCtUml~3AG!kq4+A@15oV`PDFoLa3`W8Xr6sRC98;PQ z_gAdiF9F@rhWoS31gtEYv4xK*F(a!2A_e|pB+VyttlpeXx?Qi(ShB1gp;DvBMNTPH z`*GcbYy6lr1w|L zcC#V&fg)?b6~JW`rJ0n8E^y^n`k>rPrMOa8{lD4N;Eg_kAy+ApoaD+kO#V@ z7m=CS@0Y8<)tp-+b`FeGwSdy%%5*xN6wWM^3fO4Qh-s^y&3I`+@_0I_@nhcfE5rJ# zWc4_EBv{iDnQ=f7+nm#w7Rz(gw3kpV7$QxoA*Il?0J%D3t!$k1C6p zb&AnwTxF)cs@ml0jk!|q6iYlO_f*Ff1U{T}FqoRl{b3~8m4;YIl13~L9l-c}FAZZ} zDB76=n-eKWD+|2kPwJI;-l?^!#YmUPSZel2e%>lESd|)01hL5j$HNoDieC+eR;AgH z8?%fl4`9NSmL%0lJjm17RlrSKOI;Kq(@fQ7sr7VZ-0OB2&6r_su?!_T zG$`n%nH5(&VD<^ZMS^XX`DsYEC(s=da+X(Wiq} zX@PNJg=`E}I_jy#cF$!syPmQQVIE1<(O4t>P6K6!DZ%*2SzBsjq!KAKK7Y&G! z0+J|9rn2Ql8srZIKN~KHl8mmV^q5?c_H5dk2NVc8V{JH%<$<`QW@9r_t#O^IAroXc zY0f&TiPK^z_ac`@$ITjqr>a}*LHOK-IcGAfbJJc+lbtdPD}A9W>m3rW*>zu9;^k%0 zZ4{&xg*jc+Kr1c6Dpi3X6)O=&b!WzGwW8^!1AO=->EcRDsug1uvFKK7?v$JHQZeC5 zbHo)J1XOEIB;ZF?G0B&8*5N3R>14wso>bKp%zznc)?9rkHO5O<1T533!ePqC5;dAy z@w8iHT)kxWn~RD&fjNKDl!Rcu>Xd=uFpQ2O8(6=Fa)kopz%DnRlxU-d8t|$McTKch zTV^;j7`0*vggI@p&ezfr=RsOM@l>TM*RX{MnLxoHetUtCu~M31j227qq~Ef-wQOlK z1ly@YF3EOBVTjh|dbQ50Rk$MxA>chNQxuD%4Rgtd>MAJIsw_cJWmG8w>$hk^1z|w8 z<^+Dl*p_UC@kVKvq+SK zb}ET!AS0>4iacsswGjceL(N93oeXXE;^JIkTV=>@m7`d<2V6^%{mBII!!!dc--CI) zgXvb=Sr(goOH2CU!1T;z75J|vio>mX$sNk8vFmp{l5LK9*i`o;(Q^DHhYoExMCUCx zEotRaQxv>esi&>_qUg`YR4J<$_!di8jE*ItmzJtRvN4Pm2Uy%GHSFc(O_Kd;$352!V6{A83o2dQOB`OcwNqn{O+`XR6`1cK>Y&UH~K2Z zk1UZ7di;j9bH~QCy(#RYNamWnXpcR3A|$?r1P7F2e;doM^J>i%qhOP|G6S5#3-E zGvyUc^$HL+Pt7Q0n5?LE#Q=jvHH-Vp#i*)~c|SSrPI^sSo@iLd?e{zG0({~lJE@Bm zyAugeY}V#IzTEBJgLyE9&k4R&jI9Nwg?R3(`GW?9 zEo;=G(-mEcLYFRBnvsi-)r=YVi&fvB`{O31uZ%v$u5?N3*Dvk zh1MKeGkr;ER*ft~K*!QxO$SLMYE7m?8D%+sRxqLg$qc3gjVA zo#?$pgCNSF$Kte7uX~KQU`CBtDmH{{t~($GAIQyW!wpoWQ|^ajr97y&maA#hj_W*^ zPPDY47Of>-sVDVmuhwc5$5pY}nZ>OL#if;IS=LDGxfHM$SX*XF5wfV0B<(srYwKnTV}MF~(L+)YXj@v(Op?R%Z&Wu3ONN7=-?r zr+Cn;z%WUoDREskC9yv93vOq|v2x1?`dYiGc zu;#3yNG3*=fXa&UltY1*nr1Da9}8l&JfI>FJxi_C0HWa{5@T>Ovnpt1$vKABNs2Vm zweX2htD-!Ybv<0JDPmWc*DW4+(bHyGA@pUdMK-C?P(UniZcTv?F|Q#E$qA9gRX~8c z8c$G4wJHm&Je$)q)*8_g33Z3V*3{Q@;4Sh3@F#pxqNEv-lw~+$iRzFAaZ{)-EwNSZ zE@L$+@^Y9~t+wk&_)A$+^Nc@kTs{P$%yr{0+#4$VWD+0u2wdSOhM>{Ep}C%7p2m0 z%*-ktHLJ7X(B)BOVXR~j&5bHkV1U>PwpzhbT1QLj%t3mzTk0t*EygNIJayP9gF0v+ie0E!N*C>#Y(NZF=mO6u>1dj23YJ99R!Fs7j$34{AJ$;4 zzjQo78w$}#j0y!z0lt>Gm<@pYUaRtx7>#>znZuVNsnG;U(Mk=5sYDv(&Y!C<>V_ zv*D_==!lBcv0K$nTN)|_4EXjyR{$BDbE53_fWE+BhFPdamO%5dTF_Rm)P+{vlqrTo zFvV)uC@^G=q9}$57*(oKE2bW_a$aWiZcQ8`)v9X;APk2SiJDD(zYPKXF1m{jKN?`v zREp=rCeQQ~TUd(YYS;{h3FkIQtZ!6{MT4OWGF+%50tA^WhEK&Rh7#t$PiHP+;hvw? z2GKj!7eM5rPgwyBboQVB6h25PNS%kY9p z)})E6GuYHJYNKj1iTkD6T#e0Y5yDs&8?%+7H^zV)p-u;yVzpHpD%UwFb}h}}AtqaC z4W@ujTV7f$6D~yfNhwMi<&2moWmpD&b1CSok`4z!sOo_KK>mtj2BHQuOSk8_8iYrU zaU1wRwq`~BMYC=V%JcF7!c;tqF*WGL3==g9Bfebo&9uK@EqEA=2}g~%i50Z8Nu?g* z)s;L&7R%B?utgv5goxHeIi9lS4W&|dCO8Vb5Uy!8EUj0n3*ivy1j8j?S^Gtesnx45 z+h|=Y%f)#$lUc106X*iNK|DeN$FUQYnM579Gp5@p_7>3$LS{{jFZGLh7bhpVeRFhr z;105e-fhmo)bebPHu|;@EUVs7lwoV>j8u1!WJ*A5qqZ(Z8j5bNPROn812;KV}b;4u=-U3XFL0-P02;>Ib6S;|O5cHsJ$3s!)k zkZI7DEz!2;B2=r0zEW9wrExg*(N2cQY#P?RS`BV?D85|9duUU#MsvCbJYAS;kwCAB zANzOKMaK zTdiu@j^weVq;i2Iuw+tO1;gnAaa;+l6c^H{=a+B+Nm9*8i&{GrlR_(j%iUGn(u^{1 zxwHlq9Ge?uPzg#fsnD+}KG_#6O7{AlW@Aw>9KL~e7xMmiHkG=1oh!kMJSdLLmG2=Sidd1=X1`fnWif-o5Y?X52UXgk z2UDfD>LZQ*kOW+sb!N0KE-{OWW~F7*$gm!1vRb#EBqUuWfl_TP(jvPY@(l>@NfwW) zq1v7-BA==Dt!zNX1uTk6F~S5zJW)%wKew3$gGcxvq(~#BTzzJGnr}c@z|5$wu)_MV z>J0i_k&K7+h6>?y$Us!hA1Vqp>@-=Y1=^0sSpkxv1cg>=1JNa;L-S2(iL!&KHWFOH z?Pb=gHAE8>t`7QDGjKKE4t!TBj)AvTk(t4KUXP>hz^aZ1ErKza%GfdIi&WHvj2<*) zN2~9mT&K?~u8 zc`8b}Y_|2950kULI0$iLz-Zxc2qmcmO^QRbRu0WH0xmq?kFXdPR%$k`M6E~)i$SHj zQU^iB!wI+J@-;0kaR~3UhJ&b3>wDe)Y7!GhqQ#R$i6=v%iO!pIhtx+T8F3XMoup)0 z3YyR`?vjzR6pa!;oqFBwd``^*B(6o6hXVgv9Uv|kUR>6+Qkzsf${IigxHZz$GKHq& z4l=0@LmlS%f(b&Tu+%|es-h3|R;68BniAK?V%(IP)R1z-@oY34%yE`oqDmhP#fHhN z>3lNlSJ^g*pDHz;v|5IbiT!Af(WBhSRU^AfK_jJ9Z%mc8Q1{1+QNg% zEUt^}3Me$Z<>6xxEonM7?iu=ILeC7XuLHU48X>Kca?mQRaA(x1;y|tR*uKwcE81ab zW31&yDGRlzz)9w~ok*I1rVX1?bRX*wtk5S27q}5&CGE~grq2|zRie{^ZWIdsXoPy9 z0vyh!wF;amE6tpaJYT>zsrDNV5P}wpQ%g}87gtC&=o<=-852Tk_SwOLsIeZe3~i=H z`*wjM!y!J#TWm>3N1QrnbsDqC9wRO}M0t(HV55fjj4DH7npa^{RbW`DI38wMZxVJ2 z@o<5(Q@0B2aaJOru4nd!UNm9t`Ba)u%=RECbV5nu%Y6(pbQ{GKZeH%s(WO3*C#|Vx zm2FB4)yk0Q^8UEt%`=umo!GAyQ?G*X)4CtEOA!-$O|C_BXL^AqP`bdUs}*N9M!hMi zs&dV&%;)s5BV=+qjs(+c6qf{!VM&UI7FJ0+oH#^12UD$9+bkBUW)CtcCTmrgIgfNb zk{^z`69m^OW03S$_00Arge#5f+N6bQRlE)9orM_mjB(<4XiH8Sgq->HDZy~HD$w;> zS6YoF6feyqF(?cqh{cp?KT%4>+=rtQC0cS`1(RkyR8hMLLb|RR^<5&K@q;KpM-!H@GDOiq46<5-% znCBSW7L_E@z;}xCjy7SGh8Q}$saWBlKTe7*L2_ViNiApHNyJ8#2+dXrk=5oA5Ygw7NZ)maPeH*E2KU|F=6)<|GE>%5gPY%?mCF14d(|ojEW& zOwukNFPf%gEma$C4~G3okxR8{kn{Ff;)?aD)|ADdR%b?rmMLznR?um#EEMZyrP!lo zeIlit0rYeM_3Xzf})oP>eR%?^;t$D3n!%AIS$diKILK-oAhXC)#`S;sy^uywQ-Z~7Fu=^ClF&} zORv~px=krjLyMIgq3DYUl6GvBQ97tahs|ENtVs4i}_VU}UtFxq8}|PG(AtjmWIkULi5C zZ7Y&*5s5pfRqD1M3`^xHtJdf;2G!vPf)#^OOcy3blkX-rSwv=(D5xNcLuNx$s*=R4 z9O714#o0Pim1DYJRSPY0G#*LfSS1t^wqw^V7dsReI;@v(7YAZf#X1t-cgnL4Qf+u% zh?lKSSx_PpZc7VTXcV<9)8i~!DxD!&XgM@x4cRcjTH>rWnq>)zOoEKClpZufB$2IW zN)mVGet;AjbRptL&5@auyb{-L7kg6>+qw-&s32aHBxB5=+r3JwR+tmeT#*>2328LR z@pxg75@SUlI&7~9yJAFl7lrb00K%B%LNDMd<&H*U5U&I((h~ekEzg-C3NT_dSW#%T zEzYD`r`isxKAQ$h5~s%$pD@*=J(C!Mr2}j(RqM-m)MEyIpQ_E)d6-3m>!T4wRuZyb zhX+U}qEJe9Rz(uYB6OLhld{%YOvXeP2*DTzEaMoowo+?N-HtkTrUvsCCE26aYT)61 zLsRR+dIeo+iXA%Q64>j|l6!^vm`~fNQA~NbvCtZlg*gKou@>&YUov>hUX6h_+H2QG zQ@1e)H3V*E*0?_^F%_kYHHXYpFy|{8?-!PZn53!(F@k8X$~Uwo;)OJTjExB#pBjC} zt&IYuTM8>&)tpxNxJOosQj?ys>BuRYuuuqq3s%LuO%3UG4Irlsgb*_W68jx;NXL>U z!eiU!J-mpwMa@E3roDhQbY&KR4=IOK#tgK6qoPgpX(ebbWo;;qD_*fzu9I!tm={{? zYAnc+Crn1lhFS#jKtPdl|7Z;eb}gadFjP>>;{pF7Op5QM10M zM_$C$nmu1wN<4to$)HKuSV1~mDwAek^Hw}1IE@12C8S|bnghY*@N#=Dm8vUZ8bK_(YygsBD;-W}CQ`QNzSJ^?3>wk}i{!-xN>Ve~)FULK zre#rS2TP*dU>3a~ilxc8ICKRCALP2nu9n(bd&*4Hk^{MeS=1X4IM-BNLzi`=qYRX; zkkrK_8CtVur~{pcDI|!Is4gtDYiucOyPb;Z)|wM0-`gZJaoPU=IXaK*R-qsYE{Fk1 zq#=l$MTQwU=gjIq{xcuhK7Ks9Z&yixVW@p|()@cv+Uk#L4A-wZ?CTzAmp>gzruFsY zDnkMjTA)9MVK!8AZ2B682ixoy^`ZR8@C9*TWh>dI)zLOpHvihU_^2@|qJaGv9?j zam+hy=O9YwTv&%pkdrrqN?4>=5ldD=UxAn1z#k4a((as&V}PLRcH2c)+z@lh*LlQ}X14trzx zSGJ6Tj2Qbnn_08W1s&4jabN;hkA|JCb4;BoFE{Yk^t=x5~8V~CVhSH$@1#FRn=Z{E||NNPuY3RHTp^PVYXtmYh5NE zVt?idv5zXrbUr=Hb1Z!&Q-?Mgv;hw_(jKpaXzmY3aHyHOyz{-?y1xrB&@{}1+j#v? zY}27iw!b->@f6YY3w~<-S1MyR_=Q3LCA9Y2>P0Zc{_51XA%LkNLLUUVjyFt;sotMo zTq&6eJuuqzF=}I##Sx}?L=SFJrKNv0&Z^sdH%I_Iikx~>pj)z^z#tu!RgFcb=F}5= zEC>v#ol&M3jEIm;n#IROAt#-^@&-2J2$bbViweN<0Z`vT$LkidRezlv-FjAggxSkg zWjK+h7~RyqE?32H;`l*kl;Y`QAc<)Jh(YiHY!GQMuu68JwitA*#gA+T>$rIfL}W7$ z>7xOw`tS`u*@@Z&@MjV%OI#?W>1-wnbhfLVt1efhgp|2}R&6P_4Ct9MUN zUSfF*y%Kq63cf5PWTx0$S&qA@+&X5wW`%nmf zHx|%D_je5j-{>G96=j}9z5o?|e=~GO?40ZV#zdMk_ltpFC{UQ(w+UlY3_oOgjDRd17h6%Rz-f=JT5B86ANxG# zZ^`OnGqFoHcgRlJ&@`ZLE{#*iE0T*Bqu9I{FMGci^FG28mZDVj;d`LPfnj@J7y-|q z^nIr$31IYDtg+L5N9Gj0cw+?Ptv8PUQ1z1)#V$pCwmBL>nufTloY0gOfIh(s2Xf#B zh#nxz5Nakkq2Q%Y3vsc(sE_6n5svaWBwC}Z^$Q7aR*XY~o#zQ48CdPlF} z2>|{s&EdaV;`s*LmJ?*3F-jl8YM}4}r2=iw!HJL%p-7PS(f5}GWr!D5$7vMKF-q^Rg?qj+nt=o9ONOID^FP}Me^2dhbp!Aao;&21h325 z-eZ(w9t<=J7#_eUxgGjaB0WLllF%egZ}K*x14r?IWO3TTq2yoPO6gXAS&2it9yxV8 zmXDTl#~a7*v~fePvW17EObE27z2_y4bGd8&az8DdqDVOKRMZsoXXdATJl0nEMij&@K&GDH=DVP+bxnkM_#B4b44Q+ z1tL6%6Cm#<|IPhM?ypn{8k2Mokhook8Cc&QrWR9dcz*#PUu6Py>{H*`Kw7o$H$x2D z<%j|$qBNT1fk4U&Ofwx|Sqy`JQUZ!h=SS!#7s=HWedA|a5*FTsgwM51LvRG`T$BL3 zqT!eHu6NUi&yRac(Y2ab|1Q@K*(4fzJqHVie*%Ns*Bkx|nJ_tnRZ1^RVCHKiZdJ!Ve4*IaRGl%sG3J|V!iD!EfA`UIj30$g zE2omxK@K-jwsxzBo>5Di0Xj=nKR`I)j|ErQ)-Zg%Stg~~s%s+&c6c2(Ig0ePM)!c& ziWH0@HQb6eDzoidXyC4nq1^cyU$6Qf5rGBENWg3CnAAhG*OV8X9C^CM&NhF(#_EwQsO2#xSF zu7e7GJ2OAI00PfWk$whxrAj_ukUTe}C<>Ml@9H=DUZ^}#wVVT6FHKT6IDW3_vYsVZ=YHuU=RLN=5F`q};X`;`ZS67Aw>kcU!z5FA z&g0SyW)&ZSQbxHsnfkOC0{Q(;=rButFL?$pm4O{bukhmBn!C z=_r*GYpLgQCgtuXo4&#L;M>(tjS5+{pHdhO$m@R^dkFq}B(UxLo)WvWEfTS>S9S=7 z`$m0prhB8x&wAWOYUJPcRfP|mm;R%`<(=)?f)9?JMxG+-@lW0jr|lSpu7`Dt zy1~;0K6IChk@ww%bRFx23AFR6u8rVjcPULpF}iw9ldg4>>cZ@BEIn`;mHz&SIq6m?=rcw01Qyb_3bt(noAW3F)>+!2pFhH|=$_SEA~Rnc;%EM(m% z)Oh_T3Mu=z@*34c%qBUgVU(`jxr4s!7n2gceIAOx(7@IFtz~+|k7sY3ERKru38#LC zZ~Dv)sqwoA0olIc0~3Ak+zyf-Y-aJ1O2w==%0}0CT-bT0NM|4RaUM(#gT%kOf)C|D zSGKqH)}q79i4o2%CTxHHe2+*w8>o@{)!*vpD-_2Q*OFgWM1tJ{Qqnygqo3X0Ea0-C zO8N5QPu~`G3|}ptuq*d=6?|m}`}nhM!SRX;cP#~D$SdUcUdn;pSGv!_f$1jX^doPJ z{f4#QbfjyCW->yKygYnMQR~=%N=o_HC-Xpgl--v` z6>vZKujOry8gjs2D!z;C5js47ejeGQuFZF`45tWj57<@iXd|{C@N<T_`=DqsmY-P2TM30tIco+I{B?@pOA*?z0Dwy zpKL3NT)+NK6Q5(Y1P3IORh#{$KV;ca?d|{<6+@)0fkye`7jP%3TiWAwAMg%koji~Q zHo;iXln+R(>ox@6^-Fjl)JE+<7|%p`F_rn)NEQyDoWau5yp#0~y3fD$-GWw6lQGK_ zbck4Cj{tb+(cSam355D|r7}F=Lz=7UxbGOM%JE^BYrWhz(6(2xcHfODkEr}y+Y27z zTh*8MsQ?f~CpP4AJ(Mf1xS}{cC$y*1h0C(`yC9bu*JsmP`L~6ZYwT3g{&ufH`eLw} zxIH41McY1{+xnQ1A>95f0XL+~8=sl=>@TgFAwK;++})jP_l|l~NDU`7g7f|&cdxen zjN@MO(Q+?y+DNSSgR|3#kD%3rPmj+7$pTT8a*C1bo|_0Tj1zQMDb zVh2I^m)uOOu)t5Rh|v18!VeQWijj2*-3;#3Q%hG%T@VlN>UF`nElLc)E#}@TNgNre zr4@1J%U8a4y&s}^NeViaUXC5Q`Q|o-4(frcy=_N3r5@rM-|aDzn0ik{-=-uf-G`Uu z+j;kXLLmKAgV?={o4#;2i7G)%2~*xtey)P=)b&cQ5kyH~BaX{*QF0EpH9hN+=nK=h z-2a|jQ+?f+rCh~H`m{d_!T7$Oa=r|H4+S}H2AY%sCv6AzTqvk%0w9$g$~FoXd-Qy* zNa8c3$ui}gIDx=%X}Kb8GEdZB5FUj|IRmTkU(?v zt^2h&f-;d@P|h5oWZpaAPWE}U8BGa;MG8zR_IKJ&3nt^1|F|9xFtnqP6TWd(9{}jr z6Fb>}tbOLXsL0fP@>^nWYk~Zjd%VtTysDCK6h>jaM3R)n|A3Dl{4QV3O_cKVw^%>0 zlZAlZfM%}~xK$AZK>fBgNf=3$Lp@#K5DexylB`IISmkcVZ&&hK!a-6$LLc%0PJD`z zvDWQ*a6tG&MqjDF;Bl?LYuw9sEB{Oj!**OfH7mLH?oAD^?m`>DO}F;rpfyy4KMQCd{pmIK{P zQsnq5-BPDYW*Vy8L_mlAK;$F>+Q5|ZT*@6Q-assf01W_&K-ZI5P>>cV8x1y}m?nGE zs-bIV{BAD;h7_hwJe8UW1p_^3ewi5Qgu2`tn4T%W%4lAR?r6fNU3FsR_p8k^oMW$I zb@@v9GnfKDK3#gQ+*9Zn1`!bjjY(2-SUHppI!J&_hQq64Ho@~Kpq0LBc46T`yMR*% zwn=!-bXQPO*zU}SGh4#8dsZh7^ClRn5&<|j?zTc;cTs1PnEMzua86b3a&1p>Zoj@w z^Vdt19#CNZLy}lNKOY!+u+M8Bq{z8SE8rLL6C0VN)Z+69marpF4;n$iX5G*22}8oV zo7_H@1ih6GHWB`w#lwQvx{Y6<7X7i(HktoufpJBX@?FyemQ9^aYrIhz>(_;g?q6}C z%nl9=^vp+>t;C4wj?48@!aI|vI`ro`!+3dFpPhA7@D(vC4yYFC^KOe7&U**&+S5r~ zuqX&rB1ULvE*yUUcMV=7?AD9t#%KS8u+iyf%ngSAoArDW^3|HHfn8W}qDE1b%`xEl zS3~m+o4?ifsy48S94V4jVXMAYXeHk)hpoV!t7p2eQg#7^dv^@fMsGga&+=QEPWlXE z8i?dr*|-12{XUU~syg#Htw*Zop|!VvjscY$$KeX^q9qVjEH0af)hqCLR`=5WD|A@%jDT0H)a8pz&2cC{s%eSLjMJ!Jt0yoI-TZ5?`8eAw8fC!Qzgn^`9*73`N|Qz?0;M{e+H zi7GN}!&8l@7B-|Z2QtlsRG8pj{iPX*W4L!r#en%_J3?lT3)8RQ zXMYz>d;yVu`*;6EoR`XL{0Ud-yyO~7-!QeZuRb0NgAy2*?P#>odV`EV>HsA7s2QOr* zC?0?{LF?v|Y!CN?0{Y=VIPTIp$Bk<~R%hM&Mu);j-XOxh74F{ray)$kc3Ja~`}u{m z_9Iu|d-_YTSu@7RhqZY#{-w(eaa9SSKm$ZKeUCwKEBLfeLd*JbomL1fuh!oWH@!=o zj2QO6~&=)7MUY{h#da@uBaISoEl@~eO(?lpI7K(_^ z%I8bPyBw#m-5;uJ7>_b*AKh^TUr%;JuIe_Cex1DO-^F-lwaVJ6o@C+rmI$J{<-3Cy zbrkYbFCC;6dBqAkHDKc_uhOr2j_=ucsrtLsV!F0Lj#@IkL!Ok0Tj|}PB(1#Ru0pUCKZ`9#1V*50u=KrFf7hxbSEV4;~F*?GOBUh`-#r zsdt=e$<(ts@=k283b7KchJK6Fc+xR>#I-#;xnWSWyed%Jb?2;Mi_88DZ4SKW`m@Hm zK4ioSbrp0+?dq1N9tI9f7ANxnlJPtms(^s9$2?ETgS^Q22a1s6AYhEci)i^C4Z*)1 z!~o)^ukKbOO>yH{VN-;Uj!BiF8RKL`CQ<1bK6>am%WcMqvsW!4=T;bXrM5_46Hc~A z4rf@{Ji0W*)-j)3e~fe{viHx(PznMa-_EJCo+sF_G|-U4aHkOv$zEDZI0c#^;X7sv z06AZkQycXwNroqiV2-+VFn-HOhZ=SNk>zf5$*HO z&&Hhn=z62gO}e5Db`B#su!oT99Bqs0thQ1fZvT!K`uZRoPZL2=5|)nE8I^JT6xMol zHs84JSfx#uDwAi=MzhFs_%WRcqGD`S9N--I5X?FA1b@(AWyPd2-S<^ibUjj?z?jdO zf-7@K>C>wAOsKf}eybmzXqgM3aztv!c->k?r!Mp;kD878aei$oQFK>Q@^~}Z=?>4v zM|7lpVULeanvC6y8H)Qf(kbjl_}l?~h8hRDu8ZAB2W9-zXD=PHDMuJ2fJWTlpj z7_?pO?&qm@Cro{Ni4UJToB-I1;{ZV{Bc3G)QszY`3MYe=A_(!8AavbviXdGu3m5r8 zZvizI*fW4n2_(<+m>}U(4x-xf7=eW0Ed=$L=o_KhVICQ5;8=whzz3yTJ+_JafIl+2 z+Cy+ejFSovsE0?uH9s8k3wN@-ajvn4KXDw<4uhg}86%owbexC1U-mV3Wc1PxNz&cY zEFP}EXf*gc4C86I#-wQs$+Mv#L{I}*egE!N(VEaK(%-Yyk)YYibutc3#ZZ7H9|~8c`{6?lObL7YVMXvP0sGiLcyxd(5gi0T zfMWA|>e^d(U#76UQx_WoFL-`tZ>gxG`yEn4d|~urD17!@TW2;R6g2q~ z{%*vhDvV$nO9TOjz^zab*?q-; zZWY_dmfDx6OF|cZdSA38x-T9zHK;Gc=e)V9X)i)v;Z!(@H^Nl;=8mWSDW<=G?+-zd zrHwyS?n?W^Z~2|*wk6Bn9tKUb4Xg|X*~Y~mDBp0eJ1#l8$51H(*!5dlnG=s8Yi%r-j#fE8ue%+Qm|NW4G(RIC-Cptf;WKatXYR#l&6b#rssg(PAdy@GChCwQ0~aHtKjbd> zYmhj`UN%IHm&ffCfb!y31H%&};lo{qhOK85ZI=e|i|H_bHnEqZHswvauG>bF*;zrE z{rD_t<&j}C+h0M!2Wu!)AzJK%s?J5oVxYC zx%Gi^O=vk)r>*0I=q-STbMqhqj&@@Rlyv4l2yC^Lz?eCuWOTP2yFe$)Oyl=9@ z@%cA9%hdQNSn*IJdW4+-wZH|N3t`6icM+?Q200<6QJofE?mO5QGM|Vdp1~X0FQC>E zU;SmqHe}voO864=0Z68yD%&2d6sThNF-0|gRnc!F^>SPFGTc333Rq^~`{+f<@$|^4L1Mj(Ntt<5M4*5!s87`#H~6Ml8xtHUU7J|D zY(0WO?wEy!zt=e7_%GAqCm@y=wo>Wft;i1$c8ipnPdnsYQ!e0`mI!au$;8eukKVsD zZQMHWQz$XFPbVDJlbOZ?mPl&+3ng zArbqzIHK6lR7Vg!r~c)}F7HRE$w zSpJBpInWlxuxXo2b6wxL2*jlTA@0NaJgvC*$ML?)INDAX7rNR>(a~Ty)!85zLdtFC;6qXkk5*c zMf|xKzMMJI=Xz&`d&I9_p3+CXT0U>y%F;pf3y3RVn3^g^xPvdSZw|O?|9l#s;<|Jn zy>dGUZ^6-n7UrcWoTG4+6eICYa~A^0Pv_W2eVJC~+m2BdI9|xDau}kG`xE*~Oila{ zho%I6_G!;s;9@|eIi_ja*$pHol`3Mi$v<*f9pIhsbmDwDNqK@L%sWp|Kf|znFmQMd z^IL$G>6sw)4j8NVxzVuGZj!w6r?-J{gKjRbx@^H*EIc0ZTA{{n3p2ApMJQ*M^E^k9 zKRHtkdEUG=-&yrRX_BVtlIA+@=Q+&% zuuz3sc`{&JzVXU-?HvBr&aU?WqJWplpO-rVI7~e9I;%ycY+)P}MQg4J04LHg7iJcOI|s0JU{h!XcK)J&4jdgJ!?GKFj%HOweTi=0|sSY#`|8?1ikC1y+Y-u zlmAFU$|Q^A-pHq5&+~GC4-JN(v4c(1fx}8z^hYSQPnmaDrA5tu0Pk6_NZzfrZhvJ(JCY$v`WzGe`-q=T1X<12c=LnFJcpY zhHQh^2JsrFgdb=DCjUV2A}4W-LLZX=5r@(ui1K_Up{pi*YMyR|PU^q0V<@?)4PO=| zCCOmI<#~sSWQ-r5vk~Hz^`W19F>@4(jAC|X> z#t+S539n_lf7i>|meZ=tu5`i-Vr+-7c+Qy_Ht3d1w>LF5NTu718mdE?O_6;uAXLyP zyd!?B4tj^g9AygKec^s{QWAx&Vi|WRJvBs4#<+fCXLR|2_m`ECRT7CU>pD%|ZM5@=H|yKUGhZK>hJN(o*z*E>O_VvXH1^+8XJE{a?!EjBFYg#)gK5uE$TX2GpPJmaay%;8RcJ-io8#u<0tW?xpZs35{0^{xp;+{9y095 zSteMilcCr~pGbKaTY%zaB}>X>T(TgZj^*Gbk5bN-L~z?>gU=&;Xfn) zTrE$5mX>F}^yTXoC(NrmA9@O-{Lw`5wnRV>mW2=kQzB1ni0tr9FU=u-QVvWw8uhEp zmWO&)?fWv;r+(H?LR8DOZaYM$g?%OIvwO-)H9oBN9z zjDTU3K!;*kd@}YIcm85u5MxxHP%d5_vtouizH=j%AB5(YRqIySW2Fdmm*JSP*0%zSANRzwX%mc@bj!VpWziA1M?6#p-#4ZN>YA zToWfrj+u%l*nBy1NJ)uT89N z5~(P3co1A=oKEtc{uQ#&Ti`^7wq4aLjG}qXU0lVH&z!9$dNZ|cc)o{K7SZ;S=-IIX zqSMw4`XgIZoHbeHb{|#S7Nyf)NwH8tG*PR|$cbED3wM?TmmvYgs=w@2v5?#M?kx-k zKUauo)3G2lQu0P|6sBR32v*r}qO$I%)|PIiS6-_0A;KP{;HeNqtyt{q4k?#?8QOC% z>0!8YII9uai?{Pz2A8Z+{Tj(KH*bDxomr9u4Np(^>o(<$9f33Fd`v0CDtl7R0UALR zoYY@0;5ZA6>95NZ9rgZsC?A}Nd$6W85cm5f@+O#COGpzn$~0boOG;X6z>ala1{^%U z(x<)qC${{>>^HIA`H_Kl_?w=Pxc+`HHIUD#Y2CN{d&rHvC(@gtt)9GnYit6`A+U(m zqfJn7!L65=ZEP5YZGV}+Y*T|MeU<4n?KAu7pHD9Ty=fKV`o~G*A$k;Tq`*A{KIr^3 zz5D&FX@mOj2neb3{G6|3)S?{-MGM@Ptz^-LrpxPwj7bbs`VJ%}@_SGd4RiI8$(~@% z9xTypsXJ#%qKjnZt}Ucd60j3KM@b*uZEUGEGh!;Poo-r}N*9QOHnpoxT2 zn!a?7J+UtzspAfp`xU@cd*7e?+He?TVk9|Zn zonIoP$4;x(XNJ8drpP+$E4i#~g?!!CZZ@+n$FDD^pYuC~w;&xFz%THN>orwp;ZYvJ3+`1c+U_^qQw;1p~vpVUVeiUCb zTD*fv-TLqOS9hk{kV0bT9Xq69h50DS^xFf73Nn2(*0J-8k>t*a@RM@2#ZK^>BHSpVysad-k8Yj0p5RL`bbw8oLznYdKXuejp z3Qk+GEv)!=3tp6^v?v!U!CU>`8)rf{6s!G%G>y<}=8q3M?Qq$*6!L>EYw$pz%)-YU zLvqNU0*Uwne@~2dEMRt>?PhqTc`zle>PQ07-t- z*a8?r`s}s%7L0Gt9`Mfwv5zj`&5A@)fgYD$QldCj{kgJ$iTBTg6S%GI|c%JYz<*z;n z#!<2-^0V7{J7?zqzK&iL!C@Hk=f(n9Z@wirbzq;vsvtEkf}%hWq$!Y;^x!B49KT6! z)`JCk`4#gN8CE7Q(hBiwzb-J$*<~!=E0N)u%Pe~d?tPlAzaDNihBh5^4ab<`eM*6>rysN2AlC)*!!0peX8Bo4US!%|&^jc*%cp z`rX}#r>U8U>WMZBSFt9?B~J=mDz}Ealn~h9@%JO^%AxnIlSE>r{%oK6vyVW~_6JDw zVL^!&+*9dYVD}9S`#^$66NZAxUR$UyHNGp?rOm5l#|`aU%V1{*XxYcxds^gzz?U^X zU$!fXeSApFo~d-IZb3!1#y%5wgTRWg;$o@_i)8d!R0#MQ;c~o@;JPX@n}PwF&C2Z3 zm98|Xr#xdY)aei@0Hk0%&x_|820pyZX~vcc!zKZ}=g4DamcItwQ_ zKA*z!ENTE1nZqv(hqokgvkH;*7iD&9l@C=Zb%(Pq;=w=)amfPI2h&=T<8@k%dwoY0 z=X`F!yEX#-VOagih0M@I2o2IF)p=f*{^r)=^(LBAS51=be81Fe>#sWLJv^tL1J}QS zbjJZ-P>lau ziDtn?kP5mu-W`QB6jha3AZ71q!pCbwv?+1?AUN!0cvp1x!e73hu8;95haU@>(s8L# z|9TB#WNh!uhG%%%sXw6Uy424U@engZG>CQfhwZiJPp2}=nes3sdFyA>Q1mDLDex-M znPscDHSy=Vmi;G1wOYxL*L9v`#Nvd0TP%=LX#+(FSNels=oFP~uI!W@EgVCPHGD8l zE@}5SJ_R2Ko4mqxN%;bFFAM738ea*W;`r)Y@a#J3n-0qFGQtkWzk)5@8q`!MYX-f2 zZ9;>6UvCyDU|46FBhSo8CG~r*6qbDPJt%pd19D7f3(!K4DDl~A^5%%(X{m+DDK5Vx z3!EalEBLpv#L-O zJYcaQb-eVh4QX~EH!VprS%V3bT}pV@`)H5$&O0?C11`Qkm}LZCdgLF$KwTw4@O`mM zh$|9lTm@u}bh&@M@-flRb;IJhuu;OdO_TY%;h@=lpzdF;q*=NT!tKNA1b1e#o+;KU zEQ-l;&|KJ%z56X^THRGYKYusC5?Ycxow*aFvpzUOoQ)@v@C3F>EzdufgMy_{%xw$W zq)$4xxsOwX&P+qW8^X5RE~+4jq}1o5mzUSM^TecnYD{pVK$uxJ}et>E`Rrxvr0Of=1l;(f~jD^RP}p|QWzHw9TEecomk|% z!vN&>AvT>7_x=k>v*#~IrdNG}$29m|ynGQG==eTjg%446Nv`B)r+G1-T`gmZYcrHMw2j0OkF9|XH31%N4pSsWo=zWNJi$&wIMW)>ThfK14EgssBw% zeQI46D0r*5d8Xj1nexsaf?|l$RkLkid-S6bDrEl(S$lYCEG-x~E5w?^tQXNAZkcyW z*A~mF*Ma5>I`erZk6BtPo>TN>pur6{xdkaPQIrJhr99`9}U*@jkE0X>pWOF|1E?-otkR>;-{nutqq$bCPj5SO57$^?W!1Nf(FR&E@;ZfK9x75HsVOz^fjU@eaA+i2F zbmCN#+qQt|Uw2os>nxI`_w^K4e=ll*vY4l=%#@hHU<1YsMydt#JeV1tzT;-4vbuWH zQZIVZw&ZI9IBum^@6j`55}9cd_26PtO5&?A=6 z;EWoZ%3(o&XvMTVj#eEE-IAH=D(DjTr_>*y7r81o{iZeD9?AWCYoK9^;KkFY7Hhbw z$QS|@PJ_>|b+|o0EE?dCeHT_Qm$hyzi=>bwe_=;a8y(nT?@CJ%aBxs}l>l0?PrOo> zT%hUmmNYnAxNVQYx&dqI5}-h5q-s3!m8xcpN^g-rrIm2L<7n58qf{ zMA>i}S);c`CqrWTp2us;x`Xqz{KHAtu0vcAuQ0TDt}F!%{DOgEaf`QQbOug63ph`t z3#XAc91VzQE=?y7&e2?qnRe$Ly*CC+$*QAVxnrtCk^)_3nemi*s-B)HRFQYSno7%q z(}~5KX#Kv~7|7{hT4F1fURo79>(B49uOGLGa;Rx2rHXcun1ZIZ+d-NVv|NPE+UzIB zirvbO0Vy~a5NhbPlKtj9UM_*j3`cY__V~6cv1bM4V74FCIB(k4-dk$CJ(`}{Jl|4$ z{G%%xn%}Rys|{K>X{56`Evf?{4MyHeBLy=r#}g(gp-p#SDqIjbV6lZO=&?=vo?cCI zcu2o)aqw}3_a}6(Fz|%QQsNw+%>avA#Q05&D;KkgOw_`O@(`3o((u=A$ROK zG74bnOy4dQKrod*rnHv~G{2?^7*s#mcl~b7Gbb12UwHLl-Ki-!6wr~ zGG#sz42$o}(%8Iw@lbw278cQuvf1lTb`41ux$i% z&v$E&W{1Jojov_FXnMfqk-}bMMHdbw+PpYh4EUgepN!!c%hN5Ens99HGwwH3Mdw-G z`LJkUD#7UTc*B(MV|18qIErFSq#SeX8=izj(a8~fmUsDXtHk<}So=VLtF-7j2g)@tEDoE)odn$sN%IJG?V!6+AwtaIwhk;Wbcdwn2Z1 zk=Wu8i?fb^7J%0G(=mshhyOfNlcb}2f^`XX>6Gy?ht~-o-2emuc2nw_ zMn5iW*evqt?GcC~WmSsGru^zkb~9mieHZVp%Pcq-x-Z4lY8fXti=38~G1u?^*H@7! z`JUtQ8E{P65>y06eM>-@bXVNpS~gOTH;-OrLQcCfb?0T6<^9bjK4fjd z2_lLZ@AlyRG_$N&oRCrCAS%vfM@k9CPhk> zBTF|zN);R&JvHZjcxdVH%hT409=Rv94?O+KkAr$*&$NEHvqR|Rax0WUEaAWcz{mm@ zZ$n}Kmh8Fd`kgy{>EWAm0S5|kw^7)#A9*lvI~O+6hd(5M$E`ArF%yItHzDrwsbi8M z`C=6|E+=lfa4)q7R@|N{1}-T@_Sk5R*9cJ_da$J>M-ATw}H z{BzK;SM?A^a!K=*jP{oc!W7Yi*=U0A?Xqxj^~65@zN?B5Z? zLHsCLXi+rw#BT*Gp|cf>j8zze9EzIV2iM)IS%$a9j;AE=26MzC*bmo8Oy`xTg^&x) zgzcm7+D({;4!O9yJYKvQZs1j}P7Mx{lxou4NA$FoK( zgnIIAIXdfb$9sDdSR&dslk-O?DU9SbM1oX%#0P}>cF2#TkY#FUS^)rvmVE#9QX};- z8m+M*snG}f%uG6SaK^FnR>%l2aq`{30?~)Iztz$C{+3l5@ZTjVWFQ zU~BjglRHP@x?2NVz!`~^V3~xN)29~={I)MPaxIJ)vyJ5LDx=^Zkq9i_>%yY!CDO#%LLEO8Fi@?Ag7QLFv(1 zgoRUh75J8OLCQ)1Rz-Mk8u@6uLs|t9GvVD}J~^52Frj7qn|u(D-4#8gKe(02b2Nt; zn%hELOW5T(u&5w)W$xrVpK+?AM_kRUdt{E}T``*Q5V^Hr_!g*D1t2>KGalFTY61Gy zHb$j}y!w>FleY?nn<%yLFwI(#3F>pP%e}FS8p{_J)w7>-s}g`sVJF!Njk#5Y8=Bot z6wSkTcs{m`liKCiqK*F2Yg|h;hOe)7Tq!`x-uUB&o>pPEu6w)?l+*TpH-dU_oN6gy z=aDFdWQ?F|DRsS0wv3D6lkY=}7t^ML`ZDfyco%KWJ3See<{Fo%N7^gi0RuU|i)d&A zj1V+dd0|k0qhhYmwC+$>niGYm>uRgU?@wjr;zY1m`En(RN22%ka$d;R?pkj5`U8bu zs5JA~Q)A!AW&wn84F6-%cM6WD%19Ku+}o#kpo?AWW`jYjEmG}#^9X{$#={O2w$_&6 zF|tlN%=d=QSQEp5Ml8Jv!%tjnJNsEdujligoKIZUq9uQF4k~j4VqII?lbOtzGXB#L%8%J{_ zXN7G^D>6#)-r9T(8Uz$@x~ka^8xhEb60Re7<+w@IiLpNfGo5tkJeGzp#a)o>AFE9k{I1Fevzg*G-g{FOTJw$nG5(VuJ)l)H#UEo4lS}0(1tF_Z)Fb{!d0ze z!BYy<%lEz00d{q)Ns_6d%pQamgeSxA@MrodA-bbxn^ zf#YyhlF#6L%~s-F!Dn6R!#@$@T@TIhKte3B%Xk+|R7Q4Z1r&Gh8z*K;`S@@hQ^>{h zr0swf2Bc6M8|M-Wc7g;N*xlPrGhBU&yH7qY?@rEd%#Cw=c%c;6fTOmEA6k9v8hkAZ zrUvFzeY5}r1m|mOubPPdNJYRN%yrj1*d_^{ulCpDVmZ19c7>Kw9ug=mxbOUATax)y zSX6R!2!6@c#uuX6@Sh-_ICvMK4@qBs#&u7vhr9r1&4XX*8s~79H<@S|(4*g3BY0%{ z<4xl014R>tjWEfIw3TO4p43%;YNiU&Im8R`ofd^EII`( z96)bD@>G!UfGd#ftA)W>y0aCQAM8s!5e^Dm#O-Dpwp^}|n7fS@gBs0u9nSc=GlthJ zMmqed2@Zi7^YH<_S0?5&6BxCQU?rv8l9tqWniQcl?9k;AgH!walOsOXf(=i)wqLk8 zxrID(Jqx>U$yDCYAl3Gvm<{PEP|n1{ys0SNEKHKhjT$1be6NIwrf!dCEx6P^cpa^`6-J8Q&ide4WP%cKXG!xXeTC$#B=>eJax^8rQ&X5 zQt}+4lHfG&dihDw48q>a+SNR2t{m#-QRt98N)04f_Bn`X1(kU5oK!9L_mGpTP}hKc zNuwq!$occOiRa{TMWL7`&!tVnRRl(-w?$sGet;j&BCV7XavMcQuSM(t{Kn%1xQXS5S!yppvzeb9S~b?bf~Q#-@1&0j{gC`KaYI zL6DIrOzns%#Roa?Ep^m_pq~vV7y7KCV299o5h53eiHXcM)X_vZ#&vmLE%<)w8)qMb zk1&tEO@AMYBQHANjet)m* zjHUu%?P*`p4AoAtB{Ly{ir9hPaMpINw(OCYpoMZNL){V>g_pdIRimLx<8OEQH@%VEY!1OY7 z|JKxI`SNcKQ&p6Iw0y^p+aYOws`?Zh`e)?tUrTl@e=dG+ln?##Qyz6yd;a`aaT==h z%Zd8=uYatWt1J5%MaJci`wapY+48lN8OSkSJMgWVzm_RV8n9uP&0qNhQuzh`KKlQM?)W47g!z7hIQzc; zt&X~^|64l1EgYd3!_xGB$@dpH9?}#1DVMJO?H~X6Zk&cXOO}3EviY~ae1X?5OVwp< z)n{Mrh>pYj>kk}9FyaSToR??;r-*!iNB+4AB^c%h{KuXNIKgRX+idzH7HG4wsQFj> zR}aqb*B`>a8qgnKS)PMa(9&OjU<69xER8WgzP{@W#Q!rk{NI9uw1&O_mk@5dZvN?y NJbwGje*!NlV4+A1IH~{u From 7859f1c72de59a0fce485e208903de72883b57a5 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Thu, 16 May 2024 16:23:32 -0400 Subject: [PATCH 164/191] Fix macOS CI builds (#7261) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1926b593b..e3697f8cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,7 @@ jobs: cmake -S . \ -B build \ -DCMAKE_INSTALL_PREFIX="../target" \ - -DCMAKE_PREFIX_PATH="$(brew --prefix qt5)" \ + -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" \ $CMAKE_OPTS \ -DUSE_WERROR=OFF - name: Build From f891bb30eb932d444b9c08f8641f624693d3aa3f Mon Sep 17 00:00:00 2001 From: Bimal Poudel Date: Fri, 17 May 2024 19:32:59 -0600 Subject: [PATCH 165/191] Update widths of combo boxes (#7262) * Update widths of combo boxes * Update src/gui/editors/PianoRoll.cpp no space right after brace Co-authored-by: saker * Update src/gui/editors/PianoRoll.cpp Co-authored-by: saker * Update src/gui/editors/PianoRoll.cpp Co-authored-by: saker * Update src/gui/editors/PianoRoll.cpp Co-authored-by: saker --------- Co-authored-by: saker --- src/gui/editors/PianoRoll.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 75bac2243..2e99aab4d 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -4894,7 +4894,7 @@ PianoRollWindow::PianoRollWindow() : m_quantizeComboBox = new ComboBox( m_toolBar ); m_quantizeComboBox->setModel( &m_editor->m_quantizeModel ); - m_quantizeComboBox->setFixedSize( 64, ComboBox::DEFAULT_HEIGHT ); + m_quantizeComboBox->setFixedSize(85, ComboBox::DEFAULT_HEIGHT); m_quantizeComboBox->setToolTip( tr( "Quantization") ); // setup note-len-stuff @@ -4918,7 +4918,7 @@ PianoRollWindow::PianoRollWindow() : m_scaleComboBox = new ComboBox( m_toolBar ); m_scaleComboBox->setModel( &m_editor->m_scaleModel ); - m_scaleComboBox->setFixedSize( 105, ComboBox::DEFAULT_HEIGHT ); + m_scaleComboBox->setFixedSize(155, ComboBox::DEFAULT_HEIGHT); m_scaleComboBox->setToolTip( tr( "Scale") ); // setup chord-stuff @@ -4927,7 +4927,7 @@ PianoRollWindow::PianoRollWindow() : m_chordComboBox = new ComboBox( m_toolBar ); m_chordComboBox->setModel( &m_editor->m_chordModel ); - m_chordComboBox->setFixedSize( 105, ComboBox::DEFAULT_HEIGHT ); + m_chordComboBox->setFixedSize(125, ComboBox::DEFAULT_HEIGHT); m_chordComboBox->setToolTip( tr( "Chord" ) ); // setup snap-stuff @@ -4936,7 +4936,7 @@ PianoRollWindow::PianoRollWindow() : m_snapComboBox = new ComboBox(m_toolBar); m_snapComboBox->setModel(&m_editor->m_snapModel); - m_snapComboBox->setFixedSize(105, ComboBox::DEFAULT_HEIGHT); + m_snapComboBox->setFixedSize(96, ComboBox::DEFAULT_HEIGHT); m_snapComboBox->setToolTip(tr("Snap mode")); // -- Clear ghost MIDI clip button From 0ee05f5ad8874e76b9c1effab4e9f8923e081f3a Mon Sep 17 00:00:00 2001 From: Pascal <81458575+khoidauminh@users.noreply.github.com> Date: Sun, 19 May 2024 05:40:29 +0700 Subject: [PATCH 166/191] Fix crash in AFP when playing with loop while no sample is loaded (#7266) --- src/core/Sample.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp index cd238b2ca..584d1bc13 100644 --- a/src/core/Sample.cpp +++ b/src/core/Sample.cpp @@ -170,6 +170,8 @@ void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, void Sample::playRaw(sampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const { + if (m_buffer->size() < 1) { return; } + auto index = state->m_frameIndex; auto backwards = state->m_backwards; From ca109f94f4e99abbfd50ca581fabed13ff70b14a Mon Sep 17 00:00:00 2001 From: wujekbrezniew Date: Sun, 19 May 2024 15:39:20 +0200 Subject: [PATCH 167/191] Migrate to new LV2 header paths (#6990) --- include/Lv2Options.h | 4 ++-- include/Lv2UridMap.h | 2 +- include/Lv2Worker.h | 2 +- src/core/lv2/Lv2Evbuf.cpp | 2 +- src/core/lv2/Lv2Manager.cpp | 6 +++--- src/core/lv2/Lv2Ports.cpp | 4 ++-- src/core/lv2/Lv2Proc.cpp | 8 ++++---- src/core/lv2/Lv2UridCache.cpp | 8 ++++---- src/gui/Lv2ViewBase.cpp | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/include/Lv2Options.h b/include/Lv2Options.h index 603cdda43..69d294cbe 100644 --- a/include/Lv2Options.h +++ b/include/Lv2Options.h @@ -30,8 +30,8 @@ #ifdef LMMS_HAVE_LV2 #include -#include -#include +#include +#include #include #include #include diff --git a/include/Lv2UridMap.h b/include/Lv2UridMap.h index 6c22aca3e..23f8f758a 100644 --- a/include/Lv2UridMap.h +++ b/include/Lv2UridMap.h @@ -29,7 +29,7 @@ #ifdef LMMS_HAVE_LV2 -#include +#include #include // TODO: use semaphore, even though this is not realtime critical #include #include diff --git a/include/Lv2Worker.h b/include/Lv2Worker.h index 90a3d9d4f..b15bd9026 100644 --- a/include/Lv2Worker.h +++ b/include/Lv2Worker.h @@ -30,7 +30,7 @@ #ifdef LMMS_HAVE_LV2 #include -#include +#include #include #include diff --git a/src/core/lv2/Lv2Evbuf.cpp b/src/core/lv2/Lv2Evbuf.cpp index 07a3d58e6..486910fee 100644 --- a/src/core/lv2/Lv2Evbuf.cpp +++ b/src/core/lv2/Lv2Evbuf.cpp @@ -34,7 +34,7 @@ #include #include -#include +#include namespace lmms { diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index 6a1b2a8af..1633b8626 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -29,9 +29,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include #include #include diff --git a/src/core/lv2/Lv2Ports.cpp b/src/core/lv2/Lv2Ports.cpp index a4625936e..657046817 100644 --- a/src/core/lv2/Lv2Ports.cpp +++ b/src/core/lv2/Lv2Ports.cpp @@ -27,8 +27,8 @@ #ifdef LMMS_HAVE_LV2 -#include -#include +#include +#include #include "Engine.h" #include "Lv2Basics.h" diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 682caea69..cc8e333be 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -27,10 +27,10 @@ #ifdef LMMS_HAVE_LV2 #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include diff --git a/src/core/lv2/Lv2UridCache.cpp b/src/core/lv2/Lv2UridCache.cpp index 746878afb..7d3a14c93 100644 --- a/src/core/lv2/Lv2UridCache.cpp +++ b/src/core/lv2/Lv2UridCache.cpp @@ -26,10 +26,10 @@ #ifdef LMMS_HAVE_LV2 -#include -#include -#include -#include +#include +#include +#include +#include #include #include "Lv2UridMap.h" diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index b93788ea8..fc025e268 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include "AudioEngine.h" #include "Controls.h" From 0071cb6f636bd60151eb71511c9ce1bd55df214b Mon Sep 17 00:00:00 2001 From: Pascal <81458575+khoidauminh@users.noreply.github.com> Date: Mon, 20 May 2024 03:36:30 +0700 Subject: [PATCH 168/191] Fix UI freeze when zooming in on long samples (#7253) --- src/gui/SampleWaveform.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/SampleWaveform.cpp b/src/gui/SampleWaveform.cpp index 01d21a8c5..783543ac5 100644 --- a/src/gui/SampleWaveform.cpp +++ b/src/gui/SampleWaveform.cpp @@ -53,7 +53,7 @@ void SampleWaveform::visualize(Parameters parameters, QPainter& painter, const Q int pixelIndex = 0; - for (int i = 0; i < maxFrames; i += resolution) + for (int i = 0; i < maxFrames; i += static_cast(resolution)) { pixelIndex = i / framesPerPixel; const int frameIndex = !parameters.reversed ? i : maxFrames - i; From 76d8f65485e4c18617bd3411b8c91536eb9efda3 Mon Sep 17 00:00:00 2001 From: szeli1 <143485814+szeli1@users.noreply.github.com> Date: Sun, 19 May 2024 23:03:07 +0200 Subject: [PATCH 169/191] Fix lost connections when restoring automation clip (#7002) --- src/core/ProjectJournal.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/ProjectJournal.cpp b/src/core/ProjectJournal.cpp index fc77c98e6..ae17b2aa8 100644 --- a/src/core/ProjectJournal.cpp +++ b/src/core/ProjectJournal.cpp @@ -23,11 +23,13 @@ */ #include +#include #include "ProjectJournal.h" #include "Engine.h" #include "JournallingObject.h" #include "Song.h" +#include "AutomationClip.h" namespace lmms { @@ -67,6 +69,12 @@ void ProjectJournal::undo() jo->restoreState( c.data.content().firstChildElement() ); setJournalling( prev ); Engine::getSong()->setModified(); + + // loading AutomationClip connections correctly + if (!c.data.content().elementsByTagName("automationclip").isEmpty()) + { + AutomationClip::resolveAllIDs(); + } break; } } From d60fd0d022ba66571bb31eea3a0fa422ba97bf26 Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Mon, 20 May 2024 00:18:11 +0200 Subject: [PATCH 170/191] Fix glitch with automation points (#7269) Co-authored-by: Dalton Messmer --- src/gui/editors/AutomationEditor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 46521b3f0..9c9e4fd26 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1647,14 +1647,14 @@ float AutomationEditor::getLevel(int y ) { int level_line_y = height() - SCROLLBAR_SIZE - 1; // pressed level - float level = roundf( ( m_bottomLevel + ( m_y_auto ? + float level = ( ( m_bottomLevel + ( m_y_auto ? ( m_maxLevel - m_minLevel ) * ( level_line_y - y ) / (float)( level_line_y - ( TOP_MARGIN + 2 ) ) : ( level_line_y - y ) / (float)m_y_delta ) ) / m_step ) * m_step; // some range-checking-stuff level = qBound( m_bottomLevel, level, m_topLevel ); - return( level ); + return std::roundf(level); } From a527427abfc70122589c36bc5fbc6ff21f9e02ae Mon Sep 17 00:00:00 2001 From: BoredGuy1 <66702733+BoredGuy1@users.noreply.github.com> Date: Mon, 20 May 2024 03:37:18 -0700 Subject: [PATCH 171/191] Changed bar lines to follow snap size (#7034) * Added lines in between bars * Changed bar lines to follow snap size * Changed default zoom and quantization value * Added constants for line widths * Added QSS configuration for new grid line colors * Tied line widths to QSS properties * Changed default quantization to 1/4 * Removed clear() from destructor model * Removed destructor in ComboBoxModel.h * Changed member set/get functions to pass by value * Updated signal connection with newer syntax --- data/themes/classic/style.css | 24 +++-- data/themes/default/style.css | 18 +++- include/ComboBoxModel.h | 5 -- include/SongEditor.h | 1 + include/TrackContainerView.h | 2 +- include/TrackContentWidget.h | 61 ++++++++++--- src/gui/editors/SongEditor.cpp | 6 +- src/gui/tracks/TrackContentWidget.cpp | 125 ++++++++++++++++++++++---- 8 files changed, 192 insertions(+), 50 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index dfaee134d..5489a7d21 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -352,14 +352,24 @@ lmms--gui--TrackView > QWidget { /* autoscroll, loop, stop behaviour toggle buttons */ -/* track background colors */ +/* track background config */ lmms--gui--TrackContentWidget { - qproperty-darkerColor: qlineargradient(x1:0, y1:0, x2:0, y2:1, - stop:0 rgb( 50, 50, 50 ), stop:0.33 rgb( 20, 20, 20 ), stop:1 rgb( 15, 15, 15 ) ); - qproperty-lighterColor: qlineargradient(x1:0, y1:0, x2:0, y2:1, - stop:0 rgb( 50, 50, 50 ), stop:0.33 rgb( 40, 40, 40 ), stop:1 rgb( 30, 30, 30 ) ); - qproperty-gridColor: rgba( 0, 0, 0, 160 ); - qproperty-embossColor: rgba( 140, 140, 140, 64 ); + /* colors */ + qproperty-darkerColor: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgb(50, 50, 50), stop:0.33 rgb(20, 20, 20), stop:1 rgb(15, 15, 15)); + qproperty-lighterColor: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgb(50, 50, 50), stop:0.33 rgb(40, 40, 40), stop:1 rgb(30, 30, 30)); + qproperty-coarseGridColor: rgba(0, 0, 0, 160); + qproperty-fineGridColor: rgba(0, 0, 0, 80); + qproperty-horizontalColor: rgba(0, 0, 0, 160); + qproperty-embossColor: rgba(140, 140, 140, 64); + + /* line widths */ + qproperty-coarseGridWidth: 2; + qproperty-fineGridWidth: 1; + qproperty-horizontalWidth: 1; + qproperty-embossWidth: 0; + + /* positive offset shifts emboss to the right */ + qproperty-embossOffset: 0; } diff --git a/data/themes/default/style.css b/data/themes/default/style.css index f13ec09d8..83316b9f2 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -388,12 +388,24 @@ lmms--gui--TrackView > QWidget { /* autoscroll, loop, stop behaviour toggle buttons */ -/* track background colors */ +/* track background config */ lmms--gui--TrackContentWidget { + /* colors */ qproperty-darkerColor: #0C0E0F; qproperty-lighterColor: #14151A; - qproperty-gridColor: #262B30; - qproperty-embossColor: rgba( 0, 0, 0, 0 ); + qproperty-coarseGridColor: #3C444C; + qproperty-fineGridColor: #262B30; + qproperty-horizontalColor: #3C444C; + qproperty-embossColor: rgba(0, 0, 0, 0); + + /* line widths */ + qproperty-coarseGridWidth: 2; + qproperty-fineGridWidth: 1; + qproperty-horizontalWidth: 1; + qproperty-embossWidth: 0; + + /* positive offset shifts emboss to the right */ + qproperty-embossOffset: 0; } diff --git a/include/ComboBoxModel.h b/include/ComboBoxModel.h index e90d804e2..7037495ea 100644 --- a/include/ComboBoxModel.h +++ b/include/ComboBoxModel.h @@ -47,11 +47,6 @@ public: { } - ~ComboBoxModel() override - { - clear(); - } - void addItem( QString item, std::unique_ptr loader = nullptr ); void replaceItem(std::size_t index, QString item, std::unique_ptr loader = nullptr); diff --git a/include/SongEditor.h b/include/SongEditor.h index 19d652b4f..98a9096fb 100644 --- a/include/SongEditor.h +++ b/include/SongEditor.h @@ -164,6 +164,7 @@ private: signals: void pixelsPerBarChanged(float); + void proportionalSnapChanged(); } ; diff --git a/include/TrackContainerView.h b/include/TrackContainerView.h index 82d6f993b..9bdcdcab6 100644 --- a/include/TrackContainerView.h +++ b/include/TrackContainerView.h @@ -166,7 +166,7 @@ public slots: protected: - static const int DEFAULT_PIXELS_PER_BAR = 16; + static const int DEFAULT_PIXELS_PER_BAR = 128; void resizeEvent( QResizeEvent * ) override; diff --git a/include/TrackContentWidget.h b/include/TrackContentWidget.h index 7cf236323..f93b0a58d 100644 --- a/include/TrackContentWidget.h +++ b/include/TrackContentWidget.h @@ -49,18 +49,24 @@ class TrackContentWidget : public QWidget, public JournallingObject Q_OBJECT // qproperties for track background gradients - Q_PROPERTY( QBrush darkerColor READ darkerColor WRITE setDarkerColor ) - Q_PROPERTY( QBrush lighterColor READ lighterColor WRITE setLighterColor ) - Q_PROPERTY( QBrush gridColor READ gridColor WRITE setGridColor ) - Q_PROPERTY( QBrush embossColor READ embossColor WRITE setEmbossColor ) + Q_PROPERTY(QBrush darkerColor READ darkerColor WRITE setDarkerColor) + Q_PROPERTY(QBrush lighterColor READ lighterColor WRITE setLighterColor) + Q_PROPERTY(QBrush coarseGridColor READ coarseGridColor WRITE setCoarseGridColor) + Q_PROPERTY(QBrush fineGridColor READ fineGridColor WRITE setFineGridColor) + Q_PROPERTY(QBrush horizontalColor READ horizontalColor WRITE setHorizontalColor) + Q_PROPERTY(QBrush embossColor READ embossColor WRITE setEmbossColor) + + Q_PROPERTY(int coarseGridWidth READ coarseGridWidth WRITE setCoarseGridWidth) + Q_PROPERTY(int fineGridWidth READ fineGridWidth WRITE setFineGridWidth) + Q_PROPERTY(int horizontalWidth READ horizontalWidth WRITE setHorizontalWidth) + Q_PROPERTY(int embossWidth READ embossWidth WRITE setEmbossWidth) + + Q_PROPERTY(int embossOffset READ embossOffset WRITE setEmbossOffset) public: TrackContentWidget( TrackView * parent ); ~TrackContentWidget() override = default; - /*! \brief Updates the background tile pixmap. */ - void updateBackground(); - void addClipView( ClipView * clipv ); void removeClipView( ClipView * clipv ); void removeClipView( int clipNum ) @@ -82,17 +88,37 @@ public: QBrush darkerColor() const; QBrush lighterColor() const; - QBrush gridColor() const; + QBrush coarseGridColor() const; + QBrush fineGridColor() const; + QBrush horizontalColor() const; QBrush embossColor() const; - void setDarkerColor( const QBrush & c ); - void setLighterColor( const QBrush & c ); - void setGridColor( const QBrush & c ); - void setEmbossColor( const QBrush & c); + int coarseGridWidth() const; + int fineGridWidth() const; + int horizontalWidth() const; + int embossWidth() const; + + int embossOffset() const; + + void setDarkerColor(const QBrush & c); + void setLighterColor(const QBrush & c); + void setCoarseGridColor(const QBrush & c); + void setFineGridColor(const QBrush & c); + void setHorizontalColor(const QBrush & c); + void setEmbossColor(const QBrush & c); + + void setCoarseGridWidth(int c); + void setFineGridWidth(int c); + void setHorizontalWidth(int c); + void setEmbossWidth(int c); + + void setEmbossOffset(int c); public slots: void update(); void changePosition( const lmms::TimePos & newPos = TimePos( -1 ) ); + /*! \brief Updates the background tile pixmap. */ + void updateBackground(); protected: enum class ContextMenuAction @@ -140,8 +166,17 @@ private: // qproperty fields QBrush m_darkerColor; QBrush m_lighterColor; - QBrush m_gridColor; + QBrush m_coarseGridColor; + QBrush m_fineGridColor; + QBrush m_horizontalColor; QBrush m_embossColor; + + int m_coarseGridWidth; + int m_fineGridWidth; + int m_horizontalWidth; + int m_embossWidth; + + int m_embossOffset; } ; diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index d486be5c8..9ba9ac083 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -63,7 +63,7 @@ namespace lmms::gui namespace { -constexpr int MIN_PIXELS_PER_BAR = 2; +constexpr int MIN_PIXELS_PER_BAR = 4; constexpr int MAX_PIXELS_PER_BAR = 400; constexpr int ZOOM_STEPS = 200; @@ -263,7 +263,7 @@ SongEditor::SongEditor( Song * song ) : m_snappingModel->addItem(QString("1/%1 Bar").arg(1 / bars)); } } - m_snappingModel->setInitValue( m_snappingModel->findText( "1 Bar" ) ); + m_snappingModel->setInitValue( m_snappingModel->findText( "1/4 Bar" ) ); setFocusPolicy( Qt::StrongFocus ); setFocus(); @@ -447,6 +447,8 @@ void SongEditor::toggleProportionalSnap() { m_proportionalSnap = !m_proportionalSnap; m_timeLine->setSnapSize(getSnapSize()); + + emit proportionalSnapChanged(); } diff --git a/src/gui/tracks/TrackContentWidget.cpp b/src/gui/tracks/TrackContentWidget.cpp index 619eff831..0c7b3f2fc 100644 --- a/src/gui/tracks/TrackContentWidget.cpp +++ b/src/gui/tracks/TrackContentWidget.cpp @@ -43,14 +43,15 @@ #include "ClipView.h" #include "TrackView.h" - namespace lmms::gui { /*! Alternate between a darker and a lighter background color every 4 bars */ const int BARS_PER_GROUP = 4; - +/* Lines between bars will disappear if zoomed too far out (i.e + if there are less than 4 pixels between lines)*/ +const int MIN_PIXELS_BETWEEN_LINES = 4; /*! \brief Create a new trackContentWidget * @@ -65,7 +66,9 @@ TrackContentWidget::TrackContentWidget( TrackView * parent ) : m_trackView( parent ), m_darkerColor( Qt::SolidPattern ), m_lighterColor( Qt::SolidPattern ), - m_gridColor( Qt::SolidPattern ), + m_coarseGridColor( Qt::SolidPattern ), + m_fineGridColor( Qt::SolidPattern ), + m_horizontalColor( Qt::SolidPattern ), m_embossColor( Qt::SolidPattern ) { setAcceptDrops( true ); @@ -74,6 +77,14 @@ TrackContentWidget::TrackContentWidget( TrackView * parent ) : SIGNAL( positionChanged( const lmms::TimePos& ) ), this, SLOT( changePosition( const lmms::TimePos& ) ) ); + // Update background if snap size changes + connect(getGUI()->songEditor()->m_editor->snappingModel(), &Model::dataChanged, + this, &TrackContentWidget::updateBackground); + + // Also update background if proportional snap is enabled/disabled + connect(getGUI()->songEditor()->m_editor, &SongEditor::proportionalSnapChanged, + this, &TrackContentWidget::updateBackground); + setStyle( QApplication::style() ); updateBackground(); @@ -82,16 +93,30 @@ TrackContentWidget::TrackContentWidget( TrackView * parent ) : - - - void TrackContentWidget::updateBackground() -{ +{ + // use snapSize to determine number of lines to draw + float snapSize = getGUI()->songEditor()->m_editor->getSnapSize(); + const TrackContainerView * tcv = m_trackView->trackContainerView(); // Assume even-pixels-per-bar. Makes sense, should be like this anyways int ppb = static_cast( tcv->pixelsPerBar() ); + // Coarse grid appears every bar (less frequently if quantization > 1 bar) + float coarseGridResolution = (snapSize >= 1) ? snapSize : 1; + // Fine grid appears within bars + float fineGridResolution = snapSize; + // Increase fine grid resolution (size between lines) if it results in less than + // 4 pixels between each line to avoid cluttering + float pixelsBetweenLines = ppb * snapSize; + if (pixelsBetweenLines < MIN_PIXELS_BETWEEN_LINES) { + // Scale fineGridResolution so that there are enough pixels between lines + // scaleFactor should be a power of 2 + int scaleFactor = 1 << static_cast( std::ceil( std::log2( MIN_PIXELS_BETWEEN_LINES / pixelsBetweenLines ) ) ); + fineGridResolution *= scaleFactor; + } + int w = ppb * BARS_PER_GROUP; int h = height(); m_background = QPixmap( w * 2, height() ); @@ -101,22 +126,29 @@ void TrackContentWidget::updateBackground() pmp.fillRect( w, 0, w , h, lighterColor() ); // draw lines - // vertical lines - pmp.setPen( QPen( gridColor(), 1 ) ); - for( float x = 0; x < w * 2; x += ppb ) + // draw fine grid + pmp.setPen( QPen( fineGridColor(), fineGridWidth() ) ); + for (float x = 0; x < w * 2; x += ppb * fineGridResolution) { pmp.drawLine( QLineF( x, 0.0, x, h ) ); } - pmp.setPen( QPen( embossColor(), 1 ) ); - for( float x = 1.0; x < w * 2; x += ppb ) + // draw coarse grid + pmp.setPen( QPen( coarseGridColor(), coarseGridWidth() ) ); + for (float x = 0; x < w * 2; x += ppb * coarseGridResolution) { pmp.drawLine( QLineF( x, 0.0, x, h ) ); } - // horizontal line - pmp.setPen( QPen( gridColor(), 1 ) ); - pmp.drawLine( 0, h-1, w*2, h-1 ); + pmp.setPen( QPen( embossColor(), embossWidth() ) ); + for (float x = (coarseGridWidth() + embossOffset()); x < w * 2; x += ppb * coarseGridResolution) + { + pmp.drawLine( QLineF( x, 0.0, x, h ) ); + } + + // draw horizontal line + pmp.setPen( QPen( horizontalColor(), horizontalWidth() ) ); + pmp.drawLine(0, h - (horizontalWidth() + 1) / 2, w * 2, h - (horizontalWidth() + 1) / 2); pmp.end(); @@ -690,13 +722,41 @@ QBrush TrackContentWidget::lighterColor() const { return m_lighterColor; } //! \brief CSS theming qproperty access method -QBrush TrackContentWidget::gridColor() const -{ return m_gridColor; } +QBrush TrackContentWidget::coarseGridColor() const +{ return m_coarseGridColor; } + +//! \brief CSS theming qproperty access method +QBrush TrackContentWidget::fineGridColor() const +{ return m_fineGridColor; } + +//! \brief CSS theming qproperty access method +QBrush TrackContentWidget::horizontalColor() const +{ return m_horizontalColor; } //! \brief CSS theming qproperty access method QBrush TrackContentWidget::embossColor() const { return m_embossColor; } +//! \brief CSS theming qproperty access method +int TrackContentWidget::coarseGridWidth() const +{ return m_coarseGridWidth; } + +//! \brief CSS theming qproperty access method +int TrackContentWidget::fineGridWidth() const +{ return m_fineGridWidth; } + +//! \brief CSS theming qproperty access method +int TrackContentWidget::horizontalWidth() const +{ return m_horizontalWidth; } + +//! \brief CSS theming qproperty access method +int TrackContentWidget::embossWidth() const +{ return m_embossWidth; } + +//! \brief CSS theming qproperty access method +int TrackContentWidget::embossOffset() const +{ return m_embossOffset; } + //! \brief CSS theming qproperty access method void TrackContentWidget::setDarkerColor( const QBrush & c ) { m_darkerColor = c; } @@ -706,12 +766,39 @@ void TrackContentWidget::setLighterColor( const QBrush & c ) { m_lighterColor = c; } //! \brief CSS theming qproperty access method -void TrackContentWidget::setGridColor( const QBrush & c ) -{ m_gridColor = c; } +void TrackContentWidget::setCoarseGridColor( const QBrush & c ) +{ m_coarseGridColor = c; } + +//! \brief CSS theming qproperty access method +void TrackContentWidget::setFineGridColor( const QBrush & c ) +{ m_fineGridColor = c; } + +//! \brief CSS theming qproperty access method +void TrackContentWidget::setHorizontalColor( const QBrush & c ) +{ m_horizontalColor = c; } //! \brief CSS theming qproperty access method void TrackContentWidget::setEmbossColor( const QBrush & c ) { m_embossColor = c; } +//! \brief CSS theming qproperty access method +void TrackContentWidget::setCoarseGridWidth(int c) +{ m_coarseGridWidth = c; } + +//! \brief CSS theming qproperty access method +void TrackContentWidget::setFineGridWidth(int c) +{ m_fineGridWidth = c; } + +//! \brief CSS theming qproperty access method +void TrackContentWidget::setHorizontalWidth(int c) +{ m_horizontalWidth = c; } + +//! \brief CSS theming qproperty access method +void TrackContentWidget::setEmbossWidth(int c) +{ m_embossWidth = c; } + +//! \brief CSS theming qproperty access method +void TrackContentWidget::setEmbossOffset(int c) +{ m_embossOffset = c; } } // namespace lmms::gui From 75d6be2eac329efab8fb20469984c67e80240f19 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 20 May 2024 18:46:52 +0200 Subject: [PATCH 172/191] Fix missing initialization (#7271) Fix the missing initialization of some variables in `TrackContentWidget`. This led to some performances issues when the widget was painted because a for loop was executed for which the variable started at a very large negative number and was then incremented. --- src/gui/tracks/TrackContentWidget.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gui/tracks/TrackContentWidget.cpp b/src/gui/tracks/TrackContentWidget.cpp index 0c7b3f2fc..e205a0c00 100644 --- a/src/gui/tracks/TrackContentWidget.cpp +++ b/src/gui/tracks/TrackContentWidget.cpp @@ -69,7 +69,12 @@ TrackContentWidget::TrackContentWidget( TrackView * parent ) : m_coarseGridColor( Qt::SolidPattern ), m_fineGridColor( Qt::SolidPattern ), m_horizontalColor( Qt::SolidPattern ), - m_embossColor( Qt::SolidPattern ) + m_embossColor( Qt::SolidPattern ), + m_coarseGridWidth(2), + m_fineGridWidth(1), + m_horizontalWidth(1), + m_embossWidth(0), + m_embossOffset(0) { setAcceptDrops( true ); From 32fe3e50e7d951a456764dc1692b83f812bb2a87 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Tue, 21 May 2024 11:32:28 -0400 Subject: [PATCH 173/191] Add "natural" scrolling support for trackpads (#5510) Adds QWheelEvent::inverted() support to spinboxes, knobs, sliders --- src/gui/clips/MidiClipView.cpp | 6 +++--- src/gui/editors/PianoRoll.cpp | 3 ++- src/gui/widgets/ComboBox.cpp | 3 ++- src/gui/widgets/Fader.cpp | 10 ++-------- src/gui/widgets/FloatModelEditorBase.cpp | 5 +++++ src/gui/widgets/LcdSpinBox.cpp | 4 +++- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 0a6fece31..b735913e4 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -332,7 +332,8 @@ void MidiClipView::wheelEvent(QWheelEvent * we) } Note * n = m_clip->noteAtStep( step ); - if(!n && we->angleDelta().y() > 0) + const int direction = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); + if(!n && direction > 0) { n = m_clip->addStepNote( step ); n->setVolume( 0 ); @@ -340,8 +341,7 @@ void MidiClipView::wheelEvent(QWheelEvent * we) if( n != nullptr ) { int vol = n->getVolume(); - - if(we->angleDelta().y() > 0) + if(direction > 0) { n->setVolume( qMin( 100, vol + 5 ) ); } diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 2e99aab4d..4723004e8 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -3774,7 +3774,8 @@ void PianoRoll::wheelEvent(QWheelEvent * we ) } if( nv.size() > 0 ) { - const int step = we->angleDelta().y() > 0 ? 1 : -1; + const int step = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); + if( m_noteEditMode == NoteEditMode::Volume ) { for ( Note * n : nv ) diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index eb019876a..0daae1b24 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -220,7 +220,8 @@ void ComboBox::wheelEvent( QWheelEvent* event ) { if( model() ) { - model()->setInitValue(model()->value() + ((event->angleDelta().y() < 0) ? 1 : -1)); + const int direction = (event->angleDelta().y() < 0 ? 1 : -1) * (event->inverted() ? -1 : 1); + model()->setInitValue(model()->value() + direction); update(); event->accept(); } diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index 1eb06756a..d2d7c1c2e 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -193,15 +193,9 @@ void Fader::mouseReleaseEvent(QMouseEvent* mouseEvent) void Fader::wheelEvent (QWheelEvent* ev) { ev->accept(); + const int direction = (ev->angleDelta().y() > 0 ? 1 : -1) * (ev->inverted() ? -1 : 1); - if (ev->angleDelta().y() > 0) - { - model()->incValue(1); - } - else - { - model()->incValue(-1); - } + model()->incValue(direction); updateTextFloat(); s_textFloat->setVisibilityTimeOut(1000); } diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp index dd6be8958..ebd0d3d9d 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -326,6 +326,11 @@ void FloatModelEditorBase::wheelEvent(QWheelEvent * we) } } + // Handle "natural" scrolling, which is common on trackpads and touch devices + if (we->inverted()) { + direction = -direction; + } + // Compute the number of steps but make sure that we always do at least one step const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); const int inc = direction * stepMult; diff --git a/src/gui/widgets/LcdSpinBox.cpp b/src/gui/widgets/LcdSpinBox.cpp index b53d7ddb5..3f12360cc 100644 --- a/src/gui/widgets/LcdSpinBox.cpp +++ b/src/gui/widgets/LcdSpinBox.cpp @@ -141,7 +141,9 @@ void LcdSpinBox::mouseReleaseEvent(QMouseEvent*) void LcdSpinBox::wheelEvent(QWheelEvent * we) { we->accept(); - model()->setValue(model()->value() + ((we->angleDelta().y() > 0) ? 1 : -1) * model()->step()); + const int direction = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); + + model()->setValue(model()->value() + direction * model()->step()); emit manualChange(); } From fa5f2aa41a6f2e8e1dec412df8ad430806193b7d Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Thu, 23 May 2024 00:34:00 +0200 Subject: [PATCH 174/191] Lv2Proc: Fix all code-style issues with {} --- src/core/lv2/Lv2Proc.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index cc8e333be..7dfdbc767 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -446,7 +446,8 @@ void Lv2Proc::initPlugin() { const auto iface = static_cast( lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface)); - if (iface) { + if (iface) + { m_worker->setHandle(lilv_instance_get_handle(m_instance)); m_worker->setInterface(iface); } @@ -531,7 +532,8 @@ void Lv2Proc::initPluginSpecificFeatures() // worker (if plugin has worker extension) Lv2Manager* mgr = Engine::getLv2Manager(); - if (lilv_plugin_has_extension_data(m_plugin, mgr->uri(LV2_WORKER__interface).get())) { + if (lilv_plugin_has_extension_data(m_plugin, mgr->uri(LV2_WORKER__interface).get())) + { bool threaded = !Engine::audioEngine()->renderOnly(); m_worker.emplace(&m_workLock, threaded); m_features[LV2_WORKER__schedule] = m_worker->feature(); @@ -685,7 +687,8 @@ void Lv2Proc::createPort(std::size_t portNum) AutoLilvNode rszMinimumSize = mgr->uri(LV2_RESIZE_PORT__minimumSize); AutoLilvNodes minSizeV(lilv_port_get_value(m_plugin, lilvPort, rszMinimumSize.get())); LilvNode* minSize = minSizeV ? lilv_nodes_get_first(minSizeV.get()) : nullptr; - if (minSize && lilv_node_is_int(minSize)) { + if (minSize && lilv_node_is_int(minSize)) + { minimumSize = std::max(minimumSize, lilv_node_as_int(minSize)); } } @@ -847,7 +850,8 @@ void Lv2Proc::dumpPort(std::size_t num) { struct DumpPortDetail : public Lv2Ports::ConstVisitor { - void visit(const Lv2Ports::Control& ctrl) override { + void visit(const Lv2Ports::Control& ctrl) override + { qDebug() << " control port"; // output ports may be uninitialized yet, only print inputs if (ctrl.m_flow == Lv2Ports::Flow::Input) @@ -855,7 +859,8 @@ void Lv2Proc::dumpPort(std::size_t num) qDebug() << " value:" << ctrl.m_val; } } - void visit(const Lv2Ports::Audio& audio) override { + void visit(const Lv2Ports::Audio& audio) override + { qDebug() << (audio.isSideChain() ? " audio port (sidechain)" : " audio port"); qDebug() << " buffer size:" << audio.bufferSize(); From c66af602ad105faefb8ee9f6f3f555a6c2141221 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Wed, 22 May 2024 23:52:53 -0400 Subject: [PATCH 175/191] Fix fftw linking when cross-compiling (#7276) --- plugins/LadspaEffect/swh/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/LadspaEffect/swh/CMakeLists.txt b/plugins/LadspaEffect/swh/CMakeLists.txt index b76c95931..3847429d3 100644 --- a/plugins/LadspaEffect/swh/CMakeLists.txt +++ b/plugins/LadspaEffect/swh/CMakeLists.txt @@ -112,6 +112,6 @@ TARGET_LINK_LIBRARIES(se4_1883 rms db) ADD_LIBRARY(pitchscale STATIC ladspa/util/pitchscale.c) SET_TARGET_PROPERTIES(pitchscale PROPERTIES COMPILE_FLAGS "${PIC_FLAGS}") -TARGET_LINK_LIBRARIES(pitchscale -lfftw3f) +TARGET_LINK_LIBRARIES(pitchscale ${FFTW3F_LIBRARIES}) TARGET_LINK_LIBRARIES(pitch_scale_1193 pitchscale) TARGET_LINK_LIBRARIES(pitch_scale_1194 pitchscale) From bd2362a8c0aae1efa5a5931d1af6a221ce6efcbf Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Thu, 23 May 2024 01:25:18 -0400 Subject: [PATCH 176/191] FindWine improvements (#7268) Improve WineHQ detection Closes #7169 --------- Co-authored-by: Rossmaxx --- cmake/modules/FindWine.cmake | 80 ++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/cmake/modules/FindWine.cmake b/cmake/modules/FindWine.cmake index aabb5ef78..19a0cffbb 100644 --- a/cmake/modules/FindWine.cmake +++ b/cmake/modules/FindWine.cmake @@ -2,8 +2,15 @@ # Once done this will define # # WINE_FOUND - System has wine -# WINE_INCLUDE_DIRS - The wine include directories -# WINE_DEFINITIONS - Compiler switches required for using wine +# +# WINE_INCLUDE_DIR - Wine include directory +# WINE_BUILD - Path to winebuild +# WINE_CXX - Path to wineg++ +# WINE_GCC - Path to winegcc +# WINE_32_LIBRARY_DIRS - Path(s) to 32-bit wine libs +# WINE_32_FLAGS - 32-bit linker flags +# WINE_64_LIBRARY_DIRS - Path(s) to 64-bit wine libs +# WINE_64_FLAGS - 64-bit linker flags # MACRO(_findwine_find_flags output expression result) @@ -23,17 +30,49 @@ MACRO(_regex_replace_foreach EXPRESSION REPLACEMENT RESULT INPUT) ENDFOREACH() ENDMACRO() -LIST(APPEND CMAKE_PREFIX_PATH /opt/wine-stable /opt/wine-devel /opt/wine-staging /usr/lib/wine/) +# Prefer newest wine first +list(APPEND WINE_LOCATIONS + /opt/wine-staging + /opt/wine-devel + /opt/wine-stable + /usr/lib/wine) -FIND_PROGRAM(WINE_CXX - NAMES wineg++ winegcc winegcc64 winegcc32 winegcc-stable - PATHS /usr/lib/wine +# Prepare bin search +foreach(_loc ${WINE_LOCATIONS}) + if(_loc STREQUAL /usr/lib/wine) + # /usr/lib/wine doesn't have a "bin" + list(APPEND WINE_CXX_LOCATIONS "${_loc}") + else() + # expect "bin" + list(APPEND WINE_CXX_LOCATIONS "${_loc}/bin") + endif() +endforeach() +# Fallback +list(APPEND WINE_CXX_LOCATIONS "/usr/bin") + +# Prefer most-common to least common +FIND_PROGRAM(WINE_CXX NAMES + wineg++ + wineg++-stable + PATHS + ${WINE_CXX_LOCATIONS} + NO_DEFAULT_PATH ) -FIND_PROGRAM(WINE_BUILD NAMES winebuild) + +FIND_PROGRAM(WINE_GCC NAMES + winegcc + winegcc-stable + PATHS + ${WINE_CXX_LOCATIONS} + NO_DEFAULT_PATH +) + +FIND_PROGRAM(WINE_BUILD NAMES winebuild PATHS ${WINE_CXX_LOCATIONS} NO_DEFAULT_PATH) # Detect wine paths and handle linking problems IF(WINE_CXX) - execute_process(COMMAND ${WINE_CXX} -m32 -v /dev/zero OUTPUT_VARIABLE WINEBUILD_OUTPUT_32) - execute_process(COMMAND ${WINE_CXX} -m64 -v /dev/zero OUTPUT_VARIABLE WINEBUILD_OUTPUT_64) + # call wineg++ to obtain implied includes and libs + execute_process(COMMAND ${WINE_CXX} -m32 -v /dev/zero OUTPUT_VARIABLE WINEBUILD_OUTPUT_32 ERROR_QUIET) + execute_process(COMMAND ${WINE_CXX} -m64 -v /dev/zero OUTPUT_VARIABLE WINEBUILD_OUTPUT_64 ERROR_QUIET) _findwine_find_flags("${WINEBUILD_OUTPUT_32}" "^-isystem/usr/include$" BUGGED_WINEGCC) _findwine_find_flags("${WINEBUILD_OUTPUT_32}" "^-isystem" WINEGCC_INCLUDE_DIR) _findwine_find_flags("${WINEBUILD_OUTPUT_32}" "libwinecrt0\\.a.*" WINECRT_32) @@ -42,6 +81,9 @@ IF(WINE_CXX) _regex_replace_foreach("/wine/windows$" "" WINE_INCLUDE_HINT "${WINE_INCLUDE_HINT}") STRING(REGEX REPLACE "wine/libwinecrt0\\.a.*" "" WINE_32_LIBRARY_DIR "${WINECRT_32}") STRING(REGEX REPLACE "wine/libwinecrt0\\.a.*" "" WINE_64_LIBRARY_DIR "${WINECRT_64}") + # Handle winehq + STRING(REGEX REPLACE "/libwinecrt0\\.a.*" "/" WINE_32_LIBRARY_DIR "${WINE_32_LIBRARY_DIR}") + STRING(REGEX REPLACE "/libwinecrt0\\.a.*" "/" WINE_64_LIBRARY_DIR "${WINE_64_LIBRARY_DIR}") IF(BUGGED_WINEGCC) MESSAGE(WARNING "Your winegcc is unusable due to https://bugs.winehq.org/show_bug.cgi?id=46293,\n @@ -98,7 +140,11 @@ find_package_handle_standard_args(Wine DEFAULT_MSG WINE_CXX WINE_INCLUDE_DIRS) mark_as_advanced(WINE_INCLUDE_DIR WINE_LIBRARY WINE_CXX WINE_BUILD) IF(WINE_32_LIBRARY_DIR) - IF(WINE_32_LIBRARY_DIR MATCHES "wine*/lib") + IF(WINE_32_LIBRARY_DIR MATCHES "^/opt/wine-.*") + # winehq uses a singular lib directory + SET(WINE_32_FLAGS "-L${WINE_32_LIBRARY_DIR}") + SET(WINE_32_LIBRARY_DIRS "${WINE_32_LIBRARY_DIR}") + ELSEIF(WINE_32_LIBRARY_DIR MATCHES "wine*/lib") SET(WINE_32_FLAGS "-L${WINE_32_LIBRARY_DIR} -L${WINE_32_LIBRARY_DIR}../") SET(WINE_32_LIBRARY_DIRS "${WINE_32_LIBRARY_DIR}:${WINE_32_LIBRARY_DIR}/..") ELSE() @@ -108,7 +154,11 @@ IF(WINE_32_LIBRARY_DIR) ENDIF() IF(WINE_64_LIBRARY_DIR) - IF(WINE_64_LIBRARY_DIR MATCHES "wine*/lib") + IF(WINE_32_LIBRARY_DIR MATCHES "^/opt/wine-.*") + # winehq uses a singular lib directory + SET(WINE_64_FLAGS "-L${WINE_64_LIBRARY_DIR}") + SET(WINE_64_LIBRARY_DIRS "${WINE_64_LIBRARY_DIR}") + ELSEIF(WINE_64_LIBRARY_DIR MATCHES "wine*/lib") SET(WINE_64_FLAGS "-L${WINE_64_LIBRARY_DIR} -L${WINE_64_LIBRARY_DIR}../") SET(WINE_64_LIBRARY_DIRS "${WINE_64_LIBRARY_DIR}:${WINE_64_LIBRARY_DIR}/..") ELSE() @@ -117,6 +167,12 @@ IF(WINE_64_LIBRARY_DIR) ENDIF() ENDIF() -# Create winegcc wrapper +message(STATUS " WINE_INCLUDE_DIR: ${WINE_INCLUDE_DIR}") +message(STATUS " WINE_CXX: ${WINE_CXX}") +message(STATUS " WINE_GCC: ${WINE_GCC}") +message(STATUS " WINE_32_FLAGS: ${WINE_32_FLAGS}") +message(STATUS " WINE_64_FLAGS: ${WINE_64_FLAGS}") + +# Create winegcc (technically, wineg++) wrapper configure_file(${CMAKE_CURRENT_LIST_DIR}/winegcc_wrapper.in winegcc_wrapper @ONLY) SET(WINEGCC "${CMAKE_CURRENT_BINARY_DIR}/winegcc_wrapper") From b803e92d63e2921eb9618373bf7790dbbdf66d1c Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Thu, 23 May 2024 17:38:47 +0530 Subject: [PATCH 177/191] Add menu option and hotkeys to move controllers/effects (#7139) * added controller rack modules * remove this-> from setFocusPolicy() and obsolete comment Co-authored-by: saker * Use std::swap Co-authored-by: saker * some more cleanup Co-authored-by: saker * Replace slots with function pointers Co-authored-by: saker * Apply fixes --------- Co-authored-by: ejaaskel Co-authored-by: saker --- include/ControllerRackView.h | 8 +++--- include/ControllerView.h | 8 ++++-- include/EffectRackView.h | 7 +++-- include/EffectView.h | 7 +++-- src/gui/ControllerRackView.cpp | 47 +++++++++++++++++++++++++++++----- src/gui/ControllerView.cpp | 20 +++++++++------ src/gui/EffectRackView.cpp | 24 ++++++++++++----- src/gui/EffectView.cpp | 7 ++--- 8 files changed, 91 insertions(+), 37 deletions(-) diff --git a/include/ControllerRackView.h b/include/ControllerRackView.h index 303cc2b40..93d1e8438 100644 --- a/include/ControllerRackView.h +++ b/include/ControllerRackView.h @@ -65,9 +65,11 @@ public: public slots: - void deleteController( lmms::gui::ControllerView * _view ); - void onControllerAdded( lmms::Controller * ); - void onControllerRemoved( lmms::Controller * ); + void deleteController(ControllerView* view); + void moveUp(ControllerView* view); + void moveDown(ControllerView* view); + void addController(Controller* controller); + void removeController(Controller* controller); protected: void closeEvent( QCloseEvent * _ce ) override; diff --git a/include/ControllerView.h b/include/ControllerView.h index d1ba533a1..9b442672d 100644 --- a/include/ControllerView.h +++ b/include/ControllerView.h @@ -63,12 +63,16 @@ public: public slots: void editControls(); - void deleteController(); + void removeController(); void closeControls(); void renameController(); + void moveUp(); + void moveDown(); signals: - void deleteController( lmms::gui::ControllerView * _view ); + void movedUp(ControllerView* view); + void movedDown(ControllerView* view); + void removedController(ControllerView* view); protected: diff --git a/include/EffectRackView.h b/include/EffectRackView.h index a1e21be09..4a90c6b7a 100644 --- a/include/EffectRackView.h +++ b/include/EffectRackView.h @@ -53,10 +53,9 @@ public: public slots: void clearViews(); - void moveUp( lmms::gui::EffectView* view ); - void moveDown( lmms::gui::EffectView* view ); - void deletePlugin( lmms::gui::EffectView* view ); - + void moveUp(EffectView* view); + void moveDown(EffectView* view); + void deletePlugin(EffectView* view); private slots: virtual void update(); diff --git a/include/EffectView.h b/include/EffectView.h index e90700952..805e4a427 100644 --- a/include/EffectView.h +++ b/include/EffectView.h @@ -77,10 +77,9 @@ public slots: signals: - void moveUp( lmms::gui::EffectView * _plugin ); - void moveDown( lmms::gui::EffectView * _plugin ); - void deletePlugin( lmms::gui::EffectView * _plugin ); - + void movedUp(EffectView* view); + void movedDown(EffectView* view); + void deletedPlugin(EffectView* view); protected: void contextMenuEvent( QContextMenuEvent * _me ) override; diff --git a/src/gui/ControllerRackView.cpp b/src/gui/ControllerRackView.cpp index 54c325dc6..e7d2efebd 100644 --- a/src/gui/ControllerRackView.cpp +++ b/src/gui/ControllerRackView.cpp @@ -24,6 +24,7 @@ */ #include +#include #include #include #include @@ -68,8 +69,8 @@ ControllerRackView::ControllerRackView() : this, SLOT(addController())); Song * song = Engine::getSong(); - connect( song, SIGNAL(controllerAdded(lmms::Controller*)), SLOT(onControllerAdded(lmms::Controller*))); - connect( song, SIGNAL(controllerRemoved(lmms::Controller*)), SLOT(onControllerRemoved(lmms::Controller*))); + connect(song, &Song::controllerAdded, this, qOverload(&ControllerRackView::addController)); + connect(song, &Song::controllerRemoved, this, &ControllerRackView::removeController); auto layout = new QVBoxLayout(); layout->addWidget( m_scrollArea ); @@ -132,17 +133,51 @@ void ControllerRackView::deleteController( ControllerView * _view ) song->removeController( c ); } +void ControllerRackView::moveUp(ControllerView* view) +{ + if (view == m_controllerViews.first()) { return; } + const auto storedView = std::find(m_controllerViews.begin(), m_controllerViews.end(), view); + assert(storedView != m_controllerViews.end()); + const auto index = std::distance(m_controllerViews.begin(), storedView); -void ControllerRackView::onControllerAdded( Controller * controller ) + std::swap(m_controllerViews[index - 1], m_controllerViews[index]); + m_scrollAreaLayout->removeWidget(view); + m_scrollAreaLayout->insertWidget(index - 1, view); +} + +void ControllerRackView::moveDown(ControllerView* view) +{ + if (view == m_controllerViews.last()) { return; } + + const auto storedView = std::find(m_controllerViews.begin(), m_controllerViews.end(), view); + assert(storedView != m_controllerViews.end()); + moveUp(*std::next(storedView)); +} + +void ControllerRackView::addController(Controller* controller) { QWidget * scrollAreaWidget = m_scrollArea->widget(); auto controllerView = new ControllerView(controller, scrollAreaWidget); - connect( controllerView, SIGNAL(deleteController(lmms::gui::ControllerView*)), - this, SLOT(deleteController(lmms::gui::ControllerView*)), Qt::QueuedConnection ); + connect(controllerView, &ControllerView::movedUp, this, &ControllerRackView::moveUp); + connect(controllerView, &ControllerView::movedDown, this, &ControllerRackView::moveDown); + connect(controllerView, &ControllerView::removedController, this, &ControllerRackView::deleteController, Qt::QueuedConnection); + + auto moveUpAction = new QAction(controllerView); + moveUpAction->setShortcut(Qt::Key_Up | Qt::AltModifier); + moveUpAction->setShortcutContext(Qt::WidgetShortcut); + connect(moveUpAction, &QAction::triggered, controllerView, &ControllerView::moveUp); + controllerView->addAction(moveUpAction); + + auto moveDownAction = new QAction(controllerView); + moveDownAction->setShortcut(Qt::Key_Down | Qt::AltModifier); + moveDownAction->setShortcutContext(Qt::WidgetShortcut); + connect(moveDownAction, &QAction::triggered, controllerView, &ControllerView::moveDown); + controllerView->addAction(moveDownAction); + m_controllerViews.append( controllerView ); m_scrollAreaLayout->insertWidget( m_nextIndex, controllerView ); @@ -153,7 +188,7 @@ void ControllerRackView::onControllerAdded( Controller * controller ) -void ControllerRackView::onControllerRemoved( Controller * removedController ) +void ControllerRackView::removeController(Controller* removedController) { ControllerView * viewOfRemovedController = 0; diff --git a/src/gui/ControllerView.cpp b/src/gui/ControllerView.cpp index d32e8d49c..7f7c4729c 100644 --- a/src/gui/ControllerView.cpp +++ b/src/gui/ControllerView.cpp @@ -53,6 +53,7 @@ ControllerView::ControllerView( Controller * _model, QWidget * _parent ) : { this->setFrameStyle( QFrame::StyledPanel ); this->setFrameShadow( QFrame::Raised ); + setFocusPolicy(Qt::StrongFocus); auto vBoxLayout = new QVBoxLayout(this); @@ -132,11 +133,11 @@ void ControllerView::closeControls() m_show = true; } +void ControllerView::moveUp() { emit movedUp(this); } -void ControllerView::deleteController() -{ - emit( deleteController( this ) ); -} +void ControllerView::moveDown() { emit movedDown(this); } + +void ControllerView::removeController() { emit removedController(this); } void ControllerView::renameController() { @@ -173,10 +174,13 @@ void ControllerView::modelChanged() void ControllerView::contextMenuEvent( QContextMenuEvent * ) { - QPointer contextMenu = new CaptionMenu( model()->displayName(), this ); - contextMenu->addAction( embed::getIconPixmap( "cancel" ), - tr( "&Remove this controller" ), - this, SLOT(deleteController())); + Controller* c = castModel(); + QPointer contextMenu = new CaptionMenu(c->name(), this); + contextMenu->addAction(embed::getIconPixmap("arp_up"), tr("Move &up"), this, &ControllerView::moveUp); + contextMenu->addAction(embed::getIconPixmap("arp_down"), tr("Move &down"), this, &ControllerView::moveDown); + contextMenu->addSeparator(); + contextMenu->addAction( + embed::getIconPixmap("cancel"), tr("&Remove this controller"), this, &ControllerView::removeController); contextMenu->addAction( tr("Re&name this controller"), this, SLOT(renameController())); contextMenu->addSeparator(); contextMenu->exec( QCursor::pos() ); diff --git a/src/gui/EffectRackView.cpp b/src/gui/EffectRackView.cpp index aa790d74d..478e117fe 100644 --- a/src/gui/EffectRackView.cpp +++ b/src/gui/EffectRackView.cpp @@ -24,6 +24,7 @@ */ #include +#include #include #include #include @@ -170,13 +171,22 @@ void EffectRackView::update() if( i >= m_effectViews.size() ) { auto view = new EffectView(effect, w); - connect( view, SIGNAL(moveUp(lmms::gui::EffectView*)), - this, SLOT(moveUp(lmms::gui::EffectView*))); - connect( view, SIGNAL(moveDown(lmms::gui::EffectView*)), - this, SLOT(moveDown(lmms::gui::EffectView*))); - connect( view, SIGNAL(deletePlugin(lmms::gui::EffectView*)), - this, SLOT(deletePlugin(lmms::gui::EffectView*)), - Qt::QueuedConnection ); + connect(view, &EffectView::movedUp, this, &EffectRackView::moveUp); + connect(view, &EffectView::movedDown, this, &EffectRackView::moveDown); + connect(view, &EffectView::deletedPlugin, this, &EffectRackView::deletePlugin, Qt::QueuedConnection); + + QAction* moveUpAction = new QAction(view); + moveUpAction->setShortcut(Qt::Key_Up | Qt::AltModifier); + moveUpAction->setShortcutContext(Qt::WidgetShortcut); + connect(moveUpAction, &QAction::triggered, view, &EffectView::moveUp); + view->addAction(moveUpAction); + + QAction* moveDownAction = new QAction(view); + moveDownAction->setShortcut(Qt::Key_Down | Qt::AltModifier); + moveDownAction->setShortcutContext(Qt::WidgetShortcut); + connect(moveDownAction, &QAction::triggered, view, &EffectView::moveDown); + view->addAction(moveDownAction); + view->show(); m_effectViews.append( view ); if( i < view_map.size() ) diff --git a/src/gui/EffectView.cpp b/src/gui/EffectView.cpp index 76010232a..6f2b984c3 100644 --- a/src/gui/EffectView.cpp +++ b/src/gui/EffectView.cpp @@ -53,6 +53,7 @@ EffectView::EffectView( Effect * _model, QWidget * _parent ) : m_dragging(false) { setFixedSize(EffectView::DEFAULT_WIDTH, EffectView::DEFAULT_HEIGHT); + setFocusPolicy(Qt::StrongFocus); // Disable effects that are of type "DummyEffect" bool isEnabled = !dynamic_cast( effect() ); @@ -162,7 +163,7 @@ void EffectView::editControls() void EffectView::moveUp() { - emit moveUp( this ); + emit movedUp(this); } @@ -170,14 +171,14 @@ void EffectView::moveUp() void EffectView::moveDown() { - emit moveDown( this ); + emit movedDown(this); } void EffectView::deletePlugin() { - emit deletePlugin( this ); + emit deletedPlugin(this); } From e9848dbbbb839e9267e67968ed4c9cb3b61c7e0a Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Thu, 23 May 2024 17:46:15 +0530 Subject: [PATCH 178/191] A few accessibility changes in default theme (#7202) * some css tweaks for accessibility * suggestions from review * classic theme focus * fix bug where button color disappears on focus * More scrollbar color changes on hover. * Commented the hover effect for now. * Remove handle "hover" effect. * scrollbar * revert button active state --- data/themes/default/style.css | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 83316b9f2..a4f94de09 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -278,33 +278,33 @@ QScrollBar::add-page:vertical:pressed, QScrollBar::sub-page:vertical:pressed { /* scrollbar: handles (sliders) */ QScrollBar::handle:horizontal { - background: #3f4750; + background: #5d6b77; border: none; border-radius: 4px; min-width: 24px; } QScrollBar::handle:horizontal:hover { - background: #525e69; + background: #b8c4d1; } QScrollBar::handle:horizontal:pressed { - background: rgba(11,213,86,100); + background: #ffffff; } QScrollBar::handle:vertical { - background: #3f4750; + background: #5d6b77; border: none; border-radius: 4px; min-height: 24px; } QScrollBar::handle:vertical:hover { - background: #525e69; + background: #b8c4d1; } QScrollBar::handle:vertical:pressed { - background: rgba(11,213,86,100); + background: #ffffff; } QScrollBar::handle:horizontal:disabled, QScrollBar::handle:vertical:disabled { @@ -551,6 +551,12 @@ QToolButton:hover { background: qlineargradient(spread:reflect, x1:0, y1:0, x2:0, y2:1, stop:0 #7c8799, stop:1 #343840) } +QToolButton:hover:checked { + border: 2px solid #343840; + border-radius: 2px; + background: qlineargradient(spread:reflect, x1:0, y1:0, x2:0, y2:1, stop:0 #292d33, stop:1 #22262c) +} + QToolButton:pressed { border-top: 1px solid #778394; border-bottom: 1px solid #1e2226; From 2e6545328c1b234b7bb1f7a79226161621e3f057 Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Thu, 23 May 2024 13:21:57 -0400 Subject: [PATCH 179/191] Update MinGW CI to Ubuntu 20.04 (#7259) Update MinGW CI to Ubuntu 20.04 * Use ghcr.io/lmms/linux.mingw:20.04 * Fix deprecation in ClipView.cpp * Fix ccache and simplify git configuration * Apply patch by @DomClark for MinGW's SDL2 target * Update excludelist-win --- .github/workflows/build.yml | 12 ++++-------- cmake/install/excludelist-win | 4 ++++ cmake/modules/FindSDL2.cmake | 12 ++++++++++++ src/gui/clips/ClipView.cpp | 2 +- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3697f8cf..3ec25cd2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: arch: ['32', '64'] name: mingw${{ matrix.arch }} runs-on: ubuntu-latest - container: lmmsci/linux.mingw${{ matrix.arch }}:18.04 + container: ghcr.io/lmms/linux.mingw:20.04 env: CMAKE_OPTS: >- -DUSE_WERROR=ON @@ -163,12 +163,8 @@ jobs: CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j2 steps: - - name: Update and configure Git - run: | - add-apt-repository ppa:git-core/ppa - apt-get update - apt-get --yes install git - git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Configure git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Check out uses: actions/checkout@v3 with: @@ -201,7 +197,7 @@ jobs: run: | ccache --cleanup echo "[ccache config]" - ccache --print-config + ccache --show-config echo "[ccache stats]" ccache --show-stats env: diff --git a/cmake/install/excludelist-win b/cmake/install/excludelist-win index 17793a113..ac3c47901 100644 --- a/cmake/install/excludelist-win +++ b/cmake/install/excludelist-win @@ -3,17 +3,21 @@ ADVAPI32.dll COMCTL32.dll comdlg32.dll +d3d11.dll dwmapi.dll +dxgi.dll GDI32.dll IMM32.dll KERNEL32.dll MPR.DLL msvcrt.dll +netapi32.dll ole32.dll OLEAUT32.dll OPENGL32.DLL SHELL32.dll USER32.dll +userenv.dll UxTheme.dll VERSION.dll WINMM.DLL diff --git a/cmake/modules/FindSDL2.cmake b/cmake/modules/FindSDL2.cmake index 3bad1002e..6e07f7aff 100644 --- a/cmake/modules/FindSDL2.cmake +++ b/cmake/modules/FindSDL2.cmake @@ -33,6 +33,18 @@ find_package(SDL2 CONFIG QUIET) if(TARGET SDL2::SDL2) + # SDL2::SDL2 under MinGW is an interface target for reasons, so we can't get + # the library location from it. Print minimal information and return early. + get_target_property(sdl2_target_type SDL2::SDL2 TYPE) + if(sdl2_target_type STREQUAL "INTERFACE_LIBRARY") + unset(sdl2_target_type) + if(NOT SDL2_FIND_QUIETLY) + message(STATUS "Found SDL2 (found version \"${SDL2_VERSION}\")") + endif() + return() + endif() + unset(sdl2_target_type) + # Extract details for find_package_handle_standard_args get_target_property(SDL2_LIBRARY SDL2::SDL2 LOCATION) get_target_property(SDL2_INCLUDE_DIR SDL2::SDL2 INTERFACE_INCLUDE_DIRECTORIES) diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index 5c8a12b91..a0f7f7c53 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -356,7 +356,7 @@ void ClipView::selectColor() // Get a color from the user const auto newColor = ColorChooser{this} .withPalette(ColorChooser::Palette::Track) - ->getColor(m_clip->color().value_or(palette().background().color())); + ->getColor(m_clip->color().value_or(palette().window().color())); if (newColor.isValid()) { setColor(newColor); } } From 0a93e1777b1e9e4daf5718071c0cfb0eadf3ee27 Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Fri, 24 May 2024 20:24:10 +0200 Subject: [PATCH 180/191] Bump SWH submodule to fix a crash with Reverse Delay (#7277) --- plugins/LadspaEffect/swh/ladspa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/LadspaEffect/swh/ladspa b/plugins/LadspaEffect/swh/ladspa index 02bda2320..0f54d2430 160000 --- a/plugins/LadspaEffect/swh/ladspa +++ b/plugins/LadspaEffect/swh/ladspa @@ -1 +1 @@ -Subproject commit 02bda232041380c2846414945798cbbfecb2f3f2 +Subproject commit 0f54d2430febb4d5f02d13132dd91d7345e080b5 From 4033406430a76d35a5abda536547f95bde321d97 Mon Sep 17 00:00:00 2001 From: Oskar Wallgren Date: Tue, 28 May 2024 14:31:09 +0200 Subject: [PATCH 181/191] Automation Editor - Fix automation point forced snapping to integer value. (#7282) * Revert "Fix glitch with automation points (#7269)" This reverts commit d60fd0d022ba66571bb31eea3a0fa422ba97bf26. * Fix glitch in Automation Editor. This reverts the earlier fix and tries to solve the issue by instead rounding off the values of the top/bottom levels before comparison with the automation point value. --------- Co-authored-by: Dalton Messmer --- src/gui/editors/AutomationEditor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 9c9e4fd26..41042f524 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1647,14 +1647,14 @@ float AutomationEditor::getLevel(int y ) { int level_line_y = height() - SCROLLBAR_SIZE - 1; // pressed level - float level = ( ( m_bottomLevel + ( m_y_auto ? + float level = std::roundf( ( m_bottomLevel + ( m_y_auto ? ( m_maxLevel - m_minLevel ) * ( level_line_y - y ) / (float)( level_line_y - ( TOP_MARGIN + 2 ) ) : ( level_line_y - y ) / (float)m_y_delta ) ) / m_step ) * m_step; // some range-checking-stuff - level = qBound( m_bottomLevel, level, m_topLevel ); + level = qBound(std::roundf(m_bottomLevel), level, std::roundf(m_topLevel)); - return std::roundf(level); + return level; } From d1a15e5ff8bca4fd38c9a5b89348ed40f51fe22e Mon Sep 17 00:00:00 2001 From: FyiurAmron Date: Tue, 28 May 2024 22:39:38 +0200 Subject: [PATCH 182/191] Fix for Werror=self-move in test for GCC >= 13 (#7288) --- tests/src/core/ArrayVectorTest.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/src/core/ArrayVectorTest.cpp b/tests/src/core/ArrayVectorTest.cpp index 1eba265e9..9e6ed40b8 100644 --- a/tests/src/core/ArrayVectorTest.cpp +++ b/tests/src/core/ArrayVectorTest.cpp @@ -226,9 +226,17 @@ private slots: { { // Self-assignment should not change the contents + //// Please note the following: + //// https://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#2468 + //// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81159 auto v = ArrayVector{1, 2, 3}; const auto oldValue = v; +#pragma GCC diagnostic push +#if __GNUC__ >= 13 +# pragma GCC diagnostic ignored "-Wself-move" +#endif v = std::move(v); +#pragma GCC diagnostic pop QCOMPARE(v, oldValue); } { From 948bb4ac69827757289fe93e9eb04bdcfab69c48 Mon Sep 17 00:00:00 2001 From: FyiurAmron Date: Wed, 29 May 2024 01:28:58 +0200 Subject: [PATCH 183/191] Fix to allow NSIS packaging to work for non-MSVC Windows builds (#7287) --- cmake/nsis/CMakeLists.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmake/nsis/CMakeLists.txt b/cmake/nsis/CMakeLists.txt index ba41b085f..e926e074d 100644 --- a/cmake/nsis/CMakeLists.txt +++ b/cmake/nsis/CMakeLists.txt @@ -3,10 +3,9 @@ if(LMMS_MSVC_YEAR) SET(WIN_PLATFORM "msvc${LMMS_MSVC_YEAR}") endif() -SET(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/cmake/nsis/nsis_branding.bmp") -IF(MSVC) - STRING(REPLACE "/" "\\\\" CPACK_PACKAGE_ICON ${CPACK_PACKAGE_ICON}) -ENDIF(MSVC) +# the final slash needs to be flipped for CPACK_PACKAGE_ICON to work: +# https://cmake.org/pipermail/cmake/2008-June/022085.html +SET(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/cmake/nsis\\\\nsis_branding.bmp") SET(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/cmake/nsis/icon.ico") SET(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe" PARENT_SCOPE) SET(CPACK_NSIS_DISPLAY_NAME "${PROJECT_NAME_UCASE} ${VERSION}" PARENT_SCOPE) From a262956ed3240d6c4771c362ab5431d4449d2b3f Mon Sep 17 00:00:00 2001 From: "Raine M. Ekman" Date: Wed, 29 May 2024 15:16:35 +0300 Subject: [PATCH 184/191] Update CPU to Pentium 4 for win32 builds (#6155) The CPU feature requirements for any currently supported 32-bit version of Windows (8.1 and 10) are PAE, NX and SSE2. That should mean a green light for bumping the CPU we build for to the minimum one with SSE2. --- cmake/build_win32.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/build_win32.sh b/cmake/build_win32.sh index 33cd8ecce..bccda3a48 100755 --- a/cmake/build_win32.sh +++ b/cmake/build_win32.sh @@ -31,7 +31,7 @@ fi export PATH=$MINGW/bin:$PATH export CXXFLAGS="$CFLAGS" if [ "$ARCH" == "32" ]; then - export CFLAGS="-march=pentium3 -mtune=generic -mpreferred-stack-boundary=5 -mfpmath=sse" + export CFLAGS="-march=pentium4 -mtune=generic -mpreferred-stack-boundary=5 -mfpmath=sse" fi CMAKE_OPTS="-DCMAKE_PREFIX_PATH=$MINGW $CMAKE_OPTS" From 7197f1de39821d0ee28727d0f16c24dd61085502 Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Wed, 29 May 2024 15:47:25 -0400 Subject: [PATCH 185/191] Update upload-artifacts actions (#7263) --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ec25cd2b..dd7125e0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,7 @@ jobs: cmake --build build --target install cmake --build build --target appimage - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linux path: build/lmms-*.AppImage @@ -128,7 +128,7 @@ jobs: cmake --build build --target install cmake --build build --target dmg - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: macos path: build/lmms-*.dmg @@ -189,7 +189,7 @@ jobs: - name: Package run: cmake --build build --target package - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: mingw${{ matrix.arch }} path: build/lmms-*.exe @@ -281,7 +281,7 @@ jobs: - name: Package run: cmake --build build --target package - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: msvc-${{ matrix.arch }} path: build\lmms-*.exe From e82e3f573ac33c7f4d7a31e007ed5e191a46373e Mon Sep 17 00:00:00 2001 From: FyiurAmron Date: Wed, 29 May 2024 22:04:45 +0200 Subject: [PATCH 186/191] update veal submodule --- plugins/LadspaEffect/calf/veal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/LadspaEffect/calf/veal b/plugins/LadspaEffect/calf/veal index 422168dc6..0162621fa 160000 --- a/plugins/LadspaEffect/calf/veal +++ b/plugins/LadspaEffect/calf/veal @@ -1 +1 @@ -Subproject commit 422168dc670bccdba14cfac2946f6ad413bce9ba +Subproject commit 0162621fa75cf90c319c704d646c734e1ed21e14 From bfeb781dc0c54bea79b7ff6cfcb17dedd353a275 Mon Sep 17 00:00:00 2001 From: saker Date: Wed, 29 May 2024 21:46:58 -0400 Subject: [PATCH 187/191] Redesign file browser searching (#7130) --- include/FileBrowser.h | 20 +++-- include/FileBrowserSearcher.h | 148 -------------------------------- include/FileSearch.h | 73 ++++++++++++++++ include/ThreadPool.h | 100 +++++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/FileSearch.cpp | 96 +++++++++++++++++++++ src/core/ThreadPool.cpp | 85 ++++++++++++++++++ src/gui/CMakeLists.txt | 1 - src/gui/FileBrowser.cpp | 69 ++++++++------- src/gui/FileBrowserSearcher.cpp | 135 ----------------------------- 10 files changed, 403 insertions(+), 326 deletions(-) delete mode 100644 include/FileBrowserSearcher.h create mode 100644 include/FileSearch.h create mode 100644 include/ThreadPool.h create mode 100644 src/core/FileSearch.cpp create mode 100644 src/core/ThreadPool.cpp delete mode 100644 src/gui/FileBrowserSearcher.cpp diff --git a/include/FileBrowser.h b/include/FileBrowser.h index b0c8a5199..9185f5147 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -28,10 +28,11 @@ #include #include #include -#include "embed.h" - -#include "FileBrowserSearcher.h" #include +#include + +#include "FileSearch.h" +#include "embed.h" #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include @@ -76,9 +77,9 @@ public: ~FileBrowser() override = default; - static QStringList directoryBlacklist() + static QStringList excludedPaths() { - static auto s_blacklist = QStringList{ + static auto s_excludedPaths = QStringList{ #ifdef LMMS_BUILD_LINUX "/bin", "/boot", "/dev", "/etc", "/proc", "/run", "/sbin", "/sys" @@ -87,7 +88,7 @@ public: "C:\\Windows" #endif }; - return s_blacklist; + return s_excludedPaths; } static QDir::Filters dirFilters() { return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; } static QDir::SortFlags sortFlags() { return QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase; } @@ -105,16 +106,17 @@ private: void saveDirectoriesStates(); void restoreDirectoriesStates(); - void buildSearchTree(); + void foundSearchMatch(FileSearch* search, const QString& match); + void searchCompleted(FileSearch* search); void onSearch(const QString& filter); - void toggleSearch(bool on); + void displaySearch(bool on); FileBrowserTreeWidget * m_fileBrowserTreeWidget; FileBrowserTreeWidget * m_searchTreeWidget; QLineEdit * m_filterEdit; - std::shared_ptr m_currentSearch; + std::shared_ptr m_currentSearch; QProgressBar* m_searchIndicator = nullptr; QString m_directories; //!< Directories to search, split with '*' diff --git a/include/FileBrowserSearcher.h b/include/FileBrowserSearcher.h deleted file mode 100644 index 4f4d3ff1c..000000000 --- a/include/FileBrowserSearcher.h +++ /dev/null @@ -1,148 +0,0 @@ -/* - * FileBrowserSearcher.h - Batch processor for searching the filesystem - * - * Copyright (c) 2023 saker - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#ifndef LMMS_FILE_BROWSER_SEARCHER_H -#define LMMS_FILE_BROWSER_SEARCHER_H - -#include -#include -#include -#include -#include - -#ifdef __MINGW32__ -#include -#include -#include -#else -#include -#include -#include -#endif - -namespace lmms::gui { - -//! An active object that handles searching for files that match a certain filter across the file system. -class FileBrowserSearcher -{ -public: - //! Number of milliseconds to wait for before a match should be processed by the user. - static constexpr int MillisecondsPerMatch = 1; - - //! The future object for FileBrowserSearcher. It is used to track the current state of search operations, as - // well as retrieve matches. - class SearchFuture - { - public: - //! Possible state values of the future object. - enum class State - { - Idle, - Running, - Cancelled, - Completed - }; - - //! Constructs a future object using the specified filter, paths, and valid file extensions in the Idle state. - SearchFuture(const QString& filter, const QStringList& paths, const QStringList& extensions) - : m_filter(filter) - , m_paths(paths) - , m_extensions(extensions) - { - } - - //! Retrieves a match from the match list. - auto match() -> QString - { - const auto lock = std::lock_guard{m_matchesMutex}; - return m_matches.empty() ? QString{} : m_matches.takeFirst(); - } - - //! Returns the current state of this future object. - auto state() -> State { return m_state; } - - //! Returns the filter used. - auto filter() -> const QString& { return m_filter; } - - //! Returns the paths to filter. - auto paths() -> const QStringList& { return m_paths; } - - //! Returns the valid file extensions. - auto extensions() -> const QStringList& { return m_extensions; } - - private: - //! Adds a match to the match list. - auto addMatch(const QString& match) -> void - { - const auto lock = std::lock_guard{m_matchesMutex}; - m_matches.append(match); - } - - QString m_filter; - QStringList m_paths; - QStringList m_extensions; - - QStringList m_matches; - std::mutex m_matchesMutex; - - std::atomic m_state = State::Idle; - - friend FileBrowserSearcher; - }; - - ~FileBrowserSearcher(); - - //! Enqueues a search to be ran by the worker thread. - //! Returns a future that the caller can use to track state and results of the operation. - auto search(const QString& filter, const QStringList& paths, const QStringList& extensions) - -> std::shared_ptr; - - //! Sends a signal to cancel a running search. - auto cancel() -> void { m_cancelRunningSearch = true; } - - //! Returns the global instance of the searcher object. - static auto instance() -> FileBrowserSearcher* - { - static auto s_instance = FileBrowserSearcher{}; - return &s_instance; - } - -private: - //! Event loop for the worker thread. - auto run() -> void; - - //! Using Depth-first search (DFS), filters the specified path and adds any matches to the future list. - auto process(SearchFuture* searchFuture, const QString& path) -> bool; - - std::queue> m_searchQueue; - std::atomic m_cancelRunningSearch = false; - - bool m_workerStopped = false; - std::mutex m_workerMutex; - std::condition_variable m_workerCond; - std::thread m_worker{[this] { run(); }}; -}; -} // namespace lmms::gui - -#endif // LMMS_FILE_BROWSER_SEARCHER_H diff --git a/include/FileSearch.h b/include/FileSearch.h new file mode 100644 index 000000000..fd311c903 --- /dev/null +++ b/include/FileSearch.h @@ -0,0 +1,73 @@ +/* + * FileSearch.h - File system search task + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_FILE_SEARCH_H +#define LMMS_FILE_SEARCH_H + +#include +#include +#include + +namespace lmms { +//! A Qt object that encapsulates the operation of searching the file system. +class FileSearch : public QObject +{ + Q_OBJECT +public: + //! Number of milliseconds the search waits before signaling a matching result. + static constexpr int MillisecondsBetweenResults = 1; + + //! Create a `FileSearch` object that uses the specified string filter `filter` and extension filters in + //! `extensions` to search within the given `paths`. + //! `excludedPaths`, `dirFilters`, and `sortFlags` can optionally be specified to exclude certain directories, filter + //! out certain types of entries, and sort the matches. + FileSearch(const QString& filter, const QStringList& paths, const QStringList& extensions, + const QStringList& excludedPaths = {}, QDir::Filters dirFilters = QDir::Filters{}, + QDir::SortFlags sortFlags = QDir::SortFlags{}); + + //! Execute the search, emitting the `foundResult` signal when matches are found. + void operator()(); + + //! Cancel the search. + void cancel(); + +signals: + //! Emitted when a result is found when searching the file system. + void foundMatch(FileSearch* search, const QString& match); + + //! Emitted when the search completes. + void searchCompleted(FileSearch* search); + +private: + static auto isPathExcluded(const QString& path) -> bool; + QString m_filter; + QStringList m_paths; + QStringList m_extensions; + QStringList m_excludedPaths; + QDir::Filters m_dirFilters; + QDir::SortFlags m_sortFlags; + std::atomic m_cancel = false; +}; +} // namespace lmms +#endif // LMMS_FILE_SEARCH_H diff --git a/include/ThreadPool.h b/include/ThreadPool.h new file mode 100644 index 000000000..b1d7900a6 --- /dev/null +++ b/include/ThreadPool.h @@ -0,0 +1,100 @@ +/* + * ThreadPool.h + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_THREAD_POOL_H +#define LMMS_THREAD_POOL_H + +#include +#include +#include +#include +#include + +#ifdef __MINGW32__ +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif + +namespace lmms { +//! A thread pool that can be used for asynchronous processing. +class ThreadPool +{ +public: + //! Destroys the `ThreadPool` object. + //! This blocks until all workers have finished executing. + ~ThreadPool(); + + //! Enqueue function `fn` with arguments `args` to be ran asynchronously. + template + auto enqueue(Fn&& fn, Args&&... args) -> std::future> + { + using ReturnType = std::invoke_result_t; + + auto promise = std::make_shared>(); + auto task = [promise, fn = std::forward(fn), args = std::make_tuple(std::forward(args)...)] + { + if constexpr (!std::is_same_v) + { + promise->set_value(std::apply(fn, args)); + return; + } + std::apply(fn, args); + promise->set_value(); + }; + + { + const auto lock = std::unique_lock{m_runMutex}; + m_queue.push(std::move(task)); + } + + m_runCond.notify_one(); + return promise->get_future(); + } + + //! Return the number of worker threads used. + auto numWorkers() const -> size_t; + + //! Return the global `ThreadPool` instance. + static auto instance() -> ThreadPool&; + +private: + ThreadPool(size_t numWorkers); + void run(); + std::vector m_workers; + std::queue> m_queue; + std::atomic m_done = false; + std::condition_variable m_runCond; + std::mutex m_runMutex; + inline static size_t s_numWorkers = std::thread::hardware_concurrency(); +}; +} // namespace lmms + +#endif // LMMS_THREAD_POOL_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1a4871fc7..9ebe2c355 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -23,6 +23,7 @@ set(LMMS_SRCS core/Engine.cpp core/EnvelopeAndLfoParameters.cpp core/fft_helpers.cpp + core/FileSearch.cpp core/Mixer.cpp core/ImportFilter.cpp core/InlineAutomation.cpp @@ -76,6 +77,7 @@ set(LMMS_SRCS core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp + core/ThreadPool.cpp core/Timeline.cpp core/TimePos.cpp core/ToolPlugin.cpp diff --git a/src/core/FileSearch.cpp b/src/core/FileSearch.cpp new file mode 100644 index 000000000..fe1efd97e --- /dev/null +++ b/src/core/FileSearch.cpp @@ -0,0 +1,96 @@ +/* + * FileSearch.cpp - File system search task + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "FileSearch.h" + +#include +#include +#include + +#ifdef __MINGW32__ +#include +#else +#include +#endif + +namespace lmms { +FileSearch::FileSearch(const QString& filter, const QStringList& paths, const QStringList& extensions, + const QStringList& excludedPaths, QDir::Filters dirFilters, QDir::SortFlags sortFlags) + : m_filter(filter) + , m_paths(paths) + , m_extensions(extensions) + , m_excludedPaths(excludedPaths) + , m_dirFilters(dirFilters) + , m_sortFlags(sortFlags) +{ +} + +void FileSearch::operator()() +{ + auto stack = QFileInfoList{}; + for (const auto& path : m_paths) + { + if (m_excludedPaths.contains(path)) { continue; } + + auto dir = QDir{path}; + stack.append(dir.entryInfoList(m_dirFilters, m_sortFlags)); + + while (!stack.empty()) + { + if (m_cancel.load(std::memory_order_relaxed)) { return; } + + const auto info = stack.takeFirst(); + const auto entryPath = info.absoluteFilePath(); + if (m_excludedPaths.contains(entryPath)) { continue; } + + const auto name = info.fileName(); + const auto validFile = info.isFile() && m_extensions.contains(info.suffix(), Qt::CaseInsensitive); + const auto passesFilter = name.contains(m_filter, Qt::CaseInsensitive); + + if ((validFile || info.isDir()) && passesFilter) + { + std::this_thread::sleep_for(std::chrono::milliseconds{MillisecondsBetweenResults}); + emit foundMatch(this, entryPath); + } + + if (info.isDir()) + { + dir.setPath(entryPath); + const auto entries = dir.entryInfoList(m_dirFilters, m_sortFlags); + + // Reverse to maintain the sorting within this directory when popped + std::for_each(entries.rbegin(), entries.rend(), [&stack](const auto& entry) { stack.push_front(entry); }); + } + } + } + + emit searchCompleted(this); +} + +void FileSearch::cancel() +{ + m_cancel.store(true, std::memory_order_relaxed); +} + +} // namespace lmms diff --git a/src/core/ThreadPool.cpp b/src/core/ThreadPool.cpp new file mode 100644 index 000000000..2e5f00df0 --- /dev/null +++ b/src/core/ThreadPool.cpp @@ -0,0 +1,85 @@ +/* + * ThreadPool.cpp + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ +#include "ThreadPool.h" + +#include +#include +#include + +namespace lmms { +ThreadPool::ThreadPool(size_t numWorkers) +{ + assert(numWorkers > 0); + + m_workers.reserve(numWorkers); + for (size_t i = 0; i < numWorkers; ++i) + { + m_workers.emplace_back([this] { run(); }); + } +} + +ThreadPool::~ThreadPool() +{ + { + const auto lock = std::unique_lock{m_runMutex}; + m_done = true; + } + + m_runCond.notify_all(); + + for (auto& worker : m_workers) + { + if (worker.joinable()) { worker.join(); } + } +} + +auto ThreadPool::numWorkers() const -> size_t +{ + return m_workers.size(); +} + +void ThreadPool::run() +{ + while (!m_done) + { + std::function task; + { + auto lock = std::unique_lock{m_runMutex}; + m_runCond.wait(lock, [this] { return !m_queue.empty() || m_done; }); + + if (m_done) { break; } + task = m_queue.front(); + m_queue.pop(); + } + task(); + } +} + +auto ThreadPool::instance() -> ThreadPool& +{ + static auto s_pool = ThreadPool{s_numWorkers}; + return s_pool; +} + +} // namespace lmms diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f6115dbf6..2485b92d2 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -14,7 +14,6 @@ SET(LMMS_SRCS gui/EffectView.cpp gui/embed.cpp gui/FileBrowser.cpp - gui/FileBrowserSearcher.cpp gui/GuiApplication.cpp gui/LadspaControlView.cpp gui/LfoControllerDialog.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 89201cb04..04f7ff46f 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -31,23 +31,22 @@ #include #include #include -#include -#include #include #include +#include #include +#include #include #include - +#include #include -#include "FileBrowser.h" -#include "FileBrowserSearcher.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "DataFile.h" -#include "embed.h" #include "Engine.h" +#include "FileBrowser.h" +#include "FileSearch.h" #include "GuiApplication.h" #include "ImportFilter.h" #include "Instrument.h" @@ -65,6 +64,8 @@ #include "Song.h" #include "StringPairDrag.h" #include "TextFloat.h" +#include "ThreadPool.h" +#include "embed.h" namespace lmms::gui { @@ -152,10 +153,6 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_searchTreeWidget->hide(); addContentWidget(m_searchTreeWidget); - auto searchTimer = new QTimer(this); - connect(searchTimer, &QTimer::timeout, this, &FileBrowser::buildSearchTree); - searchTimer->start(FileBrowserSearcher::MillisecondsPerMatch); - m_searchIndicator = new QProgressBar(this); m_searchIndicator->setMinimum(0); m_searchIndicator->setMaximum(100); @@ -181,20 +178,10 @@ void FileBrowser::restoreDirectoriesStates() expandItems(m_savedExpandedDirs); } -void FileBrowser::buildSearchTree() +void FileBrowser::foundSearchMatch(FileSearch* search, const QString& match) { - if (!m_currentSearch) { return; } - - const auto match = m_currentSearch->match(); - using State = FileBrowserSearcher::SearchFuture::State; - if ((m_currentSearch->state() == State::Completed && match.isEmpty()) - || m_currentSearch->state() == State::Cancelled) - { - m_currentSearch = nullptr; - m_searchIndicator->setMaximum(100); - return; - } - else if (match.isEmpty()) { return; } + assert(search != nullptr); + if (m_currentSearch.get() != search) { return; } auto basePath = QString{}; for (const auto& path : m_directories.split('*')) @@ -258,13 +245,22 @@ void FileBrowser::buildSearchTree() } } - +void FileBrowser::searchCompleted(FileSearch* search) +{ + assert(search != nullptr); + if (m_currentSearch.get() != search) { return; } + + m_currentSearch.reset(); + m_searchIndicator->setMaximum(100); +} + void FileBrowser::onSearch(const QString& filter) { + if (m_currentSearch) { m_currentSearch->cancel(); } + if (filter.isEmpty()) { - toggleSearch(false); - FileBrowserSearcher::instance()->cancel(); + displaySearch(false); return; } @@ -274,14 +270,21 @@ void FileBrowser::onSearch(const QString& filter) if (directories.isEmpty()) { return; } m_searchTreeWidget->clear(); - toggleSearch(true); + displaySearch(true); auto browserExtensions = m_filter; const auto searchExtensions = browserExtensions.remove("*.").split(' '); - m_currentSearch = FileBrowserSearcher::instance()->search(filter, directories, searchExtensions); + + auto search = std::make_shared( + filter, directories, searchExtensions, excludedPaths(), dirFilters(), sortFlags()); + connect(search.get(), &FileSearch::foundMatch, this, &FileBrowser::foundSearchMatch, Qt::QueuedConnection); + connect(search.get(), &FileSearch::searchCompleted, this, &FileBrowser::searchCompleted, Qt::QueuedConnection); + + m_currentSearch = search; + ThreadPool::instance().enqueue([search] { (*search)(); }); } -void FileBrowser::toggleSearch(bool on) +void FileBrowser::displaySearch(bool on) { if (on) { @@ -376,7 +379,7 @@ void FileBrowser::giveFocusToFilter() void FileBrowser::addItems(const QString & path ) { - if (FileBrowser::directoryBlacklist().contains(path)) { return; } + if (FileBrowser::excludedPaths().contains(path)) { return; } if( m_dirsAsItems ) { @@ -391,7 +394,7 @@ void FileBrowser::addItems(const QString & path ) m_filter.split(' '), dirFilters(), QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); for (const auto& entry : entries) { - if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + if (FileBrowser::excludedPaths().contains(entry.absoluteFilePath())) { continue; } QString fileName = entry.fileName(); if (entry.isDir()) @@ -1066,7 +1069,7 @@ void Directory::update() bool Directory::addItems(const QString& path) { - if (FileBrowser::directoryBlacklist().contains(path)) { return false; } + if (FileBrowser::excludedPaths().contains(path)) { return false; } QDir thisDir(path); if (!thisDir.isReadable()) { return false; } @@ -1077,7 +1080,7 @@ bool Directory::addItems(const QString& path) = thisDir.entryInfoList(m_filter.split(' '), FileBrowser::dirFilters(), FileBrowser::sortFlags()); for (const auto& entry : entries) { - if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + if (FileBrowser::excludedPaths().contains(entry.absoluteFilePath())) { continue; } QString fileName = entry.fileName(); if (entry.isDir()) diff --git a/src/gui/FileBrowserSearcher.cpp b/src/gui/FileBrowserSearcher.cpp deleted file mode 100644 index 80c238058..000000000 --- a/src/gui/FileBrowserSearcher.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/* - * FileBrowserSearcher.cpp - Batch processor for searching the filesystem - * - * Copyright (c) 2023 saker - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#include "FileBrowserSearcher.h" - -#include -#include - -#include "FileBrowser.h" - -namespace lmms::gui { - -FileBrowserSearcher::~FileBrowserSearcher() -{ - m_cancelRunningSearch = true; - - { - const auto lock = std::lock_guard{m_workerMutex}; - m_workerStopped = true; - } - - m_workerCond.notify_one(); - m_worker.join(); -} - -auto FileBrowserSearcher::search(const QString& filter, const QStringList& paths, const QStringList& extensions) - -> std::shared_ptr -{ - m_cancelRunningSearch = true; - auto future = std::make_shared(filter, paths, extensions); - - { - const auto lock = std::lock_guard{m_workerMutex}; - m_searchQueue.push(future); - m_cancelRunningSearch = false; - } - - m_workerCond.notify_one(); - return future; -} - -auto FileBrowserSearcher::run() -> void -{ - while (true) - { - auto lock = std::unique_lock{m_workerMutex}; - m_workerCond.wait(lock, [this] { return m_workerStopped || !m_searchQueue.empty(); }); - - if (m_workerStopped) { return; } - - const auto future = m_searchQueue.front(); - future->m_state = SearchFuture::State::Running; - m_searchQueue.pop(); - - auto cancelled = false; - for (const auto& path : future->m_paths) - { - if (FileBrowser::directoryBlacklist().contains(path)) { continue; } - - if (!process(future.get(), path)) - { - future->m_state = SearchFuture::State::Cancelled; - cancelled = true; - break; - } - } - - if (!cancelled) { future->m_state = SearchFuture::State::Completed; } - } -} - -auto FileBrowserSearcher::process(SearchFuture* searchFuture, const QString& path) -> bool -{ - auto stack = QFileInfoList{}; - - auto dir = QDir{path}; - stack.append(dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags())); - - while (!stack.empty()) - { - if (m_cancelRunningSearch) - { - m_cancelRunningSearch = false; - return false; - } - - const auto info = stack.takeFirst(); - const auto path = info.absoluteFilePath(); - if (FileBrowser::directoryBlacklist().contains(path)) { continue; } - - const auto name = info.fileName(); - const auto validFile = info.isFile() && searchFuture->m_extensions.contains(info.suffix(), Qt::CaseInsensitive); - const auto passesFilter = name.contains(searchFuture->m_filter, Qt::CaseInsensitive); - - // Only when a directory doesn't pass the filter should we search further - if (info.isDir() && !passesFilter) - { - dir.setPath(path); - auto entries = dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags()); - - // Reverse to maintain the sorting within this directory when popped - std::reverse(entries.begin(), entries.end()); - - for (const auto& entry : entries) - { - stack.push_front(entry); - } - } - else if ((validFile || info.isDir()) && passesFilter) { searchFuture->addMatch(path); } - } - return true; -} - -} // namespace lmms::gui From 6ec1ced49cc9845832aa7c451167e0b143fee785 Mon Sep 17 00:00:00 2001 From: "Raine M. Ekman" Date: Thu, 30 May 2024 19:37:08 +0300 Subject: [PATCH 188/191] Don't recalculate the song length for every added TCO while loading (#5236) Don't make LMMS calculate the song length for every added TCO when a new project is created or a project is loaded. Instead do it only once afterwards. This is accomplished by preventing any calculations in `Song::updateLength` if a song is currently loaded. `Song::updateLength` is then called immediately after the loading flag has been set to `false` in both cases. --------- Co-authored-by: IanCaio --- src/core/Song.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 81a263287..392e9e256 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -562,6 +562,8 @@ void Song::playMidiClip( const MidiClip* midiClipToPlay, bool loop ) void Song::updateLength() { + if (m_loadingProject) { return; } + m_length = 0; m_tracksMutex.lockForRead(); for (auto track : tracks()) @@ -963,7 +965,7 @@ void Song::createNewProject() QCoreApplication::instance()->processEvents(); m_loadingProject = false; - + updateLength(); Engine::patternStore()->updateAfterTrackAdd(); Engine::projectJournal()->setJournalling( true ); @@ -1206,6 +1208,7 @@ void Song::loadProject( const QString & fileName ) } m_loadingProject = false; + updateLength(); setModified(false); m_loadOnLaunch = false; } From a85c98648c131879f83339a058e27e0bb4433886 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Thu, 30 May 2024 22:41:26 -0400 Subject: [PATCH 189/191] Add macOS arm64 builds (#7285) Add macOS arm64 builds --- .github/workflows/build.yml | 20 ++++++++++++++++---- cmake/apple/CMakeLists.txt | 13 ++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd7125e0b..b80d5df75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,8 +64,19 @@ jobs: env: CCACHE_MAXSIZE: 500M macos: - name: macos - runs-on: macos-12 + strategy: + fail-fast: false + matrix: + arch: [ x86_64, arm64 ] + include: + - arch: x86_64 + os: macos-12 + xcode: "13.1" + - arch: arm64 + os: macos-14 + xcode: "14.3.1" + name: macos-${{ matrix.arch }} + runs-on: ${{ matrix.os }} env: CMAKE_OPTS: >- -DUSE_WERROR=ON @@ -74,7 +85,7 @@ jobs: CCACHE_MAXSIZE: 0 CCACHE_NOCOMPRESS: 1 MAKEFLAGS: -j3 - DEVELOPER_DIR: /Applications/Xcode_13.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer steps: - name: Check out uses: actions/checkout@v3 @@ -115,6 +126,7 @@ jobs: -B build \ -DCMAKE_INSTALL_PREFIX="../target" \ -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" \ + -DCMAKE_OSX_ARCHITECTURES=${{ matrix.arch }} \ $CMAKE_OPTS \ -DUSE_WERROR=OFF - name: Build @@ -130,7 +142,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: macos + name: macos-${{ matrix.arch }} path: build/lmms-*.dmg - name: Trim ccache and print statistics run: | diff --git a/cmake/apple/CMakeLists.txt b/cmake/apple/CMakeLists.txt index 0b66689e7..3fd0a4da4 100644 --- a/cmake/apple/CMakeLists.txt +++ b/cmake/apple/CMakeLists.txt @@ -19,8 +19,19 @@ CONFIGURE_FILE("lmms.plist.in" "${CMAKE_BINARY_DIR}/Info.plist") CONFIGURE_FILE("install_apple.sh.in" "${CMAKE_BINARY_DIR}/install_apple.sh" @ONLY) CONFIGURE_FILE("package_apple.json.in" "${CMAKE_BINARY_DIR}/package_apple.json" @ONLY) +IF(CMAKE_OSX_ARCHITECTURES) + SET(DMG_ARCH "${CMAKE_OSX_ARCHITECTURES}") +ELSEIF(IS_ARM64) + # Target arch is host arch + SET(DMG_ARCH "arm64") +ELSE() + # Fallback to Intel + SET(DMG_ARCH "x86_64") +ENDIF() + # DMG creation target -SET(DMG_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}-${VERSION}-mac${APPLE_OS_VER}.dmg") +SET(DMG_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}-${VERSION}-mac${APPLE_OS_VER}-${DMG_ARCH}.dmg") + FILE(REMOVE "${DMG_FILE}") ADD_CUSTOM_TARGET(removedmg COMMAND touch "\"${DMG_FILE}\"" && rm "\"${DMG_FILE}\"" From 94b1a382dd934b5d27de011e3206926c12323593 Mon Sep 17 00:00:00 2001 From: BoredGuy1 <66702733+BoredGuy1@users.noreply.github.com> Date: Thu, 30 May 2024 23:19:34 -0700 Subject: [PATCH 190/191] fixed position line disappearing when zoomed out (#7296) --- src/gui/editors/PositionLine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editors/PositionLine.cpp b/src/gui/editors/PositionLine.cpp index 8b938443d..dda44e8f7 100644 --- a/src/gui/editors/PositionLine.cpp +++ b/src/gui/editors/PositionLine.cpp @@ -93,7 +93,7 @@ void PositionLine::zoomChange(float zoom) { int playHeadPos = x() + width() - 1; - resize(8.0 * zoom, height()); + resize(std::max(8.0 * zoom, 1.0), height()); move(playHeadPos - width() + 1, y()); update(); From 37795ae20ab0db3b2441ca7403436b5649efa72b Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Fri, 31 May 2024 13:11:45 +0200 Subject: [PATCH 191/191] Resizable mixer channels/strips (#7293) ## Make mixer channels resizable Make the mixer channels resizable within the mixer view. Remove the setting of the size policy from `MixerChannelView`. Add the `Fader` widget with a stretch factor so that it is resized within the layout of the mixer channel/strip. Remove the stretch that was added to the layout because the fader now stretches. In `MixerView` remove the top alignments when widgets are added to the layout so that they can resize. Set the channel layout to align to the left so that it behaves correctly when it is resized by the scroll area it is contained in. Make the widget resizable in the scroll area so that it always fills the space. Set the minimum height of the scroll area to the minimum size of the widget plus the scrollbar height so that the channel strips are never overlapped by the scrollbar. Set the size policy of the "new channel" button so that it grows vertically with the mixer view. Set a fixed size so that it is as wide as a mixer strip. ## Enable maximization for mixer view Enable the maximize button for the mixer view now that it is fully resizable. --- src/gui/MixerChannelView.cpp | 5 +---- src/gui/MixerView.cpp | 16 ++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/gui/MixerChannelView.cpp b/src/gui/MixerChannelView.cpp index 22251d551..0afdb684e 100644 --- a/src/gui/MixerChannelView.cpp +++ b/src/gui/MixerChannelView.cpp @@ -51,8 +51,6 @@ namespace lmms::gui m_mixerView(mixerView), m_channelIndex(channelIndex) { - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); - auto retainSizeWhenHidden = [](QWidget* widget) { auto sizePolicy = widget->sizePolicy(); @@ -133,10 +131,9 @@ namespace lmms::gui mainLayout->addWidget(m_sendKnob, 0, Qt::AlignHCenter); mainLayout->addWidget(m_sendArrow, 0, Qt::AlignHCenter); mainLayout->addWidget(m_channelNumberLcd, 0, Qt::AlignHCenter); - mainLayout->addStretch(); mainLayout->addWidget(m_renameLineEditView, 0, Qt::AlignHCenter); mainLayout->addLayout(soloMuteLayout, 0); - mainLayout->addWidget(m_fader, 0, Qt::AlignHCenter); + mainLayout->addWidget(m_fader, 1, Qt::AlignHCenter); connect(m_renameLineEdit, &QLineEdit::editingFinished, this, &MixerChannelView::renameFinished); } diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index e97b5414f..b9a698a96 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -89,6 +89,7 @@ MixerView::MixerView(Mixer* mixer) : chLayout->setSizeConstraint(QLayout::SetMinimumSize); chLayout->setSpacing(0); chLayout->setContentsMargins(0, 0, 0, 0); + chLayout->setAlignment(Qt::AlignLeft); m_channelAreaWidget->setLayout(chLayout); // create rack layout before creating the first channel @@ -105,7 +106,7 @@ MixerView::MixerView(Mixer* mixer) : m_racksLayout->addWidget(m_mixerChannelViews[0]->m_effectRackView); - ml->addWidget(masterView, 0, Qt::AlignTop); + ml->addWidget(masterView, 0); auto mixerChannelSize = masterView->sizeHint(); @@ -137,18 +138,20 @@ MixerView::MixerView(Mixer* mixer) : channelArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); channelArea->setFrameStyle(QFrame::NoFrame); channelArea->setMinimumWidth(mixerChannelSize.width() * 6); + channelArea->setWidgetResizable(true); int const scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent); - channelArea->setFixedHeight(mixerChannelSize.height() + scrollBarExtent); + channelArea->setMinimumHeight(mixerChannelSize.height() + scrollBarExtent); - ml->addWidget(channelArea, 1, Qt::AlignTop); + ml->addWidget(channelArea, 1); // show the add new mixer channel button auto newChannelBtn = new QPushButton(embed::getIconPixmap("new_channel"), QString(), this); newChannelBtn->setObjectName("newChannelBtn"); - newChannelBtn->setFixedSize(mixerChannelSize); + newChannelBtn->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); + newChannelBtn->setFixedWidth(mixerChannelSize.width()); connect(newChannelBtn, SIGNAL(clicked()), this, SLOT(addNewChannel())); - ml->addWidget(newChannelBtn, 0, Qt::AlignTop); + ml->addWidget(newChannelBtn, 0); // add the stacked layout for the effect racks of mixer channels @@ -165,9 +168,6 @@ MixerView::MixerView(Mixer* mixer) : // add ourself to workspace QMdiSubWindow* subWin = mainWindow->addWindowedWidget(this); - Qt::WindowFlags flags = subWin->windowFlags(); - flags &= ~Qt::WindowMaximizeButtonHint; - subWin->setWindowFlags(flags); layout()->setSizeConstraint(QLayout::SetMinimumSize); subWin->layout()->setSizeConstraint(QLayout::SetMinAndMaxSize);