Hello,

I have been working on resolving fragmentation among LV2 implementations of plugin state persistence. The most recent / hopefully best extensions are:

http://lv2plug.in/ns/ext/persist <http://lv2plug.in/ns/ext/files/>
http://lv2plug.in/ns/ext/files <http://lv2plug.in/ns/ext/files/>

This is implemented in the latest Ardour3 SVN.

Attached is a patch for LinuxSampler that works with Ardour3 (at least as far as I have tested, e.g. you can load the plugin, load a bunch of sample banks, save the session, quit ardour, and reload the session, to have the sampler state successfully restored).

I have added a touch of API to Plugin to allow mapping of all filenames in the state string. This will allow the host to make a 'deep export' of LS plugin state for distribution/archival/etc. All other changes are local to PluginLv2.

These extensions are still provisional, feedback on them is welcome (when they are implemented in at least one other plugin and host (I'll probably do QTractor) and any issues people bring up have been addressed, then they will become stable/released).

Cheers,

-dr

P.S. It seems CVS (wow, really guys?) won't let me add a file without write access. You'll have to copy files.h from the above link to hostplugins/lv2/lv2_files.h to build.
? Doxyfile
? Makefile
? Makefile.in
? aclocal.m4
? autom4te.cache
? changes.diff
? config.guess
? config.h
? config.h.in
? config.log
? config.status
? config.sub
? configure
? depcomp
? install-sh
? libtool
? linuxsampler.pc
? linuxsampler.spec
? ltmain.sh
? missing
? stamp-h1
? Artwork/Makefile
? Artwork/Makefile.in
? Documentation/Makefile
? Documentation/Makefile.in
? Documentation/Engines/Makefile
? Documentation/Engines/Makefile.in
? Documentation/Engines/gig/Makefile
? Documentation/Engines/gig/Makefile.in
? debian/Makefile
? debian/Makefile.in
? man/Makefile
? man/Makefile.in
? man/linuxsampler.1
? osx/Makefile
? osx/Makefile.in
? osx/linuxsampler.xcodeproj/Makefile
? osx/linuxsampler.xcodeproj/Makefile.in
? scripts/Makefile
? scripts/Makefile.in
? src/.deps
? src/.libs
? src/Makefile
? src/Makefile.in
? src/Sampler.lo
? src/liblinuxsampler.la
? src/linuxsampler
? src/common/.deps
? src/common/.libs
? src/common/Condition.lo
? src/common/ConditionServer.lo
? src/common/DynamicLibraries.lo
? src/common/Features.lo
? src/common/File.lo
? src/common/IDGenerator.lo
? src/common/Makefile
? src/common/Makefile.in
? src/common/Mutex.lo
? src/common/Path.lo
? src/common/RTMath.lo
? src/common/Thread.lo
? src/common/WorkerThread.lo
? src/common/global_private.lo
? src/common/liblinuxsamplercommon.la
? src/common/optional.lo
? src/common/stacktrace.lo
? src/db/.deps
? src/db/.libs
? src/db/Makefile
? src/db/Makefile.in
? src/db/liblinuxsamplerdb.la
? src/db/liblinuxsamplerdb_la-InstrumentsDb.lo
? src/db/liblinuxsamplerdb_la-InstrumentsDbUtilities.lo
? src/drivers/.deps
? src/drivers/.libs
? src/drivers/Device.lo
? src/drivers/DeviceParameter.lo
? src/drivers/DeviceParameterFactory.lo
? src/drivers/Makefile
? src/drivers/Makefile.in
? src/drivers/Plugin.lo
? src/drivers/liblinuxsamplerdrivers.la
? src/drivers/audio/.deps
? src/drivers/audio/.libs
? src/drivers/audio/AudioChannel.lo
? src/drivers/audio/AudioOutputDevice.lo
? src/drivers/audio/AudioOutputDeviceAlsa.lo
? src/drivers/audio/AudioOutputDeviceFactory.lo
? src/drivers/audio/AudioOutputDeviceJack.lo
? src/drivers/audio/AudioOutputDevicePlugin.lo
? src/drivers/audio/Makefile
? src/drivers/audio/Makefile.in
? src/drivers/audio/liblinuxsampleraudiodriver.la
? src/drivers/midi/.deps
? src/drivers/midi/.libs
? src/drivers/midi/Makefile
? src/drivers/midi/Makefile.in
? src/drivers/midi/MidiInputDevice.lo
? src/drivers/midi/MidiInputDeviceAlsa.lo
? src/drivers/midi/MidiInputDeviceFactory.lo
? src/drivers/midi/MidiInputDeviceJack.lo
? src/drivers/midi/MidiInputDevicePlugin.lo
? src/drivers/midi/MidiInputPort.lo
? src/drivers/midi/MidiInstrumentMapper.lo
? src/drivers/midi/VirtualMidiDevice.lo
? src/drivers/midi/liblinuxsamplermididriver.la
? src/effects/.deps
? src/effects/.libs
? src/effects/Effect.lo
? src/effects/EffectChain.lo
? src/effects/EffectControl.lo
? src/effects/EffectFactory.lo
? src/effects/LadspaEffect.lo
? src/effects/Makefile
? src/effects/Makefile.in
? src/effects/liblinuxsamplereffects.la
? src/engines/.deps
? src/engines/.libs
? src/engines/AbstractEngine.lo
? src/engines/AbstractEngineChannel.lo
? src/engines/Engine.lo
? src/engines/EngineChannel.lo
? src/engines/EngineChannelFactory.lo
? src/engines/EngineFactory.lo
? src/engines/FxSend.lo
? src/engines/InstrumentManager.lo
? src/engines/InstrumentManagerThread.lo
? src/engines/Makefile
? src/engines/Makefile.in
? src/engines/liblinuxsamplerengines.la
? src/engines/common/.deps
? src/engines/common/.libs
? src/engines/common/AbstractVoice.lo
? src/engines/common/DiskThreadBase.lo
? src/engines/common/EG.lo
? src/engines/common/Event.lo
? src/engines/common/Makefile
? src/engines/common/Makefile.in
? src/engines/common/SampleFile.lo
? src/engines/common/StreamBase.lo
? src/engines/common/liblinuxsamplercommonengine.la
? src/engines/gig/.deps
? src/engines/gig/.libs
? src/engines/gig/DiskThread.lo
? src/engines/gig/EGADSR.lo
? src/engines/gig/EGDecay.lo
? src/engines/gig/Engine.lo
? src/engines/gig/EngineChannel.lo
? src/engines/gig/InstrumentResourceManager.lo
? src/engines/gig/Makefile
? src/engines/gig/Makefile.in
? src/engines/gig/Profiler.lo
? src/engines/gig/SmoothVolume.lo
? src/engines/gig/Stream.lo
? src/engines/gig/Synthesizer.lo
? src/engines/gig/Voice.lo
? src/engines/gig/liblinuxsamplergigengine.la
? src/engines/sf2/.deps
? src/engines/sf2/.libs
? src/engines/sf2/DiskThread.lo
? src/engines/sf2/Engine.lo
? src/engines/sf2/EngineChannel.lo
? src/engines/sf2/InstrumentResourceManager.lo
? src/engines/sf2/Makefile
? src/engines/sf2/Makefile.in
? src/engines/sf2/Stream.lo
? src/engines/sf2/Voice.lo
? src/engines/sf2/liblinuxsamplersf2engine.la
? src/engines/sfz/.deps
? src/engines/sfz/.libs
? src/engines/sfz/DiskThread.lo
? src/engines/sfz/EG.lo
? src/engines/sfz/EGADSR.lo
? src/engines/sfz/Engine.lo
? src/engines/sfz/EngineChannel.lo
? src/engines/sfz/InstrumentResourceManager.lo
? src/engines/sfz/LookupTable.lo
? src/engines/sfz/Makefile
? src/engines/sfz/Makefile.in
? src/engines/sfz/Stream.lo
? src/engines/sfz/Voice.lo
? src/engines/sfz/liblinuxsamplersfzengine.la
? src/engines/sfz/sfz.lo
? src/hostplugins/Makefile
? src/hostplugins/Makefile.in
? src/hostplugins/au/.deps
? src/hostplugins/au/Makefile
? src/hostplugins/au/Makefile.in
? src/hostplugins/dssi/.deps
? src/hostplugins/dssi/Makefile
? src/hostplugins/dssi/Makefile.in
? src/hostplugins/lv2/.deps
? src/hostplugins/lv2/.libs
? src/hostplugins/lv2/Makefile
? src/hostplugins/lv2/Makefile.in
? src/hostplugins/lv2/PluginLv2.lo
? src/hostplugins/lv2/linuxsampler.la
? src/hostplugins/lv2/lv2_files.h
? src/hostplugins/lv2/lv2_persist.h
? src/hostplugins/lv2/lv2_uri_map.h
? src/hostplugins/vst/.deps
? src/hostplugins/vst/Makefile
? src/hostplugins/vst/Makefile.in
? src/network/.deps
? src/network/.libs
? src/network/Makefile
? src/network/Makefile.in
? src/network/liblinuxsamplernetwork.la
? src/network/lscpevent.lo
? src/network/lscpparser.cpp
? src/network/lscpparser.lo
? src/network/lscpresultset.lo
? src/network/lscpserver.lo
? src/network/lscpsymbols.h
? src/plugins/.deps
? src/plugins/.libs
? src/plugins/InstrumentEditor.lo
? src/plugins/InstrumentEditorFactory.lo
? src/plugins/Makefile
? src/plugins/Makefile.in
? src/plugins/liblinuxsamplerplugins.la
? src/testcases/.deps
? src/testcases/Makefile
? src/testcases/Makefile.in
Index: AUTHORS
===================================================================
RCS file: /var/cvs/linuxsampler/linuxsampler/AUTHORS,v
retrieving revision 1.8
diff -u -r1.8 AUTHORS
--- AUTHORS	31 Jul 2009 07:45:47 -0000	1.8
+++ AUTHORS	3 Apr 2011 02:24:51 -0000
@@ -46,3 +46,4 @@
     Andrew Williams
     Devin Anderson
     Chris Cherrett
+    David Robillard
Index: src/drivers/Plugin.cpp
===================================================================
RCS file: /var/cvs/linuxsampler/linuxsampler/src/drivers/Plugin.cpp,v
retrieving revision 1.11
diff -u -r1.11 Plugin.cpp
--- src/drivers/Plugin.cpp	20 Feb 2011 14:20:21 -0000	1.11
+++ src/drivers/Plugin.cpp	3 Apr 2011 02:24:52 -0000
@@ -173,6 +173,20 @@
     }
 
     /*
+      These methods can be overloaded by different plugin types to map
+      file names to/from file names to be used in the state text, making
+      it possible for state to be self-contained and/or movable.
+    */
+
+    String Plugin::PathToState(const String& string) {
+        return string;
+    }
+
+    String Plugin::PathFromState(const String& string) {
+        return string;
+    }
+
+    /*
       The sampler state is stored in a text base format, designed to
       be easy to parse with the istream >> operator. Values are
       separated by spaces or newlines. All string values that may
@@ -218,7 +232,7 @@
                      int(iter->first.midi_bank_lsb)) << ' ' <<
                     int(iter->first.midi_prog) << ' ' <<
                     iter->second.EngineName << ' ' <<
-                    iter->second.InstrumentFile << '\n' <<
+                    PathToState(iter->second.InstrumentFile) << '\n' <<
                     MIDIMAPPING << ' ' <<
                     iter->second.InstrumentIndex << ' ' <<
                     iter->second.Volume << ' ' <<
@@ -240,7 +254,7 @@
                 String filename = engine_channel->InstrumentFileName();
                 s << channel->GetMidiInputChannel() << ' ' <<
                     engine_channel->Volume() << ' ' <<
-                    filename << '\n' <<
+                    PathToState(filename) << '\n' <<
                     engine_channel->InstrumentIndex() << ' ' <<
                     engine_channel->GetSolo() << ' ' <<
                     (engine_channel->GetMute() == 1) << ' ' <<
@@ -336,7 +350,7 @@
                 }
                 if (!filename.empty() && index != -1) {
                     InstrumentManager::instrument_id_t id;
-                    id.FileName = filename;
+                    id.FileName = PathFromState(filename);
                     id.Index    = index;
                     InstrumentManager::LoadInstrumentInBackground(id, engine_channel);
                 }
Index: src/drivers/Plugin.h
===================================================================
RCS file: /var/cvs/linuxsampler/linuxsampler/src/drivers/Plugin.h,v
retrieving revision 1.4
diff -u -r1.4 Plugin.h
--- src/drivers/Plugin.h	28 Aug 2009 07:48:46 -0000	1.4
+++ src/drivers/Plugin.h	3 Apr 2011 02:24:52 -0000
@@ -69,6 +69,9 @@
         void PreInit();
         void Init(int SampleRate, int FragmentSize, int Channels = -1);
 
+        virtual String PathToState(const String& string);
+        virtual String PathFromState(const String& string);
+        
         void InitState();
         String GetState();
         bool SetState(String State);
Index: src/hostplugins/lv2/PluginLv2.cpp
===================================================================
RCS file: /var/cvs/linuxsampler/linuxsampler/src/hostplugins/lv2/PluginLv2.cpp,v
retrieving revision 1.1
diff -u -r1.1 PluginLv2.cpp
--- src/hostplugins/lv2/PluginLv2.cpp	15 Sep 2008 16:58:10 -0000	1.1
+++ src/hostplugins/lv2/PluginLv2.cpp	3 Apr 2011 02:24:52 -0000
@@ -19,6 +19,7 @@
  ***************************************************************************/
 
 #include <algorithm>
+#include <cassert>
 #include <cstdio>
 #include <cstdlib>
 #include <cstring>
@@ -34,8 +35,15 @@
                          const LV2_Feature* const* Features) {
         Out[0] = 0;
         Out[1] = 0;
+        UriMap = 0;
+        FileSupport = 0;
         for (int i = 0 ; Features[i] ; i++) {
             dmsg(2, ("linuxsampler: host feature: %s\n", Features[i]->URI));
+            if (!strcmp(Features[i]->URI, LV2_URI_MAP_URI)) {
+                UriMap = (LV2_URI_Map_Feature*)Features[i]->data;
+            } else if (!strcmp(Features[i]->URI, LV2_FILES_FILE_SUPPORT_URI)) {
+                FileSupport = (LV2_Files_File_Support*)Features[i]->data;
+            }
         }
 
         Init(SampleRate, 128);
@@ -86,44 +94,97 @@
         dmsg(2, ("linuxsampler: Deactivate\n"));
     }
 
