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:
Lukas W
2017-03-26 12:27:15 +02:00
committed by GitHub
parent 84d662409c
commit 6004edae5c
11 changed files with 265 additions and 115 deletions

View File

@@ -444,6 +444,7 @@ public:
} ;
typedef QMap<AutomatableModel*, float> AutomatedValueMap;
#endif

View File

@@ -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 );

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 );

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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})

View File

@@ -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;

View 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"