changeset 1483:7459f4c4d7c3

Merge from branch plugin-path-config
author Chris Cannam
date Mon, 11 Jun 2018 14:40:09 +0100
parents 85e9b7b31a8d (current diff) c014839f49c7 (diff)
children 5b2ce6c30258
files
diffstat 17 files changed, 694 insertions(+), 51 deletions(-) [+]
line wrap: on
line diff
--- a/base/ResourceFinder.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/base/ResourceFinder.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -35,6 +35,8 @@
 #include <iostream>
 #include <stdexcept>
 
+#include "system/System.h"
+
 /**
    Resource files may be found in three places:
 
@@ -67,10 +69,11 @@
     QStringList list;
 
 #ifdef Q_OS_WIN32
-    char *programFiles = getenv("ProgramFiles");
-    if (programFiles && programFiles[0]) {
+    std::string programFiles;
+    (void)getEnvUtf8("ProgramFiles", programFiles);
+    if (programFiles != "") {
         list << QString("%1/%2/%3")
-            .arg(programFiles)
+            .arg(QString::fromStdString(programFiles))
             .arg(qApp->organizationName())
             .arg(qApp->applicationName());
     } else {
--- a/data/model/test/svcore-data-model-test.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/data/model/test/svcore-data-model-test.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -34,10 +34,10 @@
     }
 
     if (bad > 0) {
-    SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
+        SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
         return 1;
     } else {
-    SVCERR << "All tests passed" << endl;
+        SVCERR << "All tests passed" << endl;
         return 0;
     }
 }
--- a/files.pri	Thu May 24 16:30:55 2018 +0100
+++ b/files.pri	Mon Jun 11 14:40:09 2018 +0100
@@ -108,6 +108,7 @@
            plugin/NativeVampPluginFactory.h \
            plugin/PiperVampPluginFactory.h \
            plugin/PluginIdentifier.h \
+           plugin/PluginPathSetter.h \
            plugin/PluginXml.h \
            plugin/RealTimePluginFactory.h \
            plugin/RealTimePluginInstance.h \
@@ -220,6 +221,7 @@
            plugin/NativeVampPluginFactory.cpp \
            plugin/PiperVampPluginFactory.cpp \
            plugin/PluginIdentifier.cpp \
+           plugin/PluginPathSetter.cpp \
            plugin/PluginXml.cpp \
            plugin/RealTimePluginFactory.cpp \
            plugin/RealTimePluginInstance.cpp \
--- a/plugin/DSSIPluginFactory.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/DSSIPluginFactory.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -39,6 +39,7 @@
 #include "lrdf.h"
 #endif // HAVE_LRDF
 
+using std::string;
 
 DSSIPluginFactory::DSSIPluginFactory() :
     LADSPAPluginFactory()
@@ -206,38 +207,38 @@
 DSSIPluginFactory::getPluginPath()
 {
     std::vector<QString> pathList;
-    std::string path;
+    string path;
 
-    char *cpath = getenv("DSSI_PATH");
-    if (cpath) path = cpath;
+    (void)getEnvUtf8("DSSI_PATH", path);
 
     if (path == "") {
 
         path = DEFAULT_DSSI_PATH;
 
-        char *home = getenv("HOME");
-        if (home) {
-            std::string::size_type f;
-            while ((f = path.find("$HOME")) != std::string::npos &&
+        string home;
+        if (getEnvUtf8("HOME", home)) {
+            string::size_type f;
+            while ((f = path.find("$HOME")) != string::npos &&
                    f < path.length()) {
                 path.replace(f, 5, home);
             }
         }
 
 #ifdef _WIN32
-        const char *pfiles = getenv("ProgramFiles");
-        if (!pfiles) pfiles = "C:\\Program Files";
-        {
-        std::string::size_type f;
-        while ((f = path.find("%ProgramFiles%")) != std::string::npos &&
+        string pfiles;
+        if (!getEnvUtf8("ProgramFiles", pfiles)) {
+            pfiles = "C:\\Program Files";
+        }
+
+        string::size_type f;
+        while ((f = path.find("%ProgramFiles%")) != string::npos &&
                f < path.length()) {
             path.replace(f, 14, pfiles);
         }
-        }
 #endif
     }
 
-    std::string::size_type index = 0, newindex = 0;
+    string::size_type index = 0, newindex = 0;
 
     while ((newindex = path.find(PATH_SEPARATOR, index)) < path.size()) {
         pathList.push_back(path.substr(index, newindex - index).c_str());
@@ -347,7 +348,7 @@
         }
 
         if (category == "") {
-            std::string name = rtd->name;
+            string name = rtd->name;
             if (name.length() > 4 &&
                 name.substr(name.length() - 4) == " VST") {
                 if (descriptor->run_synth || descriptor->run_multiple_synths) {
--- a/plugin/DSSIPluginFactory.h	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/DSSIPluginFactory.h	Mon Jun 11 14:40:09 2018 +0100
@@ -44,6 +44,8 @@
                                                       int blockSize,
                                                       int channels);
 
+    static std::vector<QString> getPluginPath();
+
 protected:
     DSSIPluginFactory();
     friend class RealTimePluginFactory;
@@ -52,8 +54,6 @@
         return PluginScan::DSSIPlugin;
     }
 
-    virtual std::vector<QString> getPluginPath();
-
     virtual std::vector<QString> getLRDFPath(QString &baseUri);
 
     virtual void discoverPluginsFrom(QString soName);
--- a/plugin/LADSPAPluginFactory.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/LADSPAPluginFactory.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -40,6 +40,7 @@
 #include "lrdf.h"
 #endif // HAVE_LRDF
 
+using std::string;
 
 LADSPAPluginFactory::LADSPAPluginFactory()
 {
@@ -566,38 +567,38 @@
 LADSPAPluginFactory::getPluginPath()
 {
     std::vector<QString> pathList;
-    std::string path;
+    string path;
 
-    char *cpath = getenv("LADSPA_PATH");
-    if (cpath) path = cpath;
+    (void)getEnvUtf8("DSSI_PATH", path);
 
     if (path == "") {
 
         path = DEFAULT_LADSPA_PATH;
 
-        char *home = getenv("HOME");
-        if (home) {
-            std::string::size_type f;
-            while ((f = path.find("$HOME")) != std::string::npos &&
+        string home;
+        if (getEnvUtf8("HOME", home)) {
+            string::size_type f;
+            while ((f = path.find("$HOME")) != string::npos &&
                    f < path.length()) {
                 path.replace(f, 5, home);
             }
         }
 
 #ifdef _WIN32
-        const char *pfiles = getenv("ProgramFiles");
-        if (!pfiles) pfiles = "C:\\Program Files";
-        {
-        std::string::size_type f;
-        while ((f = path.find("%ProgramFiles%")) != std::string::npos &&
+        string pfiles;
+        if (!getEnvUtf8("ProgramFiles", pfiles)) {
+            pfiles = "C:\\Program Files";
+        }
+
+        string::size_type f;
+        while ((f = path.find("%ProgramFiles%")) != string::npos &&
                f < path.length()) {
             path.replace(f, 14, pfiles);
         }
-        }
 #endif
     }
 
-    std::string::size_type index = 0, newindex = 0;
+    string::size_type index = 0, newindex = 0;
 
     while ((newindex = path.find(PATH_SEPARATOR, index)) < path.size()) {
         pathList.push_back(path.substr(index, newindex - index).c_str());
@@ -734,7 +735,7 @@
         QString category = m_taxonomy[identifier];
         
         if (category == "") {
-            std::string name = rtd->name;
+            string name = rtd->name;
             if (name.length() > 4 &&
                 name.substr(name.length() - 4) == " VST") {
                 category = "VST effects";
--- a/plugin/LADSPAPluginFactory.h	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/LADSPAPluginFactory.h	Mon Jun 11 14:40:09 2018 +0100
@@ -63,6 +63,8 @@
     float getPortQuantization(const LADSPA_Descriptor *, int port);
     int getPortDisplayHint(const LADSPA_Descriptor *, int port);
 
+    static std::vector<QString> getPluginPath();
+
 protected:
     LADSPAPluginFactory();
     friend class RealTimePluginFactory;
@@ -71,8 +73,6 @@
         return PluginScan::LADSPAPlugin;
     }
 
-    virtual std::vector<QString> getPluginPath();
-
     virtual std::vector<QString> getLRDFPath(QString &baseUri);
 
     virtual void discoverPluginsFrom(QString soName);
--- a/plugin/PiperVampPluginFactory.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/PiperVampPluginFactory.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -217,7 +217,10 @@
             string soname = QFileInfo(c.libraryPath).baseName().toStdString();
             SVDEBUG << "INFO: For tag \"" << tag << "\" giving library " << soname << endl;
             from.push_back(soname);
-            m_libraries[QString::fromStdString(soname)] = c.libraryPath;
+            QString qsoname = QString::fromStdString(soname);
+            if (m_libraries.find(qsoname) == m_libraries.end()) {
+                m_libraries[qsoname] = c.libraryPath;
+            }
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/PluginPathSetter.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -0,0 +1,278 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "PluginPathSetter.h"
+
+#include <vamp-hostsdk/PluginHostAdapter.h>
+
+#include "RealTimePluginFactory.h"
+#include "LADSPAPluginFactory.h"
+#include "DSSIPluginFactory.h"
+
+#include <QSettings>
+#include <QMutexLocker>
+
+#include "system/System.h"
+#include "base/Preferences.h"
+#include "base/HelperExecPath.h"
+
+QMutex
+PluginPathSetter::m_mutex;
+
+PluginPathSetter::Paths
+PluginPathSetter::m_defaultPaths;
+
+PluginPathSetter::Paths
+PluginPathSetter::m_environmentPaths;
+
+std::map<QString, QString>
+PluginPathSetter::m_originalEnvValues;
+
+PluginPathSetter::TypeKeys
+PluginPathSetter::m_supportedKeys;
+
+using namespace std;
+
+PluginPathSetter::TypeKeys
+PluginPathSetter::getSupportedKeys()
+{
+    QMutexLocker locker(&m_mutex);
+
+    if (!m_supportedKeys.empty()) {
+        return m_supportedKeys;
+    }
+
+    TypeKeys keys;
+    keys.push_back({ KnownPlugins::VampPlugin, KnownPlugins::FormatNative });
+    
+    bool inProcess = Preferences::getInstance()->getRunPluginsInProcess();
+    HelperExecPath hep(inProcess ?
+                       HelperExecPath::NativeArchitectureOnly :
+                       HelperExecPath::AllInstalled);
+    auto execs = hep.getHelperExecutables("vamp-plugin-load-checker");
+    if (execs.size() > 1) {
+        keys.push_back({
+                KnownPlugins::VampPlugin, KnownPlugins::FormatNonNative32Bit });
+    }
+
+    keys.push_back({ KnownPlugins::LADSPAPlugin, KnownPlugins::FormatNative });
+    keys.push_back({ KnownPlugins::DSSIPlugin, KnownPlugins::FormatNative });
+
+    m_supportedKeys = keys;
+    return keys;
+}
+
+// call with mutex held please
+PluginPathSetter::Paths
+PluginPathSetter::getEnvironmentPathsUncached(const TypeKeys &keys)
+{
+    Paths paths;
+
+    for (auto k: keys) {
+
+        KnownPlugins kp(k.second);
+
+        auto path = kp.getPathFor(k.first);
+        QStringList qPath;
+        for (auto s: path) {
+            qPath.push_back(QString::fromStdString(s));
+        }
+
+        auto var = kp.getPathEnvironmentVariableFor(k.first);
+        QString qVar = QString::fromStdString(var);
+        
+        paths[k] = { qPath, qVar, true };
+    }
+
+    return paths;
+}
+
+PluginPathSetter::Paths
+PluginPathSetter::getDefaultPaths()
+{
+    TypeKeys keys = getSupportedKeys();
+    
+    QMutexLocker locker(&m_mutex);
+
+    Paths paths;
+
+    for (auto k: keys) {
+
+        KnownPlugins kp(k.second);
+
+        auto path = kp.getDefaultPathFor(k.first);
+        QStringList qPath;
+        for (auto s: path) {
+            qPath.push_back(QString::fromStdString(s));
+        }
+
+        auto var = kp.getPathEnvironmentVariableFor(k.first);
+        QString qVar = QString::fromStdString(var);
+        
+        paths[k] = { qPath, qVar, true };
+    }
+
+    return paths;
+}
+
+PluginPathSetter::Paths
+PluginPathSetter::getEnvironmentPaths()
+{
+    TypeKeys keys = getSupportedKeys();
+    
+    QMutexLocker locker(&m_mutex);
+
+    if (!m_environmentPaths.empty()) {
+        return m_environmentPaths;
+    }
+        
+    m_environmentPaths = getEnvironmentPathsUncached(keys);
+    return m_environmentPaths;
+}
+
+QString
+PluginPathSetter::getSettingTagFor(TypeKey tk)
+{
+    string tag = KnownPlugins(tk.second).getTagFor(tk.first);
+    if (tk.second == KnownPlugins::FormatNonNative32Bit) {
+        tag += "-32";
+    }
+    return QString::fromStdString(tag);
+}
+
+PluginPathSetter::Paths
+PluginPathSetter::getPaths()
+{
+    Paths paths = getEnvironmentPaths();
+       
+    QSettings settings;
+    settings.beginGroup("Plugins");
+
+    for (auto p: paths) {
+
+        TypeKey tk = p.first;
+
+        QString settingTag = getSettingTagFor(tk);
+
+        QStringList directories =
+            settings.value(QString("directories-%1").arg(settingTag),
+                           p.second.directories)
+            .toStringList();
+        QString envVariable =
+            settings.value(QString("env-variable-%1").arg(settingTag),
+                           p.second.envVariable)
+            .toString();
+        bool useEnvVariable =
+            settings.value(QString("use-env-variable-%1").arg(settingTag),
+                           p.second.useEnvVariable)
+            .toBool();
+
+        string envVarStr = envVariable.toStdString();
+        string currentValue;
+        (void)getEnvUtf8(envVarStr, currentValue);
+
+        if (currentValue != "" && useEnvVariable) {
+            directories = QString::fromStdString(currentValue).split(
+#ifdef Q_OS_WIN
+               ";"
+#else
+               ":"
+#endif
+                );
+        }
+        
+        paths[tk] = { directories, envVariable, useEnvVariable };
+    }
+
+    settings.endGroup();
+
+    return paths;
+}
+
+void
+PluginPathSetter::savePathSettings(Paths paths)
+{
+    QSettings settings;
+    settings.beginGroup("Plugins");
+
+    for (auto p: paths) {
+        QString settingTag = getSettingTagFor(p.first);
+        settings.setValue(QString("directories-%1").arg(settingTag),
+                          p.second.directories);
+        settings.setValue(QString("env-variable-%1").arg(settingTag),
+                          p.second.envVariable);
+        settings.setValue(QString("use-env-variable-%1").arg(settingTag),
+                          p.second.useEnvVariable);
+    }
+
+    settings.endGroup();
+}
+
+QString
+PluginPathSetter::getOriginalEnvironmentValue(QString envVariable)
+{
+    if (m_originalEnvValues.find(envVariable) != m_originalEnvValues.end()) {
+        return m_originalEnvValues.at(envVariable);
+    } else {
+        return QString();
+    }
+}
+
+void
+PluginPathSetter::initialiseEnvironmentVariables()
+{
+    // Set the relevant environment variables from user configuration,
+    // so that later lookups through the standard APIs will follow the
+    // same paths as we have in the user config
+
+    // First ensure the default paths have been recorded for later, so
+    // we don't erroneously re-read them from the environment
+    // variables we've just set
+    (void)getDefaultPaths();
+    (void)getEnvironmentPaths();
+    
+    Paths paths = getPaths();
+
+    for (auto p: paths) {
+        QString envVariable = p.second.envVariable;
+        string envVarStr = envVariable.toStdString();
+        string currentValue;
+        getEnvUtf8(envVarStr, currentValue);
+        m_originalEnvValues[envVariable] = QString::fromStdString(currentValue);
+        if (currentValue != "" && p.second.useEnvVariable) {
+            // don't override
+            SVDEBUG << "PluginPathSetter: for environment variable "
+                    << envVariable << ", useEnvVariable setting is false; "
+                    << "leaving current value alone: it is \""
+                    << currentValue << "\"" << endl;
+            continue;
+        }
+        QString separator =
+#ifdef Q_OS_WIN
+            ";"
+#else
+            ":"
+#endif
+            ;
+        QString proposedValue = p.second.directories.join(separator);
+        SVDEBUG << "PluginPathSetter: for environment variable "
+                << envVariable << ", useEnvVariable setting is true or "
+                << "variable is currently unset; "
+                << "changing value from \"" << currentValue
+                << "\" to setting preference of \"" << proposedValue
+                << "\"" << endl;
+        putEnvUtf8(envVarStr, proposedValue.toStdString());
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/PluginPathSetter.h	Mon Jun 11 14:40:09 2018 +0100
@@ -0,0 +1,85 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_PLUGIN_PATH_SETTER_H
+#define SV_PLUGIN_PATH_SETTER_H
+
+#include <QString>
+#include <QStringList>
+#include <QMutex>
+
+#include <map>
+
+#include "checker/knownplugins.h"
+
+class PluginPathSetter
+{
+public:
+    typedef std::pair<KnownPlugins::PluginType,
+                      KnownPlugins::BinaryFormat> TypeKey;
+
+    typedef std::vector<TypeKey> TypeKeys;
+
+    struct PathConfig {
+        QStringList directories; // Actual list of directories arising
+                                 // from user settings, environment
+                                 // variables, and defaults as
+                                 // appropriate
+        
+        QString envVariable; // Name of env var, e.g. LADSPA_PATH
+        
+        bool useEnvVariable; // True if env variable should override
+                             // any user settings for this
+    };
+
+    typedef std::map<TypeKey, PathConfig> Paths;
+
+    /// Update *_PATH environment variables from the settings, on
+    /// application startup. Must be called exactly once, before any
+    /// of the other functions in this class has been called
+    static void initialiseEnvironmentVariables();
+
+    /// Return default values of paths only, without any environment
+    /// variables or user-defined preferences
+    static Paths getDefaultPaths();
+
+    /// Return paths arising from environment variables only, falling
+    /// back to the defaults, without any user-defined preferences
+    static Paths getEnvironmentPaths();
+
+    /// Return paths arising from user settings + environment
+    /// variables + defaults as appropriate
+    static Paths getPaths();
+
+    /// Save the given paths to the settings
+    static void savePathSettings(Paths paths);
+
+    /// Return the original value observed on startup for the given
+    /// environment variable, if it is one of the variables used by a
+    /// known path config.
+    static QString getOriginalEnvironmentValue(QString envVariable);
+    
+private:
+    static Paths m_defaultPaths;
+    static Paths m_environmentPaths;
+    static std::map<QString, QString> m_originalEnvValues;
+    static TypeKeys m_supportedKeys;
+    static QMutex m_mutex;
+
+    static std::vector<TypeKey> getSupportedKeys();
+    static Paths getEnvironmentPathsUncached(const TypeKeys &keys);
+    static QString getSettingTagFor(TypeKey);
+};
+
+#endif
--- a/plugin/PluginScan.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/PluginScan.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -19,9 +19,9 @@
 #include "base/HelperExecPath.h"
 
 #ifdef HAVE_PLUGIN_CHECKER_HELPER
-#include "checker/knownplugins.h"
+#include "checker/knownplugincandidates.h"
 #else
-class KnownPlugins {};
+class KnownPluginCandidates {};
 #endif
 
 #include <QMutex>
@@ -92,7 +92,7 @@
 
     for (auto p: helpers) {
         try {
-            KnownPlugins *kp = new KnownPlugins
+            KnownPluginCandidates *kp = new KnownPluginCandidates
                 (p.executable.toStdString(), m_logger);
             if (m_kp.find(p.tag) != m_kp.end()) {
                 SVDEBUG << "WARNING: PluginScan::scan: Duplicate tag " << p.tag
@@ -151,7 +151,7 @@
 
     for (auto rec: m_kp) {
 
-        KnownPlugins *kp = rec.second;
+        KnownPluginCandidates *kp = rec.second;
         
         auto c = kp->getCandidateLibrariesFor(kpt);
 
--- a/plugin/PluginScan.h	Thu May 24 16:30:55 2018 +0100
+++ b/plugin/PluginScan.h	Mon Jun 11 14:40:09 2018 +0100
@@ -20,7 +20,7 @@
 #include <vector>
 #include <map>
 
-class KnownPlugins;
+class KnownPluginCandidates;
 
 class PluginScan
 {
@@ -75,7 +75,7 @@
 
     mutable QMutex m_mutex; // while scanning; definitely can't multi-thread this
     
-    std::map<QString, KnownPlugins *> m_kp; // tag -> KnownPlugins client
+    std::map<QString, KnownPluginCandidates *> m_kp; // tag -> KnownPlugins client
     bool m_succeeded;
 
     class Logger;
--- a/system/System.cpp	Thu May 24 16:30:55 2018 +0100
+++ b/system/System.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -4,7 +4,7 @@
   Sonic Visualiser
   An audio file viewer and annotation editor.
   Centre for Digital Music, Queen Mary, University of London.
-  This file copyright 2006 Chris Cannam and QMUL.
+  This file copyright 2006-2018 Chris Cannam and QMUL.
     
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as
@@ -63,7 +63,7 @@
     }
 #endif
 
-    int gettimeofday(struct timeval *tv, void *tz)
+    int gettimeofday(struct timeval *tv, void * /* tz */)
     {
         union { 
             long long ns100;  
@@ -328,4 +328,109 @@
 double princarg(double a) { return mod(a + M_PI, -2 * M_PI) + M_PI; }
 float princargf(float a) { return float(princarg(a)); }
 
+bool
+getEnvUtf8(std::string variable, std::string &value)
+{
+    value = "";
+    
+#ifdef _WIN32
+    int wvarlen = MultiByteToWideChar(CP_UTF8, 0,
+                                      variable.c_str(), int(variable.length()),
+                                      0, 0);
+    if (wvarlen < 0) {
+        SVCERR << "WARNING: Unable to convert environment variable name "
+               << variable << " to wide characters" << endl;
+        return false;
+    }
+    
+    wchar_t *wvarbuf = new wchar_t[wvarlen + 1];
+    (void)MultiByteToWideChar(CP_UTF8, 0,
+                              variable.c_str(), int(variable.length()),
+                              wvarbuf, wvarlen);
+    wvarbuf[wvarlen] = L'\0';
+    
+    wchar_t *wvalue = _wgetenv(wvarbuf);
 
+    delete[] wvarbuf;
+
+    if (!wvalue) {
+        return false;
+    }
+
+    int wvallen = int(wcslen(wvalue));
+    int vallen = WideCharToMultiByte(CP_UTF8, 0,
+                                     wvalue, wvallen,
+                                     0, 0, 0, 0);
+    if (vallen < 0) {
+        SVCERR << "WARNING: Unable to convert environment value to UTF-8"
+               << endl;
+        return false;
+    }
+
+    char *val = new char[vallen + 1];
+    (void)WideCharToMultiByte(CP_UTF8, 0,
+                              wvalue, wvallen,
+                              val, vallen, 0, 0);
+    val[vallen] = '\0';
+
+    value = val;
+
+    delete[] val;
+    return true;
+
+#else
+
+    char *val = getenv(variable.c_str());
+    if (!val) {
+        return false;
+    }
+
+    value = val;
+    return true;
+    
+#endif
+}
+
+bool
+putEnvUtf8(std::string variable, std::string value)
+{
+#ifdef _WIN32
+    std::string entry = variable + "=" + value;
+    
+    int wentlen = MultiByteToWideChar(CP_UTF8, 0,
+                                      entry.c_str(), int(entry.length()),
+                                      0, 0);
+    if (wentlen < 0) {
+        SVCERR << "WARNING: Unable to convert environment entry to "
+               << "wide characters" << endl;
+        return false;
+    }
+    
+    wchar_t *wentbuf = new wchar_t[wentlen + 1];
+    (void)MultiByteToWideChar(CP_UTF8, 0,
+                              entry.c_str(), int(entry.length()),
+                              wentbuf, wentlen);
+    wentbuf[wentlen] = L'\0';
+
+    int rv = _wputenv(wentbuf);
+
+    delete[] wentbuf;
+
+    if (rv != 0) {
+        SVCERR << "WARNING: Failed to set environment entry" << endl;
+        return false;
+    }
+    return true;
+
+#else
+
+    int rv = setenv(variable.c_str(), value.c_str(), 1);
+    if (rv != 0) {
+        SVCERR << "WARNING: Failed to set environment entry" << endl;
+        return false;
+    }
+    return true;
+    
+#endif
+}
+
--- a/system/System.h	Thu May 24 16:30:55 2018 +0100
+++ b/system/System.h	Mon Jun 11 14:40:09 2018 +0100
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 Chris Cannam and QMUL.
+    This file copyright 2006-2018 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SYSTEM_H_
-#define _SYSTEM_H_
+#ifndef SV_SYSTEM_H
+#define SV_SYSTEM_H
 
 #include "base/Debug.h"
 
@@ -182,6 +182,18 @@
 #define powf pow
 #endif
 
+/** Return the value of the given environment variable by reference.
+    Return true if successfully retrieved, false if unset or on error.
+    Both the variable name and the returned value are UTF-8 encoded.
+*/
+extern bool getEnvUtf8(std::string variable, std::string &value);
+
+/** Set the value of the given environment variable.
+    Return true if successfully set, false on error.
+    Both the variable name and the value must be UTF-8 encoded.
+*/
+extern bool putEnvUtf8(std::string variable, std::string value);
+
 #endif /* ! _SYSTEM_H_ */
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/test/TestEnv.h	Mon Jun 11 14:40:09 2018 +0100
@@ -0,0 +1,107 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef TEST_ENV_H
+#define TEST_ENV_H
+
+#include "../System.h"
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+
+const std::string utf8_name_sprkt = "\343\202\271\343\203\235\343\203\274\343\202\257\343\201\256\345\257\272\351\231\242";
+
+class TestEnv : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void getAbsent()
+    {
+        string value = "blather";
+        bool rv = getEnvUtf8("nonexistent_environment_variable_I_sincerely_hope_including_a_missspellling_just_to_be_sure",
+                             value);
+        QCOMPARE(rv, false);
+        QCOMPARE(value, "");
+    }
+
+    void getExpected()
+    {
+        string value;
+        bool rv = getEnvUtf8("PATH", value);
+        QCOMPARE(rv, true);
+        QVERIFY(value != "");
+        QVERIFY(value.size() > 5); // Not quite but nearly certain,
+                                   // and weeds out an unfortunate
+                                   // case where we accidentally
+                                   // returned the variable's name
+                                   // instead of its value!
+    }
+    
+    void roundTripAsciiAscii()
+    {
+        bool rv = false;
+        rv = putEnvUtf8("SV_CORE_TEST_SYSTEM_RT_A_A", "EXPECTED_VALUE");
+        QCOMPARE(rv, true);
+        string value;
+        rv = getEnvUtf8("SV_CORE_TEST_SYSTEM_RT_A_A", value);
+        QCOMPARE(rv, true);
+        QCOMPARE(value, "EXPECTED_VALUE");
+    }
+    
+    void roundTripAsciiUtf8()
+    {
+        bool rv = false;
+        rv = putEnvUtf8("SV_CORE_TEST_SYSTEM_RT_A_U", utf8_name_sprkt);
+        QCOMPARE(rv, true);
+        string value;
+        rv = getEnvUtf8("SV_CORE_TEST_SYSTEM_RT_A_U", value);
+        QCOMPARE(rv, true);
+        QCOMPARE(value, utf8_name_sprkt);
+    }
+    
+    void roundTripUtf8Ascii()
+    {
+        bool rv = false;
+        rv = putEnvUtf8("SV_CORE_TEST_SYSTEM_RT_\351\207\215\345\272\206_A", "EXPECTED_VALUE");
+        QCOMPARE(rv, true);
+        string value;
+        rv = getEnvUtf8("SV_CORE_TEST_SYSTEM_RT_\351\207\215\345\272\206_A", value);
+        QCOMPARE(rv, true);
+        QCOMPARE(value, "EXPECTED_VALUE");
+    }
+
+    void roundTripUtf8Utf8()
+    {
+        bool rv = false;
+        rv = putEnvUtf8("SV_CORE_TEST_SYSTEM_RT_\351\207\215\345\272\206_A", utf8_name_sprkt);
+        QCOMPARE(rv, true);
+        string value;
+        rv = getEnvUtf8("SV_CORE_TEST_SYSTEM_RT_\351\207\215\345\272\206_A", value);
+        QCOMPARE(rv, true);
+        QCOMPARE(value, utf8_name_sprkt);
+    }
+};
+
+#endif
+
+    
+
+    
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/test/files.pri	Mon Jun 11 14:40:09 2018 +0100
@@ -0,0 +1,5 @@
+TEST_HEADERS = \
+	     TestEnv.h
+	     
+TEST_SOURCES += \
+	     svcore-system-test.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/test/svcore-system-test.cpp	Mon Jun 11 14:40:09 2018 +0100
@@ -0,0 +1,41 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "TestEnv.h"
+
+#include <QtTest>
+
+#include <iostream>
+
+int main(int argc, char *argv[])
+{
+    int good = 0, bad = 0;
+
+    QCoreApplication app(argc, argv);
+    app.setOrganizationName("sonic-visualiser");
+    app.setApplicationName("test-svcore-system");
+
+    {
+        TestEnv t;
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+    if (bad > 0) {
+        SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
+        return 1;
+    } else {
+        SVCERR << "All tests passed" << endl;
+        return 0;
+    }
+}