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@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@120: #include "widgets/TipDialog.h" Chris@315: #include "transform/TransformFactory.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@231: #include Chris@252: #include Chris@331: #include dan@365: #include Chris@331: Chris@331: #include "../version.h" Chris@0: 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@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@0: cleanupMutex.lock(); Chris@0: std::cerr << "signalHandler: cleaning up and exiting" << std::endl; Chris@0: TempDirectory::getInstance()->cleanup(); Chris@0: exit(0); // without releasing mutex 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) dan@365: { } 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: dan@365: void handleFilepathArgument(QString path, QSplashScreen *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 *); dan@365: 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@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@0: #ifndef Q_WS_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@70: std::cerr << QApplication::tr( mathieu@406: "\nSonic Visualiser is a program for viewing and exploring audio data\nfor semantic music analysis and annotation. The Kiosk edition provides all of the features of the full Sonic Visualiser, except that audio files cannot be saved from it.\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]).toStdString() << std::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"); mathieu@406: QApplication::setApplicationName(QApplication::tr("Sonic Visualiser (Kiosk edition)")); Chris@141: Chris@283: QSplashScreen *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@331: QPixmap pixmap(":/icons/sv-splash.png"); Chris@331: QPainter painter; Chris@331: painter.begin(&pixmap); Chris@331: QString text = QString("v%1").arg(SV_VERSION); Chris@331: painter.drawText Chris@331: (pixmap.width() - painter.fontMetrics().width(text) - 10, Chris@331: 10 + painter.fontMetrics().ascent(), Chris@331: text); Chris@331: painter.end(); Chris@283: splash = new QSplashScreen(pixmap); 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@141: for (int i = 0; i < 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@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@253: std::cerr << "Done" << std::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@0: // Permit size_t and PropertyName to be used as args in queued signal calls Chris@0: qRegisterMetaType("size_t"); Chris@0: qRegisterMetaType("PropertyContainer::PropertyName"); Chris@0: dan@365: MainWindow *gui = new MainWindow(audioOutput, oscSupport); Chris@222: application.setMainWindow(gui); Chris@283: if (splash) { Chris@283: QObject::connect(gui, SIGNAL(hideSplash()), splash, SLOT(hide())); 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@5: QSize size = settings.value("size", QSize(width, height)).toSize(); Chris@319: gui->resizeConstrained(size); 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@5: settings.endGroup(); Chris@5: mathieu@459: settings.beginGroup("Preferences"); mathieu@459: bool mini = settings.value("start-in-mini-mode",true).toBool(); mathieu@459: mathieu@459: //std::cerr << "Minimal mode at startup (true/false): " << mini << std::endl; mathieu@459: mathieu@459: if (mini) { mathieu@459: gui->toggleViewMode(); mathieu@459: } mathieu@459: settings.endGroup(); mathieu@459: 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@283: if (splash) splash->finish(gui); Chris@283: delete splash; Chris@180: Chris@123: /* Chris@120: TipDialog tipDialog; Chris@120: if (tipDialog.isOK()) { Chris@120: tipDialog.exec(); Chris@120: } Chris@123: */ Chris@0: int rv = application.exec(); Chris@0: Chris@298: gui->hide(); Chris@298: Chris@0: cleanupMutex.lock(); Chris@332: Chris@315: TransformFactory::deleteInstance(); Chris@0: TempDirectory::getInstance()->cleanup(); 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@222: delete gui; Chris@222: Chris@0: return rv; Chris@0: } dan@365: dan@365: bool SVApplication::event(QEvent *event){ dan@365: QString thePath; 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 */ dan@365: void SVApplication::handleFilepathArgument(QString path, QSplashScreen *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: dan@365: if (path.endsWith("sv")) { dan@365: if (!haveSession) { dan@365: status = m_mainWindow->openSessionFile(path); dan@365: if (status == MainWindow::FileOpenSucceeded) { dan@365: haveSession = true; dan@365: haveMainModel = true; dan@365: } dan@365: } else { Chris@432: std::cerr << "WARNING: Ignoring additional session file argument \"" << path << "\"" << std::endl; dan@365: status = MainWindow::FileOpenSucceeded; dan@365: } dan@365: } dan@365: if (status != MainWindow::FileOpenSucceeded) { dan@365: if (!haveMainModel) { Chris@417: status = m_mainWindow->open(path, MainWindow::ReplaceSession); dan@365: if (status == MainWindow::FileOpenSucceeded) { dan@365: haveMainModel = true; dan@365: } dan@365: } else { dan@365: if (haveSession && !havePriorCommandLineModel) { dan@365: status = m_mainWindow->open(path, MainWindow::AskUser); dan@365: if (status == MainWindow::FileOpenSucceeded) { dan@365: havePriorCommandLineModel = true; dan@365: } dan@365: } else { dan@365: status = m_mainWindow->open(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: }