Index: CLAM/src/Processing/Controls/ControlTrace.cxx =================================================================== --- CLAM/src/Processing/Controls/ControlTrace.cxx (revision 0) +++ CLAM/src/Processing/Controls/ControlTrace.cxx (revision 0) @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2001-2004 MUSIC TECHNOLOGY GROUP (MTG) + * UNIVERSITAT POMPEU FABRA + * Copyright (c) 2007 Superlucidity Services, LLC and Zachary T Welch + * + * + * 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; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "ControlTrace.hxx" +#include "Factory.hxx" +#include "XMLAdapter.hxx" +#include "XMLStorage.hxx" +#include "XMLIterableAdapter.hxx" +#include "XMLAdapter.hxx" +#include "XmlStorageErr.hxx" +#include + +namespace CLAM +{ + +ControlTraceEvent::ControlTraceEvent() + : mSteps(1) +{ +} +ControlTraceEvent::ControlTraceEvent(const ControlTraceEvent &obj) + : mSteps(obj.mSteps) + , mValues(obj.mValues) +{ +} +ControlTraceEvent::ControlTraceEvent(const InControlArray &inputs) + : mSteps(1) +{ + mValues.resize(inputs.Size()); + for (int i = 0; i < inputs.Size(); i++) + mValues[i] = inputs[i].GetLastValue(); +} + +void ControlTraceEvent::UpdateControls(OutControlArray &outputs) const +{ + for (int i = 0; i < outputs.Size(); i++) + outputs[i].SendControl(mValues[i]); +} + +void ControlTraceEvent::LoadFrom( Storage& storage ) +{ + XMLIterableAdapter vAdapter(mValues, "x", "controls", true); + storage.Load(vAdapter); + + XMLAdapter sAdapter(mSteps, "repeats", true); + storage.Load(sAdapter); +} + +void ControlTraceEvent::StoreOn( Storage& storage ) const +{ + XMLIterableAdapter vAdapter(mValues, "x", "controls", true); + storage.Store(vAdapter); + + if (mSteps > 1) + { + XMLAdapter sAdapter(mSteps, "repeats", true); + storage.Store(sAdapter); + } +} + +const ControlTraceEvent& ControlTraceEvent::operator=(const ControlTraceEvent& rhs) +{ + mValues = rhs.mValues; + return *this; +} + +bool ControlTraceEvent::ValuesEqual(const ControlTraceEvent &rhs) const +{ + if (mValues.size() != rhs.mValues.size()) + return false; + + for (size_t i = 0; i < mValues.size(); i++) + { + if (mValues[i] != rhs.mValues[i]) + return false; + } + + return true; +} + +/* ================================================================== */ + +const unsigned int ControlTraceData::DumpVersion = 1; + +ControlTraceData::ControlTraceData() + : mVersion(DumpVersion) +{ +} +ControlTraceData::ControlTraceData(const ControlTraceData &obj) + : mVersion(DumpVersion), mEvents(obj.mEvents) +{ +} + +void ControlTraceData::LoadFrom( Storage& storage ) +{ + mVersion = 0; + XMLAdapter versionAdapter(mVersion, "version"); + storage.Load(versionAdapter); + if (!mVersion || mVersion > DumpVersion) + { + std::stringstream err; + err << "Unknown CLAM Control Trace file version: " << mVersion; + throw XmlStorageErr(err.str()); + } + + XMLIterableAdapter adapter(mEvents, "event", "events", true); + storage.Load(adapter); +} +void ControlTraceData::StoreOn( Storage& storage ) const +{ + XMLAdapter versionAdapter(mVersion, "version"); + storage.Store(versionAdapter); + + XMLIterableAdapter adapter(mEvents, "event", "events", true); + storage.Store(adapter); +} + +void ControlTraceData::Append(const ControlTraceEvent &data) +{ + ControlTraceEvent &prev = mEvents.back(); + if (prev.ValuesEqual(data)) + prev.AddStep(); + else + mEvents.push_back(data); +} + +/* ================================================================== */ + +static const char clamControlTraceFileTypeFamily[] = "CLAM Control Trace"; +static const Filename::Filter clamControlTraceFileFilters[] = { + { "CLAM Control Traces (v1)", "*.clamtrace" }, + }; + +const char* ControlTraceInFilename::TypeFamily() const +{ + return clamControlTraceFileTypeFamily; +} +const Filename::Filter * ControlTraceInFilename::Filters() const +{ + return clamControlTraceFileFilters; +} + +const char* ControlTraceOutFilename::TypeFamily() const +{ + return clamControlTraceFileTypeFamily; +} +const Filename::Filter * ControlTraceOutFilename::Filters() const +{ + return clamControlTraceFileFilters; +} + +/* ================================================================= */ + +void ControlTraceWriterConfig::DefaultInit() +{ + AddAll(); + UpdateData(); + SetNumberOfInputs(1.); +} + +ControlTraceWriter::ControlTraceWriter() +{ + Configure(mConfig); +} + +ControlTraceWriter::ControlTraceWriter( const ProcessingConfig& cfg ) +{ + Configure( cfg ); +} + +ControlTraceWriter::~ControlTraceWriter() +{ + RemoveOldControls(); +} + +bool ControlTraceWriter::ConcreteConfigure( const ProcessingConfig& cfgObj ) +{ + RemoveOldControls(); + CopyAsConcreteConfig( mConfig, cfgObj ); + if ( !mConfig.HasTraceFile() ) + { + AddConfigErrorMessage("No 'trace file' was specified in the configuration!"); + return false; + } + + ControlTraceOutFilename &file = mConfig.GetTraceFile(); + if ( file == "" ) + { + AddConfigErrorMessage("No trace file selected"); + return false; + } + + if (!mConfig.HasNumberOfInputs() || mConfig.GetNumberOfInputs() < 1.) + { + AddConfigErrorMessage("The number of inputs has not been configured."); + return false; + } + + mInputs.Resize(int(mConfig.GetNumberOfInputs()), "Input", this); + return true; +} + +bool ControlTraceWriter::ConcreteStop() +{ + XMLStorage::Dump(mTrace, "trace", mConfig.GetTraceFile().c_str()); + mTrace.Clear(); + return true; +} + +bool ControlTraceWriter::Do() +{ + mTrace.Append(ControlTraceEvent(mInputs)); + return true; +} + + +void ControlTraceWriter::RemoveOldControls() +{ + mInputs.Clear(); + GetInControls().Clear(); +} + +/* ================================================================= */ + +void ControlTraceReaderConfig::DefaultInit() +{ + AddAll(); + UpdateData(); +} + +ControlTraceReader::ControlTraceReader() +{ + Configure(mConfig); +} + +ControlTraceReader::ControlTraceReader( const ProcessingConfig& cfg ) +{ + Configure( cfg ); +} + +ControlTraceReader::~ControlTraceReader() +{ + RemoveOldControls(); +} + +bool ControlTraceReader::ConcreteConfigure( const ProcessingConfig& cfgObj ) +{ + RemoveOldControls(); + CopyAsConcreteConfig( mConfig, cfgObj ); + if ( !mConfig.HasTraceFile() ) + { + AddConfigErrorMessage("No 'trace file' was specified in the configuration!"); + return false; + } + + ControlTraceInFilename &file = mConfig.GetTraceFile(); + if ( file == "" ) + { + AddConfigErrorMessage("No trace file selected"); + return false; + } + + try { + XMLStorage::Restore(mTrace, mConfig.GetTraceFile().c_str()); + } + catch (XmlStorageErr &e) + { + AddConfigErrorMessage(e.what()); + return false; + } + + if (mTrace.GetNumberOfControls() < 1) + { + AddConfigErrorMessage("The specified file does not contain any control events."); + return false; + } + + mOutputs.Resize(mTrace.GetNumberOfControls(), "Output", this); + return true; +} + +bool ControlTraceReader::ConcreteStart() +{ + mIterator = mTrace.Begin(); + mStepCounter = 0; + return true; +} + +bool ControlTraceReader::Do() +{ + if (mIterator == mTrace.End()) + return false; + + const ControlTraceEvent &event = *mIterator; + + // if this is the zeroth (first) step, update the controls + if (!mStepCounter) + event.UpdateControls(mOutputs); + + // if this step completes the current event, advance and reset counter + if (++mStepCounter == event.Steps()) { + mIterator++; + mStepCounter = 0; + } + return true; +} + +void ControlTraceReader::RemoveOldControls() +{ + mOutputs.Clear(); + GetOutControls().Clear(); +} + +/* ================================================================== */ + +namespace detail +{ +typedef CLAM::Factory ProcessingFactory; +static ProcessingFactory::Registrator + regtControlTraceReader("ControlTraceReader"); +static ProcessingFactory::Registrator + regtControlTraceWriter("ControlTraceWriter"); +} + + +} // CLAM namespace + Index: CLAM/src/Processing/Controls/ControlTrace.hxx =================================================================== --- CLAM/src/Processing/Controls/ControlTrace.hxx (revision 0) +++ CLAM/src/Processing/Controls/ControlTrace.hxx (revision 0) @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2001-2004 MUSIC TECHNOLOGY GROUP (MTG) + * UNIVERSITAT POMPEU FABRA + * Copyright (c) 2007 Superlucidity Services, LLC and Zachary T Welch + * + * + * 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; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef CONTROL_TRACE_HXX +#define CONTROL_TRACE_HXX + +#include +#include +#include +#include +#include +#include + +class Network; + +struct fann; + +namespace CLAM +{ + +class ControlTraceEvent : public Component { +public: + typedef std::vector ValueArray; + + ControlTraceEvent(); + ControlTraceEvent(const InControlArray &inputs); + ControlTraceEvent(const ControlTraceEvent &event); + const ControlTraceEvent& operator=( const ControlTraceEvent& ); + virtual ~ControlTraceEvent() { } + + const char *GetClassName() const { return "ControlTraceEvent"; } + + void LoadFrom( Storage& storage); + void StoreOn( Storage& storage ) const; + + void AddStep() { mSteps++; } + unsigned int Steps() const { return mSteps; } + size_t Size() const { return mValues.size(); } + + void UpdateControls(OutControlArray &array) const; + + bool ValuesEqual(const ControlTraceEvent &rhs) const; + +private: + unsigned int mSteps; + ValueArray mValues; +}; + +class ControlTraceData : public Component { + static const unsigned int DumpVersion; +public: + typedef std::list EventList; + virtual ~ControlTraceData() { } + + ControlTraceData(); + ControlTraceData(const ControlTraceData &obj); + + const char *GetClassName() const { return "ControlTraceData"; } + + unsigned int GetVersion() const { return mVersion; } + + void LoadFrom( Storage& storage); + void StoreOn( Storage& storage ) const; + + size_t GetNumberOfControls() const { return mEvents.front().Size(); } + + void Append(const ControlTraceEvent &data); + + EventList::iterator Begin() { return mEvents.begin(); } + EventList::iterator End() { return mEvents.end(); } + + void Clear() { mEvents.clear(); } + +private: + unsigned int mVersion; + EventList mEvents; +}; + + +class ControlTraceInFilename : public InFilename +{ +public: + ControlTraceInFilename(const std::string & s="") : InFilename(s) {} + ControlTraceInFilename(const char * s) : InFilename(s) {} + const char* TypeFamily() const; + const Filter * Filters() const; +}; +CLAM_TYPEINFOGROUP(BasicCTypeInfo, ControlTraceInFilename); + +class ControlTraceReaderConfig : public ProcessingConfig { + DYNAMIC_TYPE_USING_INTERFACE + ( ControlTraceReaderConfig, 1, ProcessingConfig ); + + DYN_ATTRIBUTE( 0, public, ControlTraceInFilename, TraceFile ); + +protected: + void DefaultInit(); +}; + +class ControlTraceReader : public Processing +{ +public: + ControlTraceReader(); + ControlTraceReader( const ProcessingConfig& cfg ); + virtual ~ControlTraceReader(); + + const char* GetClassName() const { return "ControlTraceReader"; } + const ProcessingConfig& GetConfig() const { return mConfig; } + + bool Do(); + +protected: // methods + bool ConcreteConfigure( const ProcessingConfig& cfgObject ); + bool ConcreteStart(); + void RemoveOldControls(); + +protected: // attributes + ControlTraceReaderConfig mConfig; + ControlTraceData mTrace; + ControlTraceData::EventList::iterator mIterator; + unsigned int mStepCounter; + OutControlArray mOutputs; +}; + + +class ControlTraceOutFilename : public OutFilename +{ +public: + ControlTraceOutFilename(const std::string & s="") : OutFilename(s) {} + ControlTraceOutFilename(const char * s) : OutFilename(s) {} + const char* TypeFamily() const; + const Filter * Filters() const; +}; +CLAM_TYPEINFOGROUP(BasicCTypeInfo, ControlTraceOutFilename); + +class ControlTraceWriterConfig : public ProcessingConfig { + DYNAMIC_TYPE_USING_INTERFACE + ( ControlTraceWriterConfig, 2, ProcessingConfig ); + + DYN_ATTRIBUTE( 0, public, ControlTraceOutFilename, TraceFile ); + DYN_ATTRIBUTE( 1, public, TData, NumberOfInputs ); + +protected: + void DefaultInit(); +}; + +class ControlTraceWriter : public Processing +{ +public: + ControlTraceWriter(); + ControlTraceWriter( const ProcessingConfig& cfg ); + virtual ~ControlTraceWriter(); + + const char* GetClassName() const { return "ControlTraceWriter"; } + const ProcessingConfig& GetConfig() const { return mConfig; } + + bool Do(); + +protected: // methods + bool ConcreteConfigure( const ProcessingConfig& cfgObject ); + bool ConcreteStop(); + void RemoveOldControls(); + +protected: // attributes + ControlTraceWriterConfig mConfig; + ControlTraceData mTrace; + InControlArray mInputs; +}; + +} // CLAM namespace + +#endif Index: NetworkEditor/src/ProcessingTree.cxx =================================================================== --- NetworkEditor/src/ProcessingTree.cxx (revision 10016) +++ NetworkEditor/src/ProcessingTree.cxx (working copy) @@ -67,6 +67,8 @@ "ControlSource", "ControlSink", "ControlPrinter", + "ControlTraceReader", + "ControlTraceWriter", "ControlScaler", "AutoPanner", "FlagControl", Index: NetworkEditor/src/ProcessingBox.cxx =================================================================== --- NetworkEditor/src/ProcessingBox.cxx (revision 10016) +++ NetworkEditor/src/ProcessingBox.cxx (working copy) @@ -85,7 +85,7 @@ if (className=="ControlSurface") return new ControlSurfaceWidget(processing); - if (className=="ControlPrinter") + if (className=="ControlPrinter" || className=="ControlTraceWriter") return new ControlPrinterWidget(processing); if (className=="Vumeter") Index: NetworkEditor/src/RegisterConfiguratorLaunchers.cxx =================================================================== --- NetworkEditor/src/RegisterConfiguratorLaunchers.cxx (revision 10016) +++ NetworkEditor/src/RegisterConfiguratorLaunchers.cxx (working copy) @@ -66,6 +66,7 @@ #include #include "ControlSurface.hxx" #include +#include #include //MIDI @@ -132,6 +133,8 @@ STANDARD_PROCESSING_CONFIG_REGISTER(ControlScalerConfig); STANDARD_PROCESSING_CONFIG_REGISTER(ControlSourceConfig); STANDARD_PROCESSING_CONFIG_REGISTER(ControlSurfaceConfig); +STANDARD_PROCESSING_CONFIG_REGISTER(ControlTraceReaderConfig); +STANDARD_PROCESSING_CONFIG_REGISTER(ControlTraceWriterConfig); STANDARD_PROCESSING_CONFIG_REGISTER(FlagControlConfig); STANDARD_PROCESSING_CONFIG_REGISTER(Fundamental2ControlConfig); STANDARD_PROCESSING_CONFIG_REGISTER(OneOverFConfig); Index: CLAM/test/FunctionalTests/ProcessingTests/TestControlTraces.cxx =================================================================== --- CLAM/test/FunctionalTests/ProcessingTests/TestControlTraces.cxx (revision 0) +++ CLAM/test/FunctionalTests/ProcessingTests/TestControlTraces.cxx (revision 0) @@ -0,0 +1,251 @@ +#include +#include "cppUnitHelper.hxx" +#include +#include "similarityHelper.hxx" +#include +#include + +namespace CLAMTest +{ + +class ControlTraceReaderFunctionalTest; +CPPUNIT_TEST_SUITE_REGISTRATION( ControlTraceReaderFunctionalTest ); + +class ControlTraceReaderFunctionalTest : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE( ControlTraceReaderFunctionalTest ); + + // Configuration values checking tests + CPPUNIT_TEST( testConfigure_ReturnsTrueWithJustFilename ); + CPPUNIT_TEST( testConfigure_ReturnsFalseWithoutTraceFileInConfig ); + CPPUNIT_TEST( testConfigure_ReturnsTrueWhenFileExists ); + CPPUNIT_TEST( testConfigure_ReturnsFalseWhenFileDoesNotExist ); + CPPUNIT_TEST( testConfigure_ReturnsTrueWhenTraceEventsExist ); + CPPUNIT_TEST( testConfigure_ReturnsFalseWhenTraceFileIsEmpty ); + + CPPUNIT_TEST_SUITE_END(); + +protected: // Attributes + + std::string mPathToTestData; + std::string mInputFileName; + std::string mEmptyFileName; + +protected: // Auxiliary methods + +public: // TestFixture interface + + void setUp() + { + mPathToTestData = GetTestDataDirectory() + "ControlTraceReader"; + mInputFileName = mPathToTestData + "Input.clamtrace"; + mEmptyFileName = mPathToTestData + "Empty.clamtrace"; + } + + void tearDown() + { + } + +private: // tests cases + + void testConfigure_ReturnsTrueWithJustFilename() + { + CLAM::ControlTraceReaderConfig cfg; + + CLAM::ControlTraceInFilename file(mInputFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceReader proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( true, configResult ); + } + + void testConfigure_ReturnsFalseWithoutTraceFileInConfig() + { + CLAM::ControlTraceReaderConfig cfg; + cfg.RemoveTraceFile(); + cfg.UpdateData(); + + CLAM::ControlTraceReader proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( false, configResult ); + } + + void testConfigure_ReturnsTrueWhenFileExists() + { + CLAM::ControlTraceReaderConfig cfg; + + CLAM::ControlTraceInFilename file(mInputFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceReader proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( true, configResult ); + } + + void testConfigure_ReturnsFalseWhenFileDoesNotExist() + { + CLAM::ControlTraceReaderConfig cfg; + + CLAM::ControlTraceInFilename file(mPathToTestData + + std::string( "missing-file.clamtrace" ) ); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceReader proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( false, configResult ); + } + + void testConfigure_ReturnsTrueWhenTraceEventsExist() + { + CLAM::ControlTraceReaderConfig cfg; + + CLAM::ControlTraceInFilename file(mInputFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceReader proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( true, configResult ); + } + + void testConfigure_ReturnsFalseWhenTraceFileIsEmpty() + { + CLAM::ControlTraceReaderConfig cfg; + + CLAM::ControlTraceInFilename file(mEmptyFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceReader proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( false, configResult ); + + } + +}; + +class ControlTraceWriterFunctionalTest; +CPPUNIT_TEST_SUITE_REGISTRATION( ControlTraceWriterFunctionalTest ); + +class ControlTraceWriterFunctionalTest : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE( ControlTraceWriterFunctionalTest ); + + // Configuration values checking tests + CPPUNIT_TEST( testConfigure_ReturnsTrueWithJustFilename ); + CPPUNIT_TEST( testConfigure_ReturnsFalseWithoutTraceFileInConfig ); + CPPUNIT_TEST( testConfigure_ReturnsTrueWhenFileExists ); + CPPUNIT_TEST( testConfigure_ReturnsFalseWithoutInputsInConfig ); + CPPUNIT_TEST( testConfigure_ReturnsFalseWhenConfiguredWithNoInputs ); + + CPPUNIT_TEST_SUITE_END(); + +protected: // Attributes + + std::string mPathToTestData; + std::string mOutputFileName; + +protected: // Auxiliary methods + +public: // TestFixture interface + + void setUp() + { + mPathToTestData = GetTestDataDirectory() + "ControlTraceWriter"; + mOutputFileName = mPathToTestData + "Output.clamtrace"; + } + + void tearDown() + { + } + +private: // tests cases + + void testConfigure_ReturnsTrueWithJustFilename() + { + CLAM::ControlTraceWriterConfig cfg; + + CLAM::ControlTraceOutFilename file(mOutputFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceWriter proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( true, configResult ); + } + + void testConfigure_ReturnsFalseWithoutTraceFileInConfig() + { + CLAM::ControlTraceWriterConfig cfg; + cfg.RemoveTraceFile(); + cfg.UpdateData(); + + CLAM::ControlTraceWriter proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( false, configResult ); + } + + void testConfigure_ReturnsTrueWhenFileExists() + { + CLAM::ControlTraceWriterConfig cfg; + + CLAM::ControlTraceOutFilename file(mOutputFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceWriter proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( true, configResult ); + } + + void testConfigure_ReturnsFalseWithoutInputsInConfig() + { + CLAM::ControlTraceWriterConfig cfg; + cfg.RemoveNumberOfInputs(); + cfg.UpdateData(); + + CLAM::ControlTraceOutFilename file(mOutputFileName); + cfg.SetTraceFile( file ); + + CLAM::ControlTraceWriter proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( false, configResult ); + + } + + void testConfigure_ReturnsFalseWhenConfiguredWithNoInputs() + { + CLAM::ControlTraceWriterConfig cfg; + + CLAM::ControlTraceOutFilename file(mOutputFileName); + cfg.SetTraceFile( file ); + cfg.SetNumberOfInputs( 0 ); + + CLAM::ControlTraceWriter proc; + + bool configResult = proc.Configure( cfg ); + + CPPUNIT_ASSERT_EQUAL( false, configResult ); + + } +}; + +} +