/* * AutomatableModel.cpp - some implementations of AutomatableModel-class * * Copyright (c) 2008-2014 Tobias Doerffel * * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net * * 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 #include "AutomatableModel.h" #include "AutomationPattern.h" #include "ControllerConnection.h" float AutomatableModel::s_copiedValue = 0; AutomatableModel::AutomatableModel( DataType type, const float val, const float min, const float max, const float step, Model* parent, const QString & displayName, bool defaultConstructed ) : Model( parent, displayName, defaultConstructed ), m_dataType( type ), m_scaleType( Linear ), m_value( val ), m_initValue( val ), m_minValue( min ), m_maxValue( max ), m_step( step ), m_range( max - min ), m_centerValue( m_minValue ), m_setValueDepth( 0 ), m_hasLinkedModels( false ), m_controllerConnection( NULL ) { setInitValue( val ); } AutomatableModel::~AutomatableModel() { while( m_linkedModels.empty() == false ) { m_linkedModels.last()->unlinkModel( this ); m_linkedModels.erase( m_linkedModels.end() - 1 ); } if( m_controllerConnection ) { delete m_controllerConnection; } emit destroyed( id() ); } bool AutomatableModel::isAutomated() const { return AutomationPattern::isAutomated( this ); } void AutomatableModel::saveSettings( QDomDocument& doc, QDomElement& element, const QString& name ) { bool automatedOrControlled = false; if( isAutomated() ) { // automation needs tuple of data (name, id, value) // => it must be appended as a node QDomElement me = doc.createElement( name ); me.setAttribute( "id", id() ); me.setAttribute( "value", m_value ); element.appendChild( me ); automatedOrControlled = true; } else { // non automation => can be saved as attribute element.setAttribute( name, m_value ); } if( m_controllerConnection ) { QDomElement controllerElement; // get "connection" element (and create it if needed) QDomNode node = element.namedItem( "connection" ); if( node.isElement() ) { controllerElement = node.toElement(); } else { controllerElement = doc.createElement( "connection" ); element.appendChild( controllerElement ); } QDomElement element = doc.createElement( name ); m_controllerConnection->saveSettings( doc, element ); controllerElement.appendChild( element ); automatedOrControlled = true; } if( automatedOrControlled && ( m_scaleType != Linear ) ) { // note: if we have more scale types than two, make // a mapper function enums <-> string if(m_scaleType == Logarithmic) { element.setAttribute( "scale_type", "log" ); } } } void AutomatableModel::loadSettings( const QDomElement& element, const QString& name ) { // read scale type and overwrite default scale type if( element.hasAttribute("scale_type") ) // wrong in most cases { if( element.attribute("scale_type") == "log" ) setScaleType( Logarithmic ); } else { setScaleType( Linear ); } // compat code QDomNode node = element.namedItem( AutomationPattern::classNodeName() ); if( node.isElement() ) { node = node.namedItem( name ); if( node.isElement() ) { AutomationPattern * p = AutomationPattern::globalAutomationPattern( this ); p->loadSettings( node.toElement() ); setValue( p->valueAt( 0 ) ); // in older projects we sometimes have odd automations // with just one value in - eliminate if necessary if( !p->hasAutomation() ) { delete p; } return; } // logscales were not existing at this point of time // so they can be ignored } QDomNode connectionNode = element.namedItem( "connection" ); // reads controller connection if( connectionNode.isElement() ) { QDomNode thisConnection = connectionNode.toElement().namedItem( name ); if( thisConnection.isElement() ) { setControllerConnection( new ControllerConnection( (Controller*)NULL ) ); m_controllerConnection->loadSettings( thisConnection.toElement() ); //m_controllerConnection->setTargetName( displayName() ); } } // models can be stored as elements (port00) or attributes (port10): // // // // element => there is automation data node = element.namedItem( name ); if( node.isElement() ) { changeID( node.toElement().attribute( "id" ).toInt() ); setValue( node.toElement().attribute( "value" ).toFloat() ); } else if( element.hasAttribute( name ) ) // attribute => read the element's value from the attribute list { setInitValue( element.attribute( name ).toFloat() ); } else { reset(); } } void AutomatableModel::setValue( const float value ) { ++m_setValueDepth; const float old_val = m_value; m_value = fittedValue( value ); if( old_val != m_value ) { // add changes to history so user can undo it addJournalCheckPoint(); // notify linked models for( AutoModelVector::Iterator it = m_linkedModels.begin(); it != m_linkedModels.end(); ++it ) { if( (*it)->m_setValueDepth < 1 && (*it)->fittedValue( value ) != (*it)->m_value ) { bool journalling = (*it)->testAndSetJournalling( isJournalling() ); (*it)->setValue( value ); (*it)->setJournalling( journalling ); } } emit dataChanged(); } else { emit dataUnchanged(); } --m_setValueDepth; } //! @brief Scales @value from linear to logarithmic. //! Value should be within [0,1] //! @todo This should be moved into a maths header template T logToLinearScale(T min, T max, T value) { return exp( ( log(max) - log(min) ) * value + log(min) ); } template T AutomatableModel::logToLinearScale(T value) const { return ::logToLinearScale( minValue(), maxValue(), value ); } //! @todo: this should be moved into a maths header template void roundAt( T& value, const T& where, const T& step_size ) { if( qAbs( value - where ) < typeInfo::minEps() * qAbs( step_size ) ) { value = where; } } template void AutomatableModel::roundAt( T& value, const T& where ) const { ::roundAt(value, where, m_step); } void AutomatableModel::setAutomatedValue( const float value ) { ++m_setValueDepth; const float oldValue = m_value; const float scaled_value = ( m_scaleType == Linear ) ? value : logToLinearScale( // fits value into [0,1]: (value - minValue()) / maxValue() ); m_value = fittedValue( scaled_value ); if( oldValue != m_value ) { // notify linked models for( AutoModelVector::Iterator it = m_linkedModels.begin(); it != m_linkedModels.end(); ++it ) { if( (*it)->m_setValueDepth < 1 && !(*it)->fittedValue( m_value ) != (*it)->m_value ) { // @TOBY: don't take m_value, but better: value, // otherwise, we convert to log twice? (*it)->setAutomatedValue( m_value ); } } emit dataChanged(); } --m_setValueDepth; } void AutomatableModel::setRange( const float min, const float max, const float step ) { if( ( m_maxValue != max ) || ( m_minValue != min ) ) { m_minValue = min; m_maxValue = max; if( m_minValue > m_maxValue ) { qSwap( m_minValue, m_maxValue ); } m_range = m_maxValue - m_minValue; setStep( step ); // re-adjust value setValue( value() ); emit propertiesChanged(); } } void AutomatableModel::setStep( const float step ) { if( m_step != step ) { m_step = step; emit propertiesChanged(); } } float AutomatableModel::fittedValue( float value ) const { value = tLimit( value, m_minValue, m_maxValue ); if( m_step != 0 ) { value = nearbyintf( value / m_step ) * m_step; } roundAt( value, m_maxValue ); roundAt( value, m_minValue ); roundAt( value, 0.0f ); if( value < m_minValue ) { return m_minValue; } else if( value > m_maxValue ) { return m_maxValue; } return value; } void AutomatableModel::linkModel( AutomatableModel* model ) { if( !m_linkedModels.contains( model ) && model != this ) { m_linkedModels.push_back( model ); m_hasLinkedModels = true; if( !model->hasLinkedModels() ) { QObject::connect( this, SIGNAL( dataChanged() ), model, SIGNAL( dataChanged() ) ); } } } void AutomatableModel::unlinkModel( AutomatableModel* model ) { AutoModelVector::Iterator it = qFind( m_linkedModels.begin(), m_linkedModels.end(), model ); if( it != m_linkedModels.end() ) { m_linkedModels.erase( it ); } m_hasLinkedModels = !m_linkedModels.isEmpty(); } void AutomatableModel::linkModels( AutomatableModel* model1, AutomatableModel* model2 ) { model1->linkModel( model2 ); model2->linkModel( model1 ); } void AutomatableModel::unlinkModels( AutomatableModel* model1, AutomatableModel* model2 ) { model1->unlinkModel( model2 ); model2->unlinkModel( model1 ); } void AutomatableModel::unlinkAllModels() { foreach( AutomatableModel* model, m_linkedModels ) { unlinkModels( this, model ); } m_hasLinkedModels = false; } void AutomatableModel::setControllerConnection( ControllerConnection* c ) { m_controllerConnection = c; if( c ) { QObject::connect( m_controllerConnection, SIGNAL( valueChanged() ), this, SIGNAL( dataChanged() ) ); QObject::connect( m_controllerConnection, SIGNAL( destroyed() ), this, SLOT( unlinkControllerConnection() ) ); emit dataChanged(); } } float AutomatableModel::controllerValue( int frameOffset ) const { if( m_controllerConnection ) { float v = 0; switch(m_scaleType) { case Linear: v = minValue() + ( range() * controllerConnection()->currentValue( frameOffset ) ); break; case Logarithmic: v = logToLinearScale( controllerConnection()->currentValue( frameOffset )); break; default: qFatal("AutomatableModel::controllerValue(int)" "lacks implementation for a scale type"); break; } if( typeInfo::isEqual( m_step, 1 ) ) { return qRound( v ); } return v; } AutomatableModel* lm = m_linkedModels.first(); if( lm->controllerConnection() ) { return fittedValue( lm->controllerValue( frameOffset ) ); } return fittedValue( lm->m_value ); } void AutomatableModel::unlinkControllerConnection() { if( m_controllerConnection ) { m_controllerConnection->disconnect( this ); } m_controllerConnection = NULL; } void AutomatableModel::setInitValue( const float value ) { m_initValue = fittedValue( value ); bool journalling = testAndSetJournalling( false ); setValue( value ); setJournalling( journalling ); emit initValueChanged( value ); } void AutomatableModel::reset() { setValue( initValue() ); } void AutomatableModel::copyValue() { s_copiedValue = value(); } void AutomatableModel::pasteValue() { setValue( copiedValue() ); } float AutomatableModel::globalAutomationValueAt( const MidiTime& time ) { // get patterns that connect to this model QVector patterns = AutomationPattern::patternsForModel( this ); if( patterns.isEmpty() ) { // if no such patterns exist, return current value return m_value; } else { // of those patterns: // find the patterns which overlap with the miditime position QVector patternsInRange; for( QVector::ConstIterator it = patterns.begin(); it != patterns.end(); it++ ) { int s = ( *it )->startPosition(); int e = ( *it )->endPosition(); if( s <= time && e >= time ) { patternsInRange += ( *it ); } } AutomationPattern * latestPattern = NULL; if( ! patternsInRange.isEmpty() ) { // if there are more than one overlapping patterns, just use the first one because // multiple pattern behaviour is undefined anyway latestPattern = patternsInRange[0]; } else // if we find no patterns at the exact miditime, we need to search for the last pattern before time and use that { int latestPosition = 0; for( QVector::ConstIterator it = patterns.begin(); it != patterns.end(); it++ ) { int e = ( *it )->endPosition(); if( e <= time && e > latestPosition ) { latestPosition = e; latestPattern = ( *it ); } } } if( latestPattern ) { // scale/fit the value appropriately and return it const float value = latestPattern->valueAt( time - latestPattern->startPosition() ); const float scaled_value = ( m_scaleType == Linear ) ? value : logToLinearScale( // fits value into [0,1]: (value - minValue()) / maxValue() ); return fittedValue( scaled_value ); } // if we still find no pattern, the value at that time is undefined so // just return current value as the best we can do else return m_value; } } #include "moc_AutomatableModel.cxx"