From 5674bb58e2554e3cfc9e01951de27a81c82f7c67 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 29 Jul 2023 09:58:40 +0200 Subject: [PATCH 01/51] Fix HiDPI issues for menus Menus now use the size that users have set globally for their applications. This is accomplished by removing the fixed font size definition (in points) for `QMenu` in `style.css`. In some places the menus had been set to hard coded font sizes. This code is also removed and applies to the following menus: * Combo box menus * Track operation widget (gear icon on tracks) * The MIDI port menu --- data/themes/default/style.css | 4 ---- src/gui/menus/MidiPortMenu.cpp | 1 - src/gui/tracks/TrackOperationsWidget.cpp | 1 - src/gui/widgets/ComboBox.cpp | 1 - 4 files changed, 7 deletions(-) diff --git a/data/themes/default/style.css b/data/themes/default/style.css index a9646cfe49a..12b63cfec96 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -115,7 +115,6 @@ QSplashScreen QLabel { QMenu { border-top: 2px solid #08993E; background-color: #15191c; - font-size: 11px; } QMenu::separator { @@ -133,15 +132,12 @@ QMenu::item { QMenu::item:selected { color: #d1d8e4; - font-weight: normal; background-color: #21272b; } QMenu::item:disabled { color: #515459; background-color: #262b30; - font-size: 12px; - font-weight: normal; padding: 4px 32px 4px 20px; } diff --git a/src/gui/menus/MidiPortMenu.cpp b/src/gui/menus/MidiPortMenu.cpp index b1ddf71c9ad..67ad01f870f 100644 --- a/src/gui/menus/MidiPortMenu.cpp +++ b/src/gui/menus/MidiPortMenu.cpp @@ -34,7 +34,6 @@ MidiPortMenu::MidiPortMenu( MidiPort::Modes _mode ) : ModelView( nullptr, this ), m_mode( _mode ) { - setFont( pointSize<9>( font() ) ); connect( this, SIGNAL(triggered(QAction*)), this, SLOT(activatedPort(QAction*))); } diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index ddbd2eacdb1..3e3654974dc 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -64,7 +64,6 @@ TrackOperationsWidget::TrackOperationsWidget( TrackView * parent ) : "to begin a new drag'n'drop action." ).arg(UI_CTRL_KEY) ); auto toMenu = new QMenu(this); - toMenu->setFont( pointSize<9>( toMenu->font() ) ); connect( toMenu, SIGNAL(aboutToShow()), this, SLOT(updateMenu())); diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index bdf78ccce36..2377a37abf8 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -70,7 +70,6 @@ ComboBox::ComboBox( QWidget * _parent, const QString & _name ) : } setFont( pointSize<9>( font() ) ); - m_menu.setFont( pointSize<8>( m_menu.font() ) ); connect( &m_menu, SIGNAL(triggered(QAction*)), this, SLOT(setItem(QAction*))); From 67dcdf2c65a306933e2b34379a04c2cee85ed8ce Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 29 Jul 2023 10:19:47 +0200 Subject: [PATCH 02/51] Fix HiDPI issue of sample track window Fix one of the HiDPI issues of the sample track window by removing the hard coded font size of the name line edit. --- src/gui/SampleTrackWindow.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gui/SampleTrackWindow.cpp b/src/gui/SampleTrackWindow.cpp index 6fe70bf4128..06afdfd3da8 100644 --- a/src/gui/SampleTrackWindow.cpp +++ b/src/gui/SampleTrackWindow.cpp @@ -74,7 +74,6 @@ SampleTrackWindow::SampleTrackWindow(SampleTrackView * tv) : // setup line edit for changing sample track name m_nameLineEdit = new QLineEdit; - m_nameLineEdit->setFont(pointSize<9>(m_nameLineEdit->font())); connect(m_nameLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(textChanged(const QString&))); From 48cdaddd9dd1a9c1a3716995d752bed567b6c9ad Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 29 Jul 2023 16:21:05 +0200 Subject: [PATCH 03/51] HiDPI fixes for elements based on QTreeView Fix the HiDPI problems for elements that are based on QTreeView by removing the fixed point based font size from the style sheet. This affects: * The file browser * The headings of the plugin browser * The patch selection dialogs for GigPlayer and SoundFont player Also make the text size of the check boxes for user content and factory content based on a point size instead of a pixel size. --- data/themes/default/style.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/themes/default/style.css b/data/themes/default/style.css index a9646cfe49a..52f60e4ed7a 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -9,7 +9,6 @@ QLabel, QTreeWidget, QListWidget, QGroupBox, QMenuBar { QTreeView { outline: none; - font-size: 12px; } QTreeWidget::item { @@ -42,7 +41,7 @@ QMdiArea { lmms--gui--FileBrowser QCheckBox { - font-size: 10px; + font-size: 8pt; color: white; } From 4cb09e2b6058155efc9920ea09a973071ce1c58e Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 27 Aug 2023 18:21:38 +0200 Subject: [PATCH 04/51] Fix the base note automation fix (#6828) Towards the end of the development for the fix of #6548 (via #6725) the upgrade code was refactored into its own class. While doing so it was forgotten to actually call the `upgrade` method on the `UpgradeExtendedNoteRange` instance. As a result almost all files should currently open in a wrong state with many instruments transposed. This commit fixes this. Also explicitly check the assertion that BB tracks do not contain other BB tracks. --- src/core/DataFile.cpp | 2 ++ src/core/UpgradeExtendedNoteRange.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 6ad2f8526d8..5c98ec81c5c 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -1671,6 +1671,8 @@ void DataFile::upgrade_extendedNoteRange() { auto root = documentElement(); UpgradeExtendedNoteRange upgradeExtendedNoteRange(root); + + upgradeExtendedNoteRange.upgrade(); } diff --git a/src/core/UpgradeExtendedNoteRange.cpp b/src/core/UpgradeExtendedNoteRange.cpp index e61da3723c4..6ed98e63e9e 100644 --- a/src/core/UpgradeExtendedNoteRange.cpp +++ b/src/core/UpgradeExtendedNoteRange.cpp @@ -193,6 +193,7 @@ static void fixTrack(QDomElement & track, std::set & automatedBase for (int i = 0; i < subTracks.size(); ++i) { QDomElement subTrack = subTracks.item(i).toElement(); + assert (static_cast(subTrack.attribute("type").toInt()) != Track::Type::Pattern); fixTrack(subTrack, automatedBaseNoteIds); } } From e2fd288ae7413e37e078a1f1b57ba9bf45761a3a Mon Sep 17 00:00:00 2001 From: MrTopom Date: Sun, 27 Aug 2023 20:11:41 +0200 Subject: [PATCH 05/51] =?UTF-8?q?Change=20the=20title=20for=20SideBarWidge?= =?UTF-8?q?ts=20to=20be=20vertically=20centered=20related=E2=80=A6=20(#683?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change the title for SideBarWidgets to be vertically centered related to icon and with no underlining * Update src/gui/SideBarWidget.cpp Co-authored-by: saker --------- Co-authored-by: saker --- src/gui/SideBarWidget.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui/SideBarWidget.cpp b/src/gui/SideBarWidget.cpp index 60760ba5904..c218bedd335 100644 --- a/src/gui/SideBarWidget.cpp +++ b/src/gui/SideBarWidget.cpp @@ -62,16 +62,16 @@ void SideBarWidget::paintEvent( QPaintEvent * ) QFont f = p.font(); f.setBold( true ); - f.setUnderline( true ); + f.setUnderline(false); f.setPointSize( f.pointSize() + 2 ); p.setFont( f ); p.setPen( palette().highlightedText().color() ); - const int tx = m_icon.width()+4; + const int tx = m_icon.width() + 8; QFontMetrics metrics( f ); - const int ty = metrics.ascent(); + const int ty = (metrics.ascent() + m_icon.height()) / 2; p.drawText( tx, ty, m_title ); p.drawPixmap( 2, 2, m_icon.transformed( QTransform().rotate( -90 ) ) ); From 1e6a66f4ac4ecd3890d1dc66268590979cfb2b53 Mon Sep 17 00:00:00 2001 From: saker Date: Mon, 28 Aug 2023 13:14:19 -0400 Subject: [PATCH 06/51] Add mixer LCD channels for Instrument & Sample tracks (#6831) * Add mixer channel LCD to SampleTrackView * Increase sizes to compensate for LCD box The DEFAULT_SETTINGS_WIDGET_WIDTH and DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT were both increased by +32 pixels. TRACK_OP_WIDTH and TRACK_OP_WIDTH_COMPACT were then changed relative to that increase. * Use Qt layout in SampleTrackView * Add mixer channel LCD to InstrumentTrackView * Move LCD box to the right of the track label * Revert changes to TRACK_OP_WIDTH and TRACK_OP_WIDTH_COMPACT --- include/InstrumentTrackView.h | 3 +++ include/SampleTrackView.h | 2 ++ include/TrackView.h | 4 +-- src/gui/tracks/InstrumentTrackView.cpp | 36 ++++++++++++++------------ src/gui/tracks/SampleTrackView.cpp | 23 ++++++++++------ 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index 363f5b3abc0..d7d5fb83a39 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -25,6 +25,7 @@ #ifndef LMMS_GUI_INSTRUMENT_TRACK_VIEW_H #define LMMS_GUI_INSTRUMENT_TRACK_VIEW_H +#include "MixerLineLcdSpinBox.h" #include "TrackView.h" #include "InstrumentTrack.h" @@ -72,6 +73,7 @@ class InstrumentTrackView : public TrackView protected: + void modelChanged() override; void dragEnterEvent( QDragEnterEvent * _dee ) override; void dropEvent( QDropEvent * _de ) override; @@ -97,6 +99,7 @@ private slots: // widgets in track-settings-widget TrackLabelButton * m_tlb; + MixerLineLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/SampleTrackView.h b/include/SampleTrackView.h index b586df15edc..3ccb97aeaa9 100644 --- a/include/SampleTrackView.h +++ b/include/SampleTrackView.h @@ -26,6 +26,7 @@ #define LMMS_GUI_SAMPLE_TRACK_VIEW_H +#include "MixerLineLcdSpinBox.h" #include "TrackView.h" namespace lmms @@ -90,6 +91,7 @@ private slots: private: SampleTrackWindow * m_window; + MixerLineLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/TrackView.h b/include/TrackView.h index 763705599c5..f697d9ea86a 100644 --- a/include/TrackView.h +++ b/include/TrackView.h @@ -48,11 +48,11 @@ class FadeButton; class TrackContainerView; -const int DEFAULT_SETTINGS_WIDGET_WIDTH = 224; +const int DEFAULT_SETTINGS_WIDGET_WIDTH = 256; const int TRACK_OP_WIDTH = 78; // This shaves 150-ish pixels off track buttons, // ruled from config: ui.compacttrackbuttons -const int DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT = 96; +const int DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT = 128; const int TRACK_OP_WIDTH_COMPACT = 62; diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 669fdaccb8f..87c0f044944 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -63,7 +63,6 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_tlb = new TrackLabelButton( this, getTrackSettingsWidget() ); m_tlb->setCheckable( true ); m_tlb->setIcon( embed::getIconPixmap( "instrument_track" ) ); - m_tlb->move( 3, 1 ); m_tlb->show(); connect( m_tlb, SIGNAL(toggled(bool)), @@ -75,24 +74,14 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV connect(ConfigManager::inst(), SIGNAL(valueChanged(QString,QString,QString)), this, SLOT(handleConfigChange(QString,QString,QString))); - // creation of widgets for track-settings-widget - int widgetWidth; - if( ConfigManager::inst()->value( "ui", - "compacttrackbuttons" ).toInt() ) - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT; - } - else - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH; - } + m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber->show(); m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), tr( "Volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_it->m_volumeModel ); m_volumeKnob->setHintText( tr( "Volume:" ), "%" ); - m_volumeKnob->move( widgetWidth-2*24, 2 ); m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); @@ -100,7 +89,6 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV tr( "Panning" ) ); m_panningKnob->setModel( &_it->m_panningModel ); m_panningKnob->setHintText(tr("Panning:"), "%"); - m_panningKnob->move( widgetWidth-24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -151,9 +139,18 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV QApplication::palette().color( QPalette::Active, QPalette::BrightText).darker(), getTrackSettingsWidget() ); - m_activityIndicator->setGeometry( - widgetWidth-2*24-11, 2, 8, 28 ); + m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); + + auto layout = new QHBoxLayout(getTrackSettingsWidget()); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tlb); + layout->addWidget(m_mixerChannelNumber); + layout->addWidget(m_activityIndicator); + layout->addWidget(m_volumeKnob); + layout->addWidget(m_panningKnob); + connect( m_activityIndicator, SIGNAL(pressed()), this, SLOT(activityIndicatorPressed())); connect( m_activityIndicator, SIGNAL(released()), @@ -268,6 +265,13 @@ void InstrumentTrackView::handleConfigChange(QString cls, QString attr, QString } } +void InstrumentTrackView::modelChanged() +{ + TrackView::modelChanged(); + auto st = castModel(); + m_mixerChannelNumber->setModel(&st->m_mixerChannelModel); +} + void InstrumentTrackView::dragEnterEvent( QDragEnterEvent * _dee ) { InstrumentTrackWindow::dragEnterEventGeneric( _dee ); diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 6a6a2c5fd2e..8516eb5c2a9 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -56,20 +56,17 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : connect(m_tlb, SIGNAL(clicked(bool)), this, SLOT(showEffects())); m_tlb->setIcon(embed::getIconPixmap("sample_track")); - m_tlb->move(3, 1); m_tlb->show(); + m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber->show(); + m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), tr( "Track volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_t->m_volumeModel ); m_volumeKnob->setHintText( tr( "Channel volume:" ), "%" ); - int settingsWidgetWidth = ConfigManager::inst()-> - value( "ui", "compacttrackbuttons" ).toInt() - ? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT - : DEFAULT_SETTINGS_WIDGET_WIDTH; - m_volumeKnob->move( settingsWidgetWidth - 2 * 24, 2 ); m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); @@ -77,7 +74,6 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : tr( "Panning" ) ); m_panningKnob->setModel( &_t->m_panningModel ); m_panningKnob->setHintText( tr( "Panning:" ), "%" ); - m_panningKnob->move( settingsWidgetWidth - 24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -87,8 +83,18 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : QApplication::palette().color(QPalette::Active, QPalette::BrightText).darker(), getTrackSettingsWidget() ); - m_activityIndicator->setGeometry(settingsWidgetWidth - 2 * 24 - 11, 2, 8, 28); + m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); + + auto layout = new QHBoxLayout(getTrackSettingsWidget()); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tlb); + layout->addWidget(m_mixerChannelNumber); + layout->addWidget(m_activityIndicator); + layout->addWidget(m_volumeKnob); + layout->addWidget(m_panningKnob); + connect(_t, SIGNAL(playingChanged()), this, SLOT(updateIndicator())); setModel( _t ); @@ -170,6 +176,7 @@ void SampleTrackView::modelChanged() { auto st = castModel(); m_volumeKnob->setModel(&st->m_volumeModel); + m_mixerChannelNumber->setModel(&st->m_mixerChannelModel); TrackView::modelChanged(); } From d2a0780a6eca1fbddc7b3cd2dc050d283799883d Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Mon, 28 Aug 2023 20:12:09 +0200 Subject: [PATCH 07/51] Adjust classic style sheet Adjust the classic style sheet in the same way that the default style sheet was adjusted. The change also removes the usage of a bold font weight for selected menu items to achieve a more consistent feel. --- data/themes/classic/style.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 2880fe661fe..777dbc230f0 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -82,7 +82,6 @@ lmms--gui--TextFloat, lmms--gui--SimpleTextFloat { QMenu { border:1px solid #747474; background-color: #c9c9c9; - font-size:11px; } QMenu::separator { @@ -98,15 +97,12 @@ QMenu::item { QMenu::item:selected { color: white; - font-weight:bold; background-color: #747474; } QMenu::item:disabled { color: #747474; background-color: #c9c9c9; - font-size:12px; - font-weight: normal; padding: 4px 32px 4px 20px; } From fcdf4c0568407762db38c180684cea9af101a937 Mon Sep 17 00:00:00 2001 From: MrTopom Date: Tue, 29 Aug 2023 21:32:11 +0200 Subject: [PATCH 08/51] Showing Knob value on mouse over (#6835) * Showing Knob value on mouse over * Correcting minors source code issues * Correcting double QTimer include * Removing blank lines * Removing space and add one * Update src/gui/widgets/SimpleTextFloat.cpp Co-authored-by: saker * Correcting QTimer * Remove a parameter that has the default value --------- Co-authored-by: saker --- include/Knob.h | 4 ++++ include/SimpleTextFloat.h | 8 +++++++- src/gui/widgets/Knob.cpp | 26 ++++++++++++++++++------ src/gui/widgets/SimpleTextFloat.cpp | 31 +++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/include/Knob.h b/include/Knob.h index 85a51e363e8..d5739bb1c3d 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -144,6 +144,9 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView 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: @@ -160,6 +163,7 @@ private slots: float _innerRadius = 1) const; void drawKnob( QPainter * _p ); + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); void setPosition( const QPoint & _p ); bool updateAngle(); diff --git a/include/SimpleTextFloat.h b/include/SimpleTextFloat.h index f720d0b3ef3..bde6c84faab 100644 --- a/include/SimpleTextFloat.h +++ b/include/SimpleTextFloat.h @@ -31,6 +31,7 @@ #include "lmms_export.h" class QLabel; +class QTimer; namespace lmms::gui { @@ -44,6 +45,8 @@ class LMMS_EXPORT SimpleTextFloat : public QWidget void setText(const QString & text); + void showWithDelay(int msecBeforeDisplay, int msecDisplayTime); + void setVisibilityTimeOut(int msecs); void moveGlobal(QWidget * w, const QPoint & offset) @@ -51,11 +54,14 @@ class LMMS_EXPORT SimpleTextFloat : public QWidget move(w->mapToGlobal(QPoint(0, 0)) + offset); } + void hide(); + private: QLabel * m_textLabel; + QTimer * m_showTimer; + QTimer * m_hideTimer; }; - } // namespace lmms::gui #endif diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index c2f90fb2b71..56cf29345d0 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -22,6 +22,8 @@ * */ +#include "Knob.h" + #include #include #include @@ -34,7 +36,6 @@ #endif #include "lmms_math.h" -#include "Knob.h" #include "CaptionMenu.h" #include "ConfigManager.h" #include "ControllerConnection.h" @@ -48,7 +49,6 @@ #include "SimpleTextFloat.h" #include "StringPairDrag.h" - namespace lmms::gui { @@ -484,6 +484,13 @@ 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; @@ -580,10 +587,8 @@ void Knob::mousePressEvent( QMouseEvent * _me ) emit sliderPressed(); - s_textFloat->setText( displayValue() ); - s_textFloat->moveGlobal( this, - QPoint( width() + 2, 0 ) ); - s_textFloat->show(); + showTextFloat(0, 0); + m_buttonPressed = true; } else if( _me->button() == Qt::LeftButton && @@ -613,6 +618,7 @@ void Knob::mouseMoveEvent( QMouseEvent * _me ) m_lastMousePos = _me->pos(); } s_textFloat->setText( displayValue() ); + s_textFloat->show(); } @@ -638,7 +644,15 @@ void Knob::mouseReleaseEvent( QMouseEvent* event ) s_textFloat->hide(); } +void Knob::enterEvent(QEvent *event) +{ + showTextFloat(700, 2000); +} +void Knob::leaveEvent(QEvent *event) +{ + s_textFloat->hide(); +} void Knob::focusOutEvent( QFocusEvent * _fe ) diff --git a/src/gui/widgets/SimpleTextFloat.cpp b/src/gui/widgets/SimpleTextFloat.cpp index d1f490b5ef9..df438423e27 100644 --- a/src/gui/widgets/SimpleTextFloat.cpp +++ b/src/gui/widgets/SimpleTextFloat.cpp @@ -45,6 +45,14 @@ SimpleTextFloat::SimpleTextFloat() : m_textLabel = new QLabel(this); layout->addWidget(m_textLabel); + + m_showTimer = new QTimer(); + m_showTimer->setSingleShot(true); + QObject::connect(m_showTimer, &QTimer::timeout, this, &SimpleTextFloat::show); + + m_hideTimer = new QTimer(); + m_hideTimer->setSingleShot(true); + QObject::connect(m_hideTimer, &QTimer::timeout, this, &SimpleTextFloat::hide); } void SimpleTextFloat::setText(const QString & text) @@ -52,6 +60,29 @@ void SimpleTextFloat::setText(const QString & text) m_textLabel->setText(text); } +void SimpleTextFloat::showWithDelay(int msecBeforeDisplay, int msecDisplayTime) +{ + if (msecBeforeDisplay != 0) + { + m_showTimer->start(msecBeforeDisplay); + } + else + { + show(); + } + + if (msecDisplayTime != 0) + { + m_hideTimer->start(msecBeforeDisplay + msecDisplayTime); + } +} + +void SimpleTextFloat::hide() +{ + m_showTimer->stop(); + m_hideTimer->stop(); + QWidget::hide(); +} void SimpleTextFloat::setVisibilityTimeOut(int msecs) { From 3263bfd555982d52735a19763eff26f4be7e0eb7 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Thu, 31 Aug 2023 02:01:15 +0100 Subject: [PATCH 09/51] Fix generator expression in strip command (#6762) * Fix generator expression in strip command * Add TODO comment for CMake 3.19 --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a68b788d68e..1ec3f992e54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -605,7 +605,9 @@ else() 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() From 4804ab678501f9c746b4b2c870f0a323f7d3953b Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Thu, 31 Aug 2023 12:12:00 +0100 Subject: [PATCH 10/51] Support per-note detuning and panning with Sf2 Player (#6602) * Add `ArrayVector` class template and tests * Fix counting of failed test suites * Support detuning and panning with Sf2 Player * Restrict panning to supported FluidSynth versions * Fix data array cast type * Fix tests for Qt<5.10 and correct mistaken test * DIsplay warning for FluidSynth < 2 * Remove unnecessary clamp * Update include guard name --- CMakeLists.txt | 6 +- include/ArrayVector.h | 388 ++++++++++++++ include/NotePlayHandle.h | 3 + plugins/Sf2Player/Sf2Player.cpp | 99 +++- tests/CMakeLists.txt | 1 + tests/main.cpp | 2 +- tests/src/core/ArrayVectorTest.cpp | 831 +++++++++++++++++++++++++++++ 7 files changed, 1306 insertions(+), 24 deletions(-) create mode 100644 include/ArrayVector.h create mode 100644 tests/src/core/ArrayVectorTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ec3f992e54..e20f1352701 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -494,7 +494,11 @@ IF(WANT_SF2) find_package(FluidSynth 1.1.0) if(FluidSynth_FOUND) SET(LMMS_HAVE_FLUIDSYNTH TRUE) - SET(STATUS_FLUIDSYNTH "OK") + if(FluidSynth_VERSION_STRING VERSION_GREATER_EQUAL 2) + set(STATUS_FLUIDSYNTH "OK") + else() + set(STATUS_FLUIDSYNTH "OK (FluidSynth version < 2: per-note panning unsupported)") + endif() else() SET(STATUS_FLUIDSYNTH "not found, libfluidsynth-dev (or similar)" "is highly recommended") diff --git a/include/ArrayVector.h b/include/ArrayVector.h new file mode 100644 index 00000000000..06e09226cf7 --- /dev/null +++ b/include/ArrayVector.h @@ -0,0 +1,388 @@ +/* + * ArrayVector.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_ARRAY_VECTOR_H +#define LMMS_ARRAY_VECTOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lmms { + +namespace detail { + +template +constexpr bool is_input_iterator_v = false; + +template +constexpr bool is_input_iterator_v::iterator_category>> = + std::is_convertible_v::iterator_category, std::input_iterator_tag>; + +} // namespace detail + +/** + * A container that stores up to a maximum of `N` elements of type `T` directly + * within itself, rather than separately on the heap. Useful when a dynamically + * resizeable container is needed for use in real-time code. Can be thought of + * as a hybrid between `std::array` and `std::vector`. The interface follows + * that of `std::vector` - see standard C++ documentation. + */ +template +class ArrayVector +{ +public: + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using value_type = T; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = pointer; + using const_iterator = const_pointer; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + ArrayVector() = default; + + ArrayVector(const ArrayVector& other) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{other.m_size} + { + std::uninitialized_copy(other.begin(), other.end(), begin()); + } + + ArrayVector(ArrayVector&& other) noexcept(std::is_nothrow_move_constructible_v) : + m_size{other.m_size} + { + std::uninitialized_move(other.begin(), other.end(), begin()); + other.clear(); + } + + ArrayVector(size_type count, const T& value) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{count} + { + assert(count <= N); + std::uninitialized_fill_n(begin(), count, value); + } + + explicit ArrayVector(size_type count) noexcept(std::is_nothrow_default_constructible_v) : + m_size{count} + { + assert(count <= N); + std::uninitialized_value_construct_n(begin(), count); + } + + template, int> = 0> + ArrayVector(It first, It last) + { + // Can't check the size first as the iterator may not be multipass + const auto end = std::uninitialized_copy(first, last, begin()); + m_size = end - begin(); + assert(m_size <= N); + } + + ArrayVector(std::initializer_list il) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{il.size()} + { + assert(il.size() <= N); + std::uninitialized_copy(il.begin(), il.end(), begin()); + } + + ~ArrayVector() { std::destroy(begin(), end()); } + + ArrayVector& operator=(const ArrayVector& other) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + if (this != &other) { + const auto toAssign = std::min(other.size(), size()); + const auto assignedFromEnd = other.begin() + toAssign; + const auto assignedToEnd = std::copy(other.begin(), other.begin() + toAssign, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_copy(assignedFromEnd, other.end(), end()); + m_size = other.size(); + } + return *this; + } + + ArrayVector& operator=(ArrayVector&& other) + noexcept(std::is_nothrow_move_assignable_v && std::is_nothrow_move_constructible_v) + { + if (this != &other) { + const auto toAssign = std::min(other.size(), size()); + const auto assignedFromEnd = other.begin() + toAssign; + const auto assignedToEnd = std::move(other.begin(), other.begin() + toAssign, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_move(assignedFromEnd, other.end(), end()); + m_size = other.size(); + other.clear(); + } + return *this; + } + + ArrayVector& operator=(std::initializer_list il) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + assert(il.size() <= N); + const auto toAssign = std::min(il.size(), size()); + const auto assignedFromEnd = il.begin() + toAssign; + const auto assignedToEnd = std::copy(il.begin(), assignedFromEnd, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_copy(assignedFromEnd, il.end(), end()); + m_size = il.size(); + return *this; + } + + void assign(size_type count, const T& value) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + assert(count <= N); + const auto temp = value; + const auto toAssign = std::min(count, size()); + const auto toConstruct = count - toAssign; + const auto assignedToEnd = std::fill_n(begin(), toAssign, temp); + std::destroy(assignedToEnd, end()); + std::uninitialized_fill_n(assignedToEnd, toConstruct, temp); + m_size = count; + } + + template, int> = 0> + void assign(It first, It last) + { + // Can't check the size first as the iterator may not be multipass + auto pos = begin(); + for (; first != last && pos != end(); ++pos, ++first) { + *pos = *first; + } + std::destroy(pos, end()); + pos = std::uninitialized_copy(first, last, pos); + m_size = pos - begin(); + assert(m_size <= N); + } + + reference at(size_type index) + { + if (index >= m_size) { throw std::out_of_range{"index out of range"}; } + return data()[index]; + } + + const_reference at(size_type index) const + { + if (index >= m_size) { throw std::out_of_range{"index out of range"}; } + return data()[index]; + } + + reference operator[](size_type index) noexcept + { + assert(index < m_size); + return data()[index]; + } + + const_reference operator[](size_type index) const noexcept + { + assert(index < m_size); + return data()[index]; + } + + reference front() noexcept { return operator[](0); } + const_reference front() const noexcept { return operator[](0); } + + reference back() noexcept { return operator[](m_size - 1); } + const_reference back() const noexcept { return operator[](m_size - 1); } + + pointer data() noexcept { return *std::launder(reinterpret_cast(m_data)); } + const_pointer data() const noexcept { return *std::launder(reinterpret_cast(m_data)); } + + iterator begin() noexcept { return data(); } + const_iterator begin() const noexcept { return data(); } + const_iterator cbegin() const noexcept { return data(); } + + iterator end() noexcept { return data() + m_size; } + const_iterator end() const noexcept { return data() + m_size; } + const_iterator cend() const noexcept { return data() + m_size; } + + reverse_iterator rbegin() noexcept { return std::reverse_iterator{end()}; } + const_reverse_iterator rbegin() const noexcept { return std::reverse_iterator{end()}; } + const_reverse_iterator crbegin() const noexcept { return std::reverse_iterator{cend()}; } + + reverse_iterator rend() noexcept { return std::reverse_iterator{begin()}; } + const_reverse_iterator rend() const noexcept { return std::reverse_iterator{begin()}; } + const_reverse_iterator crend() const noexcept { return std::reverse_iterator{cbegin()}; } + + bool empty() const noexcept { return m_size == 0; } + bool full() const noexcept { return m_size == N; } + size_type size() const noexcept { return m_size; } + size_type max_size() const noexcept { return N; } + size_type capacity() const noexcept { return N; } + + void clear() noexcept + { + std::destroy(begin(), end()); + m_size = 0; + } + + iterator insert(const_iterator pos, const T& value) { return emplace(pos, value); } + iterator insert(const_iterator pos, T&& value) { return emplace(pos, std::move(value)); } + + iterator insert(const_iterator pos, size_type count, const T& value) + { + assert(m_size + count <= N); + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + const auto newEnd = std::uninitialized_fill_n(end(), count, value); + std::rotate(mutPos, end(), newEnd); + m_size += count; + return mutPos; + } + + template, int> = 0> + iterator insert(const_iterator pos, It first, It last) + { + // Can't check the size first as the iterator may not be multipass + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + const auto newEnd = std::uninitialized_copy(first, last, end()); + std::rotate(mutPos, end(), newEnd); + m_size = newEnd - begin(); + assert(m_size <= N); + return mutPos; + } + + iterator insert(const_iterator pos, std::initializer_list il) { return insert(pos, il.begin(), il.end()); } + + template + iterator emplace(const_iterator pos, Args&&... args) + { + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + emplace_back(std::forward(args)...); + std::rotate(mutPos, end() - 1, end()); + return mutPos; + } + + iterator erase(const_iterator pos) { return erase(pos, pos + 1); } + iterator erase(const_iterator first, const_iterator last) + { + assert(cbegin() <= first && first <= last && last <= cend()); + const auto mutFirst = begin() + (first - cbegin()); + const auto mutLast = begin() + (last - cbegin()); + const auto newEnd = std::move(mutLast, end(), mutFirst); + std::destroy(newEnd, end()); + m_size = newEnd - begin(); + return mutFirst; + } + + void push_back(const T& value) { emplace_back(value); } + void push_back(T&& value) { emplace_back(std::move(value)); } + + template + reference emplace_back(Args&&... args) + { + assert(!full()); + // TODO C++20: Use std::construct_at + const auto result = new(static_cast(end())) T(std::forward(args)...); + ++m_size; + return *result; + } + + void pop_back() + { + assert(!empty()); + --m_size; + std::destroy_at(end()); + } + + void resize(size_type size) + { + if (size > N) { throw std::length_error{"size exceeds maximum size"}; } + if (size < m_size) { + std::destroy(begin() + size, end()); + } else { + std::uninitialized_value_construct(end(), begin() + size); + } + m_size = size; + } + + void resize(size_type size, const value_type& value) + { + if (size > N) { throw std::length_error{"size exceeds maximum size"}; } + if (size < m_size) { + std::destroy(begin() + size, end()); + } else { + std::uninitialized_fill(end(), begin() + size, value); + } + m_size = size; + } + + void swap(ArrayVector& other) + noexcept(std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v) + { + using std::swap; + swap(*this, other); + } + + friend void swap(ArrayVector& a, ArrayVector& b) + noexcept(std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v) + { + const auto toSwap = std::min(a.size(), b.size()); + const auto aSwapEnd = a.begin() + toSwap; + const auto bSwapEnd = b.begin() + toSwap; + std::swap_ranges(a.begin(), aSwapEnd, b.begin()); + std::uninitialized_move(aSwapEnd, a.end(), bSwapEnd); + std::uninitialized_move(bSwapEnd, b.end(), aSwapEnd); + std::destroy(aSwapEnd, a.end()); + std::destroy(bSwapEnd, b.end()); + std::swap(a.m_size, b.m_size); + } + + // TODO C++20: Replace with operator<=> + friend bool operator<(const ArrayVector& l, const ArrayVector& r) + { + return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); + } + friend bool operator<=(const ArrayVector& l, const ArrayVector& r) { return !(r < l); } + friend bool operator>(const ArrayVector& l, const ArrayVector& r) { return r < l; } + friend bool operator>=(const ArrayVector& l, const ArrayVector& r) { return !(l < r); } + + friend bool operator==(const ArrayVector& l, const ArrayVector& r) + { + return std::equal(l.begin(), l.end(), r.begin(), r.end()); + } + // TODO C++20: Remove + friend bool operator!=(const ArrayVector& l, const ArrayVector& r) { return !(l == r); } + +private: + alignas(T) std::byte m_data[std::max(N * sizeof(T), std::size_t{1})]; // Intentionally a raw array + size_type m_size = 0; +}; + +} // namespace lmms + +#endif // LMMS_ARRAY_VECTOR_H diff --git a/include/NotePlayHandle.h b/include/NotePlayHandle.h index 46b14c4cd44..61df5a77aa3 100644 --- a/include/NotePlayHandle.h +++ b/include/NotePlayHandle.h @@ -108,6 +108,9 @@ class LMMS_EXPORT NotePlayHandle : public PlayHandle, public Note return m_unpitchedFrequency; } + //! Get the current per-note detuning for this note + float currentDetuning() const { return m_baseDetuning->value(); } + /*! Renders one chunk using the attached instrument into the buffer */ void play( sampleFrame* buffer ) override; diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 1f0cb7c59e2..d46af5e4f6e 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -30,6 +30,7 @@ #include #include +#include "ArrayVector.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "FileDialog.h" @@ -71,17 +72,47 @@ 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. + */ +class FluidVoice +{ +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)} + { } + + //! Get a pointer to the referenced voice. + fluid_voice_t* get() const noexcept { return m_voice; } + + //! Test whether this object still refers to the original voice. + bool isValid() const + { + return fluid_voice_get_id(m_voice) == m_id && fluid_voice_is_playing(m_voice); + } + +private: + fluid_voice_t* m_voice; + unsigned int m_id; +}; struct Sf2PluginData { int midiNote; int lastPanning; float lastVelocity; - fluid_voice_t * fluidVoice; + // The soundfonts I checked used at most two voices per note, so space for + // four should be safe. This may need to be increased if a soundfont with + // more voices per note is found. + ArrayVector fluidVoices; bool isNew; f_cnt_t offset; bool noteOffSent; -} ; + panning_t panning; +}; @@ -681,10 +712,10 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) pluginData->midiNote = midiNote; pluginData->lastPanning = 0; pluginData->lastVelocity = _n->midiVelocity( baseVelocity ); - pluginData->fluidVoice = nullptr; pluginData->isNew = true; pluginData->offset = _n->offset(); pluginData->noteOffSent = false; + pluginData->panning = _n->getPanning(); _n->m_pluginData = pluginData; @@ -703,6 +734,17 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) m_playingNotes.append( _n ); m_playingNotesMutex.unlock(); } + + // Update the pitch of all the voices + if (const auto data = static_cast(_n->m_pluginData)) { + 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_update_param(voice.get(), GEN_COARSETUNE); + } + } + } } @@ -715,34 +757,46 @@ void Sf2Instrument::noteOn( Sf2PluginData * n ) const int poly = fluid_synth_get_polyphony( m_synth ); #ifndef _MSC_VER fluid_voice_t* voices[poly]; - unsigned int id[poly]; #else const auto voices = static_cast(_alloca(poly * sizeof(fluid_voice_t*))); - const auto id = static_cast(_alloca(poly * sizeof(unsigned int))); #endif - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly; ++i ) - { - id[i] = 0; - } - for( int i = 0; i < poly && voices[i]; ++i ) - { - id[i] = fluid_voice_get_id( voices[i] ); - } fluid_synth_noteon( m_synth, m_channel, n->midiNote, n->lastVelocity ); - // get new voice and save it - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly && voices[i]; ++i ) + // Get any new voices and store them in the plugin data + fluid_synth_get_voicelist(m_synth, voices, poly, -1); + for (int i = 0; i < poly && voices[i] && !n->fluidVoices.full(); ++i) { - const unsigned int newID = fluid_voice_get_id( voices[i] ); - if( id[i] != newID || newID == 0 ) - { - n->fluidVoice = voices[i]; - break; + const auto voice = voices[i]; + // FluidSynth stops voices with the same channel and pitch upon note-on, + // so voices with the current channel and pitch are playing this note. + if (fluid_voice_get_channel(voice) == m_channel + && fluid_voice_get_key(voice) == n->midiNote + && fluid_voice_is_on(voice) + ) { + n->fluidVoices.emplace_back(voices[i]); + } + } + +#if FLUIDSYNTH_VERSION_MAJOR >= 2 + // Smallest balance value that results in full attenuation of one channel. + // Corresponds to internal FluidSynth macro `FLUID_CB_AMP_SIZE`. + constexpr static auto maxBalance = 1441.f; + // Convert panning from linear to exponential for FluidSynth + const auto panning = n->panning; + const auto factor = 1.f - std::abs(panning) / static_cast(PanningRight); + const auto balance = std::copysign( + factor <= 0 ? maxBalance : std::min(-200.f * std::log10(factor), maxBalance), + panning + ); + // Set note panning on all the voices + for (const auto& voice : n->fluidVoices) { + if (voice.isValid()) { + fluid_voice_gen_set(voice.get(), GEN_CUSTOM_BALANCE, balance); + fluid_voice_update_param(voice.get(), GEN_CUSTOM_BALANCE); } } +#endif m_synthMutex.unlock(); @@ -859,6 +913,7 @@ void Sf2Instrument::play( sampleFrame * _working_buffer ) 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() && m_srcState != nullptr ) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6ff9c41e967..514bf78154e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,7 @@ ADD_EXECUTABLE(tests QTestSuite.cpp $ + src/core/ArrayVectorTest.cpp src/core/AutomatableModelTest.cpp src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp diff --git a/tests/main.cpp b/tests/main.cpp index 6d375e6c657..c1a5b5a1016 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -16,7 +16,7 @@ int main(int argc, char* argv[]) int failed = 0; for (QTestSuite*& suite : QTestSuite::suites()) { - failed += QTest::qExec(suite, argc, argv); + if (QTest::qExec(suite, argc, argv) != 0) { ++failed; } } qDebug() << "<<" << failed << "out of"< +#include + +#include "QTestSuite.h" + +using lmms::ArrayVector; + +struct ShouldNotConstruct +{ + ShouldNotConstruct() { QFAIL("should not construct"); } +}; + +struct ShouldNotDestruct +{ + ~ShouldNotDestruct() { QFAIL("should not destruct"); } +}; + +enum class Construction { Default, Copy, Move, CopyAssign, MoveAssign }; + +struct Constructible +{ + Constructible() : construction{Construction::Default} {} + Constructible(const Constructible&) : construction{Construction::Copy} {} + Constructible(Constructible&&) : construction{Construction::Move} {} + Constructible& operator=(const Constructible&) { construction = Construction::CopyAssign; return *this; } + Constructible& operator=(Constructible&&) { construction = Construction::MoveAssign; return *this; } + Construction construction; +}; + +struct DestructorCheck +{ + ~DestructorCheck() { *destructed = true; } + bool* destructed; +}; + +class ArrayVectorTest : QTestSuite +{ + Q_OBJECT + +private slots: + void defaultConstructorTest() + { + // Ensure no elements are constructed + const auto v = ArrayVector(); + // Ensure the container is empty + QVERIFY(v.empty()); + } + + void copyConstructorTest() + { + { + // Ensure all elements are copy constructed + const auto v = ArrayVector{{}}; + const auto copy = v; + for (const auto& element : copy) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + const auto v = ArrayVector{1, 2, 3}; + const auto copy = v; + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(copy.begin(), copy.end(), expected.begin(), expected.end())); + } + } + + void moveConstructorTest() + { + { + // Ensure all elements are move constructed + auto v = ArrayVector{{}}; + const auto moved = std::move(v); + for (const auto& element : moved) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{1, 2, 3}; + const auto moved = std::move(v); + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(moved.begin(), moved.end(), expected.begin(), expected.end())); + // Move construction should leave the source empty + QVERIFY(v.empty()); + } + } + + void fillValueConstructorTest() + { + // Ensure all elements are copy constructed + const auto v = ArrayVector(1, {}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + // Ensure the container has the correct size + QCOMPARE(v.size(), std::size_t{1}); + } + + void fillDefaultConstructorTest() + { + // Ensure all elements are copy constructed + const auto v = ArrayVector(1); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Default); + } + // Ensure the container has the correct size + QCOMPARE(v.size(), std::size_t{1}); + } + + void rangeConstructorTest() + { + { + // Ensure the elements are copy constructed from normal iterators + const auto data = std::array{Constructible{}}; + const auto v = ArrayVector(data.begin(), data.end()); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure the elements are move constructed from move iterators + auto data = std::array{Constructible{}}; + const auto v = ArrayVector( + std::move_iterator{data.begin()}, std::move_iterator{data.end()}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + const auto data = std::array{1, 2, 3}; + const auto v = ArrayVector(data.begin(), data.end()); + QVERIFY(std::equal(v.begin(), v.end(), data.begin(), data.end())); + } + } + + void initializerListConstructorTest() + { + // Ensure the container is constructed with the correct data + const auto v = ArrayVector{1, 2, 3}; + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + } + + void destructorTest() + { + { + // Should not call destructors for space without elements + const auto v = ArrayVector{}; + } + { + // Should call destructors for all elements + auto destructed = false; + { + const auto v = ArrayVector{{&destructed}}; + } + QVERIFY(destructed); + } + } + + void copyAssignmentTest() + { + { + // Self-assignment should not change the contents + auto v = ArrayVector{1, 2, 3}; + const auto oldValue = v; + v = v; + QCOMPARE(v, oldValue); + } + { + // Assignment to a larger container should copy assign + const auto src = ArrayVector(3); + auto dst = ArrayVector(5); + dst = src; + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + const auto src = ArrayVector(3); + auto dst = ArrayVector{}; + dst = src; + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + const auto src = ArrayVector{1, 2, 3}; + auto dst = ArrayVector{}; + dst = src; + QCOMPARE(dst, (ArrayVector{1, 2, 3})); + } + } + + void moveAssignmentTest() + { + { + // Self-assignment should not change the contents + auto v = ArrayVector{1, 2, 3}; + const auto oldValue = v; + v = std::move(v); + QCOMPARE(v, oldValue); + } + { + // Assignment to a larger container should move assign + auto src = ArrayVector(3); + auto dst = ArrayVector(5); + dst = std::move(src); + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::MoveAssign); + } + } + { + // Assignment to a smaller container should move construct + auto src = ArrayVector(3); + auto dst = ArrayVector{}; + dst = std::move(src); + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + auto src = ArrayVector{1, 2, 3}; + auto dst = ArrayVector{}; + dst = std::move(src); + QCOMPARE(dst, (ArrayVector{1, 2, 3})); + } + } + + void initializerListAssignmentTest() + { + { + // Assignment to a larger container should copy assign + auto v = ArrayVector(2); + v = {Constructible{}}; + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + auto v = ArrayVector{}; + v = {Constructible{}}; + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{}; + v = {1, 2, 3}; + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void fillValueAssignTest() + { + { + // Assignment to a larger container should copy assign + auto v = ArrayVector(5); + v.assign(3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + auto v = ArrayVector{}; + v.assign(3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + auto v = ArrayVector{}; + v.assign(3, 1); + QCOMPARE(v, (ArrayVector{1, 1, 1})); + } + } + + void rangeAssignTest() + { + { + // Assignment to a larger container should copy assign + const auto data = std::array{Constructible{}}; + auto v = ArrayVector(2); + v.assign(data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + const auto data = std::array{Constructible{}}; + auto v = ArrayVector{}; + v.assign(data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + const auto data = std::array{1, 2, 3}; + auto v = ArrayVector{}; + v.assign(data.begin(), data.end()); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void atTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.at(1), 2); + QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.at(1), 2); + QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range); + } + } + + void subscriptTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v[1], 2); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v[1], 2); + } + } + + void frontTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.front(), 1); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.front(), 1); + } + } + + void backTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.back(), 3); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.back(), 3); + } + } + + void dataTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.data(), &v.front()); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.data(), &v.front()); + } + } + + void beginEndTest() + { + const auto expected = std::array{1, 2, 3}; + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + QVERIFY(std::equal(v.cbegin(), v.cend(), expected.begin(), expected.end())); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + } + } + + void rbeginRendTest() + { + const auto expected = std::array{3, 2, 1}; + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end())); + QVERIFY(std::equal(v.crbegin(), v.crend(), expected.begin(), expected.end())); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end())); + } + } + + void emptyFullSizeMaxCapacityTest() + { + auto v = ArrayVector{}; + QVERIFY(v.empty()); + QVERIFY(!v.full()); + QCOMPARE(v.size(), std::size_t{0}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + v.push_back(1); + QVERIFY(!v.empty()); + QVERIFY(!v.full()); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + v.push_back(2); + QVERIFY(!v.empty()); + QVERIFY(v.full()); + QCOMPARE(v.size(), std::size_t{2}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + auto empty = ArrayVector{}; + QVERIFY(empty.empty()); + QVERIFY(empty.full()); + QCOMPARE(empty.size(), std::size_t{0}); + QCOMPARE(empty.max_size(), std::size_t{0}); + QCOMPARE(empty.capacity(), std::size_t{0}); + } + + void insertValueTest() + { + { + // Copy + const auto data = Constructible{}; + auto v = ArrayVector{}; + v.insert(v.cbegin(), data); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Move + auto v = ArrayVector{}; + v.insert(v.cbegin(), Constructible{}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Move); + } + { + // Ensure the correct value is used (copy) + const auto data = 1; + auto v = ArrayVector{2, 3}; + v.insert(v.cbegin(), data); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{2, 3}; + v.insert(v.cbegin(), 1); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void insertFillValueTest() + { + { + // Insertion should copy construct + auto v = ArrayVector{}; + v.insert(v.cbegin(), 3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + auto v = ArrayVector{1, 3}; + v.insert(v.cbegin() + 1, 3, 2); + QCOMPARE(v, (ArrayVector{1, 2, 2, 2, 3})); + } + } + + void insertRangeTest() + { + { + // Insertion should copy construct + const auto data = std::array{Constructible{}}; + auto v = ArrayVector{}; + v.insert(v.cbegin(), data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + const auto data = std::array{2, 3}; + auto v = ArrayVector{1, 4}; + v.insert(v.cbegin() + 1, data.begin(), data.end()); + QCOMPARE(v, (ArrayVector{1, 2, 3, 4})); + } + } + + void insertInitializerListTest() + { + { + // Insertion should copy construct + auto v = ArrayVector{}; + v.insert(v.cbegin(), {Constructible{}}); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{1, 4}; + v.insert(v.cbegin() + 1, {2, 3}); + QCOMPARE(v, (ArrayVector{1, 2, 3, 4})); + } + } + + void emplaceTest() + { + { + // Ensure the value is constructed in-place + auto v = ArrayVector{}; + v.emplace(v.cbegin()); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{2, 3}; + v.emplace(v.cbegin(), 1); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void eraseTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.erase(v.cbegin()); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{10, 1, 2, 3}; + v.erase(v.cbegin()); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void eraseRangeTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.erase(v.cbegin(), v.cend()); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 20, 21, 2, 3}; + v.erase(v.cbegin() + 1, v.cbegin() + 3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void pushBackTest() + { + { + // Copy + const auto data = Constructible{}; + auto v = ArrayVector{}; + v.push_back(data); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Move + auto v = ArrayVector{}; + v.push_back({}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Move); + } + { + // Ensure the correct value is used (copy) + const auto data = 3; + auto v = ArrayVector{1, 2}; + v.push_back(data); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{1, 2}; + v.push_back(3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void emplaceBackTest() + { + { + // Ensure the value is constructed in-place + auto v = ArrayVector{}; + v.emplace_back(); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{1, 2}; + v.emplace_back(3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void popBackTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.pop_back(); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 2, 3}; + v.pop_back(); + QCOMPARE(v, (ArrayVector{1, 2})); + } + } + + void resizeDefaultTest() + { + { + // Smaller + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + QCOMPARE(v.size(), std::size_t{1}); + v.resize(0); + QCOMPARE(v.size(), std::size_t{0}); + QVERIFY(destructed); + } + { + // Bigger + auto v = ArrayVector{}; + QCOMPARE(v.size(), std::size_t{0}); + v.resize(1); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Too big + auto v = ArrayVector{}; + QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error); + } + } + + void resizeValueTest() + { + { + // Smaller + auto dummy = false; + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + QCOMPARE(v.size(), std::size_t{1}); + v.resize(0, {&dummy}); + QCOMPARE(v.size(), std::size_t{0}); + QVERIFY(destructed); + } + { + // Bigger + auto v = ArrayVector{}; + QCOMPARE(v.size(), std::size_t{0}); + v.resize(1, {}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Too big + auto v = ArrayVector{}; + QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error); + } + { + // Ensure the correct value is used + auto v = ArrayVector{}; + v.resize(1, 1); + QCOMPARE(v, (ArrayVector{1})); + } + } + + void clearTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.clear(); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 2, 3}; + v.clear(); + QCOMPARE(v, (ArrayVector{})); + } + } + + void memberSwapTest() + { + auto a = ArrayVector{1, 2, 3, 4}; + auto b = ArrayVector{2, 4, 6}; + + const auto aOriginal = a; + const auto bOriginal = b; + + a.swap(b); + + QCOMPARE(a, bOriginal); + QCOMPARE(b, aOriginal); + } + + void freeSwapTest() + { + auto a = ArrayVector{1, 2, 3, 4}; + auto b = ArrayVector{2, 4, 6}; + + const auto aOriginal = a; + const auto bOriginal = b; + + swap(a, b); + + QCOMPARE(a, bOriginal); + QCOMPARE(b, aOriginal); + } + + void comparisonTest() + { + const auto v = ArrayVector{1, 2, 3}; + const auto l = ArrayVector{1, 2, 2}; + const auto e = ArrayVector{1, 2, 3}; + const auto g = ArrayVector{1, 3, 3}; + + QVERIFY(l < v); + QVERIFY(!(e < v)); + QVERIFY(!(g < v)); + + QVERIFY(l <= v); + QVERIFY(e <= v); + QVERIFY(!(g <= v)); + + QVERIFY(!(l > v)); + QVERIFY(!(e > v)); + QVERIFY(g > v); + + QVERIFY(!(l >= v)); + QVERIFY(e >= v); + QVERIFY(g >= v); + + QVERIFY(!(l == v)); + QVERIFY(e == v); + QVERIFY(!(g == v)); + + QVERIFY(l != v); + QVERIFY(!(e != v)); + QVERIFY(g != v); + } +} ArrayVectorTests; + +#include "ArrayVectorTest.moc" From 005ee47d439f0ccf023de4c73f552f8c5119ec63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= Date: Thu, 31 Aug 2023 12:17:00 -0300 Subject: [PATCH 11/51] pitch wheel recording in the detune pattern (#6297) --- include/Note.h | 2 +- include/NotePlayHandle.h | 2 +- src/core/NotePlayHandle.cpp | 16 +++++++++++----- src/gui/editors/PianoRoll.cpp | 4 ++-- src/tracks/InstrumentTrack.cpp | 10 +++++++--- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/include/Note.h b/include/Note.h index 5e3a1b8a2bd..2df196af20a 100644 --- a/include/Note.h +++ b/include/Note.h @@ -91,7 +91,7 @@ const int DefaultMiddleKey = Octave::Octave_4 + Key::C; const int DefaultBaseKey = Octave::Octave_4 + Key::A; const float DefaultBaseFreq = 440.f; -const float MaxDetuning = 4 * 12.0f; +const float MaxDetuning = 5 * 12.0f; diff --git a/include/NotePlayHandle.h b/include/NotePlayHandle.h index 61df5a77aa3..7105d6672cd 100644 --- a/include/NotePlayHandle.h +++ b/include/NotePlayHandle.h @@ -248,7 +248,7 @@ class LMMS_EXPORT NotePlayHandle : public PlayHandle, public Note } /*! Process note detuning automation */ - void processTimePos( const TimePos& time ); + void processTimePos(const TimePos& time, float pitchValue, bool isRecording); /*! Updates total length (m_frames) depending on a new tempo */ void resize( const bpm_t newTempo ); diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index 70007ebf187..eb9c7ddbff4 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -557,14 +557,20 @@ void NotePlayHandle::updateFrequency() -void NotePlayHandle::processTimePos( const TimePos& time ) +void NotePlayHandle::processTimePos(const TimePos& time, float pitchValue, bool isRecording) { - if( detuning() && time >= songGlobalParentOffset()+pos() ) + if (!detuning() || time < songGlobalParentOffset() + pos()) { return; } + + if (isRecording && m_origin == Origin::MidiInput) + { + detuning()->automationClip()->recordValue(time - songGlobalParentOffset() - pos(), pitchValue / 100); + } + else { - const float v = detuning()->automationClip()->valueAt( time - songGlobalParentOffset() - pos() ); - if( !typeInfo::isEqual( v, m_baseDetuning->value() ) ) + const float v = detuning()->automationClip()->valueAt(time - songGlobalParentOffset() - pos()); + if (!typeInfo::isEqual(v, m_baseDetuning->value())) { - m_baseDetuning->setValue( v ); + m_baseDetuning->setValue(v); updateFrequency(); } } diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 0cc8257255f..05900bf0ba9 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -4134,9 +4134,9 @@ void PianoRoll::finishRecordNote(const Note & n ) { if( it->key() == n.key() ) { - Note n1( n.length(), it->pos(), + Note n1(n.length(), it->pos(), it->key(), it->getVolume(), - it->getPanning() ); + it->getPanning(), n.detuning()); n1.quantizeLength( quantization() ); m_midiClip->addNote( n1 ); update(); diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 84b73614b86..a4de188a5d1 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -28,6 +28,7 @@ #include "ConfigManager.h" #include "ControllerConnection.h" #include "DataFile.h" +#include "GuiApplication.h" #include "Mixer.h" #include "InstrumentTrackView.h" #include "Instrument.h" @@ -37,6 +38,7 @@ #include "MixHelpers.h" #include "PatternStore.h" #include "PatternTrack.h" +#include "PianoRoll.h" #include "Pitch.h" #include "Song.h" @@ -72,6 +74,7 @@ InstrumentTrack::InstrumentTrack( TrackContainer* tc ) : m_microtuner() { m_pitchModel.setCenterValue( 0 ); + m_pitchModel.setStrictStepSize(true); m_panningModel.setCenterValue( DefaultPanning ); m_baseNoteModel.setInitValue( DefaultKey ); m_firstKeyModel.setInitValue(0); @@ -341,9 +344,10 @@ void InstrumentTrack::processInEvent( const MidiEvent& event, const TimePos& tim NotePlayHandleManager::acquire( this, offset, typeInfo::max() / 2, - Note( TimePos(), TimePos(), event.key(), event.volume( midiPort()->baseVelocity() ) ), + Note(TimePos(), Engine::getSong()->getPlayPos(Engine::getSong()->playMode()), + event.key(), event.volume(midiPort()->baseVelocity())), nullptr, event.channel(), - NotePlayHandle::Origin::MidiInput ); + NotePlayHandle::Origin::MidiInput); m_notes[event.key()] = nph; if( ! Engine::audioEngine()->addPlayHandle( nph ) ) { @@ -710,7 +714,7 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, // Handle automation: detuning for (const auto& processHandle : m_processHandles) { - processHandle->processTimePos(_start); + processHandle->processTimePos(_start, m_pitchModel.value(), gui::GuiApplication::instance()->pianoRoll()->isRecording()); } if ( clips.size() == 0 ) From 8a94fb36818b7af8eb6fa5815c4836f19e7c2210 Mon Sep 17 00:00:00 2001 From: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:55:02 -0400 Subject: [PATCH 12/51] Fix std::vector refactor mistake (#6836) * Use std::vector const reference instead of copying * Add assertions * Simplification --- include/MidiClip.h | 2 +- include/PianoRoll.h | 4 ++-- src/core/EffectChain.cpp | 3 +++ src/core/Mixer.cpp | 4 ++-- src/core/PatternStore.cpp | 12 +++++------ src/core/RenderManager.cpp | 4 ++-- src/core/Song.cpp | 2 +- src/gui/MixerView.cpp | 4 ++-- src/gui/clips/ClipView.cpp | 4 +++- src/gui/editors/PatternEditor.cpp | 4 ++-- src/gui/editors/PianoRoll.cpp | 29 ++++++++++++--------------- src/gui/tracks/TrackContentWidget.cpp | 4 ++-- src/tracks/MidiClip.cpp | 4 ++-- 13 files changed, 41 insertions(+), 39 deletions(-) diff --git a/include/MidiClip.h b/include/MidiClip.h index 43b322f8075..c2287bd009e 100644 --- a/include/MidiClip.h +++ b/include/MidiClip.h @@ -79,7 +79,7 @@ class LMMS_EXPORT MidiClip : public Clip void setStep( int step, bool enabled ); // Split the list of notes on the given position - void splitNotes(NoteVector notes, TimePos pos); + void splitNotes(const NoteVector& notes, TimePos pos); // clip-type stuff inline Type type() const diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 9f3bbcd7d20..38788180f8f 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -308,9 +308,9 @@ protected slots: TimePos newNoteLen() const; void shiftPos(int amount); - void shiftPos(NoteVector notes, int amount); + void shiftPos(const NoteVector& notes, int amount); void shiftSemiTone(int amount); - void shiftSemiTone(NoteVector notes, int amount); + void shiftSemiTone(const NoteVector& notes, int amount); bool isSelection() const; int selectionCount() const; void testPlayNote( Note * n ); diff --git a/src/core/EffectChain.cpp b/src/core/EffectChain.cpp index b07a7227bd9..4da5c519787 100644 --- a/src/core/EffectChain.cpp +++ b/src/core/EffectChain.cpp @@ -25,6 +25,7 @@ #include +#include #include "EffectChain.h" #include "Effect.h" @@ -162,6 +163,7 @@ void EffectChain::moveDown( Effect * _effect ) if (_effect != m_effects.back()) { auto it = std::find(m_effects.begin(), m_effects.end(), _effect); + assert(it != m_effects.end()); std::swap(*std::next(it), *it); } } @@ -174,6 +176,7 @@ void EffectChain::moveUp( Effect * _effect ) if (_effect != m_effects.front()) { auto it = std::find(m_effects.begin(), m_effects.end(), _effect); + assert(it != m_effects.end()); std::swap(*std::prev(it), *it); } } diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index e14660e1fa3..59c2dd72e16 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -394,8 +394,8 @@ void Mixer::moveChannelLeft( int index ) else if (m_lastSoloed == b) { m_lastSoloed = a; } // go through every instrument and adjust for the channel index change - TrackContainer::TrackList songTrackList = Engine::getSong()->tracks(); - TrackContainer::TrackList patternTrackList = Engine::patternStore()->tracks(); + const TrackContainer::TrackList& songTrackList = Engine::getSong()->tracks(); + const TrackContainer::TrackList& patternTrackList = Engine::patternStore()->tracks(); for (const auto& trackList : {songTrackList, patternTrackList}) { diff --git a/src/core/PatternStore.cpp b/src/core/PatternStore.cpp index c5a3521398a..6af434f65b3 100644 --- a/src/core/PatternStore.cpp +++ b/src/core/PatternStore.cpp @@ -61,7 +61,7 @@ bool PatternStore::play(TimePos start, fpp_t frames, f_cnt_t offset, int clipNum start = start % (lengthOfPattern(clipNum) * TimePos::ticksPerBar()); - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { if (t->play(start, frames, offset, clipNum)) @@ -117,7 +117,7 @@ int PatternStore::numOfPatterns() const void PatternStore::removePattern(int pattern) { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { delete t->getClip(pattern); @@ -134,7 +134,7 @@ void PatternStore::removePattern(int pattern) void PatternStore::swapPattern(int pattern1, int pattern2) { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { t->swapPositionOfClips(pattern1, pattern2); @@ -159,7 +159,7 @@ void PatternStore::updatePatternTrack(Clip* clip) void PatternStore::fixIncorrectPositions() { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { for (int i = 0; i < numOfPatterns(); ++i) @@ -215,7 +215,7 @@ void PatternStore::updateComboBox() void PatternStore::currentPatternChanged() { // now update all track-labels (the current one has to become white, the others gray) - TrackList tl = Engine::getSong()->tracks(); + const TrackList& tl = Engine::getSong()->tracks(); for (Track * t : tl) { if (t->type() == Track::Type::Pattern) @@ -230,7 +230,7 @@ void PatternStore::currentPatternChanged() void PatternStore::createClipsForPattern(int pattern) { - TrackList tl = tracks(); + const TrackList& tl = tracks(); for (Track * t : tl) { t->createClipsForPattern(pattern); diff --git a/src/core/RenderManager.cpp b/src/core/RenderManager.cpp index 969cad15bed..9f619203903 100644 --- a/src/core/RenderManager.cpp +++ b/src/core/RenderManager.cpp @@ -97,7 +97,7 @@ void RenderManager::renderNextTrack() // Render the song into individual tracks void RenderManager::renderTracks() { - const TrackContainer::TrackList & tl = Engine::getSong()->tracks(); + const TrackContainer::TrackList& tl = Engine::getSong()->tracks(); // find all currently unnmuted tracks -- we want to render these. for (const auto& tk : tl) @@ -112,7 +112,7 @@ void RenderManager::renderTracks() } } - const TrackContainer::TrackList t2 = Engine::patternStore()->tracks(); + const TrackContainer::TrackList& t2 = Engine::patternStore()->tracks(); for (const auto& tk : t2) { Track::Type type = tk->type(); diff --git a/src/core/Song.cpp b/src/core/Song.cpp index e8073f225f9..3a735331c64 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -383,7 +383,7 @@ void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp } values = container->automatedValuesAt(timeStart, clipNum); - TrackList tracks = container->tracks(); + const TrackList& tracks = container->tracks(); Track::clipVector clips; for (Track* track : tracks) diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 0edebcb8a94..dff19ca3eb7 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -248,8 +248,8 @@ void MixerView::refreshDisplay() // update the and max. channel number for every instrument void MixerView::updateMaxChannelSelector() { - TrackContainer::TrackList songTracks = Engine::getSong()->tracks(); - TrackContainer::TrackList patternStoreTracks = Engine::patternStore()->tracks(); + const TrackContainer::TrackList& songTracks = Engine::getSong()->tracks(); + const TrackContainer::TrackList& patternStoreTracks = Engine::patternStore()->tracks(); for (const auto& trackList : {songTracks, patternStoreTracks}) { diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index 7a7a19c1119..de7690d26e1 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -25,6 +25,7 @@ #include "ClipView.h" #include +#include #include #include @@ -545,6 +546,7 @@ DataFile ClipView::createClipDataFiles( // Insert into the dom under the "clips" element Track* clipTrack = clipView->m_trackView->getTrack(); int trackIndex = std::distance(tc->tracks().begin(), std::find(tc->tracks().begin(), tc->tracks().end(), clipTrack)); + assert(trackIndex != tc->tracks().size()); QDomElement clipElement = dataFile.createElement("clip"); clipElement.setAttribute( "trackIndex", trackIndex ); clipElement.setAttribute( "trackType", static_cast(clipTrack->type()) ); @@ -1308,7 +1310,7 @@ void ClipView::mergeClips(QVector clipvs) continue; } - NoteVector currentClipNotes = mcView->getMidiClip()->notes(); + const NoteVector& currentClipNotes = mcView->getMidiClip()->notes(); TimePos mcViewPos = mcView->getMidiClip()->startPosition(); for (Note* note: currentClipNotes) diff --git a/src/gui/editors/PatternEditor.cpp b/src/gui/editors/PatternEditor.cpp index 229c90bc234..5237690a7e7 100644 --- a/src/gui/editors/PatternEditor.cpp +++ b/src/gui/editors/PatternEditor.cpp @@ -69,7 +69,7 @@ void PatternEditor::cloneSteps() void PatternEditor::removeSteps() { - TrackContainer::TrackList tl = model()->tracks(); + const TrackContainer::TrackList& tl = model()->tracks(); for (const auto& track : tl) { @@ -176,7 +176,7 @@ void PatternEditor::updatePosition() void PatternEditor::makeSteps( bool clone ) { - TrackContainer::TrackList tl = model()->tracks(); + const TrackContainer::TrackList& tl = model()->tracks(); for (const auto& track : tl) { diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 05900bf0ba9..b3833599583 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -741,10 +741,10 @@ void PianoRoll::fitNoteLengths(bool fill) { if (!hasValidMidiClip()) { return; } m_midiClip->addJournalCheckPoint(); + m_midiClip->rearrangeAllNotes(); // Reference notes - NoteVector refNotes = m_midiClip->notes(); - std::sort(refNotes.begin(), refNotes.end(), Note::lessThan); + const NoteVector& refNotes = m_midiClip->notes(); // Notes to edit NoteVector notes = getSelectedNotes(); @@ -762,7 +762,7 @@ void PianoRoll::fitNoteLengths(bool fill) } int length; - NoteVector::iterator ref = refNotes.begin(); + auto ref = refNotes.begin(); for (Note* note : notes) { // Fast forward to next reference note @@ -797,14 +797,11 @@ void PianoRoll::constrainNoteLengths(bool constrainMax) if (!hasValidMidiClip()) { return; } m_midiClip->addJournalCheckPoint(); - NoteVector notes = getSelectedNotes(); - if (notes.empty()) - { - notes = m_midiClip->notes(); - } + const NoteVector selectedNotes = getSelectedNotes(); + const auto& notes = selectedNotes.empty() ? m_midiClip->notes() : selectedNotes; - TimePos bound = m_lenOfNewNotes; // will be length of last note - for (Note* note : notes) + TimePos bound = m_lenOfNewNotes; // will be length of last note + for (auto note : notes) { if (constrainMax ? note->length() > bound : note->length() < bound) { @@ -1207,11 +1204,11 @@ void PianoRoll::shiftSemiTone(int amount) //Shift notes by amount semitones auto selectedNotes = getSelectedNotes(); //If no notes are selected, shift all of them, otherwise shift selection - if (selectedNotes.empty()) { return shiftSemiTone(m_midiClip->notes(), amount); } - else { return shiftSemiTone(selectedNotes, amount); } + if (selectedNotes.empty()) { shiftSemiTone(m_midiClip->notes(), amount); } + else { shiftSemiTone(selectedNotes, amount); } } -void PianoRoll::shiftSemiTone(NoteVector notes, int amount) +void PianoRoll::shiftSemiTone(const NoteVector& notes, int amount) { m_midiClip->addJournalCheckPoint(); for (Note *note : notes) { note->setKey( note->key() + amount ); } @@ -1232,11 +1229,11 @@ void PianoRoll::shiftPos(int amount) //Shift notes pos by amount auto selectedNotes = getSelectedNotes(); //If no notes are selected, shift all of them, otherwise shift selection - if (selectedNotes.empty()) { return shiftPos(m_midiClip->notes(), amount); } - else { return shiftPos(selectedNotes, amount); } + if (selectedNotes.empty()) { shiftPos(m_midiClip->notes(), amount); } + else { shiftPos(selectedNotes, amount); } } -void PianoRoll::shiftPos(NoteVector notes, int amount) +void PianoRoll::shiftPos(const NoteVector& notes, int amount) { m_midiClip->addJournalCheckPoint(); diff --git a/src/gui/tracks/TrackContentWidget.cpp b/src/gui/tracks/TrackContentWidget.cpp index 442d717bfd1..619eff8317b 100644 --- a/src/gui/tracks/TrackContentWidget.cpp +++ b/src/gui/tracks/TrackContentWidget.cpp @@ -344,7 +344,7 @@ bool TrackContentWidget::canPasteSelection( TimePos clipPos, const QMimeData* md const int initialTrackIndex = tiAttr.value().toInt(); // Get the current track's index - const TrackContainer::TrackList tracks = t->trackContainer()->tracks(); + const TrackContainer::TrackList& tracks = t->trackContainer()->tracks(); const auto currentTrackIt = std::find(tracks.begin(), tracks.end(), t); const int currentTrackIndex = currentTrackIt != tracks.end() ? std::distance(tracks.begin(), currentTrackIt) : -1; @@ -443,7 +443,7 @@ bool TrackContentWidget::pasteSelection( TimePos clipPos, const QMimeData * md, TimePos grabbedClipPos = clipPosAttr.value().toInt(); // Snap the mouse position to the beginning of the dropped bar, in ticks - const TrackContainer::TrackList tracks = getTrack()->trackContainer()->tracks(); + const TrackContainer::TrackList& tracks = getTrack()->trackContainer()->tracks(); const auto currentTrackIt = std::find(tracks.begin(), tracks.end(), getTrack()); const int currentTrackIndex = currentTrackIt != tracks.end() ? std::distance(tracks.begin(), currentTrackIt) : -1; diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index b35979f61f8..490f6e6d041 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -305,7 +305,7 @@ void MidiClip::setStep( int step, bool enabled ) -void MidiClip::splitNotes(NoteVector notes, TimePos pos) +void MidiClip::splitNotes(const NoteVector& notes, TimePos pos) { if (notes.empty()) { return; } @@ -472,7 +472,7 @@ MidiClip * MidiClip::nextMidiClip() const MidiClip * MidiClip::adjacentMidiClipByOffset(int offset) const { - std::vector clips = m_instrumentTrack->getClips(); + auto& clips = m_instrumentTrack->getClips(); int clipNum = m_instrumentTrack->getClipNum(this); if (clipNum < 0 || clipNum > clips.size() - 1) { return nullptr; } return dynamic_cast(clips[clipNum + offset]); From e1d3ecb1843df3a4aff96ea086df3260fdd26672 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 31 Aug 2023 19:21:26 +0200 Subject: [PATCH 13/51] Microtuner UI update (#6206) * Replace deprecated sprintf() function * Update microtuner-related UI (add more clues explaining how to use it) * Simpler wording in the tooltips for "apply" buttons * Post-merge fix of a forward declaration * Rename Misc tab to Tuning and transposition; move Microtuner config dialog to Edit menu and make it also available from instrument tab * Enable word wrap on "MIDI unsupported" label * Remove forgotten new unnecessary includes * Rename InstrumentMiscView to InstrumentTuningView in locales --- data/locale/ar.ts | 2 +- data/locale/bs.ts | 2 +- data/locale/ca.ts | 2 +- data/locale/cs.ts | 2 +- data/locale/de.ts | 2 +- data/locale/el.ts | 2 +- data/locale/en.ts | 2 +- data/locale/eo.ts | 2 +- data/locale/es.ts | 2 +- data/locale/eu.ts | 2 +- data/locale/fa.ts | 2 +- data/locale/fr.ts | 2 +- data/locale/gl.ts | 2 +- data/locale/he.ts | 2 +- data/locale/hi_IN.ts | 2 +- data/locale/hu_HU.ts | 2 +- data/locale/id.ts | 2 +- data/locale/it.ts | 2 +- data/locale/ja.ts | 2 +- data/locale/ka.ts | 2 +- data/locale/ko.ts | 2 +- data/locale/ms_MY.ts | 2 +- data/locale/nb.ts | 2 +- data/locale/nl.ts | 2 +- data/locale/oc.ts | 2 +- data/locale/pl.ts | 2 +- data/locale/pt.ts | 2 +- data/locale/ro.ts | 2 +- data/locale/ru.ts | 2 +- data/locale/sl.ts | 2 +- data/locale/sr.ts | 2 +- data/locale/sv.ts | 2 +- data/locale/tr.ts | 2 +- data/locale/uk.ts | 2 +- data/locale/zh_CN.ts | 2 +- data/locale/zh_TW.ts | 2 +- data/themes/default/edit_draw_small.png | Bin 0 -> 5367 bytes data/themes/default/tuning_tab.png | Bin 0 -> 7016 bytes include/InstrumentTrack.h | 4 +- include/InstrumentTrackWindow.h | 4 +- ...umentMiscView.h => InstrumentTuningView.h} | 22 ++++++---- src/gui/CMakeLists.txt | 2 +- src/gui/MainWindow.cpp | 19 +++------ src/gui/MicrotunerConfig.cpp | 20 +++++---- src/gui/instrument/InstrumentTrackWindow.cpp | 38 +++++++++--------- ...tMiscView.cpp => InstrumentTuningView.cpp} | 33 ++++++++++++--- 46 files changed, 122 insertions(+), 92 deletions(-) create mode 100644 data/themes/default/edit_draw_small.png create mode 100644 data/themes/default/tuning_tab.png rename include/{InstrumentMiscView.h => InstrumentTuningView.h} (72%) rename src/gui/instrument/{InstrumentMiscView.cpp => InstrumentTuningView.cpp} (69%) diff --git a/data/locale/ar.ts b/data/locale/ar.ts index 1f159c42a2a..0d44c22bf5c 100644 --- a/data/locale/ar.ts +++ b/data/locale/ar.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/bs.ts b/data/locale/bs.ts index 506b401bd61..7abf0baf1e1 100644 --- a/data/locale/bs.ts +++ b/data/locale/bs.ts @@ -3677,7 +3677,7 @@ You can remove and move mixer channels in the context menu, which is accessed by - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ca.ts b/data/locale/ca.ts index 765cf3b6081..0e27c39db80 100644 --- a/data/locale/ca.ts +++ b/data/locale/ca.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/cs.ts b/data/locale/cs.ts index 0ed175022be..022f554592a 100644 --- a/data/locale/cs.ts +++ b/data/locale/cs.ts @@ -6361,7 +6361,7 @@ Ověřte si prosím, zda máte povolen zápis do souboru a do složky, ve které - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/de.ts b/data/locale/de.ts index 51ca7d56204..7817857fdff 100644 --- a/data/locale/de.ts +++ b/data/locale/de.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/el.ts b/data/locale/el.ts index 320a6657f61..07e61778f1e 100644 --- a/data/locale/el.ts +++ b/data/locale/el.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/en.ts b/data/locale/en.ts index e52ae39ab7e..15c3ab1f07c 100644 --- a/data/locale/en.ts +++ b/data/locale/en.ts @@ -6362,7 +6362,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/eo.ts b/data/locale/eo.ts index 005ee8100bf..0dd9c405f67 100644 --- a/data/locale/eo.ts +++ b/data/locale/eo.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/es.ts b/data/locale/es.ts index 4fc4951ef92..3953ddc11bf 100644 --- a/data/locale/es.ts +++ b/data/locale/es.ts @@ -6361,7 +6361,7 @@ Asegúrate de tener permisos de escritura tanto del archivo como del directorio - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/eu.ts b/data/locale/eu.ts index 25c165f81f8..fe6495c0a65 100644 --- a/data/locale/eu.ts +++ b/data/locale/eu.ts @@ -6641,7 +6641,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/fa.ts b/data/locale/fa.ts index 181ca0ca1ff..b376a8424f8 100644 --- a/data/locale/fa.ts +++ b/data/locale/fa.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/fr.ts b/data/locale/fr.ts index 2c65444a8e0..4862f4263ce 100644 --- a/data/locale/fr.ts +++ b/data/locale/fr.ts @@ -6645,7 +6645,7 @@ Veuillez vous assurez que vous avez les droits d'écriture sur le fichier e - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/gl.ts b/data/locale/gl.ts index cf04fd5d428..a1a9e6bf1a4 100644 --- a/data/locale/gl.ts +++ b/data/locale/gl.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/he.ts b/data/locale/he.ts index ee5a23613b2..fef0caa9178 100644 --- a/data/locale/he.ts +++ b/data/locale/he.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/hi_IN.ts b/data/locale/hi_IN.ts index 15550231f85..82cf364e332 100644 --- a/data/locale/hi_IN.ts +++ b/data/locale/hi_IN.ts @@ -6362,7 +6362,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/hu_HU.ts b/data/locale/hu_HU.ts index 83605994674..a0f1e4d4542 100644 --- a/data/locale/hu_HU.ts +++ b/data/locale/hu_HU.ts @@ -6366,7 +6366,7 @@ Ellenőrizd, hogy rendelkezel-e a szükséges engedélyekkel és próbáld újra - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/id.ts b/data/locale/id.ts index e381ea726d8..c504740e936 100644 --- a/data/locale/id.ts +++ b/data/locale/id.ts @@ -6362,7 +6362,7 @@ Pastikan Anda memiliki izin menulis ke file dan direktori yang berisi berkas ter - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/it.ts b/data/locale/it.ts index ff146d47120..d5a68e6e7c3 100644 --- a/data/locale/it.ts +++ b/data/locale/it.ts @@ -6366,7 +6366,7 @@ Si prega di controllare i permessi di scrittura sul file e la cartella che lo co - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ja.ts b/data/locale/ja.ts index e10ca511818..14b38c6984f 100644 --- a/data/locale/ja.ts +++ b/data/locale/ja.ts @@ -6362,7 +6362,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ka.ts b/data/locale/ka.ts index 1956d8d04e5..51eededf26b 100644 --- a/data/locale/ka.ts +++ b/data/locale/ka.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ko.ts b/data/locale/ko.ts index 7373b5ca9a7..43b99e7f437 100644 --- a/data/locale/ko.ts +++ b/data/locale/ko.ts @@ -6364,7 +6364,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ms_MY.ts b/data/locale/ms_MY.ts index 209d51d108c..ff34784219a 100644 --- a/data/locale/ms_MY.ts +++ b/data/locale/ms_MY.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/nb.ts b/data/locale/nb.ts index 3675b7f589c..659344d64de 100644 --- a/data/locale/nb.ts +++ b/data/locale/nb.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/nl.ts b/data/locale/nl.ts index ad630a249c9..7ff3e8735a8 100644 --- a/data/locale/nl.ts +++ b/data/locale/nl.ts @@ -6362,7 +6362,7 @@ Zorg ervoor dat u schrijfbevoegdheid heeft voor het bestand en voor de map die h - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/oc.ts b/data/locale/oc.ts index 58c81c96453..045eaf3ad69 100644 --- a/data/locale/oc.ts +++ b/data/locale/oc.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/pl.ts b/data/locale/pl.ts index bb0c64edec0..ff36a8daca6 100644 --- a/data/locale/pl.ts +++ b/data/locale/pl.ts @@ -6646,7 +6646,7 @@ Upewnij się, że masz uprawnienia do zapisu do pliku i katalogu zawierającego - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/pt.ts b/data/locale/pt.ts index b375e289f38..f8cfe76181c 100644 --- a/data/locale/pt.ts +++ b/data/locale/pt.ts @@ -6363,7 +6363,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ro.ts b/data/locale/ro.ts index eceb45a6485..58abbba9959 100644 --- a/data/locale/ro.ts +++ b/data/locale/ro.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/ru.ts b/data/locale/ru.ts index 8235f291f34..73b7e06ad2e 100644 --- a/data/locale/ru.ts +++ b/data/locale/ru.ts @@ -6375,7 +6375,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/sl.ts b/data/locale/sl.ts index 3ad55a4c044..e7bfbc3081c 100644 --- a/data/locale/sl.ts +++ b/data/locale/sl.ts @@ -6360,7 +6360,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/sr.ts b/data/locale/sr.ts index 9b90164ab3b..183936bc74c 100644 --- a/data/locale/sr.ts +++ b/data/locale/sr.ts @@ -2956,7 +2956,7 @@ You can remove and move mixer channels in the context menu, which is accessed by - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/sv.ts b/data/locale/sv.ts index 4963b07a9ad..f5d4e0fb496 100644 --- a/data/locale/sv.ts +++ b/data/locale/sv.ts @@ -6644,7 +6644,7 @@ Se till att du har skrivbehörighet till filen och mappen som innehåller filen - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/tr.ts b/data/locale/tr.ts index 387be6d8b96..b899337a543 100644 --- a/data/locale/tr.ts +++ b/data/locale/tr.ts @@ -6646,7 +6646,7 @@ Lütfen dosyaya ve dosyayı içeren dizine yazma izniniz olduğundan emin olun v - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/uk.ts b/data/locale/uk.ts index 50df10e4b72..9fb6389c956 100644 --- a/data/locale/uk.ts +++ b/data/locale/uk.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/zh_CN.ts b/data/locale/zh_CN.ts index 63b22df9902..9b783b963dd 100644 --- a/data/locale/zh_CN.ts +++ b/data/locale/zh_CN.ts @@ -6370,7 +6370,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/locale/zh_TW.ts b/data/locale/zh_TW.ts index 791a45599f9..a3a727edb0b 100644 --- a/data/locale/zh_TW.ts +++ b/data/locale/zh_TW.ts @@ -6361,7 +6361,7 @@ Please make sure you have write permission to the file and the directory contain - InstrumentMiscView + InstrumentTuningView MASTER PITCH diff --git a/data/themes/default/edit_draw_small.png b/data/themes/default/edit_draw_small.png new file mode 100644 index 0000000000000000000000000000000000000000..9979c8223457ac27e70116b300062a3237b3272a GIT binary patch literal 5367 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 - * Copyright (c) 2020 Martin Pavelek + * Copyright (c) 2020-2022 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -24,11 +24,13 @@ * */ -#ifndef LMMS_GUI_INSTRUMENT_MISC_VIEW_H -#define LMMS_GUI_INSTRUMENT_MISC_VIEW_H +#ifndef LMMS_GUI_INSTRUMENT_TUNING_VIEW_H +#define LMMS_GUI_INSTRUMENT_TUNING_VIEW_H #include +class QLabel; + namespace lmms { @@ -42,15 +44,17 @@ class GroupBox; class LedCheckBox; -class InstrumentMiscView : public QWidget +class InstrumentTuningView : public QWidget { Q_OBJECT public: - InstrumentMiscView(InstrumentTrack *it, QWidget *parent); + InstrumentTuningView(InstrumentTrack *it, QWidget *parent); GroupBox *pitchGroupBox() {return m_pitchGroupBox;} GroupBox *microtunerGroupBox() {return m_microtunerGroupBox;} + QLabel *microtunerNotSupportedLabel() {return m_microtunerNotSupportedLabel;} + ComboBox *scaleCombo() {return m_scaleCombo;} ComboBox *keymapCombo() {return m_keymapCombo;} @@ -60,6 +64,8 @@ class InstrumentMiscView : public QWidget GroupBox *m_pitchGroupBox; GroupBox *m_microtunerGroupBox; + QLabel *m_microtunerNotSupportedLabel; + ComboBox *m_scaleCombo; ComboBox *m_keymapCombo; @@ -71,4 +77,4 @@ class InstrumentMiscView : public QWidget } // namespace lmms -#endif // LMMS_GUI_INSTRUMENT_MISC_VIEW_H +#endif // LMMS_GUI_INSTRUMENT_TUNING_VIEW_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9f940c0354b..afed153f928 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -61,7 +61,7 @@ SET(LMMS_SRCS gui/instrument/EnvelopeAndLfoView.cpp gui/instrument/InstrumentFunctionViews.cpp gui/instrument/InstrumentMidiIOView.cpp - gui/instrument/InstrumentMiscView.cpp + gui/instrument/InstrumentTuningView.cpp gui/instrument/InstrumentSoundShapingView.cpp gui/instrument/InstrumentTrackWindow.cpp gui/instrument/InstrumentView.cpp diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 559756169c6..10805fe01c4 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -363,10 +363,12 @@ void MainWindow::finalize() } edit_menu->addSeparator(); - edit_menu->addAction( embed::getIconPixmap( "setup_general" ), - tr( "Settings" ), - this, SLOT(showSettingsDialog())); - connect( edit_menu, SIGNAL(aboutToShow()), this, SLOT(updateUndoRedoButtons())); + edit_menu->addAction(embed::getIconPixmap("microtuner"), tr("Scales and keymaps"), + this, SLOT(toggleMicrotunerWin())); + edit_menu->addAction(embed::getIconPixmap("setup_general"), tr("Settings"), + this, SLOT(showSettingsDialog())); + + connect(edit_menu, SIGNAL(aboutToShow()), this, SLOT(updateUndoRedoButtons())); m_viewMenu = new QMenu( this ); menuBar()->addMenu( m_viewMenu )->setText( tr( "&View" ) ); @@ -485,10 +487,6 @@ void MainWindow::finalize() tr("Show/hide project notes") + " (Ctrl+7)", this, SLOT(toggleProjectNotesWin()), m_toolBar); project_notes_window->setShortcut( Qt::CTRL + Qt::Key_7 ); - auto microtuner_window = new ToolButton(embed::getIconPixmap("microtuner"), - tr("Microtuner configuration") + " (Ctrl+8)", this, SLOT(toggleMicrotunerWin()), m_toolBar); - microtuner_window->setShortcut( Qt::CTRL + Qt::Key_8 ); - m_toolBarLayout->addWidget( song_editor_window, 1, 1 ); m_toolBarLayout->addWidget( pattern_editor_window, 1, 2 ); m_toolBarLayout->addWidget( piano_roll_window, 1, 3 ); @@ -496,7 +494,6 @@ void MainWindow::finalize() m_toolBarLayout->addWidget( mixer_window, 1, 5 ); m_toolBarLayout->addWidget( controllers_window, 1, 6 ); m_toolBarLayout->addWidget( project_notes_window, 1, 7 ); - m_toolBarLayout->addWidget( microtuner_window, 1, 8 ); m_toolBarLayout->setColumnStretch( 100, 1 ); // setup-dialog opened before? @@ -1100,10 +1097,6 @@ void MainWindow::updateViewMenu() tr( "Project Notes" ) + "\tCtrl+7", this, SLOT(toggleProjectNotesWin()) ); - m_viewMenu->addAction(embed::getIconPixmap( "microtuner" ), - tr( "Microtuner" ) + "\tCtrl+8", - this, SLOT(toggleMicrotunerWin()) - ); m_viewMenu->addSeparator(); diff --git a/src/gui/MicrotunerConfig.cpp b/src/gui/MicrotunerConfig.cpp index 7ab4cc0b1ac..4156b9e79df 100644 --- a/src/gui/MicrotunerConfig.cpp +++ b/src/gui/MicrotunerConfig.cpp @@ -56,8 +56,8 @@ namespace lmms::gui MicrotunerConfig::MicrotunerConfig() : QWidget(), - m_scaleComboModel(nullptr, tr("Selected scale")), - m_keymapComboModel(nullptr, tr("Selected keymap")), + m_scaleComboModel(nullptr, tr("Selected scale slot")), + m_keymapComboModel(nullptr, tr("Selected keymap slot")), m_firstKeyModel(0, 0, NumKeys - 1, nullptr, tr("First key")), m_lastKeyModel(NumKeys - 1, 0, NumKeys - 1, nullptr, tr("Last key")), m_middleKeyModel(DefaultMiddleKey, 0, NumKeys - 1, nullptr, tr("Middle key")), @@ -75,7 +75,7 @@ MicrotunerConfig::MicrotunerConfig() : #endif setWindowIcon(embed::getIconPixmap("microtuner")); - setWindowTitle(tr("Microtuner")); + setWindowTitle(tr("Microtuner Configuration")); // Organize into 2 main columns: scales and keymaps auto microtunerLayout = new QGridLayout(); @@ -84,7 +84,7 @@ MicrotunerConfig::MicrotunerConfig() : // ---------------------------------- // Scale sub-column // - auto scaleLabel = new QLabel(tr("Scale:")); + auto scaleLabel = new QLabel(tr("Scale slot to edit:")); microtunerLayout->addWidget(scaleLabel, 0, 0, 1, 2, Qt::AlignBottom); for (unsigned int i = 0; i < MaxScaleCount; i++) @@ -102,6 +102,8 @@ MicrotunerConfig::MicrotunerConfig() : auto loadScaleButton = new QPushButton(tr("Load")); auto saveScaleButton = new QPushButton(tr("Save")); + loadScaleButton->setToolTip(tr("Load scale definition from a file.")); + saveScaleButton->setToolTip(tr("Save scale definition to a file.")); microtunerLayout->addWidget(loadScaleButton, 3, 0, 1, 1); microtunerLayout->addWidget(saveScaleButton, 3, 1, 1, 1); connect(loadScaleButton, &QPushButton::clicked, [=] {loadScaleFromFile();}); @@ -112,14 +114,15 @@ MicrotunerConfig::MicrotunerConfig() : m_scaleTextEdit->setToolTip(tr("Enter intervals on separate lines. Numbers containing a decimal point are treated as cents.\nOther inputs are treated as integer ratios and must be in the form of \'a/b\' or \'a\'.\nUnity (0.0 cents or ratio 1/1) is always present as a hidden first value; do not enter it manually.")); microtunerLayout->addWidget(m_scaleTextEdit, 4, 0, 2, 2); - auto applyScaleButton = new QPushButton(tr("Apply scale")); + auto applyScaleButton = new QPushButton(tr("Apply scale changes")); + applyScaleButton->setToolTip(tr("Verify and apply changes made to the selected scale. To use the scale, select it in the settings of a supported instrument.")); microtunerLayout->addWidget(applyScaleButton, 6, 0, 1, 2); connect(applyScaleButton, &QPushButton::clicked, [=] {applyScale();}); // ---------------------------------- // Mapping sub-column // - auto keymapLabel = new QLabel(tr("Keymap:")); + auto keymapLabel = new QLabel(tr("Keymap slot to edit:")); microtunerLayout->addWidget(keymapLabel, 0, 2, 1, 2, Qt::AlignBottom); for (unsigned int i = 0; i < MaxKeymapCount; i++) @@ -137,6 +140,8 @@ MicrotunerConfig::MicrotunerConfig() : auto loadKeymapButton = new QPushButton(tr("Load")); auto saveKeymapButton = new QPushButton(tr("Save")); + loadKeymapButton->setToolTip(tr("Load key mapping definition from a file.")); + saveKeymapButton->setToolTip(tr("Save key mapping definition to a file.")); microtunerLayout->addWidget(loadKeymapButton, 3, 2, 1, 1); microtunerLayout->addWidget(saveKeymapButton, 3, 3, 1, 1); connect(loadKeymapButton, &QPushButton::clicked, [=] {loadKeymapFromFile();}); @@ -181,7 +186,8 @@ MicrotunerConfig::MicrotunerConfig() : baseFreqSpin->setToolTip(tr("Base note frequency")); keymapRangeLayout->addWidget(baseFreqSpin, 1, 1, 1, 2); - auto applyKeymapButton = new QPushButton(tr("Apply keymap")); + auto applyKeymapButton = new QPushButton(tr("Apply keymap changes")); + applyKeymapButton->setToolTip(tr("Verify and apply changes made to the selected key mapping. To use the mapping, select it in the settings of a supported instrument.")); microtunerLayout->addWidget(applyKeymapButton, 6, 2, 1, 2); connect(applyKeymapButton, &QPushButton::clicked, [=] {applyKeymap();}); diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index 43cca0dac1f..28cd8c6c8aa 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -49,7 +49,7 @@ #include "InstrumentFunctions.h" #include "InstrumentFunctionViews.h" #include "InstrumentMidiIOView.h" -#include "InstrumentMiscView.h" +#include "InstrumentTuningView.h" #include "InstrumentSoundShapingView.h" #include "InstrumentTrack.h" #include "InstrumentTrackView.h" @@ -255,25 +255,25 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : instrumentFunctionsLayout->addStretch(); // MIDI tab - m_midiView = new InstrumentMidiIOView( m_tabWidget ); + m_midiView = new InstrumentMidiIOView(m_tabWidget); // FX tab - m_effectView = new EffectRackView( m_track->m_audioPort.effects(), m_tabWidget ); + m_effectView = new EffectRackView(m_track->m_audioPort.effects(), m_tabWidget); - // MISC tab - m_miscView = new InstrumentMiscView( m_track, m_tabWidget ); + // Tuning tab + m_tuningView = new InstrumentTuningView(m_track, m_tabWidget); - m_tabWidget->addTab( m_ssView, tr( "Envelope, filter & LFO" ), "env_lfo_tab", 1 ); - m_tabWidget->addTab( instrumentFunctions, tr( "Chord stacking & arpeggio" ), "func_tab", 2 ); - m_tabWidget->addTab( m_effectView, tr( "Effects" ), "fx_tab", 3 ); - m_tabWidget->addTab( m_midiView, tr( "MIDI" ), "midi_tab", 4 ); - m_tabWidget->addTab( m_miscView, tr( "Miscellaneous" ), "misc_tab", 5 ); + m_tabWidget->addTab(m_ssView, tr("Envelope, filter & LFO"), "env_lfo_tab", 1); + m_tabWidget->addTab(instrumentFunctions, tr("Chord stacking & arpeggio"), "func_tab", 2); + m_tabWidget->addTab(m_effectView, tr("Effects"), "fx_tab", 3); + m_tabWidget->addTab(m_midiView, tr("MIDI"), "midi_tab", 4); + m_tabWidget->addTab(m_tuningView, tr("Tuning and transposition"), "tuning_tab", 5); adjustTabSize(m_ssView); adjustTabSize(instrumentFunctions); m_effectView->resize(EffectRackView::DEFAULT_WIDTH, INSTRUMENT_HEIGHT - 4 - 1); adjustTabSize(m_midiView); - adjustTabSize(m_miscView); + adjustTabSize(m_tuningView); // setup piano-widget m_pianoView = new PianoView( this ); @@ -376,12 +376,14 @@ void InstrumentTrackWindow::modelChanged() if (m_track->instrument() && m_track->instrument()->flags().testFlag(Instrument::Flag::IsMidiBased)) { - m_miscView->microtunerGroupBox()->hide(); + m_tuningView->microtunerNotSupportedLabel()->show(); + m_tuningView->microtunerGroupBox()->hide(); m_track->m_microtuner.enabledModel()->setValue(false); } else { - m_miscView->microtunerGroupBox()->show(); + m_tuningView->microtunerNotSupportedLabel()->hide(); + m_tuningView->microtunerGroupBox()->show(); } m_ssView->setModel( &m_track->m_soundShaping ); @@ -389,11 +391,11 @@ void InstrumentTrackWindow::modelChanged() m_arpeggioView->setModel( &m_track->m_arpeggio ); m_midiView->setModel( &m_track->m_midiPort ); m_effectView->setModel( m_track->m_audioPort.effects() ); - m_miscView->pitchGroupBox()->setModel(&m_track->m_useMasterPitchModel); - m_miscView->microtunerGroupBox()->setModel(m_track->m_microtuner.enabledModel()); - m_miscView->scaleCombo()->setModel(m_track->m_microtuner.scaleModel()); - m_miscView->keymapCombo()->setModel(m_track->m_microtuner.keymapModel()); - m_miscView->rangeImportCheckbox()->setModel(m_track->m_microtuner.keyRangeImportModel()); + m_tuningView->pitchGroupBox()->setModel(&m_track->m_useMasterPitchModel); + m_tuningView->microtunerGroupBox()->setModel(m_track->m_microtuner.enabledModel()); + m_tuningView->scaleCombo()->setModel(m_track->m_microtuner.scaleModel()); + m_tuningView->keymapCombo()->setModel(m_track->m_microtuner.keymapModel()); + m_tuningView->rangeImportCheckbox()->setModel(m_track->m_microtuner.keyRangeImportModel()); updateName(); } diff --git a/src/gui/instrument/InstrumentMiscView.cpp b/src/gui/instrument/InstrumentTuningView.cpp similarity index 69% rename from src/gui/instrument/InstrumentMiscView.cpp rename to src/gui/instrument/InstrumentTuningView.cpp index 514db579cb6..355d7d18c73 100644 --- a/src/gui/instrument/InstrumentMiscView.cpp +++ b/src/gui/instrument/InstrumentTuningView.cpp @@ -1,8 +1,8 @@ /* - * InstrumentMiscView.cpp - Miscellaneous instrument settings + * InstrumentTuningView.cpp - Instrument settings for tuning and transpositions * * Copyright (c) 2005-2014 Tobias Doerffel - * Copyright (c) 2020 Martin Pavelek + * Copyright (c) 2020-2022 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -23,24 +23,28 @@ * */ -#include "InstrumentMiscView.h" +#include "InstrumentTuningView.h" #include #include +#include #include #include "ComboBox.h" #include "GroupBox.h" +#include "GuiApplication.h" #include "gui_templates.h" #include "InstrumentTrack.h" #include "LedCheckBox.h" +#include "MainWindow.h" +#include "PixmapButton.h" namespace lmms::gui { -InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) : +InstrumentTuningView::InstrumentTuningView(InstrumentTrack *it, QWidget *parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); @@ -60,6 +64,11 @@ InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) : masterPitchLayout->addWidget(tlabel); // Microtuner settings + m_microtunerNotSupportedLabel = new QLabel(tr("Microtuner is not available for MIDI-based instruments.")); + m_microtunerNotSupportedLabel->setWordWrap(true); + m_microtunerNotSupportedLabel->hide(); + layout->addWidget(m_microtunerNotSupportedLabel); + m_microtunerGroupBox = new GroupBox(tr("MICROTUNER")); m_microtunerGroupBox->setModel(it->m_microtuner.enabledModel()); layout->addWidget(m_microtunerGroupBox); @@ -67,8 +76,22 @@ InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) : auto microtunerLayout = new QVBoxLayout(m_microtunerGroupBox); microtunerLayout->setContentsMargins(8, 18, 8, 8); + auto scaleEditLayout = new QHBoxLayout(); + scaleEditLayout->setContentsMargins(0, 0, 4, 0); + microtunerLayout->addLayout(scaleEditLayout); + auto scaleLabel = new QLabel(tr("Active scale:")); - microtunerLayout->addWidget(scaleLabel); + scaleEditLayout->addWidget(scaleLabel); + + QPixmap editPixmap(embed::getIconPixmap("edit_draw_small")); + auto editPixButton = new PixmapButton(this, tr("Edit scales and keymaps")); + editPixButton->setToolTip(tr("Edit scales and keymaps")); + editPixButton->setInactiveGraphic(editPixmap); + editPixButton->setActiveGraphic(editPixmap); + editPixButton->setFixedSize(16, 16); + connect(editPixButton, SIGNAL(clicked()), getGUI()->mainWindow(), SLOT(toggleMicrotunerWin())); + + scaleEditLayout->addWidget(editPixButton); m_scaleCombo = new ComboBox(); m_scaleCombo->setModel(it->m_microtuner.scaleModel()); From 0768f5ad2febea80e4766b25547d42fa12b7bd9a Mon Sep 17 00:00:00 2001 From: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Date: Sun, 3 Sep 2023 17:29:31 -0400 Subject: [PATCH 14/51] Fix a few memory issues found with ASan (#6843) * Fix LADSPA effects memory leak * Fix buffer overflow in PianoView * Avoid using invalid iterators in AutomationClip * Fix memory leaks in SimpleTextFloat * Handle potential case where QMap::lowerBound(...) returns end iterator * Implement suggestions from review --- plugins/LadspaEffect/caps/Descriptor.h | 3 +- src/core/AutomationClip.cpp | 27 +++---- src/gui/instrument/PianoView.cpp | 103 ++++++++++++------------- src/gui/widgets/SimpleTextFloat.cpp | 4 +- 4 files changed, 62 insertions(+), 75 deletions(-) diff --git a/plugins/LadspaEffect/caps/Descriptor.h b/plugins/LadspaEffect/caps/Descriptor.h index 12c5d1c8846..c3e1c325e73 100644 --- a/plugins/LadspaEffect/caps/Descriptor.h +++ b/plugins/LadspaEffect/caps/Descriptor.h @@ -53,7 +53,7 @@ class DescriptorStub PortCount = 0; } - ~DescriptorStub() + virtual ~DescriptorStub() { if (PortCount) { @@ -87,6 +87,7 @@ class Descriptor public: Descriptor() { setup(); } + ~Descriptor() override = default; void setup(); void autogen() diff --git a/src/core/AutomationClip.cpp b/src/core/AutomationClip.cpp index 906cb148c82..3b36f6b49b7 100644 --- a/src/core/AutomationClip.cpp +++ b/src/core/AutomationClip.cpp @@ -1106,16 +1106,16 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) { QMutexLocker m(&m_clipMutex); - if( m_timeMap.size() < 2 && numToGenerate > 0 ) + for (int i = 0; i < numToGenerate && it != m_timeMap.end(); ++i, ++it) { - it.value().setInTangent(0); - it.value().setOutTangent(0); - return; - } - - for( int i = 0; i < numToGenerate; i++ ) - { - if( it == m_timeMap.begin() ) + if (it + 1 == m_timeMap.end()) + { + // Previously, the last value's tangent was always set to 0. That logic was kept for both tangents + // of the last node + it.value().setInTangent(0); + it.value().setOutTangent(0); + } + else if (it == m_timeMap.begin()) { // On the first node there's no curve behind it, so we will only calculate the outTangent // and inTangent will be set to 0. @@ -1123,14 +1123,6 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) it.value().setInTangent(0); it.value().setOutTangent(tangent); } - else if( it+1 == m_timeMap.end() ) - { - // Previously, the last value's tangent was always set to 0. That logic was kept for both tangents - // of the last node - it.value().setInTangent(0); - it.value().setOutTangent(0); - return; - } else { // When we are in a node that is in the middle of two other nodes, we need to check if we @@ -1159,7 +1151,6 @@ void AutomationClip::generateTangents(timeMap::iterator it, int numToGenerate) it.value().setOutTangent(outTangent); } } - it++; } } diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index 20db2e8e83f..d20cbcac52e 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -322,70 +322,65 @@ void PianoView::modelChanged() -// gets the key from the given mouse-position +// Gets the key from the given mouse position /*! \brief Get the key from the mouse position in the piano display * - * First we determine it roughly by the position of the point given in - * white key widths from our start. We then add in any black keys that - * might have been skipped over (they take a key number, but no 'white - * key' space). We then add in our starting key number. - * - * We then determine whether it was a black key that was pressed by - * checking whether it was within the vertical range of black keys. - * Black keys sit exactly between white keys on this keyboard, so - * we then shift the note down or up if we were in the left or right - * half of the white note. We only do this, of course, if the white - * note has a black key on that side, so to speak. - * - * This function returns const because there is a linear mapping from - * the point given to the key returned that never changes. - * - * \param _p The point that the mouse was pressed. + * \param p The point that the mouse was pressed. */ -int PianoView::getKeyFromMouse( const QPoint & _p ) const +int PianoView::getKeyFromMouse(const QPoint& p) const { - int offset = _p.x() % PW_WHITE_KEY_WIDTH; - if( offset < 0 ) offset += PW_WHITE_KEY_WIDTH; - int key_num = ( _p.x() - offset) / PW_WHITE_KEY_WIDTH; + // The left-most key visible in the piano display is always white + const int startingWhiteKey = m_pianoScroll->value(); - for( int i = 0; i <= key_num; ++i ) - { - if ( Piano::isBlackKey( m_startKey+i ) ) - { - ++key_num; - } - } - for( int i = 0; i >= key_num; --i ) - { - if ( Piano::isBlackKey( m_startKey+i ) ) - { - --key_num; - } - } + // Adjust the mouse x position as if x == 0 was the left side of the lowest key + const int adjX = p.x() + (startingWhiteKey * PW_WHITE_KEY_WIDTH); + + // Can early return for notes too low + if (adjX <= 0) { return 0; } + + // Now we can calculate the key number (in only white keys) and the octave + const int whiteKey = adjX / PW_WHITE_KEY_WIDTH; + const int octave = whiteKey / Piano::WhiteKeysPerOctave; - key_num += m_startKey; + // Calculate for full octaves + int key = octave * KeysPerOctave; - // is it a black key? - if( _p.y() < PIANO_BASE + PW_BLACK_KEY_HEIGHT ) + // Adjust for white notes in the current octave + // (WhiteKeys maps each white key to the number of notes to their left in the octave) + key += static_cast(WhiteKeys[whiteKey % Piano::WhiteKeysPerOctave]); + + // Might be a black key, which would require further adjustment + if (p.y() < PIANO_BASE + PW_BLACK_KEY_HEIGHT) { - // then do extra checking whether the mouse-cursor is over - // a black key - if( key_num > 0 && Piano::isBlackKey( key_num-1 ) && - offset <= ( PW_WHITE_KEY_WIDTH / 2 ) - - ( PW_BLACK_KEY_WIDTH / 2 ) ) + // Maps white keys to neighboring black keys + static constexpr std::array neighboringKeyMap { + std::pair{ 0, 1 }, // C --> no B#; C# + std::pair{ 1, 1 }, // D --> C#; D# + std::pair{ 1, 0 }, // E --> D#; no E# + std::pair{ 0, 1 }, // F --> no E#; F# + std::pair{ 1, 1 }, // G --> F#; G# + std::pair{ 1, 1 }, // A --> G#; A# + std::pair{ 1, 0 }, // B --> A#; no B# + }; + + const auto neighboringBlackKeys = neighboringKeyMap[whiteKey % Piano::WhiteKeysPerOctave]; + const int offset = adjX - (whiteKey * PW_WHITE_KEY_WIDTH); // mouse X offset from white key + + if (offset < PW_BLACK_KEY_WIDTH / 2) { - --key_num; + // At the location of a (possibly non-existent) black key on the left side + key -= neighboringBlackKeys.first; } - if( key_num < NumKeys - 1 && Piano::isBlackKey( key_num+1 ) && - offset >= ( PW_WHITE_KEY_WIDTH - - PW_BLACK_KEY_WIDTH / 2 ) ) + else if (offset > PW_WHITE_KEY_WIDTH - (PW_BLACK_KEY_WIDTH / 2)) { - ++key_num; + // At the location of a (possibly non-existent) black key on the right side + key += neighboringBlackKeys.second; } + + // For white keys in between black keys, no further adjustment is needed } - // some range-checking-stuff - return qBound( 0, key_num, NumKeys - 1 ); + return std::clamp(key, 0, NumKeys - 1); } @@ -396,12 +391,12 @@ int PianoView::getKeyFromMouse( const QPoint & _p ) const * * We need to update our start key position based on the new position. * - * \param _new_pos the new key position. + * \param newPos the new key position, counting only white keys. */ -void PianoView::pianoScrolled(int new_pos) +void PianoView::pianoScrolled(int newPos) { - m_startKey = static_cast(new_pos / Piano::WhiteKeysPerOctave) - + WhiteKeys[new_pos % Piano::WhiteKeysPerOctave]; + m_startKey = static_cast(newPos / Piano::WhiteKeysPerOctave) + + WhiteKeys[newPos % Piano::WhiteKeysPerOctave]; update(); } diff --git a/src/gui/widgets/SimpleTextFloat.cpp b/src/gui/widgets/SimpleTextFloat.cpp index df438423e27..e37753229ac 100644 --- a/src/gui/widgets/SimpleTextFloat.cpp +++ b/src/gui/widgets/SimpleTextFloat.cpp @@ -46,11 +46,11 @@ SimpleTextFloat::SimpleTextFloat() : m_textLabel = new QLabel(this); layout->addWidget(m_textLabel); - m_showTimer = new QTimer(); + m_showTimer = new QTimer(this); m_showTimer->setSingleShot(true); QObject::connect(m_showTimer, &QTimer::timeout, this, &SimpleTextFloat::show); - m_hideTimer = new QTimer(); + m_hideTimer = new QTimer(this); m_hideTimer->setSingleShot(true); QObject::connect(m_hideTimer, &QTimer::timeout, this, &SimpleTextFloat::hide); } From fa05ce20b8578b77b19fd22d8c63eb59fe7ea0e1 Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Mon, 4 Sep 2023 07:29:29 +0530 Subject: [PATCH 15/51] Replace corrupted default samples with fixed ones. (#6752) * pulled from upstream * replaced broken sample files * Revert "pulled from upstream" This reverts commit 76967b5cc4e7fbc12086830bd6a3954146bdbb9f. * fixed shortened samples * will this fix exprtk submodule? This reverts commit 76967b5cc4e7fbc12086830bd6a3954146bdbb9f. --- data/samples/drums/kick04.ogg | Bin 7126 -> 12017 bytes data/samples/effects/scratch01.ogg | Bin 5516 -> 12370 bytes data/samples/effects/wind_chimes01.ogg | Bin 22654 -> 57873 bytes data/samples/instruments/harpsichord01.ogg | Bin 38144 -> 60978 bytes data/samples/misc/hit01.ogg | Bin 16606 -> 43603 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/data/samples/drums/kick04.ogg b/data/samples/drums/kick04.ogg index 567480abd683475c1ba4325469adc111dac42b8d..8f7dce5271252405b7adfe8bc2bb606479c342ce 100644 GIT binary patch literal 12017 zcmb`tcQ{<%_cnZpXhHN|qmCA%j}|1#j1mUH2vMUGHBo}-1ks~KA1x7tAqdfl=%PeV zQKBcJx91F>&-eMhzxTbK=b!gnXO6x1T6^ui?zQf{_Zf57)>aR|1^&4l>@N#VIia#R ztT^7T?w0l**e0AiZ~jqnyHtLUa~Iq5KNq$q4p@tPSKmN^I{$xOV^=OMiGvD8_K)m@ z@4DNvJK0;_yRgp=XBQO_6%mmV5ohN^+E{y8*}K@XtGIaEySutL+qif_u!bqYkBau~ zyLVKO@|V_xEv%ezu3nY}lyBd|0SG}Is3v=7&``Vq0FVQKITwU1=^-3~PR`@=NlRA8 zmO4eE(vqV(>8)a*UH?AV#jR-o01pV_BZ@0nf;$YrZ0UL9+;d^}+S0}B1kt(+Frv0y z9xIoknj%M+0SGrCsqs|+#7$A1Ba9k}eON|vgwunM2xj*lj^ZqLV*ZlcU}Di8_7F|c zUxi89GQUa+;$&9&YKD}r3Dt}sG@)bntll=^oBDeUz?QTAc@SOr!4A$v8K1$LT$vDS zJ`z%t<_?;=bc-Db0^J1E5~$%-)Ss)E`-i#i{t?urk{A|K(bLy505307LvNc2Z?6gO zr^abvChwmbzYjB+3^Uyiv!DG6CEkD}7Ql-hI=r>dthIYx>-_nh;OBQF7b;-P@vyT2a_uvU{$KY)<5Zjf`>kl# z!w)EeSa!QHce`<^Ai28T1qm(??g7A;Rw$W$tui{sf|d6dHAxF!Ye0@X(q7p}->Yf4-hw4%YJEasL{XO6Z;>1XoRm!@T2M9^t~;RhH?NN;*)Zu(G$ z#{irp1ASTYq0A&GFC78f7?!ihx)>}#J^PUig3_->U|-{*jHQx#YOoZQoXLx+*T1y) zPg<0kWev2#zJWcWrZOGtiA=%Lp>@_~KM}U=f2NNj7&nVa`mR^O>`&wHEvY$*IFeXB zUyvo`LVF;Etg&&bh|&-pBm*znGx)-Db^r(^zsTZ$9~Y^7nZ<>P(fqx9)qSF$1+kAe z?E^AvmF?uWNuXdB3rc`l9E(}X_IL|ZcSK{xQ3B{>Bm#oIWE2=wcI$X0e1;2>gr~Xp zLW)6F{CD8Cxdtg%hX3mx{LQRO$qnAaqC$FN@&@__hThJ`FTJMg!b~T<<|m%cPsBc; z3;&;q^&jQ{AZbD`IGJo21MSWV(N-q94Di3qaii`|VE&ZArSXnSYecaBkNDjk@nI?z zU2!cv8q;AKuTfeXJxSA137b&^n=x;jueCNNbx)0J{~^pJZRRJu{);(Sirj$3{ZLQD z``^sT<%|0+9mfez7Ag~&ZUp?!yurZDs=xH003x-C(*cA zM|6=wV@QcHq>zEW{QsFTpma=JYg8NrHU$6}0AQsPM4ddbAQeU4Y}gCqlh`+dKBVrL z^Fya8#kp^%TOwe5nBoB_Ix^wDi9o`Q4hu3F18qo&((*W*P>29r_V55*kZ#J6&<=Xb z7^NQeJ26l~hR{eTK1axq5+Qe3vM3=(N<6ZI9;|+14^38vGbG0&)zJVD1Q39qYk5QQ zG&qV`01zDS5eX%vu_BitEcGNumeGgA!+5zp$Pj$oXmSJs6o@W!Sr5R8xC^Nf@Y0ZY zBp({BgK&VL>Gcpo-sDJPG$^o#V5m(HLS4~N!gLR$=yG8w5(NrUVZ3Ot2SHwPBGM6E zI1UOthv2{1laWaK(&R5FK5h&s;7%DwIYNuZElDm+l^1@obb!>3quyK$UWgqWD}W1m zXd?(XQU;VDU=MIEHUn5wBT9Ve!Vwf4jnP49aHGeSA?28HOU+UYBLWVZLcvQhUr^=c z78XO!`iygd1A8C*6;ZWWM4qgg6=m@PHcZh{{FIL0u!Q)8EkRg9DY0=OM?fIpwyd)N?&anoMIGOPu<$o{fHE#X zADV302U}(bV-OmTjDamPha!>DT0XxBwssZ9*FjU2R$ z0k{6F5OQS*1Tzly^#n&lqbFs+zTl7`-YC!>sE0<6TdrN`aihPWo^ymE5pZsFs^xPi zSOWEuQO}{N`bb16sQ0|83yA>Ny>YOuD+3%C3|LH67fKI-g$ry$IfADc0y6NFq&fsw zO;MS&YYQXDy+~PV5GfK%5@>jq2e~L=f(MB*0f#%72!+;2WqS^^CPJONkQN5!8B!FI zht@&za2M*MAONrq!n@w0%qMQm4a5_GyH_DKc&G_a24U!8zKsA;wu%9B8f;rmMuIM< zd4LaQaK8Z0Js^6_R(&8`9&sQRU|Tm@P4L1;9H0f{vJyUr74~El3O>Mj z(qrc-l1D<#_>K}#2Tx@rp#KCQyDoAI0f%6-0-Gt=RKo`UpQkg{-&BDs zmt!f0)+&|-T+O0|x)?n+KxjaJ!2ARk!AVeMJmWS5s{%TDAM6GS`Z3E`K)_OYeKFR` z_luMdk296(O{6$J& ztJYZldR_{Yd;UuhI)o+mg+NCcf(CmKTm+pXR0r{|fD*xnPX5;v8q3RnR4B2Dg_OBW zlgl&7rKyXILn4SS^)3!8$&}bhUiJVJ5UZDWDL^7F@)m0fYY$9bj(-K;0AT1Mo)jsxmcEBuW|6l$890Dl$OhVAILd0Zi1eXzDCKbNmAT7wa_B5+FC0>xmD95y59p#n6!V!d1 zl>|Zw3lw6_Xxh8daFv<@{|%E1T$$FBQb&UBP*=|4Pf@}JNCmO!4)1l&gjuEu|J~$TF!8M zD?yOsL0*KObcmH$&hjIdIrxH#tzTmG98y|l^+c3|yWoI40f+x~J;unfUt)PKpkO8swPID2-qSk{!uFX%$z%>#Nr0Z%M}Xpek;HhJ&y^#QOm|zz zpMmQOF%SuEXffad6nYsG%jiq0{}-9TmjoVZ@BnVq`nGSREcSbm_MiOgxH&@V=Y{_SRF2 znIqLNyjcC-d@Na$Z~i!OGCT0Z>D0*E*oh!q^dn&fOVgmwl^>pK+G_L0uJjsE&zbLR z=LPr+d)52{Nlko@OCAVj8A!{^8iqim3?Eqb{U*{CE?bKJ^z8~64$$Xv%(NW_nNbl7 z^5*oJczcs)Pj*}_Jm8%KkJ_1)m}P!;1WWT1Z7uP}eg@!_8Zc&BYly!~9z-D6LctCv zT!cy5@^};dZ-O8&SE)#TZI~LP~GPWMT;>HIBx>s4oP^WSKZcB-V z{x!+hZ(AFWro=uuMqFuGu{T}_yLP0W|DI)8$A`9`)F32y_Zy};{0aab9D>S(K0c=t z3~<$cVNcNKlCvM0+<3q0i_+Hlr_VaJZW*;pPf}hzcsKQQC*|qp!rZG;HyDeHpH0Ly zG7~_=38tx;s21mwFx)>$>+BQR*xx-$u4$lmqLbMkULeP4#ry&QYFut*TYC;_0p{xg z@eO>Bqfc8Ex;Li0&i%8i|4z6vL3wjbVzW)}{k8VlWCehSTQesohp@h?=}dmo+O~0K zp!qr}>lIHho>LjKA7+)QDyO^p8v$VBSg2n_b*D&@RYH5YQNd_7f-QE+#eRbdJ`{AqpTL-=ip|?>`v8+g51wK zV<9<~o!!8O=C=_)@?oaMO_LJ{8kb!Z&Z)g7O7wyI>|jm21F zk8~;G%|4uYFB`uwPMLMaJC1PQT*D?uynR?!d+*!WV^bh){ulCWF8u0T#_5Co!&IIi zQo+WL?uABzBAKtG#GQ`V5pDNry`!#xI99$&UbQXCYHgqRrLvb#`lYY0?wtuqGrzf1 ze@g$d0q@9R4_Dvgxt)?mXy!8e zwici6oW8MqBVxYEA&P!Z+-1;g?3dWn?vK@yyT}*WhDSg9B%e3$hs+-im~=sw4*JkE z8Zxz=DRH7sPVCatQqJJHfQ$7#WNo(xFWfW5Rd z73LX5cZjJHi4X%Vy-A{|Urp60ipUK=&Zx&3o-gNPyGl*>514Mv&0CizgwQ{&aY*l) ztSMZato^_d=)d{ES2L?J>ML=njLz{%X{%mdLarcM1s@ zSHG0dXLjiSN~)^;k#QpS%1Tj%Sn6;hhiUq8a*fIXy;Je`cTaI@Qi#^Oj%Fi?Mt(h0 zH2kqsWpuqMcdhFo0qLfeoFmt`W`n5JH=BvHZ`skV<6R`J!b%b7<5xc-dDJHgjdtDy zI+=}yeZx0G^Yd|56uz(!4eQ%LDnF&t5P3^ZMFS|Tg0mLp9>XR;iFKl4Tcv|I*+ZVR zcZt?`^i}}iX`fYw*kZMDg;&1AG68wG6_q`S1{D7fprR?m!Q2a>quaXew zl62tUJ@BQ zGVPdsMDAF2AC4tKi2YR=_*d|S*Pgw4B2<|K z2twSN`{0Jl+ePbjM<8Qs?~j4ilM&^CPi9eXHq=+A(5)GvQ9gYgjwzYyv3__3<` zEAX<0bH<@R-icXXaLFN?L6a}7(Izs(+Uoa|y4N(64Jj<)ewODhv&_%kZtwLSXP)de z%dHVXgkJ2|$DO7Rnl<|^SYIQcUVx$nJJ~k?v)+?H;T(BE&^yK)+3Qid?a48}8*}>I zXnZm#x+H#A;=&VStf_&@k>lS7&MO&Zhs>JUA7f$irl&3;IfdnX{-Vzka3GV8&&5A~ zB66TE`?&4Ak;)2>#EidZeZg6fc>MueXex=9_D0E>OTqg_{O0|zv#`KLv))YQ2a}%? zsR-D1@joia`fE9Am;7Fn9jwjdK6)j2Qbn^k`@vpnT@JbsJ zA;)_riC%s2GjGJzk{JH*O3#{HYB0>+nM#jE1m+k&MMcgxXDM)Jyc%y{mq)67fHo^zS_@&k7Qt|eYT)JdRA z-P}+I9@7(9c}L(HTSlMPEAqags%M|Y8w;ne=HaBu4MOn&qk#QZsbD^>6mz*Q$vsue zdX|&@?o>7CT~$JoA+D&iKKHGp{`mgdH@;8mXjnIslSWW4(>u1uxWvhM^WE>eXXeI-VsJA(S zCfzRiLP~T8StNC{fAv(Z)Gfj|9Xd5jiRs<9-Y@K0ZjCw+)XU7j;|%G~pGQG-hbV5A za|z2<+n@6GMW&A`V6I8Wv}iRkM>F3EKP739(l#o0`d9_Yd{8mEo5Det_%@UrF!MPH z%ziPBT*YU4A~sGEB%Ve*+R>#AW6Pj8lwK--#(VuiUT*3VUJR~c_u=&f8QZqkWTF}# zQ9+F_%IHqjIGq*K^dA$n0~UQg6Y|8Z1Fz*Jk_ebHorMb7HcP2`eAZsA)p&>qO~_=)JPChtzRR;D-GoK1WzF^!$L@?Dk@jZC&Havdw?w5uk?bRR-K znb9K-Qxa+>>+X1{QFw$mcuG?-=vj6$#E8phF+&dQlCRQBw~h?P!*Mij$Pkh_d{2Yd zh<57kU>KtBx`;|pI}i|Lq%}rmyk;L_ix=r^RbP!rN)^KxhL%!F3TsPDKfuR_UX%0k zl687G-G8f{Uf1;2HH!{K6cJ!%kHw`52il6CI>0s?pC7Gir%B-_*vMAX;o`Wn1nz#+ z0y8h`9o=8qhSU#Koz8RQjTyJLB>C%$VwmC-x{71YC)?CJkvyvbwG?Xvrd__7j3)($ z@#nnEnfUGO+CEkAmo8Icw;w*Ax(j_RM13rtnF+dAmtUXa`7qv~Ak`$ByX=Z{@~-aU zG<)876Im&&Yb6oi9Od+vnRPx=(=#gKf_KiKd+G9(tq`mZVhWpdMBDEC* ztd_Irt3sYLnetEN-OH3@`NQEh6lk!!6S2!bc=Rb@$D_FRDg$9{*kn-(v|(Ot!d^9C zk$?7upNZ2XdZXB`lv9Rv)@dezDq0GjrD%N6I(y7-{&TS`^HaJ9kB8Q7-p0UZF)`2Y?{P}m0W@dA+e}-gREN|ir(QgU`7z)4g6CmZ2Cf0gn3|gS zsIX-ZynUH7{uq&eY zuZ{Pd0 zNlbHTek&*R>$g4UXGIzoc^HXw)ls%effk`gAO=z-($1-i5 zyL`d-=NDPFbHq)b5Q$BIxh=*?WhjN%q?4dQ{Exo5#4ELd z9J8vZYk|WYqA4;4wA=xO5#=1tQUk+e5^JUn9RYDcG>&TPN=oL496=Qpdx3cE{Ypve zH%eO^mp|XYOL7L^C&3H*l?(RO@_YXqI+}C%xf=Msg@_B>XDn=?39{#-D<7V#?gl9S z_;#`~S@3e+=`pGBH{P)vyKSkaC>cVE(Pj3ZLT$w`TN3g-B{jd_S!LXkD94`153Kzg z9C#{zYv4oQ#?z#fwaiLchZz-|$e~uf`S8r8nc;&|i-u)TIIPdAo8p0ZVVmtTom_ke ze2z?4pl&KN_uDMP>4=42+CcB!{D`A*^N!%`A)--priYe`&LrN69{A|{81vUpl~N=RX@gt_jn$u$hv4 zP?zReE5%0)C>$IIZdQy33*-W>n^2sFysx31Kh9cKADN3QT$_?i?oXfw)V6Ux9{Qk) zSKM3(&ia>aejS_U-Eo4*i7w|7QR)hY?Yz9%=$tbaO;w#gFW)tD&Fd+O)0ri(=4*cS zhfUmr;m670Fo{~RVVV5+DYca*3CZe+O8LT%rB#M|4QEV85B%4UVJB<#w+#q7ZZUAL zg&!GEFTShRv~zhbr_>Amtilq^@>kon`HF!>c;$xpQNq0VV)E~PBr?d2qg{WKDu~>p zsqme*UZAg?frX|>OBj2DOmvt<6)-&neLu{zvE zemeJ#3FIpCRgo_ifho+8$EBh#1C-qZjN*1FO?E8gq|@r@9n+7pRypD_kEJ zmL8`2zNs#=tDtn@yf6Cjz-2PkU41vJ7kOQ4iP*I4D_v(%MY-4a;&OpB~;^6zQ;$3sbWTt zzA5k&|9;?@d`C0AY^aN9f}5YCKA2F0)+&LIFmLza>=tx$`^%G;i~Y4<2V5P~k*+G~ zaQ3ISZJaXVjGvQ5@3c!hav9)Jl4YGOA9vmIHQMYzS8!buU`S3TGJng2Gq(@o$cDkI z!&zuJ3BM<5o@#6Xa%n+zm0(7%-rFQcCUMS=nDB9H}?MQQApT)RM1N09hhJCUN+s^mo7*B}VNmnTdJW^(*-#q@ zB0$k?q3c!$iB%@e*Jz(WrX3!HO)_c=Umzstk$Xy zv$`?DvfLlU{}wMtie1e3kMv8H@v7*fm+2oJYML|X`t+>>oJITR)GYnjC1Q&0(bZ*y zHw48#$l&8~#tcXOO!Onm#@*#*m)A>o`IAdsgWioTwk!J)UuidB^QI{_6!$1xCT233 zk=Ei35kS)?X0M^-9fO++3yiKP#h6 z`u4IO`drnIi$B$HDP4+Y>S`R{IXZYJ9_qf@e$>k((@uW=`b^qKjBnZ7aai&i?pJ8~ z=-}%J1zm+hnWCTM9_2s3(b9Z0OI{~#W6SK+<=l613c7nh(9ju>L0@}4`&5D~g$ZRD zMM|ybD}8l&B=^K-xkNcEr~D=<{i|*LAtx%g>_RRxl*~+n*HS<0R=#_klI)pnE$r>I zhGAq4Q$eWnpOikRB9xaI9KbME)L0OOX8&Am0X8 z?5kvmX`+SZh9-Vw9eG&jt3`lbbC%)LlML@rMc^|ops;^@UQr%YiDPbmtWi!<0yo*E zZlE>hCXVgua%8edqHeK=8NFMCn*8PVqT|UbmI|0_sT}O=Qjjs~viiI6wL%=@+|DUk zR~Mt;2lJ7{B+|J(9u(rHt^FWV$fhW~uUVkt^Lm?x?Wv#sF?Exh~odpWG-ge#P^_Y+sP< zu{Ev#l7kN_Ltyy2=7jz0GoqAj_hTYmrvawN?{e;bhWt5W#(B{p|~mdEpz_h|6NF=p4(>*J1y33iuN0jQvum~ z$G0XZyX}Fj`9Fzxl#g7=N-{>=0NPw<&No~I0RMGFFPAM`#*&QEcT9E0`?uriIQ(aC zUcuN@m%^L#S!ljT)4~dExyBP-Lj~UF;`s|&wO75bI^ujB3x6d7OY+_LGZb!=39YYQ z3*4g{N|5bbqSVZGzV^w+(mmv}%6U$*8%`Vo#=&e#t!FpGw5l)sLGE*y!)iAvM!HO+ z;Q7fcCxSHon$)CMG>3^{iTAXYx7fh}&qpTkKk0KtGsDA)zAQ)XtwHJaYgMoO+!Noc zHRMfJI`ESX=9O#p{??hmq%U0MKWmKB*|P>V$W&v(84f8<7tw}oFPIl zUdfdwTl5yw)^wHJXc#BJihr_>WLS@!gZoQi4mQdlLx2(Ka^5#g_zWReObDH8nl^czaz3?-@ z?e}Ym`!{93!*Zx82{b3O)lif%*2cw4UlNX;ZD$mX5;a6mHGgb!XNkU}V)XUs(PsP= zUf#SV-^X9XXiM4;s4YEoMg|;>)W$=IW=9;dLmdIL-Q%aFr0+=qjcL~qi!>Bm)U!>v z)7!W4Yxt0iQ+6TXU&y`L9Cf`<*sDTK<(GGhR0b~&-kUZDcRgPw`rirRiau;)o2nyw za~g}QrhQ3UZ+M3uq&c?@M%~|u>VjpGc|8qB74uG|r5`CWu^s1~2Kk7!B=ZtDVrJd9 zC70R)9S}^u#nT6$ZqQHF5!}(|-9DC*E1+mGEOkSO#r%w_MVR6}rObm#EO?H2p6_v2 ziD0Cr8+OzxMib3x(5byDjoCR#exd=;syQ*4yA?T;i{i^3_rKfpM~G(& zAnKA&hdRxSaa&y0JmA~2T*^rXE+n!a!owvS> z7%z>yRcKkF+-??%noBmh36nH1#Fy49RE@Ul@o5&!cmXdF!~qm`kG;}~$))%fX4~aa z?0Q7R30*(#@6Di2cS95nzAVuLD`a}J{ab-_ZIkDB)*@EjlasP)6F)qCel%F+`mJkz zn`DOahl{}ED9icj{F%8~*&doyf47o#qBH4zdfe+{?2wlUu@Mo#!1_XsJgD%I;xsj-2b)BxYUL=-sqf%+^E?~}$o_f!^_U6#g%J4L MGw>pjd%^O*0FRxTr2qf` literal 7126 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 diff --git a/data/samples/effects/scratch01.ogg b/data/samples/effects/scratch01.ogg index 9f216038dc49095c6a07a217e28ec0ab95842f32..0b05505cd3615b2d450a59b7719793c315865a38 100644 GIT binary patch literal 12370 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 literal 5516 zcmb_fc|6qH-#;VSl7`eBQD!VMB*qfMm5WNoK3p-X zE9%}db}9^_bW;>XQMtDK&WvC8dG71^_c^aKA7}Y|-kFhfEYuyYUlw0z&y`MD6;q_CLG- zucM1k(p>>K2taXOlr?@bPxu(^vaFv7z7*BBl{T3}PqLX*_&RuYhBNUza)txVZM)%T zjcdypFqP=X&fNYdN^NS(yANhNz`imx0i4_&kG4-!x4P#bC zL+kj#G7tt2QE`&mt>{|}Z3*0Cq?rT$t*lFiQ$qep6v-MBFH5k-jthdQAhz+ydY}Tp zJGIo;s8j?!p;8EFLxdBOZBJw!rMEj1Rr`emX@cIv$_lzv9F)2ASVaqP^qugGJNs5V>X2OgEG9y~w0y`~GkiApP>4+_X#nz1|vCu%!5~0v`6?PSy3>DkknjOzH zyPoOT&*?bM8T8BkE?$SDUHe_VU>|bz8psOsIORXgi}o6b_F4-vOqpKlm|BJsHbWVop<;6|s8t;t z6yaBVQsIHrG3(Sf?ioAJWCvc$4lc_rFUy^}Ct!gW5@g}G1XfVecw&;;nT)_I*+Exw zORwa(^e3ul97szD3Yz$yxMa~r0eE#&O8>-p;Pbc+uAd{;BAj3_-$FY$I0GRBRIl5+}@f+o`E06E~uW>JxcU4unT2 zYAU`ok4n_rbuG?;uyp<}| zfoQ|x93oJ*kcrw8rGDK`60ABmLWmYq#>1wJn@(hCaY-J@oCG3C65H%fOyY3dD9JUv zL>yKwim5G%&0_j$Vw-bJtjgjtNV`~^K2imiL)tf?7A-@Xyiu1;D%XM%5Q<4+mvKns z3S0|`Durz(k;-f8+$a@VE$)3&z(G7Q@=wWi_2z)Xkq(Nl#lnCbEwv3 zygt5iPcr)+RKAI?yqzym$(eAc%xH1*JQ#3;)?G8;OWk7%C6d{2L#n>ThdOO?n#gyM zL@ejPjY{2l7!n9&8v^~HrPYk@AeIZ1vw6@D-GKsU2L2A;s&Q|s)Sq0>xMbWs)WPN! zPo2@Og*;=!bOeLqV+sfR+GYZvt)tts{XK&1Z^W#}$|vc#)fQruI0RoVekyznq+3AlqnX=;rTLBhzt`o zRaQ1BxrS?v<)R@#2)h|Yd5fYG2{vUM6osuzhRmw0J`$sb_Z%{3LHHUTi8_sD5{U$M zvpZzD48_)%C-Tg&SQd}O;P7&(-Ks38gU<{fM`%zDpPBzSQna`zN|!DiO*eA3lGm8= z6C(oVwU_ibjtiMNf`_36n!(}mpNRRFP zsC9-gi^p%9~02!$bkBX$J2*$V5rWNfcV`_t8Nt#+?iezzIbkrejF$@by z6j3dbC1L^0CO@O5YC3+Z0J$Cj)FTv=w~da?ZX<_kaiQ3MbBQ8TaP_bX1=AWnRLu{RO2L{yB_CM;WiL<(3!iTgl;!IX zjMyA_tx@2{+B5}L8~}e_S_fXmUS+w5wB`r;Trp3@29-XP*my)5n8O5HuLZR^2Rg!n zB?bvO2`4}(7QirJytz65s{rAV6jFdcO}ZeX2LVW|b5q~NB6i|-YH26oEVaxMO{_4^ zVovX6#W0fVwu@mD*0H$Rcv1NAfD{IjVu*E{LF|o(J|aK?t#WSTrB_IwGv}B9tj8!R z?YkwTup?W@obCzIb8e(df-PHr!fsXU9E;tUwm0x0kZ?y|6v3u{GL<$wfM`Koc%#&= zV-u`{Dus89Ff1+*ivR`yDExHL+$;(uw@qGQ`wm4k2C9Z_9Z&?|vKaCya=T>$SH$Xa zi>S4=V24kTNTguQ|3p9{1t0aoN^XKB0fCN#OR;&kc4%qbudT_yQB>1}QBhGwt7sH4 z6Fd+$@OE$x$OBs-ojnumUI11@X#;)vSIZ4&=yIK zm%sQ)5MvqNk#2e2_U+{SXrXq+B5~r-J*FntGL=-d=()fc31@#4%AFG8V1#~ZHe;*@ zl>IY$TFPftEOTPM+pi@4vQ*d1i8vhKANK1Jip5vu2fXj~DlgJ@lsS5Knhj^ortE)9 zeckdv-1eE(q~o33k9qmUinyISuetbDk0nw_L#-@}t1Go%cCkZ?v=-Oh=QOcvzAx;~ zQ#w5TxprsL%peztNAv)*WcM`aTBg`&%RgjPq;F%Z$JCP6kgdr z%jx#f4K&}BkEiY>o7vsf{_>=;#H!)KhsEFRmg|;=jZw7WZ>d+6tsJ)Wq@AU>zR)>^ zXlh_L{hj)Xn2t#J{CdOKw#W`$qrr*eS==2mDVDNTzn2?nC(lHa!%zN6ev$UfKj_b6 z*S!NecFsS!|1?>tf2O_U4Cl*;u;Yp4&3*-)f2|Y-b`Cyy`EGk#?%$9A7Gz9CG}&slW4E{Prbp)^>5=o#a_C>3G-2^Vw|7T^{nqa7*C8kZtmb zeq$TUlus66=Qr(dT<)Eym~oCdn#B!c9BVy4^2poBl8y3QxvSpsBxcz)s5(<8%=E>j z80Yz9|4|)xSEDMrg`0SCQj5Wpe%^&n^w-7u9YRA`6J@f9#j_S>E^JW*1&yMVT6IeTfbJi_C zQ#yKTS8!;JK~;#WYcyMZ(diz;1T1DLnD7!9r-)KWIubok`>=>>7)$(-J7NXGWl{=aZ+?5?1k=Wy&7?X9bLMrM_l*-Lr z+5HbfwBt+}wsPKSlt<62XmW%4l9AF|WtA)E=1SETyI$UI>a_BYf0Oe^sA8tJ+d^^S zc&mpr&hCWBFFRq4PaoA87`Kxt= zJ^KC}>Gq41g`;W=dwCj*5pqKLWQAr&-#gEUgXKAf;B@$PX^-3WT9|~2$tn%Kd71Br zJ@nZ_zPY*6^}Tfslm4si{ezn0WbAoe#~mNHihlfJ|HAIU>6OUE)ft}@b;j-jjnuAO zC&TBx`tz*|$GQf3Q^&$*gsvW6mC+8*tctj{YS9q1)H1C%CwX=h^ZIt<&vFqyRwczz ziY@U+n|`}_s@c6b*Q++n(0y0%om=`w@7iu2Q(U}uV9%Jy+wYU_zfAii^$%VA*7c=# zS#F_JC0W?67K`Znsq#gxY6Z{x^SJGF%X7^OORW{ja`Np4b{!+Yu2x*7%(%5I2~s9^Q^)27gvM)65|S> zcjiOUciei+LEjG`mb_{B=gxgN8FTtV3_bkJY`~r6oc*isN-EiV>TfIA4=>$AzL5Vs z8j;*vZCST^u4(wJfqgBnq_(K|)XpN`l&_}Gswd0$iCRAIvb=D~&v?c@t|4Gsyhh3E z*vyBWbKVQ*R2%xp_~j!j!+)LVHyFBlyEREZ<%9W{Uii!J@&n3uR$C0ZX1g-&?1b`9 zT0B!ESZz9VBLWMW&V0Bt7?DM&`Yp!R>%d{|o4eDtwS`(1YU8UamQ$WhN7XI4^_y3W zzXj5IUWw*vzx69TFFtkh)b+RcvkLP1mRTl-&$_LLJ4L1=Rh_$KnrO!(P5)Hfk~vb| zIPr;87OLJMeP`2`fc(RA-%_N_Jey1BpIF*?oJ^$4GfK{~e5BJ|Jvo)q)r;PV$x(GY zy_EH4!l1zV!@k3t+(Nv~`{>l3yvu*oM_av7l5Jj2Q)1Rd4iC6F7u?ESeobn4J#^1~ zN~t!K?D2l-gs#pneWJ@BDxk5?+s(gwdPuO@M3W5C{;b<0Bsc8llOemUGH~aMS-6D_ zH81au8}HdMWndrO+P)_u-reg}d%AVy^=SR63+>+nabciPpAeB_olN5{Qw35~HUd9W#EwohAUgF=R4 z@B5i7R*T13nSdp-Lrk@1Bh5B#VB3BlAoZgGI~!*g_@4!^ajv2d{Be>9owMq34k HhJyb9N{p{9 diff --git a/data/samples/effects/wind_chimes01.ogg b/data/samples/effects/wind_chimes01.ogg index 35d3374a2e2465f38380d7556b5ff4c6fecb6f12..7fb3c441a24c9184ea544604b474bcf998450dd0 100644 GIT binary patch literal 57873 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 AJ^%m! literal 22654 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�N8BI=>A&+m!_r3kJq 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�Vb`AKuTSXj;ait*miRnbc)i?K3Y`y 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 QFJbgN{C~2M8UKIz7tkX;xBvhE diff --git a/data/samples/instruments/harpsichord01.ogg b/data/samples/instruments/harpsichord01.ogg index 028bbd912596ea0492dbcbb23f0cab73e8b9f371..c84ffd7df44519fc3323c22aac403ca69a25a375 100644 GIT binary patch literal 60978 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

8H#vZ7^Yax`|9Q=JWLJ*QP(UD1| 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

f6AE{sO1O8|-| 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 WXje3ae0ayF!)1--1L*#*@BabsA+zTI literal 16606 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 From de062d6c54d48cc53d080fb25ed33a6c72d6e26f Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Tue, 5 Sep 2023 01:33:42 +0200 Subject: [PATCH 16/51] Show detailed CPU load information in a tool-tip (#5970) * Profiler rework * Workaround for GCC bug * Rollback QFile removal * Use enum instead of a plain index to describe detailed stats * Use the GCC workaround code for all compilers to avoid redundancy * Update and fix comments * Implement suggestions from review * Split AudioEngine::renderNextBuffer() into separate functions, fix old formatting * Remove QFile include * Revert formatting changes * Apply suggestion from review (remove unnecessary template parameter) * Revert more formatting changes * Convert DetailType to enum class * DetailType enum class improvements suggested in review * Use std::atomic for m_detailLoad * RAII-style profiler probes * Apply suggestions from code review Co-authored-by: Dominic Clark * Fix namespace comment * Improve CPU load widget precision and use floats for load computations * Atomic m_cpuLoad * Add custom step size support for CPULoadWidget * Apply suggestions from review (convert the profiler probe into a nested class, other small changes) * Do not limit stored load averages to 100% --------- Co-authored-by: sakertooth Co-authored-by: Dominic Clark --- data/themes/classic/style.css | 1 + data/themes/default/style.css | 1 + include/AudioEngine.h | 9 ++++++ include/AudioEngineProfiler.h | 48 +++++++++++++++++++++++++++++- include/CPULoadWidget.h | 6 ++++ src/core/AudioEngine.cpp | 49 ++++++++++++++++++++++++------- src/core/AudioEngineProfiler.cpp | 22 ++++++++++++-- src/gui/widgets/CPULoadWidget.cpp | 27 ++++++++++++----- 8 files changed, 140 insertions(+), 23 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index d1f4d0588a9..ac22a14ec21 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -209,6 +209,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; background: url(resources:cpuload_bg.png); + qproperty-stepSize: 4; } /* scrollbar: trough */ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index e88e51e5bd4..092332eee65 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -241,6 +241,7 @@ lmms--gui--Oscilloscope { lmms--gui--CPULoadWidget { border: none; background: url(resources:cpuload_bg.png); + qproperty-stepSize: 1; } /* scrollbar: trough */ diff --git a/include/AudioEngine.h b/include/AudioEngine.h index 030c5bce39e..f056c22e108 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -275,6 +275,11 @@ class LMMS_EXPORT AudioEngine : public QObject return m_profiler.cpuLoad(); } + int detailLoad(const AudioEngineProfiler::DetailType type) const + { + return m_profiler.detailLoad(type); + } + const qualitySettings & currentQualitySettings() const { return m_qualitySettings; @@ -401,6 +406,10 @@ class LMMS_EXPORT AudioEngine : public QObject AudioDevice * tryAudioDevices(); MidiClient * tryMidiClients(); + void renderStageNoteSetup(); + void renderStageInstruments(); + void renderStageEffects(); + void renderStageMix(); const surroundSampleFrame * renderNextBuffer(); diff --git a/include/AudioEngineProfiler.h b/include/AudioEngineProfiler.h index 7b5191e76b4..b0d62a1dc9e 100644 --- a/include/AudioEngineProfiler.h +++ b/include/AudioEngineProfiler.h @@ -25,6 +25,8 @@ #ifndef LMMS_AUDIO_ENGINE_PROFILER_H #define LMMS_AUDIO_ENGINE_PROFILER_H +#include +#include #include #include "lmms_basics.h" @@ -53,11 +55,55 @@ class AudioEngineProfiler void setOutputFile( const QString& outputFile ); + enum class DetailType { + NoteSetup, + Instruments, + Effects, + Mixing, + Count + }; + + constexpr static auto DetailCount = static_cast(DetailType::Count); + + int detailLoad(const DetailType type) const + { + return m_detailLoad[static_cast(type)].load(std::memory_order_relaxed); + } + + class Probe + { + public: + Probe(AudioEngineProfiler& profiler, AudioEngineProfiler::DetailType type) + : m_profiler(profiler) + , m_type(type) + { + profiler.startDetail(type); + } + ~Probe() { m_profiler.finishDetail(m_type); } + Probe& operator=(const Probe&) = delete; + Probe(const Probe&) = delete; + Probe(Probe&&) = delete; + + private: + AudioEngineProfiler &m_profiler; + const AudioEngineProfiler::DetailType m_type; + }; private: + void startDetail(const DetailType type) { m_detailTimer[static_cast(type)].reset(); } + void finishDetail(const DetailType type) + { + m_detailTime[static_cast(type)] = m_detailTimer[static_cast(type)].elapsed(); + } + MicroTimer m_periodTimer; - int m_cpuLoad; + std::atomic m_cpuLoad; QFile m_outputFile; + + // Use arrays to avoid dynamic allocations in realtime code + std::array m_detailTimer; + std::array m_detailTime{0}; + std::array, DetailCount> m_detailLoad{0}; }; } // namespace lmms diff --git a/include/CPULoadWidget.h b/include/CPULoadWidget.h index 904445c67f0..dfa5bac73da 100644 --- a/include/CPULoadWidget.h +++ b/include/CPULoadWidget.h @@ -26,6 +26,7 @@ #ifndef LMMS_GUI_CPU_LOAD_WIDGET_H #define LMMS_GUI_CPU_LOAD_WIDGET_H +#include #include #include #include @@ -40,6 +41,7 @@ namespace lmms::gui class CPULoadWidget : public QWidget { Q_OBJECT + Q_PROPERTY(int stepSize MEMBER m_stepSize) public: CPULoadWidget( QWidget * _parent ); ~CPULoadWidget() override = default; @@ -54,6 +56,8 @@ protected slots: private: + int stepSize() const { return std::max(1, m_stepSize); } + int m_currentLoad; QPixmap m_temp; @@ -64,6 +68,8 @@ protected slots: QTimer m_updateTimer; + int m_stepSize; + } ; diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 21a9a3598bc..2d077babcfd 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -333,12 +333,9 @@ void AudioEngine::pushInputFrames( sampleFrame * _ab, const f_cnt_t _frames ) - -const surroundSampleFrame * AudioEngine::renderNextBuffer() +void AudioEngine::renderStageNoteSetup() { - m_profiler.startPeriod(); - - s_renderingThread = true; + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::NoteSetup); if( m_clearSignal ) { @@ -387,9 +384,15 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() m_newPlayHandles.free( e ); e = next; } +} + + - // STAGE 1: run and render all play handles - AudioEngineWorkerThread::fillJobQueue( m_playHandles ); +void AudioEngine::renderStageInstruments() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Instruments); + + AudioEngineWorkerThread::fillJobQueue(m_playHandles); AudioEngineWorkerThread::startAndWaitForJobs(); // removed all play handles which are done @@ -417,15 +420,27 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() ++it; } } +} + + + +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(); +} - // STAGE 3: do master mix in mixer - mixer->masterMix(m_outputBufferWrite); +void AudioEngine::renderStageMix() +{ + AudioEngineProfiler::Probe profilerProbe(m_profiler, AudioEngineProfiler::DetailType::Mixing); + + Mixer *mixer = Engine::mixer(); + mixer->masterMix(m_outputBufferWrite); emit nextAudioBuffer(m_outputBufferRead); @@ -435,10 +450,22 @@ const surroundSampleFrame * AudioEngine::renderNextBuffer() EnvelopeAndLfoParameters::instances()->trigger(); Controller::triggerFrameCounter(); AutomatableModel::incrementPeriodCounter(); +} - s_renderingThread = false; - m_profiler.finishPeriod( processingSampleRate(), m_framesPerPeriod ); + +const surroundSampleFrame *AudioEngine::renderNextBuffer() +{ + m_profiler.startPeriod(); + s_renderingThread = true; + + renderStageNoteSetup(); // STAGE 0: clear old play handles and buffers, setup new play handles + renderStageInstruments(); // STAGE 1: run and render all play handles + renderStageEffects(); // STAGE 2: process effects of all instrument- and sampletracks + renderStageMix(); // STAGE 3: do master mix in mixer + + s_renderingThread = false; + m_profiler.finishPeriod(processingSampleRate(), m_framesPerPeriod); return m_outputBufferRead; } diff --git a/src/core/AudioEngineProfiler.cpp b/src/core/AudioEngineProfiler.cpp index 9e05ff80afa..82a412cbb98 100644 --- a/src/core/AudioEngineProfiler.cpp +++ b/src/core/AudioEngineProfiler.cpp @@ -24,6 +24,8 @@ #include "AudioEngineProfiler.h" +#include + namespace lmms { @@ -38,10 +40,24 @@ AudioEngineProfiler::AudioEngineProfiler() : void AudioEngineProfiler::finishPeriod( sample_rate_t sampleRate, fpp_t framesPerPeriod ) { - int periodElapsed = m_periodTimer.elapsed(); + // Time taken to process all data and fill the audio buffer. + const unsigned int periodElapsed = m_periodTimer.elapsed(); + // Maximum time the processing can take before causing buffer underflow. Convert to us. + const uint64_t timeLimit = static_cast(1000000) * framesPerPeriod / sampleRate; + + // Compute new overall CPU load and apply exponential averaging. + // The result is used for overload detection in AudioEngine::criticalXRuns() + // → the weight of a new sample must be high enough to allow relatively fast changes! + const auto newCpuLoad = 100.f * periodElapsed / timeLimit; + m_cpuLoad = newCpuLoad * 0.1f + m_cpuLoad * 0.9f; - const float newCpuLoad = periodElapsed / 10000.0f * sampleRate / framesPerPeriod; - m_cpuLoad = std::clamp((newCpuLoad * 0.1f + m_cpuLoad * 0.9f), 0, 100); + // Compute detailed load analysis. Can use stronger averaging to get more stable readout. + for (std::size_t i = 0; i < DetailCount; i++) + { + const auto newLoad = 100.f * m_detailTime[i] / timeLimit; + const auto oldLoad = m_detailLoad[i].load(std::memory_order_relaxed); + m_detailLoad[i].store(newLoad * 0.05f + oldLoad * 0.95f, std::memory_order_relaxed); + } if( m_outputFile.isOpen() ) { diff --git a/src/gui/widgets/CPULoadWidget.cpp b/src/gui/widgets/CPULoadWidget.cpp index 799e037ef43..db1f5cacc8f 100644 --- a/src/gui/widgets/CPULoadWidget.cpp +++ b/src/gui/widgets/CPULoadWidget.cpp @@ -24,6 +24,7 @@ */ +#include #include #include "AudioEngine.h" @@ -72,10 +73,9 @@ void CPULoadWidget::paintEvent( QPaintEvent * ) QPainter p( &m_temp ); p.drawPixmap( 0, 0, m_background ); - // as load-indicator consists of small 2-pixel wide leds with - // 1 pixel spacing, we have to make sure, only whole leds are - // shown which we achieve by the following formula - int w = ( m_leds.width() * m_currentLoad / 300 ) * 3; + // Normally the CPU load indicator moves smoothly, with 1 pixel resolution. However, some themes may want to + // draw discrete elements (like LEDs), so the stepSize property can be used to specify a larger step size. + int w = (m_leds.width() * std::min(m_currentLoad, 100) / (stepSize() * 100)) * stepSize(); if( w > 0 ) { p.drawPixmap( 23, 3, m_leds, 0, 0, w, @@ -91,10 +91,21 @@ void CPULoadWidget::paintEvent( QPaintEvent * ) void CPULoadWidget::updateCpuLoad() { - // smooth load-values a bit - int new_load = ( m_currentLoad + Engine::audioEngine()->cpuLoad() ) / 2; - if( new_load != m_currentLoad ) + // Additional display smoothing for the main load-value. Stronger averaging + // cannot be used directly in the profiler: cpuLoad() must react fast enough + // to be useful as overload indicator in AudioEngine::criticalXRuns(). + const int new_load = (m_currentLoad + Engine::audioEngine()->cpuLoad()) / 2; + + if (new_load != m_currentLoad) { + auto engine = Engine::audioEngine(); + setToolTip( + tr("DSP total: %1%").arg(new_load) + "\n" + + tr(" - Notes and setup: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::NoteSetup)) + "\n" + + tr(" - Instruments: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Instruments)) + "\n" + + tr(" - Effects: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Effects)) + "\n" + + tr(" - Mixing: %1%").arg(engine->detailLoad(AudioEngineProfiler::DetailType::Mixing)) + ); m_currentLoad = new_load; m_changed = true; update(); @@ -102,4 +113,4 @@ void CPULoadWidget::updateCpuLoad() } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui From b7196337a4fedec03566f39a220a42f154a5d806 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Thu, 7 Sep 2023 11:43:23 -0700 Subject: [PATCH 17/51] Fix Compressor arithmetic exception in Debug mode (#6854) --- plugins/Compressor/Compressor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp index 3c5ad615714..33571606e7e 100755 --- a/plugins/Compressor/Compressor.cpp +++ b/plugins/Compressor/Compressor.cpp @@ -320,8 +320,8 @@ bool CompressorEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) float inputValue = feedback ? m_prevOut[i] : s[i]; // Calculate the crest factor of the audio by diving the peak by the RMS - m_crestPeakVal[i] = qMax(inputValue * inputValue, m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); - m_crestRmsVal[i] = m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inputValue * inputValue)); + m_crestPeakVal[i] = qMax(qMax(COMP_NOISE_FLOOR, inputValue * inputValue), m_crestTimeConst * m_crestPeakVal[i] + (1 - m_crestTimeConst) * (inputValue * inputValue)); + m_crestRmsVal[i] = qMax(COMP_NOISE_FLOOR, m_crestTimeConst * m_crestRmsVal[i] + ((1 - m_crestTimeConst) * (inputValue * inputValue))); m_crestFactorVal[i] = m_crestPeakVal[i] / m_crestRmsVal[i]; m_rmsVal[i] = m_rmsTimeConst * m_rmsVal[i] + ((1 - m_rmsTimeConst) * (inputValue * inputValue)); From 10f1b21fb7ec2beeef42b43bef53e30fdb4e87eb Mon Sep 17 00:00:00 2001 From: consolegrl <5942029+consolegrl@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:19:56 -0400 Subject: [PATCH 18/51] fixes #4049: Off-grid nodes not draggable (#6852) The quantPos arg of AutomationClip::setDragValue causes the node time to be quantized before its looked up in the timeMap iterator. This results in the node not being found and a new one being created inside the setDragValue function even though we had found one. Simply setting it to true causes this bug, simply setting it to false causes a new node to be created off grid/not snapped. So the fix is to quantize the position for the lookup only if we haven't found an existing node under the cursor. --- src/gui/editors/AutomationEditor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index a24165332d0..f98066bbaa2 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -531,7 +531,7 @@ void AutomationEditor::mousePressEvent( QMouseEvent* mouseEvent ) : POS(clickedNode) ), level, - true, + clickedNode == tm.end(), mouseEvent->modifiers() & Qt::ControlModifier ); From b353cbea822b73ccef5f4f0ea7a22cf5e19162e5 Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 8 Sep 2023 11:49:43 -0400 Subject: [PATCH 19/51] Add options to enable sanitizers (#6840) * Add options to enable sanitizers * Specify them as debugging options * Apply suggestions made by Dom * Fix linking issues There were linking issues, most likely because we were applying the sanitizer flags to unnecessary targets that are part of the build. Now, we only focus on adding the compiler flags to lmmsobjs as a PUBLIC dependency, and selectively choose what targets we need to apply the linker flags to as PRIVATE dependencies. * Add UBSan * Add status messages * Remove GNU as supported compiler for MSan * Revert to using add__options again * Fix sanitizer linkage within veal and cmt libraries I suspect that our sanitizer flags were removed for these two CMake targets. Instead of setting the properties directly, we call target_compile_options and target_link_options instead. * Remove TODO comment * Revert "Fix sanitizer linkage within veal and cmt libraries" This reverts commit b04dce8d8c0092353ec9fc462d237bac369a9eb6. * Use CMAKE__FLAGS_ and apply fixes * Remove quotes * Add support for additional flags * Disable vptr for UBSan * Print "Debug using" in status for clarity * Wrap ${supported_compilers} and ${status_flag} in quotes * Replace semicolons with spaces in additional_flags * Remove platform check for no-undefined flag The platform check was added as an attempt to make this branch compile with the veal and cmt libraries. However, it seems to work without it, so the problem must've been something else occuring in these files. * Undo change to FP exceptions status message * Use spaces instead of tabs * Remove -no-undefined flag for cmt and veal libraries --- CMakeLists.txt | 36 ++++++++++++++++++++++-- plugins/LadspaEffect/calf/CMakeLists.txt | 6 ++-- plugins/LadspaEffect/cmt/CMakeLists.txt | 6 ++-- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e20f1352701..1b17d0e7c98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,10 @@ OPTION(WANT_VST_32 "Include 32-bit VST support" ON) OPTION(WANT_VST_64 "Include 64-bit 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) +option(WANT_DEBUG_TSAN "Enable ThreadSanitizer" OFF) +option(WANT_DEBUG_MSAN "Enable MemorySanitizer" OFF) +option(WANT_DEBUG_UBSAN "Enable UndefinedBehaviorSanitizer" OFF) OPTION(BUNDLE_QT_TRANSLATIONS "Install Qt translation files for LMMS" OFF) @@ -649,6 +653,31 @@ IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") ENDIF(WIN32) ENDIF() +# add enabled sanitizers +function(add_sanitizer sanitizer supported_compilers want_flag status_flag) + if(${want_flag}) + if(CMAKE_CXX_COMPILER_ID MATCHES "${supported_compilers}") + set("${status_flag}" "Enabled" PARENT_SCOPE) + string(REPLACE ";" " " additional_flags "${ARGN}") + # todo CMake 3.13: use add_compile_options/add_link_options instead + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=${sanitizer} ${additional_flags}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=${sanitizer} ${additional_flags}" PARENT_SCOPE) + else() + set("${status_flag}" "Wanted but disabled due to unsupported compiler" PARENT_SCOPE) + endif() + else() + set("${status_flag}" "Disabled" PARENT_SCOPE) + endif() +endfunction() + +add_sanitizer(address "GNU|Clang|MSVC" WANT_DEBUG_ASAN STATUS_DEBUG_ASAN) +add_sanitizer(thread "GNU|Clang" WANT_DEBUG_TSAN STATUS_DEBUG_TSAN) +add_sanitizer(memory "Clang" WANT_DEBUG_MSAN STATUS_DEBUG_MSAN -fno-omit-frame-pointer) +# UBSan does not link with vptr enabled due to a problem with references from PeakControllerEffect +# not being found by PeakController +add_sanitizer(undefined "GNU|Clang" WANT_DEBUG_UBSAN STATUS_DEBUG_UBSAN -fno-sanitize=vptr) + + # use ccache include(CompileCache) @@ -717,7 +746,6 @@ ADD_CUSTOM_TARGET(uninstall COMMAND ${CMAKE_COMMAND} -DCMAKE_INSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/uninstall.cmake" ) - # # display configuration information # @@ -783,7 +811,11 @@ MESSAGE( MESSAGE( "Developer options\n" "-----------------------------------------\n" -"* Debug FP exceptions : ${STATUS_DEBUG_FPE}\n" +"* Debug FP exceptions : ${STATUS_DEBUG_FPE}\n" +"* Debug using AddressSanitizer : ${STATUS_DEBUG_ASAN}\n" +"* Debug using ThreadSanitizer : ${STATUS_DEBUG_TSAN}\n" +"* Debug using MemorySanitizer : ${STATUS_DEBUG_MSAN}\n" +"* Debug using UBSanitizer : ${STATUS_DEBUG_UBSAN}\n" ) MESSAGE( diff --git a/plugins/LadspaEffect/calf/CMakeLists.txt b/plugins/LadspaEffect/calf/CMakeLists.txt index 90f50641738..0c9cd8fa96e 100644 --- a/plugins/LadspaEffect/calf/CMakeLists.txt +++ b/plugins/LadspaEffect/calf/CMakeLists.txt @@ -36,9 +36,9 @@ TARGET_COMPILE_DEFINITIONS(veal PRIVATE DISABLE_OSC=1) SET(INLINE_FLAGS "") IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - SET(INLINE_FLAGS "-finline-functions-called-once -finline-limit=80") + SET(INLINE_FLAGS -finline-functions-called-once -finline-limit=80) ENDIF() -SET_TARGET_PROPERTIES(veal PROPERTIES COMPILE_FLAGS "-fexceptions -O2 -finline-functions ${INLINE_FLAGS}") +target_compile_options(veal PRIVATE -fexceptions -O2 -finline-functions ${INLINE_FLAGS}) if(LMMS_BUILD_WIN32) add_custom_command( @@ -50,5 +50,5 @@ if(LMMS_BUILD_WIN32) ) endif() IF(NOT LMMS_BUILD_APPLE AND NOT LMMS_BUILD_OPENBSD) - SET_TARGET_PROPERTIES(veal PROPERTIES LINK_FLAGS "${LINK_FLAGS} -shared -Wl,-no-undefined") + target_link_libraries(veal PRIVATE -shared) ENDIF() diff --git a/plugins/LadspaEffect/cmt/CMakeLists.txt b/plugins/LadspaEffect/cmt/CMakeLists.txt index ded7b895898..75dba319d6c 100644 --- a/plugins/LadspaEffect/cmt/CMakeLists.txt +++ b/plugins/LadspaEffect/cmt/CMakeLists.txt @@ -5,7 +5,7 @@ ADD_LIBRARY(cmt MODULE ${SOURCES}) INSTALL(TARGETS cmt LIBRARY DESTINATION "${PLUGIN_DIR}/ladspa") SET_TARGET_PROPERTIES(cmt PROPERTIES PREFIX "") -SET_TARGET_PROPERTIES(cmt PROPERTIES COMPILE_FLAGS "-Wall -O3 -fno-strict-aliasing") +target_compile_options(cmt PRIVATE -Wall -O3 -fno-strict-aliasing) if(LMMS_BUILD_WIN32) add_custom_command( @@ -18,10 +18,10 @@ if(LMMS_BUILD_WIN32) endif() if(NOT LMMS_BUILD_WIN32) - set_target_properties(cmt PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -fPIC") + target_compile_options(cmt PRIVATE -fPIC) endif() IF(NOT LMMS_BUILD_APPLE AND NOT LMMS_BUILD_OPENBSD) - SET_TARGET_PROPERTIES(cmt PROPERTIES LINK_FLAGS "${LINK_FLAGS} -shared -Wl,-no-undefined") + target_link_libraries(cmt PRIVATE -shared) ENDIF() From a8d765f8d57075217546282a55d3bb407512f4bf Mon Sep 17 00:00:00 2001 From: MrTopom Date: Sun, 10 Sep 2023 17:52:18 +0200 Subject: [PATCH 20/51] Make files visible when searching in ``FileBrowser`` (#6845) * Change the title for SideBarWidgets to be vertically centered related to icon and with no underlining * Update src/gui/SideBarWidget.cpp Co-authored-by: saker * Updating FileBrowser display when searching : showing matching files * Correcting increment and removing duplicated calls * Correcting reload tree when filter is activated --------- Co-authored-by: saker --- include/FileBrowser.h | 9 ++- src/gui/FileBrowser.cpp | 124 ++++++++++++++++++++++++++++------------ 2 files changed, 95 insertions(+), 38 deletions(-) diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 3334a73f659..eafb827dac5 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -75,8 +75,7 @@ class FileBrowser : public SideBarWidget private slots: void reloadTree(); void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); - // call with item=NULL to filter the entire tree - bool filterItems( const QString & filter, QTreeWidgetItem * item=nullptr ); + bool filterAndExpandItems(const QString & filter, QTreeWidgetItem * item = nullptr); void giveFocusToFilter(); private: @@ -84,6 +83,9 @@ private slots: void addItems( const QString & path ); + void saveDirectoriesStates(); + void restoreDirectoriesStates(); + FileBrowserTreeWidget * m_fileBrowserTreeWidget; QLineEdit * m_filterEdit; @@ -99,6 +101,8 @@ private slots: QCheckBox* m_showFactoryContent = nullptr; QString m_userDir; QString m_factoryDir; + QList m_savedExpandedDirs; + QString m_previousFilterValue; } ; @@ -115,7 +119,6 @@ class FileBrowserTreeWidget : public QTreeWidget //! that are expanded in the tree. QList expandedDirs( QTreeWidgetItem * item = nullptr ) const; - protected: void contextMenuEvent( QContextMenuEvent * e ) override; void mousePressEvent( QMouseEvent * me ) override; diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index c0763d542ae..181e67cd749 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -23,7 +23,6 @@ * */ - #include #include #include @@ -126,7 +125,7 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_filterEdit->setPlaceholderText( tr("Search") ); m_filterEdit->setClearButtonEnabled( true ); connect( m_filterEdit, SIGNAL( textEdited( const QString& ) ), - this, SLOT( filterItems( const QString& ) ) ); + this, SLOT( filterAndExpandItems( const QString& ) ) ); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); @@ -145,53 +144,95 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, auto filterFocusShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this, SLOT(giveFocusToFilter())); filterFocusShortcut->setContext(Qt::WidgetWithChildrenShortcut); + m_previousFilterValue = ""; + reloadTree(); show(); } -bool FileBrowser::filterItems( const QString & filter, QTreeWidgetItem * item ) +void FileBrowser::saveDirectoriesStates() +{ + m_savedExpandedDirs = m_fileBrowserTreeWidget->expandedDirs(); +} + +void FileBrowser::restoreDirectoriesStates() +{ + expandItems(nullptr, m_savedExpandedDirs); +} + +bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item) { - // call with item=NULL to filter the entire tree + // 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 ) + + for (int i = 0; i < numChildren; ++i) { QTreeWidgetItem * it = item ? item->child( i ) : m_fileBrowserTreeWidget->topLevelItem(i); - // is directory? - if( it->childCount() ) + auto d = dynamic_cast(it); + if (d) { - // matches filter? - if( it->text( 0 ). - contains( filter, Qt::CaseInsensitive ) ) + if (it->text(0).contains(filter, Qt::CaseInsensitive)) { - // yes, then show everything below - it->setHidden( false ); - filterItems( QString(), it ); + it->setHidden(false); + it->setExpanded(true); + filterAndExpandItems(QString(), it); anyMatched = true; } else { - // only show if item below matches filter - bool didMatch = filterItems( filter, it ); - it->setHidden( !didMatch ); + // 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; } } - // a standard item (i.e. no file or directory item?) - else if( it->type() == QTreeWidgetItem::Type ) - { - // hide if there's any filter - it->setHidden( !filter.isEmpty() ); - } + else { - // file matches filter? - bool didMatch = it->text( 0 ). - contains( filter, Qt::CaseInsensitive ); - it->setHidden( !didMatch ); - anyMatched = anyMatched || didMatch; + 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()); + } } } @@ -201,15 +242,20 @@ bool FileBrowser::filterItems( const QString & filter, QTreeWidgetItem * item ) void FileBrowser::reloadTree() { - QList expandedDirs = m_fileBrowserTreeWidget->expandedDirs(); - const QString text = m_filterEdit->text(); - m_filterEdit->clear(); + if (m_filterEdit->text().isEmpty()) + { + saveDirectoriesStates(); + } + m_fileBrowserTreeWidget->clear(); + QStringList paths = m_directories.split('*'); + if (m_showUserContent && !m_showUserContent->isChecked()) { paths.removeAll(m_userDir); } + if (m_showFactoryContent && !m_showFactoryContent->isChecked()) { paths.removeAll(m_factoryDir); @@ -222,9 +268,15 @@ void FileBrowser::reloadTree() addItems(path); } } - expandItems(nullptr, expandedDirs); - m_filterEdit->setText( text ); - filterItems( text ); + + if (m_filterEdit->text().isEmpty()) + { + restoreDirectoriesStates(); + } + else + { + filterAndExpandItems(m_filterEdit->text()); + } } @@ -240,12 +292,16 @@ void FileBrowser::expandItems(QTreeWidgetItem* item, QList expandedDirs { // 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()) { expandItems(it, expandedDirs); } } + + it->setHidden(false); } } @@ -416,8 +472,6 @@ QList FileBrowserTreeWidget::expandedDirs( QTreeWidgetItem * item ) con } - - void FileBrowserTreeWidget::keyPressEvent(QKeyEvent * ke ) { // Shorter names for some commonly used properties of the event From 296d5736c8371e365a877f14de8bd45f57ff720f Mon Sep 17 00:00:00 2001 From: Babakinha <59146844+Babakinha@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:57:36 -0300 Subject: [PATCH 21/51] Remember to check for Linux VST Effects (#6751) Remember to also check .so files and not only .dll --- plugins/VstEffect/VstSubPluginFeatures.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/VstEffect/VstSubPluginFeatures.cpp b/plugins/VstEffect/VstSubPluginFeatures.cpp index f929b5526bb..7eab7a9bf67 100644 --- a/plugins/VstEffect/VstSubPluginFeatures.cpp +++ b/plugins/VstEffect/VstSubPluginFeatures.cpp @@ -82,7 +82,11 @@ void VstSubPluginFeatures::addPluginsFromDir( QStringList* filenames, QString pa } } QStringList dlls = QDir( ConfigManager::inst()->vstDir() + path ). - entryList( QStringList() << "*.dll", + entryList( QStringList() << "*.dll" +#ifdef LMMS_BUILD_LINUX + << "*.so" +#endif + , QDir::Files, QDir::Name ); for( int i = 0; i < dlls.size(); i++ ) { From b64912cd57db7bfa2692be42bfa904adc1e72b65 Mon Sep 17 00:00:00 2001 From: consolegrl <5942029+consolegrl@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:35:54 -0400 Subject: [PATCH 22/51] fixes #6354: Sample and Hold for LFO Controller (#6850) * fixes #6354: Sample and Hold for LFO Controller LFO controller's "white noise" wave shape didn't respect the frequency knob at all, so Sample-and-Hold was added to extend the functionality of the LFO Controller with this random waveshape. The original functionallity can still be accessed by setting the FREQ knob to minimum (0.01) --------- Co-authored-by: Kevin Zander Co-authored-by: saker --- include/DataFile.h | 1 + include/LfoController.h | 1 + src/core/DataFile.cpp | 18 ++++++++++++++++++ src/core/LfoController.cpp | 34 ++++++++++++++++++++++++++++++---- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/include/DataFile.h b/include/DataFile.h index 137f0156fe2..c19a00f7f30 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -126,6 +126,7 @@ class LMMS_EXPORT DataFile : public QDomDocument void upgrade_defaultTripleOscillatorHQ(); void upgrade_mixerRename(); void upgrade_bbTcoRename(); + void upgrade_sampleAndHold(); // List of all upgrade methods static const std::vector UPGRADE_METHODS; diff --git a/include/LfoController.h b/include/LfoController.h index 1c63ba69883..109edbd3fd8 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -86,6 +86,7 @@ public slots: sample_t (*m_sampleFunction)( const float ); private: + float m_heldSample; SampleBuffer * m_userDefSampleBuffer; protected slots: diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 5c98ec81c5c..d237d13aee3 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -79,6 +79,7 @@ const std::vector DataFile::UPGRADE_METHODS = { &DataFile::upgrade_automationNodes , &DataFile::upgrade_extendedNoteRange, &DataFile::upgrade_defaultTripleOscillatorHQ, &DataFile::upgrade_mixerRename , &DataFile::upgrade_bbTcoRename, + &DataFile::upgrade_sampleAndHold , }; // Vector of all versions that have upgrade routines. @@ -1762,6 +1763,23 @@ void DataFile::upgrade_bbTcoRename() } +// Set LFO speed to 0.01 on projects made before sample-and-hold PR +void DataFile::upgrade_sampleAndHold() +{ + QDomNodeList elements = elementsByTagName("lfocontroller"); + for (int i = 0; i < elements.length(); ++i) + { + if (elements.item(i).isNull()) { continue; } + auto e = elements.item(i).toElement(); + // Correct old random wave LFO speeds + if (e.attribute("wave").toInt() == 6) + { + e.setAttribute("speed",0.01f); + } + } +} + + void DataFile::upgrade() { // Runs all necessary upgrade methods diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 23621b84727..88f64803c2b 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -88,6 +88,7 @@ void LfoController::updateValueBuffer() { m_phaseOffset = m_phaseModel.value() / 360.0; float phase = m_currentPhase + m_phaseOffset; + float phasePrev = 0.0f; // roll phase up until we're in sync with period counter m_bufferLastUpdated++; @@ -102,20 +103,45 @@ void LfoController::updateValueBuffer() ValueBuffer *amountBuffer = m_amountModel.valueBuffer(); int amountInc = amountBuffer ? 1 : 0; float *amountPtr = amountBuffer ? &(amountBuffer->values()[ 0 ] ) : &amount; + Oscillator::WaveShape waveshape = static_cast(m_waveModel.value()); for( float& f : m_valueBuffer ) { - const float currentSample = m_sampleFunction != nullptr - ? m_sampleFunction( phase ) - : m_userDefSampleBuffer->userWaveSample( phase ); + float currentSample = 0; + switch (waveshape) + { + case Oscillator::WaveShape::WhiteNoise: + { + if (absFraction(phase) < absFraction(phasePrev)) + { + // Resample when phase period has completed + m_heldSample = m_sampleFunction(phase); + } + currentSample = m_heldSample; + break; + } + case Oscillator::WaveShape::UserDefined: + { + currentSample = m_userDefSampleBuffer->userWaveSample(phase); + break; + } + default: + { + if (m_sampleFunction != nullptr) + { + currentSample = m_sampleFunction(phase); + } + } + } f = std::clamp(m_baseModel.value() + (*amountPtr * currentSample / 2.0f), 0.0f, 1.0f); + phasePrev = phase; phase += 1.0 / m_duration; amountPtr += amountInc; } - m_currentPhase = absFraction( phase - m_phaseOffset ); + m_currentPhase = absFraction(phase - m_phaseOffset); m_bufferLastUpdated = s_periods; } From 89baab6b87df8401cea43118a067573c1e99d303 Mon Sep 17 00:00:00 2001 From: MrTopom Date: Tue, 12 Sep 2023 23:30:31 +0200 Subject: [PATCH 23/51] FileDialog : Adding mounted volumes on Linux (#6834) * FileDialog : Adding mounted volumes on Linux * Adding some file systems types and collapsing 2 'if' statements in one * Removing the foreach * Correcting PREPROCESSOR directive --- src/gui/modals/FileDialog.cpp | 58 ++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/gui/modals/FileDialog.cpp b/src/gui/modals/FileDialog.cpp index 512d7179f0d..a6cf4827a3e 100644 --- a/src/gui/modals/FileDialog.cpp +++ b/src/gui/modals/FileDialog.cpp @@ -26,11 +26,12 @@ #include #include #include +#include +#include #include "ConfigManager.h" #include "FileDialog.h" - namespace lmms::gui { @@ -45,19 +46,38 @@ FileDialog::FileDialog( QWidget *parent, const QString &caption, setOption( QFileDialog::DontUseNativeDialog ); - // Add additional locations to the sidebar +#ifdef LMMS_BUILD_LINUX + QList urls; +#else QList urls = sidebarUrls(); - urls << QUrl::fromLocalFile( QStandardPaths::writableLocation( QStandardPaths::DesktopLocation ) ); - // Find downloads directory - QDir downloadDir( QDir::homePath() + "/Downloads" ); - if ( ! downloadDir.exists() ) - downloadDir.setPath(QStandardPaths::writableLocation( QStandardPaths::DownloadLocation )); - if ( downloadDir.exists() ) - urls << QUrl::fromLocalFile( downloadDir.absolutePath() ); +#endif - urls << QUrl::fromLocalFile( QStandardPaths::writableLocation( QStandardPaths::MusicLocation ) ); - urls << QUrl::fromLocalFile( ConfigManager::inst()->workingDir() ); + QDir desktopDir; + desktopDir.setPath(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); + if (desktopDir.exists()) + { + urls << QUrl::fromLocalFile(desktopDir.absolutePath()); + } + + QDir downloadDir(QDir::homePath() + "/Downloads"); + if (!downloadDir.exists()) + { + downloadDir.setPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + } + if (downloadDir.exists()) + { + urls << QUrl::fromLocalFile(downloadDir.absolutePath()); + } + + QDir musicDir; + musicDir.setPath(QStandardPaths::writableLocation(QStandardPaths::MusicLocation)); + if (musicDir.exists()) + { + urls << QUrl::fromLocalFile(musicDir.absolutePath()); + } + urls << QUrl::fromLocalFile(ConfigManager::inst()->workingDir()); + // Add `/Volumes` directory on OS X systems, this allows the user to browse // external disk drives. #ifdef LMMS_BUILD_APPLE @@ -66,6 +86,22 @@ FileDialog::FileDialog( QWidget *parent, const QString &caption, urls << QUrl::fromLocalFile( volumesDir.absolutePath() ); #endif +#ifdef LMMS_BUILD_LINUX + + // FileSystem types : https://www.javatpoint.com/linux-file-system + QStringList usableFileSystems = {"ext", "ext2", "ext3", "ext4", "jfs", "reiserfs", "ntfs3", "fuse.sshfs", "fuseblk"}; + + for(QStorageInfo storage : QStorageInfo::mountedVolumes()) + { + storage.refresh(); + + if (usableFileSystems.contains(QString(storage.fileSystemType()), Qt::CaseInsensitive) && storage.isValid() && storage.isReady()) + { + urls << QUrl::fromLocalFile(storage.rootPath()); + } + } +#endif + setSidebarUrls(urls); } From 3a0e68c0ac93a743cedca379c66de00d13647d69 Mon Sep 17 00:00:00 2001 From: consolegrl <5942029+consolegrl@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:08:06 -0400 Subject: [PATCH 24/51] fixes #6736: Sample tracks lose FX Channel (#6851) * fixes #6736: Sample tracks lose FX Channel * Styling fixes * Restyled remaining blocks of upgrade routine * Fixed hang due to sleepy typo * Update src/core/DataFile.cpp Co-authored-by: saker --------- Co-authored-by: saker --- src/core/DataFile.cpp | 46 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index d237d13aee3..5915b25ef14 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -1704,26 +1704,56 @@ void DataFile::upgrade_mixerRename() { // Change nodename to QDomNodeList fxmixer = elementsByTagName("fxmixer"); - for (int i = 0; !fxmixer.item(i).isNull(); ++i) + for (int i = 0; i < fxmixer.length(); ++i) { - fxmixer.item(i).toElement().setTagName("mixer"); + auto item = fxmixer.item(i).toElement(); + if (item.isNull()) + { + continue; + } + item.setTagName("mixer"); } // Change nodename to QDomNodeList fxchannel = elementsByTagName("fxchannel"); - for (int i = 0; !fxchannel.item(i).isNull(); ++i) + for (int i = 0; i < fxchannel.length(); ++i) { - fxchannel.item(i).toElement().setTagName("mixerchannel"); + auto item = fxchannel.item(i).toElement(); + if (item.isNull()) + { + continue; + } + item.setTagName("mixerchannel"); } // Change the attribute fxch of element to mixch QDomNodeList fxch = elementsByTagName("instrumenttrack"); - for(int i = 0; !fxch.item(i).isNull(); ++i) + for (int i = 0; i < fxch.length(); ++i) + { + auto item = fxch.item(i).toElement(); + if (item.isNull()) + { + continue; + } + if (item.hasAttribute("fxch")) + { + item.setAttribute("mixch", item.attribute("fxch")); + item.removeAttribute("fxch"); + } + } + // Change the attribute fxch of element to mixch + fxch = elementsByTagName("sampletrack"); + for (int i = 0; i < fxch.length(); ++i) { - if(fxch.item(i).toElement().hasAttribute("fxch")) + auto item = fxch.item(i).toElement(); + if (item.isNull()) + { + continue; + } + if (item.hasAttribute("fxch")) { - fxch.item(i).toElement().setAttribute("mixch", fxch.item(i).toElement().attribute("fxch")); - fxch.item(i).toElement().removeAttribute("fxch"); + item.setAttribute("mixch", item.attribute("fxch")); + item.removeAttribute("fxch"); } } } From 9b1933e544697bfa67d337ed12e0501d66a66ec0 Mon Sep 17 00:00:00 2001 From: Maxwell Voorhes <100326501+mvoorhes@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:17:36 -0700 Subject: [PATCH 25/51] Added flats to array of keys (#6865) --- 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 b3833599583..751fb9d2aea 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -130,7 +130,7 @@ QPixmap* PianoRoll::s_toolKnife = nullptr; SimpleTextFloat * PianoRoll::s_textFloat = nullptr; -static std::array s_noteStrings {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; +static std::array s_noteStrings {"C", "C# / D\u266D", "D", "D# / E\u266D", "E", "F", "F# / G\u266D", "G", "G# / A\u266D", "A", "A# / B\u266D", "B"}; static QString getNoteString(int key) { From d3d710e0adac3798ae65f1e2b34fd582e4c02bc6 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Thu, 14 Sep 2023 23:12:22 +0200 Subject: [PATCH 26/51] Remove identical calls to `InstrumentTrack::processAudioBuffer` Remove identical calls to `InstrumentTrack::processAudioBuffer` which appear in the overridden implementations of `Instrument::play` and `Instrument::playNotes`. Instead the call to `processAudioBuffer` has been moved into `InstrumentPlayHandle::play` and `InstrumentTrack::playNote`. These two methods call the aforementioned methods of `Instrument`. Especially in the case of `InstrumentTrack::playNote` the previous implementation resulted in some unncessary "ping pong" where `InstrumentTrack` called a method on an `Instrument` which then in turn called a method on `InstrumentTrack`. And this was done in almost every instrument. In `InstrumentTrack::playNote` an additional check was added which only calls `processAudioBuffer` if the buffer is not `nullptr`. The reason is that under certain circumstances `PlayHandle::doProcessing` calls the `play` method by explicitly passing a `nullptr` as the buffer. This behavior was added with commit 7bc97f5d5bd. Because it is unknown if this was done for some side effects the code was adjusted so that it behaves identical in this case. Move the complex implementation for `InstrumentPlayHandle::play` and `InstrumentPlayHandle::isFromTrack` into the cpp file and optimize the includes. --- include/InstrumentPlayHandle.h | 38 +++--------------- .../AudioFileProcessor/AudioFileProcessor.cpp | 3 -- plugins/BitInvader/BitInvader.cpp | 2 - plugins/CarlaBase/Carla.cpp | 3 -- plugins/FreeBoy/FreeBoy.cpp | 1 - plugins/GigPlayer/GigPlayer.cpp | 2 - plugins/Kicker/Kicker.cpp | 2 - plugins/Lb302/Lb302.cpp | 1 - plugins/Lv2Instrument/Lv2Instrument.cpp | 2 - plugins/Monstro/Monstro.cpp | 2 - plugins/Nes/Nes.cpp | 2 - plugins/OpulenZ/OpulenZ.cpp | 4 -- plugins/Organic/Organic.cpp | 2 - plugins/Patman/Patman.cpp | 2 - plugins/Sf2Player/Sf2Player.cpp | 2 - plugins/Sfxr/Sfxr.cpp | 3 -- plugins/Sid/SidInstrument.cpp | 2 - plugins/Stk/Mallets/Mallets.cpp | 2 - plugins/TripleOscillator/TripleOscillator.cpp | 2 - plugins/Vestige/Vestige.cpp | 2 - plugins/Vibed/Vibed.cpp | 2 - plugins/Watsyn/Watsyn.cpp | 2 - plugins/Xpressive/Xpressive.cpp | 2 - plugins/ZynAddSubFx/ZynAddSubFx.cpp | 1 - src/core/InstrumentPlayHandle.cpp | 39 +++++++++++++++++++ src/tracks/InstrumentTrack.cpp | 7 ++++ 26 files changed, 51 insertions(+), 81 deletions(-) diff --git a/include/InstrumentPlayHandle.h b/include/InstrumentPlayHandle.h index bbf53d16c21..1455134e640 100644 --- a/include/InstrumentPlayHandle.h +++ b/include/InstrumentPlayHandle.h @@ -26,13 +26,14 @@ #define LMMS_INSTRUMENT_PLAY_HANDLE_H #include "PlayHandle.h" -#include "Instrument.h" -#include "NotePlayHandle.h" #include "lmms_export.h" namespace lmms { +class Instrument; +class InstrumentTrack; + class LMMS_EXPORT InstrumentPlayHandle : public PlayHandle { public: @@ -40,46 +41,17 @@ class LMMS_EXPORT InstrumentPlayHandle : public PlayHandle ~InstrumentPlayHandle() override = default; - - void play( sampleFrame * _working_buffer ) override - { - // ensure that all our nph's have been processed first - ConstNotePlayHandleList nphv = NotePlayHandle::nphsOfInstrumentTrack( m_instrument->instrumentTrack(), true ); - - bool nphsLeft; - do - { - nphsLeft = false; - for( const NotePlayHandle * constNotePlayHandle : nphv ) - { - NotePlayHandle * notePlayHandle = const_cast( constNotePlayHandle ); - if( notePlayHandle->state() != ThreadableJob::ProcessingState::Done && - !notePlayHandle->isFinished()) - { - nphsLeft = true; - notePlayHandle->process(); - } - } - } - while( nphsLeft ); - - m_instrument->play( _working_buffer ); - } + void play( sampleFrame * _working_buffer ) override; bool isFinished() const override { return false; } - bool isFromTrack( const Track* _track ) const override - { - return m_instrument->isFromTrack( _track ); - } - + bool isFromTrack( const Track* _track ) const override; private: Instrument* m_instrument; - } ; diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index a941e773f25..6671022070c 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -171,9 +171,6 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, static_cast( m_loopModel.value() ) ) ) { applyRelease( _working_buffer, _n ); - instrumentTrack()->processAudioBuffer( _working_buffer, - frames + offset, _n ); - emit isPlaying( ((handleState *)_n->m_pluginData)->frameIndex() ); } else diff --git a/plugins/BitInvader/BitInvader.cpp b/plugins/BitInvader/BitInvader.cpp index 98ef1e97cf1..4ea73dc7116 100644 --- a/plugins/BitInvader/BitInvader.cpp +++ b/plugins/BitInvader/BitInvader.cpp @@ -307,8 +307,6 @@ void BitInvader::playNote( NotePlayHandle * _n, } applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/CarlaBase/Carla.cpp b/plugins/CarlaBase/Carla.cpp index faff94b570b..819736e928b 100644 --- a/plugins/CarlaBase/Carla.cpp +++ b/plugins/CarlaBase/Carla.cpp @@ -508,7 +508,6 @@ void CarlaInstrument::play(sampleFrame* workingBuffer) if (fHandle == nullptr) { - instrumentTrack()->processAudioBuffer(workingBuffer, bufsize, nullptr); return; } @@ -556,8 +555,6 @@ void CarlaInstrument::play(sampleFrame* workingBuffer) workingBuffer[i][0] = buf1[i]; workingBuffer[i][1] = buf2[i]; } - - instrumentTrack()->processAudioBuffer(workingBuffer, bufsize, nullptr); } bool CarlaInstrument::handleMidiEvent(const MidiEvent& event, const TimePos&, f_cnt_t offset) diff --git a/plugins/FreeBoy/FreeBoy.cpp b/plugins/FreeBoy/FreeBoy.cpp index 6450253ee45..f2dc95699ed 100644 --- a/plugins/FreeBoy/FreeBoy.cpp +++ b/plugins/FreeBoy/FreeBoy.cpp @@ -419,7 +419,6 @@ void FreeBoyInstrument::playNote(NotePlayHandle* nph, sampleFrame* workingBuffer } framesLeft -= count; } - instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, nph); } diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index c2e155a20c5..0713d310038 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -494,8 +494,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) _working_buffer[i][0] *= m_gain.value(); _working_buffer[i][1] *= m_gain.value(); } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); } diff --git a/plugins/Kicker/Kicker.cpp b/plugins/Kicker/Kicker.cpp index ef1d623c1a1..e6418e2da5b 100644 --- a/plugins/Kicker/Kicker.cpp +++ b/plugins/Kicker/Kicker.cpp @@ -197,8 +197,6 @@ void KickerInstrument::playNote( NotePlayHandle * _n, _working_buffer[f+offset][1] *= fac; } } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Lb302/Lb302.cpp b/plugins/Lb302/Lb302.cpp index b8fff2c0b8a..ee49442d5d4 100644 --- a/plugins/Lb302/Lb302.cpp +++ b/plugins/Lb302/Lb302.cpp @@ -790,7 +790,6 @@ void Lb302Synth::play( sampleFrame * _working_buffer ) const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); process( _working_buffer, frames ); - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); // release_frame = 0; //removed for issue # 1432 } diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 1e45f4e919e..32f81d23c25 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -197,8 +197,6 @@ void Lv2Instrument::play(sampleFrame *buf) copyModelsToLmms(); copyBuffersToLmms(buf, fpp); - - instrumentTrack()->processAudioBuffer(buf, fpp, nullptr); } diff --git a/plugins/Monstro/Monstro.cpp b/plugins/Monstro/Monstro.cpp index f588d6b786d..2201e4ed90b 100644 --- a/plugins/Monstro/Monstro.cpp +++ b/plugins/Monstro/Monstro.cpp @@ -1040,8 +1040,6 @@ void MonstroInstrument::playNote( NotePlayHandle * _n, ms->renderOutput( frames, _working_buffer + offset ); //applyRelease( _working_buffer, _n ); // we have our own release - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } void MonstroInstrument::deleteNotePluginData( NotePlayHandle * _n ) diff --git a/plugins/Nes/Nes.cpp b/plugins/Nes/Nes.cpp index a530ac19b3b..47122a0c602 100644 --- a/plugins/Nes/Nes.cpp +++ b/plugins/Nes/Nes.cpp @@ -561,8 +561,6 @@ void NesInstrument::playNote( NotePlayHandle * n, sampleFrame * workingBuffer ) nes->renderOutput( workingBuffer + offset, frames ); applyRelease( workingBuffer, n ); - - instrumentTrack()->processAudioBuffer( workingBuffer, frames + offset, n ); } diff --git a/plugins/OpulenZ/OpulenZ.cpp b/plugins/OpulenZ/OpulenZ.cpp index 64f60999576..d90d5f343a4 100644 --- a/plugins/OpulenZ/OpulenZ.cpp +++ b/plugins/OpulenZ/OpulenZ.cpp @@ -412,10 +412,6 @@ void OpulenzInstrument::play( sampleFrame * _working_buffer ) } } emulatorMutex.unlock(); - - // Throw the data to the track... - instrumentTrack()->processAudioBuffer( _working_buffer, frameCount, nullptr ); - } diff --git a/plugins/Organic/Organic.cpp b/plugins/Organic/Organic.cpp index f8a2b0d135c..a70da642156 100644 --- a/plugins/Organic/Organic.cpp +++ b/plugins/Organic/Organic.cpp @@ -312,8 +312,6 @@ void OrganicInstrument::playNote( NotePlayHandle * _n, } // -- -- - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index a2b829940a4..668c7f461e9 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -157,8 +157,6 @@ void PatmanInstrument::playNote( NotePlayHandle * _n, play_freq, m_loopedModel.value() ? SampleBuffer::LoopMode::On : SampleBuffer::LoopMode::Off ) ) { applyRelease( _working_buffer, _n ); - instrumentTrack()->processAudioBuffer( _working_buffer, - frames + offset, _n ); } else { diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index d46af5e4f6e..79bd4b97686 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -848,7 +848,6 @@ void Sf2Instrument::play( sampleFrame * _working_buffer ) if( m_playingNotes.isEmpty() ) { renderFrames( frames, _working_buffer ); - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); return; } @@ -906,7 +905,6 @@ void Sf2Instrument::play( sampleFrame * _working_buffer ) { renderFrames( frames - currentFrame, _working_buffer + currentFrame ); } - instrumentTrack()->processAudioBuffer( _working_buffer, frames, nullptr ); } diff --git a/plugins/Sfxr/Sfxr.cpp b/plugins/Sfxr/Sfxr.cpp index fc39ea0fa28..e79b8e2adbe 100644 --- a/plugins/Sfxr/Sfxr.cpp +++ b/plugins/Sfxr/Sfxr.cpp @@ -480,9 +480,6 @@ void SfxrInstrument::playNote( NotePlayHandle * _n, sampleFrame * _working_buffe delete[] pitchedBuffer; applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frameNum + offset, _n ); - } diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index f663c3b6977..143003f9803 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -429,8 +429,6 @@ void SidInstrument::playNote( NotePlayHandle * _n, _working_buffer[frame+offset][ch] = s; } } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Stk/Mallets/Mallets.cpp b/plugins/Stk/Mallets/Mallets.cpp index 4fb077de5bc..b746e949120 100644 --- a/plugins/Stk/Mallets/Mallets.cpp +++ b/plugins/Stk/Mallets/Mallets.cpp @@ -359,8 +359,6 @@ void MalletsInstrument::playNote( NotePlayHandle * _n, _working_buffer[frame][1] = ps->nextSampleRight() * ( m_scalers[p] + add_scale ); } - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index f2340d3d609..5b8f6e8ade6 100644 --- a/plugins/TripleOscillator/TripleOscillator.cpp +++ b/plugins/TripleOscillator/TripleOscillator.cpp @@ -380,8 +380,6 @@ void TripleOscillator::playNote( NotePlayHandle * _n, applyFadeIn(_working_buffer, _n); applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index dd8e9cbef3b..aff5b7fdc46 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -409,8 +409,6 @@ void VestigeInstrument::play( sampleFrame * _buf ) m_plugin->process( nullptr, _buf ); - instrumentTrack()->processAudioBuffer( _buf, frames, nullptr ); - m_pluginMutex.unlock(); } diff --git a/plugins/Vibed/Vibed.cpp b/plugins/Vibed/Vibed.cpp index 3ed51fe79d7..ad6a3942af4 100644 --- a/plugins/Vibed/Vibed.cpp +++ b/plugins/Vibed/Vibed.cpp @@ -251,8 +251,6 @@ void Vibed::playNote(NotePlayHandle* n, sampleFrame* workingBuffer) } } } - - instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, n); } void Vibed::deleteNotePluginData(NotePlayHandle* n) diff --git a/plugins/Watsyn/Watsyn.cpp b/plugins/Watsyn/Watsyn.cpp index 7603a9c1be6..8e49942e16a 100644 --- a/plugins/Watsyn/Watsyn.cpp +++ b/plugins/Watsyn/Watsyn.cpp @@ -445,8 +445,6 @@ void WatsynInstrument::playNote( NotePlayHandle * _n, } applyRelease( _working_buffer, _n ); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); } diff --git a/plugins/Xpressive/Xpressive.cpp b/plugins/Xpressive/Xpressive.cpp index b1a17a1ce76..babc372317a 100644 --- a/plugins/Xpressive/Xpressive.cpp +++ b/plugins/Xpressive/Xpressive.cpp @@ -233,8 +233,6 @@ void Xpressive::playNote(NotePlayHandle* nph, sampleFrame* working_buffer) { const f_cnt_t offset = nph->noteOffset(); ps->renderOutput(frames, working_buffer + offset); - - instrumentTrack()->processAudioBuffer(working_buffer, frames + offset, nph); } void Xpressive::deleteNotePluginData(NotePlayHandle* nph) { diff --git a/plugins/ZynAddSubFx/ZynAddSubFx.cpp b/plugins/ZynAddSubFx/ZynAddSubFx.cpp index 2ec86459281..be38bcb7978 100644 --- a/plugins/ZynAddSubFx/ZynAddSubFx.cpp +++ b/plugins/ZynAddSubFx/ZynAddSubFx.cpp @@ -341,7 +341,6 @@ void ZynAddSubFxInstrument::play( sampleFrame * _buf ) m_plugin->processAudio( _buf ); } m_pluginMutex.unlock(); - instrumentTrack()->processAudioBuffer( _buf, Engine::audioEngine()->framesPerPeriod(), nullptr ); } diff --git a/src/core/InstrumentPlayHandle.cpp b/src/core/InstrumentPlayHandle.cpp index e1a9d9d65fd..30f5fde38a1 100644 --- a/src/core/InstrumentPlayHandle.cpp +++ b/src/core/InstrumentPlayHandle.cpp @@ -24,7 +24,10 @@ #include "InstrumentPlayHandle.h" +#include "Instrument.h" #include "InstrumentTrack.h" +#include "Engine.h" +#include "AudioEngine.h" namespace lmms { @@ -37,5 +40,41 @@ InstrumentPlayHandle::InstrumentPlayHandle( Instrument * instrument, InstrumentT setAudioPort( instrumentTrack->audioPort() ); } +void InstrumentPlayHandle::play( sampleFrame * _working_buffer ) +{ + InstrumentTrack * instrumentTrack = m_instrument->instrumentTrack(); + + // ensure that all our nph's have been processed first + ConstNotePlayHandleList nphv = NotePlayHandle::nphsOfInstrumentTrack(instrumentTrack, true ); + + bool nphsLeft; + do + { + nphsLeft = false; + for( const NotePlayHandle * constNotePlayHandle : nphv ) + { + NotePlayHandle * notePlayHandle = const_cast( constNotePlayHandle ); + if( notePlayHandle->state() != ThreadableJob::ProcessingState::Done && + !notePlayHandle->isFinished()) + { + nphsLeft = true; + notePlayHandle->process(); + } + } + } + while( nphsLeft ); + + m_instrument->play( _working_buffer ); + + // Process the audio buffer that the instrument has just worked on... + const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); + instrumentTrack->processAudioBuffer(_working_buffer, frames, nullptr); +} + +bool InstrumentPlayHandle::isFromTrack( const Track* _track ) const +{ + return m_instrument->isFromTrack( _track ); +} + } // namespace lmms \ No newline at end of file diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index a4de188a5d1..c9b87a4f2ed 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -579,6 +579,13 @@ void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) { // all is done, so now lets play the note! m_instrument->playNote( n, workingBuffer ); + + if (workingBuffer != nullptr) + { + const fpp_t frames = n->framesLeftForCurrentPeriod(); + const f_cnt_t offset = n->noteOffset(); + this->processAudioBuffer(workingBuffer, frames + offset, n); + } } } From a6b6565687d658350fe84b448676ace5a1c466c7 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Thu, 14 Sep 2023 23:35:16 +0200 Subject: [PATCH 27/51] Remove unused variable Remove the unused variable `frames` in `VestigeInstrument::play` to fix the stricter GitHub build. --- plugins/Vestige/Vestige.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index aff5b7fdc46..a696a4b2ded 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -399,8 +399,6 @@ void VestigeInstrument::play( sampleFrame * _buf ) { if (!m_pluginMutex.tryLock(Engine::getSong()->isExporting() ? -1 : 0)) {return;} - const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - if( m_plugin == nullptr ) { m_pluginMutex.unlock(); From d25723cead8dbf46e9b4983e90813ab36325f5dc Mon Sep 17 00:00:00 2001 From: saker Date: Fri, 15 Sep 2023 22:25:54 -0400 Subject: [PATCH 28/51] Update display file name when opening ``PatmanView`` (#6870) * Update display file name when loading Patman * Conditionally update file name --- plugins/Patman/Patman.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index a2b829940a4..e96431e3a06 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -446,7 +446,7 @@ namespace gui PatmanView::PatmanView( Instrument * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ), - m_pi( nullptr ) + m_pi(castModel()) { setAutoFillBackground( true ); QPalette pal; @@ -487,7 +487,15 @@ PatmanView::PatmanView( Instrument * _instrument, QWidget * _parent ) : "tune_off" ) ); m_tuneButton->setToolTip(tr("Tune mode")); - m_displayFilename = tr( "No file selected" ); + + if (m_pi->m_patchFile.isEmpty()) + { + m_displayFilename = tr("No file selected"); + } + else + { + updateFilename(); + } setAcceptDrops( true ); } From 73f9f36c9afcf65150ae8b1f47d359f2472d3eae Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 16 Sep 2023 13:35:15 +0200 Subject: [PATCH 29/51] Code review changes Remove whitespace in methods and add it to the parenthesis of some statements. Remove underscores from parameter names. Remove usage of `this` pointer. Add `auto` keyword to shorten line length in `InstrumentPlayHandle::play`. Move `const_cast` of `NotePlayHandle` closer to the `process` method as this method is the reason that the cast is needed in the first place. One might also add a version of `nphsOfInstrumentTrack` that returns a non-const result but it seems to be a general problem of a missing clear responsibility so I keep it as is. --- include/InstrumentPlayHandle.h | 9 ++++----- src/core/InstrumentPlayHandle.cpp | 32 +++++++++++++++---------------- src/tracks/InstrumentTrack.cpp | 2 +- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/include/InstrumentPlayHandle.h b/include/InstrumentPlayHandle.h index 1455134e640..dc744b4ffdb 100644 --- a/include/InstrumentPlayHandle.h +++ b/include/InstrumentPlayHandle.h @@ -37,23 +37,22 @@ class InstrumentTrack; class LMMS_EXPORT InstrumentPlayHandle : public PlayHandle { public: - InstrumentPlayHandle( Instrument * instrument, InstrumentTrack* instrumentTrack ); + InstrumentPlayHandle(Instrument * instrument, InstrumentTrack* instrumentTrack); ~InstrumentPlayHandle() override = default; - void play( sampleFrame * _working_buffer ) override; + void play(sampleFrame * working_buffer) override; bool isFinished() const override { return false; } - bool isFromTrack( const Track* _track ) const override; + bool isFromTrack(const Track* track) const override; private: Instrument* m_instrument; -} ; - +}; } // namespace lmms diff --git a/src/core/InstrumentPlayHandle.cpp b/src/core/InstrumentPlayHandle.cpp index 30f5fde38a1..097719ad83d 100644 --- a/src/core/InstrumentPlayHandle.cpp +++ b/src/core/InstrumentPlayHandle.cpp @@ -33,48 +33,48 @@ namespace lmms { -InstrumentPlayHandle::InstrumentPlayHandle( Instrument * instrument, InstrumentTrack* instrumentTrack ) : - PlayHandle( Type::InstrumentPlayHandle ), - m_instrument( instrument ) +InstrumentPlayHandle::InstrumentPlayHandle(Instrument * instrument, InstrumentTrack* instrumentTrack) : + PlayHandle(Type::InstrumentPlayHandle), + m_instrument(instrument) { - setAudioPort( instrumentTrack->audioPort() ); + setAudioPort(instrumentTrack->audioPort()); } -void InstrumentPlayHandle::play( sampleFrame * _working_buffer ) +void InstrumentPlayHandle::play(sampleFrame * working_buffer) { InstrumentTrack * instrumentTrack = m_instrument->instrumentTrack(); // ensure that all our nph's have been processed first - ConstNotePlayHandleList nphv = NotePlayHandle::nphsOfInstrumentTrack(instrumentTrack, true ); + auto nphv = NotePlayHandle::nphsOfInstrumentTrack(instrumentTrack, true); bool nphsLeft; do { nphsLeft = false; - for( const NotePlayHandle * constNotePlayHandle : nphv ) + for (const NotePlayHandle * constNotePlayHandle : nphv) { - NotePlayHandle * notePlayHandle = const_cast( constNotePlayHandle ); - if( notePlayHandle->state() != ThreadableJob::ProcessingState::Done && - !notePlayHandle->isFinished()) + if (constNotePlayHandle->state() != ThreadableJob::ProcessingState::Done && + !constNotePlayHandle->isFinished()) { nphsLeft = true; + NotePlayHandle * notePlayHandle = const_cast(constNotePlayHandle); notePlayHandle->process(); } } } - while( nphsLeft ); + while (nphsLeft); - m_instrument->play( _working_buffer ); + m_instrument->play(working_buffer); // Process the audio buffer that the instrument has just worked on... const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - instrumentTrack->processAudioBuffer(_working_buffer, frames, nullptr); + instrumentTrack->processAudioBuffer(working_buffer, frames, nullptr); } -bool InstrumentPlayHandle::isFromTrack( const Track* _track ) const +bool InstrumentPlayHandle::isFromTrack(const Track* track) const { - return m_instrument->isFromTrack( _track ); + return m_instrument->isFromTrack(track); } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index c9b87a4f2ed..0ffde61bcea 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -584,7 +584,7 @@ void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) { const fpp_t frames = n->framesLeftForCurrentPeriod(); const f_cnt_t offset = n->noteOffset(); - this->processAudioBuffer(workingBuffer, frames + offset, n); + processAudioBuffer(workingBuffer, frames + offset, n); } } } From 6c3ae30c8937173bd69ec3f12ccd5996189c3342 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 16 Sep 2023 19:19:37 +0200 Subject: [PATCH 30/51] Add comments about nullptr guard Add some comments which describe why we need to guard agains nullptr. --- src/tracks/InstrumentTrack.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 0ffde61bcea..5dee1f3a537 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -570,6 +570,10 @@ f_cnt_t InstrumentTrack::beatLen( NotePlayHandle * _n ) const void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) { + // Note: under certain circumstances the working buffer is a nullptr. + // These cases are triggered in PlayHandle::doProcessing when the play method is called with a nullptr. + // TODO: Find out if we can skip processing at a higher level if the buffer is nullptr. + // arpeggio- and chord-widget has to do its work -> adding sub-notes // for chords/arpeggios m_noteStacking.processNote( n ); @@ -580,6 +584,8 @@ void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) // all is done, so now lets play the note! m_instrument->playNote( n, workingBuffer ); + // Calling processAudioBuffer with a nullptr leads to crashes when checking if the buffer represents silence. + // Therefore we must guard against a nullptr here. if (workingBuffer != nullptr) { const fpp_t frames = n->framesLeftForCurrentPeriod(); From 4348038b0ff2f334f85a37a1caf124213f6d3439 Mon Sep 17 00:00:00 2001 From: Austin <61636545+aakers24@users.noreply.github.com> Date: Sun, 17 Sep 2023 08:34:42 -0700 Subject: [PATCH 31/51] Added automatic dark note on light background for midi clips. (#6539) --- src/gui/clips/MidiClipView.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 79c4cd73d68..151df8d3c3c 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -523,7 +523,8 @@ void MidiClipView::paintEvent( QPaintEvent * ) p.scale(width(), height() - distanceToTop - 2 * notesBorder); // set colour based on mute status - QColor noteFillColor = muted ? getMutedNoteFillColor() : getNoteFillColor(); + QColor noteFillColor = muted ? getMutedNoteFillColor().lighter(200) + : (c.lightness() > 175 ? getNoteFillColor().darker(400) : getNoteFillColor()); QColor noteBorderColor = muted ? getMutedNoteBorderColor() : ( m_clip->hasColor() ? c.lighter( 200 ) : getNoteBorderColor() ); From e9125d3ad3c8d6da568ea64641896a0f71671534 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 17 Sep 2023 17:55:04 +0200 Subject: [PATCH 32/51] Fix LcdFloatSpinBox constructor and label alignment (#6686) --- src/gui/widgets/LcdFloatSpinBox.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gui/widgets/LcdFloatSpinBox.cpp b/src/gui/widgets/LcdFloatSpinBox.cpp index 6391f314ad9..96f2b27e1db 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -49,6 +49,7 @@ namespace lmms::gui LcdFloatSpinBox::LcdFloatSpinBox(int numWhole, int numFrac, const QString& name, QWidget* parent) : + QWidget(parent), FloatModelView(new FloatModel(0, 0, 0, 0, nullptr, name, true), this), m_wholeDisplay(numWhole, parent, name, false), m_fractionDisplay(numFrac, parent, name, true), @@ -62,6 +63,7 @@ LcdFloatSpinBox::LcdFloatSpinBox(int numWhole, int numFrac, const QString& name, LcdFloatSpinBox::LcdFloatSpinBox(int numWhole, int numFrac, const QString& style, const QString& name, QWidget* parent) : + QWidget(parent), FloatModelView(new FloatModel(0, 0, 0, 0, nullptr, name, true), this), m_wholeDisplay(numWhole, style, parent, name, false), m_fractionDisplay(numFrac, style, parent, name, true), @@ -101,6 +103,7 @@ void LcdFloatSpinBox::layoutSetup(const QString &style) outerLayout->setContentsMargins(0, 0, 0, 0); outerLayout->setSizeConstraint(QLayout::SetFixedSize); this->setLayout(outerLayout); + this->setFixedHeight(32); } @@ -240,9 +243,9 @@ void LcdFloatSpinBox::paintEvent(QPaintEvent*) { p.setFont(pointSizeF(p.font(), 6.5)); p.setPen(m_wholeDisplay.textShadowColor()); - p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2 + 1, height(), m_label); + p.drawText(width() / 2 - p.fontMetrics().boundingRect(m_label).width() / 2 + 1, height(), m_label); p.setPen(m_wholeDisplay.textColor()); - p.drawText(width() / 2 - horizontalAdvance(p.fontMetrics(), m_label) / 2, height() - 1, m_label); + p.drawText(width() / 2 - p.fontMetrics().boundingRect(m_label).width() / 2, height() - 1, m_label); } } From 733e0a1e542dec42325d1110688848d4fecf4f56 Mon Sep 17 00:00:00 2001 From: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:14:08 +0530 Subject: [PATCH 33/51] Fix invalid use of iterators in piano roll (#6869) --- src/gui/editors/PianoRoll.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 751fb9d2aea..1da6aa3df75 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -1719,10 +1719,10 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) const NoteVector & notes = m_midiClip->notes(); // will be our iterator in the following loop - auto it = notes.begin() + notes.size() - 1; + auto it = notes.rbegin(); // loop through whole note-vector... - for( int i = 0; i < notes.size(); ++i ) + while (it != notes.rend()) { Note *note = *it; TimePos len = note->length(); @@ -1747,7 +1747,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) { break; } - --it; + ++it; } // first check whether the user clicked in note-edit- @@ -1769,7 +1769,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) Note * created_new_note = nullptr; // did it reach end of vector because // there's no note?? - if( it == notes.begin()-1 ) + if (it == notes.rend()) { is_new_note = true; m_midiClip->addJournalCheckPoint(); @@ -1816,8 +1816,8 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) // reset it so that it can be used for // ops (move, resize) after this // code-block - it = notes.begin(); - while( it != notes.end() && *it != created_new_note ) + it = notes.rbegin(); + while (it != notes.rend() && *it != created_new_note) { ++it; } @@ -1933,7 +1933,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) { // erase single note m_mouseDownRight = true; - if( it != notes.begin()-1 ) + if (it != notes.rend()) { m_midiClip->addJournalCheckPoint(); m_midiClip->removeNote( *it ); @@ -2513,7 +2513,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) bool altPressed = me->modifiers() & Qt::AltModifier; // We iterate from last note in MIDI clip to the first, // chronologically - auto it = notes.begin() + notes.size() - 1; + auto it = notes.rbegin(); for( int i = 0; i < notes.size(); ++i ) { Note* n = *it; @@ -2556,7 +2556,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) } - --it; + ++it; } // Emit MIDI clip has changed @@ -2575,10 +2575,10 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) const NoteVector & notes = m_midiClip->notes(); // will be our iterator in the following loop - auto it = notes.begin() + notes.size() - 1; + auto it = notes.rbegin(); // loop through whole note-vector... - for( int i = 0; i < notes.size(); ++i ) + while (it != notes.rend()) { Note *note = *it; // and check whether the cursor is over an @@ -2591,12 +2591,12 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) { break; } - --it; + ++it; } // did it reach end of vector because there's // no note?? - if( it != notes.begin()-1 ) + if (it != notes.rend()) { Note *note = *it; // x coordinate of the right edge of the note From dc88040fbe59222620535a347fc209a020133c71 Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Tue, 19 Sep 2023 00:18:28 +0100 Subject: [PATCH 34/51] Add instrument play handles unconditionally (#6875) --- src/core/AudioEngine.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/AudioEngine.cpp b/src/core/AudioEngine.cpp index 2d077babcfd..29c54647cf7 100644 --- a/src/core/AudioEngine.cpp +++ b/src/core/AudioEngine.cpp @@ -701,7 +701,10 @@ void AudioEngine::removeAudioPort(AudioPort * port) bool AudioEngine::addPlayHandle( PlayHandle* handle ) { - if( criticalXRuns() == false ) + // Only add play handles if we have the CPU capacity to process them. + // Instrument play handles are not added during playback, but when the + // associated instrument is created, so add those unconditionally. + if (handle->type() == PlayHandle::Type::InstrumentPlayHandle || !criticalXRuns()) { m_newPlayHandles.push( handle ); handle->audioPort()->addPlayHandle( handle ); From 94d0b9111b24687e44f65cdf1e5c691e249ee50d Mon Sep 17 00:00:00 2001 From: Dominic Clark Date: Wed, 20 Sep 2023 00:56:55 +0100 Subject: [PATCH 35/51] Use UTF-8 for MSVC source and execution charset (#6876) --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b17d0e7c98..c1cf3406ba4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -651,6 +651,9 @@ IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") ELSE(WIN32) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -DPIC") ENDIF(WIN32) +elseif(MSVC) + # Use UTF-8 as the source and execution character set + add_compile_options("/utf-8") ENDIF() # add enabled sanitizers From 7e8c79a191e2dbb8ac5a2a85fd758b0782d37662 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Wed, 20 Sep 2023 19:52:59 +0200 Subject: [PATCH 36/51] Enable build under NixOS (#6855) Explicitly call the perl interpreter when building the SWH LADSPA plugins --- CMakeLists.txt | 20 ++++++++++++++++++-- plugins/LadspaEffect/swh/CMakeLists.txt | 3 ++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1cf3406ba4..7c3770f318e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,13 @@ 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) @@ -266,8 +273,17 @@ ELSE(WANT_CMT) ENDIF(WANT_CMT) IF(WANT_SWH) - SET(LMMS_HAVE_SWH TRUE) - SET(STATUS_SWH "OK") + IF(LMMS_BUILD_APPLE) + # Prefer system perl over Homebrew, MacPorts, etc + SET(Perl_ROOT "/usr/bin") + ENDIF() + FIND_PACKAGE(Perl) + IF(PERL_FOUND) + SET(LMMS_HAVE_SWH TRUE) + SET(STATUS_SWH "OK") + ELSE() + SET(STATUS_SWH "Skipping, perl is missing") + ENDIF() ELSE(WANT_SWH) SET(STATUS_SWH "not built as requested") ENDIF(WANT_SWH) diff --git a/plugins/LadspaEffect/swh/CMakeLists.txt b/plugins/LadspaEffect/swh/CMakeLists.txt index aec01c22f8e..a8300117735 100644 --- a/plugins/LadspaEffect/swh/CMakeLists.txt +++ b/plugins/LadspaEffect/swh/CMakeLists.txt @@ -16,6 +16,7 @@ SET(COMPILE_FLAGS "${COMPILE_FLAGS} ${PIC_FLAGS}") # Loop over every XML file FILE(GLOB XML_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/ladspa/*.xml") LIST(SORT XML_SOURCES) + FOREACH(_item ${XML_SOURCES}) # Get library name and (soon to be) C file GET_FILENAME_COMPONENT(_plugin "${_item}" NAME_WE) @@ -24,7 +25,7 @@ FOREACH(_item ${XML_SOURCES}) # Coerce XML source file to C ADD_CUSTOM_COMMAND( OUTPUT "${_out_file}" - COMMAND ./makestub.pl "${_item}" > "${_out_file}" + COMMAND "${PERL_EXECUTABLE}" ./makestub.pl "${_item}" > "${_out_file}" DEPENDS "${_item}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/ladspa" VERBATIM From 3df28e30b68db4d696b50315577bf7a04cd391f7 Mon Sep 17 00:00:00 2001 From: Maxwell Voorhes <100326501+mvoorhes@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:32:21 -0700 Subject: [PATCH 37/51] Changed sharps to music sharps unicode (#6873) --- src/gui/editors/PianoRoll.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 1da6aa3df75..cef2205d266 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -130,7 +130,10 @@ QPixmap* PianoRoll::s_toolKnife = nullptr; SimpleTextFloat * PianoRoll::s_textFloat = nullptr; -static std::array s_noteStrings {"C", "C# / D\u266D", "D", "D# / E\u266D", "E", "F", "F# / G\u266D", "G", "G# / A\u266D", "A", "A# / B\u266D", "B"}; +static std::array s_noteStrings { + "C", "C\u266F / D\u266D", "D", "D\u266F / E\u266D", "E", "F", "F\u266F / G\u266D", + "G", "G\u266F / A\u266D", "A", "A\u266F / B\u266D", "B" +}; static QString getNoteString(int key) { From b6c80bd7366a789cab3f5a6a017636faf80a95ee Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Thu, 21 Sep 2023 19:24:06 +0200 Subject: [PATCH 38/51] Replace check for nullptr Replace the check for a `nullptr` buffer with a check that checks if the NotePlayHandle uses a buffer. This is effectively the same condition that's used by `PlayHandle::doProcessing` to decide if it should call play with a `nullptr` in the first place. Hence it should be the most fitting check. --- src/tracks/InstrumentTrack.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 5dee1f3a537..29fda075e9c 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -584,9 +584,9 @@ void InstrumentTrack::playNote( NotePlayHandle* n, sampleFrame* workingBuffer ) // all is done, so now lets play the note! m_instrument->playNote( n, workingBuffer ); - // Calling processAudioBuffer with a nullptr leads to crashes when checking if the buffer represents silence. - // Therefore we must guard against a nullptr here. - if (workingBuffer != nullptr) + // This is effectively the same as checking if workingBuffer is not a nullptr. + // Calling processAudioBuffer with a nullptr leads to crashes. Hence the check. + if (n->usesBuffer()) { const fpp_t frames = n->framesLeftForCurrentPeriod(); const f_cnt_t offset = n->noteOffset(); From 07229b64048c118bd8a25904c2cbc1a603fad838 Mon Sep 17 00:00:00 2001 From: Alexandre Almeida Date: Thu, 21 Sep 2023 20:18:23 -0300 Subject: [PATCH 39/51] Show error message when loading an invalid sample file (#6286) --- include/SampleClip.h | 2 +- src/core/SampleBuffer.cpp | 62 +++++++++++++++++++++++++++++++-------- src/core/SampleClip.cpp | 27 +++++++++-------- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/include/SampleClip.h b/include/SampleClip.h index c9e247328ca..5246787bdc6 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -77,7 +77,7 @@ class SampleClip : public Clip public slots: void setSampleBuffer( lmms::SampleBuffer* sb ); - void setSampleFile( const QString & _sf ); + void setSampleFile( const QString & sf ); void updateLength(); void toggleRecord(); void playbackPositionChanged(); diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 775db125b75..5e2d09c573e 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -247,7 +247,15 @@ void SampleBuffer::update(bool keepSettings) const int fileSizeMax = 300; // MB const int sampleLengthMax = 90; // Minutes - bool fileLoadError = false; + 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 @@ -271,31 +279,40 @@ void SampleBuffer::update(bool keepSettings) m_frames = 0; const QFileInfo fileInfo(file); - if (fileInfo.size() > fileSizeMax * 1024 * 1024) + if (!fileInfo.isReadable()) { - fileLoadError = true; + 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; + 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 = true; + fileLoadError = FileLoadError::TooLarge; } sf_close(sndFile); } + else + { + fileLoadError = FileLoadError::Invalid; + } f.close(); } - if (!fileLoadError) + if (fileLoadError == FileLoadError::None) { #ifdef LMMS_HAVE_OGGVORBIS // workaround for a bug in libsndfile or our libsndfile decoder @@ -322,7 +339,7 @@ void SampleBuffer::update(bool keepSettings) } } - if (m_frames == 0 || fileLoadError) // if still no frames, bail + if (m_frames == 0 || fileLoadError != FileLoadError::None) // if still no frames, bail { // sample couldn't be decoded, create buffer containing // one sample-frame @@ -363,16 +380,35 @@ void SampleBuffer::update(bool keepSettings) } Oscillator::generateAntiAliasUserWaveTable(this); - if (fileLoadError) + if (fileLoadError != FileLoadError::None) { QString title = tr("Fail to open file"); - QString message = tr("Audio files are limited to %1 MB " - "in size and %2 minutes of playing time" - ).arg(fileSizeMax).arg(sampleLengthMax); + 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); + QMessageBox::information(nullptr, title, message, QMessageBox::Ok); } else { diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 592a6382741..b09d7b3bb2b 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -143,23 +143,26 @@ void SampleClip::setSampleBuffer( SampleBuffer* sb ) -void SampleClip::setSampleFile( const QString & _sf ) +void SampleClip::setSampleFile(const QString & sf) { - int length; - if ( _sf.isEmpty() ) - { //When creating an empty sample clip make it a bar long + int length = 0; + + if (!sf.isEmpty()) + { + m_sampleBuffer->setAudioFile(sf); + length = sampleLength(); + } + + if (length == 0) + { + //If there is no sample, make the clip a bar long float nom = Engine::getSong()->getTimeSigModel().getNumerator(); float den = Engine::getSong()->getTimeSigModel().getDenominator(); - length = DefaultTicksPerBar * ( nom / den ); + length = DefaultTicksPerBar * (nom / den); } - else - { //Otherwise set it to the sample's length - m_sampleBuffer->setAudioFile( _sf ); - length = sampleLength(); - } - changeLength(length); - setStartTimeOffset( 0 ); + changeLength(length); + setStartTimeOffset(0); emit sampleChanged(); emit playbackPositionChanged(); From 93d3e8c82c4b2968c5949dde0fb8bb4a952bb54f Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Fri, 22 Sep 2023 18:16:45 +0200 Subject: [PATCH 40/51] Adjust the classic theme Adjust the classic theme so that it is consistent with the changes made to the default theme. It was only necessary to change the font-size of the check boxes in the file browser to points because the QTreeView already does not contain any font settings. --- data/themes/classic/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 2880fe661fe..4c266e0647b 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -132,7 +132,7 @@ QMenu::indicator:selected { lmms--gui--FileBrowser QCheckBox { - font-size: 10px; + font-size: 8pt; color: white; } From f0aa2862d7efe06a483f6056ba20de315d065597 Mon Sep 17 00:00:00 2001 From: Artur-Twardowski-Mobica <139768112+Artur-Twardowski-Mobica@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:07:25 +0200 Subject: [PATCH 41/51] Updated MIDI CC handling not to count from 1 (#6774) --- include/DataFile.h | 1 + include/MidiController.h | 1 + src/core/DataFile.cpp | 23 ++++++++++++++++++- src/core/midi/MidiController.cpp | 11 ++++----- src/core/midi/MidiPort.cpp | 7 +++--- src/gui/modals/ControllerConnectionDialog.cpp | 6 ++--- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/include/DataFile.h b/include/DataFile.h index c19a00f7f30..dc82315adb7 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -127,6 +127,7 @@ class LMMS_EXPORT DataFile : public QDomDocument void upgrade_mixerRename(); void upgrade_bbTcoRename(); void upgrade_sampleAndHold(); + void upgrade_midiCCIndexing(); // List of all upgrade methods static const std::vector UPGRADE_METHODS; diff --git a/include/MidiController.h b/include/MidiController.h index c4ef49590a6..9f49627acc6 100644 --- a/include/MidiController.h +++ b/include/MidiController.h @@ -48,6 +48,7 @@ class MidiController : public Controller, public MidiEventProcessor { Q_OBJECT public: + static constexpr int NONE = -1; MidiController( Model * _parent ); ~MidiController() override = default; diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 5915b25ef14..8d0a8dca43f 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -79,7 +79,7 @@ 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_sampleAndHold , &DataFile::upgrade_midiCCIndexing }; // Vector of all versions that have upgrade routines. @@ -1809,6 +1809,27 @@ void DataFile::upgrade_sampleAndHold() } } +//! Update MIDI CC indexes, so that they are counted from 0. Older releases of LMMS +//! count the CCs from 1. +void DataFile::upgrade_midiCCIndexing() +{ + static constexpr std::array attributesToUpdate{"inputcontroller", "outputcontroller"}; + + QDomNodeList elements = elementsByTagName("Midicontroller"); + for(int i = 0; i < elements.length(); i++) + { + if (elements.item(i).isNull()) { continue; } + auto element = elements.item(i).toElement(); + for (const char* attrName : attributesToUpdate) + { + if (element.hasAttribute(attrName)) + { + int cc = element.attribute(attrName).toInt(); + element.setAttribute(attrName, cc - 1); + } + } + } +} void DataFile::upgrade() { diff --git a/src/core/midi/MidiController.cpp b/src/core/midi/MidiController.cpp index d7c89e940c2..0ae76d352c8 100644 --- a/src/core/midi/MidiController.cpp +++ b/src/core/midi/MidiController.cpp @@ -72,21 +72,20 @@ void MidiController::updateName() -void MidiController::processInEvent( const MidiEvent& event, const TimePos& time, f_cnt_t offset ) +void MidiController::processInEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) { unsigned char controllerNum; - switch( event.type() ) + switch(event.type()) { case MidiControlChange: controllerNum = event.controllerNumber(); - if( m_midiPort.inputController() == controllerNum + 1 && - ( m_midiPort.inputChannel() == event.channel() + 1 || - m_midiPort.inputChannel() == 0 ) ) + if (m_midiPort.inputController() == controllerNum && + (m_midiPort.inputChannel() == event.channel() + 1 || m_midiPort.inputChannel() == 0)) { unsigned char val = event.controllerValue(); m_previousValue = m_lastValue; - m_lastValue = (float)( val ) / 127.0f; + m_lastValue = static_cast(val) / 127.0f; emit valueChanged(); } break; diff --git a/src/core/midi/MidiPort.cpp b/src/core/midi/MidiPort.cpp index c7c947e8e4f..24263f91315 100644 --- a/src/core/midi/MidiPort.cpp +++ b/src/core/midi/MidiPort.cpp @@ -31,6 +31,7 @@ #include "MidiEventProcessor.h" #include "Note.h" #include "Song.h" +#include "MidiController.h" namespace lmms @@ -54,8 +55,8 @@ MidiPort::MidiPort( const QString& name, m_mode( mode ), m_inputChannelModel( 0, 0, MidiChannelCount, this, tr( "Input channel" ) ), m_outputChannelModel( 1, 0, MidiChannelCount, this, tr( "Output channel" ) ), - m_inputControllerModel( 0, 0, MidiControllerCount, this, tr( "Input controller" ) ), - m_outputControllerModel( 0, 0, MidiControllerCount, this, tr( "Output controller" ) ), + m_inputControllerModel(MidiController::NONE, MidiController::NONE, MidiControllerCount - 1, this, tr( "Input controller" )), + m_outputControllerModel(MidiController::NONE, MidiController::NONE, MidiControllerCount - 1, this, tr( "Output controller" )), m_fixedInputVelocityModel( -1, -1, MidiMaxVelocity, this, tr( "Fixed input velocity" ) ), m_fixedOutputVelocityModel( -1, -1, MidiMaxVelocity, this, tr( "Fixed output velocity" ) ), m_fixedOutputNoteModel( -1, -1, MidiMaxKey, this, tr( "Fixed output note" ) ), @@ -436,4 +437,4 @@ void MidiPort::invalidateCilent() } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/src/gui/modals/ControllerConnectionDialog.cpp b/src/gui/modals/ControllerConnectionDialog.cpp index 79daa25b565..4d1090d5c88 100644 --- a/src/gui/modals/ControllerConnectionDialog.cpp +++ b/src/gui/modals/ControllerConnectionDialog.cpp @@ -54,7 +54,7 @@ class AutoDetectMidiController : public MidiController AutoDetectMidiController( Model* parent ) : MidiController( parent ), m_detectedMidiChannel( 0 ), - m_detectedMidiController( 0 ) + m_detectedMidiController(NONE) { updateName(); } @@ -69,7 +69,7 @@ class AutoDetectMidiController : public MidiController ( m_midiPort.inputChannel() == 0 || m_midiPort.inputChannel() == event.channel() + 1 ) ) { m_detectedMidiChannel = event.channel() + 1; - m_detectedMidiController = event.controllerNumber() + 1; + m_detectedMidiController = event.controllerNumber(); m_detectedMidiPort = Engine::audioEngine()->midiClient()->sourcePortName( event ); emit valueChanged(); @@ -152,7 +152,7 @@ ControllerConnectionDialog::ControllerConnectionDialog( QWidget * _parent, m_midiControllerSpinBox = new LcdSpinBox( 3, m_midiGroupBox, tr( "Input controller" ) ); - m_midiControllerSpinBox->addTextForValue( 0, "---" ); + m_midiControllerSpinBox->addTextForValue(MidiController::NONE, "---" ); m_midiControllerSpinBox->setLabel( tr( "CONTROLLER" ) ); m_midiControllerSpinBox->move( 68, 24 ); From 49c713df5bad0c7285dd04d236ea3f5e9f3c34ef Mon Sep 17 00:00:00 2001 From: saker Date: Sat, 23 Sep 2023 19:33:20 -0400 Subject: [PATCH 42/51] Use resid submodule from libsidplayfp (#6883) --- .gitmodules | 6 +-- CMakeLists.txt | 24 ++++++++--- plugins/Sid/CMakeLists.txt | 47 +++------------------- plugins/Sid/SidInstrument.cpp | 24 +++++------ plugins/Sid/resid | 1 - plugins/Sid/resid/CMakeLists.txt | 68 ++++++++++++++++++++++++++++++++ plugins/Sid/resid/resid | 1 + 7 files changed, 108 insertions(+), 63 deletions(-) delete mode 160000 plugins/Sid/resid create mode 100644 plugins/Sid/resid/CMakeLists.txt create mode 160000 plugins/Sid/resid/resid diff --git a/.gitmodules b/.gitmodules index ee6e7eac9d8..fa6980ac5f8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,9 +40,9 @@ [submodule "plugins/CarlaBase/carla"] path = plugins/CarlaBase/carla url = https://github.com/falktx/carla -[submodule "plugins/Sid/resid"] - path = plugins/Sid/resid - url = https://github.com/simonowen/resid +[submodule "plugins/Sid/resid/resid"] + path = plugins/Sid/resid/resid + url = https://github.com/libsidplayfp/resid [submodule "src/3rdparty/jack2"] path = src/3rdparty/jack2 url = https://github.com/jackaudio/jack2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c3770f318e..4163ca5ccab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ OPTION(WANT_SOUNDIO "Include libsoundio support" ON) OPTION(WANT_SDL "Include SDL (Simple DirectMedia Layer) support" ON) OPTION(WANT_SF2 "Include SoundFont2 player plugin" ON) OPTION(WANT_GIG "Include GIG player plugin" ON) +option(WANT_SID "Include Sid instrument" ON) 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) @@ -211,6 +212,13 @@ CHECK_CXX_SOURCE_COMPILES( LMMS_HAVE_SF_COMPLEVEL ) +# check for perl +if(LMMS_BUILD_APPLE) + # Prefer system perl over Homebrew, MacPorts, etc + set(Perl_ROOT "/usr/bin") +endif() +find_package(Perl) + IF(WANT_LV2) IF(PKG_CONFIG_FOUND) PKG_CHECK_MODULES(LV2 lv2) @@ -273,11 +281,6 @@ ELSE(WANT_CMT) ENDIF(WANT_CMT) IF(WANT_SWH) - IF(LMMS_BUILD_APPLE) - # Prefer system perl over Homebrew, MacPorts, etc - SET(Perl_ROOT "/usr/bin") - ENDIF() - FIND_PACKAGE(Perl) IF(PERL_FOUND) SET(LMMS_HAVE_SWH TRUE) SET(STATUS_SWH "OK") @@ -349,6 +352,16 @@ IF(WANT_SDL AND NOT LMMS_HAVE_SDL2) ENDIF() ENDIF() +# check for Sid +if(WANT_SID) + if(PERL_FOUND) + set(LMMS_HAVE_SID TRUE) + set(STATUS_SID "OK") + else() + set(STATUS_SID "not found, please install perl if you require the Sid instrument") + endif() +endif() + # check for Stk IF(WANT_STK) FIND_PACKAGE(STK) @@ -816,6 +829,7 @@ MESSAGE( "* ZynAddSubFX instrument : ${STATUS_ZYN}\n" "* Carla Patchbay & Rack : ${STATUS_CARLA}\n" "* 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" diff --git a/plugins/Sid/CMakeLists.txt b/plugins/Sid/CMakeLists.txt index c9fce7bb77d..c771fc66de6 100644 --- a/plugins/Sid/CMakeLists.txt +++ b/plugins/Sid/CMakeLists.txt @@ -1,51 +1,14 @@ INCLUDE(BuildPlugin) -INCLUDE_DIRECTORIES(resid) +if(NOT LMMS_HAVE_SID) + return() +endif() BUILD_PLUGIN(sid SidInstrument.cpp SidInstrument.h - resid/envelope.h - resid/extfilt.h - resid/filter.h - resid/pot.h - resid/siddefs.h - resid/sid.h - resid/spline.h - resid/voice.h - resid/wave.h - resid/envelope.cc - resid/extfilt.cc - resid/filter.cc - resid/pot.cc - resid/sid.cc - resid/version.cc - resid/voice.cc - resid/wave6581_PS_.cc - resid/wave6581_PST.cc - resid/wave6581_P_T.cc - resid/wave6581__ST.cc - resid/wave8580_PS_.cc - resid/wave8580_PST.cc - resid/wave8580_P_T.cc - resid/wave8580__ST.cc - resid/wave.cc MOCFILES SidInstrument.h EMBEDDED_RESOURCES *.png) -# Parse VERSION -FILE(READ "resid/CMakeLists.txt" lines) -STRING(REGEX MATCH "set\\(MAJOR_VER [A-Za-z0-9_]*\\)" MAJOR_RAW ${lines}) -STRING(REGEX MATCH "set\\(MINOR_VER [A-Za-z0-9_]*\\)" MINOR_RAW ${lines}) -STRING(REGEX MATCH "set\\(PATCH_VER [A-Za-z0-9_]*\\)" PATCH_RAW ${lines}) -SEPARATE_ARGUMENTS(MAJOR_RAW) -SEPARATE_ARGUMENTS(MINOR_RAW) -SEPARATE_ARGUMENTS(PATCH_RAW) -LIST(GET MAJOR_RAW 1 MAJOR_RAW) -LIST(GET MINOR_RAW 1 MINOR_RAW) -LIST(GET PATCH_RAW 1 PATCH_RAW) -STRING(REPLACE ")" "" MAJOR_VER "${MAJOR_RAW}") -STRING(REPLACE ")" "" MINOR_VER "${MINOR_RAW}") -STRING(REPLACE ")" "" PATCH_VER "${PATCH_RAW}") - -TARGET_COMPILE_DEFINITIONS(sid PRIVATE VERSION="${MAJOR_VER}.${MINOR_VER}.${PATCH_VER}") +add_subdirectory(resid) +target_link_libraries(sid resid) diff --git a/plugins/Sid/SidInstrument.cpp b/plugins/Sid/SidInstrument.cpp index 143003f9803..7f9edf13f18 100644 --- a/plugins/Sid/SidInstrument.cpp +++ b/plugins/Sid/SidInstrument.cpp @@ -239,7 +239,7 @@ f_cnt_t SidInstrument::desiredReleaseFrames() const -static int sid_fillbuffer(unsigned char* sidreg, SID *sid, int tdelta, short *ptr, int samples) +static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, short *ptr, int samples) { int tdelta2; int result; @@ -302,9 +302,9 @@ void SidInstrument::playNote( NotePlayHandle * _n, if (!_n->m_pluginData) { - SID *sid = new SID(); - sid->set_sampling_parameters( clockrate, SAMPLE_FAST, samplerate ); - sid->set_chip_model( MOS8580 ); + auto sid = new reSID::SID(); + sid->set_sampling_parameters(clockrate, reSID::SAMPLE_FAST, samplerate); + sid->set_chip_model(reSID::MOS8580); sid->enable_filter( true ); sid->reset(); _n->m_pluginData = sid; @@ -312,7 +312,7 @@ void SidInstrument::playNote( NotePlayHandle * _n, const fpp_t frames = _n->framesLeftForCurrentPeriod(); const f_cnt_t offset = _n->noteOffset(); - SID *sid = static_cast( _n->m_pluginData ); + auto sid = static_cast(_n->m_pluginData); int delta_t = clockrate * frames / samplerate + 4; // avoid variable length array for msvc compat auto buf = reinterpret_cast(_working_buffer + offset); @@ -325,20 +325,20 @@ void SidInstrument::playNote( NotePlayHandle * _n, if( (ChipModel)m_chipModel.value() == ChipModel::MOS6581 ) { - sid->set_chip_model( MOS6581 ); + sid->set_chip_model(reSID::MOS6581); } else { - sid->set_chip_model( MOS8580 ); + sid->set_chip_model(reSID::MOS8580); } // voices - reg8 data8 = 0; - reg8 data16 = 0; - reg8 base = 0; + reSID::reg8 data8 = 0; + reSID::reg16 data16 = 0; + size_t base = 0; float freq = 0.0; float note = 0.0; - for( reg8 i = 0 ; i < 3 ; ++i ) + for (size_t i = 0; i < 3; ++i) { base = i*7; // freq ( Fn = Fout / Fclk * 16777216 ) + coarse detuning @@ -436,7 +436,7 @@ void SidInstrument::playNote( NotePlayHandle * _n, void SidInstrument::deleteNotePluginData( NotePlayHandle * _n ) { - delete static_cast( _n->m_pluginData ); + delete static_cast(_n->m_pluginData); } diff --git a/plugins/Sid/resid b/plugins/Sid/resid deleted file mode 160000 index 02afcc5cefa..00000000000 --- a/plugins/Sid/resid +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 02afcc5cefac34bd0c665dc0fa6b748d238c1831 diff --git a/plugins/Sid/resid/CMakeLists.txt b/plugins/Sid/resid/CMakeLists.txt new file mode 100644 index 00000000000..bb39e3d166f --- /dev/null +++ b/plugins/Sid/resid/CMakeLists.txt @@ -0,0 +1,68 @@ +# These are the defaults +set(RESID_INLINING 1) +set(RESID_INLINE inline) +set(RESID_BRANCH_HINTS 1) +set(NEW_8580_FILTER 0) + +set(HAVE_BOOL 1) +set(HAVE_LOG1P 1) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GCC|Clang") + set(HAVE_BUILTIN_EXPECT 1) +else() + set(HAVE_BUILTIN_EXPECT 0) +endif() + +configure_file(resid/siddefs.h.in resid/siddefs.h @ONLY) + +add_library(resid_objects OBJECT + resid/sid.cc + resid/voice.cc + resid/wave.cc + resid/envelope.cc + resid/filter.cc + resid/dac.cc + resid/extfilt.cc + resid/pot.cc + resid/version.cc +) + +target_include_directories(resid_objects PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/resid" + "${CMAKE_CURRENT_BINARY_DIR}/resid" +) +target_compile_definitions(resid_objects PUBLIC VERSION="1.0") + +set(RESID_WAVES + wave6581_PST + wave6581_PS_ + wave6581_P_T + wave6581__ST + wave8580_PST + wave8580_PS_ + wave8580_P_T + wave8580__ST +) + +set(RESID_SAMP2SRC_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/resid/samp2src.pl) +foreach(WAVE_DATA IN LISTS RESID_WAVES) + set(WAVE_DATA_IN ${CMAKE_CURRENT_SOURCE_DIR}/resid/${WAVE_DATA}.dat) + set(WAVE_SRC_OUT ${CMAKE_CURRENT_BINARY_DIR}/resid/${WAVE_DATA}.h) + set(WAVE_COMMAND + "${PERL_EXECUTABLE}" + "${RESID_SAMP2SRC_SCRIPT}" + "${WAVE_DATA}" + "${WAVE_DATA_IN}" + "${WAVE_SRC_OUT}" + ) + add_custom_command(OUTPUT ${WAVE_SRC_OUT} COMMAND ${WAVE_COMMAND} VERBATIM) + target_sources(resid_objects PUBLIC ${WAVE_SRC_OUT}) +endforeach() + +# TODO CMake 3.12: Use target_link_libraries() to propagate usage requirements directly to sid plugin +add_library(resid INTERFACE) + +target_sources(resid INTERFACE $) + +get_target_property(resid_includes resid_objects INCLUDE_DIRECTORIES) +target_include_directories(resid INTERFACE ${resid_includes}) diff --git a/plugins/Sid/resid/resid b/plugins/Sid/resid/resid new file mode 160000 index 00000000000..ef72462f5fa --- /dev/null +++ b/plugins/Sid/resid/resid @@ -0,0 +1 @@ +Subproject commit ef72462f5fa0682d099413512b764ae479e77f9b From 006c43820bfb7867c84abffc96bf178bf12437a0 Mon Sep 17 00:00:00 2001 From: Lost Robot <34612565+LostRobotMusic@users.noreply.github.com> Date: Sun, 24 Sep 2023 04:19:19 -0700 Subject: [PATCH 43/51] Fix Compressor zero divisions (#6887) * Fix threshold zero division * Fix RMS zero division --- plugins/Compressor/Compressor.cpp | 3 ++- plugins/Compressor/CompressorControlDialog.cpp | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/Compressor/Compressor.cpp b/plugins/Compressor/Compressor.cpp index 33571606e7e..0fe13942083 100755 --- a/plugins/Compressor/Compressor.cpp +++ b/plugins/Compressor/Compressor.cpp @@ -177,7 +177,8 @@ void CompressorEffect::calcRange() void CompressorEffect::resizeRMS() { - m_rmsTimeConst = exp(-1.f / (m_compressorControls.m_rmsModel.value() * 0.001f * m_sampleRate)); + const float rmsValue = m_compressorControls.m_rmsModel.value(); + m_rmsTimeConst = (rmsValue > 0) ? exp(-1.f / (rmsValue * 0.001f * m_sampleRate)) : 0; } void CompressorEffect::calcLookaheadLength() diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 114980a7d96..1516456a22a 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -497,10 +497,12 @@ void CompressorControlDialog::redrawKnee() float actualRatio = m_controls->m_limiterModel.value() ? 0 : m_controls->m_effect->m_ratioVal; // Calculate endpoints for the two straight lines - float kneePoint1 = m_controls->m_effect->m_thresholdVal - m_controls->m_effect->m_kneeVal; - float kneePoint2X = m_controls->m_effect->m_thresholdVal + m_controls->m_effect->m_kneeVal; - float kneePoint2Y = (m_controls->m_effect->m_thresholdVal + (-m_controls->m_effect->m_thresholdVal * (actualRatio * (m_controls->m_effect->m_kneeVal / -m_controls->m_effect->m_thresholdVal)))); - float ratioPoint = m_controls->m_effect->m_thresholdVal + (-m_controls->m_effect->m_thresholdVal * actualRatio); + const float thresholdVal = m_controls->m_effect->m_thresholdVal; + const float kneeVal = m_controls->m_effect->m_kneeVal; + float kneePoint1 = thresholdVal - kneeVal; + float kneePoint2X = thresholdVal + kneeVal; + float kneePoint2Y = thresholdVal + kneeVal * actualRatio; + float ratioPoint = thresholdVal + (-thresholdVal * actualRatio); // Draw two straight lines m_p.drawLine(0, m_kneeWindowSizeY, dbfsToXPoint(kneePoint1), dbfsToYPoint(kneePoint1)); @@ -510,7 +512,7 @@ void CompressorControlDialog::redrawKnee() } // Draw knee section - if (m_controls->m_effect->m_kneeVal) + if (kneeVal) { m_p.setPen(QPen(m_kneeColor2, 3)); @@ -522,8 +524,8 @@ void CompressorControlDialog::redrawKnee() { newPoint[0] = linearInterpolate(kneePoint1, kneePoint2X, (i + 1) / (float)COMP_KNEE_LINES); - const float temp = newPoint[0] - m_controls->m_effect->m_thresholdVal + m_controls->m_effect->m_kneeVal; - newPoint[1] = (newPoint[0] + (actualRatio - 1) * temp * temp / (4 * m_controls->m_effect->m_kneeVal)); + const float temp = newPoint[0] - thresholdVal + kneeVal; + newPoint[1] = (newPoint[0] + (actualRatio - 1) * temp * temp / (4 * kneeVal)); m_p.drawLine(dbfsToXPoint(prevPoint[0]), dbfsToYPoint(prevPoint[1]), dbfsToXPoint(newPoint[0]), dbfsToYPoint(newPoint[1])); @@ -768,4 +770,4 @@ void CompressorControlDialog::resetCompressorView() } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui From c309d8bd11659a3f14557960a5e83366374d0ec2 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:54:07 +0200 Subject: [PATCH 44/51] Lv2ViewProc: Improve variable names --- include/Lv2ViewBase.h | 2 +- src/gui/Lv2ViewBase.cpp | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/Lv2ViewBase.h b/include/Lv2ViewBase.h index f7d0e9bcb4f..3c8f1bc3faf 100644 --- a/include/Lv2ViewBase.h +++ b/include/Lv2ViewBase.h @@ -56,7 +56,7 @@ class Lv2ViewProc : public LinkedModelGroupView { public: //! @param colNum numbers of columns for the controls - Lv2ViewProc(QWidget *parent, Lv2Proc *ctrlBase, int colNum); + Lv2ViewProc(QWidget *parent, Lv2Proc *proc, int colNum); ~Lv2ViewProc() override = default; private: diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 3fd1d44b109..12be5e06924 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -51,13 +51,13 @@ namespace lmms::gui { -Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : - LinkedModelGroupView (parent, ctrlBase, colNum) +Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* proc, int colNum) : + LinkedModelGroupView (parent, proc, colNum) { - class SetupWidget : public Lv2Ports::ConstVisitor + class SetupTheWidget : public Lv2Ports::ConstVisitor { public: - QWidget* m_par; // input + QWidget* m_parent; // input const LilvNode* m_commentUri; // input Control* m_control = nullptr; // output void visit(const Lv2Ports::Control& port) override @@ -69,20 +69,20 @@ Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : switch (port.m_vis) { case PortVis::Generic: - m_control = new KnobControl(m_par); + m_control = new KnobControl(m_parent); break; case PortVis::Integer: { sample_rate_t sr = Engine::audioEngine()->processingSampleRate(); m_control = new LcdControl((port.max(sr) <= 9.0f) ? 1 : 2, - m_par); + m_parent); break; } case PortVis::Enumeration: - m_control = new ComboControl(m_par); + m_control = new ComboControl(m_parent); break; case PortVis::Toggled: - m_control = new CheckControl(m_par); + m_control = new CheckControl(m_parent); break; } m_control->setText(port.name()); @@ -100,14 +100,14 @@ Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : }; AutoLilvNode commentUri = uri(LILV_NS_RDFS "comment"); - ctrlBase->foreach_port( + proc->foreach_port( [this, &commentUri](const Lv2Ports::PortBase* port) { if(!lilv_port_has_property(port->m_plugin, port->m_port, uri(LV2_PORT_PROPS__notOnGUI).get())) { - SetupWidget setup; - setup.m_par = this; + SetupTheWidget setup; + setup.m_parent = this; setup.m_commentUri = commentUri.get(); port->accept(setup); From d97377b640c3f288ed68860a3ffcf60ad7b5c7d9 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:58:52 +0200 Subject: [PATCH 45/51] Lv2: Improve LED widget's digit calculation This improves the way digits are calculated for display in `Lv2ViewProc`: - More than 2 digits are now recognized - Minus signs are now recognized - Tests have been added --- include/lmms_math.h | 26 ++++++++++++++++++ src/gui/Lv2ViewBase.cpp | 7 +++-- tests/CMakeLists.txt | 1 + tests/src/core/MathTest.cpp | 53 +++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 tests/src/core/MathTest.cpp diff --git a/include/lmms_math.h b/include/lmms_math.h index b62da81c249..ea0a75581e8 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -325,6 +325,32 @@ static inline T absMin( T a, T b ) return std::abs(a) < std::abs(b) ? a : 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) +{ + // use rounding: + // LcdSpinBox sometimes uses roundf(), sometimes cast rounding + // we use rounding to be on the "safe side" + const float rounded = roundf(f); + int asInt = static_cast(rounded); + int digits = 1; // always at least 1 + if(asInt < 0) + { + ++digits; + asInt = -asInt; + } + // "asInt" is positive from now + int32_t power = 1; + for(int32_t i = 1; i<10; ++i) + { + power *= 10; + if(static_cast(asInt) >= power) { ++digits; } // 2 digits for >=10, 3 for >=100 + else { break; } + } + return digits; +} + } // namespace lmms diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 12be5e06924..830a994c8c6 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -39,6 +39,7 @@ #include "GuiApplication.h" #include "embed.h" #include "gui_templates.h" +#include "lmms_math.h" #include "Lv2ControlBase.h" #include "Lv2Manager.h" #include "Lv2Proc.h" @@ -74,8 +75,10 @@ Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* proc, int colNum) : case PortVis::Integer: { sample_rate_t sr = Engine::audioEngine()->processingSampleRate(); - m_control = new LcdControl((port.max(sr) <= 9.0f) ? 1 : 2, - m_parent); + auto pMin = port.min(sr); + auto pMax = port.max(sr); + int numDigits = std::max(numDigitsAsInt(pMin), numDigitsAsInt(pMax)); + m_control = new LcdControl(numDigits, m_parent); break; } case PortVis::Enumeration: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 514bf78154e..ddf9e29621b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,7 @@ ADD_EXECUTABLE(tests src/core/ArrayVectorTest.cpp src/core/AutomatableModelTest.cpp + src/core/MathTest.cpp src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp diff --git a/tests/src/core/MathTest.cpp b/tests/src/core/MathTest.cpp new file mode 100644 index 00000000000..2b6404cfd5c --- /dev/null +++ b/tests/src/core/MathTest.cpp @@ -0,0 +1,53 @@ +/* + * MathTest.cpp + * + * Copyright (c) 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. + * + */ + +#include "QTestSuite.h" + +#include "lmms_math.h" + +#include + +class MathTest : QTestSuite +{ + Q_OBJECT +private slots: + void NumDigitsTest() + { + using namespace lmms; + QCOMPARE(numDigitsAsInt(1.f), 1); + QCOMPARE(numDigitsAsInt(9.9f), 2); + QCOMPARE(numDigitsAsInt(10.f), 2); + QCOMPARE(numDigitsAsInt(0.f), 1); + QCOMPARE(numDigitsAsInt(-100.f), 4); + QCOMPARE(numDigitsAsInt(-99.f), 3); + QCOMPARE(numDigitsAsInt(-0.4f), 1); // there is no "-0" for LED spinbox + QCOMPARE(numDigitsAsInt(-0.99f), 2); + QCOMPARE(numDigitsAsInt(1000000000), 10); + QCOMPARE(numDigitsAsInt(-1000000000), 11); + QCOMPARE(numDigitsAsInt(900000000), 9); + QCOMPARE(numDigitsAsInt(-900000000), 10); + } +} MathTests; + +#include "MathTest.moc" From 9c46370234fe45437f0f64fab963a232cb3e9a46 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Sat, 23 Sep 2023 00:00:02 +0200 Subject: [PATCH 46/51] Support LV2_BUF_SIZE__powerOf2BlockLength --- src/core/lv2/Lv2Manager.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index 9c62703e0a4..df6e5a7c19f 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -35,6 +35,7 @@ #include #include +#include "AudioEngine.h" #include "Engine.h" #include "Plugin.h" #include "Lv2ControlBase.h" @@ -156,6 +157,10 @@ Lv2Manager::Lv2Manager() : m_supportedFeatureURIs.insert(LV2_BUF_SIZE__boundedBlockLength); // block length is only changed initially in AudioEngine CTOR m_supportedFeatureURIs.insert(LV2_BUF_SIZE__fixedBlockLength); + if (const auto fpp = Engine::audioEngine()->framesPerPeriod(); (fpp & (fpp - 1)) == 0) // <=> ffp is power of 2 (for ffp > 0) + { + m_supportedFeatureURIs.insert(LV2_BUF_SIZE__powerOf2BlockLength); + } auto supportOpt = [this](Lv2UridCache::Id id) { From 61b612634d1690ba5ac21c4a9fea455e8a9e6f64 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:38:57 +0200 Subject: [PATCH 47/51] Add plugin blacklist for buffersize<=32 --- include/Lv2Features.h | 3 ++- include/Lv2Manager.h | 20 ++++++++++---------- src/core/lv2/Lv2Features.cpp | 6 +++--- src/core/lv2/Lv2Manager.cpp | 31 +++++++++++++++++++++---------- src/core/lv2/Lv2Proc.cpp | 16 ++++++++++++---- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/include/Lv2Features.h b/include/Lv2Features.h index b5bc284c87d..69a456bbde3 100644 --- a/include/Lv2Features.h +++ b/include/Lv2Features.h @@ -30,6 +30,7 @@ #ifdef LMMS_HAVE_LV2 #include +#include #include #include "Lv2Manager.h" @@ -78,7 +79,7 @@ class Lv2Features //! pointers to m_features, required for lilv_plugin_instantiate std::vector m_featurePointers; //! features + data, ordered by URI - std::map m_featureByUri; + std::map m_featureByUri; }; diff --git a/include/Lv2Manager.h b/include/Lv2Manager.h index 909dba5607e..58126a0a448 100644 --- a/include/Lv2Manager.h +++ b/include/Lv2Manager.h @@ -31,6 +31,7 @@ #include #include +#include #include #include "Lv2Basics.h" @@ -120,15 +121,9 @@ class Lv2Manager Iterator begin() { return m_lv2InfoMap.begin(); } Iterator end() { return m_lv2InfoMap.end(); } - //! strcmp based key comparator for std::set and std::map - struct CmpStr - { - bool operator()(char const *a, char const *b) const; - }; - UridMap& uridMap() { return m_uridMap; } const Lv2UridCache& uridCache() const { return m_uridCache; } - const std::set& supportedFeatureURIs() const + const std::set& supportedFeatureURIs() const { return m_supportedFeatureURIs; } @@ -136,17 +131,21 @@ class Lv2Manager AutoLilvNodes findNodes(const LilvNode *subject, const LilvNode *predicate, const LilvNode *object); - static const std::set& getPluginBlacklist() + static const std::set& getPluginBlacklist() { return pluginBlacklist; } + static const std::set& getPluginBlacklistBuffersizeLessThan32() + { + return pluginBlacklistBuffersizeLessThan32; + } private: // general data bool m_debug; //!< if set, debug output will be printed LilvWorld* m_world; Lv2InfoMap m_lv2InfoMap; - std::set m_supportedFeatureURIs; + std::set m_supportedFeatureURIs; // feature data that are common for all Lv2Proc UridMap m_uridMap; @@ -155,7 +154,8 @@ class Lv2Manager Lv2UridCache m_uridCache; // static - static const std::set pluginBlacklist; + static const std::set + pluginBlacklist, pluginBlacklistBuffersizeLessThan32; // functions bool isSubclassOf(const LilvPluginClass *clvss, const char *uriStr); diff --git a/src/core/lv2/Lv2Features.cpp b/src/core/lv2/Lv2Features.cpp index 6e74a89360c..c8fc0546517 100644 --- a/src/core/lv2/Lv2Features.cpp +++ b/src/core/lv2/Lv2Features.cpp @@ -48,7 +48,7 @@ Lv2Features::Lv2Features() { const Lv2Manager* man = Engine::getLv2Manager(); // create (yet empty) map feature URI -> feature - for(const char* uri : man->supportedFeatureURIs()) + for(auto uri : man->supportedFeatureURIs()) { m_featureByUri.emplace(uri, nullptr); } @@ -71,7 +71,7 @@ void Lv2Features::initCommon() void Lv2Features::createFeatureVectors() { // create vector of features - for(std::pair& pr : m_featureByUri) + for(const auto& [uri, feature] : m_featureByUri) { /* If pr.second is nullptr here, this means that the LV2_feature @@ -82,7 +82,7 @@ void Lv2Features::createFeatureVectors() vector creation (This can be done in Lv2Proc::initPluginSpecificFeatures or in Lv2Features::initCommon) */ - m_features.push_back(LV2_Feature { pr.first, pr.second }); + m_features.push_back(LV2_Feature{(const char*)uri.data(), (void*)feature}); } // create pointer vector (for lilv_plugin_instantiate) diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index df6e5a7c19f..489e613b705 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -28,7 +28,6 @@ #include #include -#include #include #include #include @@ -47,7 +46,7 @@ namespace lmms { -const std::set Lv2Manager::pluginBlacklist = +const std::set Lv2Manager::pluginBlacklist = { // github.com/calf-studio-gear/calf, #278 "http://calf.sourceforge.net/plugins/Analyzer", @@ -138,6 +137,26 @@ const std::set Lv2Manager::pluginBlacklist = "urn:juced:DrumSynth" }; +const std::set Lv2Manager::pluginBlacklistBuffersizeLessThan32 = +{ + "http://moddevices.com/plugins/mod-devel/2Voices", + "http://moddevices.com/plugins/mod-devel/Capo", + "http://moddevices.com/plugins/mod-devel/Drop", + "http://moddevices.com/plugins/mod-devel/Harmonizer", + "http://moddevices.com/plugins/mod-devel/Harmonizer2", + "http://moddevices.com/plugins/mod-devel/HarmonizerCS", + "http://moddevices.com/plugins/mod-devel/SuperCapo", + "http://moddevices.com/plugins/mod-devel/SuperWhammy", + "http://moddevices.com/plugins/mod-devel/Gx2Voices", + "http://moddevices.com/plugins/mod-devel/GxCapo", + "http://moddevices.com/plugins/mod-devel/GxDrop", + "http://moddevices.com/plugins/mod-devel/GxHarmonizer", + "http://moddevices.com/plugins/mod-devel/GxHarmonizer2", + "http://moddevices.com/plugins/mod-devel/GxHarmonizerCS", + "http://moddevices.com/plugins/mod-devel/GxSuperCapo", + "http://moddevices.com/plugins/mod-devel/GxSuperWhammy" +}; + @@ -293,14 +312,6 @@ void Lv2Manager::initPlugins() -bool Lv2Manager::CmpStr::operator()(const char *a, const char *b) const -{ - return std::strcmp(a, b) < 0; -} - - - - bool Lv2Manager::isFeatureSupported(const char *featName) const { return m_supportedFeatureURIs.find(featName) != m_supportedFeatureURIs.end(); diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index e0541b948c9..6776a26ed63 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -75,11 +75,19 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin, // TODO: manage a global blacklist outside of the code // for now, this will help // this is only a fix for the meantime - const auto& pluginBlacklist = Lv2Manager::getPluginBlacklist(); - if (!Engine::ignorePluginBlacklist() && - pluginBlacklist.find(pluginUri) != pluginBlacklist.end()) + if (!Engine::ignorePluginBlacklist()) { - issues.emplace_back(PluginIssueType::Blacklisted); + const auto& pluginBlacklist = Lv2Manager::getPluginBlacklist(); + const auto& pluginBlacklist32 = Lv2Manager::getPluginBlacklistBuffersizeLessThan32(); + if(pluginBlacklist.find(pluginUri) != pluginBlacklist.end()) + { + issues.emplace_back(PluginIssueType::Blacklisted); + } + else if(Engine::audioEngine()->framesPerPeriod() <= 32 && + pluginBlacklist32.find(pluginUri) != pluginBlacklist32.end()) + { + issues.emplace_back(PluginIssueType::Blacklisted); // currently no special blacklist category + } } for (unsigned portNum = 0; portNum < maxPorts; ++portNum) From 7aca8ae726ab425b47802721703fb5589fe8289a Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:56:45 +0200 Subject: [PATCH 48/51] SetupDialog: Warn of unusual buffersizes This displays a warning dialog if the users requests unusual buffersizes: - buffersizes less than 32 - buffersizes which are not a (natural number) power of 2 This commit also replaces some `setGeometry` stuff by `QBoxLayout`. --- include/SetupDialog.h | 2 ++ src/gui/modals/SetupDialog.cpp | 38 ++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/include/SetupDialog.h b/include/SetupDialog.h index de4cdd9ddbd..fa41325db79 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -102,6 +102,7 @@ private slots: // Audio settings widget. void audioInterfaceChanged(const QString & driver); void toggleHQAudioDev(bool enabled); + void updateBufferSizeWarning(int value); void setBufferSize(int value); void resetBufferSize(); @@ -179,6 +180,7 @@ private slots: int m_bufferSize; QSlider * m_bufferSizeSlider; QLabel * m_bufferSizeLbl; + QLabel * m_bufferSizeWarnLbl; // MIDI settings widgets. QComboBox * m_midiInterfaces; diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 63b84506e46..0266285a7a4 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -579,32 +579,39 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : // Buffer size tab. auto bufferSize_tw = new TabWidget(tr("Buffer size"), audio_w); - bufferSize_tw->setFixedHeight(76); + auto bufferSize_layout = new QVBoxLayout(bufferSize_tw); + bufferSize_layout->setSpacing(10); + bufferSize_layout->setContentsMargins(10, 18, 10, 10); m_bufferSizeSlider = new QSlider(Qt::Horizontal, bufferSize_tw); m_bufferSizeSlider->setRange(1, 128); m_bufferSizeSlider->setTickInterval(8); m_bufferSizeSlider->setPageStep(8); m_bufferSizeSlider->setValue(m_bufferSize / BUFFERSIZE_RESOLUTION); - m_bufferSizeSlider->setGeometry(10, 18, 340, 18); m_bufferSizeSlider->setTickPosition(QSlider::TicksBelow); + m_bufferSizeLbl = new QLabel(bufferSize_tw); + + m_bufferSizeWarnLbl = new QLabel(bufferSize_tw); + m_bufferSizeWarnLbl->setWordWrap(true); + connect(m_bufferSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBufferSize(int))); connect(m_bufferSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(showRestartWarning())); - - m_bufferSizeLbl = new QLabel(bufferSize_tw); - m_bufferSizeLbl->setGeometry(10, 40, 200, 24); setBufferSize(m_bufferSizeSlider->value()); auto bufferSize_reset_btn = new QPushButton(embed::getIconPixmap("reload"), "", bufferSize_tw); - bufferSize_reset_btn->setGeometry(320, 40, 28, 28); connect(bufferSize_reset_btn, SIGNAL(clicked()), this, SLOT(resetBufferSize())); bufferSize_reset_btn->setToolTip( tr("Reset to default value")); + bufferSize_layout->addWidget(m_bufferSizeSlider); + bufferSize_layout->addWidget(m_bufferSizeLbl); + bufferSize_layout->addWidget(m_bufferSizeWarnLbl); + bufferSize_layout->addWidget(bufferSize_reset_btn); + // Audio layout ordering. audio_layout->addWidget(audioiface_tw); @@ -1172,6 +1179,24 @@ void SetupDialog::audioInterfaceChanged(const QString & iface) } +void SetupDialog::updateBufferSizeWarning(int value) +{ + QString text = "
    "; + if((value & (value - 1)) != 0) // <=> value is not a power of 2 (for value > 0) + { + text += "
  • " + tr("The currently selected value is not a power of 2 " + "(32, 64, 128, 256, 512, 1024, ...). Some plugins may not be available.") + "
  • "; + } + if(value <= 32) + { + text += "
  • " + tr("The currently selected value is less than or equal to 32. " + "Some plugins may not be available.") + "
  • "; + } + text += "
"; + m_bufferSizeWarnLbl->setText(text); +} + + void SetupDialog::setBufferSize(int value) { const int step = DEFAULT_BUFFER_SIZE / BUFFERSIZE_RESOLUTION; @@ -1197,6 +1222,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)); + updateBufferSizeWarning(m_bufferSize); } From 94608eaad1da0622a5eb1fa6d169f8e92d06a6aa Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:42:45 +0200 Subject: [PATCH 49/51] Lv2Proc: Sort scale point maps (#6859) lilv can return the scale points in randomized order, and so the linked models (of stereo effects wit 2 `Lv2Proc`) may have different scale points. This would mean that the same combo enumeration value would have different float values in linked models. This problem is fixed by sorting the scale values. Also, it is more natural for users if scale points are enumerated in a numerical order. --- include/Lv2Basics.h | 6 ++++++ src/core/lv2/Lv2Proc.cpp | 28 +++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/include/Lv2Basics.h b/include/Lv2Basics.h index 9a958d9736a..53489e30d10 100644 --- a/include/Lv2Basics.h +++ b/include/Lv2Basics.h @@ -47,8 +47,14 @@ struct LilvNodesDeleter void operator()(LilvNodes* n) { lilv_nodes_free(n); } }; +struct LilvScalePointsDeleter +{ + void operator()(LilvScalePoints* s) { lilv_scale_points_free(s); } +}; + using AutoLilvNode = std::unique_ptr; using AutoLilvNodes = std::unique_ptr; +using AutoLilvScalePoints = std::unique_ptr; /** Return QString from a plugin's node, everything will be freed automatically diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 6776a26ed63..a5fdf404156 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -574,19 +574,25 @@ void Lv2Proc::createPort(std::size_t portNum) break; case Lv2Ports::Vis::Enumeration: { - auto comboModel = new ComboBoxModel(nullptr, dispName); - LilvScalePoints* sps = - lilv_port_get_scale_points(m_plugin, lilvPort); - LILV_FOREACH(scale_points, i, sps) + ComboBoxModel* comboModel = new ComboBoxModel(nullptr, dispName); + { - const LilvScalePoint* sp = lilv_scale_points_get(sps, i); - ctrl->m_scalePointMap.push_back(lilv_node_as_float( - lilv_scale_point_get_value(sp))); - comboModel->addItem( - lilv_node_as_string( - lilv_scale_point_get_label(sp))); + AutoLilvScalePoints sps (static_cast(lilv_port_get_scale_points(m_plugin, lilvPort))); + // temporary map, since lilv may return scale points in random order + std::map scalePointMap; + LILV_FOREACH(scale_points, i, sps.get()) + { + const LilvScalePoint* sp = lilv_scale_points_get(sps.get(), i); + const float f = lilv_node_as_float(lilv_scale_point_get_value(sp)); + const char* s = lilv_node_as_string(lilv_scale_point_get_label(sp)); + scalePointMap[f] = s; + } + for (const auto& [f,s] : scalePointMap) + { + ctrl->m_scalePointMap.push_back(f); + comboModel->addItem(s); + } } - lilv_scale_points_free(sps); ctrl->m_connectedModel.reset(comboModel); // TODO: use default value on comboModel, too? break; From 83777dc1f73736ec58376590f09cba92331cd901 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:42:45 +0200 Subject: [PATCH 50/51] Lv2Proc: Set def val for ComboModel, too --- src/core/lv2/Lv2Proc.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index a5fdf404156..242f3d92bc2 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -593,8 +593,16 @@ void Lv2Proc::createPort(std::size_t portNum) comboModel->addItem(s); } } + for(std::size_t i = 0; i < ctrl->m_scalePointMap.size(); ++i) + { + if(meta.def() == ctrl->m_scalePointMap[i]) + { + comboModel->setValue(i); + comboModel->setInitValue(i); + break; + } + } ctrl->m_connectedModel.reset(comboModel); - // TODO: use default value on comboModel, too? break; } case Lv2Ports::Vis::Toggled: From 33d1baddc0284bd68df752a08ff9c673a706c4d7 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Fri, 22 Sep 2023 23:27:02 +0200 Subject: [PATCH 51/51] Implement Lv2 Worker (#6484) --- include/AudioEngine.h | 1 + include/LmmsSemaphore.h | 93 ++++++++++++++++ include/LocklessRingBuffer.h | 3 +- include/Lv2Proc.h | 13 ++- include/Lv2Worker.h | 93 ++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/LmmsSemaphore.cpp | 143 ++++++++++++++++++++++++ src/core/lv2/Lv2Manager.cpp | 2 + src/core/lv2/Lv2Proc.cpp | 31 +++++- src/core/lv2/Lv2Worker.cpp | 203 +++++++++++++++++++++++++++++++++++ 10 files changed, 580 insertions(+), 4 deletions(-) create mode 100644 include/LmmsSemaphore.h create mode 100644 include/Lv2Worker.h create mode 100644 src/core/LmmsSemaphore.cpp create mode 100644 src/core/lv2/Lv2Worker.cpp diff --git a/include/AudioEngine.h b/include/AudioEngine.h index f056c22e108..d3d0d025ffc 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -197,6 +197,7 @@ class LMMS_EXPORT AudioEngine : public QObject // audio-device-stuff + bool renderOnly() const { return m_renderOnly; } // Returns the current audio device's name. This is not necessarily // the user's preferred audio device, in case you were thinking that. inline const QString & audioDevName() const diff --git a/include/LmmsSemaphore.h b/include/LmmsSemaphore.h new file mode 100644 index 00000000000..4170eef6c65 --- /dev/null +++ b/include/LmmsSemaphore.h @@ -0,0 +1,93 @@ +/* + * Semaphore.h - Semaphore declaration + * + * Copyright (c) 2022-2022 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. + * + */ + +/* + * This code has been copied and adapted from https://github.com/drobilla/jalv + * File src/zix/sem.h + */ + +#ifndef LMMS_SEMAPHORE_H +#define LMMS_SEMAPHORE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_BUILD_APPLE +# include +#elif defined(LMMS_BUILD_WIN32) +# include +#else +# include +#endif + +#include + +namespace lmms { + +/** + A counting semaphore. + + This is an integer that is always positive, and has two main operations: + increment (post) and decrement (wait). If a decrement can not be performed + (i.e. the value is 0) the caller will be blocked until another thread posts + and the operation can succeed. + + Semaphores can be created with any starting value, but typically this will + be 0 so the semaphore can be used as a simple signal where each post + corresponds to one wait. + + Semaphores are very efficient (much moreso than a mutex/cond pair). In + particular, at least on Linux, post is async-signal-safe, which means it + does not block and will not be interrupted. If you need to signal from + a realtime thread, this is the most appropriate primitive to use. + + @note Likely outdated with C++20's std::counting_semaphore + (though we have to check that this will be RT conforming on all platforms) +*/ +class Semaphore +{ +public: + Semaphore(unsigned initial); + Semaphore(const Semaphore&) = delete; + Semaphore& operator=(const Semaphore&) = delete; + Semaphore(Semaphore&&) = delete; + Semaphore& operator=(Semaphore&&) = delete; + ~Semaphore(); + + void post(); + void wait(); + bool tryWait(); + +private: +#ifdef LMMS_BUILD_APPLE + semaphore_t m_sem; +#elif defined(LMMS_BUILD_WIN32) + HANDLE m_sem; +#else + sem_t m_sem; +#endif +}; + +} // namespace lmms + +#endif // LMMS_SEMAPHORE_H diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 99c48cc904a..2d65badfe58 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -51,13 +51,14 @@ class LocklessRingBuffer std::size_t capacity() const {return m_buffer.maximum_eventual_write_space();} std::size_t free() const {return m_buffer.write_space();} void wakeAll() {m_notifier.wakeAll();} - std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) + std::size_t write(const T *src, std::size_t cnt, bool notify = false) { std::size_t written = LocklessRingBuffer::m_buffer.write(src, cnt); // Let all waiting readers know new data are available. if (notify) {LocklessRingBuffer::m_notifier.wakeAll();} return written; } + void mlock() { m_buffer.mlock(); } protected: ringbuffer_t m_buffer; diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 62070def7f6..76fa5eec25b 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -1,7 +1,7 @@ /* * Lv2Proc.h - Lv2 processor class * - * Copyright (c) 2019-2020 Johannes Lorenz + * Copyright (c) 2019-2022 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -31,11 +31,14 @@ #include #include +#include +#include "LinkedModelGroups.h" +#include "LmmsSemaphore.h" #include "Lv2Basics.h" #include "Lv2Features.h" #include "Lv2Options.h" -#include "LinkedModelGroups.h" +#include "Lv2Worker.h" #include "Plugin.h" #include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" #include "TimePos.h" @@ -174,8 +177,14 @@ class Lv2Proc : public LinkedModelGroup const LilvPlugin* m_plugin; LilvInstance* m_instance; Lv2Features m_features; + + // options Lv2Options m_options; + // worker + std::optional m_worker; + Semaphore m_workLock; // this must be shared by different workers + // full list of ports std::vector> m_ports; // quick reference to specific, unique ports diff --git a/include/Lv2Worker.h b/include/Lv2Worker.h new file mode 100644 index 00000000000..7931f8e7cde --- /dev/null +++ b/include/Lv2Worker.h @@ -0,0 +1,93 @@ +/* + * Lv2Worker.h - Lv2Worker class + * + * Copyright (c) 2022-2022 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 LV2WORKER_H +#define LV2WORKER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include +#include + +#include "LocklessRingBuffer.h" +#include "LmmsSemaphore.h" + +namespace lmms +{ + +/** + Worker container +*/ +class Lv2Worker +{ +public: + // CTOR/DTOR/feature access + Lv2Worker(const LV2_Worker_Interface* iface, Semaphore* common_work_lock, bool threaded); + ~Lv2Worker(); + void setHandle(LV2_Handle handle) { m_handle = handle; } + 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); } + } + + // to be called only by static functions + LV2_Worker_Status scheduleWork(uint32_t size, const void* data); + LV2_Worker_Status respond(uint32_t size, const void* data); + +private: + // functions + void workerFunc(); + std::size_t bufferSize() const; //!< size of internal buffers + + // parameters + const LV2_Worker_Interface* m_iface; + bool m_threaded; + LV2_Handle m_handle; + LV2_Worker_Schedule m_scheduleFeature; + + // threading/synchronization + std::thread m_thread; + std::vector m_response; //!< buffer where single requests from m_requests are unpacked + LocklessRingBuffer m_requests, m_responses; //!< ringbuffer to queue multiple requests + LocklessRingBufferReader m_requestsReader, m_responsesReader; + std::atomic m_exit = false; //!< Whether the worker function should keep looping + Semaphore m_sem; + Semaphore* m_workLock; +}; + + +} // namespace lmms + +#endif // LMMS_HAVE_LV2 + +#endif // LV2WORKER_H + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 319882af2f9..1155f5e0d22 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -70,6 +70,7 @@ set(LMMS_SRCS core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp core/Scale.cpp + core/LmmsSemaphore.cpp core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp @@ -112,6 +113,7 @@ set(LMMS_SRCS core/lv2/Lv2SubPluginFeatures.cpp core/lv2/Lv2UridCache.cpp core/lv2/Lv2UridMap.cpp + core/lv2/Lv2Worker.cpp core/midi/MidiAlsaRaw.cpp core/midi/MidiAlsaSeq.cpp diff --git a/src/core/LmmsSemaphore.cpp b/src/core/LmmsSemaphore.cpp new file mode 100644 index 00000000000..daa70a45ba3 --- /dev/null +++ b/src/core/LmmsSemaphore.cpp @@ -0,0 +1,143 @@ +/* + * Semaphore.cpp - Semaphore implementation + * + * Copyright (c) 2022-2022 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. + * + */ + +/* + * This code has been copied and adapted from https://github.com/drobilla/jalv + * File src/zix/sem.h + */ + +#include "LmmsSemaphore.h" + +#if defined(LMMS_BUILD_WIN32) +# include +#else +# include +#endif + +#include + +namespace lmms { + +#ifdef LMMS_BUILD_APPLE +Semaphore::Semaphore(unsigned val) +{ + kern_return_t rval = semaphore_create(mach_task_self(), &m_sem, SYNC_POLICY_FIFO, val); + if(rval != 0) { + throw std::system_error(rval, std::system_category(), "Could not create semaphore"); + } +} + +Semaphore::~Semaphore() +{ + semaphore_destroy(mach_task_self(), m_sem); +} + +void Semaphore::post() +{ + semaphore_signal(m_sem); +} + +void Semaphore::wait() +{ + kern_return_t rval = semaphore_wait(m_sem); + if (rval != KERN_SUCCESS) { + throw std::system_error(rval, std::system_category(), "Waiting for semaphore failed"); + } +} + +bool Semaphore::tryWait() +{ + const mach_timespec_t zero = { 0, 0 }; + return semaphore_timedwait(m_sem, zero) == KERN_SUCCESS; +} + +#elif defined(LMMS_BUILD_WIN32) + +Semaphore::Semaphore(unsigned initial) +{ + if(CreateSemaphore(nullptr, initial, LONG_MAX, nullptr) == nullptr) { + throw std::system_error(GetLastError(), std::system_category(), "Could not create semaphore"); + } +} + +Semaphore::~Semaphore() +{ + CloseHandle(m_sem); +} + +void Semaphore::post() +{ + ReleaseSemaphore(m_sem, 1, nullptr); +} + +void Semaphore::wait() +{ + if (WaitForSingleObject(m_sem, INFINITE) != WAIT_OBJECT_0) { + throw std::system_error(GetLastError(), std::system_category(), "Waiting for semaphore failed"); + } +} + +bool Semaphore::tryWait() +{ + return WaitForSingleObject(m_sem, 0) == WAIT_OBJECT_0; +} + +#else /* !defined(LMMS_BUILD_APPLE) && !defined(LMMS_BUILD_WIN32) */ + +Semaphore::Semaphore(unsigned initial) +{ + if(sem_init(&m_sem, 0, initial) != 0) { + throw std::system_error(errno, std::generic_category(), "Could not create semaphore"); + } +} + +Semaphore::~Semaphore() +{ + sem_destroy(&m_sem); +} + +void Semaphore::post() +{ + sem_post(&m_sem); +} + +void Semaphore::wait() +{ + while (sem_wait(&m_sem) != 0) { + if (errno != EINTR) { + throw std::system_error(errno, std::generic_category(), "Waiting for semaphore failed"); + } + /* Otherwise, interrupted, so try again. */ + } +} + +bool Semaphore::tryWait() +{ + return (sem_trywait(&m_sem) == 0); +} + +#endif + +} // namespace lmms + diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index 489e613b705..6a1b2a8af20 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -172,6 +173,7 @@ Lv2Manager::Lv2Manager() : m_supportedFeatureURIs.insert(LV2_URID__map); m_supportedFeatureURIs.insert(LV2_URID__unmap); m_supportedFeatureURIs.insert(LV2_OPTIONS__options); + m_supportedFeatureURIs.insert(LV2_WORKER__schedule); // min/max is always passed in the options m_supportedFeatureURIs.insert(LV2_BUF_SIZE__boundedBlockLength); // block length is only changed initially in AudioEngine CTOR diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 242f3d92bc2..11290013e5b 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -1,7 +1,7 @@ /* * Lv2Proc.cpp - Lv2 processor class * - * Copyright (c) 2019-2020 Johannes Lorenz + * Copyright (c) 2019-2022 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -170,6 +171,7 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin, Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : LinkedModelGroup(parent), m_plugin(plugin), + m_workLock(1), m_midiInputBuf(m_maxMidiInputEvents), m_midiInputReader(m_midiInputBuf) { @@ -360,7 +362,19 @@ void Lv2Proc::copyBuffersToCore(sampleFrame* buf, void Lv2Proc::run(fpp_t frames) { + if (m_worker) + { + // Process any worker replies + m_worker->emitResponses(); + } + lilv_instance_run(m_instance, static_cast(frames)); + + if (m_worker) + { + // Notify the plugin the run() cycle is finished + m_worker->notifyPluginThatRunFinished(); + } } @@ -428,6 +442,9 @@ void Lv2Proc::initPlugin() if (m_instance) { + if(m_worker) { + m_worker->setHandle(lilv_instance_get_handle(m_instance)); + } for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum) connectPort(portNum); lilv_instance_activate(m_instance); @@ -504,8 +521,20 @@ void Lv2Proc::initMOptions() void Lv2Proc::initPluginSpecificFeatures() { + // options initMOptions(); m_features[LV2_OPTIONS__options] = const_cast(m_options.feature()); + + // 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_features[LV2_WORKER__schedule] = m_worker->feature(); + // Note: m_worker::setHandle will still need to be called later + } } diff --git a/src/core/lv2/Lv2Worker.cpp b/src/core/lv2/Lv2Worker.cpp new file mode 100644 index 00000000000..5af955ff766 --- /dev/null +++ b/src/core/lv2/Lv2Worker.cpp @@ -0,0 +1,203 @@ +/* + * Lv2Worker.cpp - Lv2Worker implementation + * + * Copyright (c) 2022-2022 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. + * + */ + +#include "Lv2Worker.h" + +#include +#include + +#ifdef LMMS_HAVE_LV2 + +#include "Engine.h" + + +namespace lmms +{ + + +// static wrappers + +static LV2_Worker_Status +staticWorkerRespond(LV2_Worker_Respond_Handle handle, + uint32_t size, const void* data) +{ + Lv2Worker* worker = static_cast(handle); + return worker->respond(size, data); +} + + + + +std::size_t Lv2Worker::bufferSize() const +{ + // ardour uses this fixed size for ALSA: + return 8192 * 4; + // for jack, they use 4 * jack_port_type_get_buffer_size (..., JACK_DEFAULT_MIDI_TYPE) + // (possible extension for AudioDevice) +} + + + + +Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface, + Semaphore* common_work_lock, + bool threaded) : + m_iface(iface), + m_threaded(threaded), + m_response(bufferSize()), + m_requests(bufferSize()), + m_responses(bufferSize()), + m_requestsReader(m_requests), + m_responsesReader(m_responses), + m_sem(0), + m_workLock(common_work_lock) +{ + 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 + { + Lv2Worker* worker = static_cast(handle); + return worker->scheduleWork(size, data); + }; + + if (threaded) { m_thread = std::thread(&Lv2Worker::workerFunc, this); } + + m_requests.mlock(); + m_responses.mlock(); +} + + + + +Lv2Worker::~Lv2Worker() +{ + m_exit = true; + if(m_threaded) { + m_sem.post(); + m_thread.join(); + } +} + + + + +// Let the worker send responses to the audio thread +LV2_Worker_Status Lv2Worker::respond(uint32_t size, const void* data) +{ + if(m_threaded) + { + if(m_responses.free() < sizeof(size) + size) + { + return LV2_WORKER_ERR_NO_SPACE; + } + else + { + m_responses.write((const char*)&size, sizeof(size)); + if(size && data) { m_responses.write((const char*)data, size); } + } + } + else + { + m_iface->work_response(m_handle, size, data); + } + return LV2_WORKER_SUCCESS; +} + + + + +// Let the worker receive work from the audio thread and "work" on it +void Lv2Worker::workerFunc() +{ + std::vector buf; + uint32_t size; + 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) + + m_requestsReader.read(sizeof(size)).copy((char*)&size, sizeof(size)); + assert(size <= readSpace - sizeof(size)); + if(size > buf.size()) { buf.resize(size); } + if(size) { m_requestsReader.read(size).copy(buf.data(), size); } + + m_workLock->wait(); + m_iface->work(m_handle, staticWorkerRespond, this, size, buf.data()); + m_workLock->post(); + } +} + + + + +// Let the audio thread schedule work for the worker +LV2_Worker_Status Lv2Worker::scheduleWork(uint32_t size, const void *data) +{ + if (m_threaded) + { + if(m_requests.free() < sizeof(size) + size) + { + return LV2_WORKER_ERR_NO_SPACE; + } + else + { + // Schedule a request to be executed by the worker thread + m_requests.write((const char*)&size, sizeof(size)); + if(size && data) { m_requests.write((const char*)data, size); } + m_sem.post(); + } + } + else + { + // Execute work immediately in this thread + m_workLock->wait(); + m_iface->work(m_handle, staticWorkerRespond, this, size, data); + m_workLock->post(); + } + + return LV2_WORKER_SUCCESS; +} + + + + +// Let the audio thread read incoming worker responses, and process it +void Lv2Worker::emitResponses() +{ + std::size_t read_space = m_responsesReader.read_space(); + uint32_t size; + while (read_space > sizeof(size)) { + 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()); + read_space -= sizeof(size) + size; + } +} + + +} // namespace lmms + +#endif // LMMS_HAVE_LV2