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:
szeli1
2025-12-22 23:37:40 +01:00
committed by GitHub
parent c92741f567
commit 69f7c3243d
13 changed files with 338 additions and 221 deletions

View 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

View 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

View File

@@ -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,9 +368,15 @@ 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]
template<class T> T logToLinearScale( T value ) const;
@@ -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

View File

@@ -98,7 +98,6 @@ public slots:
void execConnectionDialog();
void removeConnection();
void editSongGlobalAutomation();
void unlinkAllModels();
void removeSongGlobalAutomation();
private slots:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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