-    char* PluginLv2::Save(const char* Directory, LV2SR_File*** Files) {
-        dmsg(2, ("linuxsampler: Save Directory=%s\n", Directory));
-
-        String filename(Directory);
-        filename += "/linuxsampler";
-        dmsg(2, ("saving to %s\n", filename.c_str()));
-        std::ofstream out(filename.c_str());
-        out << GetState();
-        out.close();
-
-        LV2SR_File** filearr = static_cast<LV2SR_File**>(malloc(sizeof(LV2SR_File*) * 2));
-
-        LV2SR_File* file = static_cast<LV2SR_File*>(malloc(sizeof(LV2SR_File)));
-        file->name = strdup("linuxsampler");
-        file->path = strdup(filename.c_str());
-        file->must_copy = 0;
-
-        filearr[0] = file;
-        filearr[1] = 0;
+    String PluginLv2::PathToState(const String& path) {
+        if (FileSupport) {
+            char* cstr = FileSupport->abstract_path(FileSupport->host_data,
+                                                    path.c_str());
+            const String abstract_path(cstr);
+            free(cstr);
+            return abstract_path;
+        }
+        return path;
+    }
 
-        *Files = filearr;
+    String PluginLv2::PathFromState(const String& path) {
+        if (FileSupport) {
+            char* cstr = FileSupport->absolute_path(FileSupport->host_data,
+                                                    path.c_str());
+            const String abstract_path(cstr);
+            free(cstr);
+            return abstract_path;
+        }
+        return path;
+    }
 
+    void PluginLv2::Save(LV2_Persist_Store_Function store, void* host_data) {
+        if (FileSupport) {
+            char* path = FileSupport->new_file_path(FileSupport->host_data,
+                                                    "linuxsampler");
+            dmsg(2, ("saving to file %s\n", path));
+
+            std::ofstream out(path);
+            out << GetState();
+
+            const String abstract_path = PathToState(path);
+
+            store(host_data,
+                  uri_to_id(NULL, "http://linuxsampler.org/ns/state-file";),
+                  abstract_path.c_str(),
+                  abstract_path.length() + 1,
+                  uri_to_id(NULL, LV2_FILES_URI "#AbstractPath"),
+                  LV2_PERSIST_IS_PORTABLE);
+        } else {
+            dmsg(2, ("saving to string\n"));
+
+            std::ostringstream out;
+            out << GetState();
+
+            store(host_data,
+                  uri_to_id(NULL, "http://linuxsampler.org/ns/state-string";),
+                  out.str().c_str(),
+                  out.str().length() + 1,
+                  uri_to_id(NULL, "http://www.w3.org/2001/XMLSchema#string";),
+                  LV2_PERSIST_IS_POD | LV2_PERSIST_IS_PORTABLE);
+        }
         dmsg(2, ("saving done\n"));
-        return 0;
     }
 
-    char* PluginLv2::Restore(const LV2SR_File** Files) {
-        dmsg(2, ("linuxsampler: restore\n"));
-        for (int i = 0 ; Files[i] ; i++) {
-            dmsg(2, ("  name=%s path=%s\n", Files[i]->name, Files[i]->path));
-            if (strcmp(Files[i]->name, "linuxsampler") == 0) {
-                std::ifstream in(Files[0]->path);
-                String state;
-                std::getline(in, state, '\0');
-                SetState(state);
-            }
+    void PluginLv2::Restore(LV2_Persist_Retrieve_Function retrieve, void* data) {
+        size_t   size;
+        uint32_t type;
+        uint32_t flags;
+
+        const void* value = retrieve(
+            data,
+            uri_to_id(NULL, "http://linuxsampler.org/ns/state-file";),
+            &size, &type, &flags);
+
+        if (value) {
+            assert(type == uri_to_id(NULL, LV2_FILES_URI "#AbstractPath"));
+            const String path = PathFromState((const char*)value);
+            dmsg(2, ("linuxsampler: restoring from file %s\n", path.c_str()));
+            std::ifstream in(path.c_str());
+            String state;
+            std::getline(in, state, '\0');
+            SetState(state);
+            return;
         }
-        return 0;
+
+        value = retrieve(
+                data,
+                uri_to_id(NULL, "http://linuxsampler.org/ns/state-string";),
+                &size, &type, &flags);
+        if (value) {
+            dmsg(2, ("linuxsampler: restoring from string\n"));
+            assert(type == uri_to_id(NULL, "http://www.w3.org/2001/XMLSchema#string";));
+            String state((const char*)value);
+            SetState(state);
+            return;
+        }
+
+        // No valid state found, reset to default state
+        dmsg(2, ("linuxsampler: restoring default state\n"));
+        SetState("");
     }
 
     LV2_Handle instantiate(const LV2_Descriptor* descriptor,
@@ -152,12 +213,12 @@
         delete static_cast<PluginLv2*>(instance);
     }
 
-    char* save(LV2_Handle handle, const char* directory, LV2SR_File*** files) {
-        return static_cast<PluginLv2*>(handle)->Save(directory, files);
+    void save(LV2_Handle handle, LV2_Persist_Store_Function store, void* callback_data) {
+	    return static_cast<PluginLv2*>(handle)->Save(store, callback_data);
     }
 
-    char* restore(LV2_Handle handle, const LV2SR_File** files) {
-        return static_cast<PluginLv2*>(handle)->Restore(files);
+    void restore(LV2_Handle handle, LV2_Persist_Retrieve_Function store, void* callback_data) {
+        return static_cast<PluginLv2*>(handle)->Restore(store, callback_data);
     }
 
     PluginInfo PluginInfo::Instance;
@@ -171,15 +232,15 @@
         Lv2.instantiate = instantiate;
         Lv2.run = run;
         Lv2.extension_data = extension_data;
-        Lv2sr.save = save;
-        Lv2sr.restore = restore;
+        Persist.save = save;
+        Persist.restore = restore;
     }
 
 
     const void* extension_data(const char* uri) {
         dmsg(2, ("linuxsampler: extension_data %s\n", uri));
-        if (strcmp(uri, LV2_SAVERESTORE_URI) == 0) {
-            return PluginInfo::Lv2srDescriptor();
+        if (strcmp(uri, LV2_PERSIST_URI) == 0) {
+            return PluginInfo::Lv2PersistDescriptor();
         }
         return 0;
     }
Index: src/hostplugins/lv2/PluginLv2.h
===================================================================
RCS file: /var/cvs/linuxsampler/linuxsampler/src/hostplugins/lv2/PluginLv2.h,v
retrieving revision 1.1
diff -u -r1.1 PluginLv2.h
--- src/hostplugins/lv2/PluginLv2.h	15 Sep 2008 16:58:10 -0000	1.1
+++ src/hostplugins/lv2/PluginLv2.h	3 Apr 2011 02:24:52 -0000
@@ -23,7 +23,9 @@
 
 #include <lv2.h>
 #include "lv2_event.h"
-#include "lv2-saverestore.h"
+#include "lv2_persist.h"
+#include "lv2_uri_map.h"
+#include "lv2_files.h"
 #include "../../drivers/Plugin.h"
 
 namespace {
@@ -37,12 +39,22 @@
         void Activate();
         void Run(uint32_t SampleCount);
         void Deactivate();
-        char* Save(const char* Directory, LV2SR_File*** Files);
-        char* Restore(const LV2SR_File** Files);
+        void Save(LV2_Persist_Store_Function store, void* data);
+        void Restore(LV2_Persist_Retrieve_Function retrieve, void* data);
+
+	protected:
+        virtual String PathToState(const String& string);
+        virtual String PathFromState(const String& string);
 
     private:
+        uint32_t uri_to_id(const char* map, const char* uri) {
+	        return UriMap->uri_to_id(UriMap->callback_data, map, uri);
+        }
+
         float* Out[2];
         LV2_Event_Buffer* MidiBuf;
+        LV2_URI_Map_Feature* UriMap;
+        LV2_Files_File_Support* FileSupport;
     };
 
     class PluginInfo {
@@ -50,12 +62,12 @@
         static const LV2_Descriptor* Lv2Descriptor() {
             return &Instance.Lv2;
         }
-        static const LV2SR_Descriptor* Lv2srDescriptor() {
-            return &Instance.Lv2sr;
+        static const LV2_Persist* Lv2PersistDescriptor() {
+            return &Instance.Persist;
         }
     private:
         LV2_Descriptor Lv2;
-        LV2SR_Descriptor Lv2sr;
+        LV2_Persist Persist;
 
         PluginInfo();
         static PluginInfo Instance;
@@ -73,9 +85,13 @@
         static void cleanup(LV2_Handle instance);
         static const void* extension_data(const char* uri);
 
-        static char* save(LV2_Handle handle, const char* directory,
-                          LV2SR_File*** files);
-        static char* restore(LV2_Handle handle, const LV2SR_File** files);
+        static void save(LV2_Handle                 handle,
+                         LV2_Persist_Store_Function store,
+                         void*                      data);
+
+        static void restore(LV2_Handle                    handle,
+                            LV2_Persist_Retrieve_Function retrieve,
+                            void*                         data);
     }
 }
 
Index: src/hostplugins/lv2/linuxsampler.ttl
===================================================================
RCS file: /var/cvs/linuxsampler/linuxsampler/src/hostplugins/lv2/linuxsampler.ttl,v
retrieving revision 1.1
diff -u -r1.1 linuxsampler.ttl
--- src/hostplugins/lv2/linuxsampler.ttl	15 Sep 2008 16:58:10 -0000	1.1
+++ src/hostplugins/lv2/linuxsampler.ttl	3 Apr 2011 02:24:52 -0000
@@ -8,7 +8,8 @@
     doap:name "LinuxSampler" ;
     doap:license <http://linuxsampler.org/downloads.html#exception> ;
     lv2:optionalFeature lv2:hardRtCapable ;
-    lv2:optionalFeature <http://ll-plugins.nongnu.org/lv2/ext/saverestore> ;
+    lv2:optionalFeature <http://lv2plug.in/ns/ext/persist> ;
+    lv2:optionalFeature <http://lv2plug.in/ns/ext/uri-map> ;
     lv2:port [
 	a lv2:AudioPort , lv2:OutputPort ;
 	lv2:index 0 ;
------------------------------------------------------------------------------
Create and publish websites with WebMatrix
Use the most popular FREE web apps or write code yourself; 
WebMatrix provides all the features you need to develop and 
publish your website. http://p.sf.net/sfu/ms-webmatrix-sf
_______________________________________________
Linuxsampler-devel mailing list
Linuxsampler-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/linuxsampler-devel

Reply via email to