changeset 180:98ba77e0d897

* Merge from sv-match-alignment branch (excluding alignment-specific document). - add aggregate wave model (not yet complete enough to be added as a true model in a layer, but there's potential) - add play solo mode - add alignment model -- unused in plain SV - fix two plugin leaks - add m3u playlist support (opens all files at once, potentially hazardous) - fix retrieval of pre-encoded URLs - add ability to resample audio files on import, so as to match rates with other files previously loaded; add preference for same - add preliminary support in transform code for range and rate of transform input - reorganise preferences dialog, move dark-background option to preferences, add option for temporary directory location
author Chris Cannam
date Fri, 28 Sep 2007 13:56:38 +0000
parents dab257bd9d2d
children a65a01870d8c
files audioio/AudioCallbackPlaySource.cpp audioio/AudioCallbackPlaySource.h audioio/AudioGenerator.cpp audioio/AudioGenerator.h document/Document.cpp document/Document.h main/MainWindow.cpp main/MainWindow.h main/PreferencesDialog.cpp main/PreferencesDialog.h main/main.cpp sonic-visualiser.qrc
diffstat 12 files changed, 491 insertions(+), 71 deletions(-) [+]
line wrap: on
line diff
--- a/audioio/AudioCallbackPlaySource.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/audioio/AudioCallbackPlaySource.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -697,6 +697,20 @@
     if (formerPlugin) m_pluginScavenger.claim(formerPlugin);
 }
 
+void
+AudioCallbackPlaySource::setSoloModelSet(std::set<Model *> s)
+{
+    m_audioGenerator->setSoloModelSet(s);
+    clearRingBuffers();
+}
+
+void
+AudioCallbackPlaySource::clearSoloModelSet()
+{
+    m_audioGenerator->clearSoloModelSet();
+    clearRingBuffers();
+}
+
 size_t
 AudioCallbackPlaySource::getTargetSampleRate() const
 {
@@ -1108,9 +1122,9 @@
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
             std::cout << "Using crappy converter" << std::endl;
 #endif
-            src_process(m_crapConverter, &data);
+            err = src_process(m_crapConverter, &data);
         } else {
-            src_process(m_converter, &data);
+            err = src_process(m_converter, &data);
         }
 
 	size_t toCopy = size_t(got * ratio + 0.1);
--- a/audioio/AudioCallbackPlaySource.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/audioio/AudioCallbackPlaySource.h	Fri Sep 28 13:56:38 2007 +0000
@@ -215,6 +215,17 @@
      */
     void setAuditioningPlugin(RealTimePluginInstance *plugin);
 
+    /**
+     * Specify that only the given set of models should be played.
+     */
+    void setSoloModelSet(std::set<Model *>s);
+
+    /**
+     * Specify that all models should be played as normal (if not
+     * muted).
+     */
+    void clearSoloModelSet();
+
 signals:
     void modelReplaced();
 
--- a/audioio/AudioGenerator.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/audioio/AudioGenerator.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -47,7 +47,8 @@
 
 AudioGenerator::AudioGenerator() :
     m_sourceSampleRate(0),
-    m_targetChannelCount(1)
+    m_targetChannelCount(1),
+    m_soloing(false)
 {
     connect(PlayParameterRepository::getInstance(),
             SIGNAL(playPluginIdChanged(const Model *, QString)),
@@ -352,6 +353,26 @@
     return m_pluginBlockSize;
 }
 
+void
+AudioGenerator::setSoloModelSet(std::set<Model *> s)
+{
+    QMutexLocker locker(&m_mutex);
+
+    std::cerr << "setting solo set" << std::endl;
+
+    m_soloModelSet = s;
+    m_soloing = true;
+}
+
+void
+AudioGenerator::clearSoloModelSet()
+{
+    QMutexLocker locker(&m_mutex);
+
+    m_soloModelSet.clear();
+    m_soloing = false;
+}
+
 size_t
 AudioGenerator::mixModel(Model *model, size_t startFrame, size_t frameCount,
 			 float **buffer, size_t fadeIn, size_t fadeOut)
@@ -375,6 +396,15 @@
         return frameCount;
     }
 
+    if (m_soloing) {
+        if (m_soloModelSet.find(model) == m_soloModelSet.end()) {
+#ifdef DEBUG_AUDIO_GENERATOR
+            std::cout << "AudioGenerator::mixModel(" << model << "): not one of the solo'd models" << std::endl;
+#endif
+            return frameCount;
+        }
+    }
+
     float gain = parameters->getPlayGain();
     float pan = parameters->getPlayPan();
 
