From 31cac25a998fcde0a10a38effdab25a46239a90d Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 26 Oct 2021 12:52:32 +0200 Subject: [PATCH 1/8] Beats: Add initial read-only support for downbeats --- src/track/beats.cpp | 8 ++++++++ src/track/beats.h | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/track/beats.cpp b/src/track/beats.cpp index d8600979c2b..a184093ac63 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -25,6 +25,14 @@ constexpr double kEpsilon = 0.01; namespace mixxx { +int Beats::ConstIterator::beatsPerBar() const { + if (m_it == m_beats->m_markers.cend()) { + return m_beats->lastBeatsPerBar(); + } + + return m_it->beatsPerBar(); +} + mixxx::audio::FrameDiff_t Beats::ConstIterator::beatLengthFrames() const { if (m_it == m_beats->m_markers.cend()) { return m_beats->lastBeatLengthFrames(); diff --git a/src/track/beats.h b/src/track/beats.h index 3edf87c1146..2eb577f0d14 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -32,14 +32,29 @@ class BeatMarker { DEBUG_ASSERT(m_beatsTillNextMarker > 0); } + /// Return the position of this beat marker. mixxx::audio::FramePos position() const { return m_position; } + /// Returns the number of beats between this marker and the next one. + /// + /// The length of an individual beat can be calculated by subtracting the + /// marker positions and dividing the result by the return value of this + /// function. int beatsTillNextMarker() const { return m_beatsTillNextMarker; } + /// Returns the number of beats per bar. + /// + /// Note: Currently always returns 4, as time signatures other than 4/4 + /// are not supported yet. + int beatsPerBar() const { + // TODO: Add support for different time signatures. + return 4; + } + private: mixxx::audio::FramePos m_position; int m_beatsTillNextMarker; @@ -83,8 +98,25 @@ class Beats : private std::enable_shared_from_this { updateValue(); } + /// Returns the frame duration between this beat and the next beat. mixxx::audio::FrameDiff_t beatLengthFrames() const; + /// Returns the number of beats per bar. + /// + /// Note: Currently always returns 4, as time signatures other than 4/4 + /// are not supported yet. + int beatsPerBar() const; + + /// Returns the current index of the beat in the bar. + int beatIndexInBar() const { + return m_beatOffset % beatsPerBar(); + } + + /// Returns true if this beat is a downbeat. + bool isDownbeat() const { + return beatIndexInBar() == 0; + } + // Iterator methods const value_type& operator*() const { @@ -430,6 +462,15 @@ class Beats : private std::enable_shared_from_this { mixxx::audio::FrameDiff_t firstBeatLengthFrames() const; mixxx::audio::FrameDiff_t lastBeatLengthFrames() const; + /// Returns the number of beats per bar of the last marker. + /// + /// Note: Currently always returns 4, as time signatures other than 4/4 + /// are not supported yet. + int lastBeatsPerBar() const { + // TODO: Add support for different time signatures. + return 4; + } + std::vector m_markers; mixxx::audio::FramePos m_lastMarkerPosition; mixxx::Bpm m_lastMarkerBpm; From 0e6dd48c362f7985d6f8675dc7ea6a9f38acba8a Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 9 Nov 2021 22:35:43 +0100 Subject: [PATCH 2/8] Beats: Add method to get the iterator to a beat close to a position --- src/track/beats.cpp | 16 ++++++++++++++++ src/track/beats.h | 1 + 2 files changed, 17 insertions(+) diff --git a/src/track/beats.cpp b/src/track/beats.cpp index a184093ac63..c6e7fe87131 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -474,6 +474,22 @@ Beats::ConstIterator Beats::iteratorFrom(audio::FramePos position) const { return it; } +Beats::ConstIterator Beats::iteratorClosestTo(audio::FramePos position) const { + auto it = iteratorFrom(position); + if (it == cbegin()) { + return it; + } + + const auto deltaFrames = *it - position; + it--; + if ((position - *it) < deltaFrames) { + return it; + } + + it++; + return it; +} + audio::FramePos Beats::findNthBeat(audio::FramePos position, int n) const { if (n == 0) { return audio::kInvalidFramePos; diff --git a/src/track/beats.h b/src/track/beats.h index 2eb577f0d14..fd80406583f 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -245,6 +245,7 @@ class Beats : private std::enable_shared_from_this { } ConstIterator iteratorFrom(audio::FramePos position) const; + ConstIterator iteratorClosestTo(audio::FramePos position) const; friend bool operator==(const Beats& lhs, const Beats& rhs) { return lhs.m_markers == rhs.m_markers && From cbabd52b1ed47166cf6eee2645c810dc3eb9d537 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 7 Nov 2021 18:52:14 +0100 Subject: [PATCH 3/8] BpmControl: Add Beats editing controls --- src/engine/controls/bpmcontrol.cpp | 93 +++++++++--- src/engine/controls/bpmcontrol.h | 4 + src/skin/legacy/tooltips.cpp | 14 +- src/track/beats.cpp | 234 +++++++++++++++++++++++++++++ src/track/beats.h | 27 ++++ 5 files changed, 351 insertions(+), 21 deletions(-) diff --git a/src/engine/controls/bpmcontrol.cpp b/src/engine/controls/bpmcontrol.cpp index 444752db10d..8ee6fa18328 100644 --- a/src/engine/controls/bpmcontrol.cpp +++ b/src/engine/controls/bpmcontrol.cpp @@ -22,8 +22,6 @@ constexpr double kBpmRangeMax = 200.0; constexpr double kBpmRangeStep = 1.0; constexpr double kBpmRangeSmallStep = 0.1; -constexpr double kBpmAdjustMin = kBpmRangeMin; -constexpr double kBpmAdjustStep = 0.01; constexpr double kBpmTabRounding = 1 / 12.0; // Maximum allowed interval between beats (calculated from kBpmTapMin). @@ -93,6 +91,18 @@ BpmControl::BpmControl(const QString& group, this, &BpmControl::slotTranslateBeatsMove, Qt::DirectConnection); + m_pBeatsSetMarker = new ControlPushButton(ConfigKey(group, "beats_set_marker"), false); + connect(m_pBeatsSetMarker, + &ControlObject::valueChanged, + this, + &BpmControl::slotBeatsSetMarker, + Qt::DirectConnection); + m_pBeatsRemoveMarker = new ControlPushButton(ConfigKey(group, "beats_remove_marker"), false); + connect(m_pBeatsRemoveMarker, + &ControlObject::valueChanged, + this, + &BpmControl::slotBeatsRemoveMarker, + Qt::DirectConnection); // Pick a wide range (kBpmRangeMin to kBpmRangeMax) and allow out of bounds sets. This lets you // map a soft-takeover MIDI knob to the BPM. This also creates bpm_up and @@ -144,6 +154,8 @@ BpmControl::~BpmControl() { delete m_pTranslateBeatsEarlier; delete m_pTranslateBeatsLater; delete m_pTranslateBeatsMove; + delete m_pBeatsSetMarker; + delete m_pBeatsRemoveMarker; delete m_pAdjustBeatsFaster; delete m_pAdjustBeatsSlower; } @@ -152,7 +164,11 @@ mixxx::Bpm BpmControl::getBpm() const { return mixxx::Bpm(m_pEngineBpm->get()); } -void BpmControl::adjustBeatsBpm(double deltaBpm) { +void BpmControl::slotAdjustBeatsFaster(double v) { + if (v <= 0) { + return; + } + const TrackPointer pTrack = getEngineBuffer()->getLoadedTrack(); if (!pTrack) { return; @@ -162,31 +178,32 @@ void BpmControl::adjustBeatsBpm(double deltaBpm) { return; } - const mixxx::Bpm bpm = pBeats->getBpmInRange( - mixxx::audio::kStartFramePos, frameInfo().trackEndPosition); - // FIXME: calling bpm.value() without checking bpm.isValid() - const auto centerBpm = mixxx::Bpm(math_max(kBpmAdjustMin, bpm.value() + deltaBpm)); - mixxx::Bpm adjustedBpm = BeatUtils::roundBpmWithinRange( - centerBpm - kBpmAdjustStep / 2, centerBpm, centerBpm + kBpmAdjustStep / 2); - const auto newBeats = pBeats->trySetBpm(adjustedBpm); - if (!newBeats) { - return; + const auto adjustedBeats = pBeats->tryAdjustTempo( + frameInfo().currentPosition, mixxx::Beats::TempoAdjustment::Faster); + if (adjustedBeats) { + pTrack->trySetBeats(*adjustedBeats); } - pTrack->trySetBeats(*newBeats); } -void BpmControl::slotAdjustBeatsFaster(double v) { +void BpmControl::slotAdjustBeatsSlower(double v) { if (v <= 0) { return; } - adjustBeatsBpm(kBpmAdjustStep); -} -void BpmControl::slotAdjustBeatsSlower(double v) { - if (v <= 0) { + const TrackPointer pTrack = getEngineBuffer()->getLoadedTrack(); + if (!pTrack) { + return; + } + const mixxx::BeatsPointer pBeats = pTrack->getBeats(); + if (!pBeats) { return; } - adjustBeatsBpm(-kBpmAdjustStep); + + const auto adjustedBeats = pBeats->tryAdjustTempo( + frameInfo().currentPosition, mixxx::Beats::TempoAdjustment::Slower); + if (adjustedBeats) { + pTrack->trySetBeats(*adjustedBeats); + } } void BpmControl::slotTranslateBeatsEarlier(double v) { @@ -224,6 +241,44 @@ void BpmControl::slotTranslateBeatsMove(double v) { } } +void BpmControl::slotBeatsSetMarker(double v) { + if (v <= 0) { + return; + } + const TrackPointer pTrack = getEngineBuffer()->getLoadedTrack(); + if (!pTrack) { + return; + } + const mixxx::BeatsPointer pBeats = pTrack->getBeats(); + if (!pBeats) { + return; + } + + const auto modifiedBeats = pBeats->trySetMarker(frameInfo().currentPosition); + if (modifiedBeats) { + pTrack->trySetBeats(*modifiedBeats); + } +} + +void BpmControl::slotBeatsRemoveMarker(double v) { + if (v <= 0) { + return; + } + const TrackPointer pTrack = getEngineBuffer()->getLoadedTrack(); + if (!pTrack) { + return; + } + const mixxx::BeatsPointer pBeats = pTrack->getBeats(); + if (!pBeats) { + return; + } + + const auto modifiedBeats = pBeats->tryRemoveMarker(frameInfo().currentPosition); + if (modifiedBeats) { + pTrack->trySetBeats(*modifiedBeats); + } +} + void BpmControl::slotBpmTap(double v) { if (v > 0) { m_tapFilter.tap(); diff --git a/src/engine/controls/bpmcontrol.h b/src/engine/controls/bpmcontrol.h index e08a3164088..a93c24eeb9f 100644 --- a/src/engine/controls/bpmcontrol.h +++ b/src/engine/controls/bpmcontrol.h @@ -102,6 +102,8 @@ class BpmControl : public EngineControl { void slotTranslateBeatsEarlier(double); void slotTranslateBeatsLater(double); void slotTranslateBeatsMove(double); + void slotBeatsSetMarker(double); + void slotBeatsRemoveMarker(double); void slotTapFilter(double,int); void slotBpmTap(double); void slotUpdateRateSlider(double v = 0.0); @@ -144,6 +146,8 @@ class BpmControl : public EngineControl { ControlPushButton* m_pTranslateBeatsEarlier; ControlPushButton* m_pTranslateBeatsLater; ControlEncoder* m_pTranslateBeatsMove; + ControlPushButton* m_pBeatsSetMarker; + ControlPushButton* m_pBeatsRemoveMarker; // The current effective BPM of the engine ControlLinPotmeter* m_pEngineBpm; diff --git a/src/skin/legacy/tooltips.cpp b/src/skin/legacy/tooltips.cpp index 388f8b49da8..c6bd4b6cf80 100644 --- a/src/skin/legacy/tooltips.cpp +++ b/src/skin/legacy/tooltips.cpp @@ -393,13 +393,23 @@ void Tooltips::addStandardTooltips() { << tr("BPM Tap") << tr("When tapped repeatedly, adjusts the BPM to match the tapped BPM."); + add("beats_set_marker") + << tr("Set Beat Marker") + << tr("Set a beat marker at the current play position."); + + add("beats_remove_marker") + << tr("Remove Beat Marker") + << tr("Remove the beat marker at the current play position."); + add("beats_adjust_slower") << tr("Adjust BPM Down") - << tr("When tapped, adjusts the average BPM down by a small amount."); + << tr("When tapped, decrease the BPM of the region around the " + "current play position by a small amount."); add("beats_adjust_faster") << tr("Adjust BPM Up") - << tr("When tapped, adjusts the average BPM up by a small amount."); + << tr("When tapped, increases the BPM of the region around the " + "current play position by a small amount."); add("beats_translate_earlier") << tr("Adjust Beats Earlier") diff --git a/src/track/beats.cpp b/src/track/beats.cpp index c6e7fe87131..251144665c4 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -21,6 +21,36 @@ struct BeatGridV1Data { constexpr double kEpsilon = 0.01; +// The amount that `Beats::tryAdjustTempo()` changes the last marker's BPM by. +constexpr double kBpmAdjustStep = 0.01; + +int roundBeatCountToFullBar(int numBeats, int beatsPerBar) { + const int numExcessBeats = (numBeats % beatsPerBar); + if (numExcessBeats < (beatsPerBar / 2)) { + return numBeats - numExcessBeats; + } + + return numBeats + beatsPerBar - numExcessBeats; +} + +int calculateBeatsTillNextMarker(const mixxx::Beats::ConstIterator& it, + mixxx::audio::FramePos nextMarkerPosition) { + const double numBeatsDouble = std::fabs(nextMarkerPosition - *it) / it.beatLengthFrames(); + const int numBeatsInt = static_cast(std::round(numBeatsDouble)); + + return roundBeatCountToFullBar(numBeatsInt, it.beatsPerBar()); +} + +bool isBeatMarkerLessThanFramePos(const mixxx::BeatMarker& marker, + const mixxx::audio::FramePos& position) { + return marker.position() < position; +} + +bool isFramePosLessThanBeatMarker(const mixxx::audio::FramePos& position, + const mixxx::BeatMarker& marker) { + return position < marker.position(); +} + } // namespace namespace mixxx { @@ -702,6 +732,210 @@ std::optional Beats::tryScale(BpmScale scale) const { m_subVersion)); } +std::optional Beats::tryAdjustTempo( + audio::FramePos position, TempoAdjustment adjustment) const { + auto markers = m_markers; + + // Retrieve the Beat Marker before the current position. + // + // Hence, we first get the marker *after* the position, then decrement the + // iterator. + // If there is no marker before the current position, the marker after the + // position can be used because the beats before the first marker are + // interpolated using the same beat length as the beats after the first + // marker. + auto markerIt = std::upper_bound(markers.begin(), + markers.end(), + position, + isFramePosLessThanBeatMarker); + if (markerIt != markers.begin() && position < m_lastMarkerPosition) { + markerIt--; + } + + // At this point, `markerIt` points to the beat marker that we need to + // modify (or `cend()`, which means that we need to modify the BPM of the + // last marker). + Bpm lastMarkerBpm = m_lastMarkerBpm; + if (markerIt == markers.end()) { + lastMarkerBpm += (adjustment == TempoAdjustment::Faster) ? kBpmAdjustStep : -kBpmAdjustStep; + if (!lastMarkerBpm.isValid()) { + qWarning() << "Beats: Tempo adjustment would result in invalid Bpm!"; + return std::nullopt; + } + } else { + const auto marker = *markerIt; + const int adjustedBeatsTillNextMarker = marker.beatsTillNextMarker() + + ((adjustment == TempoAdjustment::Faster) + ? marker.beatsPerBar() + : -marker.beatsPerBar()); + + if (adjustedBeatsTillNextMarker < marker.beatsPerBar()) { + qWarning() << "Beats: Tempo adjustment would result in a marker " + "with less than the minimum beats in one bar!"; + return std::nullopt; + } + + markerIt = markers.erase(markerIt); + markerIt = markers.emplace(markerIt, + marker.position(), + adjustedBeatsTillNextMarker); + } + + return fromBeatMarkers(m_sampleRate, std::move(markers), m_lastMarkerPosition, lastMarkerBpm); +} + +std::optional Beats::trySetMarker(audio::FramePos position) const { + auto markers = m_markers; + auto markerIt = std::lower_bound(markers.begin(), + markers.end(), + position, + isBeatMarkerLessThanFramePos); + + auto lastMarkerPosition = m_lastMarkerPosition; + if (markerIt == markers.end()) { + const int numBeats = calculateBeatsTillNextMarker( + iteratorClosestTo(position), lastMarkerPosition); + if (numBeats == 0) { + // The new marker would be too close to the existing marker, so we + // just move the existing marker instead. + lastMarkerPosition = position.toLowerFrameBoundary(); + } else if (lastMarkerPosition < position) { + // We are behind the last marker. We convert the current last + // marker into a regular beat marker and use the current position + // as the new last marker. + markers.push_back(mixxx::BeatMarker(lastMarkerPosition, numBeats)); + lastMarkerPosition = position.toLowerFrameBoundary(); + } else { + // We are between the last regular beat marker and the last marker, + // so we can just append a new beat marker. + markers.push_back(mixxx::BeatMarker(position.toLowerFrameBoundary(), numBeats)); + } + } else if (markerIt == markers.begin()) { + const int numBeats = calculateBeatsTillNextMarker( + iteratorClosestTo(position), markerIt->position()); + if (numBeats == 0) { + // The new marker would be too close to the existing marker, so we + // need to move the existing marker instead. + const auto marker = *markerIt; + markerIt = markers.erase(markerIt); + markerIt = markers.emplace(markerIt, + position.toLowerFrameBoundary(), + marker.beatsTillNextMarker()); + } else { + // We are in front of the first regular beat marker, so we can just + // prepend a new beat marker. + markers.emplace(markerIt, position.toLowerFrameBoundary(), numBeats); + } + } else { + markerIt--; + + const int numBeats = calculateBeatsTillNextMarker( + iteratorClosestTo(position), markerIt->position()); + if (numBeats == 0) { + // The new marker would be too close to the existing marker, so we + // need to move the existing marker instead. + const auto marker = *markerIt; + markerIt = markers.erase(markerIt); + markerIt = markers.emplace(markerIt, + position.toLowerFrameBoundary(), + marker.beatsTillNextMarker()); + } else { + // The new marker would be placed between two regular beat markers. + // This means we need to update the `beatsTillNextMarker` value of + // the preceding marker, then insert the new beat marker. + const auto marker = *markerIt; + + // First we update `beatsTillnextMarker` of the preceding marker. + markerIt = markers.erase(markerIt); + markerIt = markers.emplace(markerIt, + marker.position(), + marker.beatsTillNextMarker() - numBeats); + + // Now we can insert new beat marker at the desired position. + markers.emplace(markerIt, position.toLowerFrameBoundary(), numBeats); + } + } + + return fromBeatMarkers(m_sampleRate, std::move(markers), lastMarkerPosition, m_lastMarkerBpm); +} + +std::optional Beats::tryRemoveMarker(audio::FramePos position) const { + if (m_markers.empty()) { + // There are no markers beside the mandatory last marker, so there is + // nothing to remove. + return std::nullopt; + } + + auto it = iteratorClosestTo(position); + if (!it.isMarker()) { + // The position is not near a marker, don't remove anything. + return std::nullopt; + } + DEBUG_ASSERT(it != cbegin()); + + auto lastMarkerBpm = m_lastMarkerBpm; + auto lastMarkerPosition = m_lastMarkerPosition; + auto markers = m_markers; + if (it == clastmarker()) { + // We are near the last marker. It's not possible to actually remove + // it, so we remove the marker *before* the last marker instead, and + // use it's BPM and position values for the last marker. The end result + // is the same. + it--; + lastMarkerBpm = mixxx::Bpm(60.0 * m_sampleRate / it.beatLengthFrames()); + lastMarkerPosition = markers.back().position(); + markers.pop_back(); + } else { + auto markerIt = std::lower_bound(markers.begin(), + markers.end(), + position, + isBeatMarkerLessThanFramePos); + + // At this point we already checked that the search position is on a + // marker and that it is not at the last marker. This means that + // `markerIt` should point to a marker in any case. + VERIFY_OR_DEBUG_ASSERT(markerIt != markers.end()) { + return std::nullopt; + } + + markerIt = markers.erase(markerIt); + + // If the marker we just deleted was the first marker, we don't + // need to modify anything else. In that case `markerIt` now points + // to the *new* first marker. + if (markerIt != markers.begin()) { + // However, when removing markers other than the first one, we also need to + // update the `beatsTillNextMarker` value of the marker before + // it, to ensure that the beat length stays roughly the same. + // + // To do that, we use the old beat length in the section between + // the marker we just removed and the marker before it, then use + // that to calculate how many beats the new section show have. + const auto endPosition = (markerIt != markers.end()) + ? markerIt->position() + : lastMarkerPosition; + markerIt--; + + // `it` currently points at the beat directly at the beat marker + // that we just removed. By decrementing it, we make sure that it + // points at the beat in the region *before* the marker. This means + // we can use it's `beatLengthFrames` and `beatsPerBar` properties + // for our calculations. + it--; + const double numBeatsDouble = (endPosition - markerIt->position()) / + it.beatLengthFrames(); + const int numBeatsInt = static_cast(std::round(numBeatsDouble)); + const int numBeats = roundBeatCountToFullBar(numBeatsInt, it.beatsPerBar()); + + const auto marker = *markerIt; + markerIt = markers.erase(markerIt); + markerIt = markers.emplace(markerIt, marker.position(), numBeats); + } + } + + return fromBeatMarkers(m_sampleRate, std::move(markers), lastMarkerPosition, lastMarkerBpm); +} + std::optional Beats::trySetBpm(mixxx::Bpm bpm) const { const auto it = cfirstmarker(); return BeatsPointer(new Beats({}, *it, bpm, m_sampleRate, m_subVersion)); diff --git a/src/track/beats.h b/src/track/beats.h index fd80406583f..fb73816b957 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -306,6 +306,11 @@ class Beats : private std::enable_shared_from_this { ThreeHalves, }; + enum class TempoAdjustment { + Faster, + Slower, + }; + /// Returns false if the beats implementation supports non-const beats. /// /// TODO: This is only needed for the "Asumme Constant Tempo" checkbox in @@ -437,6 +442,28 @@ class Beats : private std::enable_shared_from_this { /// failure. std::optional tryScale(BpmScale scale) const; + /// Adjust the tempo of the region around `position`. The direction is + /// determined by `adjustment`. + // + /// Returns a pointer to the modified beats object, or `nullopt` on + /// failure. + std::optional tryAdjustTempo( + audio::FramePos position, TempoAdjustment adjustment) const; + + /// Insert a new beat marker at `position`, or move a close existing marker + /// to `position`. + // + /// Returns a pointer to the modified beats object, or `nullopt` on + /// failure. + std::optional trySetMarker(audio::FramePos position) const; + + /// Remove the beat marker near `position`. The preceding beat marker is + /// updated, so that the beat length roughly stays the same. + // + /// Returns a pointer to the modified beats object, or `nullopt` on + /// failure. + std::optional tryRemoveMarker(audio::FramePos position) const; + /// Adjust the beats so the global average BPM matches `bpm`. // /// Returns a pointer to the modified beats object, or `nullopt` on From 65441e95e45f058911d094877f16507395cf9623 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 26 Oct 2021 12:53:45 +0200 Subject: [PATCH 4/8] WaveformRenderBeat: Add support for rendering downbeats --- src/waveform/renderers/waveformrenderbeat.cpp | 60 +++++++++++++------ src/waveform/renderers/waveformrenderbeat.h | 2 + 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/waveform/renderers/waveformrenderbeat.cpp b/src/waveform/renderers/waveformrenderbeat.cpp index ba3c4e3919d..2700cb3cfdb 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -11,7 +11,8 @@ class QPaintEvent; WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidgetRenderer) : WaveformRendererAbstract(waveformWidgetRenderer) { - m_beats.resize(128); + m_beats.reserve(128); + m_downbeats.reserve(32); } WaveformRenderBeat::~WaveformRenderBeat() { @@ -20,6 +21,9 @@ WaveformRenderBeat::~WaveformRenderBeat() { void WaveformRenderBeat::setup(const QDomNode& node, const SkinContext& context) { m_beatColor = QColor(context.selectString(node, "BeatColor")); m_beatColor = WSkinColor::getCorrectColor(m_beatColor).toRgb(); + + m_downbeatColor.setNamedColor(context.selectString(node, "DownbeatColor")); + m_downbeatColor = WSkinColor::getCorrectColor(m_downbeatColor).toRgb(); } void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { @@ -43,8 +47,10 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { // drawing with QPainter on the QOpenGLWindow: instead of individual lines // a large rectangle encompassing all beatlines is drawn. m_beatColor.setAlphaF(1.f); + m_downbeatColor.setAlphaF(1.f); #else m_beatColor.setAlphaF(alpha/100.0); + m_downbeatColor.setAlphaF(alpha / 100.0); #endif const double trackSamples = m_waveformRenderer->getTrackSamples(); @@ -72,19 +78,12 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { return; } - PainterScope PainterScope(painter); - - painter->setRenderHint(QPainter::Antialiasing); - - QPen beatPen(m_beatColor); - beatPen.setWidthF(std::max(1.0, scaleFactor())); - painter->setPen(beatPen); - const Qt::Orientation orientation = m_waveformRenderer->getOrientation(); const float rendererWidth = m_waveformRenderer->getWidth(); const float rendererHeight = m_waveformRenderer->getHeight(); - int beatCount = 0; + m_beats.clear(); + m_downbeats.clear(); for (; it != trackBeats->cend() && *it <= endPosition; ++it) { double beatPosition = it->toEngineSamplePos(); @@ -93,18 +92,41 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { xBeatPoint = qRound(xBeatPoint); - // If we don't have enough space, double the size. - if (beatCount >= m_beats.size()) { - m_beats.resize(m_beats.size() * 2); - } - + QLineF line; if (orientation == Qt::Horizontal) { - m_beats[beatCount++].setLine(xBeatPoint, 0.0f, xBeatPoint, rendererHeight); + line.setLine(xBeatPoint, 0.0f, xBeatPoint, rendererHeight); } else { - m_beats[beatCount++].setLine(0.0f, xBeatPoint, rendererWidth, xBeatPoint); + line.setLine(0.0f, xBeatPoint, rendererWidth, xBeatPoint); + } + + auto& lines = it.isDownbeat() ? m_downbeats : m_beats; + + // If we don't have enough space, double the capacity. + if (lines.size() == lines.capacity()) { + lines.reserve(lines.capacity() * 2); } + + lines.append(line); + } + + PainterScope PainterScope(painter); + painter->setRenderHint(QPainter::Antialiasing); + + if (!m_beats.isEmpty()) { + QPen pen(m_beatColor); + pen.setWidthF(std::max(1.0, scaleFactor())); + painter->setPen(pen); + + // Make sure to use constData to prevent detaches! + painter->drawLines(m_beats.constData(), m_beats.size()); } - // Make sure to use constData to prevent detaches! - painter->drawLines(m_beats.constData(), beatCount); + if (!m_downbeats.isEmpty()) { + QPen pen(m_downbeatColor); + pen.setWidthF(std::max(1.0, scaleFactor())); + painter->setPen(pen); + + // Make sure to use constData to prevent detaches! + painter->drawLines(m_downbeats.constData(), m_downbeats.size()); + } } diff --git a/src/waveform/renderers/waveformrenderbeat.h b/src/waveform/renderers/waveformrenderbeat.h index 439b23c3e45..9d0f05c6671 100644 --- a/src/waveform/renderers/waveformrenderbeat.h +++ b/src/waveform/renderers/waveformrenderbeat.h @@ -15,8 +15,10 @@ class WaveformRenderBeat : public WaveformRendererAbstract { virtual void draw(QPainter* painter, QPaintEvent* event); private: + QColor m_downbeatColor; QColor m_beatColor; QVector m_beats; + QVector m_downbeats; DISALLOW_COPY_AND_ASSIGN(WaveformRenderBeat); }; From 7d0ca41135dbbbadfe1a3d4da71f5ff3e76394b7 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 26 Oct 2021 12:54:11 +0200 Subject: [PATCH 5/8] LateNight: Set downbeat color in skin.xml --- res/skins/LateNight/skin.xml | 1 + res/skins/LateNight/waveform.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/res/skins/LateNight/skin.xml b/res/skins/LateNight/skin.xml index 1b17d07cd6e..e1b814e144a 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -166,6 +166,7 @@ #999 #999 + #f00 #00c6ff #ff7a01 #00b400 diff --git a/res/skins/LateNight/waveform.xml b/res/skins/LateNight/waveform.xml index 2fb5db34968..ee84a84fd0a 100644 --- a/res/skins/LateNight/waveform.xml +++ b/res/skins/LateNight/waveform.xml @@ -25,6 +25,7 @@ + From 8f22757faa924009d7be27cffa8493e76a8b088b Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 26 Oct 2021 14:11:29 +0200 Subject: [PATCH 6/8] WaveformRenderBeat: Pass QVector directly to `drawLines()` --- src/waveform/renderers/waveformrenderbeat.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/waveform/renderers/waveformrenderbeat.cpp b/src/waveform/renderers/waveformrenderbeat.cpp index 2700cb3cfdb..6efcb6813a5 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -118,7 +118,7 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { painter->setPen(pen); // Make sure to use constData to prevent detaches! - painter->drawLines(m_beats.constData(), m_beats.size()); + painter->drawLines(m_beats); } if (!m_downbeats.isEmpty()) { @@ -127,6 +127,6 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { painter->setPen(pen); // Make sure to use constData to prevent detaches! - painter->drawLines(m_downbeats.constData(), m_downbeats.size()); + painter->drawLines(m_downbeats); } } From f0054fed6602b4f614b6259401e9bbb7de73352f Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 5 Nov 2021 23:41:01 +0100 Subject: [PATCH 7/8] Show markers on waveforms [WIP] --- res/skins/LateNight/skin.xml | 1 + res/skins/LateNight/waveform.xml | 1 + src/track/beats.h | 5 +++++ src/waveform/renderers/waveformrenderbeat.cpp | 20 ++++++++++++++----- src/waveform/renderers/waveformrenderbeat.h | 4 +++- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/res/skins/LateNight/skin.xml b/res/skins/LateNight/skin.xml index e1b814e144a..181ab510976 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -167,6 +167,7 @@ #999 #999 #f00 + #f0f #00c6ff #ff7a01 #00b400 diff --git a/res/skins/LateNight/waveform.xml b/res/skins/LateNight/waveform.xml index ee84a84fd0a..4a86a6194fd 100644 --- a/res/skins/LateNight/waveform.xml +++ b/res/skins/LateNight/waveform.xml @@ -26,6 +26,7 @@ + diff --git a/src/track/beats.h b/src/track/beats.h index fb73816b957..33d7c93e179 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -112,6 +112,11 @@ class Beats : private std::enable_shared_from_this { return m_beatOffset % beatsPerBar(); } + /// Returns true if this beat is at a marker position. + bool isMarker() const { + return m_beatOffset == 0; + } + /// Returns true if this beat is a downbeat. bool isDownbeat() const { return beatIndexInBar() == 0; diff --git a/src/waveform/renderers/waveformrenderbeat.cpp b/src/waveform/renderers/waveformrenderbeat.cpp index 6efcb6813a5..05547b3caaf 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -13,6 +13,7 @@ WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidgetRen : WaveformRendererAbstract(waveformWidgetRenderer) { m_beats.reserve(128); m_downbeats.reserve(32); + m_markerbeats.reserve(32); } WaveformRenderBeat::~WaveformRenderBeat() { @@ -24,6 +25,9 @@ void WaveformRenderBeat::setup(const QDomNode& node, const SkinContext& context) m_downbeatColor.setNamedColor(context.selectString(node, "DownbeatColor")); m_downbeatColor = WSkinColor::getCorrectColor(m_downbeatColor).toRgb(); + + m_markerbeatColor.setNamedColor(context.selectString(node, "MarkerbeatColor")); + m_markerbeatColor = WSkinColor::getCorrectColor(m_markerbeatColor).toRgb(); } void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { @@ -48,9 +52,11 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { // a large rectangle encompassing all beatlines is drawn. m_beatColor.setAlphaF(1.f); m_downbeatColor.setAlphaF(1.f); + m_markerbeatColor.setAlphaF(1.f); #else m_beatColor.setAlphaF(alpha/100.0); m_downbeatColor.setAlphaF(alpha / 100.0); + m_markerbeatColor.setAlphaF(alpha / 100.0); #endif const double trackSamples = m_waveformRenderer->getTrackSamples(); @@ -84,6 +90,7 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { m_beats.clear(); m_downbeats.clear(); + m_markerbeats.clear(); for (; it != trackBeats->cend() && *it <= endPosition; ++it) { double beatPosition = it->toEngineSamplePos(); @@ -99,7 +106,7 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { line.setLine(0.0f, xBeatPoint, rendererWidth, xBeatPoint); } - auto& lines = it.isDownbeat() ? m_downbeats : m_beats; + auto& lines = it.isMarker() ? m_markerbeats : (it.isDownbeat() ? m_downbeats : m_beats); // If we don't have enough space, double the capacity. if (lines.size() == lines.capacity()) { @@ -116,8 +123,6 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { QPen pen(m_beatColor); pen.setWidthF(std::max(1.0, scaleFactor())); painter->setPen(pen); - - // Make sure to use constData to prevent detaches! painter->drawLines(m_beats); } @@ -125,8 +130,13 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { QPen pen(m_downbeatColor); pen.setWidthF(std::max(1.0, scaleFactor())); painter->setPen(pen); - - // Make sure to use constData to prevent detaches! painter->drawLines(m_downbeats); } + + if (!m_markerbeats.isEmpty()) { + QPen pen(m_markerbeatColor); + pen.setWidthF(std::max(1.0, scaleFactor())); + painter->setPen(pen); + painter->drawLines(m_markerbeats); + } } diff --git a/src/waveform/renderers/waveformrenderbeat.h b/src/waveform/renderers/waveformrenderbeat.h index 9d0f05c6671..e7f54685d95 100644 --- a/src/waveform/renderers/waveformrenderbeat.h +++ b/src/waveform/renderers/waveformrenderbeat.h @@ -15,10 +15,12 @@ class WaveformRenderBeat : public WaveformRendererAbstract { virtual void draw(QPainter* painter, QPaintEvent* event); private: - QColor m_downbeatColor; QColor m_beatColor; + QColor m_downbeatColor; + QColor m_markerbeatColor; QVector m_beats; QVector m_downbeats; + QVector m_markerbeats; DISALLOW_COPY_AND_ASSIGN(WaveformRenderBeat); }; From d6c507075bb4cd09376f6d35f5db408c07d75895 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 7 Nov 2021 22:54:52 +0100 Subject: [PATCH 8/8] LateNight: Add buttons for setting/removing beats markers [WIP] --- .../buttons/btn__beats_remove_marker.svg | 2 ++ .../btn__beats_remove_marker_active.svg | 2 ++ ...os_large.svg => btn__beats_set_marker.svg} | 0 ...e.svg => btn__beats_set_marker_active.svg} | 0 res/skins/LateNight/style_palemoon.qss | 14 +++++++--- res/skins/LateNight/waveform.xml | 27 ++++++++++++------- 6 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker.svg create mode 100644 res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker_active.svg rename res/skins/LateNight/palemoon/buttons/{btn__beat_curpos_large.svg => btn__beats_set_marker.svg} (100%) rename res/skins/LateNight/palemoon/buttons/{btn__beat_curpos_large_active.svg => btn__beats_set_marker_active.svg} (100%) diff --git a/res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker.svg b/res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker.svg new file mode 100644 index 00000000000..86ac6e0a9a5 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker.svg @@ -0,0 +1,2 @@ + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker_active.svg b/res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker_active.svg new file mode 100644 index 00000000000..628d885e42f --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__beats_remove_marker_active.svg @@ -0,0 +1,2 @@ + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__beat_curpos_large.svg b/res/skins/LateNight/palemoon/buttons/btn__beats_set_marker.svg similarity index 100% rename from res/skins/LateNight/palemoon/buttons/btn__beat_curpos_large.svg rename to res/skins/LateNight/palemoon/buttons/btn__beats_set_marker.svg diff --git a/res/skins/LateNight/palemoon/buttons/btn__beat_curpos_large_active.svg b/res/skins/LateNight/palemoon/buttons/btn__beats_set_marker_active.svg similarity index 100% rename from res/skins/LateNight/palemoon/buttons/btn__beat_curpos_large_active.svg rename to res/skins/LateNight/palemoon/buttons/btn__beats_set_marker_active.svg diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index ca54d9f1ed4..a279a9d57c9 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -2322,11 +2322,17 @@ WPushButton#PlayDeck[value="0"] { image: url(skin:../LateNight/palemoon/buttons/btn__beatgrid_controls_collapse.svg) no-repeat center center; } - #BeatCurposLarge[displayValue="0"] { - image: url(skin:../LateNight/palemoon/buttons/btn__beat_curpos_large.svg) no-repeat center center; + #BeatsSetMarker[displayValue="0"] { + image: url(skin:../LateNight/palemoon/buttons/btn__beats_set_marker.svg) no-repeat center center; } - #BeatCurposLarge[pressed="true"] { - image: url(skin:../LateNight/palemoon/buttons/btn__beat_curpos_large_active.svg) no-repeat center center; + #BeatsSetMarker[pressed="true"] { + image: url(skin:../LateNight/palemoon/buttons/btn__beats_set_marker_active.svg) no-repeat center center; + } + #BeatsRemoveMarker[displayValue="0"] { + image: url(skin:../LateNight/palemoon/buttons/btn__beats_remove_marker.svg) no-repeat center center; + } + #BeatsRemoveMarker[pressed="true"] { + image: url(skin:../LateNight/palemoon/buttons/btn__beats_remove_marker_active.svg) no-repeat center center; } #BeatsEarlier { diff --git a/res/skins/LateNight/waveform.xml b/res/skins/LateNight/waveform.xml index 4a86a6194fd..fbe1354e18d 100644 --- a/res/skins/LateNight/waveform.xml +++ b/res/skins/LateNight/waveform.xml @@ -155,15 +155,24 @@ 1f,0min - - + + vertical + 26f,52f + + + + + vertical