mirror of
https://github.com/LMMS/lmms.git
synced 2025-12-23 22:58:33 -05:00
Fix knob linking / refactor linking (#7883)
Closes #7869 This PR aims to fix linking bugs and it aims to make linking code faster. In the future I would like to replace controller and automation code with linked models, so it is essential for this feature to work as efficiently as possible. Before this PR: - AutomatableModels store a list of AutomatableModels that they are linked to. - setValue() and other functions make recursive calls to themself resulting in the #7869 crash. - Each AutomatableModel can unlink from other AutomatableModels, unlinking is the inverse operation to linking. After this PR: - AutomatableModels store a pointer to an other AutomatableModel making a linked list. The end is connected to the first element resulting in a "ring". - setValue() and others are now recursion free, the code runs for every linked model, more efficiently than before. - Each AutomatableModel can NOT unlink form other AutomatableModels, unlinking is NOT the inverse operation to linking. AutomatableModels can unlink themself from the linked list, they can not unlink themself from single models. --------- Co-authored-by: allejok96 <allejok96@gmail.com>
This commit is contained in:
76
data/themes/classic/edit_unlink.svg
Normal file
76
data/themes/classic/edit_unlink.svg
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 5.2916665 5.2916666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="chain400.png"
|
||||
inkscape:export-xdpi="1920"
|
||||
inkscape:export-ydpi="1920"
|
||||
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
|
||||
sodipodi:docname="chain peki 2.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#686868"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="true"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="2.2318058"
|
||||
inkscape:cy="14.539883"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458333"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="4"
|
||||
enabled="true"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="opacity:1;baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.3;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 2.9882813,2.9882813 a 0.2645835,0.2645835 0 0 0 0,0.3730468 l 1.5878906,1.5878906 a 0.2645835,0.2645835 0 0 0 0.3730468,0 0.2645835,0.2645835 0 0 0 0,-0.3730468 L 3.3613281,2.9882813 a 0.2645835,0.2645835 0 0 0 -0.3730468,0 z"
|
||||
id="path13"
|
||||
inkscape:label="down" />
|
||||
<path
|
||||
style="opacity:1;baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.3;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 0.34179688,0.34179688 a 0.2645835,0.2645835 0 0 0 0,0.37499999 L 1.9296875,2.3046875 a 0.2645835,0.2645835 0 0 0 0.375,0 0.2645835,0.2645835 0 0 0 0,-0.375 L 0.71679687,0.34179688 a 0.2645835,0.2645835 0 0 0 -0.37499999,0 z"
|
||||
id="path15"
|
||||
inkscape:label="up" />
|
||||
<path
|
||||
id="path11"
|
||||
style="baseline-shift:baseline;display:inline;overflow:visible;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.3;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1;stroke:#ffffff;stroke-opacity:1;stroke-width:0.13229167;stroke-dasharray:none"
|
||||
inkscape:label="cen"
|
||||
d="M 2.1171834 0.79271647 A 0.26460981 0.26460981 0 0 0 1.929598 0.87126465 L 1.7285767 1.072286 L 2.102714 1.4464233 L 2.2267375 1.3223999 L 2.5373128 1.3223999 L 3.0189372 1.8050578 L 3.3925578 1.4314372 L 2.8318685 0.87126465 A 0.26460981 0.26460981 0 0 0 2.6463501 0.79271647 L 2.1171834 0.79271647 z M 1.4474569 1.3534058 L 1.3534058 1.4474569 L 1.7280599 1.822111 L 1.822111 1.7280599 L 1.4474569 1.3534058 z M 1.072286 1.7285767 L 0.87126465 1.929598 A 0.26460981 0.26460981 0 0 0 0.79271647 2.1171834 L 0.79271647 2.6463501 A 0.26460981 0.26460981 0 0 0 0.87126465 2.8318685 L 1.4314372 3.3925578 L 1.8050578 3.0189372 L 1.3223999 2.5373128 L 1.3223999 2.2282878 L 1.4469401 2.1032308 L 1.072286 1.7285767 z M 3.8602295 1.8996257 L 3.4866089 2.2732463 L 3.96875 2.7559041 L 3.96875 3.0644124 L 3.843693 3.1894694 L 4.2178304 3.5636068 L 4.4198853 3.3615519 A 0.26460981 0.26460981 0 0 0 4.4979167 3.1760335 L 4.4979167 2.6463501 A 0.26460981 0.26460981 0 0 0 4.4198853 2.4587646 L 3.8602295 1.8996257 z M 3.5625732 3.4705892 L 3.4685221 3.5646403 L 3.8426595 3.9387777 L 3.9367106 3.8447266 L 3.5625732 3.4705892 z M 2.2732463 3.4866089 L 1.8996257 3.8602295 L 2.4587646 4.4198853 A 0.26460981 0.26460981 0 0 0 2.6463501 4.4979167 L 3.1760335 4.4979167 A 0.26460981 0.26460981 0 0 0 3.3615519 4.4198853 L 3.5615397 4.2198975 L 3.1874023 3.8457601 L 3.0644124 3.96875 L 2.7559041 3.96875 L 2.2732463 3.4866089 z " />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
76
data/themes/default/edit_unlink.svg
Normal file
76
data/themes/default/edit_unlink.svg
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 5.2916665 5.2916666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="chain400.png"
|
||||
inkscape:export-xdpi="1920"
|
||||
inkscape:export-ydpi="1920"
|
||||
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
|
||||
sodipodi:docname="chain peki 2.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#686868"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="true"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="2.2318058"
|
||||
inkscape:cy="14.539883"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458333"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="4"
|
||||
enabled="true"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="opacity:1;baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.3;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 2.9882813,2.9882813 a 0.2645835,0.2645835 0 0 0 0,0.3730468 l 1.5878906,1.5878906 a 0.2645835,0.2645835 0 0 0 0.3730468,0 0.2645835,0.2645835 0 0 0 0,-0.3730468 L 3.3613281,2.9882813 a 0.2645835,0.2645835 0 0 0 -0.3730468,0 z"
|
||||
id="path13"
|
||||
inkscape:label="down" />
|
||||
<path
|
||||
style="opacity:1;baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.3;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||
d="m 0.34179688,0.34179688 a 0.2645835,0.2645835 0 0 0 0,0.37499999 L 1.9296875,2.3046875 a 0.2645835,0.2645835 0 0 0 0.375,0 0.2645835,0.2645835 0 0 0 0,-0.375 L 0.71679687,0.34179688 a 0.2645835,0.2645835 0 0 0 -0.37499999,0 z"
|
||||
id="path15"
|
||||
inkscape:label="up" />
|
||||
<path
|
||||
id="path11"
|
||||
style="baseline-shift:baseline;display:inline;overflow:visible;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.3;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1;stroke:#ffffff;stroke-opacity:1;stroke-width:0.13229167;stroke-dasharray:none"
|
||||
inkscape:label="cen"
|
||||
d="M 2.1171834 0.79271647 A 0.26460981 0.26460981 0 0 0 1.929598 0.87126465 L 1.7285767 1.072286 L 2.102714 1.4464233 L 2.2267375 1.3223999 L 2.5373128 1.3223999 L 3.0189372 1.8050578 L 3.3925578 1.4314372 L 2.8318685 0.87126465 A 0.26460981 0.26460981 0 0 0 2.6463501 0.79271647 L 2.1171834 0.79271647 z M 1.4474569 1.3534058 L 1.3534058 1.4474569 L 1.7280599 1.822111 L 1.822111 1.7280599 L 1.4474569 1.3534058 z M 1.072286 1.7285767 L 0.87126465 1.929598 A 0.26460981 0.26460981 0 0 0 0.79271647 2.1171834 L 0.79271647 2.6463501 A 0.26460981 0.26460981 0 0 0 0.87126465 2.8318685 L 1.4314372 3.3925578 L 1.8050578 3.0189372 L 1.3223999 2.5373128 L 1.3223999 2.2282878 L 1.4469401 2.1032308 L 1.072286 1.7285767 z M 3.8602295 1.8996257 L 3.4866089 2.2732463 L 3.96875 2.7559041 L 3.96875 3.0644124 L 3.843693 3.1894694 L 4.2178304 3.5636068 L 4.4198853 3.3615519 A 0.26460981 0.26460981 0 0 0 4.4979167 3.1760335 L 4.4979167 2.6463501 A 0.26460981 0.26460981 0 0 0 4.4198853 2.4587646 L 3.8602295 1.8996257 z M 3.5625732 3.4705892 L 3.4685221 3.5646403 L 3.8426595 3.9387777 L 3.9367106 3.8447266 L 3.5625732 3.4705892 z M 2.2732463 3.4866089 L 1.8996257 3.8602295 L 2.4587646 4.4198853 A 0.26460981 0.26460981 0 0 0 2.6463501 4.4979167 L 3.1760335 4.4979167 A 0.26460981 0.26460981 0 0 0 3.3615519 4.4198853 L 3.5615397 4.2198975 L 3.1874023 3.8457601 L 3.0644124 3.96875 L 2.7559041 3.96875 L 2.2732463 3.4866089 z " />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -77,8 +77,6 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using AutoModelVector = std::vector<AutomatableModel*>;
|
||||
|
||||
enum class ScaleType
|
||||
{
|
||||
Linear,
|
||||
@@ -150,22 +148,26 @@ public:
|
||||
template<class T>
|
||||
inline T value( int frameOffset = 0 ) const
|
||||
{
|
||||
if (m_controllerConnection)
|
||||
// TODO
|
||||
// The `m_value` should only be updated whenever the Controller value changes,
|
||||
// instead of the Model calling `controller->currentValue()` every time.
|
||||
// This becomes even worse in the case of linked Models, where it has to
|
||||
// loop through the list of all links.
|
||||
|
||||
if (m_useControllerValue)
|
||||
{
|
||||
if (!m_useControllerValue)
|
||||
{
|
||||
return castValue<T>(m_value);
|
||||
}
|
||||
else
|
||||
if (m_controllerConnection)
|
||||
{
|
||||
return castValue<T>(controllerValue(frameOffset));
|
||||
}
|
||||
for (auto next = m_nextLink; next != this; next = next->m_nextLink)
|
||||
{
|
||||
if (next->controllerConnection() && next->useControllerValue())
|
||||
{
|
||||
return castValue<T>(fittedValue(next->controllerValue(frameOffset)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (hasLinkedModels())
|
||||
{
|
||||
return castValue<T>( controllerValue( frameOffset ) );
|
||||
}
|
||||
|
||||
return castValue<T>( m_value );
|
||||
}
|
||||
|
||||
@@ -211,8 +213,7 @@ public:
|
||||
|
||||
void setInitValue( const float value );
|
||||
|
||||
void setAutomatedValue( const float value );
|
||||
void setValue( const float value );
|
||||
void setValue(const float value, const bool isAutomated = false);
|
||||
|
||||
void incValue( int steps )
|
||||
{
|
||||
@@ -249,11 +250,10 @@ public:
|
||||
m_centerValue = centerVal;
|
||||
}
|
||||
|
||||
//! link @p m1 and @p m2, let @p m1 take the values of @p m2
|
||||
static void linkModels( AutomatableModel* m1, AutomatableModel* m2 );
|
||||
static void unlinkModels( AutomatableModel* m1, AutomatableModel* m2 );
|
||||
|
||||
void unlinkAllModels();
|
||||
//! link this to @p model, copying the value from @p model
|
||||
void linkToModel(AutomatableModel* model);
|
||||
//! @return number of other models linked to this
|
||||
size_t countLinks() const;
|
||||
|
||||
/**
|
||||
* @brief Saves settings (value, automation links and controller connections) of AutomatableModel into
|
||||
@@ -276,9 +276,9 @@ public:
|
||||
|
||||
virtual QString displayValue( const float val ) const = 0;
|
||||
|
||||
bool hasLinkedModels() const
|
||||
bool isLinked() const
|
||||
{
|
||||
return !m_linkedModels.empty();
|
||||
return m_nextLink != this;
|
||||
}
|
||||
|
||||
// a way to track changed values in the model and avoid using signals/slots - useful for speed-critical code.
|
||||
@@ -311,13 +311,14 @@ public:
|
||||
s_periodCounter = 0;
|
||||
}
|
||||
|
||||
bool useControllerValue()
|
||||
bool useControllerValue() const
|
||||
{
|
||||
return m_useControllerValue;
|
||||
}
|
||||
|
||||
public slots:
|
||||
virtual void reset();
|
||||
void unlink();
|
||||
void unlinkControllerConnection();
|
||||
void setUseControllerValue(bool b = true);
|
||||
|
||||
@@ -367,8 +368,14 @@ private:
|
||||
loadSettings( element, "value" );
|
||||
}
|
||||
|
||||
void linkModel( AutomatableModel* model );
|
||||
void unlinkModel( AutomatableModel* model );
|
||||
void setValueInternal(const float value);
|
||||
|
||||
//! linking is stored in a linked list ring
|
||||
//! @return the model whose `m_nextLink` is `this`,
|
||||
//! or `this` if there are no linked models
|
||||
AutomatableModel* getLastLinkedModel() const;
|
||||
//! @return true if the `model` is in the linked list
|
||||
bool isLinkedToModel(AutomatableModel* model) const;
|
||||
|
||||
//! @brief Scales @value from linear to logarithmic.
|
||||
//! Value should be within [0,1]
|
||||
@@ -389,16 +396,15 @@ private:
|
||||
float m_centerValue;
|
||||
|
||||
bool m_valueChanged;
|
||||
|
||||
// currently unused?
|
||||
float m_oldValue;
|
||||
int m_setValueDepth;
|
||||
float m_oldValue; //!< used by valueBuffer for interpolation
|
||||
|
||||
// used to determine if step size should be applied strictly (ie. always)
|
||||
// or only when value set from gui (default)
|
||||
bool m_hasStrictStepSize;
|
||||
|
||||
AutoModelVector m_linkedModels;
|
||||
//! an `AutomatableModel` can be linked together with others in a linked list
|
||||
//! the list has no end, the last model is connected to the first forming a ring
|
||||
AutomatableModel* m_nextLink;
|
||||
|
||||
|
||||
//! NULL if not appended to controller, otherwise connection info
|
||||
|
||||
@@ -98,7 +98,6 @@ public slots:
|
||||
void execConnectionDialog();
|
||||
void removeConnection();
|
||||
void editSongGlobalAutomation();
|
||||
void unlinkAllModels();
|
||||
void removeSongGlobalAutomation();
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -1052,8 +1052,8 @@ void ManageVestigeInstrumentView::syncPlugin( void )
|
||||
std::snprintf(paramStr.data(), paramStr.size(), "param%d", i);
|
||||
s_dumpValues = dump[paramStr.data()].split(":");
|
||||
float f_value = LocaleHelper::toFloat(s_dumpValues.at(2));
|
||||
m_vi->knobFModel[ i ]->setAutomatedValue( f_value );
|
||||
m_vi->knobFModel[ i ]->setInitValue( f_value );
|
||||
m_vi->knobFModel[i]->setValue(f_value, true);
|
||||
m_vi->knobFModel[i]->setInitValue(f_value);
|
||||
}
|
||||
}
|
||||
syncParameterText();
|
||||
|
||||
@@ -455,8 +455,8 @@ void ManageVSTEffectView::syncPlugin()
|
||||
std::snprintf(paramStr.data(), paramStr.size(), "param%d", i);
|
||||
s_dumpValues = dump[paramStr.data()].split(":");
|
||||
float f_value = LocaleHelper::toFloat(s_dumpValues.at(2));
|
||||
m_vi2->knobFModel[ i ]->setAutomatedValue( f_value );
|
||||
m_vi2->knobFModel[ i ]->setInitValue( f_value );
|
||||
m_vi2->knobFModel[i]->setValue(f_value, true);
|
||||
m_vi2->knobFModel[i]->setInitValue(f_value);
|
||||
}
|
||||
}
|
||||
syncParameterText();
|
||||
|
||||
@@ -53,8 +53,9 @@ AutomatableModel::AutomatableModel(
|
||||
m_range( max - min ),
|
||||
m_centerValue( m_minValue ),
|
||||
m_valueChanged( false ),
|
||||
m_setValueDepth( 0 ),
|
||||
m_oldValue(val),
|
||||
m_hasStrictStepSize( false ),
|
||||
m_nextLink(this),
|
||||
m_controllerConnection( nullptr ),
|
||||
m_valueBuffer( static_cast<int>( Engine::audioEngine()->framesPerPeriod() ) ),
|
||||
m_lastUpdatedPeriod( -1 ),
|
||||
@@ -71,13 +72,9 @@ AutomatableModel::AutomatableModel(
|
||||
|
||||
AutomatableModel::~AutomatableModel()
|
||||
{
|
||||
while( m_linkedModels.empty() == false )
|
||||
{
|
||||
m_linkedModels.back()->unlinkModel(this);
|
||||
m_linkedModels.erase( m_linkedModels.end() - 1 );
|
||||
}
|
||||
unlink();
|
||||
|
||||
if( m_controllerConnection )
|
||||
if (m_controllerConnection)
|
||||
{
|
||||
delete m_controllerConnection;
|
||||
}
|
||||
@@ -294,41 +291,42 @@ void AutomatableModel::loadSettings( const QDomElement& element, const QString&
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::setValue( const float value )
|
||||
void AutomatableModel::setValue(const float value, const bool isAutomated)
|
||||
{
|
||||
if (fittedValue(value) == m_value)
|
||||
{
|
||||
// TODO check why we need this signal, is there a better solution?
|
||||
if (!isAutomated) { emit dataUnchanged(); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAutomated) { addJournalCheckPoint(); }
|
||||
|
||||
// set value for this and the other linked models
|
||||
setValueInternal(value);
|
||||
for (auto model = m_nextLink; model != this; model = model->m_nextLink)
|
||||
{
|
||||
model->setValueInternal(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::setValueInternal(const float value)
|
||||
{
|
||||
m_oldValue = m_value;
|
||||
++m_setValueDepth;
|
||||
const float old_val = m_value;
|
||||
m_value = fittedValue(value);
|
||||
|
||||
m_value = fittedValue( value );
|
||||
if( old_val != m_value )
|
||||
if (m_oldValue != m_value)
|
||||
{
|
||||
// add changes to history so user can undo it
|
||||
addJournalCheckPoint();
|
||||
|
||||
// notify linked models
|
||||
for (const auto& linkedModel : m_linkedModels)
|
||||
{
|
||||
if (linkedModel->m_setValueDepth < 1 && linkedModel->fittedValue(value) != linkedModel->m_value)
|
||||
{
|
||||
bool journalling = linkedModel->testAndSetJournalling(isJournalling());
|
||||
linkedModel->setValue(value);
|
||||
linkedModel->setJournalling(journalling);
|
||||
}
|
||||
}
|
||||
m_valueChanged = true;
|
||||
emit dataChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
emit dataUnchanged();
|
||||
}
|
||||
--m_setValueDepth;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
template<class T> T AutomatableModel::logToLinearScale( T value ) const
|
||||
{
|
||||
return castValue<T>( lmms::logToLinearScale( minValue<float>(), maxValue<float>(), static_cast<float>( value ) ) );
|
||||
@@ -362,34 +360,6 @@ void AutomatableModel::roundAt( T& value, const T& where ) const
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::setAutomatedValue( const float value )
|
||||
{
|
||||
setUseControllerValue(false);
|
||||
|
||||
m_oldValue = m_value;
|
||||
++m_setValueDepth;
|
||||
const float oldValue = m_value;
|
||||
|
||||
const float scaled_value = scaledValue( value );
|
||||
|
||||
m_value = fittedValue( scaled_value );
|
||||
|
||||
if( oldValue != m_value )
|
||||
{
|
||||
// notify linked models
|
||||
for (const auto& linkedModel : m_linkedModels)
|
||||
{
|
||||
if (!(linkedModel->controllerConnection()) && linkedModel->m_setValueDepth < 1 &&
|
||||
linkedModel->fittedValue(m_value) != linkedModel->m_value)
|
||||
{
|
||||
linkedModel->setAutomatedValue(value);
|
||||
}
|
||||
}
|
||||
m_valueChanged = true;
|
||||
emit dataChanged();
|
||||
}
|
||||
--m_setValueDepth;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -459,79 +429,67 @@ float AutomatableModel::fittedValue( float value ) const
|
||||
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::linkModel( AutomatableModel* model )
|
||||
AutomatableModel* AutomatableModel::getLastLinkedModel() const
|
||||
{
|
||||
auto containsModel = std::find(m_linkedModels.begin(), m_linkedModels.end(), model) != m_linkedModels.end();
|
||||
if (!containsModel && model != this)
|
||||
for (auto model = m_nextLink; ; model = model->m_nextLink)
|
||||
{
|
||||
m_linkedModels.push_back( model );
|
||||
|
||||
if( !model->hasLinkedModels() )
|
||||
{
|
||||
QObject::connect( this, SIGNAL(dataChanged()),
|
||||
model, SIGNAL(dataChanged()), Qt::DirectConnection );
|
||||
}
|
||||
// The last model in the circular reference links back to this
|
||||
if (model->m_nextLink == this) { return model; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::unlinkModel( AutomatableModel* model )
|
||||
bool AutomatableModel::isLinkedToModel(AutomatableModel* model) const
|
||||
{
|
||||
auto it = std::find(m_linkedModels.begin(), m_linkedModels.end(), model);
|
||||
if( it != m_linkedModels.end() )
|
||||
if (model == this) { return true; }
|
||||
for (auto next = m_nextLink; next != this; next = next->m_nextLink)
|
||||
{
|
||||
m_linkedModels.erase( it );
|
||||
if (next == model) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t AutomatableModel::countLinks() const
|
||||
{
|
||||
size_t output = 0;
|
||||
for (auto model = m_nextLink; model != this; model = model->m_nextLink)
|
||||
{
|
||||
output++;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::linkModels( AutomatableModel* model1, AutomatableModel* model2 )
|
||||
void AutomatableModel::linkToModel(AutomatableModel* other)
|
||||
{
|
||||
auto model1ContainsModel2 = std::find(model1->m_linkedModels.begin(), model1->m_linkedModels.end(), model2) != model1->m_linkedModels.end();
|
||||
if (!model1ContainsModel2 && model1 != model2)
|
||||
if (isLinkedToModel(other)) { return; }
|
||||
|
||||
if (valueBuffer() && other->valueBuffer())
|
||||
{
|
||||
// copy data
|
||||
model1->m_value = model2->m_value;
|
||||
if (model1->valueBuffer() && model2->valueBuffer())
|
||||
{
|
||||
std::copy_n(model2->valueBuffer()->data(),
|
||||
model1->valueBuffer()->length(),
|
||||
model1->valueBuffer()->data());
|
||||
}
|
||||
// send dataChanged() before linking (because linking will
|
||||
// connect the two dataChanged() signals)
|
||||
emit model1->dataChanged();
|
||||
// finally: link the models
|
||||
model1->linkModel( model2 );
|
||||
model2->linkModel( model1 );
|
||||
std::copy_n(other->valueBuffer()->data(),
|
||||
valueBuffer()->length(),
|
||||
valueBuffer()->data());
|
||||
}
|
||||
// copy data from other to this
|
||||
setValue(other->m_value);
|
||||
// link the models
|
||||
AutomatableModel* thisEnd = getLastLinkedModel();
|
||||
AutomatableModel* otherEnd = other->getLastLinkedModel();
|
||||
thisEnd->m_nextLink = other;
|
||||
otherEnd->m_nextLink = this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::unlinkModels( AutomatableModel* model1, AutomatableModel* model2 )
|
||||
void AutomatableModel::unlink()
|
||||
{
|
||||
model1->unlinkModel( model2 );
|
||||
model2->unlinkModel( model1 );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void AutomatableModel::unlinkAllModels()
|
||||
{
|
||||
for( AutomatableModel* model : m_linkedModels )
|
||||
{
|
||||
unlinkModels( this, model );
|
||||
}
|
||||
getLastLinkedModel()->m_nextLink = m_nextLink;
|
||||
m_nextLink = this;
|
||||
}
|
||||
|
||||
|
||||
@@ -555,11 +513,11 @@ void AutomatableModel::setControllerConnection( ControllerConnection* c )
|
||||
|
||||
float AutomatableModel::controllerValue( int frameOffset ) const
|
||||
{
|
||||
if( m_controllerConnection )
|
||||
assert(m_controllerConnection != nullptr);
|
||||
|
||||
float v = 0;
|
||||
switch (m_scaleType)
|
||||
{
|
||||
float v = 0;
|
||||
switch(m_scaleType)
|
||||
{
|
||||
case ScaleType::Linear:
|
||||
v = minValue<float>() + ( range() * controllerConnection()->currentValue( frameOffset ) );
|
||||
break;
|
||||
@@ -571,24 +529,14 @@ float AutomatableModel::controllerValue( int frameOffset ) const
|
||||
qFatal("AutomatableModel::controllerValue(int)"
|
||||
"lacks implementation for a scale type");
|
||||
break;
|
||||
}
|
||||
if (approximatelyEqual(m_step, 1) && m_hasStrictStepSize)
|
||||
{
|
||||
return std::round(v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
AutomatableModel* lm = m_linkedModels.front();
|
||||
if (lm->controllerConnection() && lm->useControllerValue())
|
||||
if (approximatelyEqual(m_step, 1) && m_hasStrictStepSize)
|
||||
{
|
||||
return fittedValue( lm->controllerValue( frameOffset ) );
|
||||
return std::round(v);
|
||||
}
|
||||
|
||||
return fittedValue( lm->m_value );
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
ValueBuffer * AutomatableModel::valueBuffer()
|
||||
{
|
||||
QMutexLocker m( &m_valueBufferMutex );
|
||||
@@ -602,6 +550,11 @@ ValueBuffer * AutomatableModel::valueBuffer()
|
||||
|
||||
float val = m_value; // make sure our m_value doesn't change midway
|
||||
|
||||
// TODO
|
||||
// Let the Controller set the value of connected Models,
|
||||
// instead of each Model checking the Controller value every time.
|
||||
|
||||
// Get value buffer from our controller
|
||||
if (m_controllerConnection && m_useControllerValue && m_controllerConnection->getController()->isSampleExact())
|
||||
{
|
||||
auto vb = m_controllerConnection->valueBuffer();
|
||||
@@ -634,32 +587,36 @@ ValueBuffer * AutomatableModel::valueBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_controllerConnection)
|
||||
// Get value buffer from one of the linked models' controller
|
||||
if (m_useControllerValue)
|
||||
{
|
||||
AutomatableModel* lm = nullptr;
|
||||
if (hasLinkedModels())
|
||||
for (auto next = m_nextLink; next != this; next = next->m_nextLink)
|
||||
{
|
||||
lm = m_linkedModels.front();
|
||||
}
|
||||
if (lm && lm->controllerConnection() && lm->useControllerValue() &&
|
||||
lm->controllerConnection()->getController()->isSampleExact())
|
||||
{
|
||||
auto vb = lm->valueBuffer();
|
||||
float * values = vb->values();
|
||||
float * nvalues = m_valueBuffer.values();
|
||||
for (int i = 0; i < vb->length(); i++)
|
||||
{
|
||||
nvalues[i] = fittedValue(values[i]);
|
||||
}
|
||||
m_lastUpdatedPeriod = s_periodCounter;
|
||||
m_hasSampleExactData = true;
|
||||
return &m_valueBuffer;
|
||||
if (next->controllerConnection() && next->useControllerValue() &&
|
||||
next->controllerConnection()->getController()->isSampleExact())
|
||||
{
|
||||
auto vb = next->valueBuffer();
|
||||
float* values = vb->values();
|
||||
float* nvalues = m_valueBuffer.values();
|
||||
for (int i = 0; i < vb->length(); i++)
|
||||
{
|
||||
nvalues[i] = fittedValue(values[i]);
|
||||
}
|
||||
m_lastUpdatedPeriod = s_periodCounter;
|
||||
m_hasSampleExactData = true;
|
||||
return &m_valueBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( m_oldValue != val )
|
||||
// Note: if there are linked models but no controller, `setValue()` will have
|
||||
// updated `m_value` and `m_oldValue` across all linked models, so the `valueBuffer()`
|
||||
// will be the same (even if it is calculated separatly for each model).
|
||||
|
||||
// Populate value buffer by interpolatating between the old and new value
|
||||
if (m_oldValue != val)
|
||||
{
|
||||
m_valueBuffer.interpolate( m_oldValue, val );
|
||||
m_valueBuffer.interpolate(m_oldValue, val);
|
||||
m_oldValue = val;
|
||||
m_lastUpdatedPeriod = s_periodCounter;
|
||||
m_hasSampleExactData = true;
|
||||
|
||||
@@ -291,16 +291,15 @@ void LadspaControl::linkControls( LadspaControl * _control )
|
||||
switch( m_port->data_type )
|
||||
{
|
||||
case BufferDataType::Toggled:
|
||||
BoolModel::linkModels( &m_toggledModel, _control->toggledModel() );
|
||||
_control->toggledModel()->linkToModel(&m_toggledModel);
|
||||
break;
|
||||
case BufferDataType::Integer:
|
||||
case BufferDataType::Enum:
|
||||
case BufferDataType::Floating:
|
||||
FloatModel::linkModels( &m_knobModel, _control->knobModel() );
|
||||
_control->knobModel()->linkToModel(&m_knobModel);
|
||||
break;
|
||||
case BufferDataType::Time:
|
||||
TempoSyncKnobModel::linkModels( &m_tempoSyncKnobModel,
|
||||
_control->tempoSyncKnobModel() );
|
||||
_control->tempoSyncKnobModel()->linkToModel(&m_tempoSyncKnobModel);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -342,16 +341,15 @@ void LadspaControl::unlinkControls( LadspaControl * _control )
|
||||
switch( m_port->data_type )
|
||||
{
|
||||
case BufferDataType::Toggled:
|
||||
BoolModel::unlinkModels( &m_toggledModel, _control->toggledModel() );
|
||||
_control->toggledModel()->unlink();
|
||||
break;
|
||||
case BufferDataType::Integer:
|
||||
case BufferDataType::Enum:
|
||||
case BufferDataType::Floating:
|
||||
FloatModel::unlinkModels( &m_knobModel, _control->knobModel() );
|
||||
_control->knobModel()->unlink();
|
||||
break;
|
||||
case BufferDataType::Time:
|
||||
TempoSyncKnobModel::unlinkModels( &m_tempoSyncKnobModel,
|
||||
_control->tempoSyncKnobModel() );
|
||||
_control->tempoSyncKnobModel()->unlink();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -46,7 +46,7 @@ void LinkedModelGroup::linkControls(LinkedModelGroup *other)
|
||||
{
|
||||
auto itr2 = other->m_models.find(id);
|
||||
Q_ASSERT(itr2 != other->m_models.end());
|
||||
AutomatableModel::linkModels(inf.m_model, itr2->second.m_model);
|
||||
inf.m_model->linkToModel(itr2->second.m_model);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp
|
||||
for (auto it = m_oldAutomatedValues.begin(); it != m_oldAutomatedValues.end(); it++)
|
||||
{
|
||||
AutomatableModel * am = it.key();
|
||||
if (am->controllerConnection() && !values.contains(am))
|
||||
if (!values.contains(am))
|
||||
{
|
||||
am->setUseControllerValue(true);
|
||||
}
|
||||
@@ -422,13 +422,18 @@ void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp
|
||||
// Apply values
|
||||
for (auto it = values.begin(); it != values.end(); it++)
|
||||
{
|
||||
if (! recordedModels.contains(it.key()))
|
||||
AutomatableModel* model = it.key();
|
||||
bool isRecording = recordedModels.contains(model);
|
||||
model->setUseControllerValue(isRecording);
|
||||
|
||||
if (!isRecording)
|
||||
{
|
||||
it.key()->setAutomatedValue(it.value());
|
||||
}
|
||||
else if (!it.key()->useControllerValue())
|
||||
{
|
||||
it.key()->setUseControllerValue(true);
|
||||
/* TODO
|
||||
* Remove scaleValue() from here when automation editor's
|
||||
* Y axis can be set to logarithmic, and automation clips store
|
||||
* the actual values, and not the invertedScaledValue.
|
||||
*/
|
||||
model->setValue(model->scaledValue(it.value()), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ void AutomatableModelView::addDefaultActions( QMenu* menu )
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
if( model->hasLinkedModels() )
|
||||
if (model->isLinked())
|
||||
{
|
||||
menu->addAction( embed::getIconPixmap( "edit-delete" ),
|
||||
AutomatableModel::tr( "Remove all linked controls" ),
|
||||
amvSlots, SLOT(unlinkAllModels()));
|
||||
menu->addAction(embed::getIconPixmap("edit_unlink"),
|
||||
AutomatableModel::tr("Remove all linked controls"),
|
||||
model, SLOT(unlink()));
|
||||
menu->addSeparator();
|
||||
}
|
||||
|
||||
@@ -276,11 +276,6 @@ void AutomatableModelViewSlots::removeSongGlobalAutomation()
|
||||
}
|
||||
|
||||
|
||||
void AutomatableModelViewSlots::unlinkAllModels()
|
||||
{
|
||||
m_amv->modelUntyped()->unlinkAllModels();
|
||||
}
|
||||
|
||||
void AutomatableModelViewSlots::copyToClipboard()
|
||||
{
|
||||
// For copyString() and MimeType enum class
|
||||
|
||||
@@ -147,8 +147,7 @@ void FloatModelEditorBase::dropEvent(QDropEvent * de)
|
||||
auto mod = dynamic_cast<AutomatableModel*>(Engine::projectJournal()->journallingObject(val.toInt()));
|
||||
if (mod != nullptr)
|
||||
{
|
||||
AutomatableModel::linkModels(model(), mod);
|
||||
mod->setValue(model()->value());
|
||||
model()->linkToModel(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,12 +422,16 @@ void FloatModelEditorBase::enterValue()
|
||||
|
||||
void FloatModelEditorBase::friendlyUpdate()
|
||||
{
|
||||
if (model() && (model()->controllerConnection() == nullptr ||
|
||||
model()->controllerConnection()->getController()->frequentUpdates() == false ||
|
||||
Controller::runningFrames() % (256*4) == 0))
|
||||
{
|
||||
update();
|
||||
}
|
||||
if (model() == nullptr) { return; }
|
||||
|
||||
// If the controller changes constantly, only repaint every 1024th frame
|
||||
if (model()->useControllerValue()
|
||||
&& model()->controllerConnection()
|
||||
&& model()->controllerConnection()->getController()->frequentUpdates()
|
||||
&& Controller::runningFrames() % (256 * 4) != 0)
|
||||
{ return; }
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ private slots: // tests
|
||||
{
|
||||
using namespace lmms;
|
||||
|
||||
BoolModel m1(false), m2(false);
|
||||
BoolModel m1(true), m2(false);
|
||||
|
||||
QObject::connect(&m1, SIGNAL(dataChanged()),
|
||||
this, SLOT(onM1Changed()));
|
||||
@@ -86,17 +86,19 @@ private slots: // tests
|
||||
this, SLOT(onM2Changed()));
|
||||
|
||||
resetChanged();
|
||||
AutomatableModel::linkModels(&m1, &m1);
|
||||
m1.linkToModel(&m1);
|
||||
QVERIFY(!m1Changed); // cannot link to itself
|
||||
QVERIFY(!m2Changed);
|
||||
QVERIFY(m1.countLinks() == 0);
|
||||
|
||||
resetChanged();
|
||||
AutomatableModel::linkModels(&m1, &m2);
|
||||
QVERIFY(m1Changed); // since m1 takes the value of m2
|
||||
m1.linkToModel(&m2);
|
||||
QVERIFY(m1.value() == m2.value()); // since m1 takes the value of m2
|
||||
QVERIFY(!m2Changed); // the second model is the source
|
||||
QVERIFY(m1.countLinks() == 1);
|
||||
|
||||
resetChanged();
|
||||
AutomatableModel::linkModels(&m1, &m2);
|
||||
m1.linkToModel(&m2);
|
||||
QVERIFY(!m1Changed); // it's already linked
|
||||
QVERIFY(!m2Changed);
|
||||
|
||||
@@ -104,15 +106,15 @@ private slots: // tests
|
||||
BoolModel m3(false);
|
||||
m1.setValue(1.f);
|
||||
m2.setValue(1.f);
|
||||
AutomatableModel::linkModels(&m1, &m2);
|
||||
m1.linkToModel(&m2);
|
||||
QVERIFY(m1.value());
|
||||
QVERIFY(m2.value());
|
||||
QVERIFY(!m3.value());
|
||||
AutomatableModel::linkModels(&m2, &m3); // drag m3, drop on m2
|
||||
m2.linkToModel(&m3); // drag m3, drop on m2
|
||||
// m2 should take m3's (0) value
|
||||
// due to a bug(?), this does not happen
|
||||
QVERIFY(m2.value());
|
||||
QVERIFY(m2.value() == m3.value());
|
||||
QVERIFY(!m3.value());
|
||||
QVERIFY(m1.countLinks() == 2);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user