--- a/audioio/AudioGenerator.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/audioio/AudioGenerator.h	Fri Sep 28 13:56:38 2007 +0000
@@ -90,6 +90,17 @@
     virtual size_t mixModel(Model *model, size_t startFrame, size_t frameCount,
 			    float **buffer, size_t fadeIn = 0, size_t fadeOut = 0);
 
+    /**
+     * Specify that only the given set of models should be played.
+     */
+    virtual void setSoloModelSet(std::set<Model *>s);
+
+    /**
+     * Specify that all models should be played as normal (if not
+     * muted).
+     */
+    virtual void clearSoloModelSet();
+
 protected slots:
     void playPluginIdChanged(const Model *, QString);
     void playPluginConfigurationChanged(const Model *, QString);
@@ -98,6 +109,9 @@
     size_t       m_sourceSampleRate;
     size_t       m_targetChannelCount;
 
+    bool m_soloing;
+    std::set<Model *> m_soloModelSet;
+
     struct NoteOff {
 
 	int pitch;
--- a/document/Document.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/document/Document.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -334,6 +334,8 @@
     rec.configurationXml = configurationXml;
     rec.refcount = 0;
 
+    outputModelToAdd->setSourceModel(inputModel);
+
     m_models[outputModelToAdd] = rec;
 
     emit modelAdded(outputModelToAdd);
--- a/document/Document.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/document/Document.h	Fri Sep 28 13:56:38 2007 +0000
@@ -221,9 +221,8 @@
 
     /*
      * Every model that is in use by a layer in the document must be
-     * found in either m_mainModel, m_derivedModels or
-     * m_importedModels.  We own and control the lifespan of all of
-     * these models.
+     * found in either m_mainModel or m_models.  We own and control
+     * the lifespan of all of these models.
      */
 
     /**
--- a/main/MainWindow.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/main/MainWindow.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -47,8 +47,8 @@
 #include "audioio/AudioCallbackPlayTarget.h"
 #include "audioio/AudioTargetFactory.h"
 #include "audioio/PlaySpeedRangeMapper.h"
-#include "data/fileio/AudioFileReaderFactory.h"
 #include "data/fileio/DataFileReaderFactory.h"
+#include "data/fileio/PlaylistFileReader.h"
 #include "data/fileio/WavFileWriter.h"
 #include "data/fileio/CSVFileWriter.h"
 #include "data/fileio/BZipFileDevice.h"
@@ -65,6 +65,9 @@
 #include "base/ColourDatabase.h"
 #include "osc/OSCQueue.h"
 
+//!!!
+#include "data/model/AggregateWaveModel.h"
+
 // For version information
 #include "vamp/vamp.h"
 #include "vamp-sdk/PluginBase.h"
@@ -177,6 +180,14 @@
     connect(m_viewManager, SIGNAL(inProgressSelectionChanged()),
 	    this, SLOT(inProgressSelectionChanged()));
 
+    Preferences::BackgroundMode mode =
+        Preferences::getInstance()->getBackgroundMode();
+    m_initialDarkBackground = m_viewManager->getGlobalDarkBackground();
+    if (mode != Preferences::BackgroundFromTheme) {
+        m_viewManager->setGlobalDarkBackground
+            (mode == Preferences::DarkBackground);
+    }
+
     m_descriptionLabel = new QLabel;
 
     QScrollArea *scroll = new QScrollArea(frame);
@@ -850,15 +861,6 @@
         
     menu->addSeparator();
 
-    action = new QAction(tr("Use Dar&k Background"), this);
-    action->setStatusTip(tr("Switch between light and dark background colour schemes"));
-    connect(action, SIGNAL(triggered()), this, SLOT(toggleDarkBackground()));
-    action->setCheckable(true);
-    action->setChecked(m_viewManager->getGlobalDarkBackground());
-    menu->addAction(action);
-
-    menu->addSeparator();
-
     action = new QAction(tr("Show &Zoom Wheels"), this);
     action->setShortcut(tr("Z"));
     action->setStatusTip(tr("Show thumbwheels for zooming horizontally and vertically"));
@@ -1684,9 +1686,21 @@
     connect(plAction, SIGNAL(triggered()), this, SLOT(playLoopToggled()));
     connect(this, SIGNAL(canPlay(bool)), plAction, SLOT(setEnabled(bool)));
 
+    QAction *soAction = toolbar->addAction(il.load("solo"),
+                                           tr("Solo Current Pane"));
+    soAction->setCheckable(true);
+    soAction->setChecked(m_viewManager->getPlaySoloMode());
+    soAction->setShortcut(tr("o"));
+    soAction->setStatusTip(tr("Solo the current pane during playback"));
+    connect(m_viewManager, SIGNAL(playSoloModeChanged(bool)),
+            soAction, SLOT(setChecked(bool)));
+    connect(soAction, SIGNAL(triggered()), this, SLOT(playSoloToggled()));
+    connect(this, SIGNAL(canPlay(bool)), soAction, SLOT(setEnabled(bool)));
+
     m_keyReference->registerShortcut(playAction);
     m_keyReference->registerShortcut(psAction);
     m_keyReference->registerShortcut(plAction);
+    m_keyReference->registerShortcut(soAction);
     m_keyReference->registerShortcut(m_rwdAction);
     m_keyReference->registerShortcut(m_ffwdAction);
     m_keyReference->registerShortcut(rwdStartAction);
@@ -1695,6 +1709,7 @@
     menu->addAction(playAction);
     menu->addAction(psAction);
     menu->addAction(plAction);
+    menu->addAction(soAction);
     menu->addSeparator();
     menu->addAction(m_rwdAction);
     menu->addAction(m_ffwdAction);
@@ -1706,6 +1721,7 @@
     m_rightButtonPlaybackMenu->addAction(playAction);
     m_rightButtonPlaybackMenu->addAction(psAction);
     m_rightButtonPlaybackMenu->addAction(plAction);
+    m_rightButtonPlaybackMenu->addAction(soAction);
     m_rightButtonPlaybackMenu->addSeparator();
     m_rightButtonPlaybackMenu->addAction(m_rwdAction);
     m_rightButtonPlaybackMenu->addAction(m_ffwdAction);
@@ -1996,10 +2012,72 @@
 }
 
 void
+MainWindow::playSoloToggled()
+{
+    QAction *action = dynamic_cast<QAction *>(sender());
+    
+    if (action) {
+	m_viewManager->setPlaySoloMode(action->isChecked());
+    } else {
+	m_viewManager->setPlaySoloMode(!m_viewManager->getPlaySoloMode());
+    }
+
+    if (!m_viewManager->getPlaySoloMode()) {
+        m_viewManager->setPlaybackModel(0);
+        if (m_playSource) {
+            m_playSource->clearSoloModelSet();
+        }
+    }
+}
+
+void
 MainWindow::currentPaneChanged(Pane *p)
 {
     updateMenuStates();
     updateVisibleRangeDisplay(p);
+
+    if (!p) return;
+
+    if (!(m_viewManager &&
+          m_playSource &&
+          m_viewManager->getPlaySoloMode())) {
+        if (m_viewManager) m_viewManager->setPlaybackModel(0);
+        return;
+    }
+
+    Model *prevPlaybackModel = m_viewManager->getPlaybackModel();
+
+    std::set<Model *> soloModels;
+
+    for (int i = 0; i < p->getLayerCount(); ++i) {
+        Layer *layer = p->getLayer(i);
+        if (dynamic_cast<TimeRulerLayer *>(layer)) {
+            continue;
+        }
+        if (layer && layer->getModel()) {
+            Model *model = layer->getModel();
+            if (dynamic_cast<RangeSummarisableTimeValueModel *>(model)) {
+                m_viewManager->setPlaybackModel(model);
+            }
+            soloModels.insert(model);
+        }
+    }
+    
+    RangeSummarisableTimeValueModel *a = 
+        dynamic_cast<RangeSummarisableTimeValueModel *>(prevPlaybackModel);
+    RangeSummarisableTimeValueModel *b = 
+        dynamic_cast<RangeSummarisableTimeValueModel *>(m_viewManager->
+                                                        getPlaybackModel());
+
+    m_playSource->setSoloModelSet(soloModels);
+
+    if (a && b && (a != b)) {
+        int frame = m_playSource->getCurrentPlayingFrame();
+        //!!! I don't really believe that these functions are the right way around
+        int rframe = a->alignFromReference(frame);
+        int bframe = b->alignToReference(rframe);
+        if (m_playSource->isPlaying()) m_playSource->play(bframe);
+    }
 }
 
 void
@@ -2680,7 +2758,13 @@
 
     m_openingAudioFile = true;
 
-    WaveFileModel *newModel = new WaveFileModel(path, location);
+    size_t rate = 0;
+
+    if (Preferences::getInstance()->getResampleOnLoad()) {
+        rate = m_playSource->getSourceSampleRate();
+    }
+
+    WaveFileModel *newModel = new WaveFileModel(path, location, rate);
 
     if (!newModel->isOK()) {
 	delete newModel;
@@ -2784,9 +2868,57 @@
     }
     m_openingAudioFile = false;
 
+    currentPaneChanged(m_paneStack->getCurrentPane());
+
     return FileOpenSucceeded;
 }
 
+MainWindow::FileOpenStatus
+MainWindow::openPlaylistFile(QString path, AudioFileOpenMode mode)
+{
+    return openPlaylistFile(path, path, mode);
+}
+
+MainWindow::FileOpenStatus
+MainWindow::openPlaylistFile(QString path, QString location, AudioFileOpenMode mode)
+{
+    if (!(QFileInfo(path).exists() &&
+	  QFileInfo(path).isFile() &&
+	  QFileInfo(path).isReadable())) {
+	return FileOpenFailed;
+    }
+    
+    std::set<QString> extensions;
+    PlaylistFileReader::getSupportedExtensions(extensions);
+    QString extension = QFileInfo(path).suffix();
+    if (extensions.find(extension) == extensions.end()) return FileOpenFailed;
+
+    PlaylistFileReader reader(path);
+    if (!reader.isOK()) return FileOpenFailed;
+
+    PlaylistFileReader::Playlist playlist = reader.load();
+
+    bool someSuccess = false;
+
+    for (PlaylistFileReader::Playlist::const_iterator i = playlist.begin();
+         i != playlist.end(); ++i) {
+
+        FileOpenStatus status = openURL(*i, mode);
+
+        if (status == FileOpenCancelled) {
+            return FileOpenCancelled;
+        }
+
+        if (status == FileOpenSucceeded) {
+            someSuccess = true;
+            mode = CreateAdditionalModel;
+        }
+    }
+
+    if (someSuccess) return FileOpenSucceeded;
+    else return FileOpenFailed;
+}
+
 void
 MainWindow::createPlayTarget()
 {
@@ -2965,12 +3097,15 @@
 
     } else {
 
-        if (openAudioFile(path, AskUser) == FileOpenFailed) {
-
-            if (!canImportLayer || (openLayerFile(path) == FileOpenFailed)) {
-
-                QMessageBox::critical(this, tr("Failed to open file"),
-                                      tr("File \"%1\" could not be opened").arg(path));
+        if (openPlaylistFile(path, AskUser) == FileOpenFailed) {
+
+            if (openAudioFile(path, AskUser) == FileOpenFailed) {
+
+                if (!canImportLayer || (openLayerFile(path) == FileOpenFailed)) {
+
+                    QMessageBox::critical(this, tr("Failed to open file"),
+                                          tr("File \"%1\" could not be opened").arg(path));
+                }
             }
         }
     }
@@ -3033,31 +3168,38 @@
 
     } else {
 
-        if (openAudioFile(path, AskUser) == FileOpenFailed) {
-
-            bool canImportLayer = (getMainModel() != 0 &&
-                                   m_paneStack != 0 &&
-                                   m_paneStack->getCurrentPane() != 0);
-
-            if (!canImportLayer || (openLayerFile(path) == FileOpenFailed)) {
-
-                QMessageBox::critical(this, tr("Failed to open file"),
-                                      tr("File \"%1\" could not be opened").arg(path));
+        if (openPlaylistFile(path, AskUser) == FileOpenFailed) {
+
+            if (openAudioFile(path, AskUser) == FileOpenFailed) {
+
+                bool canImportLayer = (getMainModel() != 0 &&
+                                       m_paneStack != 0 &&
+                                       m_paneStack->getCurrentPane() != 0);
+                
+                if (!canImportLayer || (openLayerFile(path) == FileOpenFailed)) {
+                    
+                    QMessageBox::critical(this, tr("Failed to open file"),
+                                          tr("File \"%1\" could not be opened").arg(path));
+                }
             }
         }
     }
 }
 
 MainWindow::FileOpenStatus
-MainWindow::openURL(QUrl url)
+MainWindow::openURL(QUrl url, AudioFileOpenMode mode)
 {
     if (url.scheme().toLower() == "file") {
-        return openSomeFile(url.toLocalFile());
+
+        return openSomeFile(url.toLocalFile(), mode);
+
     } else if (!RemoteFile::canHandleScheme(url)) {
+
         QMessageBox::critical(this, tr("Unsupported scheme in URL"),
                               tr("The URL scheme \"%1\" is not supported")
                               .arg(url.scheme()));
         return FileOpenFailed;
+
     } else {
         RemoteFile rf(url);
         rf.wait();
@@ -3068,7 +3210,8 @@
             return FileOpenFailed;
         }
         FileOpenStatus status;
-        if ((status = openSomeFile(rf.getLocalFilename(), url.toString())) !=
+        if ((status = openSomeFile(rf.getLocalFilename(), url.toString(),
+                                   mode)) !=
             FileOpenSucceeded) {
             rf.deleteLocalFile();
         }
@@ -3077,6 +3220,49 @@
 }
 
 MainWindow::FileOpenStatus
+MainWindow::openURL(QString ustr, AudioFileOpenMode mode)
+{
+    // This function is used when we don't know whether the string is
+    // an encoded or human-readable url
+
+    QUrl url(ustr);
+
+    if (url.scheme().toLower() == "file") {
+
+        return openSomeFile(url.toLocalFile(), mode);
+
+    } else if (!RemoteFile::canHandleScheme(url)) {
+
+        QMessageBox::critical(this, tr("Unsupported scheme in URL"),
+                              tr("The URL scheme \"%1\" is not supported")
+                              .arg(url.scheme()));
+        return FileOpenFailed;
+
+    } else {
+        RemoteFile rf(url);
+        rf.wait();
+        if (!rf.isOK()) {
+            // rf was created on the assumption that ustr was
+            // human-readable.  Let's try again, this time assuming it
+            // was already encoded.
+            std::cerr << "MainWindow::openURL: Failed to retrieve URL \""
+                      << ustr.toStdString() << "\" as human-readable URL; "
+                      << "trying again treating it as encoded URL"
+                      << std::endl;
+            url.setEncodedUrl(ustr.toAscii());
+            return openURL(url, mode);
+        }
+
+        FileOpenStatus status;
+        if ((status = openSomeFile(rf.getLocalFilename(), ustr, mode)) !=
+            FileOpenSucceeded) {
+            rf.deleteLocalFile();
+        }
+        return status;
+    }
+}
+
+MainWindow::FileOpenStatus
 MainWindow::openSomeFile(QString path, AudioFileOpenMode mode)
 {
     return openSomeFile(path, path, mode);
@@ -3092,7 +3278,9 @@
                            m_paneStack != 0 &&
                            m_paneStack->getCurrentPane() != 0);
 
-    if ((status = openAudioFile(path, location, mode)) != FileOpenFailed) {
+    if ((status = openPlaylistFile(path, location, mode)) != FileOpenFailed) {
+        return status;
+    } else if ((status = openAudioFile(path, location, mode)) != FileOpenFailed) {
         return status;
     } else if ((status = openSessionFile(path, location)) != FileOpenFailed) {
 	return status;
@@ -3546,23 +3734,6 @@
 }
 
 void
-MainWindow::toggleDarkBackground()
-{
-    if (!m_viewManager) return;
-
-    m_viewManager->setGlobalDarkBackground
-        (!m_viewManager->getGlobalDarkBackground());
-
-    if (m_viewManager->getGlobalDarkBackground()) {
-        m_panLayer->setBaseColour
-            (ColourDatabase::getInstance()->getColourIndex(tr("Bright Green")));
-    } else {
-        m_panLayer->setBaseColour
-            (ColourDatabase::getInstance()->getColourIndex(tr("Green")));
-    }        
-}
-
-void
 MainWindow::preferenceChanged(PropertyContainer::PropertyName name)
 {
     if (name == "Property Box Layout") {
@@ -3574,7 +3745,24 @@
                 m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout);
             }
         }
-    }
+    } else if (name == "Background Mode" && m_viewManager) {
+        Preferences::BackgroundMode mode =
+            Preferences::getInstance()->getBackgroundMode();
+        if (mode == Preferences::BackgroundFromTheme) {
+            m_viewManager->setGlobalDarkBackground(m_initialDarkBackground);
+        } else if (mode == Preferences::DarkBackground) {
+            m_viewManager->setGlobalDarkBackground(true);
+        } else {
+            m_viewManager->setGlobalDarkBackground(false);
+        }
+        if (m_viewManager->getGlobalDarkBackground()) {
+            m_panLayer->setBaseColour
+                (ColourDatabase::getInstance()->getColourIndex(tr("Bright Green")));
+        } else {
+            m_panLayer->setBaseColour
+                (ColourDatabase::getInstance()->getColourIndex(tr("Green")));
+        }      
+    }            
 }
 
 void
@@ -4413,6 +4601,9 @@
 MainWindow::modelAboutToBeDeleted(Model *model)
 {
 //    std::cerr << "MainWindow::modelAboutToBeDeleted(" << model << ")" << std::endl;
+    if (model == m_viewManager->getPlaybackModel()) {
+        m_viewManager->setPlaybackModel(0);
+    }
     m_playSource->removeModel(model);
     FFTDataServer::modelAboutToBeDeleted(model);
 }
@@ -4650,6 +4841,19 @@
             }
         }
 
+    } else if (message.getMethod() == "solo") {
+
+        if (message.getArgCount() == 1 &&
+            message.getArg(0).canConvert(QVariant::String)) {
+
+            QString str = message.getArg(0).toString();
+            if (str == "on") {
+                m_viewManager->setPlaySoloMode(true);
+            } else if (str == "off") {
+                m_viewManager->setPlaySoloMode(false);
+            }
+        }
+
     } else if (message.getMethod() == "select" ||
                message.getMethod() == "addselect") {
 
--- a/main/MainWindow.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/main/MainWindow.h	Fri Sep 28 13:56:38 2007 +0000
@@ -79,9 +79,11 @@
 
     FileOpenStatus openSomeFile(QString path, AudioFileOpenMode = AskUser);
     FileOpenStatus openAudioFile(QString path, AudioFileOpenMode = AskUser);
+    FileOpenStatus openPlaylistFile(QString path, AudioFileOpenMode = AskUser);
     FileOpenStatus openLayerFile(QString path);
     FileOpenStatus openSessionFile(QString path);
-    FileOpenStatus openURL(QUrl url);
+    FileOpenStatus openURL(QUrl url, AudioFileOpenMode = AskUser);
+    FileOpenStatus openURL(QString url, AudioFileOpenMode = AskUser);
 
     bool saveSessionFile(QString path);
     bool commitData(bool mayAskUser); // on session shutdown
@@ -155,7 +157,6 @@
     void toggleZoomWheels();
     void togglePropertyBoxes();
     void toggleStatusBar();
-    void toggleDarkBackground();
 
     void play();
     void ffwd();
@@ -172,6 +173,7 @@
 
     void playLoopToggled();
     void playSelectionToggled();
+    void playSoloToggled();
     void playSpeedChanged(int);
     void playSharpenToggled();
     void playMonoToggled();
@@ -302,6 +304,8 @@
     QPointer<PreferencesDialog> m_preferencesDialog;
     QPointer<QTreeView>      m_layerTreeView;
 
+    bool                     m_initialDarkBackground;
+
     KeyReference            *m_keyReference;
 
     WaveFileModel *getMainModel();
@@ -412,6 +416,8 @@
                                 AudioFileOpenMode = AskUser);
     FileOpenStatus openAudioFile(QString path, QString location,
                                  AudioFileOpenMode = AskUser);
+    FileOpenStatus openPlaylistFile(QString path, QString location,
+                                    AudioFileOpenMode = AskUser);
     FileOpenStatus openLayerFile(QString path, QString location);
     FileOpenStatus openSessionFile(QString path, QString location);
 
--- a/main/PreferencesDialog.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/main/PreferencesDialog.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -26,12 +26,18 @@
 #include <QString>
 #include <QDialogButtonBox>
 #include <QMessageBox>
+#include <QTabWidget>
+#include <QLineEdit>
+#include <QFileDialog>
+#include <QMessageBox>
 
 #include "widgets/WindowTypeSelector.h"
+#include "widgets/IconLoader.h"
 #include "base/Preferences.h"
 
 PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WFlags flags) :
-    QDialog(parent, flags)
+    QDialog(parent, flags),
+    m_changesOnRestart(false)
 {
     setWindowTitle(tr("Sonic Visualiser: Application Preferences"));
 
@@ -39,18 +45,20 @@
 
     QGridLayout *grid = new QGridLayout;
     setLayout(grid);
+
+    QTabWidget *tab = new QTabWidget;
+    grid->addWidget(tab, 0, 0);
     
-    QGroupBox *groupBox = new QGroupBox;
-    groupBox->setTitle(tr("Application Preferences"));
-    grid->addWidget(groupBox, 0, 0);
-    
-    QGridLayout *subgrid = new QGridLayout;
-    groupBox->setLayout(subgrid);
+    tab->setTabPosition(QTabWidget::North);
 
     // Create this first, as slots that get called from the ctor will
     // refer to it
     m_applyButton = new QPushButton(tr("Apply"));
 
+    // Create all the preference widgets first, then create the
+    // individual tab widgets and place the preferences in their
+    // appropriate places in one go afterwards
+
     int min, max, deflt, i;
 
     m_windowType = WindowType(prefs->getPropertyRangeAndValue
@@ -117,6 +125,44 @@
     connect(resampleQuality, SIGNAL(currentIndexChanged(int)),
             this, SLOT(resampleQualityChanged(int)));
 
+    QCheckBox *resampleOnLoad = new QCheckBox;
+    m_resampleOnLoad = prefs->getResampleOnLoad();
+    resampleOnLoad->setCheckState(m_resampleOnLoad ? Qt::Checked :
+                                  Qt::Unchecked);
+    connect(resampleOnLoad, SIGNAL(stateChanged(int)),
+            this, SLOT(resampleOnLoadChanged(int)));
+
+    m_tempDirRootEdit = new QLineEdit;
+    QString dir = prefs->getTemporaryDirectoryRoot();
+    m_tempDirRoot = dir;
+    dir.replace("$HOME", tr("<home directory>"));
+    m_tempDirRootEdit->setText(dir);
+    m_tempDirRootEdit->setReadOnly(true);
+    QPushButton *tempDirButton = new QPushButton;
+    tempDirButton->setIcon(IconLoader().load("fileopen"));
+    connect(tempDirButton, SIGNAL(clicked()),
+            this, SLOT(tempDirButtonClicked()));
+    tempDirButton->setFixedSize(QSize(24, 24));
+
+    QComboBox *bgMode = new QComboBox;
+    int bg = prefs->getPropertyRangeAndValue("Background Mode", &min, &max,
+                                             &deflt);
+    m_backgroundMode = bg;
+    for (i = min; i <= max; ++i) {
+        bgMode->addItem(prefs->getPropertyValueLabel("Background Mode", i));
+    }
+    bgMode->setCurrentIndex(bg);
+
+    connect(bgMode, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(backgroundModeChanged(int)));
+
+    // General tab
+
+    QFrame *frame = new QFrame;
+    
+    QGridLayout *subgrid = new QGridLayout;
+    frame->setLayout(subgrid);
+
     int row = 0;
 
     subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
@@ -125,15 +171,43 @@
     subgrid->addWidget(propertyLayout, row++, 1, 1, 2);
 
     subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
-                                                ("Tuning Frequency"))),
+                                                ("Background Mode"))),
                        row, 0);
-    subgrid->addWidget(frequency, row++, 1, 1, 2);
+    subgrid->addWidget(bgMode, row++, 1, 1, 2);
+
+    subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
+                                                ("Resample On Load"))),
+                       row, 0);
+    subgrid->addWidget(resampleOnLoad, row++, 1, 1, 1);
 
     subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
                                                 ("Resample Quality"))),
                        row, 0);
     subgrid->addWidget(resampleQuality, row++, 1, 1, 2);
 
+    subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
+                                                ("Temporary Directory Root"))),
+                       row, 0);
+    subgrid->addWidget(m_tempDirRootEdit, row, 1, 1, 1);
+    subgrid->addWidget(tempDirButton, row, 2, 1, 1);
+    row++;
+
+    subgrid->setRowStretch(row, 10);
+    
+    tab->addTab(frame, tr("&General"));
+
+    // Analysis tab
+
+    frame = new QFrame;
+    subgrid = new QGridLayout;
+    frame->setLayout(subgrid);
+    row = 0;
+
+    subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
+                                                ("Tuning Frequency"))),
+                       row, 0);
+    subgrid->addWidget(frequency, row++, 1, 1, 2);
+
     subgrid->addWidget(new QLabel(prefs->getPropertyLabel
                                   ("Spectrogram Smoothing")),
                        row, 0);
@@ -146,6 +220,10 @@
     subgrid->setRowStretch(row, 10);
     row++;
     
+    subgrid->setRowStretch(row, 10);
+    
+    tab->addTab(frame, tr("&Analysis"));
+
     QDialogButtonBox *bb = new QDialogButtonBox(Qt::Horizontal);
     grid->addWidget(bb, 1, 0);
     
@@ -202,6 +280,41 @@
 }
 
 void
+PreferencesDialog::resampleOnLoadChanged(int state)
+{
+    m_resampleOnLoad = (state == Qt::Checked);
+    m_applyButton->setEnabled(true);
+    m_changesOnRestart = true;
+}
+
+void
+PreferencesDialog::tempDirRootChanged(QString r)
+{
+    m_tempDirRoot = r;
+    m_applyButton->setEnabled(true);
+}
+
+void
+PreferencesDialog::tempDirButtonClicked()
+{
+    QString dir = QFileDialog::getExistingDirectory
+        (this, tr("Select a directory to create cache subdirectory in"),
+         m_tempDirRoot);
+    if (dir == "") return;
+    m_tempDirRootEdit->setText(dir);
+    tempDirRootChanged(dir);
+    m_changesOnRestart = true;
+}
+
+void
+PreferencesDialog::backgroundModeChanged(int mode)
+{
+    m_backgroundMode = mode;
+    m_applyButton->setEnabled(true);
+    m_changesOnRestart = true;
+}
+
+void
 PreferencesDialog::okClicked()
 {
     applyClicked();
@@ -219,7 +332,17 @@
                                 (m_propertyLayout));
     prefs->setTuningFrequency(m_tuningFrequency);
     prefs->setResampleQuality(m_resampleQuality);
+    prefs->setResampleOnLoad(m_resampleOnLoad);
+    prefs->setTemporaryDirectoryRoot(m_tempDirRoot);
+    prefs->setBackgroundMode(Preferences::BackgroundMode(m_backgroundMode));
+
     m_applyButton->setEnabled(false);
+
+    if (m_changesOnRestart) {
+        QMessageBox::information(this, tr("Preferences"),
+                                 tr("One or more of the application preferences you have changed may not take full effect until Sonic Visualiser is restarted.\nPlease exit and restart the application now if you want these changes to take effect immediately."));
+        m_changesOnRestart = false;
+    }
 }    
 
 void
--- a/main/PreferencesDialog.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/main/PreferencesDialog.h	Fri Sep 28 13:56:38 2007 +0000
@@ -22,6 +22,7 @@
 
 class WindowTypeSelector;
 class QPushButton;
+class QLineEdit;
 
 class PreferencesDialog : public QDialog
 {
@@ -40,6 +41,11 @@
     void propertyLayoutChanged(int layout);
     void tuningFrequencyChanged(double freq);
     void resampleQualityChanged(int quality);
+    void resampleOnLoadChanged(int state);
+    void tempDirRootChanged(QString root);
+    void backgroundModeChanged(int mode);
+
+    void tempDirButtonClicked();
 
     void okClicked();
     void applyClicked();
@@ -48,12 +54,19 @@
 protected:
     WindowTypeSelector *m_windowTypeSelector;
     QPushButton *m_applyButton;
+
+    QLineEdit *m_tempDirRootEdit;
     
     WindowType m_windowType;
-    int   m_spectrogramSmoothing;
-    int   m_propertyLayout;
+    int m_spectrogramSmoothing;
+    int m_propertyLayout;
     float m_tuningFrequency;
-    int   m_resampleQuality;
+    int m_resampleQuality;
+    bool m_resampleOnLoad;
+    QString m_tempDirRoot;
+    int m_backgroundMode;
+
+    bool m_changesOnRestart;
 };
 
 #endif
--- a/main/main.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/main/main.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -300,7 +300,7 @@
 
         if (i->startsWith("http:") || i->startsWith("ftp:")) {
             std::cerr << "opening URL: \"" << i->toStdString() << "\"..." << std::endl;
-            status = gui.openURL(QUrl(*i));
+            status = gui.openURL(*i);
             continue;
         }
 
@@ -340,7 +340,10 @@
                 (&gui, QMessageBox::tr("Failed to open file"),
                  QMessageBox::tr("File \"%1\" could not be opened").arg(path));
         }
-    }            
+    }
+    
+
+
 /*
     TipDialog tipDialog;
     if (tipDialog.isOK()) {
--- a/sonic-visualiser.qrc	Fri Sep 21 09:13:11 2007 +0000
+++ b/sonic-visualiser.qrc	Fri Sep 28 13:56:38 2007 +0000
@@ -16,6 +16,7 @@
     <file>icons/rewind-start.png</file>
     <file>icons/playselection.png</file>
     <file>icons/playloop.png</file>
+    <file>icons/solo.png</file>
     <file>icons/fader_background.png</file>
     <file>icons/fader_knob.png</file>
     <file>icons/fader_knob_red.png</file>