Changes AutomationPattern to use nodes instead of raw float values (#5712)

This commit is contained in:
IanCaio
2021-02-28 06:48:15 -03:00
committed by GitHub
parent 05de59c085
commit e880e3cb2a
16 changed files with 1346 additions and 1282 deletions

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

View File

@@ -20,7 +20,8 @@ AutomationEditor {
background-color: rgb(0, 0, 0);
color: #e0e0e0;
qproperty-backgroundShade: rgba(255, 255, 255, 15);
qproperty-vertexColor: #ff77af;
qproperty-nodeInValueColor: rgba(255, 119, 175, 150);
qproperty-nodeOutValueColor: rgba(129, 231, 181, 150);
qproperty-crossColor: rgb( 255, 51, 51 );
/* Grid colors */
qproperty-lineColor: rgba(128, 128, 128, 80);

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

View File

@@ -55,7 +55,8 @@ AutomationEditor {
color: #ffffff;
background-color: #141616;
qproperty-backgroundShade: rgba(255, 255, 255, 15);
qproperty-vertexColor: #6749C2;
qproperty-nodeInValueColor: rgba(103, 73, 194, 150);
qproperty-nodeOutValueColor: rgba(125, 40, 40, 150);
qproperty-crossColor: rgba(215, 210, 254, 150);
/* Grid colors */
qproperty-lineColor: #292929;

View File

@@ -26,7 +26,6 @@
#ifndef AUTOMATION_EDITOR_H
#define AUTOMATION_EDITOR_H
#include <QtCore/QMutex>
#include <QVector>
#include <QWidget>
@@ -52,14 +51,15 @@ class TimeLineWidget;
class AutomationEditor : public QWidget, public JournallingObject
{
Q_OBJECT
Q_PROPERTY(QColor barLineColor READ barLineColor WRITE setBarLineColor)
Q_PROPERTY(QColor beatLineColor READ beatLineColor WRITE setBeatLineColor)
Q_PROPERTY(QColor lineColor READ lineColor WRITE setLineColor)
Q_PROPERTY(QColor vertexColor READ vertexColor WRITE setVertexColor)
Q_PROPERTY(QBrush scaleColor READ scaleColor WRITE setScaleColor)
Q_PROPERTY(QBrush graphColor READ graphColor WRITE setGraphColor)
Q_PROPERTY(QColor crossColor READ crossColor WRITE setCrossColor)
Q_PROPERTY(QColor backgroundShade READ backgroundShade WRITE setBackgroundShade)
Q_PROPERTY(QColor barLineColor MEMBER m_barLineColor)
Q_PROPERTY(QColor beatLineColor MEMBER m_beatLineColor)
Q_PROPERTY(QColor lineColor MEMBER m_lineColor)
Q_PROPERTY(QColor nodeInValueColor MEMBER m_nodeInValueColor)
Q_PROPERTY(QColor nodeOutValueColor MEMBER m_nodeOutValueColor)
Q_PROPERTY(QBrush scaleColor MEMBER m_scaleColor)
Q_PROPERTY(QBrush graphColor MEMBER m_graphColor)
Q_PROPERTY(QColor crossColor MEMBER m_crossColor)
Q_PROPERTY(QColor backgroundShade MEMBER m_backgroundShade)
public:
void setCurrentPattern(AutomationPattern * new_pattern);
@@ -80,30 +80,11 @@ public:
return "automationeditor";
}
// qproperty access methods
QColor barLineColor() const;
void setBarLineColor(const QColor & c);
QColor beatLineColor() const;
void setBeatLineColor(const QColor & c);
QColor lineColor() const;
void setLineColor(const QColor & c);
QBrush graphColor() const;
void setGraphColor(const QBrush & c);
QColor vertexColor() const;
void setVertexColor(const QColor & c);
QBrush scaleColor() const;
void setScaleColor(const QBrush & c);
QColor crossColor() const;
void setCrossColor(const QColor & c);
QColor backgroundShade() const;
void setBackgroundShade(const QColor & c);
enum EditModes
{
DRAW,
ERASE,
SELECT,
MOVE
DRAW_OUTVALUES
};
public slots:
@@ -126,13 +107,11 @@ protected:
float getLevel( int y );
int xCoordOfTick( int tick );
float yCoordOfLevel( float level );
inline void drawLevelTick( QPainter & p, int tick, float value);// bool is_selected ); //NEEDS Change in CSS
void removeSelection();
void selectAll();
void getSelectedValues(timeMap & selected_values );
inline void drawLevelTick(QPainter & p, int tick, float value);
timeMap::iterator getNodeAt(int x, int y, bool outValue = false, int r = 5);
void drawLine( int x0, float y0, int x1, float y1 );
void removePoints( int x0, int x1 );
protected slots:
void play();
@@ -148,11 +127,6 @@ protected slots:
void setProgressionType(int type);
void setTension();
void copySelectedValues();
void cutSelectedValues();
void pasteValues();
void deleteSelectedValues();
void updatePosition( const TimePos & t );
void zoomingXChanged();
@@ -167,8 +141,10 @@ private:
{
NONE,
MOVE_VALUE,
SELECT_VALUES,
MOVE_SELECTION
ERASE_VALUES,
MOVE_OUTVALUE,
RESET_OUTVALUES,
DRAW_LINE
} ;
// some constants...
@@ -187,7 +163,7 @@ private:
static QPixmap * s_toolDraw;
static QPixmap * s_toolErase;
static QPixmap * s_toolSelect;
static QPixmap * s_toolDrawOut;
static QPixmap * s_toolMove;
static QPixmap * s_toolYFlip;
static QPixmap * s_toolXFlip;
@@ -200,7 +176,6 @@ private:
FloatModel * m_tensionModel;
QMutex m_patternMutex;
AutomationPattern * m_pattern;
float m_minLevel;
float m_maxLevel;
@@ -219,13 +194,6 @@ private:
Actions m_action;
tick_t m_selectStartTick;
tick_t m_selectedTick;
float m_selectStartLevel;
float m_selectedLevels;
float m_moveStartLevel;
tick_t m_moveStartTick;
int m_moveXOffset;
float m_drawLastLevel;
@@ -235,9 +203,8 @@ private:
int m_y_delta;
bool m_y_auto;
timeMap m_valuesToCopy;
timeMap m_selValuesForMove;
// Time position (key) of automation node whose outValue is being dragged
int m_draggedOutValueKey;
EditModes m_editMode;
@@ -255,7 +222,8 @@ private:
QColor m_beatLineColor;
QColor m_lineColor;
QBrush m_graphColor;
QColor m_vertexColor;
QColor m_nodeInValueColor;
QColor m_nodeOutValueColor;
QBrush m_scaleColor;
QColor m_crossColor;
QColor m_backgroundShade;

153
include/AutomationNode.h Normal file
View File

@@ -0,0 +1,153 @@
/*
* AutomationNode.h - Declaration of class AutomationNode, which contains
* all information about an automation node
*
* Copyright (c) 2020 Ian Caio <iancaio_dev/at/hotmail.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.
*
*/
#ifndef AUTOMATION_NODE_H
#define AUTOMATION_NODE_H
// MACROs to help handling automation nodes
#define INVAL(x) ((x).value().getInValue())
#define OUTVAL(x) ((x).value().getOutValue())
#define OFFSET(x) ((x).value().getValueOffset())
#define INTAN(x) ((x).value().getInTangent())
#define OUTTAN(x) ((x).value().getOutTangent())
#define POS(x) ((x).key())
class AutomationPattern;
// Note: We use the default copy-assignment on the AutomationPattern constructor. It's
// fine for now as we don't have dynamic allocated members, but if any are added we should
// have an user-defined one to perform a deep-copy.
class AutomationNode
{
public:
AutomationNode(); // Dummy constructor for the QMap
AutomationNode(AutomationPattern* pat, float value, int pos);
AutomationNode(AutomationPattern* pat, float inValue, float outValue, int pos);
AutomationNode& operator+=(float f)
{
m_inValue += f;
m_outValue += f;
return *this;
}
AutomationNode& operator-=(float f)
{
m_inValue -= f;
m_outValue -= f;
return *this;
}
AutomationNode& operator*=(float f)
{
m_inValue *= f;
m_outValue *= f;
return *this;
}
AutomationNode& operator/=(float f)
{
m_inValue /= f;
m_outValue /= f;
return *this;
}
inline const float getInValue() const
{
return m_inValue;
}
void setInValue(float value);
inline const float getOutValue() const
{
return m_outValue;
}
void setOutValue(float value);
void resetOutValue();
/**
* @brief Gets the offset between inValue and outValue
* @return Float representing the offset between inValue and outValue
*/
inline const float getValueOffset() const
{
return m_outValue - m_inValue;
}
/**
* @brief Gets the tangent of the left side of the node
* @return Float with the tangent from the inValue side
*/
inline const float getInTangent() const
{
return m_inTangent;
}
/**
* @brief Sets the tangent of the left side of the node
* @param Float with the tangent for the inValue side
*/
inline void setInTangent(float tangent)
{
m_inTangent = tangent;
}
/**
* @brief Gets the tangent of the right side of the node
* @return Float with the tangent from the outValue side
*/
inline const float getOutTangent() const
{
return m_outTangent;
}
/**
* @brief Sets the tangent of the right side of the node
* @param Float with the tangent for the outValue side
*/
inline void setOutTangent(float tangent)
{
m_outTangent = tangent;
}
private:
// Pattern that this node belongs to
AutomationPattern* m_pattern;
// Time position of this node (matches the timeMap key)
int m_pos;
// Values of this node
float m_inValue;
float m_outValue;
// Slope at each point for calculating spline
// We might have discrete jumps between curves, so we possibly have
// two different tangents for each side of the curve. If inValue and
// outValue are equal, inTangent and outTangent are equal too.
float m_inTangent;
float m_outTangent;
};
#endif

View File

@@ -30,6 +30,7 @@
#include <QtCore/QMap>
#include <QtCore/QPointer>
#include "AutomationNode.h"
#include "TrackContentObject.h"
@@ -49,8 +50,8 @@ public:
CubicHermiteProgression
} ;
typedef QMap<int, float> timeMap;
typedef QVector<QPointer<AutomatableModel> > objectVector;
typedef QMap<int, AutomationNode> timeMap;
typedef QVector<QPointer<AutomatableModel>> objectVector;
AutomationPattern( AutomationTrack * _auto_track );
AutomationPattern( const AutomationPattern & _pat_to_copy );
@@ -77,12 +78,25 @@ public:
TimePos timeMapLength() const;
void updateLength();
TimePos putValue( const TimePos & time,
const float value,
const bool quantPos = true,
const bool ignoreSurroundingPoints = true );
TimePos putValue(
const TimePos & time,
const float value,
const bool quantPos = true,
const bool ignoreSurroundingPoints = true
);
void removeValue( const TimePos & time );
TimePos putValues(
const TimePos & time,
const float inValue,
const float outValue,
const bool quantPos = true,
const bool ignoreSurroundingPoints = true
);
void removeNode(const TimePos & time);
void removeNodes(const int tick0, const int tick1);
void resetNodes(const int tick0, const int tick1);
void recordValue(TimePos time, float value);
@@ -109,16 +123,6 @@ public:
return m_timeMap;
}
inline const timeMap & getTangents() const
{
return m_tangents;
}
inline timeMap & getTangents()
{
return m_tangents;
}
inline float getMin() const
{
return firstObject()->minValue<float>();
@@ -170,21 +174,26 @@ public slots:
private:
void cleanObjects();
void generateTangents();
void generateTangents( timeMap::const_iterator it, int numToGenerate );
void generateTangents(timeMap::iterator it, int numToGenerate);
float valueAt( timeMap::const_iterator v, int offset ) const;
// Mutex to make methods involving automation patterns thread safe
// Mutable so we can lock it from const objects
mutable QMutex m_patternMutex;
AutomationTrack * m_autoTrack;
QVector<jo_id_t> m_idsToResolve;
objectVector m_objects;
timeMap m_timeMap; // actual values
timeMap m_oldTimeMap; // old values for storing the values before setDragValue() is called.
timeMap m_tangents; // slope at each point for calculating spline
float m_tension;
bool m_hasAutomation;
ProgressionTypes m_progressionType;
bool m_dragging;
bool m_dragKeepOutValue; // Should we keep the current dragged node's outValue?
float m_dragOutValue; // The outValue of the dragged node's
bool m_isRecording;
float m_lastRecordedValue;
@@ -194,6 +203,7 @@ private:
static const float DEFAULT_MAX_VALUE;
friend class AutomationPatternView;
friend class AutomationNode;
} ;

View File

@@ -115,6 +115,7 @@ private:
void upgrade_1_2_0_rc3();
void upgrade_1_3_0();
void upgrade_noHiddenClipNames();
void upgrade_automationNodes();
// List of all upgrade methods
static const std::vector<UpgradeMethod> UPGRADE_METHODS;

View File

@@ -25,6 +25,7 @@
#ifndef INLINE_AUTOMATION_H
#define INLINE_AUTOMATION_H
#include "AutomationNode.h"
#include "AutomationPattern.h"
#include "shared_object.h"
@@ -53,12 +54,17 @@ public:
{
if( m_autoPattern != NULL && m_autoPattern->getTimeMap().isEmpty() == false )
{
// prevent saving inline automation if there's just one value which equals value
// of model which is going to be saved anyways
if( isAtInitValue() &&
m_autoPattern->getTimeMap().size() == 1 &&
m_autoPattern->getTimeMap().keys().first() == 0 &&
m_autoPattern->getTimeMap().values().first() == value() )
// Prevent saving inline automation if there's just one node at the beginning of
// the pattern, which has a InValue equal to the value of model (which is going
// to be saved anyways) and no offset between the InValue and OutValue
AutomationPattern::timeMap::const_iterator firstNode =
m_autoPattern->getTimeMap().begin();
if (isAtInitValue()
&& m_autoPattern->getTimeMap().size() == 1
&& POS(firstNode) == 0
&& INVAL(firstNode) == value()
&& OFFSET(firstNode) == 0)
{
return false;
}

109
src/core/AutomationNode.cpp Normal file
View File

@@ -0,0 +1,109 @@
/*
* AutomationPattern.cpp - Implementation of class AutomationNode which
* holds information on a single automation pattern node
*
* Copyright (c) 2020 Ian Caio <iancaio_dev/at/hotmail.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 "AutomationNode.h"
#include "AutomationPattern.h"
// Dummy constructor for the QMap
AutomationNode::AutomationNode() :
m_pattern(nullptr),
m_pos(0),
m_inValue(0),
m_outValue(0),
m_inTangent(0),
m_outTangent(0)
{
}
AutomationNode::AutomationNode(AutomationPattern* pat, float value, int pos) :
m_pattern(pat),
m_pos(pos),
m_inValue(value),
m_outValue(value),
m_inTangent(0),
m_outTangent(0)
{
}
AutomationNode::AutomationNode(AutomationPattern* pat, float inValue, float outValue, int pos) :
m_pattern(pat),
m_pos(pos),
m_inValue(inValue),
m_outValue(outValue),
m_inTangent(0),
m_outTangent(0)
{
}
/**
* @brief Sets the inValue of an automation node
* @param Float value to be assigned
*/
void AutomationNode::setInValue(float value)
{
m_inValue = value;
// Recalculate the tangents from neighbor nodes
AutomationPattern::timeMap & tm = m_pattern->getTimeMap();
// Get an iterator pointing to this node
AutomationPattern::timeMap::iterator it = tm.lowerBound(m_pos);
// If it's not the first node, get the one immediately behind it
if (it != tm.begin()) { --it; }
// Generate tangents from the previously, current and next nodes
m_pattern->generateTangents(it, 3);
}
/**
* @brief Sets the outValue of an automation node
* @param Float value to be assigned
*/
void AutomationNode::setOutValue(float value)
{
m_outValue = value;
// Recalculate the tangents from neighbor nodes
AutomationPattern::timeMap & tm = m_pattern->getTimeMap();
// Get an iterator pointing to this node
AutomationPattern::timeMap::iterator it = tm.lowerBound(m_pos);
// If it's not the first node, get the one immediately behind it
if (it != tm.begin()) { --it; }
// Generate tangents from the previously, current and next nodes
m_pattern->generateTangents(it, 3);
}
/**
* @brief Resets the outValue so it matches inValue
*/
void AutomationNode::resetOutValue()
{
// Calls setOutValue so it also takes care of generating
// the tangents
setOutValue(m_inValue);
}

View File

@@ -26,12 +26,13 @@
#include "AutomationPattern.h"
#include "AutomationNode.h"
#include "AutomationPatternView.h"
#include "AutomationTrack.h"
#include "BBTrackContainer.h"
#include "LocaleHelper.h"
#include "Note.h"
#include "ProjectJournal.h"
#include "BBTrackContainer.h"
#include "Song.h"
#include <cmath>
@@ -43,6 +44,7 @@ const float AutomationPattern::DEFAULT_MAX_VALUE = 1;
AutomationPattern::AutomationPattern( AutomationTrack * _auto_track ) :
TrackContentObject( _auto_track ),
m_patternMutex(QMutex::Recursive),
m_autoTrack( _auto_track ),
m_objects(),
m_tension( 1.0 ),
@@ -74,16 +76,21 @@ AutomationPattern::AutomationPattern( AutomationTrack * _auto_track ) :
AutomationPattern::AutomationPattern( const AutomationPattern & _pat_to_copy ) :
TrackContentObject( _pat_to_copy.m_autoTrack ),
m_patternMutex(QMutex::Recursive),
m_autoTrack( _pat_to_copy.m_autoTrack ),
m_objects( _pat_to_copy.m_objects ),
m_tension( _pat_to_copy.m_tension ),
m_progressionType( _pat_to_copy.m_progressionType )
{
// Locks the mutex of the copied AutomationPattern to make sure it
// doesn't change while it's being copied
QMutexLocker m(&_pat_to_copy.m_patternMutex);
for( timeMap::const_iterator it = _pat_to_copy.m_timeMap.begin();
it != _pat_to_copy.m_timeMap.end(); ++it )
{
m_timeMap[it.key()] = it.value();
m_tangents[it.key()] = _pat_to_copy.m_tangents[it.key()];
// Copies the automation node (in/out values and in/out tangents)
m_timeMap[POS(it)] = it.value();
}
switch( getTrack()->trackContainer()->type() )
{
@@ -101,6 +108,8 @@ AutomationPattern::AutomationPattern( const AutomationPattern & _pat_to_copy ) :
bool AutomationPattern::addObject( AutomatableModel * _obj, bool _search_dup )
{
QMutexLocker m(&m_patternMutex);
if( _search_dup && m_objects.contains(_obj) )
{
return false;
@@ -130,6 +139,8 @@ bool AutomationPattern::addObject( AutomatableModel * _obj, bool _search_dup )
void AutomationPattern::setProgressionType(
ProgressionTypes _new_progression_type )
{
QMutexLocker m(&m_patternMutex);
if ( _new_progression_type == DiscreteProgression ||
_new_progression_type == LinearProgression ||
_new_progression_type == CubicHermiteProgression )
@@ -144,6 +155,8 @@ void AutomationPattern::setProgressionType(
void AutomationPattern::setTension( QString _new_tension )
{
QMutexLocker m(&m_patternMutex);
bool ok;
float nt = LocaleHelper::toFloat(_new_tension, & ok);
@@ -158,18 +171,22 @@ void AutomationPattern::setTension( QString _new_tension )
const AutomatableModel * AutomationPattern::firstObject() const
{
AutomatableModel * m;
if( !m_objects.isEmpty() && ( m = m_objects.first() ) != NULL )
QMutexLocker m(&m_patternMutex);
AutomatableModel* model;
if (!m_objects.isEmpty() && (model = m_objects.first()) != nullptr)
{
return m;
return model;
}
static FloatModel _fm( 0, DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, 0.001 );
return &_fm;
static FloatModel fm(0, DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, 0.001);
return &fm;
}
const AutomationPattern::objectVector& AutomationPattern::objects() const
{
QMutexLocker m(&m_patternMutex);
return m_objects;
}
@@ -178,11 +195,13 @@ const AutomationPattern::objectVector& AutomationPattern::objects() const
TimePos AutomationPattern::timeMapLength() const
{
QMutexLocker m(&m_patternMutex);
TimePos one_bar = TimePos(1, 0);
if (m_timeMap.isEmpty()) { return one_bar; }
timeMap::const_iterator it = m_timeMap.end();
tick_t last_tick = static_cast<tick_t>((it-1).key());
tick_t last_tick = static_cast<tick_t>(POS(it - 1));
// if last_tick is 0 (single item at tick 0)
// return length as a whole bar to prevent disappearing TCO
if (last_tick == 0) { return one_bar; }
@@ -202,34 +221,48 @@ void AutomationPattern::updateLength()
TimePos AutomationPattern::putValue( const TimePos & time,
const float value,
const bool quantPos,
const bool ignoreSurroundingPoints )
/**
* @brief Puts an automation node on the timeMap with the given value.
* The inValue and outValue of the created node will be the same.
* @param TimePos time to add the node to
* @param Float inValue and outValue of the node
* @param Boolean True to quantize the position (defaults to true)
* @param Boolean True to ignore unquantized surrounding nodes (defaults to true)
* @return TimePos of the recently added automation node
*/
TimePos AutomationPattern::putValue(
const TimePos & time,
const float value,
const bool quantPos,
const bool ignoreSurroundingPoints
)
{
QMutexLocker m(&m_patternMutex);
cleanObjects();
TimePos newTime = quantPos ?
Note::quantized( time, quantization() ) :
time;
TimePos newTime = quantPos ? Note::quantized(time, quantization()) : time;
m_timeMap[ newTime ] = value;
timeMap::const_iterator it = m_timeMap.find( newTime );
// Create a node or replace the existing one on newTime
m_timeMap[newTime] = AutomationNode(this, value, newTime);
timeMap::iterator it = m_timeMap.find(newTime);
// Remove control points that are covered by the new points
// quantization value. Control Key to override
if( ! ignoreSurroundingPoints )
if (!ignoreSurroundingPoints)
{
for( int i = newTime + 1; i < newTime + quantization(); ++i )
// We need to check that to avoid removing nodes from
// newTime + 1 to newTime (removing the node we are adding)
if (quantization() > 1)
{
AutomationPattern::removeValue( i );
// Remove nodes between the quantization points, them not
// being included
removeNodes(newTime + 1, newTime + quantization() - 1);
}
}
if( it != m_timeMap.begin() )
{
--it;
}
generateTangents( it, 3 );
if (it != m_timeMap.begin()) { --it; }
generateTangents(it, 3);
updateLength();
@@ -241,13 +274,69 @@ TimePos AutomationPattern::putValue( const TimePos & time,
void AutomationPattern::removeValue( const TimePos & time )
/**
* @brief Puts an automation node on the timeMap with the given inValue
* and outValue.
* @param TimePos time to add the node to
* @param Float inValue of the node
* @param Float outValue of the node
* @param Boolean True to quantize the position (defaults to true)
* @param Boolean True to ignore unquantized surrounding nodes (defaults to true)
* @return TimePos of the recently added automation node
*/
TimePos AutomationPattern::putValues(
const TimePos & time,
const float inValue,
const float outValue,
const bool quantPos,
const bool ignoreSurroundingPoints
)
{
QMutexLocker m(&m_patternMutex);
cleanObjects();
TimePos newTime = quantPos ? Note::quantized(time, quantization()) : time;
// Create a node or replace the existing one on newTime
m_timeMap[newTime] = AutomationNode(this, inValue, outValue, newTime);
timeMap::iterator it = m_timeMap.find(newTime);
// Remove control points that are covered by the new points
// quantization value. Control Key to override
if (!ignoreSurroundingPoints)
{
// We need to check that to avoid removing nodes from
// newTime + 1 to newTime (removing the node we are adding)
if (quantization() > 1)
{
// Remove nodes between the quantization points, them not
// being included
removeNodes(newTime + 1, newTime + quantization() - 1);
}
}
if (it != m_timeMap.begin()) { --it; }
generateTangents(it, 3);
updateLength();
emit dataChanged();
return newTime;
}
void AutomationPattern::removeNode(const TimePos & time)
{
QMutexLocker m(&m_patternMutex);
cleanObjects();
m_timeMap.remove( time );
m_tangents.remove( time );
timeMap::const_iterator it = m_timeMap.lowerBound( time );
timeMap::iterator it = m_timeMap.lowerBound(time);
if( it != m_timeMap.begin() )
{
--it;
@@ -261,8 +350,72 @@ void AutomationPattern::removeValue( const TimePos & time )
/**
* @brief Removes all automation nodes between the given ticks
* @param Int first tick of the range
* @param Int second tick of the range
*/
void AutomationPattern::removeNodes(const int tick0, const int tick1)
{
if (tick0 == tick1)
{
removeNode(TimePos(tick0));
return;
}
TimePos start = TimePos(qMin(tick0, tick1));
TimePos end = TimePos(qMax(tick0, tick1));
// Make a list of TimePos with nodes to be removed
// because we can't simply remove the nodes from
// the timeMap while we are iterating it.
QVector<TimePos> nodesToRemove;
for (auto it = m_timeMap.lowerBound(start), endIt = m_timeMap.upperBound(end); it != endIt; ++it)
{
nodesToRemove.append(POS(it));
}
for (auto node: nodesToRemove)
{
removeNode(node);
}
}
/**
* @brief Resets the outValues of all automation nodes between the given ticks
* @param Int first tick of the range
* @param Int second tick of the range
*/
void AutomationPattern::resetNodes(const int tick0, const int tick1)
{
if (tick0 == tick1)
{
auto it = m_timeMap.find(TimePos(tick0));
if (it != m_timeMap.end()) { it.value().resetOutValue(); }
return;
}
TimePos start = TimePos(qMin(tick0, tick1));
TimePos end = TimePos(qMax(tick0, tick1));
for (auto it = m_timeMap.lowerBound(start), endIt = m_timeMap.upperBound(end); it != endIt; ++it)
{
it.value().resetOutValue();
}
}
void AutomationPattern::recordValue(TimePos time, float value)
{
QMutexLocker m(&m_patternMutex);
if( value != m_lastRecordedValue )
{
putValue( time, value, true );
@@ -270,7 +423,7 @@ void AutomationPattern::recordValue(TimePos time, float value)
}
else if( valueAt( time ) != value )
{
removeValue( time );
removeNode(time);
}
}
@@ -279,24 +432,44 @@ void AutomationPattern::recordValue(TimePos time, float value)
/**
* @brief Set the position of the point that is being dragged.
* Calling this function will also automatically set m_dragging to true,
* which applyDragValue() have to be called to m_dragging.
* @param the time(x position) of the point being dragged
* @param the value(y position) of the point being dragged
* @param true to snip x position
* @return
* Calling this function will also automatically set m_dragging to true.
* When applyDragValue() is called, m_dragging is set back to false.
* @param TimePos of the node being dragged
* @param Float with the value to assign to the point being dragged
* @param Boolean. True to snip x position
* @param Boolean. True to ignore unquantized surrounding nodes
* @return TimePos with current time of the dragged value
*/
TimePos AutomationPattern::setDragValue( const TimePos & time,
const float value,
const bool quantPos,
const bool controlKey )
TimePos AutomationPattern::setDragValue(
const TimePos & time,
const float value,
const bool quantPos,
const bool controlKey
)
{
if( m_dragging == false )
QMutexLocker m(&m_patternMutex);
if (m_dragging == false)
{
TimePos newTime = quantPos ?
Note::quantized( time, quantization() ) :
time;
this->removeValue( newTime );
TimePos newTime = quantPos ? Note::quantized(time, quantization()) : time;
// We will keep the same outValue only if it's different from the
// inValue
m_dragKeepOutValue = false;
// Check if we already have a node on the position we are dragging
// and if we do, store the outValue so the discrete jump can be kept
timeMap::iterator it = m_timeMap.find(newTime);
if (it != m_timeMap.end())
{
if (OFFSET(it) != 0)
{
m_dragKeepOutValue = true;
m_dragOutValue = OUTVAL(it);
}
}
this->removeNode(newTime);
m_oldTimeMap = m_timeMap;
m_dragging = true;
}
@@ -304,13 +477,14 @@ TimePos AutomationPattern::setDragValue( const TimePos & time,
//Restore to the state before it the point were being dragged
m_timeMap = m_oldTimeMap;
for( timeMap::const_iterator it = m_timeMap.begin(); it != m_timeMap.end(); ++it )
generateTangents();
if (m_dragKeepOutValue)
{
generateTangents( it, 3 );
return this->putValues(time, value, m_dragOutValue, quantPos, controlKey);
}
return this->putValue( time, value, quantPos, controlKey );
return this->putValue(time, value, quantPos, controlKey);
}
@@ -321,6 +495,8 @@ TimePos AutomationPattern::setDragValue( const TimePos & time,
*/
void AutomationPattern::applyDragValue()
{
QMutexLocker m(&m_patternMutex);
m_dragging = false;
}
@@ -329,19 +505,24 @@ void AutomationPattern::applyDragValue()
float AutomationPattern::valueAt( const TimePos & _time ) const
{
QMutexLocker m(&m_patternMutex);
if( m_timeMap.isEmpty() )
{
return 0;
}
if( m_timeMap.contains( _time ) )
// If we have a node at that time, just return its value
if (m_timeMap.contains(_time))
{
return m_timeMap[_time];
// When the time is exactly the node's time, we want the inValue
return m_timeMap[_time].getInValue();
}
// lowerBound returns next value with greater key, therefore we take
// the previous element to get the current value
timeMap::ConstIterator v = m_timeMap.lowerBound( _time );
// lowerBound returns next value with equal or greater key. Since we already
// checked if the key contains a node, we know the returned node has a greater
// key than _time. Therefore we take the previous element to calculate the current value
timeMap::const_iterator v = m_timeMap.lowerBound(_time);
if( v == m_timeMap.begin() )
{
@@ -349,26 +530,37 @@ float AutomationPattern::valueAt( const TimePos & _time ) const
}
if( v == m_timeMap.end() )
{
return (v-1).value();
// When the time is after the last node, we want the outValue of it
return OUTVAL(v - 1);
}
return valueAt( v-1, _time - (v-1).key() );
return valueAt(v - 1, _time - POS(v - 1));
}
// This method will get the value at an offset from a node, so we use the outValue of
// that node and the inValue of the next node for the calculations.
float AutomationPattern::valueAt( timeMap::const_iterator v, int offset ) const
{
if( m_progressionType == DiscreteProgression || v == m_timeMap.end() )
QMutexLocker m(&m_patternMutex);
// We never use it with offset 0, but doesn't hurt to return a correct
// value if we do
if (offset == 0) { return INVAL(v); }
if (m_progressionType == DiscreteProgression)
{
return v.value();
return OUTVAL(v);
}
else if( m_progressionType == LinearProgression )
{
float slope = ((v+1).value() - v.value()) /
((v+1).key() - v.key());
return v.value() + offset * slope;
float slope =
(INVAL(v + 1) - OUTVAL(v))
/ (POS(v + 1) - POS(v));
return OUTVAL(v) + offset * slope;
}
else /* CubicHermiteProgression */
{
@@ -380,15 +572,17 @@ float AutomationPattern::valueAt( timeMap::const_iterator v, int offset ) const
// value: y. To make this work we map the values of x that this
// segment spans to values of t for t = 0.0 -> 1.0 and scale the
// tangents _m1 and _m2
int numValues = ((v+1).key() - v.key());
int numValues = (POS(v + 1) - POS(v));
float t = (float) offset / (float) numValues;
float m1 = (m_tangents[v.key()]) * numValues * m_tension;
float m2 = (m_tangents[(v+1).key()]) * numValues * m_tension;
float m1 = OUTTAN(v) * numValues * m_tension;
float m2 = INTAN(v + 1) * numValues * m_tension;
return ( 2*pow(t,3) - 3*pow(t,2) + 1 ) * v.value()
+ ( pow(t,3) - 2*pow(t,2) + t) * m1
+ ( -2*pow(t,3) + 3*pow(t,2) ) * (v+1).value()
+ ( pow(t,3) - pow(t,2) ) * m2;
auto t2 = pow(t, 2);
auto t3 = pow(t, 3);
return (2 * t3 - 3 * t2 + 1) * OUTVAL(v)
+ (t3 - 2 * t2 + t) * m1
+ (-2 * t3 + 3 * t2) * INVAL(v + 1)
+ (t3 - t2) * m2;
}
}
@@ -397,13 +591,15 @@ float AutomationPattern::valueAt( timeMap::const_iterator v, int offset ) const
float *AutomationPattern::valuesAfter( const TimePos & _time ) const
{
timeMap::ConstIterator v = m_timeMap.lowerBound( _time );
QMutexLocker m(&m_patternMutex);
timeMap::const_iterator v = m_timeMap.lowerBound(_time);
if( v == m_timeMap.end() || (v+1) == m_timeMap.end() )
{
return NULL;
}
int numValues = (v+1).key() - v.key();
int numValues = POS(v + 1) - POS(v);
float *ret = new float[numValues];
for( int i = 0; i < numValues; i++ )
@@ -417,36 +613,31 @@ float *AutomationPattern::valuesAfter( const TimePos & _time ) const
void AutomationPattern::flipY( int min, int max )
void AutomationPattern::flipY(int min, int max)
{
timeMap tempMap = m_timeMap;
timeMap::ConstIterator iterate = m_timeMap.lowerBound(0);
float tempValue = 0;
QMutexLocker m(&m_patternMutex);
int numPoints = 0;
bool changedTimeMap = false;
for( int i = 0; ( iterate + i + 1 ) != m_timeMap.end() && ( iterate + i ) != m_timeMap.end() ; i++)
for (auto it = m_timeMap.begin(); it != m_timeMap.end(); ++it)
{
numPoints++;
// Get distance from IN/OUT values to max value
float inValDist = max - INVAL(it);
float outValDist = max - OUTVAL(it);
// To flip, that will be the new distance between
// the IN/OUT values and the min value
it.value().setInValue(min + inValDist);
it.value().setOutValue(min + outValDist);
changedTimeMap = true;
}
for( int i = 0; i <= numPoints; i++ )
if (changedTimeMap)
{
if ( min < 0 )
{
tempValue = valueAt( ( iterate + i ).key() ) * -1;
putValue( TimePos( (iterate + i).key() ) , tempValue, false);
}
else
{
tempValue = max - valueAt( ( iterate + i ).key() );
putValue( TimePos( (iterate + i).key() ) , tempValue, false);
}
generateTangents();
emit dataChanged();
}
generateTangents();
emit dataChanged();
}
@@ -460,69 +651,98 @@ void AutomationPattern::flipY()
void AutomationPattern::flipX( int length )
void AutomationPattern::flipX(int length)
{
QMutexLocker m(&m_patternMutex);
timeMap::const_iterator it = m_timeMap.lowerBound(0);
if (it == m_timeMap.end()) { return; }
// Temporary map where we will store the flipped version
// of our pattern
timeMap tempMap;
timeMap::ConstIterator iterate = m_timeMap.lowerBound(0);
float tempValue = 0;
int numPoints = 0;
float tempOutValue = 0;
for( int i = 0; ( iterate + i + 1 ) != m_timeMap.end() && ( iterate + i ) != m_timeMap.end() ; i++)
// We know the QMap isn't empty, making this safe:
float realLength = m_timeMap.lastKey();
// If we have a positive length, we want to flip the area covered by that
// length, even if it goes beyond the pattern. A negative length means that
// we just want to flip the nodes we have
if (length >= 0 && length != realLength)
{
numPoints++;
}
float realLength = ( iterate + numPoints ).key();
if ( length != -1 && length != realLength)
{
if ( realLength < length )
// If length to be flipped is bigger than the real length
if (realLength < length)
{
tempValue = valueAt( ( iterate + numPoints ).key() );
putValue( TimePos( length ) , tempValue, false);
numPoints++;
for( int i = 0; i <= numPoints; i++ )
// We are flipping an area that goes beyond the last node. So we add a node to the
// beginning of the flipped timeMap representing the value of the end of the area
tempValue = valueAt(length);
tempMap[0] = AutomationNode(this, tempValue, 0);
// Now flip the nodes we have in relation to the length
do
{
tempValue = valueAt( ( iterate + i ).key() );
TimePos newTime = TimePos( length - ( iterate + i ).key() );
tempMap[newTime] = tempValue;
}
// We swap the inValue and outValue when flipping horizontally
tempValue = OUTVAL(it);
tempOutValue = INVAL(it);
TimePos newTime = TimePos(length - POS(it));
tempMap[newTime] = AutomationNode(this, tempValue, tempOutValue, newTime);
++it;
} while (it != m_timeMap.end());
}
else
else // If the length to be flipped is smaller than the real length
{
for( int i = 0; i <= numPoints; i++ )
do
{
tempValue = valueAt( ( iterate + i ).key() );
TimePos newTime;
if ( ( iterate + i ).key() <= length )
// Only flips the length to be flipped and keep the remaining values in place
// We also only swap the inValue and outValue if we are flipping the node
if (POS(it) <= length)
{
newTime = TimePos( length - ( iterate + i ).key() );
newTime = length - POS(it);
tempValue = OUTVAL(it);
tempOutValue = INVAL(it);
}
else
{
newTime = TimePos( ( iterate + i ).key() );
newTime = POS(it);
tempValue = INVAL(it);
tempOutValue = OUTVAL(it);
}
tempMap[newTime] = tempValue;
}
tempMap[newTime] = AutomationNode(this, tempValue, tempOutValue, newTime);
++it;
} while (it != m_timeMap.end());
}
}
else
else // Length to be flipped is the same as the real length
{
for( int i = 0; i <= numPoints; i++ )
do
{
tempValue = valueAt( ( iterate + i ).key() );
cleanObjects();
TimePos newTime = TimePos( realLength - ( iterate + i ).key() );
tempMap[newTime] = tempValue;
}
// Swap the inValue and outValue
tempValue = OUTVAL(it);
tempOutValue = INVAL(it);
TimePos newTime = TimePos(realLength - POS(it));
tempMap[newTime] = AutomationNode(this, tempValue, tempOutValue, newTime);
++it;
} while (it != m_timeMap.end());
}
m_timeMap.clear();
m_timeMap = tempMap;
cleanObjects();
generateTangents();
emit dataChanged();
}
@@ -532,6 +752,8 @@ void AutomationPattern::flipX( int length )
void AutomationPattern::saveSettings( QDomDocument & _doc, QDomElement & _this )
{
QMutexLocker m(&m_patternMutex);
_this.setAttribute( "pos", startPosition() );
_this.setAttribute( "len", length() );
_this.setAttribute( "name", name() );
@@ -548,8 +770,9 @@ void AutomationPattern::saveSettings( QDomDocument & _doc, QDomElement & _this )
it != m_timeMap.end(); ++it )
{
QDomElement element = _doc.createElement( "time" );
element.setAttribute( "pos", it.key() );
element.setAttribute( "value", it.value() );
element.setAttribute("pos", POS(it));
element.setAttribute("value", INVAL(it));
element.setAttribute("outValue", OUTVAL(it));
_this.appendChild( element );
}
@@ -571,6 +794,8 @@ void AutomationPattern::saveSettings( QDomDocument & _doc, QDomElement & _this )
void AutomationPattern::loadSettings( const QDomElement & _this )
{
QMutexLocker m(&m_patternMutex);
clear();
movePosition( _this.attribute( "pos" ).toInt() );
@@ -590,8 +815,11 @@ void AutomationPattern::loadSettings( const QDomElement & _this )
}
if( element.tagName() == "time" )
{
m_timeMap[element.attribute( "pos" ).toInt()]
= LocaleHelper::toFloat(element.attribute("value"));
int timeMapPos = element.attribute("pos").toInt();
float timeMapInValue = LocaleHelper::toFloat(element.attribute("value"));
float timeMapOutValue = LocaleHelper::toFloat(element.attribute("outValue"));
m_timeMap[timeMapPos] = AutomationNode(this, timeMapInValue, timeMapOutValue, timeMapPos);
}
else if( element.tagName() == "object" )
{
@@ -623,6 +851,8 @@ void AutomationPattern::loadSettings( const QDomElement & _this )
const QString AutomationPattern::name() const
{
QMutexLocker m(&m_patternMutex);
if( !TrackContentObject::name().isEmpty() )
{
return TrackContentObject::name();
@@ -639,6 +869,8 @@ const QString AutomationPattern::name() const
TrackContentObjectView * AutomationPattern::createView( TrackView * _tv )
{
QMutexLocker m(&m_patternMutex);
return new AutomationPatternView( this, _tv );
}
@@ -679,8 +911,9 @@ bool AutomationPattern::isAutomated( const AutomatableModel * _m )
}
/*! \brief returns a list of all the automation patterns everywhere that are connected to a specific model
* \param _m the model we want to look for
/**
* @brief returns a list of all the automation patterns that are connected to a specific model
* @param _m the model we want to look for
*/
QVector<AutomationPattern *> AutomationPattern::patternsForModel( const AutomatableModel * _m )
{
@@ -817,8 +1050,9 @@ void AutomationPattern::resolveAllIDs()
void AutomationPattern::clear()
{
QMutexLocker m(&m_patternMutex);
m_timeMap.clear();
m_tangents.clear();
emit dataChanged();
}
@@ -828,6 +1062,8 @@ void AutomationPattern::clear()
void AutomationPattern::objectDestroyed( jo_id_t _id )
{
QMutexLocker m(&m_patternMutex);
// TODO: distict between temporary removal (e.g. LADSPA controls
// when switching samplerate) and real deletions because in the latter
// case we had to remove ourselves if we're the global automation
@@ -854,6 +1090,8 @@ void AutomationPattern::objectDestroyed( jo_id_t _id )
void AutomationPattern::cleanObjects()
{
QMutexLocker m(&m_patternMutex);
for( objectVector::iterator it = m_objects.begin(); it != m_objects.end(); )
{
if( *it )
@@ -878,12 +1116,18 @@ void AutomationPattern::generateTangents()
void AutomationPattern::generateTangents( timeMap::const_iterator it,
int numToGenerate )
// We have two tangents, one for the left side of the node and one for the right side
// of the node (in case we have discrete value jumps in the middle of a curve).
// If the inValue and outValue of a node are the same, consequently the inTangent and
// outTangent values of the node will be the same too.
void AutomationPattern::generateTangents(timeMap::iterator it, int numToGenerate)
{
QMutexLocker m(&m_patternMutex);
if( m_timeMap.size() < 2 && numToGenerate > 0 )
{
m_tangents[it.key()] = 0;
it.value().setInTangent(0);
it.value().setOutTangent(0);
return;
}
@@ -891,26 +1135,48 @@ void AutomationPattern::generateTangents( timeMap::const_iterator it,
{
if( it == m_timeMap.begin() )
{
m_tangents[it.key()] =
( (it+1).value() - (it).value() ) /
( (it+1).key() - (it).key() );
// On the first node there's no curve behind it, so we will only calculate the outTangent
// and inTangent will be set to 0.
float tangent = (INVAL(it + 1) - OUTVAL(it)) / (POS(it + 1) - POS(it));
it.value().setInTangent(0);
it.value().setOutTangent(tangent);
}
else if( it+1 == m_timeMap.end() )
{
m_tangents[it.key()] = 0;
// Previously, the last value's tangent was always set to 0. That logic was kept for both tangents
// of the last node
it.value().setInTangent(0);
it.value().setOutTangent(0);
return;
}
else
{
m_tangents[it.key()] =
( (it+1).value() - (it-1).value() ) /
( (it+1).key() - (it-1).key() );
// When we are in a node that is in the middle of two other nodes, we need to check if we
// have a discrete jump at this node. If we do not, then we can calculate the tangents normally.
// If we do have a discrete jump, then we have to calculate the tangents differently for each side
// of the curve.
// TODO: This behavior means that a very small difference between the inValue and outValue can
// result in a big change in the curve. In the future, allowing the user to manually adjust
// the tangents would be better.
float inTangent;
float outTangent;
if (OFFSET(it) == 0)
{
inTangent = (INVAL(it + 1) - OUTVAL(it - 1)) / (POS(it + 1) - POS(it - 1));
it.value().setInTangent(inTangent);
// inTangent == outTangent in this case
it.value().setOutTangent(inTangent);
}
else
{
// Calculate the left side of the curve
inTangent = (INVAL(it) - OUTVAL(it - 1)) / (POS(it) - POS(it - 1));
// Calculate the right side of the curve
outTangent = (INVAL(it + 1) - OUTVAL(it)) / (POS(it + 1) - POS(it));
it.value().setInTangent(inTangent);
it.value().setOutTangent(outTangent);
}
}
it++;
}
}

View File

@@ -3,6 +3,7 @@ set(LMMS_SRCS
core/AutomatableModel.cpp
core/AutomationPattern.cpp
core/AutomationNode.cpp
core/BandLimitedWave.cpp
core/base64.cpp
core/BBTrackContainer.cpp

View File

@@ -59,7 +59,8 @@ const std::vector<DataFile::UpgradeMethod> DataFile::UPGRADE_METHODS = {
&DataFile::upgrade_0_4_0_beta1 , &DataFile::upgrade_0_4_0_rc2,
&DataFile::upgrade_1_0_99 , &DataFile::upgrade_1_1_0,
&DataFile::upgrade_1_1_91 , &DataFile::upgrade_1_2_0_rc3,
&DataFile::upgrade_1_3_0 , &DataFile::upgrade_noHiddenClipNames
&DataFile::upgrade_1_3_0 , &DataFile::upgrade_noHiddenClipNames,
&DataFile::upgrade_automationNodes
};
// Vector of all versions that have upgrade routines.
@@ -1385,6 +1386,32 @@ void DataFile::upgrade_noHiddenClipNames()
}
}
void DataFile::upgrade_automationNodes()
{
QDomNodeList autoPatterns = elementsByTagName("automationpattern");
// Go through all automation patterns
for (int i = 0; i < autoPatterns.size(); ++i)
{
QDomElement autoPattern = autoPatterns.item(i).toElement();
// On each automation pattern, get all <time> elements
QDomNodeList times = autoPattern.elementsByTagName("time");
// Loop through all <time> elements and change what we need
for (int j=0; j < times.size(); ++j)
{
QDomElement el = times.item(j).toElement();
float value = LocaleHelper::toFloat(el.attribute("value"));
// inValue will be equal to "value" and outValue will
// be set to the same
el.setAttribute("outValue", value);
}
}
}
void DataFile::upgrade()
{

View File

@@ -306,47 +306,54 @@ void AutomationPatternView::paintEvent( QPaintEvent * )
{
if( it+1 == m_pat->getTimeMap().end() )
{
const float x1 = x_base + it.key() * ppTick;
const float x1 = x_base + POS(it) * ppTick;
const float x2 = (float)( width() - TCO_BORDER_WIDTH );
if( x1 > ( width() - TCO_BORDER_WIDTH ) ) break;
// We are drawing the space after the last node, so we use the outValue
if( gradient() )
{
p.fillRect( QRectF( x1, 0.0f, x2 - x1, it.value() ), lin2grad );
p.fillRect(QRectF(x1, 0.0f, x2 - x1, OUTVAL(it)), lin2grad);
}
else
{
p.fillRect( QRectF( x1, 0.0f, x2 - x1, it.value() ), col );
p.fillRect(QRectF(x1, 0.0f, x2 - x1, OUTVAL(it)), col);
}
break;
}
float *values = m_pat->valuesAfter( it.key() );
float *values = m_pat->valuesAfter(POS(it));
// We are creating a path to draw a polygon representing the values between two
// nodes. When we have two nodes with discrete progression, we will basically have
// a rectangle with the outValue of the first node (that's why nextValue will match
// the outValue of the current node). When we have nodes with linear or cubic progression
// the value of the end of the shape between the two nodes will be the inValue of
// the next node.
float nextValue;
if( m_pat->progressionType() == AutomationPattern::DiscreteProgression )
{
nextValue = it.value();
nextValue = OUTVAL(it);
}
else
{
nextValue = ( it + 1 ).value();
nextValue = INVAL(it + 1);
}
QPainterPath path;
QPointF origin = QPointF( x_base + it.key() * ppTick, 0.0f );
QPointF origin = QPointF(x_base + POS(it) * ppTick, 0.0f);
path.moveTo( origin );
path.moveTo( QPointF( x_base + it.key() * ppTick,values[0] ) );
path.moveTo(QPointF(x_base + POS(it) * ppTick,values[0]));
float x;
for( int i = it.key() + 1; i < ( it + 1 ).key(); i++ )
for (int i = POS(it) + 1; i < POS(it + 1); i++)
{
x = x_base + i * ppTick;
if( x > ( width() - TCO_BORDER_WIDTH ) ) break;
float value = values[ i - it.key() ];
float value = values[i - POS(it)];
path.lineTo( QPointF( x, value ) );
}
path.lineTo( x_base + ( ( it + 1 ).key() ) * ppTick, nextValue );
path.lineTo( x_base + ( ( it + 1 ).key() ) * ppTick, 0.0f );
path.lineTo(x_base + (POS(it + 1)) * ppTick, nextValue);
path.lineTo(x_base + (POS(it + 1)) * ppTick, 0.0f);
path.lineTo( origin );
if( gradient() )
@@ -474,18 +481,27 @@ void AutomationPatternView::scaleTimemapToFit( float oldMin, float oldMax )
return;
}
// TODO: Currently when rescaling the timeMap values to fit the new range of values (newMin and newMax)
// only the inValue is being considered and the outValue is being reset to the inValue (so discrete jumps
// are discarded). Possibly later we will want discrete jumps to be maintained so we will need to upgrade
// the logic to account for them.
for( AutomationPattern::timeMap::iterator it = m_pat->m_timeMap.begin();
it != m_pat->m_timeMap.end(); ++it )
{
if( *it < oldMin )
// If the values are out of the previous range, fix them so they are
// between oldMin and oldMax.
if (INVAL(it) < oldMin)
{
*it = oldMin;
it.value().setInValue(oldMin);
}
else if( *it > oldMax )
else if (INVAL(it) > oldMax)
{
*it = oldMax;
it.value().setInValue(oldMax);
}
*it = (*it-oldMin)*(newMax-newMin)/(oldMax-oldMin)+newMin;
// Calculate what the value would be proportionally in the new range
it.value().setInValue((INVAL(it) - oldMin) * (newMax - newMin) / (oldMax - oldMin) + newMin);
// Read earlier TODO comment: For now I'm discarding the discrete jumps during the rescaling
it.value().setOutValue(INVAL(it));
}
m_pat->generateTangents();

View File

File diff suppressed because it is too large Load Diff

View File

@@ -987,39 +987,71 @@ void PianoRoll::drawDetuningInfo( QPainter & _p, const Note * _n, int _x,
width() - m_whiteKeyWidth,
keyAreaBottom() - PR_TOP_MARGIN);
// Draw lines for the detuning automation, treating cubic hermit curves
// as straight lines for now. Also draw discrete jumps.
int old_x = 0;
int old_y = 0;
timeMap & map = _n->detuning()->automationPattern()->getTimeMap();
for( timeMap::ConstIterator it = map.begin(); it != map.end(); ++it )
for (timeMap::const_iterator it = map.begin(); it != map.end(); ++it)
{
int pos_ticks = it.key();
int pos_x = _x + pos_ticks * m_ppb / TimePos::ticksPerBar();
// Current node values
int cur_ticks = POS(it);
int cur_x = _x + cur_ticks * m_ppb / TimePos::ticksPerBar();
const float cur_level = INVAL(it);
int cur_y = middle_y - cur_level * m_keyLineHeight;
const float level = it.value();
int pos_y = middle_y - level * m_keyLineHeight;
if( old_x != 0 && old_y != 0 )
// First line to represent the inValue of the first node
if (it == map.begin())
{
switch( _n->detuning()->automationPattern()->progressionType() )
_p.drawLine(cur_x - 1, cur_y, cur_x + 1, cur_y);
_p.drawLine(cur_x, cur_y - 1, cur_x, cur_y + 1);
}
// All subsequent lines will take the outValue of the previous node
// and the inValue of the current node. It will also draw a vertical
// line if there was a discrete jump (from old_x,old_y to pre_x,pre_y)
else
{
// Previous node values (based on outValue). We just calculate
// the y level because the x will be the same as old_x.
const float pre_level = OUTVAL(it - 1);
int pre_y = middle_y - pre_level * m_keyLineHeight;
// Draws the line representing the discrete jump if there's one
if (old_y != pre_y)
{
case AutomationPattern::DiscreteProgression:
_p.drawLine( old_x, old_y, pos_x, old_y );
_p.drawLine( pos_x, old_y, pos_x, pos_y );
break;
case AutomationPattern::CubicHermiteProgression: /* TODO */
case AutomationPattern::LinearProgression:
_p.drawLine( old_x, old_y, pos_x, pos_y );
break;
_p.drawLine(old_x, old_y, old_x, pre_y);
}
// Now draw the lines representing the actual progression from one
// node to the other
switch (_n->detuning()->automationPattern()->progressionType())
{
case AutomationPattern::DiscreteProgression:
_p.drawLine(old_x, pre_y, cur_x, pre_y);
_p.drawLine(cur_x, pre_y, cur_x, cur_y);
break;
case AutomationPattern::CubicHermiteProgression: /* TODO */
case AutomationPattern::LinearProgression:
_p.drawLine(old_x, pre_y, cur_x, cur_y);
break;
}
// If we are in the last node and there's a discrete jump, we draw a
// vertical line representing it
if ((it + 1) == map.end())
{
const float last_level = OUTVAL(it);
if (cur_level != last_level)
{
int last_y = middle_y - last_level * m_keyLineHeight;
_p.drawLine(cur_x, cur_y, cur_x, last_y);
}
}
}
_p.drawLine( pos_x - 1, pos_y, pos_x + 1, pos_y );
_p.drawLine( pos_x, pos_y - 1, pos_x, pos_y + 1 );
old_x = pos_x;
old_y = pos_y;
old_x = cur_x;
old_y = cur_y;
}
}