Chris@0: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@0: Chris@0: /* Chris@0: Sonic Visualiser Chris@0: An audio file viewer and annotation editor. Chris@0: Centre for Digital Music, Queen Mary, University of London. Chris@77: This file copyright 2006 Chris Cannam and QMUL. Chris@0: Chris@0: This program is free software; you can redistribute it and/or Chris@0: modify it under the terms of the GNU General Public License as Chris@0: published by the Free Software Foundation; either version 2 of the Chris@0: License, or (at your option) any later version. See the file Chris@0: COPYING included with this distribution for more information. Chris@0: */ Chris@0: Chris@0: #include "MainWindow.h" Chris@953: #include "SVSplash.h" Chris@0: Chris@1: #include "system/System.h" Chris@1: #include "system/Init.h" Chris@0: #include "base/TempDirectory.h" Chris@0: #include "base/PropertyContainer.h" Chris@0: #include "base/Preferences.h" Chris@908: #include "data/fileio/FileSource.h" Chris@120: #include "widgets/TipDialog.h" Chris@763: #include "widgets/InteractiveFileFinder.h" Chris@763: #include "svapp/framework/TransformUserConfigurator.h" Chris@315: #include "transform/TransformFactory.h" Chris@1144: #include "svcore/plugin/PluginScan.h" Chris@0: Chris@0: #include Chris@0: #include Chris@0: #include Chris@0: #include Chris@0: #include Chris@0: #include Chris@5: #include Chris@7: #include Chris@11: #include Chris@165: #include Chris@252: #include Chris@331: #include dan@365: #include Chris@331: Chris@0: #include Chris@0: #include Chris@0: Chris@215: #ifdef HAVE_FFTW3F Chris@215: #include Chris@215: #endif Chris@215: Chris@127: /*! \mainpage Sonic Visualiser Chris@127: Chris@127: \section interesting Summary of interesting classes Chris@127: Chris@127: - Data models: Model and subclasses, e.g. WaveFileModel Chris@127: Chris@127: - Graphical layers: Layer and subclasses, displayed on View and its Chris@127: subclass widgets. Chris@127: Chris@127: - Main window class, document class, and file parser: MainWindow, Chris@127: Document, SVFileReader Chris@127: Chris@127: - Turning one model (e.g. audio) into another (e.g. more audio, or a Chris@244: curve extracted from it): Transform, encapsulating the data that need Chris@244: to be stored to be able to reproduce a given transformation; Chris@244: TransformFactory, for discovering the available types of transform; Chris@244: ModelTransformerFactory, ModelTransformer and subclasses, providing Chris@244: the mechanisms for applying transforms to data models Chris@127: Chris@127: - Creating the plugins used by transforms: RealTimePluginFactory, Chris@129: FeatureExtractionPluginFactory. See also the API documentation for Chris@129: Vamp feature extraction plugins at Chris@129: http://www.vamp-plugins.org/code-doc/. Chris@127: Chris@127: - File reading and writing code: AudioFileReader and subclasses, Chris@127: WavFileWriter, DataFileReader, SVFileReader Chris@127: Chris@127: - FFT calculation and cacheing: FFTModel, FFTDataServer Chris@127: Chris@127: - Widgets that show groups of editable properties: PropertyBox for Chris@127: layer properties (contained in a PropertyStack), PluginParameterBox Chris@127: for plugins (contained in a PluginParameterDialog) Chris@127: Chris@127: - Audio playback: AudioCallbackPlaySource and subclasses, Chris@127: AudioCallbackPlayTarget and subclasses, AudioGenerator Chris@127: Chris@127: \section model Data sources: the Model hierarchy Chris@127: Chris@127: A Model is something containing, or knowing how to obtain, data. Chris@127: Chris@127: For example, WaveFileModel is a model that knows how to get data Chris@127: from an audio file; SparseTimeValueModel is a model containing Chris@127: editable "curve" data. Chris@127: Chris@127: Models typically subclass one of a number of abstract subclasses of Chris@127: Model. For example, WaveFileModel subclasses DenseTimeValueModel, Chris@127: which describes an interface for models that have a value at each Chris@127: time point for a given sampling resolution. (Note that Chris@127: WaveFileModel does not actually read the files itself: it uses Chris@127: AudioFileReader classes for that. It just makes data from the Chris@127: files available in a Model.) SparseTimeValueModel uses the Chris@127: SparseModel template class, which provides most of the Chris@127: implementation for models that contain a series of points of some Chris@127: sort -- also used by NoteModel, TextModel, and Chris@127: SparseOneDimensionalModel. Chris@127: Chris@127: Everything that goes on the screen originates from a model, via a Chris@127: layer (see below). The models are contained in a Document object. Chris@127: There is no containment hierarchy or ordering of models in the Chris@127: document. One model is the main model, which defines the sample Chris@127: rate for playback. Chris@127: Chris@127: A model may also be marked as a "derived" model, which means it was Chris@127: generated from another model using some transform (feature Chris@127: extraction or effect plugin, etc) -- the idea being that they can Chris@127: be re-generated using the same transform if a new source model is Chris@127: loaded. Chris@127: Chris@127: \section layer Things that can display data: the Layer hierarchy Chris@127: Chris@127: A Layer is something that knows how to draw parts of a model onto a Chris@127: timeline. Chris@127: Chris@127: For example, WaveformLayer is a layer which draws waveforms, based Chris@127: on WaveFileModel; TimeValueLayer draws curves, based on Chris@127: SparseTimeValueModel; SpectrogramLayer draws spectrograms, based on Chris@127: WaveFileModel (via FFTModel). Chris@127: Chris@127: The most basic functions of a layer are: to draw itself onto a Chris@127: Pane, against a timeline on the x axis; and to permit user Chris@127: interaction. If you were thinking of adding the capability to Chris@127: display a new sort of something, then you would want to add a new Chris@127: layer type. (You may also need a new model type, depending on Chris@127: whether any existing model can capture the data you need.) Chris@127: Depending on the sort of data in question, there are various Chris@127: existing layers that might be appropriate to start from -- for Chris@127: example, a layer that displays images that the user has imported Chris@127: and associated with particular times might have something in common Chris@127: with the existing TextLayer which displays pieces of text that are Chris@127: associated with particular times. Chris@127: Chris@127: Although layers are visual objects, they are contained in the Chris@127: Document in Sonic Visualiser rather than being managed together Chris@127: with display widgets. The Sonic Visualiser file format has Chris@127: separate data and layout sections, and the layers are defined in Chris@127: the data section and then referred to in the layout section which Chris@127: determines which layers may go on which panes (see Pane below). Chris@127: Chris@127: Once a layer class is defined, some basic data about it needs to be Chris@127: set up in the LayerFactory class, and then it will appear in the Chris@127: menus and so on on the main window. Chris@127: Chris@127: \section view Widgets that are used to show layers: The View hierarchy Chris@127: Chris@127: A View is a widget that displays a stack of layers. The most Chris@127: important subclass is Pane, the widget that is used to show most of Chris@127: the data in the main window of Sonic Visualiser. Chris@127: Chris@127: All a pane really does is contain a set of layers and get them to Chris@127: render themselves (one on top of the other, with the topmost layer Chris@127: being the one that is currently interacted with), cache the Chris@127: results, negotiate user interaction with them, and so on. This is Chris@127: generally fiddly, if not especially interesting. Panes are Chris@127: strictly layout objects and are not stored in the Document class; Chris@127: instead the MainWindow contains a PaneStack widget (the widget that Chris@127: takes up most of Sonic Visualiser's main window) which contains a Chris@127: set of panes stacked vertically. Chris@127: Chris@127: Another View subclass is Overview, which is the widget that Chris@127: contains that green waveform showing the entire file at the bottom Chris@127: of the window. Chris@127: Chris@127: */ Chris@127: Chris@0: static QMutex cleanupMutex; Chris@589: static bool cleanedUp = false; Chris@0: Chris@0: static void Chris@0: signalHandler(int /* signal */) Chris@0: { Chris@0: // Avoid this happening more than once across threads Chris@0: Chris@665: cerr << "signalHandler: cleaning up and exiting" << endl; Chris@0: cleanupMutex.lock(); Chris@589: if (!cleanedUp) { Chris@589: TempDirectory::getInstance()->cleanup(); Chris@589: cleanedUp = true; Chris@589: } Chris@589: cleanupMutex.unlock(); Chris@589: exit(0); Chris@0: } Chris@0: Chris@11: class SVApplication : public QApplication Chris@11: { Chris@11: public: Chris@296: SVApplication(int &argc, char **argv) : Chris@11: QApplication(argc, argv), dan@365: m_readyForFiles(false), dan@365: m_filepathQueue(QStringList()), dan@365: m_mainWindow(0) Chris@509: { Chris@509: } Chris@11: virtual ~SVApplication() { } Chris@11: Chris@11: void setMainWindow(MainWindow *mw) { m_mainWindow = mw; } Chris@11: void releaseMainWindow() { m_mainWindow = 0; } Chris@11: Chris@11: virtual void commitData(QSessionManager &manager) { Chris@11: if (!m_mainWindow) return; Chris@11: bool mayAskUser = manager.allowsInteraction(); Chris@11: bool success = m_mainWindow->commitData(mayAskUser); Chris@11: manager.release(); Chris@11: if (!success) manager.cancel(); Chris@11: } Chris@11: Chris@953: void handleFilepathArgument(QString path, SVSplash *splash); dan@362: dan@365: bool m_readyForFiles; dan@365: QStringList m_filepathQueue; dan@362: Chris@11: protected: Chris@11: MainWindow *m_mainWindow; dan@365: bool event(QEvent *); Chris@11: }; Chris@11: Chris@0: int Chris@0: main(int argc, char **argv) Chris@0: { Chris@376: svSystemSpecificInitialisation(); Chris@376: Chris@316: #ifdef Q_WS_X11 Chris@317: #if QT_VERSION >= 0x040500 Chris@342: // QApplication::setGraphicsSystem("raster"); Chris@316: #endif Chris@316: #endif Chris@316: Chris@678: #ifdef Q_OS_MAC Chris@678: if (QSysInfo::MacintoshVersion > QSysInfo::MV_10_8) { Chris@678: // Fix for OS/X 10.9 font problem Chris@678: QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande"); Chris@678: } Chris@678: #endif Chris@678: Chris@11: SVApplication application(argc, argv); Chris@0: Chris@46: QStringList args = application.arguments(); Chris@46: Chris@0: signal(SIGINT, signalHandler); Chris@0: signal(SIGTERM, signalHandler); Chris@0: Chris@640: #ifndef Q_OS_WIN32 Chris@0: signal(SIGHUP, signalHandler); Chris@0: signal(SIGQUIT, signalHandler); Chris@0: #endif Chris@0: Chris@46: bool audioOutput = true; Chris@70: bool oscSupport = true; Chris@70: Chris@133: if (args.contains("--help") || args.contains("-h") || args.contains("-?")) { Chris@665: cerr << QApplication::tr( Chris@665: "\nSonic Visualiser is a program for viewing and exploring audio data\nfor semantic music analysis and annotation.\n\nUsage:\n\n %1 [--no-audio] [--no-osc] [ ...]\n\n --no-audio: Do not attempt to open an audio output device\n --no-osc: Do not provide an Open Sound Control port for remote control\n : One or more Sonic Visualiser (.sv) and audio files may be provided.\n").arg(argv[0]) << endl; Chris@70: exit(2); Chris@70: } Chris@70: Chris@46: if (args.contains("--no-audio")) audioOutput = false; Chris@70: if (args.contains("--no-osc")) oscSupport = false; Chris@46: Chris@6: QApplication::setOrganizationName("sonic-visualiser"); Chris@5: QApplication::setOrganizationDomain("sonicvisualiser.org"); Chris@213: QApplication::setApplicationName(QApplication::tr("Sonic Visualiser")); Chris@141: Chris@952: QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); Chris@952: Chris@953: SVSplash *splash = 0; Chris@231: Chris@231: QSettings settings; Chris@237: Chris@237: settings.beginGroup("Preferences"); Chris@237: if (settings.value("show-splash", true).toBool()) { Chris@953: splash = new SVSplash(); Chris@283: splash->show(); Chris@283: QTimer::singleShot(5000, splash, SLOT(hide())); Chris@231: application.processEvents(); Chris@231: } Chris@237: settings.endGroup(); Chris@231: Chris@278: settings.beginGroup("RDF"); Chris@278: if (!settings.contains("rdf-indices")) { Chris@278: QStringList list; Chris@278: list << "http://www.vamp-plugins.org/rdf/plugins/index.txt"; Chris@278: settings.setValue("rdf-indices", list); Chris@278: } Chris@278: settings.endGroup(); Chris@278: Chris@141: QIcon icon; Chris@141: int sizes[] = { 16, 22, 24, 32, 48, 64, 128 }; Chris@730: for (int i = 0; i < int(sizeof(sizes)/sizeof(sizes[0])); ++i) { Chris@141: icon.addFile(QString(":icons/sv-%1x%2.png").arg(sizes[i]).arg(sizes[i])); Chris@141: } Chris@141: QApplication::setWindowIcon(icon); Chris@7: Chris@0: QString language = QLocale::system().name(); Chris@0: Chris@658: settings.beginGroup("Preferences"); Chris@658: language = settings.value("locale", language).toString(); Chris@658: settings.endGroup(); Chris@658: Chris@0: QTranslator qtTranslator; Chris@0: QString qtTrName = QString("qt_%1").arg(language); Chris@438: SVDEBUG << "Loading " << qtTrName << "... "; Chris@165: bool success = false; Chris@165: if (!(success = qtTranslator.load(qtTrName))) { Chris@165: QString qtDir = getenv("QTDIR"); Chris@165: if (qtDir != "") { Chris@165: success = qtTranslator.load Chris@165: (qtTrName, QDir(qtDir).filePath("translations")); Chris@165: } Chris@165: } Chris@165: if (!success) { Chris@438: SVDEBUG << "Failed\nFailed to load Qt translation for locale" << endl; Chris@253: } else { Chris@665: cerr << "Done" << endl; Chris@165: } Chris@0: application.installTranslator(&qtTranslator); Chris@0: Chris@0: QTranslator svTranslator; Chris@0: QString svTrName = QString("sonic-visualiser_%1").arg(language); Chris@438: SVDEBUG << "Loading " << svTrName << "... "; Chris@0: svTranslator.load(svTrName, ":i18n"); Chris@438: SVDEBUG << "Done" << endl; Chris@0: application.installTranslator(&svTranslator); Chris@0: Chris@187: StoreStartupLocale(); Chris@187: Chris@1144: // Make known-plugins query as early as possible after showing Chris@1148: // splash screen. This depends on our helper executable, which Chris@1148: // must exist either in the same directory as this one or Chris@1148: // (preferably) a subdirectory called "checker". Chris@1148: QString myDir = application.applicationDirPath(); Chris@1148: QString helperPath = myDir + "/checker/plugin-checker-helper"; Chris@1148: QString helperSuffix = ""; Chris@1148: #ifdef _WIN32 Chris@1148: helperSuffix = ".exe"; Chris@1148: #endif Chris@1148: if (!QFile(helperPath + helperSuffix).exists()) { Chris@1152: cerr << "NOTE: helper not found at " << (helperPath + helperSuffix) Chris@1152: << ", trying in my own directory" << endl; Chris@1148: helperPath = myDir + "/plugin-checker-helper"; Chris@1148: } Chris@1148: helperPath += helperSuffix; Chris@1152: if (!QFile(helperPath + helperSuffix).exists()) { Chris@1152: cerr << "NOTE: helper not found at " << (helperPath + helperSuffix) Chris@1152: << endl; Chris@1152: } Chris@1148: PluginScan::getInstance()->scan(helperPath); Chris@1144: Chris@0: // Permit size_t and PropertyName to be used as args in queued signal calls Chris@0: qRegisterMetaType("PropertyContainer::PropertyName"); Chris@0: Chris@1045: MainWindow::SoundOptions options = MainWindow::WithEverything; Chris@1045: if (!audioOutput) options = 0; Chris@1045: Chris@1045: MainWindow *gui = new MainWindow(options, oscSupport); Chris@222: application.setMainWindow(gui); Chris@763: InteractiveFileFinder::setParentWidget(gui); Chris@763: TransformUserConfigurator::setParentWidget(gui); Chris@283: if (splash) { Chris@283: QObject::connect(gui, SIGNAL(hideSplash()), splash, SLOT(hide())); Chris@953: QObject::connect(gui, SIGNAL(hideSplash(QWidget *)), Chris@953: splash, SLOT(finishSplash(QWidget *))); Chris@283: } Chris@0: Chris@0: QDesktopWidget *desktop = QApplication::desktop(); Chris@0: QRect available = desktop->availableGeometry(); Chris@0: Chris@378: int width = (available.width() * 2) / 3; Chris@0: int height = available.height() / 2; Chris@378: if (height < 450) height = (available.height() * 2) / 3; Chris@0: if (width > height * 2) width = height * 2; Chris@0: Chris@237: settings.beginGroup("MainWindow"); Chris@624: Chris@5: QSize size = settings.value("size", QSize(width, height)).toSize(); Chris@319: gui->resizeConstrained(size); Chris@624: Chris@5: if (settings.contains("position")) { Chris@297: QRect prevrect(settings.value("position").toPoint(), size); Chris@297: if (!(available & prevrect).isEmpty()) { Chris@297: gui->move(prevrect.topLeft()); Chris@297: } Chris@5: } Chris@624: Chris@624: if (settings.value("maximised", false).toBool()) { Chris@624: gui->setWindowState(Qt::WindowMaximized); Chris@624: } Chris@624: Chris@5: settings.endGroup(); Chris@5: Chris@222: gui->show(); Chris@64: Chris@118: // The MainWindow class seems to have trouble dealing with this if Chris@118: // it tries to adapt to this preference before the constructor is Chris@118: // complete. As a lazy hack, apply it explicitly from here Chris@222: gui->preferenceChanged("Property Box Layout"); Chris@118: dan@365: application.m_readyForFiles = true; // Ready to receive files from e.g. Apple Events dan@365: Chris@54: for (QStringList::iterator i = args.begin(); i != args.end(); ++i) { Chris@54: Chris@54: if (i == args.begin()) continue; Chris@54: if (i->startsWith('-')) continue; Chris@54: Chris@54: QString path = *i; Chris@54: dan@365: application.handleFilepathArgument(path, splash); dan@365: } dan@365: dan@365: for (QStringList::iterator i = application.m_filepathQueue.begin(); i != application.m_filepathQueue.end(); ++i) { dan@365: QString path = *i; dan@365: application.handleFilepathArgument(path, splash); Chris@180: } Chris@180: Chris@215: #ifdef HAVE_FFTW3F Chris@215: settings.beginGroup("FFTWisdom"); Chris@215: QString wisdom = settings.value("wisdom").toString(); Chris@215: if (wisdom != "") { Chris@215: fftwf_import_wisdom_from_string(wisdom.toLocal8Bit().data()); Chris@215: } Chris@267: #ifdef HAVE_FFTW3 Chris@267: wisdom = settings.value("wisdom_d").toString(); Chris@267: if (wisdom != "") { Chris@267: fftw_import_wisdom_from_string(wisdom.toLocal8Bit().data()); Chris@267: } Chris@267: #endif Chris@215: settings.endGroup(); Chris@215: #endif Chris@180: Chris@0: int rv = application.exec(); Chris@0: Chris@298: gui->hide(); Chris@298: Chris@0: cleanupMutex.lock(); Chris@332: Chris@589: if (!cleanedUp) { Chris@589: TransformFactory::deleteInstance(); Chris@589: TempDirectory::getInstance()->cleanup(); Chris@589: cleanedUp = true; Chris@589: } Chris@589: Chris@11: application.releaseMainWindow(); Chris@5: Chris@215: #ifdef HAVE_FFTW3F Chris@267: settings.beginGroup("FFTWisdom"); Chris@215: char *cwisdom = fftwf_export_wisdom_to_string(); Chris@215: if (cwisdom) { Chris@215: settings.setValue("wisdom", cwisdom); Chris@332: free(cwisdom); Chris@215: } Chris@267: #ifdef HAVE_FFTW3 Chris@267: cwisdom = fftw_export_wisdom_to_string(); Chris@267: if (cwisdom) { Chris@267: settings.setValue("wisdom_d", cwisdom); Chris@332: free(cwisdom); Chris@267: } Chris@267: #endif Chris@267: settings.endGroup(); Chris@215: #endif Chris@215: Chris@908: FileSource::debugReport(); Chris@908: Chris@222: delete gui; Chris@222: Chris@573: cleanupMutex.unlock(); Chris@573: Chris@0: return rv; Chris@0: } dan@365: dan@365: bool SVApplication::event(QEvent *event){ Chris@730: Chris@730: // Avoid warnings/errors with -Wextra because we aren't explicitly Chris@730: // handling all event types (-Wall is OK with this because of the Chris@730: // default but the stricter level insists) Chris@730: #pragma GCC diagnostic ignored "-Wswitch-enum" Chris@730: dan@365: QString thePath; Chris@730: dan@365: switch (event->type()) { dan@365: case QEvent::FileOpen: dan@365: thePath = static_cast(event)->file(); dan@365: if(m_readyForFiles) dan@365: handleFilepathArgument(thePath, NULL); dan@365: else dan@365: m_filepathQueue.append(thePath); dan@365: return true; dan@365: default: dan@365: return QApplication::event(event); dan@365: } dan@365: } dan@365: dan@365: /** Application-global handler for filepaths passed in, e.g. as command-line arguments or apple events */ Chris@953: void SVApplication::handleFilepathArgument(QString path, SVSplash *splash){ dan@365: static bool haveSession = false; dan@365: static bool haveMainModel = false; dan@365: static bool havePriorCommandLineModel = false; dan@365: dan@365: MainWindow::FileOpenStatus status = MainWindow::FileOpenFailed; dan@365: Chris@603: #ifdef Q_OS_WIN32 Chris@603: path.replace("\\", "/"); Chris@603: #endif Chris@603: dan@365: if (path.endsWith("sv")) { dan@365: if (!haveSession) { Chris@738: status = m_mainWindow->openSessionPath(path); dan@365: if (status == MainWindow::FileOpenSucceeded) { dan@365: haveSession = true; dan@365: haveMainModel = true; dan@365: } dan@365: } else { Chris@665: cerr << "WARNING: Ignoring additional session file argument \"" << path << "\"" << endl; dan@365: status = MainWindow::FileOpenSucceeded; dan@365: } dan@365: } dan@365: if (status != MainWindow::FileOpenSucceeded) { dan@365: if (!haveMainModel) { Chris@844: status = m_mainWindow->openPath(path, MainWindow::ReplaceSession); dan@365: if (status == MainWindow::FileOpenSucceeded) { dan@365: haveMainModel = true; dan@365: } dan@365: } else { dan@365: if (haveSession && !havePriorCommandLineModel) { Chris@844: status = m_mainWindow->openPath(path, MainWindow::AskUser); dan@365: if (status == MainWindow::FileOpenSucceeded) { dan@365: havePriorCommandLineModel = true; dan@365: } dan@365: } else { Chris@844: status = m_mainWindow->openPath(path, MainWindow::CreateAdditionalModel); dan@365: } dan@365: } dan@365: } dan@365: if (status == MainWindow::FileOpenFailed) { dan@365: if (splash) splash->hide(); dan@365: QMessageBox::critical dan@365: (m_mainWindow, QMessageBox::tr("Failed to open file"), dan@365: QMessageBox::tr("File or URL \"%1\" could not be opened").arg(path)); dan@365: } else if (status == MainWindow::FileOpenWrongMode) { dan@365: if (splash) splash->hide(); dan@365: QMessageBox::critical dan@365: (m_mainWindow, QMessageBox::tr("Failed to open file"), dan@365: QMessageBox::tr("Audio required

Please load at least one audio file before importing annotation data")); dan@365: } dan@365: }