From 40ddcda782c934b43db6f613739e0f76bcb50299 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 43936577926..f69a3d4230b 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -24,6 +24,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 43008b3bcb5..825432fb73e 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; @@ -81,8 +96,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 { @@ -427,6 +459,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 340326368c32f3efee4148a1475eea64655ce72e 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 f69a3d4230b..0f1db316885 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -460,6 +460,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 825432fb73e..ce79294a18c 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -242,6 +242,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 ebde151951e4531107379b706bddf23973926a9d 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 6fa0617040c..6adaadae736 100644 --- a/src/engine/controls/bpmcontrol.cpp +++ b/src/engine/controls/bpmcontrol.cpp @@ -27,8 +27,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). @@ -88,6 +86,18 @@ BpmControl::BpmControl(const QString& group, connect(m_pTranslateBeatsLater, &ControlObject::valueChanged, this, &BpmControl::slotTranslateBeatsLater, 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 @@ -139,6 +149,8 @@ BpmControl::~BpmControl() { delete m_pBeatsTranslateMatchAlignment; delete m_pTranslateBeatsEarlier; delete m_pTranslateBeatsLater; + delete m_pBeatsSetMarker; + delete m_pBeatsRemoveMarker; delete m_pAdjustBeatsFaster; delete m_pAdjustBeatsSlower; } @@ -147,7 +159,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; @@ -157,31 +173,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) { @@ -223,6 +240,44 @@ void BpmControl::slotTranslateBeatsLater(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 2c7f60e2794..f982380ed30 100644 --- a/src/engine/controls/bpmcontrol.h +++ b/src/engine/controls/bpmcontrol.h @@ -103,6 +103,8 @@ class BpmControl : public EngineControl { void slotAdjustBeatsSlower(double); void slotTranslateBeatsEarlier(double); void slotTranslateBeatsLater(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_pAdjustBeatsSlower; ControlPushButton* m_pTranslateBeatsEarlier; ControlPushButton* m_pTranslateBeatsLater; + 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 eb83cf461aa..d02e1e4b146 100644 --- a/src/skin/legacy/tooltips.cpp +++ b/src/skin/legacy/tooltips.cpp @@ -391,13 +391,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 0f1db316885..41a636e046e 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -20,6 +20,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 { @@ -688,6 +718,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 ce79294a18c..ffdaf0a56dd 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -303,6 +303,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 @@ -434,6 +439,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 c68580922f94ea9895ab40c91b1aa15711f313cc 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 | 59 +++++++++++++------ src/waveform/renderers/waveformrenderbeat.h | 2 + 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/waveform/renderers/waveformrenderbeat.cpp b/src/waveform/renderers/waveformrenderbeat.cpp index b4bc76ab906..567e81addaf 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -13,7 +13,8 @@ WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidgetRenderer) : WaveformRendererAbstract(waveformWidgetRenderer) { - m_beats.resize(128); + m_beats.reserve(128); + m_downbeats.reserve(32); } WaveformRenderBeat::~WaveformRenderBeat() { @@ -22,6 +23,9 @@ WaveformRenderBeat::~WaveformRenderBeat() { void WaveformRenderBeat::setup(const QDomNode& node, const SkinContext& context) { m_beatColor.setNamedColor(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*/) { @@ -41,6 +45,7 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { return; } m_beatColor.setAlphaF(alpha/100.0); + m_downbeatColor.setAlphaF(alpha / 100.0); const int trackSamples = m_waveformRenderer->getTrackSamples(); if (trackSamples <= 0) { @@ -67,19 +72,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(); @@ -88,18 +86,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 7348268ef10ef3c031ff2563bbd328326fe4aeb2 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 6757ad9704b..4ac8678b821 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -163,6 +163,7 @@ #999 #999 + #f00 #00c6ff #ff7a01 #00b400 diff --git a/res/skins/LateNight/waveform.xml b/res/skins/LateNight/waveform.xml index 004653fb9ed..7c427440f55 100644 --- a/res/skins/LateNight/waveform.xml +++ b/res/skins/LateNight/waveform.xml @@ -25,6 +25,7 @@ + From 5ab0bdee61b5dcf3ae90ff5ca0c4da88c7318671 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 567e81addaf..0ddc95c9e5f 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -112,7 +112,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()) { @@ -121,6 +121,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 0b15e33cce3db5d23b972314533e007f0e83c6ea 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 | 19 ++++++++++++++----- src/waveform/renderers/waveformrenderbeat.h | 4 +++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/res/skins/LateNight/skin.xml b/res/skins/LateNight/skin.xml index 4ac8678b821..16ca68090a8 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -164,6 +164,7 @@ #999 #999 #f00 + #f0f #00c6ff #ff7a01 #00b400 diff --git a/res/skins/LateNight/waveform.xml b/res/skins/LateNight/waveform.xml index 7c427440f55..db9da1dfcf7 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 ffdaf0a56dd..e6c4f4feebe 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -110,6 +110,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 0ddc95c9e5f..001504c19de 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -15,6 +15,7 @@ WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidgetRen : WaveformRendererAbstract(waveformWidgetRenderer) { m_beats.reserve(128); m_downbeats.reserve(32); + m_markerbeats.reserve(32); } WaveformRenderBeat::~WaveformRenderBeat() { @@ -26,6 +27,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*/) { @@ -46,6 +50,7 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { } m_beatColor.setAlphaF(alpha/100.0); m_downbeatColor.setAlphaF(alpha / 100.0); + m_markerbeatColor.setAlphaF(alpha / 100.0); const int trackSamples = m_waveformRenderer->getTrackSamples(); if (trackSamples <= 0) { @@ -78,6 +83,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(); @@ -93,7 +99,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()) { @@ -110,8 +116,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); } @@ -119,8 +123,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 f0faad92b8c8bf494656b9ae1119c617cb70230d 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 1f04f45668a..653685cbbf5 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -2309,11 +2309,17 @@ WPushButton#PlayDeck[value="0"] { image: url(skin:/palemoon/buttons/btn__beatgrid_controls_collapse.svg) no-repeat center center; } - #BeatCurposLarge[displayValue="0"] { - image: url(skin:/palemoon/buttons/btn__beat_curpos_large.svg) no-repeat center center; + #BeatsSetMarker[displayValue="0"] { + image: url(skin:/palemoon/buttons/btn__beats_set_marker.svg) no-repeat center center; } - #BeatCurposLarge[pressed="true"] { - image: url(skin:/palemoon/buttons/btn__beat_curpos_large_active.svg) no-repeat center center; + #BeatsSetMarker[pressed="true"] { + image: url(skin:/palemoon/buttons/btn__beats_set_marker_active.svg) no-repeat center center; + } + #BeatsRemoveMarker[displayValue="0"] { + image: url(skin:/palemoon/buttons/btn__beats_remove_marker.svg) no-repeat center center; + } + #BeatsRemoveMarker[pressed="true"] { + image: url(skin:/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 db9da1dfcf7..ee3c972a019 100644 --- a/res/skins/LateNight/waveform.xml +++ b/res/skins/LateNight/waveform.xml @@ -149,15 +149,24 @@ 1f,0min - - + + vertical + 26f,52f + + + + + vertical