From 6004edae5c5108560caf9d1f67789b5e47329212 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Sun, 26 Mar 2017 12:27:15 +0200 Subject: [PATCH] Include past automation patterns in processing (#3382) Fixes #662 * Include past automation tracks in processing * Track::getTCOsInRange: Use binary search, fix doc * Automation refactorings * Add automation tests --- include/AutomatableModel.h | 1 + include/AutomationPattern.h | 5 +- include/Song.h | 4 +- include/Track.h | 2 + src/core/AutomationPattern.cpp | 69 +++++-------- src/core/Song.cpp | 117 +++++++++++++++++++++-- src/core/Track.cpp | 38 +++----- src/tracks/AutomationTrack.cpp | 33 +------ tests/CMakeLists.txt | 2 + tests/main.cpp | 5 + tests/src/tracks/AutomationTrackTest.cpp | 104 ++++++++++++++++++++ 11 files changed, 265 insertions(+), 115 deletions(-) create mode 100644 tests/src/tracks/AutomationTrackTest.cpp diff --git a/include/AutomatableModel.h b/include/AutomatableModel.h index efa651b7a..a4096f9ba 100644 --- a/include/AutomatableModel.h +++ b/include/AutomatableModel.h @@ -444,6 +444,7 @@ public: } ; +typedef QMap AutomatedValueMap; #endif diff --git a/include/AutomationPattern.h b/include/AutomationPattern.h index 480b69f6a..0593e8bb1 100644 --- a/include/AutomationPattern.h +++ b/include/AutomationPattern.h @@ -59,6 +59,7 @@ public: bool addObject( AutomatableModel * _obj, bool _search_dup = true ); const AutomatableModel * firstObject() const; + const objectVector& objects() const; // progression-type stuff inline ProgressionTypes progressionType() const @@ -83,6 +84,8 @@ public: void removeValue( const MidiTime & time ); + void recordValue(MidiTime time, float value); + MidiTime setDragValue( const MidiTime & time, const float value, const bool quantPos = true, @@ -143,8 +146,6 @@ public: static const QString classNodeName() { return "automationpattern"; } QString nodeName() const { return classNodeName(); } - void processMidiTime( const MidiTime & _time ); - virtual TrackContentObjectView * createView( TrackView * _tv ); diff --git a/include/Song.h b/include/Song.h index 710c2b592..e38a3668a 100644 --- a/include/Song.h +++ b/include/Song.h @@ -31,7 +31,6 @@ #include #include "TrackContainer.h" -#include "AutomatableModel.h" #include "Controller.h" #include "MeterModel.h" #include "VstSyncController.h" @@ -205,6 +204,8 @@ public: return m_globalAutomationTrack; } + static AutomatedValueMap automatedValuesAt(const Track::tcoVector& tcos, MidiTime time); + // file management void createNewProject(); void createNewProjectFromTemplate( const QString & templ ); @@ -325,6 +326,7 @@ private: void removeAllControllers(); + void processAutomations(const TrackList& tracks, MidiTime timeStart, fpp_t frames, int tcoNum); AutomationTrack * m_globalAutomationTrack; diff --git a/include/Track.h b/include/Track.h index 8f1e4bb30..dcb1648e0 100644 --- a/include/Track.h +++ b/include/Track.h @@ -146,6 +146,8 @@ public: return m_selectViewOnCreate; } + /// Returns true if and only if a->startPosition() < b->startPosition() + static bool comparePosition(const TrackContentObject* a, const TrackContentObject* b); public slots: void copy(); diff --git a/src/core/AutomationPattern.cpp b/src/core/AutomationPattern.cpp index ffe13ff40..aab1eaec7 100644 --- a/src/core/AutomationPattern.cpp +++ b/src/core/AutomationPattern.cpp @@ -110,16 +110,9 @@ AutomationPattern::~AutomationPattern() bool AutomationPattern::addObject( AutomatableModel * _obj, bool _search_dup ) { - if( _search_dup ) + if( _search_dup && m_objects.contains(_obj) ) { - for( objectVector::iterator it = m_objects.begin(); - it != m_objects.end(); ++it ) - { - if( *it == _obj ) - { - return false; - } - } + return false; } // the automation track is unconnected and there is nothing in the track @@ -184,6 +177,11 @@ const AutomatableModel * AutomationPattern::firstObject() const return &_fm; } +const AutomationPattern::objectVector& AutomationPattern::objects() const +{ + return m_objects; +} + @@ -272,6 +270,21 @@ void AutomationPattern::removeValue( const MidiTime & time ) +void AutomationPattern::recordValue(MidiTime time, float value) +{ + if( value != m_lastRecordedValue ) + { + putValue( time, value, true ); + m_lastRecordedValue = value; + } + else if( valueAt( time ) != value ) + { + removeValue( time ); + } +} + + + /** * @brief Set the position of the point that is being dragged. @@ -627,44 +640,6 @@ const QString AutomationPattern::name() const -void AutomationPattern::processMidiTime( const MidiTime & time ) -{ - if( ! isRecording() ) - { - if( time >= 0 && hasAutomation() ) - { - const float val = valueAt( time ); - for( objectVector::iterator it = m_objects.begin(); - it != m_objects.end(); ++it ) - { - if( *it ) - { - ( *it )->setAutomatedValue( val ); - } - - } - } - } - else - { - if( time >= 0 && ! m_objects.isEmpty() ) - { - const float value = static_cast( firstObject()->value() ); - if( value != m_lastRecordedValue ) - { - putValue( time, value, true ); - m_lastRecordedValue = value; - } - else if( valueAt( time ) != value ) - { - removeValue( time ); - } - } - } -} - - - TrackContentObjectView * AutomationPattern::createView( TrackView * _tv ) { return new AutomationPatternView( this, _tv ); diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 684bde885..568794a54 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -30,6 +30,8 @@ #include #include +#include + #include "AutomationTrack.h" #include "AutomationEditor.h" #include "BBEditor.h" @@ -376,13 +378,7 @@ void Song::processNextBuffer() if( ( f_cnt_t ) currentFrame == 0 ) { - if( m_playMode == Mode_PlaySong ) - { - m_globalAutomationTrack->play( - m_playPos[m_playMode], - framesToPlay, - framesPlayed, tcoNum ); - } + processAutomations(trackList, m_playPos[m_playMode], framesToPlay, tcoNum); // loop through all tracks and play them for( int i = 0; i < trackList.size(); ++i ) @@ -405,6 +401,82 @@ void Song::processNextBuffer() } } +void Song::processAutomations(const TrackList &tracklist, MidiTime timeStart, fpp_t frames, int tcoNum) +{ + QVector tracks; + + if(m_playMode == Mode_PlaySong) + { + tracks << m_globalAutomationTrack; + } + for( Track* track : tracklist) + { + if (track->type() == Track::AutomationTrack || track->type() == Track::HiddenAutomationTrack) + { + tracks << dynamic_cast(track); + } + } + std::remove_if(tracks.begin(), tracks.end(), std::mem_fn(&Track::isMuted)); + + Track::tcoVector tcos; + AutomatedValueMap values; + + if (tcoNum < 0) + { + // Collect all relevant patterns, sorted by start position + MidiTime timeEnd = timeStart + static_cast(frames / Engine::framesPerTick()); + for (AutomationTrack* track: tracks) + { + track->getTCOsInRange(tcos, 0, timeEnd); + } + + values = automatedValuesAt(tcos, timeStart); + } + else + { + if (tracklist.size() != 1) + { + qWarning() << "processAutomations called with specified tcoNum but not exactly one track"; + } + + for (AutomationTrack* track: tracks) + { + TrackContentObject* tco = track->getTCO(tcoNum); + auto p = dynamic_cast(tco); + + for (AutomatableModel* object : p->objects()) + { + values[object] = p->valueAt(timeStart); + } + tcos << tco; + } + } + + QSet recordedModels; + // Process recording + for (TrackContentObject* tco : tcos) + { + auto p = dynamic_cast(tco); + MidiTime relTime = timeStart - p->startPosition(); + if (p->isRecording() && relTime >= 0 && relTime < p->length()) + { + const AutomatableModel* recordedModel = p->firstObject(); + p->recordValue(relTime, recordedModel->value()); + + recordedModels << recordedModel; + } + } + + // Apply values + for (auto it = values.begin(); it != values.end(); it++) + { + if (! recordedModels.contains(it.key())) + { + it.key()->setAutomatedValue(it.value()); + } + } +} + bool Song::isExportDone() const { if ( m_renderBetweenMarkers ) @@ -777,6 +849,37 @@ AutomationPattern * Song::tempoAutomationPattern() return AutomationPattern::globalAutomationPattern( &m_tempoModel ); } +AutomatedValueMap Song::automatedValuesAt(const Track::tcoVector &tcos, MidiTime time) +{ + AutomatedValueMap valueMap; + + for(TrackContentObject* tco : tcos) + { + if (tco->isMuted() || tco->startPosition() > time) { + continue; + } + AutomationPattern* p = dynamic_cast(tco); + if (!p) { + qCritical() << "automatedValuesAt: tco passed is not an automation pattern"; + continue; + } + + if (! p->hasAutomation()) { + continue; + } + + MidiTime relTime = time - p->startPosition(); + float value = p->valueAt(relTime); + + for (AutomatableModel* model : p->objects()) + { + valueMap[model] = value; + } + } + + return valueMap; +} + diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 25dade02b..65187f552 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -166,6 +166,11 @@ void TrackContentObject::changeLength( const MidiTime & length ) emit lengthChanged(); } +bool TrackContentObject::comparePosition(const TrackContentObject *a, const TrackContentObject *b) +{ + return a->startPosition() < b->startPosition(); +} + @@ -2269,10 +2274,8 @@ int Track::getTCONum( const TrackContentObject * tco ) /*! \brief Retrieve a list of trackContentObjects that fall within a period. * - * Here we're interested in a range of trackContentObjects that fall - * completely within a given time period - their start must be no earlier - * than the given start time and their end must be no later than the given - * end time. + * Here we're interested in a range of trackContentObjects that intersect + * the given time period. * * We return the TCOs we find in order by time, earliest TCOs first. * @@ -2283,33 +2286,16 @@ int Track::getTCONum( const TrackContentObject * tco ) void Track::getTCOsInRange( tcoVector & tcoV, const MidiTime & start, const MidiTime & end ) { - for( tcoVector::iterator itO = m_trackContentObjects.begin(); - itO != m_trackContentObjects.end(); ++itO ) + for( TrackContentObject* tco : m_trackContentObjects ) { - TrackContentObject * tco = ( *itO ); int s = tco->startPosition(); int e = tco->endPosition(); if( ( s <= end ) && ( e >= start ) ) { - // ok, TCO is posated within given range - // now let's search according position for TCO in list - // -> list is ordered by TCO's position afterwards - bool inserted = false; - for( tcoVector::iterator it = tcoV.begin(); - it != tcoV.end(); ++it ) - { - if( ( *it )->startPosition() >= s ) - { - tcoV.insert( it, tco ); - inserted = true; - break; - } - } - if( inserted == false ) - { - // no TCOs found posated behind current TCO... - tcoV.push_back( tco ); - } + // TCO is within given range + // Insert sorted by TCO's position + tcoV.insert(std::upper_bound(tcoV.begin(), tcoV.end(), tco, TrackContentObject::comparePosition), + tco); } } } diff --git a/src/tracks/AutomationTrack.cpp b/src/tracks/AutomationTrack.cpp index b8bb1a922..77693ace6 100644 --- a/src/tracks/AutomationTrack.cpp +++ b/src/tracks/AutomationTrack.cpp @@ -50,40 +50,9 @@ AutomationTrack::~AutomationTrack() -bool AutomationTrack::play( const MidiTime & _start, const fpp_t _frames, +bool AutomationTrack::play( const MidiTime & time_start, const fpp_t _frames, const f_cnt_t _frame_base, int _tco_num ) { - if( isMuted() ) - { - return false; - } - - tcoVector tcos; - if( _tco_num >= 0 ) - { - TrackContentObject * tco = getTCO( _tco_num ); - tcos.push_back( tco ); - } - else - { - getTCOsInRange( tcos, _start, _start + static_cast( - _frames / Engine::framesPerTick()) ); - } - - for( tcoVector::iterator it = tcos.begin(); it != tcos.end(); ++it ) - { - AutomationPattern * p = dynamic_cast( *it ); - if( p == NULL || ( *it )->isMuted() ) - { - continue; - } - MidiTime cur_start = _start; - if( _tco_num < 0 ) - { - cur_start -= p->startPosition(); - } - p->processMidiTime( cur_start ); - } return false; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2c5e281c6..2367ea4bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,8 @@ ADD_EXECUTABLE(tests $ src/core/ProjectVersionTest.cpp + + src/tracks/AutomationTrackTest.cpp ) TARGET_LINK_LIBRARIES(tests ${QT_LIBRARIES} ${QT_QTTEST_LIBRARY}) TARGET_LINK_LIBRARIES(tests ${LMMS_REQUIRED_LIBS}) diff --git a/tests/main.cpp b/tests/main.cpp index 5a842dc0b..7b07778fc 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -4,8 +4,13 @@ #include +#include "Engine.h" + int main(int argc, char* argv[]) { + new QCoreApplication(argc, argv); + Engine::init(true); + int numsuites = QTestSuite::suites().size(); qDebug() << ">> Will run" << numsuites << "test suites"; int failed = 0; diff --git a/tests/src/tracks/AutomationTrackTest.cpp b/tests/src/tracks/AutomationTrackTest.cpp new file mode 100644 index 000000000..63e85d7e0 --- /dev/null +++ b/tests/src/tracks/AutomationTrackTest.cpp @@ -0,0 +1,104 @@ +/* + * AutomationTrackTest.cpp + * + * Copyright (c) 2017 Lukas W + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "QTestSuite.h" + +#include "QCoreApplication" + +#include "AutomationPattern.h" +#include "AutomationTrack.h" +#include "TrackContainer.h" + +#include "Engine.h" +#include "Song.h" + +class AutomationTrackTest : QTestSuite +{ + Q_OBJECT +private slots: + void initTestCase() + { + } + + void testPatternLinear() + { + AutomationPattern p(nullptr); + p.setProgressionType(AutomationPattern::LinearProgression); + p.putValue(0, 0.0, false); + p.putValue(100, 1.0, false); + + QCOMPARE(p.valueAt(0), 0.0f); + QCOMPARE(p.valueAt(25), 0.25f); + QCOMPARE(p.valueAt(50), 0.5f); + QCOMPARE(p.valueAt(75), 0.75f); + QCOMPARE(p.valueAt(100), 1.0f); + QCOMPARE(p.valueAt(150), 1.0f); + } + + void testPatternDiscrete() + { + AutomationPattern p(nullptr); + p.setProgressionType(AutomationPattern::DiscreteProgression); + p.putValue(0, 0.0, false); + p.putValue(100, 1.0, false); + + QCOMPARE(p.valueAt(0), 0.0f); + QCOMPARE(p.valueAt(50), 0.0f); + QCOMPARE(p.valueAt(100), 1.0f); + QCOMPARE(p.valueAt(150), 1.0f); + } + + void testTrack() + { + FloatModel model; + + AutomationPattern p1(nullptr); + p1.setProgressionType(AutomationPattern::LinearProgression); + p1.putValue(0, 0.0, false); + p1.putValue(10, 1.0, false); + p1.movePosition(0); + p1.addObject(&model); + + AutomationPattern p2(nullptr); + p2.setProgressionType(AutomationPattern::LinearProgression); + p2.putValue(0, 0.0, false); + p2.putValue(100, 1.0, false); + p2.movePosition(100); + p2.addObject(&model); + + AutomationPattern p3(nullptr); + p3.addObject(&model); + //XXX: Why is this even necessary? + p3.clear(); + + QCOMPARE(Song::automatedValuesAt({&p1, &p2, &p3}, 0)[&model], 0.0f); + QCOMPARE(Song::automatedValuesAt({&p1, &p2, &p3}, 5)[&model], 0.5f); + QCOMPARE(Song::automatedValuesAt({&p1, &p2, &p3}, 10)[&model], 1.0f); + QCOMPARE(Song::automatedValuesAt({&p1, &p2, &p3}, 50)[&model], 1.0f); + QCOMPARE(Song::automatedValuesAt({&p1, &p2, &p3}, 100)[&model], 0.0f); + QCOMPARE(Song::automatedValuesAt({&p1, &p2, &p3}, 150)[&model], 0.5f); + } +} AutomationTrackTest; + +#include "AutomationTrackTest.moc"