diff --git a/data/themes/classic/edit_draw_outvalue.png b/data/themes/classic/edit_draw_outvalue.png new file mode 100644 index 00000000000..1cfdbf45daa Binary files /dev/null and b/data/themes/classic/edit_draw_outvalue.png differ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 860aa0da1ea..67192a75334 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -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); diff --git a/data/themes/default/edit_draw_outvalue.png b/data/themes/default/edit_draw_outvalue.png new file mode 100644 index 00000000000..74adf71ac3c Binary files /dev/null and b/data/themes/default/edit_draw_outvalue.png differ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index ce476f5a9ce..7d26d638532 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -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; diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index a4e9d528634..e66ab665bdf 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -26,7 +26,6 @@ #ifndef AUTOMATION_EDITOR_H #define AUTOMATION_EDITOR_H -#include #include #include @@ -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 @@ class AutomationEditor : public QWidget, public JournallingObject 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 @@ public slots: 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 @@ protected slots: { NONE, MOVE_VALUE, - SELECT_VALUES, - MOVE_SELECTION + ERASE_VALUES, + MOVE_OUTVALUE, + RESET_OUTVALUES, + DRAW_LINE } ; // some constants... @@ -187,7 +163,7 @@ protected slots: 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 @@ protected slots: FloatModel * m_tensionModel; - QMutex m_patternMutex; AutomationPattern * m_pattern; float m_minLevel; float m_maxLevel; @@ -219,13 +194,6 @@ protected slots: 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 @@ protected slots: 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 @@ protected slots: 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; diff --git a/include/AutomationNode.h b/include/AutomationNode.h new file mode 100644 index 00000000000..307c5ed36c7 --- /dev/null +++ b/include/AutomationNode.h @@ -0,0 +1,153 @@ +/* + * AutomationNode.h - Declaration of class AutomationNode, which contains + * all information about an automation node + * + * Copyright (c) 2020 Ian Caio + * + * 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 diff --git a/include/AutomationPattern.h b/include/AutomationPattern.h index aff1f26bf86..1a98f49568f 100644 --- a/include/AutomationPattern.h +++ b/include/AutomationPattern.h @@ -30,6 +30,7 @@ #include #include +#include "AutomationNode.h" #include "TrackContentObject.h" @@ -49,8 +50,8 @@ class LMMS_EXPORT AutomationPattern : public TrackContentObject CubicHermiteProgression } ; - typedef QMap timeMap; - typedef QVector > objectVector; + typedef QMap timeMap; + typedef QVector> objectVector; AutomationPattern( AutomationTrack * _auto_track ); AutomationPattern( const AutomationPattern & _pat_to_copy ); @@ -77,12 +78,25 @@ class LMMS_EXPORT AutomationPattern : public TrackContentObject 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 + ); + + TimePos putValues( + const TimePos & time, + const float inValue, + const float outValue, + const bool quantPos = true, + const bool ignoreSurroundingPoints = true + ); - void removeValue( const TimePos & time ); + 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 @@ class LMMS_EXPORT AutomationPattern : public TrackContentObject return m_timeMap; } - inline const timeMap & getTangents() const - { - return m_tangents; - } - - inline timeMap & getTangents() - { - return m_tangents; - } - inline float getMin() const { return firstObject()->minValue(); @@ -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 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 @@ public slots: static const float DEFAULT_MAX_VALUE; friend class AutomationPatternView; + friend class AutomationNode; } ; diff --git a/include/DataFile.h b/include/DataFile.h index 5d6ead5adb3..8ddb814f1b6 100644 --- a/include/DataFile.h +++ b/include/DataFile.h @@ -115,6 +115,7 @@ class LMMS_EXPORT DataFile : public QDomDocument 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 UPGRADE_METHODS; diff --git a/include/InlineAutomation.h b/include/InlineAutomation.h index 431ecbc81be..aa47d6e334f 100644 --- a/include/InlineAutomation.h +++ b/include/InlineAutomation.h @@ -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 @@ class InlineAutomation : public FloatModel, public sharedObject { 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; } diff --git a/src/core/AutomationNode.cpp b/src/core/AutomationNode.cpp new file mode 100644 index 00000000000..d289b2f216f --- /dev/null +++ b/src/core/AutomationNode.cpp @@ -0,0 +1,109 @@ +/* + * AutomationPattern.cpp - Implementation of class AutomationNode which + * holds information on a single automation pattern node + * + * Copyright (c) 2020 Ian Caio + * + * 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); +} diff --git a/src/core/AutomationPattern.cpp b/src/core/AutomationPattern.cpp index 3c6aa3e9c01..792ee3f5103 100644 --- a/src/core/AutomationPattern.cpp +++ b/src/core/AutomationPattern.cpp @@ -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 @@ -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((it-1).key()); + tick_t last_tick = static_cast(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,103 @@ 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; + + // Create a node or replace the existing one on newTime + m_timeMap[newTime] = AutomationNode(this, value, newTime); - m_timeMap[ newTime ] = value; - timeMap::const_iterator it = m_timeMap.find( 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() ) + if (it != m_timeMap.begin()) { --it; } + generateTangents(it, 3); + + updateLength(); + + emit dataChanged(); + + return newTime; +} + + + + +/** + * @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) { - --it; + // 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); + } } - generateTangents( it, 3 ); + if (it != m_timeMap.begin()) { --it; } + generateTangents(it, 3); updateLength(); @@ -241,13 +329,14 @@ TimePos AutomationPattern::putValue( const TimePos & time, -void AutomationPattern::removeValue( const TimePos & time ) +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 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; - - 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; + float m1 = OUTTAN(v) * numValues * m_tension; + float m2 = INTAN(v + 1) * numValues * m_tension; + + 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); - for( int i = 0; i <= numPoints; i++ ) - { + // 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); - 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); - } + changedTimeMap = true; } - generateTangents(); - emit dataChanged(); + if (changedTimeMap) + { + 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++) - { - numPoints++; - } - - float realLength = ( iterate + numPoints ).key(); + // We know the QMap isn't empty, making this safe: + float realLength = m_timeMap.lastKey(); - if ( length != -1 && length != realLength) + // 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) { - 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::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++; } } - - - - - diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 017ebba1426..dca1cdc3402 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 9dc413566d7..8bc48db1eb6 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -59,7 +59,8 @@ const std::vector 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