diff --git a/include/SampleThumbnail.h b/include/SampleThumbnail.h index e3362f9a5b1..59cef29eb88 100644 --- a/include/SampleThumbnail.h +++ b/include/SampleThumbnail.h @@ -56,6 +56,8 @@ namespace lmms::gui { class LMMS_EXPORT SampleThumbnail { public: + enum Type { MONO, LEFT, RIGHT }; + struct VisualizeParameters { QRect sampleRect; //!< A rectangle that covers the entire range of samples. @@ -70,6 +72,8 @@ class LMMS_EXPORT SampleThumbnail float sampleEnd = 1.0f; //!< Where the sample ends for drawing. bool reversed = false; //!< Determines if the waveform is drawn in reverse or not. + + Type waveType = Type::MONO; //!< Determines which stereo channel is drawn }; SampleThumbnail() = default; @@ -104,20 +108,22 @@ class LMMS_EXPORT SampleThumbnail }; Thumbnail() = default; - Thumbnail(std::vector peaks, double samplesPerPeak); - Thumbnail(const float* buffer, size_t size, size_t width); + Thumbnail(std::vector right_peaks, std::vector left_peaks, double samplesPerPeak); + Thumbnail(const SampleBuffer* buffer, size_t width); Thumbnail zoomOut(float factor) const; - Peak* data() { return m_peaks.data(); } - Peak& operator[](size_t index) { return m_peaks[index]; } - const Peak& operator[](size_t index) const { return m_peaks[index]; } + Peak* mono() { return m_mono_peaks.data(); } + Peak* right() { return m_right_peaks.data(); } + Peak* left() { return m_left_peaks.data(); } - int width() const { return m_peaks.size(); } + int width() const { return m_mono_peaks.size(); } double samplesPerPeak() const { return m_samplesPerPeak; } private: - std::vector m_peaks; + std::vector m_mono_peaks; + std::vector m_right_peaks; + std::vector m_left_peaks; double m_samplesPerPeak = 0.0; }; diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 63418bd8ddb..a5de509e517 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -87,6 +87,7 @@ private slots: void toggleOpenLastProject(bool enabled); void detachBehaviorChanged(); void loopMarkerModeChanged(); + void stereoChannelsPolicyChanged(); void setLanguage(int lang); // Performance settings widget. @@ -153,6 +154,8 @@ private slots: QComboBox* m_detachBehaviorComboBox; QString m_loopMarkerMode; QComboBox* m_loopMarkerComboBox; + QString m_stereoChannelsPolicy; + QComboBox* m_stereoChannelsComboBox; QString m_autoScroll; QComboBox* m_autoScrollComboBox; QString m_lang; diff --git a/src/gui/SampleThumbnail.cpp b/src/gui/SampleThumbnail.cpp index 64903dc17a5..bb9776a774d 100644 --- a/src/gui/SampleThumbnail.cpp +++ b/src/gui/SampleThumbnail.cpp @@ -25,10 +25,14 @@ #include "SampleThumbnail.h" +#include +#include #include #include #include "Sample.h" +#include "SampleBuffer.h" +#include "SampleFrame.h" namespace { constexpr auto MaxSampleThumbnailCacheSize = 32; @@ -37,22 +41,39 @@ namespace { namespace lmms::gui { -SampleThumbnail::Thumbnail::Thumbnail(std::vector peaks, double samplesPerPeak) - : m_peaks(std::move(peaks)) +SampleThumbnail::Thumbnail::Thumbnail(std::vector right_peaks, std::vector left_peaks, double samplesPerPeak) + : m_mono_peaks(right_peaks.size()) + , m_right_peaks(std::move(right_peaks)) + , m_left_peaks(std::move(left_peaks)) , m_samplesPerPeak(samplesPerPeak) { + assert(m_right_peaks.size() == m_left_peaks.size() && "Stereo peaks arrays lengths don't match"); + + for (size_t i = 0; i < m_right_peaks.size(); i++) + { + m_mono_peaks[i] = m_right_peaks[i] + m_left_peaks[i]; + } } -SampleThumbnail::Thumbnail::Thumbnail(const float* buffer, size_t size, size_t width) - : m_peaks(width) - , m_samplesPerPeak(std::max(static_cast(size) / width, 1.0)) +SampleThumbnail::Thumbnail::Thumbnail(const SampleBuffer* buffer, size_t width) + : m_mono_peaks(width) + , m_right_peaks(width) + , m_left_peaks(width) + , m_samplesPerPeak(std::max(static_cast(buffer->size()) / width, 1.0)) { for (auto peakIndex = std::size_t{0}; peakIndex < width; ++peakIndex) { - const auto beginSample = buffer + static_cast(std::floor(peakIndex * m_samplesPerPeak)); - const auto endSample = buffer + static_cast(std::ceil((peakIndex + 1) * m_samplesPerPeak)); - const auto [min, max] = std::minmax_element(beginSample, endSample); - m_peaks[peakIndex] = Peak{*min, *max}; + const auto beginSample = buffer->data() + static_cast(std::floor(peakIndex * m_samplesPerPeak)); + const auto endSample = buffer->data() + static_cast(std::ceil((peakIndex + 1) * m_samplesPerPeak)); + const auto [right_min, right_max] = std::minmax_element(beginSample, endSample, + [](const SampleFrame& a, const SampleFrame& b){ return a.right() < b.right();} + ); + const auto [left_min, left_max] = std::minmax_element(beginSample, endSample, + [](const SampleFrame& a, const SampleFrame& b){ return a.left() < b.left();} + ); + m_right_peaks[peakIndex] = Peak{right_min->right(), right_max->right()}; + m_left_peaks[peakIndex] = Peak{left_min->left(), left_max->left()}; + m_mono_peaks[peakIndex] = m_right_peaks[peakIndex] + m_left_peaks[peakIndex]; } } @@ -60,15 +81,20 @@ SampleThumbnail::Thumbnail SampleThumbnail::Thumbnail::zoomOut(float factor) con { assert(factor >= 1 && "Invalid zoom out factor"); - auto peaks = std::vector(m_peaks.size() / factor); - for (auto peakIndex = std::size_t{0}; peakIndex < peaks.size(); ++peakIndex) + auto right_peaks = std::vector(m_right_peaks.size() / factor); + auto left_peaks = std::vector(m_left_peaks.size() / factor); + + for (auto peakIndex = std::size_t{0}; peakIndex < right_peaks.size(); ++peakIndex) { - const auto beginAggregationAt = m_peaks.begin() + static_cast(std::floor(peakIndex * factor)); - const auto endAggregationAt = m_peaks.begin() + static_cast(std::ceil((peakIndex + 1) * factor)); - peaks[peakIndex] = std::accumulate(beginAggregationAt, endAggregationAt, Peak{}); - } + const auto beginRightAggregationAt = m_right_peaks.begin() + static_cast(std::floor(peakIndex * factor)); + const auto endRightAggregationAt = m_right_peaks.begin() + static_cast(std::ceil((peakIndex + 1) * factor)); + right_peaks[peakIndex] = std::accumulate(beginRightAggregationAt, endRightAggregationAt, Peak{}); - return Thumbnail{std::move(peaks), m_samplesPerPeak * factor}; + const auto beginLeftAggregationAt = m_left_peaks.begin() + static_cast(std::floor(peakIndex * factor)); + const auto endLeftAggregationAt = m_left_peaks.begin() + static_cast(std::ceil((peakIndex + 1) * factor)); + left_peaks[peakIndex] = std::accumulate(beginLeftAggregationAt, endLeftAggregationAt, Peak{}); + } + return Thumbnail{std::move(right_peaks), std::move(left_peaks), m_samplesPerPeak * factor}; } SampleThumbnail::SampleThumbnail(const Sample& sample) @@ -94,9 +120,7 @@ SampleThumbnail::SampleThumbnail(const Sample& sample) s_sampleThumbnailCacheMap[std::move(entry)] = m_thumbnailCache; } - const auto flatBuffer = m_buffer->data()->data(); - const auto flatBufferSize = m_buffer->size() * DEFAULT_CHANNELS; - m_thumbnailCache->emplace_back(flatBuffer, flatBufferSize, flatBufferSize / AggregationPerZoomStep); + m_thumbnailCache->emplace_back(m_buffer.get(), m_buffer->size() / AggregationPerZoomStep); while (m_thumbnailCache->back().width() >= AggregationPerZoomStep) { @@ -141,7 +165,11 @@ void SampleThumbnail::visualize(VisualizeParameters parameters, QPainter& painte { if (useOriginalBuffer && drawOriginalBuffer) { - const auto value = m_buffer->data()->data()[i]; + const auto value = ( + parameters.waveType == Type::RIGHT ? m_buffer->data()[i][0] : + parameters.waveType == Type::LEFT ? m_buffer->data()[i][1] : + (m_buffer->data()[i][0] + m_buffer->data()[i][1]) / 2 + ); painter.drawPoint(x, renderRect.center().y() - value * yScale); continue; } @@ -155,16 +183,40 @@ void SampleThumbnail::visualize(VisualizeParameters parameters, QPainter& painte if (useOriginalBuffer) { - const auto flatBuffer = m_buffer->data()->data(); - const auto [min, max] = std::minmax_element(flatBuffer + beginIndex, flatBuffer + endIndex); - minPeak = *min; - maxPeak = *max; + const auto frameBuffer = m_buffer->data(); + + if (parameters.waveType == RIGHT) + { + const auto [min, max] = std::minmax_element(frameBuffer + beginIndex, frameBuffer + endIndex, + [](const SampleFrame& a, const SampleFrame& b){ return a.right() < b.right(); } + ); + minPeak = min->right(); + maxPeak = max->right(); + } + else if (parameters.waveType == LEFT) + { + const auto [min, max] = std::minmax_element(frameBuffer + beginIndex, frameBuffer + endIndex, + [](const SampleFrame& a, const SampleFrame& b){ return a.left() < b.left(); } + ); + minPeak = min->left(); + maxPeak = max->left(); + } + else + { + const auto flatBuffer = frameBuffer->data(); + const auto [min, max] = std::minmax_element(flatBuffer + (2 * beginIndex), flatBuffer + (2 * endIndex)); + minPeak = *min; + maxPeak = *max; + } } else { - const auto beginAggregationAt = finerThumbnail->data() + beginIndex; - const auto endAggregationAt = finerThumbnail->data() + endIndex; - const auto peak = std::accumulate(beginAggregationAt, endAggregationAt, Thumbnail::Peak{}); + const Thumbnail::Peak* peaks = ( + parameters.waveType == Type::RIGHT ? finerThumbnail->right() : + parameters.waveType == Type::LEFT ? finerThumbnail->left() : + finerThumbnail->mono() + ); + const auto peak = std::accumulate(peaks + beginIndex, peaks + endIndex, Thumbnail::Peak{}); minPeak = peak.min; maxPeak = peak.max; } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index f9aaa646839..72ae66d8bb2 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -26,17 +26,20 @@ #include #include +#include #include -#include "FileDialog.h" -#include "GuiApplication.h" #include "AutomationEditor.h" +#include "ConfigManager.h" #include "embed.h" +#include "FileDialog.h" +#include "GuiApplication.h" #include "PathUtil.h" #include "SampleClip.h" #include "SampleThumbnail.h" #include "Song.h" #include "StringPairDrag.h" +#include "Track.h" #include "TrackContainerView.h" #include "TrackView.h" @@ -284,18 +287,34 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) const auto& sample = m_clip->m_sample; - const auto sampleRextX = static_cast(offsetStart) - m_paintPixmapXPosition; + const auto sampleRectX = static_cast(offsetStart) - m_paintPixmapXPosition; if (sample.sampleSize() > 0) { - const auto param = SampleThumbnail::VisualizeParameters{ - .sampleRect = QRect(sampleRextX, spacing, sampleLength, height() - spacing), + auto param = SampleThumbnail::VisualizeParameters{ .viewportRect = viewPortRect, .amplification = sample.amplification(), .reversed = sample.reversed() }; + const QString policy = ConfigManager::inst()->value("app", "stereochannelspolicy", "height"); + if (policy == "never" || (policy == "height" && height() < DEFAULT_TRACK_HEIGHT * 2)) + { + param.sampleRect = QRect(sampleRectX, spacing, sampleLength, height() - spacing); + param.waveType = SampleThumbnail::Type::MONO; + m_sampleThumbnail.visualize(param, p); + } + else + { + const int halfHeight = height() / 2; - m_sampleThumbnail.visualize(param, p); + param.sampleRect = QRect(sampleRectX, spacing, sampleLength, halfHeight - spacing); + param.waveType = SampleThumbnail::Type::LEFT; + m_sampleThumbnail.visualize(param, p); + + param.sampleRect = QRect(sampleRectX, halfHeight + spacing, sampleLength, height() - spacing); + param.waveType = SampleThumbnail::Type::RIGHT; + m_sampleThumbnail.visualize(param, p); + } } QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index a6ba585c7de..a65de053e71 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -121,6 +121,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : "app", "openlastproject").toInt()), m_detachBehavior{ConfigManager::inst()->value("ui", "detachbehavior", "show")}, m_loopMarkerMode{ConfigManager::inst()->value("app", "loopmarkermode", "dual")}, + m_stereoChannelsPolicy{ConfigManager::inst()->value("app", "stereochannelspolicy", "height")}, m_autoScroll(ConfigManager::inst()->value("ui", "autoscroll", "stepped")), m_lang(ConfigManager::inst()->value( "app", "language")), @@ -284,6 +285,19 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : guiGroupLayout->addWidget(new QLabel{tr("Loop edit mode"), guiGroupBox}); guiGroupLayout->addWidget(m_loopMarkerComboBox); + m_stereoChannelsComboBox = new QComboBox{guiGroupBox}; + + m_stereoChannelsComboBox->addItem(tr("Depending on track height"), "height"); + m_stereoChannelsComboBox->addItem(tr("Always"), "always"); + m_stereoChannelsComboBox->addItem(tr("Never"), "never"); + + m_stereoChannelsComboBox->setCurrentIndex(m_stereoChannelsComboBox->findData(m_stereoChannelsPolicy)); + connect(m_stereoChannelsComboBox, qOverload(&QComboBox::currentIndexChanged), + this, &SetupDialog::stereoChannelsPolicyChanged); + + guiGroupLayout->addWidget(new QLabel{tr("Show samples stereo channels"), guiGroupBox}); + guiGroupLayout->addWidget(m_stereoChannelsComboBox); + m_autoScrollComboBox = new QComboBox{guiGroupBox}; m_autoScrollComboBox->addItem(tr("Disabled"), TimeLineWidget::AutoScrollDisabledString); m_autoScrollComboBox->addItem(tr("Stepped (Scroll once the playhead goes out of view)"), TimeLineWidget::AutoScrollSteppedString); @@ -1009,6 +1023,7 @@ void SetupDialog::accept() QString::number(m_openLastProject)); ConfigManager::inst()->setValue("ui", "detachbehavior", m_detachBehavior); ConfigManager::inst()->setValue("app", "loopmarkermode", m_loopMarkerMode); + ConfigManager::inst()->setValue("app", "stereochannelspolicy", m_stereoChannelsPolicy); ConfigManager::inst()->setValue("app", "language", m_lang); ConfigManager::inst()->setValue("ui", "autoscroll", m_autoScroll); ConfigManager::inst()->setValue("ui", "saveinterval", @@ -1160,6 +1175,12 @@ void SetupDialog::loopMarkerModeChanged() } +void SetupDialog::stereoChannelsPolicyChanged() +{ + m_stereoChannelsPolicy = m_stereoChannelsComboBox->currentData().toString(); +} + + void SetupDialog::setLanguage(int lang) { m_lang = m_languages[lang];