Chris@578: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@578: Chris@578: /* Chris@578: Sonic Visualiser Chris@578: An audio file viewer and annotation editor. Chris@578: Centre for Digital Music, Queen Mary, University of London. Chris@578: This file copyright 2006 Chris Cannam and QMUL. Chris@578: Chris@578: This program is free software; you can redistribute it and/or Chris@578: modify it under the terms of the GNU General Public License as Chris@578: published by the Free Software Foundation; either version 2 of the Chris@578: License, or (at your option) any later version. See the file Chris@578: COPYING included with this distribution for more information. Chris@578: */ Chris@578: Chris@578: #include "MainWindow.h" Chris@578: Chris@578: #include "system/System.h" Chris@578: #include "system/Init.h" Chris@578: #include "base/TempDirectory.h" Chris@578: #include "base/PropertyContainer.h" Chris@578: #include "base/Preferences.h" Chris@578: #include "widgets/TipDialog.h" Chris@578: #include "transform/TransformFactory.h" Chris@578: Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: #include Chris@578: Chris@578: #include "../version.h" Chris@578: Chris@578: #include Chris@578: #include Chris@578: Chris@578: #ifdef HAVE_FFTW3F Chris@578: #include Chris@578: #endif Chris@578: Chris@578: /*! \mainpage Sonic Visualiser Chris@578: Chris@578: \section interesting Summary of interesting classes Chris@578: Chris@578: - Data models: Model and subclasses, e.g. WaveFileModel Chris@578: Chris@578: - Graphical layers: Layer and subclasses, displayed on View and its Chris@578: subclass widgets. Chris@578: Chris@578: - Main window class, document class, and file parser: MainWindow, Chris@578: Document, SVFileReader Chris@578: Chris@578: - Turning one model (e.g. audio) into another (e.g. more audio, or a Chris@578: curve extracted from it): Transform, encapsulating the data that need Chris@578: to be stored to be able to reproduce a given transformation; Chris@578: TransformFactory, for discovering the available types of transform; Chris@578: ModelTransformerFactory, ModelTransformer and subclasses, providing Chris@578: the mechanisms for applying transforms to data models Chris@578: Chris@578: - Creating the plugins used by transforms: RealTimePluginFactory, Chris@578: FeatureExtractionPluginFactory. See also the API documentation for Chris@578: Vamp feature extraction plugins at Chris@578: http://www.vamp-plugins.org/code-doc/. Chris@578: Chris@578: - File reading and writing code: AudioFileReader and subclasses, Chris@578: WavFileWriter, DataFileReader, SVFileReader Chris@578: Chris@578: - FFT calculation and cacheing: FFTModel, FFTDataServer Chris@578: Chris@578: - Widgets that show groups of editable properties: PropertyBox for Chris@578: layer properties (contained in a PropertyStack), PluginParameterBox Chris@578: for plugins (contained in a PluginParameterDialog) Chris@578: Chris@578: - Audio playback: AudioCallbackPlaySource and subclasses, Chris@578: AudioCallbackPlayTarget and subclasses, AudioGenerator Chris@578: Chris@578: \section model Data sources: the Model hierarchy Chris@578: Chris@578: A Model is something containing, or knowing how to obtain, data. Chris@578: Chris@578: For example, WaveFileModel is a model that knows how to get data Chris@578: from an audio file; SparseTimeValueModel is a model containing Chris@578: editable "curve" data. Chris@578: Chris@578: Models typically subclass one of a number of abstract subclasses of Chris@578: Model. For example, WaveFileModel subclasses DenseTimeValueModel, Chris@578: which describes an interface for models that have a value at each Chris@578: time point for a given sampling resolution. (Note that Chris@578: WaveFileModel does not actually read the files itself: it uses Chris@578: AudioFileReader classes for that. It just makes data from the Chris@578: files available in a Model.) SparseTimeValueModel uses the Chris@578: SparseModel template class, which provides most of the Chris@578: implementation for models that contain a series of points of some Chris@578: sort -- also used by NoteModel, TextModel, and Chris@578: SparseOneDimensionalModel. Chris@578: Chris@578: Everything that goes on the screen originates from a model, via a Chris@578: layer (see below). The models are contained in a Document object. Chris@578: There is no containment hierarchy or ordering of models in the Chris@578: document. One model is the main model, which defines the sample Chris@578: rate for playback. Chris@578: Chris@578: A model may also be marked as a "derived" model, which means it was Chris@578: generated from another model using some transform (feature Chris@578: extraction or effect plugin, etc) -- the idea being that they can Chris@578: be re-generated using the same transform if a new source model is Chris@578: loaded. Chris@578: Chris@578: \section layer Things that can display data: the Layer hierarchy Chris@578: Chris@578: A Layer is something that knows how to draw parts of a model onto a Chris@578: timeline. Chris@578: Chris@578: For example, WaveformLayer is a layer which draws waveforms, based Chris@578: on WaveFileModel; TimeValueLayer draws curves, based on Chris@578: SparseTimeValueModel; SpectrogramLayer draws spectrograms, based on Chris@578: WaveFileModel (via FFTModel). Chris@578: Chris@578: The most basic functions of a layer are: to draw itself onto a Chris@578: Pane, against a timeline on the x axis; and to permit user Chris@578: interaction. If you were thinking of adding the capability to Chris@578: display a new sort of something, then you would want to add a new Chris@578: layer type. (You may also need a new model type, depending on Chris@578: whether any existing model can capture the data you need.) Chris@578: Depending on the sort of data in question, there are various Chris@578: existing layers that might be appropriate to start from -- for Chris@578: example, a layer that displays images that the user has imported Chris@578: and associated with particular times might have something in common Chris@578: with the existing TextLayer which displays pieces of text that are Chris@578: associated with particular times. Chris@578: Chris@578: Although layers are visual objects, they are contained in the Chris@578: Document in Sonic Visualiser rather than being managed together Chris@578: with display widgets. The Sonic Visualiser file format has Chris@578: separate data and layout sections, and the layers are defined in Chris@578: the data section and then referred to in the layout section which Chris@578: determines which layers may go on which panes (see Pane below). Chris@578: Chris@578: Once a layer class is defined, some basic data about it needs to be Chris@578: set up in the LayerFactory class, and then it will appear in the Chris@578: menus and so on on the main window. Chris@578: Chris@578: \section view Widgets that are used to show layers: The View hierarchy Chris@578: Chris@578: A View is a widget that displays a stack of layers. The most Chris@578: important subclass is Pane, the widget that is used to show most of Chris@578: the data in the main window of Sonic Visualiser. Chris@578: Chris@578: All a pane really does is contain a set of layers and get them to Chris@578: render themselves (one on top of the other, with the topmost layer Chris@578: being the one that is currently interacted with), cache the Chris@578: results, negotiate user interaction with them, and so on. This is Chris@578: generally fiddly, if not especially interesting. Panes are Chris@578: strictly layout objects and are not stored in the Document class; Chris@578: instead the MainWindow contains a PaneStack widget (the widget that Chris@578: takes up most of Sonic Visualiser's main window) which contains a Chris@578: set of panes stacked vertically. Chris@578: Chris@578: Another View subclass is Overview, which is the widget that Chris@578: contains that green waveform showing the entire file at the bottom Chris@578: of the window. Chris@578: Chris@578: */ Chris@578: Chris@578: static QMutex cleanupMutex; Chris@578: Chris@578: static void Chris@578: signalHandler(int /* signal */) Chris@578: { Chris@578: // Avoid this happening more than once across threads Chris@578: Chris@578: cleanupMutex.lock(); Chris@578: std::cerr << "signalHandler: cleaning up and exiting" << std::endl; Chris@578: TempDirectory::getInstance()->cleanup(); Chris@578: exit(0); // without releasing mutex Chris@578: } Chris@578: Chris@578: class SVApplication : public QApplication Chris@578: { Chris@578: public: Chris@578: SVApplication(int &argc, char **argv) : Chris@578: QApplication(argc, argv), Chris@578: m_readyForFiles(false), Chris@578: m_filepathQueue(QStringList()), Chris@578: m_mainWindow(0) Chris@578: { Chris@578: #ifdef Q_OS_MAC Chris@578: // Override the Qt plugin load path. The default contains the Chris@578: // Qt installation location as well as the application Chris@578: // directory, but we don't ever want to load plugins from Chris@578: // outside the app bundle because we don't know for sure what Chris@578: // (potentially different) versions of the Qt framework Chris@578: // libraries they may have dyld dependencies on. Chris@578: QString apploc(applicationFilePath()); Chris@578: apploc.truncate(apploc.lastIndexOf(QLatin1Char('/'))); Chris@578: apploc = QDir(apploc).canonicalPath(); Chris@578: if (QFile::exists(apploc)) { Chris@578: setLibraryPaths(QStringList() << apploc); Chris@578: } else { Chris@578: setLibraryPaths(QStringList()); Chris@578: } Chris@578: #endif Chris@578: } Chris@578: virtual ~SVApplication() { } Chris@578: Chris@578: void setMainWindow(MainWindow *mw) { m_mainWindow = mw; } Chris@578: void releaseMainWindow() { m_mainWindow = 0; } Chris@578: Chris@578: virtual void commitData(QSessionManager &manager) { Chris@578: if (!m_mainWindow) return; Chris@578: bool mayAskUser = manager.allowsInteraction(); Chris@578: bool success = m_mainWindow->commitData(mayAskUser); Chris@578: manager.release(); Chris@578: if (!success) manager.cancel(); Chris@578: } Chris@578: Chris@578: void handleFilepathArgument(QString path, QSplashScreen *splash); Chris@578: Chris@578: bool m_readyForFiles; Chris@578: QStringList m_filepathQueue; Chris@578: Chris@578: protected: Chris@578: MainWindow *m_mainWindow; Chris@578: bool event(QEvent *); Chris@578: Chris@578: }; Chris@578: Chris@578: int Chris@578: main(int argc, char **argv) Chris@578: { Chris@578: svSystemSpecificInitialisation(); Chris@578: Chris@578: #ifdef Q_WS_X11 Chris@578: #if QT_VERSION >= 0x040500 Chris@578: // QApplication::setGraphicsSystem("raster"); Chris@578: #endif Chris@578: #endif Chris@578: Chris@578: SVApplication application(argc, argv); Chris@578: Chris@578: QStringList args = application.arguments(); Chris@578: Chris@578: signal(SIGINT, signalHandler); Chris@578: signal(SIGTERM, signalHandler); Chris@578: Chris@578: #ifndef Q_WS_WIN32 Chris@578: //??? signal(SIGHUP, signalHandler); Chris@578: //??? signal(SIGQUIT, signalHandler); Chris@578: #endif Chris@578: Chris@578: bool audioOutput = true; Chris@578: bool oscSupport = true; Chris@578: Chris@578: if (args.contains("--help") || args.contains("-h") || args.contains("-?")) { Chris@578: std::cerr << QApplication::tr( Chris@578: "\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]) << std::endl; Chris@578: exit(2); Chris@578: } Chris@578: Chris@578: if (args.contains("--no-audio")) audioOutput = false; Chris@578: if (args.contains("--no-osc")) oscSupport = false; Chris@578: Chris@578: QApplication::setOrganizationName("sonic-visualiser"); Chris@578: QApplication::setOrganizationDomain("sonicvisualiser.org"); Chris@578: QApplication::setApplicationName(QApplication::tr("Sonic Visualiser")); Chris@578: Chris@578: QSplashScreen *splash = 0; Chris@578: Chris@578: QSettings settings; Chris@578: Chris@578: settings.beginGroup("Preferences"); Chris@578: if (settings.value("show-splash", true).toBool()) { Chris@578: QPixmap pixmap(":/icons/sv-splash.png"); Chris@578: QPainter painter; Chris@578: painter.begin(&pixmap); Chris@578: QString text = QString("v%1").arg(SV_VERSION); Chris@578: painter.drawText Chris@578: (pixmap.width() - painter.fontMetrics().width(text) - 10, Chris@578: 10 + painter.fontMetrics().ascent(), Chris@578: text); Chris@578: painter.end(); Chris@578: splash = new QSplashScreen(pixmap); Chris@578: splash->show(); Chris@578: QTimer::singleShot(5000, splash, SLOT(hide())); Chris@578: application.processEvents(); Chris@578: } Chris@578: settings.endGroup(); Chris@578: Chris@578: settings.beginGroup("RDF"); Chris@578: if (!settings.contains("rdf-indices")) { Chris@578: QStringList list; Chris@578: list << "http://www.vamp-plugins.org/rdf/plugins/index.txt"; Chris@578: settings.setValue("rdf-indices", list); Chris@578: } Chris@578: settings.endGroup(); Chris@578: Chris@578: QIcon icon; Chris@578: int sizes[] = { 16, 22, 24, 32, 48, 64, 128 }; Chris@578: for (int i = 0; i < sizeof(sizes)/sizeof(sizes[0]); ++i) { Chris@578: icon.addFile(QString(":icons/sv-%1x%2.png").arg(sizes[i]).arg(sizes[i])); Chris@578: } Chris@578: QApplication::setWindowIcon(icon); Chris@578: Chris@578: QString language = QLocale::system().name(); Chris@578: Chris@578: QTranslator qtTranslator; Chris@578: QString qtTrName = QString("qt_%1").arg(language); Chris@578: SVDEBUG << "Loading " << qtTrName << "... "; Chris@578: bool success = false; Chris@578: if (!(success = qtTranslator.load(qtTrName))) { Chris@578: QString qtDir = getenv("QTDIR"); Chris@578: if (qtDir != "") { Chris@578: success = qtTranslator.load Chris@578: (qtTrName, QDir(qtDir).filePath("translations")); Chris@578: } Chris@578: } Chris@578: if (!success) { Chris@578: SVDEBUG << "Failed\nFailed to load Qt translation for locale" << endl; Chris@578: } else { Chris@578: std::cerr << "Done" << std::endl; Chris@578: } Chris@578: application.installTranslator(&qtTranslator); Chris@578: Chris@578: QTranslator svTranslator; Chris@578: QString svTrName = QString("sonic-visualiser_%1").arg(language); Chris@578: SVDEBUG << "Loading " << svTrName << "... "; Chris@578: svTranslator.load(svTrName, ":i18n"); Chris@578: SVDEBUG << "Done" << endl; Chris@578: application.installTranslator(&svTranslator); Chris@578: Chris@578: StoreStartupLocale(); Chris@578: Chris@578: // Permit size_t and PropertyName to be used as args in queued signal calls Chris@578: qRegisterMetaType("size_t"); Chris@578: qRegisterMetaType("PropertyContainer::PropertyName"); Chris@578: Chris@578: MainWindow *gui = new MainWindow(audioOutput, oscSupport); Chris@578: application.setMainWindow(gui); Chris@578: if (splash) { Chris@578: QObject::connect(gui, SIGNAL(hideSplash()), splash, SLOT(hide())); Chris@578: } Chris@578: Chris@578: QDesktopWidget *desktop = QApplication::desktop(); Chris@578: QRect available = desktop->availableGeometry(); Chris@578: Chris@578: int width = (available.width() * 2) / 3; Chris@578: int height = available.height() / 2; Chris@578: if (height < 450) height = (available.height() * 2) / 3; Chris@578: if (width > height * 2) width = height * 2; Chris@578: Chris@578: settings.beginGroup("MainWindow"); Chris@578: QSize size = settings.value("size", QSize(width, height)).toSize(); Chris@578: gui->resizeConstrained(size); Chris@578: if (settings.contains("position")) { Chris@578: QRect prevrect(settings.value("position").toPoint(), size); Chris@578: if (!(available & prevrect).isEmpty()) { Chris@578: gui->move(prevrect.topLeft()); Chris@578: } Chris@578: } Chris@578: settings.endGroup(); Chris@578: Chris@578: gui->show(); Chris@578: Chris@578: // The MainWindow class seems to have trouble dealing with this if Chris@578: // it tries to adapt to this preference before the constructor is Chris@578: // complete. As a lazy hack, apply it explicitly from here Chris@578: gui->preferenceChanged("Property Box Layout"); Chris@578: Chris@578: application.m_readyForFiles = true; // Ready to receive files from e.g. Apple Events Chris@578: Chris@578: for (QStringList::iterator i = args.begin(); i != args.end(); ++i) { Chris@578: Chris@578: if (i == args.begin()) continue; Chris@578: if (i->startsWith('-')) continue; Chris@578: Chris@578: QString path = *i; Chris@578: Chris@578: application.handleFilepathArgument(path, splash); Chris@578: } Chris@578: Chris@578: for (QStringList::iterator i = application.m_filepathQueue.begin(); i != application.m_filepathQueue.end(); ++i) { Chris@578: QString path = *i; Chris@578: application.handleFilepathArgument(path, splash); Chris@578: } Chris@578: Chris@578: #ifdef HAVE_FFTW3F Chris@578: settings.beginGroup("FFTWisdom"); Chris@578: QString wisdom = settings.value("wisdom").toString(); Chris@578: if (wisdom != "") { Chris@578: fftwf_import_wisdom_from_string(wisdom.toLocal8Bit().data()); Chris@578: } Chris@578: #ifdef HAVE_FFTW3 Chris@578: wisdom = settings.value("wisdom_d").toString(); Chris@578: if (wisdom != "") { Chris@578: fftw_import_wisdom_from_string(wisdom.toLocal8Bit().data()); Chris@578: } Chris@578: #endif Chris@578: settings.endGroup(); Chris@578: #endif Chris@578: Chris@578: if (splash) splash->finish(gui); Chris@578: delete splash; Chris@578: Chris@578: /* Chris@578: TipDialog tipDialog; Chris@578: if (tipDialog.isOK()) { Chris@578: tipDialog.exec(); Chris@578: } Chris@578: */ Chris@578: int rv = application.exec(); Chris@578: Chris@578: gui->hide(); Chris@578: Chris@578: cleanupMutex.lock(); Chris@578: Chris@578: TransformFactory::deleteInstance(); Chris@578: TempDirectory::getInstance()->cleanup(); Chris@578: application.releaseMainWindow(); Chris@578: Chris@578: #ifdef HAVE_FFTW3F Chris@578: settings.beginGroup("FFTWisdom"); Chris@578: char *cwisdom = fftwf_export_wisdom_to_string(); Chris@578: if (cwisdom) { Chris@578: settings.setValue("wisdom", cwisdom); Chris@578: free(cwisdom); Chris@578: } Chris@578: #ifdef HAVE_FFTW3 Chris@578: cwisdom = fftw_export_wisdom_to_string(); Chris@578: if (cwisdom) { Chris@578: settings.setValue("wisdom_d", cwisdom); Chris@578: free(cwisdom); Chris@578: } Chris@578: #endif Chris@578: settings.endGroup(); Chris@578: #endif Chris@578: Chris@578: delete gui; Chris@578: Chris@578: cleanupMutex.unlock(); Chris@578: Chris@578: return rv; Chris@578: } Chris@578: Chris@578: bool SVApplication::event(QEvent *event){ Chris@578: QString thePath; Chris@578: switch (event->type()) { Chris@578: case QEvent::FileOpen: Chris@578: thePath = static_cast(event)->file(); Chris@578: if(m_readyForFiles) Chris@578: handleFilepathArgument(thePath, NULL); Chris@578: else Chris@578: m_filepathQueue.append(thePath); Chris@578: return true; Chris@578: default: Chris@578: return QApplication::event(event); Chris@578: } Chris@578: } Chris@578: Chris@578: /** Application-global handler for filepaths passed in, e.g. as command-line arguments or apple events */ Chris@578: void SVApplication::handleFilepathArgument(QString path, QSplashScreen *splash){ Chris@578: static bool haveSession = false; Chris@578: static bool haveMainModel = false; Chris@578: static bool havePriorCommandLineModel = false; Chris@578: Chris@578: MainWindow::FileOpenStatus status = MainWindow::FileOpenFailed; Chris@578: Chris@578: if (path.endsWith("sv")) { Chris@578: if (!haveSession) { Chris@578: status = m_mainWindow->openSessionFile(path); Chris@578: if (status == MainWindow::FileOpenSucceeded) { Chris@578: haveSession = true; Chris@578: haveMainModel = true; Chris@578: } Chris@578: } else { Chris@578: std::cerr << "WARNING: Ignoring additional session file argument \"" << path << "\"" << std::endl; Chris@578: status = MainWindow::FileOpenSucceeded; Chris@578: } Chris@578: } Chris@578: if (status != MainWindow::FileOpenSucceeded) { Chris@578: if (!haveMainModel) { Chris@578: status = m_mainWindow->open(path, MainWindow::ReplaceSession); Chris@578: if (status == MainWindow::FileOpenSucceeded) { Chris@578: haveMainModel = true; Chris@578: } Chris@578: } else { Chris@578: if (haveSession && !havePriorCommandLineModel) { Chris@578: status = m_mainWindow->open(path, MainWindow::AskUser); Chris@578: if (status == MainWindow::FileOpenSucceeded) { Chris@578: havePriorCommandLineModel = true; Chris@578: } Chris@578: } else { Chris@578: status = m_mainWindow->open(path, MainWindow::CreateAdditionalModel); Chris@578: } Chris@578: } Chris@578: } Chris@578: if (status == MainWindow::FileOpenFailed) { Chris@578: if (splash) splash->hide(); Chris@578: QMessageBox::critical Chris@578: (m_mainWindow, QMessageBox::tr("Failed to open file"), Chris@578: QMessageBox::tr("File or URL \"%1\" could not be opened").arg(path)); Chris@578: } else if (status == MainWindow::FileOpenWrongMode) { Chris@578: if (splash) splash->hide(); Chris@578: QMessageBox::critical Chris@578: (m_mainWindow, QMessageBox::tr("Failed to open file"), Chris@578: QMessageBox::tr("Audio required

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