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
This commit is contained in:
@@ -444,6 +444,7 @@ public:
|
||||
|
||||
} ;
|
||||
|
||||
typedef QMap<AutomatableModel*, float> AutomatedValueMap;
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <QtCore/QVector>
|
||||
|
||||
#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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<float>( firstObject()->value<float>() );
|
||||
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 );
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#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<AutomationTrack*> 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<AutomationTrack*>(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<int>(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<AutomationPattern *>(tco);
|
||||
|
||||
for (AutomatableModel* object : p->objects())
|
||||
{
|
||||
values[object] = p->valueAt(timeStart);
|
||||
}
|
||||
tcos << tco;
|
||||
}
|
||||
}
|
||||
|
||||
QSet<const AutomatableModel*> recordedModels;
|
||||
// Process recording
|
||||
for (TrackContentObject* tco : tcos)
|
||||
{
|
||||
auto p = dynamic_cast<AutomationPattern *>(tco);
|
||||
MidiTime relTime = timeStart - p->startPosition();
|
||||
if (p->isRecording() && relTime >= 0 && relTime < p->length())
|
||||
{
|
||||
const AutomatableModel* recordedModel = p->firstObject();
|
||||
p->recordValue(relTime, recordedModel->value<float>());
|
||||
|
||||
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<AutomationPattern *>(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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int>(
|
||||
_frames / Engine::framesPerTick()) );
|
||||
}
|
||||
|
||||
for( tcoVector::iterator it = tcos.begin(); it != tcos.end(); ++it )
|
||||
{
|
||||
AutomationPattern * p = dynamic_cast<AutomationPattern *>( *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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ ADD_EXECUTABLE(tests
|
||||
$<TARGET_OBJECTS:lmmsobjs>
|
||||
|
||||
src/core/ProjectVersionTest.cpp
|
||||
|
||||
src/tracks/AutomationTrackTest.cpp
|
||||
)
|
||||
TARGET_LINK_LIBRARIES(tests ${QT_LIBRARIES} ${QT_QTTEST_LIBRARY})
|
||||
TARGET_LINK_LIBRARIES(tests ${LMMS_REQUIRED_LIBS})
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#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;
|
||||
|
||||
104
tests/src/tracks/AutomationTrackTest.cpp
Normal file
104
tests/src/tracks/AutomationTrackTest.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* AutomationTrackTest.cpp
|
||||
*
|
||||
* Copyright (c) 2017 Lukas W <lukaswhl/at/gmail.com>
|
||||
*
|
||||
* 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"
|
||||
Reference in New Issue
Block a user