From 17d3204b47ec517cbfd16e38d5a300baa694c3cb Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 8 Sep 2023 23:16:03 -0300 Subject: [PATCH 01/24] analyse when recording --- main/Analyser.cpp | 95 ++++++------ main/Analyser.h | 10 +- main/MainWindow.cpp | 345 ++++++++++++++++++++++++++------------------ main/MainWindow.h | 18 ++- 4 files changed, 272 insertions(+), 196 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 957bfe6..9669057 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -5,7 +5,7 @@ An intonation analysis and annotation tool Centre for Digital Music, Queen Mary, University of London. This file copyright 2006-2012 Chris Cannam and QMUL. - + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the @@ -83,7 +83,7 @@ Analyser::getAnalysisSettings() QString Analyser::newFileLoaded(Document *doc, ModelId model, - PaneStack *paneStack, Pane *pane) + PaneStack *paneStack, Pane *pane) { m_document = doc; m_fileModel = model; @@ -93,7 +93,7 @@ Analyser::newFileLoaded(Document *doc, ModelId model, if (!ModelById::isa(m_fileModel)) { return "Internal error: Analyser::newFileLoaded() called with no model, or a non-WaveFileModel"; } - + connect(doc, SIGNAL(layerAboutToBeDeleted(Layer *)), this, SLOT(layerAboutToBeDeleted(Layer *))); @@ -113,7 +113,7 @@ Analyser::analyseExistingFile() if (!m_pane) return "Internal error: Analyser::analyseExistingFile() called with no pane present"; if (m_fileModel.isNone()) return "Internal error: Analyser::analyseExistingFile() called with no model present"; - + if (m_layers[PitchTrack]) { m_document->removeLayerFromView(m_pane, m_layers[PitchTrack]); m_layers[PitchTrack] = 0; @@ -204,7 +204,7 @@ Analyser::getInitialAnalysisCompletion() int c = m_layers[Notes]->getCompletion(m_pane); if (c < completion) completion = c; } - + return completion; } @@ -227,7 +227,7 @@ Analyser::layerCompletionChanged(ModelId) auto audioModel = ModelById::get(m_layers[Audio]->getModel()); sv_frame_t endFrame = audioModel->getEndFrame(); - + if (m_layers[PitchTrack]) { auto model = ModelById::getAs (m_layers[PitchTrack]->getModel()); @@ -255,7 +255,7 @@ Analyser::addVisualisations() /* This is roughly what we'd do for a constant-Q spectrogram, but it currently has issues with y-axis alignment - + TransformFactory *tf = TransformFactory::getInstance(); QString name = "Constant-Q"; @@ -264,7 +264,7 @@ Analyser::addVisualisations() QString notFound = tr("Transform \"%1\" not found, spectrogram will not be enabled.

Is the %2 Vamp plugin correctly installed?"); if (!tf->haveTransform(base + out)) { - return notFound.arg(base + out).arg(name); + return notFound.arg(base + out).arg(name); } Transform transform = tf->getDefaultTransformFor @@ -275,7 +275,7 @@ Analyser::addVisualisations() (m_document->createDerivedLayer(transform, m_fileModel)); if (!spectrogram) return tr("Transform \"%1\" did not run correctly (no layer or wrong layer type returned)").arg(base + out); -*/ +*/ // As with all the visualisation layers, if we already have one in // the pane we do not create another, just record its @@ -338,7 +338,7 @@ Analyser::addWaveform() params->setPlayPan(-1); params->setPlayGain(1); } - + m_document->addLayerToView(m_pane, waveform); m_layers[Audio] = waveform; @@ -352,7 +352,7 @@ Analyser::addAnalyses() if (!waveFileModel) { return "Internal error: Analyser::addAnalyses() called with no model present"; } - + // As with the spectrogram above, if these layers exist we use // them TimeValueLayer *existingPitch = 0; @@ -382,7 +382,7 @@ Analyser::addAnalyses() } TransformFactory *tf = TransformFactory::getInstance(); - + QString plugname = "pYIN"; QString base = "vamp:pyin:pyin:"; QString f0out = "smoothedpitchtrack"; @@ -404,17 +404,17 @@ Analyser::addAnalyses() QString notFound = tr("Transform \"%1\" not found. Unable to analyse audio file.

Is the %2 Vamp plugin correctly installed?"); if (!tf->haveTransform(base + f0out)) { - return notFound.arg(base + f0out).arg(plugname); + return notFound.arg(base + f0out).arg(plugname); } if (!tf->haveTransform(base + noteout)) { - return notFound.arg(base + noteout).arg(plugname); + return notFound.arg(base + noteout).arg(plugname); } QSettings settings; settings.beginGroup("Analyser"); bool precise = false, lowamp = true, onset = true, prune = true; - + std::map flags { { "precision-analysis", precise }, { "lowamp-analysis", lowamp }, @@ -423,7 +423,7 @@ Analyser::addAnalyses() }; auto keyMap = getAnalysisSettings(); - + for (auto p: flags) { auto ki = keyMap.find(p.first); if (ki != keyMap.end()) { @@ -475,7 +475,7 @@ Analyser::addAnalyses() transforms.push_back(t); t.setOutput(noteout); - + transforms.push_back(t); std::vector layers = @@ -485,16 +485,16 @@ Analyser::addAnalyses() FlexiNoteLayer *f = qobject_cast(layers[i]); TimeValueLayer *t = qobject_cast(layers[i]); - + if (f) m_layers[Notes] = f; if (t) m_layers[PitchTrack] = t; - + m_document->addLayerToView(m_pane, layers[i]); } - + ColourDatabase *cdb = ColourDatabase::getInstance(); - - TimeValueLayer *pitchLayer = + + TimeValueLayer *pitchLayer = qobject_cast(m_layers[PitchTrack]); if (pitchLayer) { pitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); @@ -506,8 +506,8 @@ Analyser::addAnalyses() connect(pitchLayer, SIGNAL(modelCompletionChanged(ModelId)), this, SLOT(layerCompletionChanged(ModelId))); } - - FlexiNoteLayer *flexiNoteLayer = + + FlexiNoteLayer *flexiNoteLayer = qobject_cast(m_layers[Notes]); if (flexiNoteLayer) { flexiNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); @@ -523,7 +523,7 @@ Analyser::addAnalyses() connect(flexiNoteLayer, SIGNAL(materialiseReAnalysis()), this, SLOT(materialiseReAnalysis())); } - + return ""; } @@ -553,7 +553,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) if (!waveFileModel) { return "Internal error: Analyser::reAnalyseSelection() called with no model present"; } - + if (!m_reAnalysingSelection.isEmpty()) { if (sel == m_reAnalysingSelection && range == m_reAnalysingRange) { cerr << "selection & range are same as current analysis, ignoring" << endl; @@ -584,7 +584,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) } TransformFactory *tf = TransformFactory::getInstance(); - + QString plugname1 = "pYIN"; QString plugname2 = "CHP"; @@ -600,7 +600,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); if (!tf->haveTransform(base + out)) { - return notFound.arg(base + out).arg(plugname1).arg(plugname2); + return notFound.arg(base + out).arg(plugname1).arg(plugname2); } Transform t = tf->getDefaultTransformFor @@ -626,7 +626,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) } else { endSample -= 9*grid; // MM says: not sure what the CHP plugin does there } - RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); + RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); RealTime end = RealTime::frame2RealTime(endSample, waveFileModel->getSampleRate()); RealTime duration; @@ -641,12 +641,12 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) cerr << "Analyser::reAnalyseSelection: duration <= 0, not analysing" << endl; return ""; } - + t.setStartTime(start); t.setDuration(duration); transforms.push_back(t); - + m_currentAsyncHandle = m_document->createDerivedLayersAsync(transforms, m_fileModel, this); @@ -660,7 +660,7 @@ Analyser::arePitchCandidatesShown() const } void -Analyser::showPitchCandidates(bool shown) +Analyser::showPitchCandidates(bool shown) { if (m_candidatesVisible == shown) return; @@ -687,7 +687,7 @@ Analyser::layersCreated(Document::LayerCreationAsyncHandle handle, { QMutexLocker locker(&m_asyncMutex); - if (handle != m_currentAsyncHandle || + if (handle != m_currentAsyncHandle || m_reAnalysingSelection == Selection()) { // We don't want these! for (int i = 0; i < (int)primary.size(); ++i) { @@ -750,14 +750,14 @@ Analyser::haveHigherPitchCandidate() const if (m_reAnalysisCandidates.empty()) return false; return (m_currentCandidate < 0 || (m_currentCandidate + 1 < (int)m_reAnalysisCandidates.size())); -} +} bool Analyser::haveLowerPitchCandidate() const { if (m_reAnalysisCandidates.empty()) return false; return (m_currentCandidate < 0 || m_currentCandidate >= 1); -} +} void Analyser::switchPitchCandidate(Selection sel, bool up) @@ -803,14 +803,14 @@ void Analyser::shiftOctave(Selection sel, bool up) { float factor = (up ? 2.f : 0.5f); - + vector actOn; Layer *pitchTrack = m_layers[PitchTrack]; if (pitchTrack) actOn.push_back(pitchTrack); foreach (Layer *layer, actOn) { - + Clipboard clip; layer->copy(m_pane, sel, clip); layer->deleteSelection(sel); @@ -824,7 +824,7 @@ Analyser::shiftOctave(Selection sel, bool up) shifted.addPoint(e); } } - + layer->paste(m_pane, shifted, 0, false); } } @@ -849,7 +849,7 @@ Analyser::abandonReAnalysis(Selection sel) if (!myLayer) return; myLayer->deleteSelection(sel); myLayer->paste(m_pane, m_preAnalysis, 0, false); -} +} void Analyser::clearReAnalysis() @@ -880,7 +880,7 @@ void Analyser::layerAboutToBeDeleted(Layer *doomed) { cerr << "Analyser::layerAboutToBeDeleted(" << doomed << ")" << endl; - + vector notDoomed; foreach (Layer *layer, m_reAnalysisCandidates) { @@ -903,7 +903,7 @@ Analyser::takePitchTrackFrom(Layer *otherLayer) if (!myModel || !otherModel) return; Clipboard clip; - + Selection sel = Selection(myModel->getStartFrame(), myModel->getEndFrame()); myLayer->deleteSelection(sel); @@ -932,7 +932,7 @@ Analyser::takePitchTrackFrom(Layer *otherLayer) void Analyser::getEnclosingSelectionScope(sv_frame_t f, sv_frame_t &f0, sv_frame_t &f1) { - FlexiNoteLayer *flexiNoteLayer = + FlexiNoteLayer *flexiNoteLayer = qobject_cast(m_layers[Notes]); sv_frame_t f0i = f, f1i = f; @@ -942,7 +942,7 @@ Analyser::getEnclosingSelectionScope(sv_frame_t f, sv_frame_t &f0, sv_frame_t &f f0 = f1 = f; return; } - + flexiNoteLayer->snapToFeatureFrame(m_pane, f0i, res, Layer::SnapLeft, -1); flexiNoteLayer->snapToFeatureFrame(m_pane, f1i, res, Layer::SnapRight, -1); @@ -976,11 +976,11 @@ Analyser::loadState(Component c) } void -Analyser::setIntelligentActions(bool on) +Analyser::setIntelligentActions(bool on) { std::cerr << "toggle setIntelligentActions " << on << std::endl; - FlexiNoteLayer *flexiNoteLayer = + FlexiNoteLayer *flexiNoteLayer = qobject_cast(m_layers[Notes]); if (flexiNoteLayer) { flexiNoteLayer->setIntelligentActions(on); @@ -1055,7 +1055,7 @@ Analyser::getGain(Component c) const return 1.f; } } - + void Analyser::setGain(Component c, float gain) { @@ -1078,7 +1078,7 @@ Analyser::getPan(Component c) const return 1.f; } } - + void Analyser::setPan(Component c, float pan) { @@ -1091,4 +1091,3 @@ Analyser::setPan(Component c, float pan) } - diff --git a/main/Analyser.h b/main/Analyser.h index 918d239..2db3463 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -5,7 +5,7 @@ An intonation analysis and annotation tool Centre for Digital Music, Queen Mary, University of London. This file copyright 2006-2012 Chris Cannam and QMUL. - + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the @@ -56,9 +56,11 @@ class Analyser : public QObject, // layers; return "" on success or error string on failure QString analyseExistingFile(); + QString doAllAnalyses(bool withPitchTrack); + // Discard any layers etc associated with the current document void fileClosed(); - + void setIntelligentActions(bool); bool getDisplayFrequencyExtents(double &min, double &max); @@ -128,7 +130,7 @@ class Analyser : public QObject, * group in QSettings. */ static std::map getAnalysisSettings(); - + /** * Analyse the selection and schedule asynchronous adds of * candidate layers for the region it contains. Returns "" on @@ -263,7 +265,7 @@ protected slots: void discardPitchCandidates(); void stackLayers(); - + // Document::LayerCreationHandler method void layersCreated(sv::Document::LayerCreationAsyncHandle, std::vector, std::vector); diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index cd9fa4a..879ef48 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -5,7 +5,7 @@ An intonation analysis and annotation tool Centre for Digital Music, Queen Mary, University of London. This file copyright 2006-2012 Chris Cannam and QMUL. - + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the @@ -102,7 +102,7 @@ using std::endl; using namespace sv; MainWindow::MainWindow(AudioMode audioMode, - bool withSonification, + bool withSonification, bool withSpectrogram) : MainWindowBase(audioMode, MainWindowBase::MIDI_NONE, @@ -111,7 +111,7 @@ MainWindow::MainWindow(AudioMode audioMode, m_overview(0), m_mainMenusCreated(false), m_playbackMenu(0), - m_recentFilesMenu(0), + m_recentFilesMenu(0), m_rightButtonMenu(0), m_rightButtonPlaybackMenu(0), m_deleteSelectedAction(0), @@ -186,13 +186,13 @@ MainWindow::MainWindow(AudioMode audioMode, m_viewManager->setOverlayMode(ViewManager::GlobalOverlays); connect(m_viewManager, SIGNAL(selectionChangedByUser()), - this, SLOT(selectionChangedByUser())); + this, SLOT(selectionChangedByUser())); QFrame *frame = new QFrame; setCentralWidget(frame); QGridLayout *layout = new QGridLayout; - + QScrollArea *scroll = new QScrollArea(frame); scroll->setWidgetResizable(true); scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -230,7 +230,7 @@ MainWindow::MainWindow(AudioMode audioMode, } else { m_panLayer->setBaseColour (ColourDatabase::getInstance()->getColourIndex(tr("Blue"))); - } + } m_fader = new Fader(frame, false); connect(m_fader, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); @@ -311,7 +311,7 @@ MainWindow::MainWindow(AudioMode audioMode, m_activityLog->hide(); setAudioRecordMode(RecordReplaceSession); - + newSession(); settings.beginGroup("MainWindow"); @@ -350,7 +350,7 @@ MainWindow::setupMenus() // workaround, to remove the appmenu-qt5 package, but that is // awkward and the problem is so severe that it merits disabling // the system menubar integration altogether. Like this: - menuBar()->setNativeMenuBar(false); + menuBar()->setNativeMenuBar(false); #endif m_rightButtonMenu = new QMenu(); @@ -417,7 +417,7 @@ MainWindow::setupFileMenu() m_keyReference->registerShortcut(action); menu->addAction(action); toolbar->addAction(action); - + icon = il.load("filesaveas"); action = new QAction(icon, tr("Save Session &As..."), this); action->setShortcut(tr("Ctrl+Shift+S")); @@ -455,7 +455,7 @@ MainWindow::setupFileMenu() menu->addAction(action); menu->addSeparator(); - + action = new QAction(tr("Browse Recorded Audio"), this); action->setStatusTip(tr("Open the Recorded Audio folder in the system file browser")); connect(action, SIGNAL(triggered()), this, SLOT(browseRecordedAudio())); @@ -484,17 +484,17 @@ MainWindow::setupEditMenu() m_keyReference->setCategory (tr("Selection Strip Mouse Actions")); m_keyReference->registerShortcut - (tr("Jump"), tr("Left"), + (tr("Jump"), tr("Left"), tr("Click left button to move the playback position to a time")); m_keyReference->registerShortcut - (tr("Select"), tr("Left"), + (tr("Select"), tr("Left"), tr("Click left button and drag to select a region of time")); m_keyReference->registerShortcut - (tr("Select Note Duration"), tr("Double-Click Left"), + (tr("Select Note Duration"), tr("Double-Click Left"), tr("Double-click left button to select the region of time corresponding to a note")); QToolBar *toolbar = addToolBar(tr("Tools Toolbar")); - + CommandHistory::getInstance()->registerToolbar(toolbar); QActionGroup *group = new QActionGroup(this); @@ -517,18 +517,18 @@ MainWindow::setupEditMenu() m_keyReference->setCategory (tr("Navigate Tool Mouse Actions")); m_keyReference->registerShortcut - (tr("Navigate"), tr("Left"), + (tr("Navigate"), tr("Left"), tr("Click left button and drag to move around")); m_keyReference->registerShortcut - (tr("Re-Analyse Area"), tr("Shift+Left"), + (tr("Re-Analyse Area"), tr("Shift+Left"), tr("Shift-click left button and drag to define a specific pitch and time range to re-analyse")); m_keyReference->registerShortcut - (tr("Edit"), tr("Double-Click Left"), + (tr("Edit"), tr("Double-Click Left"), tr("Double-click left button on an item to edit it")); m_keyReference->setCategory(tr("Tool Selection")); action = toolbar->addAction(il.load("move"), - tr("Edit")); + tr("Edit")); action->setCheckable(true); action->setShortcut(tr("2")); action->setStatusTip(tr("Edit with Note Intelligence")); @@ -540,16 +540,16 @@ MainWindow::setupEditMenu() m_keyReference->setCategory (tr("Note Edit Tool Mouse Actions")); m_keyReference->registerShortcut - (tr("Adjust Pitch"), tr("Left"), + (tr("Adjust Pitch"), tr("Left"), tr("Click left button on the main part of a note and drag to move it up or down")); m_keyReference->registerShortcut - (tr("Split"), tr("Left"), + (tr("Split"), tr("Left"), tr("Click left button on the bottom edge of a note to split it at the click point")); m_keyReference->registerShortcut - (tr("Resize"), tr("Left"), + (tr("Resize"), tr("Left"), tr("Click left button on the left or right edge of a note and drag to change the time or duration of the note")); m_keyReference->registerShortcut - (tr("Erase"), tr("Shift+Left"), + (tr("Erase"), tr("Shift+Left"), tr("Shift-click left button on a note to remove it")); @@ -557,7 +557,7 @@ MainWindow::setupEditMenu() m_keyReference->setCategory(tr("Tool Selection")); action = toolbar->addAction(il.load("notes"), - tr("Free Edit")); + tr("Free Edit")); action->setCheckable(true); action->setShortcut(tr("3")); action->setStatusTip(tr("Free Edit")); @@ -567,7 +567,7 @@ MainWindow::setupEditMenu() */ menu->addSeparator(); - + m_keyReference->setCategory(tr("Selection")); action = new QAction(tr("Select &All"), this); @@ -593,9 +593,9 @@ MainWindow::setupEditMenu() menu->addSeparator(); m_rightButtonMenu->addSeparator(); - + m_keyReference->setCategory(tr("Pitch Track")); - + action = new QAction(tr("Choose Higher Pitch"), this); action->setShortcut(tr("Ctrl+Up")); action->setStatusTip(tr("Move pitches up an octave, or to the next higher pitch candidate")); @@ -604,7 +604,7 @@ MainWindow::setupEditMenu() connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool))); menu->addAction(action); m_rightButtonMenu->addAction(action); - + action = new QAction(tr("Choose Lower Pitch"), this); action->setShortcut(tr("Ctrl+Down")); action->setStatusTip(tr("Move pitches down an octave, or to the next lower pitch candidate")); @@ -622,7 +622,7 @@ MainWindow::setupEditMenu() connect(this, SIGNAL(canClearSelection(bool)), m_showCandidatesAction, SLOT(setEnabled(bool))); menu->addAction(m_showCandidatesAction); m_rightButtonMenu->addAction(m_showCandidatesAction); - + action = new QAction(tr("Remove Pitches"), this); action->setShortcut(tr("Ctrl+Backspace")); action->setStatusTip(tr("Remove all pitch estimates within the selected region, making it unvoiced")); @@ -634,7 +634,7 @@ MainWindow::setupEditMenu() menu->addSeparator(); m_rightButtonMenu->addSeparator(); - + m_keyReference->setCategory(tr("Note Track")); action = new QAction(tr("Split Note"), this); @@ -663,7 +663,7 @@ MainWindow::setupEditMenu() connect(this, SIGNAL(canSnapNotes(bool)), action, SLOT(setEnabled(bool))); menu->addAction(action); m_rightButtonMenu->addAction(action); - + action = new QAction(tr("Form Note from Selection"), this); action->setShortcut(tr("=")); action->setStatusTip(tr("Form a note spanning the selected region, splitting any existing notes at its boundaries")); @@ -702,7 +702,7 @@ MainWindow::setupViewMenu() connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); m_keyReference->registerShortcut(action); menu->addAction(action); - + action = new QAction(tr("Peek &Right"), this); action->setShortcut(tr("Alt+Right")); action->setStatusTip(tr("Scroll the current pane to the right without changing the play position")); @@ -723,7 +723,7 @@ MainWindow::setupViewMenu() connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); m_keyReference->registerShortcut(action); menu->addAction(action); - + action = new QAction(il.load("zoom-out"), tr("Zoom &Out"), this); action->setShortcut(tr("Down")); @@ -732,7 +732,7 @@ MainWindow::setupViewMenu() connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); m_keyReference->registerShortcut(action); menu->addAction(action); - + action = new QAction(tr("Restore &Default Zoom"), this); action->setStatusTip(tr("Restore the zoom level to the default")); connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault())); @@ -749,7 +749,7 @@ MainWindow::setupViewMenu() menu->addAction(action); menu->addSeparator(); - + action = new QAction(tr("Set Displayed Fre&quency Range..."), this); action->setStatusTip(tr("Set the minimum and maximum frequencies in the visible display")); connect(action, SIGNAL(triggered()), this, SLOT(editDisplayExtents())); @@ -768,6 +768,12 @@ MainWindow::setupAnalysisMenu() QMenu *menu = menuBar()->addMenu(tr("&Analysis")); menu->setTearOffEnabled(true); + m_analyseDuringRecord = new QAction(tr("&Analyse during recording"), this); + m_analyseDuringRecord->setStatusTip(tr("Automatically trigger analysis during recording.")); + m_analyseDuringRecord->setCheckable(true); + connect(m_analyseDuringRecord, SIGNAL(triggered()), this, SLOT(recordAnalysisToggled())); + menu->addAction(m_analyseDuringRecord); + m_autoAnalyse = new QAction(tr("Auto-Analyse &New Audio"), this); m_autoAnalyse->setStatusTip(tr("Automatically trigger analysis upon opening of a new audio file.")); m_autoAnalyse->setCheckable(true); @@ -823,7 +829,7 @@ MainWindow::resetAnalyseOptions() settings.beginGroup("Analyser"); settings.setValue("auto-analysis", true); - + auto keyMap = Analyser::getAnalysisSettings(); for (auto p: keyMap) { settings.setValue(p.first, p.second); @@ -850,7 +856,7 @@ MainWindow::updateAnalyseStates() }; auto keyMap = Analyser::getAnalysisSettings(); - + for (auto p: actions) { auto ki = keyMap.find(p.first); if (ki != keyMap.end()) { @@ -864,6 +870,23 @@ MainWindow::updateAnalyseStates() settings.endGroup(); } +void +MainWindow::recordAnalysisToggled() +{ + QAction *a = qobject_cast(sender()); + if (!a) return; + + bool set = a->isChecked(); + + QSettings settings; + settings.beginGroup("Analyser"); + settings.setValue("record-analysis", set); + settings.endGroup(); + + // make result visible explicitly, in case e.g. we just set the wrong key + updateAnalyseStates(); +} + void MainWindow::autoAnalysisToggled() { @@ -962,7 +985,7 @@ MainWindow::setupHelpMenu() { QMenu *menu = menuBar()->addMenu(tr("&Help")); menu->setTearOffEnabled(true); - + m_keyReference->setCategory(tr("Help")); IconLoader il; @@ -971,9 +994,9 @@ MainWindow::setupHelpMenu() QAction *action; action = new QAction(il.load("help"), - tr("&Help Reference"), this); + tr("&Help Reference"), this); action->setShortcut(tr("F1")); - action->setStatusTip(tr("Open the %1 reference manual").arg(name)); + action->setStatusTip(tr("Open the %1 reference manual").arg(name)); connect(action, SIGNAL(triggered()), this, SLOT(help())); m_keyReference->registerShortcut(action); menu->addAction(action); @@ -984,14 +1007,14 @@ MainWindow::setupHelpMenu() connect(action, SIGNAL(triggered()), this, SLOT(keyReference())); m_keyReference->registerShortcut(action); menu->addAction(action); - - action = new QAction(tr("What's &New In This Release?"), this); - action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); + + action = new QAction(tr("What's &New In This Release?"), this); + action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); connect(action, SIGNAL(triggered()), this, SLOT(whatsNew())); menu->addAction(action); - - action = new QAction(tr("&About %1").arg(name), this); - action->setStatusTip(tr("Show information about %1").arg(name)); + + action = new QAction(tr("&About %1").arg(name), this); + action->setStatusTip(tr("Show information about %1").arg(name)); connect(action, SIGNAL(triggered()), this, SLOT(about())); menu->addAction(action); } @@ -1076,10 +1099,14 @@ MainWindow::setupToolbars() recordAction->setShortcut(tr("Ctrl+Space")); recordAction->setStatusTip(tr("Record a new audio file")); connect(recordAction, SIGNAL(triggered()), this, SLOT(record())); + + connect(recordAction, SIGNAL(triggered()), + this, SLOT(analyseDuringRecordingRunner())); + connect(m_recordTarget, SIGNAL(recordStatusChanged(bool)), - recordAction, SLOT(setChecked(bool))); + recordAction, SLOT(setChecked(bool))); connect(m_recordTarget, SIGNAL(recordCompleted()), - this, SLOT(analyseNow())); + this, SLOT(analyseNow())); connect(this, SIGNAL(canRecord(bool)), recordAction, SLOT(setEnabled(bool))); @@ -1112,7 +1139,7 @@ MainWindow::setupToolbars() oneLeftAction->setStatusTip(tr("Move cursor to the preceding note (or silence) onset.")); connect(oneLeftAction, SIGNAL(triggered()), this, SLOT(moveOneNoteLeft())); connect(this, SIGNAL(canScroll(bool)), oneLeftAction, SLOT(setEnabled(bool))); - + QAction *oneRightAction = new QAction(tr("O&ne Note Right"), this); oneRightAction->setShortcut(tr("Ctrl+Right")); oneRightAction->setStatusTip(tr("Move cursor to the succeeding note (or silence).")); @@ -1124,7 +1151,7 @@ MainWindow::setupToolbars() selectOneLeftAction->setStatusTip(tr("Select to the preceding note (or silence) onset.")); connect(selectOneLeftAction, SIGNAL(triggered()), this, SLOT(selectOneNoteLeft())); connect(this, SIGNAL(canScroll(bool)), selectOneLeftAction, SLOT(setEnabled(bool))); - + QAction *selectOneRightAction = new QAction(tr("S&elect One Note Right"), this); selectOneRightAction->setShortcut(tr("Ctrl+Shift+Right")); selectOneRightAction->setStatusTip(tr("Select to the succeeding note (or silence).")); @@ -1186,7 +1213,7 @@ MainWindow::setupToolbars() fastAction->setStatusTip(tr("Time-stretch playback to speed it up without changing pitch")); connect(fastAction, SIGNAL(triggered()), this, SLOT(speedUpPlayback())); connect(this, SIGNAL(canSpeedUpPlayback(bool)), fastAction, SLOT(setEnabled(bool))); - + QAction *slowAction = menu->addAction(tr("Slow Down")); slowAction->setShortcut(tr("Ctrl+PgDown")); slowAction->setStatusTip(tr("Time-stretch playback to slow it down without changing pitch")); @@ -1234,7 +1261,7 @@ MainWindow::setupToolbars() lpwSize = m_viewManager->scalePixelSize(26); bigLpwSize = int(lpwSize * 2.8); #endif - + m_audioLPW->setImageSize(lpwSize); m_audioLPW->setBigImageSize(bigLpwSize); toolbar->addWidget(m_audioLPW); @@ -1303,7 +1330,7 @@ MainWindow::setupToolbars() Pane::registerShortcuts(*m_keyReference); updateLayerStatuses(); - + // QTimer::singleShot(500, this, SLOT(betaReleaseWarning())); } @@ -1340,7 +1367,7 @@ MainWindow::moveByOneNote(bool right, bool doSelect) { sv_frame_t frame = m_viewManager->getPlaybackFrame(); cerr << "MainWindow::moveByOneNote startframe: " << frame << endl; - + bool isAtSelectionBoundary = false; MultiSelection::SelectionList selections = m_viewManager->getSelections(); if (!selections.empty()) { @@ -1360,7 +1387,7 @@ MainWindow::moveByOneNote(bool right, bool doSelect) //!!! This seems like a strange and inefficient way to do this - //!!! there is almost certainly a better way making use of //!!! EventSeries api - + EventVector points = model->getAllEvents(); if (points.empty()) return; @@ -1434,24 +1461,24 @@ MainWindow::updateMenuStates() if (currentPane) currentLayer = currentPane->getSelectedLayer(); bool haveMainModel = - (getMainModel() != 0); + (getMainModel() != 0); bool havePlayTarget = - (m_playTarget != 0 || m_audioIO != 0); + (m_playTarget != 0 || m_audioIO != 0); bool haveCurrentPane = (currentPane != 0); bool haveCurrentLayer = (haveCurrentPane && (currentLayer != 0)); - bool haveSelection = + bool haveSelection = (m_viewManager && !m_viewManager->getSelections().empty()); - bool haveCurrentTimeInstantsLayer = + bool haveCurrentTimeInstantsLayer = (haveCurrentLayer && qobject_cast(currentLayer)); - bool haveCurrentTimeValueLayer = + bool haveCurrentTimeValueLayer = (haveCurrentLayer && qobject_cast(currentLayer)); - bool pitchCandidatesVisible = + bool pitchCandidatesVisible = m_analyser->arePitchCandidatesShown(); emit canChangePlaybackSpeed(true); @@ -1463,11 +1490,11 @@ MainWindow::updateMenuStates() m_analyser->isVisible(Analyser::Audio) && m_analyser->getLayer(Analyser::Audio); - bool havePitchTrack = + bool havePitchTrack = m_analyser->isVisible(Analyser::PitchTrack) && m_analyser->getLayer(Analyser::PitchTrack); - bool haveNotes = + bool haveNotes = m_analyser->isVisible(Analyser::Notes) && m_analyser->getLayer(Analyser::Notes); @@ -1611,7 +1638,7 @@ MainWindow::updateLayerStatuses() m_audioLPW->setEnabled(m_analyser->isAudible(Analyser::Audio)); m_audioLPW->setLevel(m_analyser->getGain(Analyser::Audio)); m_audioLPW->setPan(m_analyser->getPan(Analyser::Audio)); - + m_showPitch->setChecked(m_analyser->isVisible(Analyser::PitchTrack)); m_playPitch->setChecked(m_analyser->isAudible(Analyser::PitchTrack)); m_pitchLPW->setEnabled(m_analyser->isAudible(Analyser::PitchTrack)); @@ -1633,7 +1660,7 @@ MainWindow::editDisplayExtents() double min, max; double vmin = 0; double vmax = getMainModel()->getSampleRate() /2; - + if (!m_analyser->getDisplayFrequencyExtents(min, max)) { //!!! return; @@ -1691,7 +1718,7 @@ MainWindow::newSession() m_viewManager->setGlobalCentreFrame (pane->getFrameForX(width() / 2)); - + connect(pane, SIGNAL(contextHelpChanged(const QString &)), this, SLOT(contextHelpChanged(const QString &))); @@ -1730,7 +1757,7 @@ MainWindow::closeSession() m_document->removeLayerFromView (pane, pane->getLayer(pane->getLayerCount() - 1)); } - + m_overview->unregisterView(pane); m_paneStack->deletePane(pane); } @@ -1739,12 +1766,12 @@ MainWindow::closeSession() Pane *pane = m_paneStack->getHiddenPane (m_paneStack->getHiddenPaneCount() - 1); - + while (pane->getLayerCount() > 0) { m_document->removeLayerFromView (pane, pane->getLayer(pane->getLayerCount() - 1)); } - + m_overview->unregisterView(pane); m_paneStack->deletePane(pane); } @@ -1818,7 +1845,7 @@ MainWindow::openRecentFile() { QObject *obj = sender(); QAction *action = qobject_cast(obj); - + if (!action) { cerr << "WARNING: MainWindow::openRecentFile: sender is not an action" << endl; @@ -1845,19 +1872,19 @@ MainWindow::paneAdded(Pane *pane) pane->setPlaybackFollow(PlaybackScrollPage); m_paneStack->sizePanesEqually(); if (m_overview) m_overview->registerView(pane); -} +} void MainWindow::paneHidden(Pane *pane) { - if (m_overview) m_overview->unregisterView(pane); -} + if (m_overview) m_overview->unregisterView(pane); +} void MainWindow::paneAboutToBeDeleted(Pane *pane) { - if (m_overview) m_overview->unregisterView(pane); -} + if (m_overview) m_overview->unregisterView(pane); +} void MainWindow::paneDropAccepted(Pane *pane, QStringList uriList) @@ -1884,8 +1911,8 @@ MainWindow::paneDropAccepted(Pane *pane, QString text) if (pane) m_paneStack->setCurrentPane(pane); QUrl testUrl(text); - if (testUrl.scheme() == "file" || - testUrl.scheme() == "http" || + if (testUrl.scheme() == "file" || + testUrl.scheme() == "http" || testUrl.scheme() == "ftp") { QStringList list; list.push_back(text); @@ -1950,7 +1977,7 @@ MainWindow::commitData(bool mayAskUser) } else { if (!QFileInfo(svDir).isDir()) return false; } - + // This name doesn't have to be unguessable #ifndef _WIN32 QString fname = QString("tmp-%1-%2.sv") @@ -1979,7 +2006,7 @@ MainWindow::checkSaveModified() if (!m_documentModified) return true; - int button = + int button = QMessageBox::warning(this, tr("Session modified"), tr("The current session has been modified.\nDo you want to save it?"), @@ -2009,7 +2036,7 @@ MainWindow::waitForInitialAnalysis() // initial analysis is happening, because then we end up with an // incomplete session on reload. There are certainly theoretically // better ways to handle this... - + QSettings settings; settings.beginGroup("Analyser"); bool autoAnalyse = settings.value("auto-analysis", true).toBool(); @@ -2029,7 +2056,7 @@ MainWindow::waitForInitialAnalysis() QMessageBox::Cancel, this); - connect(m_analyser, SIGNAL(initialAnalysisCompleted()), + connect(m_analyser, SIGNAL(initialAnalysisCompleted()), &mb, SLOT(accept())); if (mb.exec() == QDialog::Accepted) { @@ -2164,14 +2191,14 @@ MainWindow::exportToSVL(QString path, Layer *layer) << "\n" << "\n" << " \n"; - + model->toXml(out, " "); - + out << " \n" << " \n"; - + layer->toXml(out, " "); - + out << " \n" << "\n"; @@ -2206,10 +2233,10 @@ MainWindow::importPitchLayer(FileSource source) source.waitForData(); if (!waitForInitialAnalysis()) return FileOpenCancelled; - + QString path = source.getLocalFilename(); - RDFImporter::RDFDocumentType rdfType = + RDFImporter::RDFDocumentType rdfType = RDFImporter::identifyDocumentType(QUrl::fromLocalFile(path).toString()); if (rdfType != RDFImporter::NotRDF) { @@ -2221,12 +2248,12 @@ MainWindow::importPitchLayer(FileSource source) (source.getExtension().toLower() == "xml" && (SVFileReader::identifyXmlFile(source.getLocalFilename()) == SVFileReader::SVLayerFile))) { - + //!!! return FileOpenFailed; } else { - + try { CSVFormat format(path); @@ -2246,7 +2273,7 @@ MainWindow::importPitchLayer(FileSource source) ModelId modelId = ModelById::add (std::shared_ptr(model)); - + CommandHistory::getInstance()->startCompoundOperation (tr("Import Pitch Track"), true); @@ -2272,7 +2299,7 @@ MainWindow::importPitchLayer(FileSource source) } } } - + return FileOpenFailed; } @@ -2292,7 +2319,7 @@ MainWindow::exportPitchLayer() if (path == "") return; if (!waitForInitialAnalysis()) return; - + if (QFileInfo(path).suffix() == "") path += ".svl"; QString suffix = QFileInfo(path).suffix().toLower(); @@ -2314,7 +2341,7 @@ MainWindow::exportPitchLayer() } else { DataExportOptions options = DataExportFillGaps; - + CSVFileWriter writer(path, model.get(), ((suffix == "csv") ? "," : "\t"), options); @@ -2358,7 +2385,7 @@ MainWindow::exportNoteLayer() error = exportToSVL(path, layer); } else if (suffix == "mid" || suffix == "midi") { - + MIDIFileWriter writer(path, model.get(), model->getSampleRate()); writer.write(); if (!writer.isOK()) { @@ -2376,7 +2403,7 @@ MainWindow::exportNoteLayer() } else { DataExportOptions options = DataExportOmitLevel; - + CSVFileWriter writer(path, model.get(), ((suffix == "csv") ? "," : "\t"), options); @@ -2411,7 +2438,7 @@ MainWindow::doubleClickSelectInvoked(sv_frame_t frame) { sv_frame_t f0, f1; m_analyser->getEnclosingSelectionScope(frame, f0, f1); - + cerr << "MainWindow::doubleClickSelectInvoked(" << frame << "): [" << f0 << "," << f1 << "]" << endl; Selection sel(f0, f1); @@ -2496,11 +2523,11 @@ MainWindow::regionOutlined(QRect r) sv_frame_t f0 = pane->getFrameForX(r.x()); sv_frame_t f1 = pane->getFrameForX(r.x() + r.width()); - + double v0 = spectrogram->getFrequencyForY(pane, r.y() + r.height()); double v1 = spectrogram->getFrequencyForY(pane, r.y()); - cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 + cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 << ", frequency " << v0 << " -> " << v1 << endl; m_pendingConstraint = Analyser::FrequencyRange(v0, v1); @@ -2589,7 +2616,7 @@ MainWindow::switchPitchDown() (tr("Choose Lower Pitch Candidate"), true); MultiSelection::SelectionList selections = m_viewManager->getSelections(); - + for (MultiSelection::SelectionList::iterator k = selections.begin(); k != selections.end(); ++k) { m_analyser->switchPitchCandidate(*k, false); @@ -2613,12 +2640,12 @@ MainWindow::snapNotesToPitches() CommandHistory::getInstance()->startCompoundOperation (tr("Snap Notes to Pitches"), true); - + for (MultiSelection::SelectionList::iterator k = selections.begin(); k != selections.end(); ++k) { auxSnapNotes(*k); } - + CommandHistory::getInstance()->endCompoundOperation(); } } @@ -2632,7 +2659,7 @@ MainWindow::auxSnapNotes(Selection s) if (!layer) return; layer->snapSelectedNotesToPitchTrack(m_analyser->getPane(), s); -} +} void MainWindow::splitNote() @@ -2657,12 +2684,12 @@ MainWindow::mergeNotes() CommandHistory::getInstance()->startCompoundOperation (tr("Merge Notes"), true); - + for (MultiSelection::SelectionList::iterator k = selections.begin(); k != selections.end(); ++k) { layer->mergeNotes(m_analyser->getPane(), *k, true); } - + CommandHistory::getInstance()->endCompoundOperation(); } } @@ -2680,12 +2707,12 @@ MainWindow::deleteNotes() CommandHistory::getInstance()->startCompoundOperation (tr("Delete Notes"), true); - + for (MultiSelection::SelectionList::iterator k = selections.begin(); k != selections.end(); ++k) { layer->deleteSelectionInclusive(*k); } - + CommandHistory::getInstance()->endCompoundOperation(); } } @@ -2703,7 +2730,7 @@ MainWindow::formNoteFromSelection() MultiSelection::SelectionList selections = m_viewManager->getSelections(); if (!selections.empty()) { - + CommandHistory::getInstance()->startCompoundOperation (tr("Form Note from Selection"), true); @@ -2717,7 +2744,7 @@ MainWindow::formNoteFromSelection() // existing pitch track if possible. This way we should // handle all the possible cases of existing notes that // may or may not overlap the start or end times - + sv_frame_t start = k->getStartFrame(); sv_frame_t end = k->getEndFrame(); @@ -2728,18 +2755,18 @@ MainWindow::formNoteFromSelection() if (!existing.empty()) { defaultPitch = int(roundf(existing.begin()->getValue())); } - + layer->splitNotesAt(pane, start); layer->splitNotesAt(pane, end); layer->deleteSelection(*k); - + layer->addNoteOn(start, defaultPitch, 100); layer->addNoteOff(end, defaultPitch); - + layer->mergeNotes(pane, *k, false); } - CommandHistory::getInstance()->endCompoundOperation(); + CommandHistory::getInstance()->endCompoundOperation(); } } @@ -2758,7 +2785,7 @@ MainWindow::playSpeedChanged(int position) char pcbuf[30]; char facbuf[30]; - + if (position == centre) { contextHelpChanged(tr("Playback speed: Normal")); } else if (position < centre) { @@ -2802,7 +2829,7 @@ MainWindow::playMonoToggled() playSpeedChanged(m_playSpeed->value()); // TODO: pitch gain? -} +} void MainWindow::speedUpPlayback() @@ -2841,7 +2868,7 @@ MainWindow::audioGainChanged(float gain) m_analyser->setGain(Analyser::Audio, gain); } updateMenuStates(); -} +} void MainWindow::pitchGainChanged(float gain) @@ -2856,7 +2883,7 @@ MainWindow::pitchGainChanged(float gain) m_analyser->setGain(Analyser::PitchTrack, gain); } updateMenuStates(); -} +} void MainWindow::notesGainChanged(float gain) @@ -2871,7 +2898,7 @@ MainWindow::notesGainChanged(float gain) m_analyser->setGain(Analyser::Notes, gain); } updateMenuStates(); -} +} void MainWindow::audioPanChanged(float pan) @@ -2879,7 +2906,7 @@ MainWindow::audioPanChanged(float pan) contextHelpChanged(tr("Audio Pan: %1").arg(pan)); m_analyser->setPan(Analyser::Audio, pan); updateMenuStates(); -} +} void MainWindow::pitchPanChanged(float pan) @@ -2887,7 +2914,7 @@ MainWindow::pitchPanChanged(float pan) contextHelpChanged(tr("Pitch Pan: %1").arg(pan)); m_analyser->setPan(Analyser::PitchTrack, pan); updateMenuStates(); -} +} void MainWindow::notesPanChanged(float pan) @@ -2895,7 +2922,7 @@ MainWindow::notesPanChanged(float pan) contextHelpChanged(tr("Notes Pan: %1").arg(pan)); m_analyser->setPan(Analyser::Notes, pan); updateMenuStates(); -} +} void MainWindow::updateVisibleRangeDisplay(Pane *p) const @@ -2944,7 +2971,7 @@ MainWindow::updateVisibleRangeDisplay(Pane *p) const m_myStatusMessage = tr("Visible: %1 to %2 (duration %3)") .arg(startStr).arg(endStr).arg(durationStr); } - + getStatusLabel()->setText(m_myStatusMessage); } @@ -3045,6 +3072,44 @@ MainWindow::analyseNow() } } +void +MainWindow::analyseDuringRecordingRunner() +{ + // analyseNow(); + // analyseDuringRecording(); + // QTimer::singleShot(5000, this, SLOT(analyseNow())); +} + +void +MainWindow::analyseDuringRecording() +{ + QSettings settings; + settings.beginGroup("Analyser"); + bool recordAnalyse = settings.value("record-analysis", true).toBool(); + settings.endGroup(); + if (recordAnalyse && this->m_recordTarget->isRecording()) + { + int duration_ms = 1000; + auto start_position = this->m_analysedFrames; + auto end_position = m_recordTarget->getRecordDuration(); + auto selection = Selection(start_position, end_position); + this->m_analysedFrames = end_position; + (tr("Analyse Audio"), true); + m_analyser->showPitchCandidates(true); + m_analyser->reAnalyseSelection( + selection, + Analyser::FrequencyRange()); + updateAnalyseStates(); + // TODO (alnovi): run analysis not by time but in the process of buffer filling + QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); + } + /* + TODO (alnovi): else { + analyse tail when recording is stopped + } + */ +} + void MainWindow::analyseNewMainModel() { @@ -3053,7 +3118,7 @@ MainWindow::analyseNewMainModel() SVDEBUG << "MainWindow::analyseNewMainModel: main model is " << model << endl; SVDEBUG << "(document is " << m_document << ", it says main model is " << m_document->getMainModel() << ")" << endl; - + if (!model) { cerr << "no main model!" << endl; return; @@ -3117,7 +3182,7 @@ MainWindow::analyseNewMainModel() m_analyser->setAudible(Analyser::PitchTrack, false); m_analyser->setAudible(Analyser::Notes, false); } - + updateLayerStatuses(); documentRestored(); } @@ -3265,16 +3330,16 @@ MainWindow::whatsNew() QDialog *d = new QDialog(this); d->setWindowTitle(tr("What's New")); - + QGridLayout *layout = new QGridLayout; d->setLayout(layout); int row = 0; - + QLabel *iconLabel = new QLabel; iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64)); layout->addWidget(iconLabel, row, 0); - + layout->addWidget (new QLabel(tr("

What's New in %1

") .arg(QApplication::applicationName())), @@ -3287,7 +3352,7 @@ MainWindow::whatsNew() if (m_newerVersionIs != "") { layout->addWidget(new QLabel(tr("Note: A newer version of %1 is available.
(Version %2 is available; you are using version %3)").arg(QApplication::applicationName()).arg(m_newerVersionIs).arg(TONY_VERSION)), row++, 1, 1, 2); } - + QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok); layout->addWidget(bb, row++, 0, 1, 3); connect(bb, SIGNAL(accepted()), d, SLOT(accept())); @@ -3316,7 +3381,7 @@ MainWindow::whatsNew() d->setMinimumSize(m_viewManager->scalePixelSize(520), m_viewManager->scalePixelSize(450)); - + d->exec(); delete d; @@ -3360,7 +3425,7 @@ MainWindow::about() aboutText += tr("

Using Qt framework version %1.

") .arg(QT_VERSION_STR); - aboutText += + aboutText += "

Copyright © 2005–2019 Chris Cannam, Queen Mary University of London, and the Tony project authors: Matthias Mauch, George Fazekas, Justin Salamon, and Rachel Bittner.

" "

pYIN analysis plugin written by Matthias Mauch.

" "

This program is free software; you can redistribute it and/or " @@ -3368,18 +3433,18 @@ MainWindow::about() "published by the Free Software Foundation; either version 2 of the " "License, or (at your option) any later version.
See the file " "COPYING included with this distribution for more information.

"; - + // use our own dialog so we can influence the size QDialog *d = new QDialog(this); d->setWindowTitle(tr("About %1").arg(QApplication::applicationName())); - + QGridLayout *layout = new QGridLayout; d->setLayout(layout); int row = 0; - + QLabel *iconLabel = new QLabel; iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64)); layout->addWidget(iconLabel, row, 0, Qt::AlignTop); @@ -3402,7 +3467,7 @@ MainWindow::about() d->setMinimumSize(m_viewManager->scalePixelSize(420), m_viewManager->scalePixelSize(200)); - + d->exec(); delete d; @@ -3418,7 +3483,7 @@ void MainWindow::newerVersionAvailable(QString version) { m_newerVersionIs = version; - + //!!! nicer URL would be nicer QSettings settings; settings.beginGroup("NewerVersionWarning"); @@ -3442,9 +3507,9 @@ MainWindow::ffwd() sv_samplerate_t sr = getMainModel()->getSampleRate(); - // The step is supposed to scale and be as wide as a step of + // The step is supposed to scale and be as wide as a step of // m_defaultFfwdRwdStep seconds at zoom level 720 and sr = 44100 - + ZoomLevel zoom = m_viewManager->getGlobalZoom(); double framesPerPixel = 1.0; if (zoom.zone == ZoomLevel::FramesPerPixel) { @@ -3455,20 +3520,20 @@ MainWindow::ffwd() double defaultFramesPerPixel = (720 * 44100) / sr; double scaler = framesPerPixel / defaultFramesPerPixel; RealTime step = m_defaultFfwdRwdStep * scaler; - + frame = RealTime::realTime2Frame (RealTime::frame2RealTime(frame, sr) + step, sr); if (frame > getMainModel()->getEndFrame()) { frame = getMainModel()->getEndFrame(); } - + if (frame < 0) frame = 0; if (m_viewManager->getPlaySelectionMode()) { frame = m_viewManager->constrainFrameToSelection(frame); } - + m_viewManager->setPlaybackFrame(frame); if (frame == getMainModel()->getEndFrame() && @@ -3489,7 +3554,7 @@ MainWindow::rewind() sv_samplerate_t sr = getMainModel()->getSampleRate(); - // The step is supposed to scale and be as wide as a step of + // The step is supposed to scale and be as wide as a step of // m_defaultFfwdRwdStep seconds at zoom level 720 and sr = 44100 ZoomLevel zoom = m_viewManager->getGlobalZoom(); @@ -3505,7 +3570,7 @@ MainWindow::rewind() frame = RealTime::realTime2Frame (RealTime::frame2RealTime(frame, sr) - step, sr); - + if (frame < getMainModel()->getStartFrame()) { frame = getMainModel()->getStartFrame(); } diff --git a/main/MainWindow.h b/main/MainWindow.h index f14d5ca..da7f605 100644 --- a/main/MainWindow.h +++ b/main/MainWindow.h @@ -1,3 +1,4 @@ +#pragma once /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ /* @@ -5,7 +6,7 @@ An intonation analysis and annotation tool Centre for Digital Music, Queen Mary, University of London. This file copyright 2006-2012 Chris Cannam and QMUL. - + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the @@ -31,7 +32,7 @@ class MainWindow : public sv::MainWindowBase public: MainWindow(AudioMode audioMode, - bool withSonification = true, + bool withSonification = true, bool withSpectrogram = true); virtual ~MainWindow(); @@ -87,7 +88,12 @@ protected slots: virtual void editDisplayExtents(); virtual void analyseNow(); + + virtual void analyseDuringRecording(); + virtual void analyseDuringRecordingRunner(); virtual void resetAnalyseOptions(); + virtual void recordAnalysisToggled(); + virtual void autoAnalysisToggled(); virtual void precisionAnalysisToggled(); virtual void lowampAnalysisToggled(); @@ -164,7 +170,7 @@ protected slots: virtual void whatsNew(); virtual void betaReleaseWarning(); - + virtual void newerVersionAvailable(QString); virtual void selectionChangedByUser(); @@ -205,11 +211,12 @@ protected slots: bool m_intelligentActionOn; // GF: !!! temporary QAction *m_autoAnalyse; + QAction *m_analyseDuringRecord; QAction *m_precise; QAction *m_lowamp; QAction *m_onset; QAction *m_prune; - + QAction *m_showAudio; QAction *m_showSpect; QAction *m_showPitch; @@ -224,6 +231,9 @@ protected slots: sv::ActivityLog *m_activityLog; sv::KeyReference *m_keyReference; sv::VersionTester *m_versionTester; + + sv_frame_t m_analysedFrames = 0; + QString m_newerVersionIs; sv::sv_frame_t m_selectionAnchor; From 5b734da39faed7889969fcfc664583d0cd868574 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Tue, 9 Jul 2024 13:36:48 +0300 Subject: [PATCH 02/24] note recognition works --- main/Analyser.cpp | 172 ++++++++++++++++++++++++++++++++++++++++++++ main/Analyser.h | 10 ++- main/MainWindow.cpp | 29 ++++---- 3 files changed, 194 insertions(+), 17 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 9669057..44d5d33 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -126,6 +126,21 @@ Analyser::analyseExistingFile() return doAllAnalyses(true); } +QString +Analyser::analyseRecordingFileToTheEnd(Selection analysingSelection) +{ + if (!m_document) return "Internal error: Analyser::analyseExistingFile() called with no document present"; + + if (!m_pane) return "Internal error: Analyser::analyseExistingFile() called with no pane present"; + + if (m_fileModel.isNone()) return "Internal error: Analyser::analyseExistingFile() called with no model present"; + + this->showPitchCandidates(true); + this->analyseRecording(analysingSelection); + + return ""; +} + QString Analyser::doAllAnalyses(bool withPitchTrack) { @@ -544,6 +559,153 @@ Analyser::materialiseReAnalysis() switchPitchCandidate(m_reAnalysingSelection, true); // or false, doesn't matter } +QString +Analyser::analyseRecording(Selection sel) +{ + QMutexLocker locker(&m_asyncMutex); + + FrequencyRange range = FrequencyRange(); + + auto waveFileModel = ModelById::getAs(m_fileModel); + if (!waveFileModel) { + return "Internal error: Analyser::reAnalyseSelection() called with no model present"; + } + + if (!m_reAnalysingSelection.isEmpty()) { + if (sel == m_reAnalysingSelection && range == m_reAnalysingRange) { + cerr << "selection & range are same as current analysis, ignoring" << endl; + return ""; + } + } + + if (sel.isEmpty()) return ""; + + m_reAnalysingSelection = sel; + m_reAnalysingRange = range; + + m_preAnalysis = Clipboard(); + Layer* myLayer = m_layers[PitchTrack]; + if (myLayer) { + myLayer->copy(m_pane, sel, m_preAnalysis); + } + + TransformFactory* tf = TransformFactory::getInstance(); + + QString plugname1 = "pYIN"; + + QString base = "vamp:pyin:pyin:"; + QString f0out = "smoothedpitchtrack"; + QString noteout = "notes"; + + Transforms transforms; + + QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); + if (!tf->haveTransform(base + f0out)) { + return notFound.arg(base + f0out).arg(plugname1); + } + + if (!tf->haveTransform(base + noteout)) { + return notFound.arg(base + noteout).arg(plugname1); + } + + Transform t = tf->getDefaultTransformFor + (base + f0out, waveFileModel->getSampleRate()); + t.setStepSize(256); + t.setBlockSize(2048); + + if (range.isConstrained()) { + t.setParameter("minfreq", float(range.min)); + t.setParameter("maxfreq", float(range.max)); + t.setBlockSize(4096); + } + + // get time stamps that align with the 256-sample grid of the original extraction + const sv_frame_t grid = 256; + sv_frame_t startSample = (sel.getStartFrame() / grid) * grid; + if (startSample < sel.getStartFrame()) startSample += grid; + sv_frame_t endSample = (sel.getEndFrame() / grid) * grid; + if (endSample < sel.getEndFrame()) endSample += grid; + if (!range.isConstrained()) { + startSample -= 4 * grid; // 4*256 is for 4 frames offset due to timestamp shift + endSample -= 4 * grid; + } + else { + endSample -= 9 * grid; // MM says: not sure what the CHP plugin does there + } + RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); + RealTime end = RealTime::frame2RealTime(endSample, waveFileModel->getSampleRate()); + + RealTime duration; + + if (sel.getEndFrame() > sel.getStartFrame()) { + duration = end - start; + } + + cerr << "Analyser::reAnalyseSelection: start " << start << " end " << end << " original selection start " << sel.getStartFrame() << " end " << sel.getEndFrame() << " duration " << duration << endl; + + if (duration <= RealTime::zeroTime) { + cerr << "Analyser::reAnalyseSelection: duration <= 0, not analysing" << endl; + return ""; + } + + t.setStartTime(start); + t.setDuration(duration); + + transforms.push_back(t); + + t.setOutput(noteout); + + transforms.push_back(t); + + std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); + + for (int i = 0; i < (int)layers.size(); ++i) { + + FlexiNoteLayer* f = qobject_cast(layers[i]); + TimeValueLayer* t = qobject_cast(layers[i]); + + if (f) m_layers[Notes] = f; + if (t) m_layers[PitchTrack] = t; + + m_document->addLayerToView(m_pane, layers[i]); + } + + ColourDatabase* cdb = ColourDatabase::getInstance(); + + TimeValueLayer* pitchLayer = + qobject_cast(m_layers[PitchTrack]); + if (pitchLayer) { + pitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); + auto params = pitchLayer->getPlayParameters(); + if (params) { + params->setPlayPan(1); + params->setPlayGain(0.5); + } + connect(pitchLayer, SIGNAL(modelCompletionChanged(ModelId)), + this, SLOT(layerCompletionChanged(ModelId))); + } + + FlexiNoteLayer* flexiNoteLayer = + qobject_cast(m_layers[Notes]); + if (flexiNoteLayer) { + flexiNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); + auto params = flexiNoteLayer->getPlayParameters(); + if (params) { + params->setPlayPan(1); + params->setPlayGain(0.5); + } + connect(flexiNoteLayer, SIGNAL(modelCompletionChanged(ModelId)), + this, SLOT(layerCompletionChanged(ModelId))); + connect(flexiNoteLayer, SIGNAL(reAnalyseRegion(sv_frame_t, sv_frame_t, float, float)), + this, SLOT(reAnalyseRegion(sv_frame_t, sv_frame_t, float, float))); + connect(flexiNoteLayer, SIGNAL(materialiseReAnalysis()), + this, SLOT(materialiseReAnalysis())); + } + + return ""; +} + + QString Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) { @@ -590,6 +752,8 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) QString base = "vamp:pyin:localcandidatepyin:"; QString out = "pitchtrackcandidates"; + QString f0out = "smoothedpitchtrack"; + QString noteout = "notes"; if (range.isConstrained()) { base = "vamp:chp:constrainedharmonicpeak:"; @@ -603,6 +767,14 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) return notFound.arg(base + out).arg(plugname1).arg(plugname2); } + if (!tf->haveTransform(base + f0out)) { + return notFound.arg(base + f0out).arg(plugname1).arg(plugname2); + } + + if (!tf->haveTransform(base + noteout)) { + return notFound.arg(base + noteout).arg(plugname1).arg(plugname2); + } + Transform t = tf->getDefaultTransformFor (base + out, waveFileModel->getSampleRate()); t.setStepSize(256); diff --git a/main/Analyser.h b/main/Analyser.h index 2db3463..b53f4e0 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -56,7 +56,8 @@ class Analyser : public QObject, // layers; return "" on success or error string on failure QString analyseExistingFile(); - QString doAllAnalyses(bool withPitchTrack); + // Completes analysis from the last position to the end + QString analyseRecordingFileToTheEnd(Selection analysingSelection); // Discard any layers etc associated with the current document void fileClosed(); @@ -131,6 +132,13 @@ class Analyser : public QObject, */ static std::map getAnalysisSettings(); + /** + * Analyse the selection and schedule asynchronous adds of + * candidate layers for the region it contains. Returns "" on + * success or a user-readable error string on failure. + */ + QString analyseRecording(Selection sel); + /** * Analyse the selection and schedule asynchronous adds of * candidate layers for the region it contains. Returns "" on diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 879ef48..30e7e0e 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -848,6 +848,9 @@ MainWindow::updateAnalyseStates() bool autoAnalyse = settings.value("auto-analysis", true).toBool(); m_autoAnalyse->setChecked(autoAnalyse); + bool analyseDuringRecord = settings.value("record-analysis", true).toBool(); + m_analyseDuringRecord->setChecked(analyseDuringRecord); + std::map actions { { "precision-analysis", m_precise }, { "lowamp-analysis", m_lowamp }, @@ -3075,9 +3078,8 @@ MainWindow::analyseNow() void MainWindow::analyseDuringRecordingRunner() { - // analyseNow(); - // analyseDuringRecording(); - // QTimer::singleShot(5000, this, SLOT(analyseNow())); + this->m_analysedFrames = 0; + analyseDuringRecording(); } void @@ -3087,7 +3089,7 @@ MainWindow::analyseDuringRecording() settings.beginGroup("Analyser"); bool recordAnalyse = settings.value("record-analysis", true).toBool(); settings.endGroup(); - if (recordAnalyse && this->m_recordTarget->isRecording()) + if ((recordAnalyse && this->m_recordTarget->isRecording()) || this->m_analysedFrames != 0) { int duration_ms = 1000; auto start_position = this->m_analysedFrames; @@ -3095,19 +3097,14 @@ MainWindow::analyseDuringRecording() auto selection = Selection(start_position, end_position); this->m_analysedFrames = end_position; (tr("Analyse Audio"), true); - m_analyser->showPitchCandidates(true); - m_analyser->reAnalyseSelection( - selection, - Analyser::FrequencyRange()); - updateAnalyseStates(); - // TODO (alnovi): run analysis not by time but in the process of buffer filling - QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); - } - /* - TODO (alnovi): else { - analyse tail when recording is stopped + + m_analyser->analyseRecordingFileToTheEnd(selection); + + if (this->m_recordTarget->isRecording()) { + // TODO (alnovi): run analysis not by time but in the process of buffer filling + QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); } - */ + } } void From 40eef44eefc22d5870b33102914aaa83f9bac3ea Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Tue, 9 Jul 2024 14:15:31 +0300 Subject: [PATCH 03/24] use last event for analisys --- main/MainWindow.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 30e7e0e..b9295cf 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3092,7 +3092,16 @@ MainWindow::analyseDuringRecording() if ((recordAnalyse && this->m_recordTarget->isRecording()) || this->m_analysedFrames != 0) { int duration_ms = 1000; - auto start_position = this->m_analysedFrames; + FlexiNoteLayer* layer = + qobject_cast(m_analyser->getLayer(Analyser::Notes)); + auto model = ModelById::getAs(layer->getModel()); + auto all_events = model->getAllEvents(); + auto start_position = m_analysedFrames; + if (!all_events.empty()) { + auto& last = all_events.back(); + start_position = last.getFrame(); + model->remove(last); + } auto end_position = m_recordTarget->getRecordDuration(); auto selection = Selection(start_position, end_position); this->m_analysedFrames = end_position; From d52f3e6147798f352e62651ef7db7d5278ef2817 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Tue, 9 Jul 2024 16:27:29 +0300 Subject: [PATCH 04/24] merge notes --- main/MainWindow.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index b9295cf..eb4f3ee 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3096,11 +3096,10 @@ MainWindow::analyseDuringRecording() qobject_cast(m_analyser->getLayer(Analyser::Notes)); auto model = ModelById::getAs(layer->getModel()); auto all_events = model->getAllEvents(); - auto start_position = m_analysedFrames; + auto start_position = std::max(m_analysedFrames - 1, (long long)0); + size_t prevAllEventsSize = 0; if (!all_events.empty()) { - auto& last = all_events.back(); - start_position = last.getFrame(); - model->remove(last); + prevAllEventsSize = all_events.size(); } auto end_position = m_recordTarget->getRecordDuration(); auto selection = Selection(start_position, end_position); @@ -3109,6 +3108,17 @@ MainWindow::analyseDuringRecording() m_analyser->analyseRecordingFileToTheEnd(selection); + if (prevAllEventsSize != 0 && all_events.size() > prevAllEventsSize) { + auto& prevEvent = all_events[prevAllEventsSize - 1]; + auto& nextEvent = all_events[prevAllEventsSize]; + + if (nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { + // merge events + model->add(prevEvent.withDuration(prevEvent.getDuration() + nextEvent.getDuration())); + model->remove(prevEvent); + model->remove(nextEvent); + } + } if (this->m_recordTarget->isRecording()) { // TODO (alnovi): run analysis not by time but in the process of buffer filling QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); From 26e1126f3bb1bfab647130139dcd7cca60206d32 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Wed, 10 Jul 2024 13:52:55 +0300 Subject: [PATCH 05/24] layer creation comment --- main/Analyser.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 44d5d33..17db173 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -702,6 +702,39 @@ Analyser::analyseRecording(Selection sel) this, SLOT(materialiseReAnalysis())); } + // TODO (alnovi): instead of creation of new layers, subscribe on succeeded transformation completions and copy/paste + // from new layers to existing ones: + + /* + for (int i = 0; i < (int)layers.size(); ++i) { + + FlexiNoteLayer* f = qobject_cast(layers[i]); + TimeValueLayer* t = qobject_cast(layers[i]); + + if (f) { + auto clipboard = Clipboard(); + + auto fModel = ModelById::getAs(f->getModel()); + auto nModel = ModelById::getAs(noteTrack->getModel()); + + if (!fModel->getAllEvents().empty()) { + f->copy(m_pane, sel, clipboard); + noteTrack->paste(m_pane, clipboard, 0, false); + } + } + + if (t) { + auto params = t->getPlayParameters(); + if (params) { + params->setPlayAudible(false); + } + + auto clipboard = Clipboard(); + t->copy(m_pane, sel, clipboard); + pitchTrack->paste(m_pane, clipboard, 0, false); + } + } + */ return ""; } From 3f84fe963bbea02156b5ce62c01af7c5646a24fd Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Wed, 10 Jul 2024 17:14:03 +0300 Subject: [PATCH 06/24] working copy/paste --- main/Analyser.cpp | 128 ++++++++++++++++++++++---------------------- main/Analyser.h | 2 + main/MainWindow.cpp | 24 ++------- 3 files changed, 70 insertions(+), 84 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 17db173..5c011d2 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -360,6 +360,16 @@ Analyser::addWaveform() return ""; } +void +Analyser::updatePitchTrack(ModelId) { + +} + +void +Analyser::updateNoteLayer(ModelId) { + +} + QString Analyser::addAnalyses() { @@ -584,10 +594,11 @@ Analyser::analyseRecording(Selection sel) m_reAnalysingRange = range; m_preAnalysis = Clipboard(); - Layer* myLayer = m_layers[PitchTrack]; - if (myLayer) { - myLayer->copy(m_pane, sel, m_preAnalysis); + Layer* pitchLayer = m_layers[PitchTrack]; + if (pitchLayer) { + pitchLayer->copy(m_pane, sel, m_preAnalysis); } + Layer* noteLayer = m_layers[Notes]; TransformFactory* tf = TransformFactory::getInstance(); @@ -659,82 +670,71 @@ Analyser::analyseRecording(Selection sel) std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); - for (int i = 0; i < (int)layers.size(); ++i) { + ColourDatabase* cdb = ColourDatabase::getInstance(); - FlexiNoteLayer* f = qobject_cast(layers[i]); - TimeValueLayer* t = qobject_cast(layers[i]); + for (auto* layer : layers) { - if (f) m_layers[Notes] = f; - if (t) m_layers[PitchTrack] = t; + FlexiNoteLayer* tempNoteLayer = qobject_cast(layer); + TimeValueLayer* tempPitchLayer = qobject_cast(layer); - m_document->addLayerToView(m_pane, layers[i]); - } + if (tempPitchLayer) { + tempPitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); - ColourDatabase* cdb = ColourDatabase::getInstance(); + connect(tempPitchLayer, &TimeValueLayer::modelCompletionChanged, [tempPitchLayer, pitchLayer](ModelId modelId) { + auto model = ModelById::getAs(modelId); - TimeValueLayer* pitchLayer = - qobject_cast(m_layers[PitchTrack]); - if (pitchLayer) { - pitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); - auto params = pitchLayer->getPlayParameters(); - if (params) { - params->setPlayPan(1); - params->setPlayGain(0.5); - } - connect(pitchLayer, SIGNAL(modelCompletionChanged(ModelId)), - this, SLOT(layerCompletionChanged(ModelId))); - } + if (model->getCompletion() == 100) { + auto toModel = ModelById::getAs(pitchLayer->getModel()); - FlexiNoteLayer* flexiNoteLayer = - qobject_cast(m_layers[Notes]); - if (flexiNoteLayer) { - flexiNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); - auto params = flexiNoteLayer->getPlayParameters(); - if (params) { - params->setPlayPan(1); - params->setPlayGain(0.5); - } - connect(flexiNoteLayer, SIGNAL(modelCompletionChanged(ModelId)), - this, SLOT(layerCompletionChanged(ModelId))); - connect(flexiNoteLayer, SIGNAL(reAnalyseRegion(sv_frame_t, sv_frame_t, float, float)), - this, SLOT(reAnalyseRegion(sv_frame_t, sv_frame_t, float, float))); - connect(flexiNoteLayer, SIGNAL(materialiseReAnalysis()), - this, SLOT(materialiseReAnalysis())); - } + EventVector points = model->getAllEvents(); - // TODO (alnovi): instead of creation of new layers, subscribe on succeeded transformation completions and copy/paste - // from new layers to existing ones: + for (Event p : points) { + toModel->add(p); + } - /* - for (int i = 0; i < (int)layers.size(); ++i) { + disconnect(tempPitchLayer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); + + //TODO (alnovi): remove the layer. + } + }); + } + + if (tempNoteLayer) { + tempNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); + + connect(tempNoteLayer, &TimeValueLayer::modelCompletionChanged, [tempNoteLayer, noteLayer](ModelId modelId) { + auto model = ModelById::getAs(modelId); + + if (model->getCompletion() == 100) { - FlexiNoteLayer* f = qobject_cast(layers[i]); - TimeValueLayer* t = qobject_cast(layers[i]); + auto toModel = ModelById::getAs(noteLayer->getModel()); - if (f) { - auto clipboard = Clipboard(); + EventVector points = model->getAllEvents(); - auto fModel = ModelById::getAs(f->getModel()); - auto nModel = ModelById::getAs(noteTrack->getModel()); + auto all_events = toModel->getAllEvents(); + if (!all_events.empty() && !points.empty()) { + auto& prevEvent = all_events.back(); + auto& nextEvent = points.front(); - if (!fModel->getAllEvents().empty()) { - f->copy(m_pane, sel, clipboard); - noteTrack->paste(m_pane, clipboard, 0, false); - } - } + // Merge events + if (nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { + points[0] = prevEvent.withDuration(prevEvent.getDuration() + nextEvent.getDuration()); + toModel->remove(prevEvent); + } + } + + for (Event p : points) { + toModel->add(p); + } - if (t) { - auto params = t->getPlayParameters(); - if (params) { - params->setPlayAudible(false); - } + disconnect(tempNoteLayer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); - auto clipboard = Clipboard(); - t->copy(m_pane, sel, clipboard); - pitchTrack->paste(m_pane, clipboard, 0, false); - } + //TODO (alnovi): remove the layer. + } + }); + } } - */ + return ""; } diff --git a/main/Analyser.h b/main/Analyser.h index b53f4e0..51dac15 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -242,6 +242,8 @@ class Analyser : public QObject, void initialAnalysisCompleted(); protected slots: + void updatePitchTrack(ModelId); + void updateNoteLayer(ModelId); void layerAboutToBeDeleted(sv::Layer *); void layerCompletionChanged(sv::ModelId); void reAnalyseRegion(sv::sv_frame_t, sv::sv_frame_t, float, float); diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index eb4f3ee..0da7485 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3092,15 +3092,10 @@ MainWindow::analyseDuringRecording() if ((recordAnalyse && this->m_recordTarget->isRecording()) || this->m_analysedFrames != 0) { int duration_ms = 1000; - FlexiNoteLayer* layer = - qobject_cast(m_analyser->getLayer(Analyser::Notes)); - auto model = ModelById::getAs(layer->getModel()); - auto all_events = model->getAllEvents(); - auto start_position = std::max(m_analysedFrames - 1, (long long)0); - size_t prevAllEventsSize = 0; - if (!all_events.empty()) { - prevAllEventsSize = all_events.size(); - } + + // We start with a 100-frame overlap to ensure we capture attacks in time + sv_frame_t overlap = 100; + auto start_position = std::max(m_analysedFrames - overlap, 0LL); auto end_position = m_recordTarget->getRecordDuration(); auto selection = Selection(start_position, end_position); this->m_analysedFrames = end_position; @@ -3108,17 +3103,6 @@ MainWindow::analyseDuringRecording() m_analyser->analyseRecordingFileToTheEnd(selection); - if (prevAllEventsSize != 0 && all_events.size() > prevAllEventsSize) { - auto& prevEvent = all_events[prevAllEventsSize - 1]; - auto& nextEvent = all_events[prevAllEventsSize]; - - if (nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { - // merge events - model->add(prevEvent.withDuration(prevEvent.getDuration() + nextEvent.getDuration())); - model->remove(prevEvent); - model->remove(nextEvent); - } - } if (this->m_recordTarget->isRecording()) { // TODO (alnovi): run analysis not by time but in the process of buffer filling QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); From d74f5671bd25da95fd40fa50c92e835177634f57 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Wed, 10 Jul 2024 17:59:05 +0300 Subject: [PATCH 07/24] templates --- main/Analyser.cpp | 114 ++++++++++++++++++++++---------------------- main/MainWindow.cpp | 2 +- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 5c011d2..5148c5b 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -569,6 +569,51 @@ Analyser::materialiseReAnalysis() switchPitchCandidate(m_reAnalysingSelection, true); // or false, doesn't matter } +template +void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* cdb) { + layer->setBaseColour(cdb->getColourIndex(colourName)); +} + +// Generalized helper function to process layers +template +void processLayer(LayerType* layer, LayerType* targetLayer, std::function, std::shared_ptr)> customProcessing) { + QObject::connect(layer, &TimeValueLayer::modelCompletionChanged, [layer, targetLayer, customProcessing](ModelId modelId) { + auto model = ModelById::getAs(modelId); + + if (model->getCompletion() == 100) { + auto toModel = ModelById::getAs(targetLayer->getModel()); + EventVector points = model->getAllEvents(); + + // Custom processing logic + customProcessing(model, toModel); + + for (const Event& p : points) { + toModel->add(p); + } + + QObject::disconnect(layer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); + // TODO (alnovi): remove the layer. + } + }); +} + +// Custom processing logic for FlexiNoteLayer +void processNoteModel(std::shared_ptr model, std::shared_ptr toModel) { + auto allEvents = toModel->getAllEvents(); + auto points = model->getAllEvents(); + + if (!allEvents.empty() && !points.empty()) { + auto& prevEvent = allEvents.back(); + auto& nextEvent = points.front(); + + // Merge events + if (nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { + points[0] = prevEvent.withDuration(prevEvent.getDuration() + nextEvent.getDuration()); + toModel->remove(prevEvent); + } + } +} + QString Analyser::analyseRecording(Selection sel) { @@ -594,11 +639,11 @@ Analyser::analyseRecording(Selection sel) m_reAnalysingRange = range; m_preAnalysis = Clipboard(); - Layer* pitchLayer = m_layers[PitchTrack]; + auto* pitchLayer = qobject_cast(m_layers[PitchTrack]); if (pitchLayer) { pitchLayer->copy(m_pane, sel, m_preAnalysis); } - Layer* noteLayer = m_layers[Notes]; + auto* noteLayer = qobject_cast(m_layers[Notes]); TransformFactory* tf = TransformFactory::getInstance(); @@ -677,62 +722,17 @@ Analyser::analyseRecording(Selection sel) FlexiNoteLayer* tempNoteLayer = qobject_cast(layer); TimeValueLayer* tempPitchLayer = qobject_cast(layer); - if (tempPitchLayer) { - tempPitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); - - connect(tempPitchLayer, &TimeValueLayer::modelCompletionChanged, [tempPitchLayer, pitchLayer](ModelId modelId) { - auto model = ModelById::getAs(modelId); - - if (model->getCompletion() == 100) { - auto toModel = ModelById::getAs(pitchLayer->getModel()); - - EventVector points = model->getAllEvents(); - - for (Event p : points) { - toModel->add(p); - } - - disconnect(tempPitchLayer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); - - //TODO (alnovi): remove the layer. - } - }); - } - - if (tempNoteLayer) { - tempNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); - - connect(tempNoteLayer, &TimeValueLayer::modelCompletionChanged, [tempNoteLayer, noteLayer](ModelId modelId) { - auto model = ModelById::getAs(modelId); - - if (model->getCompletion() == 100) { - - auto toModel = ModelById::getAs(noteLayer->getModel()); - - EventVector points = model->getAllEvents(); - - auto all_events = toModel->getAllEvents(); - if (!all_events.empty() && !points.empty()) { - auto& prevEvent = all_events.back(); - auto& nextEvent = points.front(); - - // Merge events - if (nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { - points[0] = prevEvent.withDuration(prevEvent.getDuration() + nextEvent.getDuration()); - toModel->remove(prevEvent); - } - } - - for (Event p : points) { - toModel->add(p); - } - - disconnect(tempNoteLayer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); + if (tempPitchLayer) { + setBaseColour(tempPitchLayer, tr("Black"), cdb); + processLayer(tempPitchLayer, pitchLayer, [](std::shared_ptr, std::shared_ptr) { + // No additional custom processing needed for TimeValueLayer + }); + } - //TODO (alnovi): remove the layer. - } - }); - } + if (tempNoteLayer) { + setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); + processLayer(tempNoteLayer, noteLayer, processNoteModel); + } } return ""; diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 0da7485..cfead35 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3091,7 +3091,7 @@ MainWindow::analyseDuringRecording() settings.endGroup(); if ((recordAnalyse && this->m_recordTarget->isRecording()) || this->m_analysedFrames != 0) { - int duration_ms = 1000; + int duration_ms = 250; // We start with a 100-frame overlap to ensure we capture attacks in time sv_frame_t overlap = 100; From 0b0d9e4932d248c3018b561d31dbcecee33e4b92 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 11 Jul 2024 11:39:04 +0300 Subject: [PATCH 08/24] bind processing with timeframes --- main/Analyser.cpp | 20 ++++++++++++++------ main/MainWindow.cpp | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 5148c5b..fd88312 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -576,7 +576,7 @@ void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* // Generalized helper function to process layers template -void processLayer(LayerType* layer, LayerType* targetLayer, std::function, std::shared_ptr)> customProcessing) { +void processLayer(LayerType* layer, LayerType* targetLayer, std::function, std::shared_ptr)> customProcessing) { QObject::connect(layer, &TimeValueLayer::modelCompletionChanged, [layer, targetLayer, customProcessing](ModelId modelId) { auto model = ModelById::getAs(modelId); @@ -585,7 +585,7 @@ void processLayer(LayerType* layer, LayerType* targetLayer, std::functiongetAllEvents(); // Custom processing logic - customProcessing(model, toModel); + points = customProcessing(model, toModel); for (const Event& p : points) { toModel->add(p); @@ -598,10 +598,15 @@ void processLayer(LayerType* layer, LayerType* targetLayer, std::function model, std::shared_ptr toModel) { +static EventVector processNoteModel(sv_frame_t context_start, std::shared_ptr model, std::shared_ptr toModel) { auto allEvents = toModel->getAllEvents(); auto points = model->getAllEvents(); + // Vamp doesn't add current timestamp for note features, so, do it manually + std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { + return point.withFrame(point.getFrame() + context_start); + }); + if (!allEvents.empty() && !points.empty()) { auto& prevEvent = allEvents.back(); auto& nextEvent = points.front(); @@ -612,6 +617,8 @@ void processNoteModel(std::shared_ptr model, std::shared_ptrremove(prevEvent); } } + + return points; } QString @@ -724,14 +731,15 @@ Analyser::analyseRecording(Selection sel) if (tempPitchLayer) { setBaseColour(tempPitchLayer, tr("Black"), cdb); - processLayer(tempPitchLayer, pitchLayer, [](std::shared_ptr, std::shared_ptr) { + processLayer(tempPitchLayer, pitchLayer, [](std::shared_ptr model, std::shared_ptr) { // No additional custom processing needed for TimeValueLayer - }); + return model->getAllEvents(); + }); } if (tempNoteLayer) { setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); - processLayer(tempNoteLayer, noteLayer, processNoteModel); + processLayer(tempNoteLayer, noteLayer, std::bind(processNoteModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); } } diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index cfead35..45fb44f 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3094,7 +3094,7 @@ MainWindow::analyseDuringRecording() int duration_ms = 250; // We start with a 100-frame overlap to ensure we capture attacks in time - sv_frame_t overlap = 100; + sv_frame_t overlap = 0; auto start_position = std::max(m_analysedFrames - overlap, 0LL); auto end_position = m_recordTarget->getRecordDuration(); auto selection = Selection(start_position, end_position); From 9e9d74078f5c49e184e5fa7a18335cbd20a2fbf3 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 11 Jul 2024 12:22:41 +0300 Subject: [PATCH 09/24] working prototype --- main/Analyser.cpp | 35 ++++++++++++++++++----------------- main/Analyser.h | 3 ++- main/MainWindow.cpp | 31 +++++++++++-------------------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index fd88312..9d7bb2a 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -127,7 +127,7 @@ Analyser::analyseExistingFile() } QString -Analyser::analyseRecordingFileToTheEnd(Selection analysingSelection) +Analyser::analyseRecordingToEnd(sv_frame_t record_duration) { if (!m_document) return "Internal error: Analyser::analyseExistingFile() called with no document present"; @@ -135,9 +135,16 @@ Analyser::analyseRecordingFileToTheEnd(Selection analysingSelection) if (m_fileModel.isNone()) return "Internal error: Analyser::analyseExistingFile() called with no model present"; - this->showPitchCandidates(true); + // We start with a 5000-frame overlap to ensure we capture attacks in time (~113ms) + sv_frame_t overlap = 5000; + auto start_position = std::max(m_analysedFrames - overlap, 0LL); + auto end_position = record_duration; + Selection analysingSelection = Selection(start_position, end_position); + this->analyseRecording(analysingSelection); + m_analysedFrames = end_position; + return ""; } @@ -360,16 +367,6 @@ Analyser::addWaveform() return ""; } -void -Analyser::updatePitchTrack(ModelId) { - -} - -void -Analyser::updateNoteLayer(ModelId) { - -} - QString Analyser::addAnalyses() { @@ -598,9 +595,9 @@ void processLayer(LayerType* layer, LayerType* targetLayer, std::function model, std::shared_ptr toModel) { +static EventVector processNoteModel(sv_frame_t context_start, std::shared_ptr fromModel, std::shared_ptr toModel) { auto allEvents = toModel->getAllEvents(); - auto points = model->getAllEvents(); + auto points = fromModel->getAllEvents(); // Vamp doesn't add current timestamp for note features, so, do it manually std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { @@ -611,9 +608,13 @@ static EventVector processNoteModel(sv_frame_t context_start, std::shared_ptr prevEvent.getFrame()) { + auto overallDuration = prevEvent.getDuration() + nextEvent.getDuration(); + auto overlapDuration = prevEvent.getFrame() + prevEvent.getDuration() - nextEvent.getFrame(); + points[0] = prevEvent.withDuration(overallDuration - overlapDuration); + + // TODO (alnovi): remove all events from toModel which ends after context_start toModel->remove(prevEvent); } } diff --git a/main/Analyser.h b/main/Analyser.h index 51dac15..3132c8b 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -57,7 +57,7 @@ class Analyser : public QObject, QString analyseExistingFile(); // Completes analysis from the last position to the end - QString analyseRecordingFileToTheEnd(Selection analysingSelection); + QString analyseRecordingToEnd(sv_frame_t record_duration); // Discard any layers etc associated with the current document void fileClosed(); @@ -259,6 +259,7 @@ protected slots: sv::Clipboard m_preAnalysis; sv::Selection m_reAnalysingSelection; + sv_frame_t m_analysedFrames = 0; FrequencyRange m_reAnalysingRange; std::vector m_reAnalysisCandidates; int m_currentCandidate; diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 45fb44f..6226fcf 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3078,36 +3078,27 @@ MainWindow::analyseNow() void MainWindow::analyseDuringRecordingRunner() { - this->m_analysedFrames = 0; - analyseDuringRecording(); + QSettings settings; + settings.beginGroup("Analyser"); + bool recordAnalyse = settings.value("record-analysis", true).toBool(); + settings.endGroup(); + if ((recordAnalyse && this->m_recordTarget->isRecording())) + { + analyseDuringRecording(); + } } void MainWindow::analyseDuringRecording() { - QSettings settings; - settings.beginGroup("Analyser"); - bool recordAnalyse = settings.value("record-analysis", true).toBool(); - settings.endGroup(); - if ((recordAnalyse && this->m_recordTarget->isRecording()) || this->m_analysedFrames != 0) - { - int duration_ms = 250; - - // We start with a 100-frame overlap to ensure we capture attacks in time - sv_frame_t overlap = 0; - auto start_position = std::max(m_analysedFrames - overlap, 0LL); - auto end_position = m_recordTarget->getRecordDuration(); - auto selection = Selection(start_position, end_position); - this->m_analysedFrames = end_position; - (tr("Analyse Audio"), true); - - m_analyser->analyseRecordingFileToTheEnd(selection); + int duration_ms = 200; + + m_analyser->analyseRecordingToEnd(m_recordTarget->getRecordDuration()); if (this->m_recordTarget->isRecording()) { // TODO (alnovi): run analysis not by time but in the process of buffer filling QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); } - } } void From def17d3f73f628789da98efae327f82fa0d413fd Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 11 Jul 2024 12:53:11 +0300 Subject: [PATCH 10/24] further refactoring --- main/Analyser.cpp | 247 ++++++++++++++++++++------------------------ main/Analyser.h | 8 ++ main/MainWindow.cpp | 2 +- 3 files changed, 122 insertions(+), 135 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 9d7bb2a..9d2a990 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -129,21 +129,21 @@ Analyser::analyseExistingFile() QString Analyser::analyseRecordingToEnd(sv_frame_t record_duration) { - if (!m_document) return "Internal error: Analyser::analyseExistingFile() called with no document present"; + if (!m_document) return "Internal error: Analyser::analyseRecordingToEnd() called with no document present"; - if (!m_pane) return "Internal error: Analyser::analyseExistingFile() called with no pane present"; + if (!m_pane) return "Internal error: Analyser::analyseRecordingToEnd() called with no pane present"; - if (m_fileModel.isNone()) return "Internal error: Analyser::analyseExistingFile() called with no model present"; + if (m_fileModel.isNone()) return "Internal error: Analyser::analyseRecordingToEnd() called with no model present"; - // We start with a 5000-frame overlap to ensure we capture attacks in time (~113ms) - sv_frame_t overlap = 5000; - auto start_position = std::max(m_analysedFrames - overlap, 0LL); - auto end_position = record_duration; - Selection analysingSelection = Selection(start_position, end_position); + // We start with a 2500-frame overlap to ensure we capture instrument attacks in time (~56ms) + sv_frame_t overlap = 2500; + auto startPosition = std::max(m_analysedFrames - overlap, 0LL); + auto endPosition = record_duration; + Selection analysingSelection = Selection(startPosition, endPosition); this->analyseRecording(analysingSelection); - m_analysedFrames = end_position; + m_analysedFrames = endPosition; return ""; } @@ -367,6 +367,76 @@ Analyser::addWaveform() return ""; } +std::map getAnalysisSettingsFromSettings() +{ + std::map analysisSettings; + + QSettings settings; + settings.beginGroup("Analyser"); + + analysisSettings["precision-analysis"] = settings.value("precision-analysis", false).toBool(); + analysisSettings["lowamp-analysis"] = settings.value("lowamp-analysis", true).toBool(); + analysisSettings["onset-analysis"] = settings.value("onset-analysis", true).toBool(); + analysisSettings["prune-analysis"] = settings.value("prune-analysis", true).toBool(); + + settings.endGroup(); + + return analysisSettings; +} + +static void setAnalysisSettings(Transform& transform) +{ + const auto analysisSettings = getAnalysisSettingsFromSettings(); + + if (analysisSettings.count("precision-analysis") > 0) { + bool precise = analysisSettings.at("precision-analysis"); + if (precise) { + cerr << "setting parameters for precise mode" << endl; + transform.setParameter("precisetime", 1); + } + else { + cerr << "setting parameters for vague mode" << endl; + transform.setParameter("precisetime", 0); + } + } + + if (analysisSettings.count("lowamp-analysis") > 0) { + bool lowamp = analysisSettings.at("lowamp-analysis"); + if (lowamp) { + cerr << "setting parameters for lowamp suppression" << endl; + transform.setParameter("lowampsuppression", 0.2f); + } + else { + cerr << "setting parameters for no lowamp suppression" << endl; + transform.setParameter("lowampsuppression", 0.0f); + } + } + + if (analysisSettings.count("onset-analysis") > 0) { + bool onset = analysisSettings.at("onset-analysis"); + if (onset) { + cerr << "setting parameters for increased onset sensitivity" << endl; + transform.setParameter("onsetsensitivity", 0.7f); + } + else { + cerr << "setting parameters for non-increased onset sensitivity" << endl; + transform.setParameter("onsetsensitivity", 0.0f); + } + } + + if (analysisSettings.count("prune-analysis") > 0) { + bool prune = analysisSettings.at("prune-analysis"); + if (prune) { + cerr << "setting parameters for duration pruning" << endl; + transform.setParameter("prunethresh", 0.1f); + } + else { + cerr << "setting parameters for no duration pruning" << endl; + transform.setParameter("prunethresh", 0.0f); + } + } +} + QString Analyser::addAnalyses() { @@ -405,11 +475,6 @@ Analyser::addAnalyses() TransformFactory *tf = TransformFactory::getInstance(); - QString plugname = "pYIN"; - QString base = "vamp:pyin:pyin:"; - QString f0out = "smoothedpitchtrack"; - QString noteout = "notes"; - Transforms transforms; /*!!! we could have more than one pitch track... @@ -423,80 +488,27 @@ Analyser::addAnalyses() m_document->addLayerToView(m_pane, lx); } */ + auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); + auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); QString notFound = tr("Transform \"%1\" not found. Unable to analyse audio file.

Is the %2 Vamp plugin correctly installed?"); - if (!tf->haveTransform(base + f0out)) { - return notFound.arg(base + f0out).arg(plugname); + if (!tf->haveTransform(f0_transform)) { + return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); } - if (!tf->haveTransform(base + noteout)) { - return notFound.arg(base + noteout).arg(plugname); + if (!tf->haveTransform(note_transform)) { + return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); } - QSettings settings; - settings.beginGroup("Analyser"); - - bool precise = false, lowamp = true, onset = true, prune = true; - - std::map flags { - { "precision-analysis", precise }, - { "lowamp-analysis", lowamp }, - { "onset-analysis", onset }, - { "prune-analysis", prune } - }; - - auto keyMap = getAnalysisSettings(); - - for (auto p: flags) { - auto ki = keyMap.find(p.first); - if (ki != keyMap.end()) { - p.second = settings.value(ki->first, ki->second).toBool(); - } else { - throw std::logic_error("Internal error: One or more analysis settings keys not found in map: check addAnalyses and getAnalysisSettings"); - } - } - - settings.endGroup(); - Transform t = tf->getDefaultTransformFor - (base + f0out, waveFileModel->getSampleRate()); + (f0_transform, waveFileModel->getSampleRate()); t.setStepSize(256); t.setBlockSize(2048); - if (precise) { - cerr << "setting parameters for precise mode" << endl; - t.setParameter("precisetime", 1); - } else { - cerr << "setting parameters for vague mode" << endl; - t.setParameter("precisetime", 0); - } - - if (lowamp) { - cerr << "setting parameters for lowamp suppression" << endl; - t.setParameter("lowampsuppression", 0.2f); - } else { - cerr << "setting parameters for no lowamp suppression" << endl; - t.setParameter("lowampsuppression", 0.0f); - } - - if (onset) { - cerr << "setting parameters for increased onset sensitivity" << endl; - t.setParameter("onsetsensitivity", 0.7f); - } else { - cerr << "setting parameters for non-increased onset sensitivity" << endl; - t.setParameter("onsetsensitivity", 0.0f); - } - - if (prune) { - cerr << "setting parameters for duration pruning" << endl; - t.setParameter("prunethresh", 0.1f); - } else { - cerr << "setting parameters for no duration pruning" << endl; - t.setParameter("prunethresh", 0.0f); - } + setAnalysisSettings(t); transforms.push_back(t); - t.setOutput(noteout); + t.setOutput(PYIN_NOTE_OUT); transforms.push_back(t); @@ -508,8 +520,12 @@ Analyser::addAnalyses() FlexiNoteLayer *f = qobject_cast(layers[i]); TimeValueLayer *t = qobject_cast(layers[i]); - if (f) m_layers[Notes] = f; - if (t) m_layers[PitchTrack] = t; + if (f) { + m_layers[Notes] = f; + } + else if (t) { + m_layers[PitchTrack] = t; + } m_document->addLayerToView(m_pane, layers[i]); } @@ -627,15 +643,13 @@ Analyser::analyseRecording(Selection sel) { QMutexLocker locker(&m_asyncMutex); - FrequencyRange range = FrequencyRange(); - auto waveFileModel = ModelById::getAs(m_fileModel); if (!waveFileModel) { - return "Internal error: Analyser::reAnalyseSelection() called with no model present"; + return "Internal error: Analyser::analyseRecording() called with no model present"; } if (!m_reAnalysingSelection.isEmpty()) { - if (sel == m_reAnalysingSelection && range == m_reAnalysingRange) { + if (sel == m_reAnalysingSelection) { cerr << "selection & range are same as current analysis, ignoring" << endl; return ""; } @@ -644,60 +658,35 @@ Analyser::analyseRecording(Selection sel) if (sel.isEmpty()) return ""; m_reAnalysingSelection = sel; - m_reAnalysingRange = range; - m_preAnalysis = Clipboard(); auto* pitchLayer = qobject_cast(m_layers[PitchTrack]); - if (pitchLayer) { - pitchLayer->copy(m_pane, sel, m_preAnalysis); - } auto* noteLayer = qobject_cast(m_layers[Notes]); TransformFactory* tf = TransformFactory::getInstance(); - QString plugname1 = "pYIN"; - - QString base = "vamp:pyin:pyin:"; - QString f0out = "smoothedpitchtrack"; - QString noteout = "notes"; - Transforms transforms; + auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); + auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); + QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); - if (!tf->haveTransform(base + f0out)) { - return notFound.arg(base + f0out).arg(plugname1); + if (!tf->haveTransform(f0_transform)) { + return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); } - if (!tf->haveTransform(base + noteout)) { - return notFound.arg(base + noteout).arg(plugname1); + if (!tf->haveTransform(note_transform)) { + return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); } Transform t = tf->getDefaultTransformFor - (base + f0out, waveFileModel->getSampleRate()); + (f0_transform, waveFileModel->getSampleRate()); t.setStepSize(256); t.setBlockSize(2048); - if (range.isConstrained()) { - t.setParameter("minfreq", float(range.min)); - t.setParameter("maxfreq", float(range.max)); - t.setBlockSize(4096); - } - - // get time stamps that align with the 256-sample grid of the original extraction - const sv_frame_t grid = 256; - sv_frame_t startSample = (sel.getStartFrame() / grid) * grid; - if (startSample < sel.getStartFrame()) startSample += grid; - sv_frame_t endSample = (sel.getEndFrame() / grid) * grid; - if (endSample < sel.getEndFrame()) endSample += grid; - if (!range.isConstrained()) { - startSample -= 4 * grid; // 4*256 is for 4 frames offset due to timestamp shift - endSample -= 4 * grid; - } - else { - endSample -= 9 * grid; // MM says: not sure what the CHP plugin does there - } - RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); - RealTime end = RealTime::frame2RealTime(endSample, waveFileModel->getSampleRate()); + setAnalysisSettings(t); + + RealTime start = RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); + RealTime end = RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); RealTime duration; @@ -705,10 +694,10 @@ Analyser::analyseRecording(Selection sel) duration = end - start; } - cerr << "Analyser::reAnalyseSelection: start " << start << " end " << end << " original selection start " << sel.getStartFrame() << " end " << sel.getEndFrame() << " duration " << duration << endl; + cerr << "Analyser::analyseRecording: start " << start << " end " << end << " original selection start " << sel.getStartFrame() << " end " << sel.getEndFrame() << " duration " << duration << endl; if (duration <= RealTime::zeroTime) { - cerr << "Analyser::reAnalyseSelection: duration <= 0, not analysing" << endl; + cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; return ""; } @@ -717,7 +706,7 @@ Analyser::analyseRecording(Selection sel) transforms.push_back(t); - t.setOutput(noteout); + t.setOutput(PYIN_NOTE_OUT); transforms.push_back(t); @@ -789,13 +778,11 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) TransformFactory *tf = TransformFactory::getInstance(); - QString plugname1 = "pYIN"; QString plugname2 = "CHP"; QString base = "vamp:pyin:localcandidatepyin:"; QString out = "pitchtrackcandidates"; - QString f0out = "smoothedpitchtrack"; - QString noteout = "notes"; + if (range.isConstrained()) { base = "vamp:chp:constrainedharmonicpeak:"; @@ -806,15 +793,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); if (!tf->haveTransform(base + out)) { - return notFound.arg(base + out).arg(plugname1).arg(plugname2); - } - - if (!tf->haveTransform(base + f0out)) { - return notFound.arg(base + f0out).arg(plugname1).arg(plugname2); - } - - if (!tf->haveTransform(base + noteout)) { - return notFound.arg(base + noteout).arg(plugname1).arg(plugname2); + return notFound.arg(base + out).arg(PYIN_PLUGIN_NAME).arg(plugname2); } Transform t = tf->getDefaultTransformFor diff --git a/main/Analyser.h b/main/Analyser.h index 3132c8b..66f9cb3 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -283,6 +283,14 @@ protected slots: void saveState(Component c) const; void loadState(Component c); + + // TODO (alnovi): Move constexpression to a static class + static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; + static constexpr const char* PYIN_TRANSFORM_BASE = "vamp:pyin:pyin:"; + static constexpr const char* PYIN_F0_OUT = "smoothedpitchtrack"; + static constexpr const char* PYIN_NOTE_OUT = "notes"; + + }; #endif diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 6226fcf..6eacc67 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3091,7 +3091,7 @@ MainWindow::analyseDuringRecordingRunner() void MainWindow::analyseDuringRecording() { - int duration_ms = 200; + constexpr int duration_ms = 100; m_analyser->analyseRecordingToEnd(m_recordTarget->getRecordDuration()); From 5e5e162ae6326805af530d3e64a7d001614a753e Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 11 Jul 2024 12:54:56 +0300 Subject: [PATCH 11/24] comments --- main/Analyser.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 9d2a990..55c1568 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -611,13 +611,13 @@ void processLayer(LayerType* layer, LayerType* targetLayer, std::function fromModel, std::shared_ptr toModel) { +static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { auto allEvents = toModel->getAllEvents(); auto points = fromModel->getAllEvents(); // Vamp doesn't add current timestamp for note features, so, do it manually std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { - return point.withFrame(point.getFrame() + context_start); + return point.withFrame(point.getFrame() + contextStart); }); if (!allEvents.empty() && !points.empty()) { @@ -630,7 +630,7 @@ static EventVector processNoteModel(sv_frame_t context_start, std::shared_ptrremove(prevEvent); } } @@ -722,7 +722,7 @@ Analyser::analyseRecording(Selection sel) if (tempPitchLayer) { setBaseColour(tempPitchLayer, tr("Black"), cdb); processLayer(tempPitchLayer, pitchLayer, [](std::shared_ptr model, std::shared_ptr) { - // No additional custom processing needed for TimeValueLayer + // TODO (alnovi): remove all events from toModel which ends after contextStart return model->getAllEvents(); }); } From 5306b572645d02a546aca9ccbf31bfa39ca956d4 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 11 Jul 2024 14:33:07 +0300 Subject: [PATCH 12/24] subscribe on record duration changed --- main/Analyser.cpp | 16 ++++++++++++++-- main/Analyser.h | 10 +++++----- main/MainWindow.cpp | 14 ++++++-------- main/MainWindow.h | 2 -- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 55c1568..df4aea8 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -582,6 +582,20 @@ Analyser::materialiseReAnalysis() switchPitchCandidate(m_reAnalysingSelection, true); // or false, doesn't matter } +void +Analyser::updatePitchTrack(sv::ModelId) +{ + // Implementation for updatePitchTrack slot + emit layersChanged(); +} + +void +Analyser::updateNoteLayer(sv::ModelId) +{ + // Implementation for updateNoteLayer slot + emit layersChanged(); +} + template void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* cdb) { layer->setBaseColour(cdb->getColourIndex(colourName)); @@ -1282,5 +1296,3 @@ Analyser::setPan(Component c, float pan) saveState(c); } } - - diff --git a/main/Analyser.h b/main/Analyser.h index 66f9cb3..04c710d 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -57,7 +57,7 @@ class Analyser : public QObject, QString analyseExistingFile(); // Completes analysis from the last position to the end - QString analyseRecordingToEnd(sv_frame_t record_duration); + QString analyseRecordingToEnd(sv::sv_frame_t record_duration); // Discard any layers etc associated with the current document void fileClosed(); @@ -137,7 +137,7 @@ class Analyser : public QObject, * candidate layers for the region it contains. Returns "" on * success or a user-readable error string on failure. */ - QString analyseRecording(Selection sel); + QString analyseRecording(sv::Selection sel); /** * Analyse the selection and schedule asynchronous adds of @@ -242,8 +242,8 @@ class Analyser : public QObject, void initialAnalysisCompleted(); protected slots: - void updatePitchTrack(ModelId); - void updateNoteLayer(ModelId); + void updatePitchTrack(sv::ModelId); + void updateNoteLayer(sv::ModelId); void layerAboutToBeDeleted(sv::Layer *); void layerCompletionChanged(sv::ModelId); void reAnalyseRegion(sv::sv_frame_t, sv::sv_frame_t, float, float); @@ -259,7 +259,7 @@ protected slots: sv::Clipboard m_preAnalysis; sv::Selection m_reAnalysingSelection; - sv_frame_t m_analysedFrames = 0; + sv::sv_frame_t m_analysedFrames = 0; FrequencyRange m_reAnalysingRange; std::vector m_reAnalysisCandidates; int m_currentCandidate; diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 6eacc67..1b17ed1 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -3084,21 +3084,19 @@ MainWindow::analyseDuringRecordingRunner() settings.endGroup(); if ((recordAnalyse && this->m_recordTarget->isRecording())) { - analyseDuringRecording(); + connect(this->m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, this, &MainWindow::analyseDuringRecording); + connect(this->m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, this, [this]() { + disconnect(this->m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, nullptr, nullptr); + disconnect(this->m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, nullptr, nullptr); + analyseDuringRecording(); + }); } } void MainWindow::analyseDuringRecording() { - constexpr int duration_ms = 100; - m_analyser->analyseRecordingToEnd(m_recordTarget->getRecordDuration()); - - if (this->m_recordTarget->isRecording()) { - // TODO (alnovi): run analysis not by time but in the process of buffer filling - QTimer::singleShot(duration_ms, this, SLOT(analyseDuringRecording())); - } } void diff --git a/main/MainWindow.h b/main/MainWindow.h index da7f605..21feff6 100644 --- a/main/MainWindow.h +++ b/main/MainWindow.h @@ -231,8 +231,6 @@ protected slots: sv::ActivityLog *m_activityLog; sv::KeyReference *m_keyReference; sv::VersionTester *m_versionTester; - - sv_frame_t m_analysedFrames = 0; QString m_newerVersionIs; From ae63f7f3a38529901511748510c3afa56af9a9d3 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 5 Sep 2025 08:48:37 +0300 Subject: [PATCH 13/24] analysis mode submenu --- main/MainWindow.cpp | 85 +++++++++++++++++++++++++++++++++++++-------- main/MainWindow.h | 10 ++++++ 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 1b17ed1..7046805 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -768,17 +768,32 @@ MainWindow::setupAnalysisMenu() QMenu *menu = menuBar()->addMenu(tr("&Analysis")); menu->setTearOffEnabled(true); - m_analyseDuringRecord = new QAction(tr("&Analyse during recording"), this); + // Create a submenu for analysis mode options + QMenu *modeMenu = menu->addMenu(tr("Analysis &Mode")); + modeMenu->setTearOffEnabled(true); + + // Create a radio button group for recording analysis mode + QActionGroup *analysisGroup = new QActionGroup(this); + + m_analyseAfterRecord = new QAction(tr("Analyse &After Recording"), this); + m_analyseAfterRecord->setStatusTip(tr("Automatically trigger analysis after recording is complete.")); + m_analyseAfterRecord->setCheckable(true); + m_analyseAfterRecord->setActionGroup(analysisGroup); + connect(m_analyseAfterRecord, SIGNAL(triggered()), this, SLOT(analyseAfterRecordToggled())); + modeMenu->addAction(m_analyseAfterRecord); + + m_analyseDuringRecord = new QAction(tr("Analyse &During Recording"), this); m_analyseDuringRecord->setStatusTip(tr("Automatically trigger analysis during recording.")); m_analyseDuringRecord->setCheckable(true); + m_analyseDuringRecord->setActionGroup(analysisGroup); connect(m_analyseDuringRecord, SIGNAL(triggered()), this, SLOT(recordAnalysisToggled())); - menu->addAction(m_analyseDuringRecord); + modeMenu->addAction(m_analyseDuringRecord); m_autoAnalyse = new QAction(tr("Auto-Analyse &New Audio"), this); m_autoAnalyse->setStatusTip(tr("Automatically trigger analysis upon opening of a new audio file.")); m_autoAnalyse->setCheckable(true); connect(m_autoAnalyse, SIGNAL(triggered()), this, SLOT(autoAnalysisToggled())); - menu->addAction(m_autoAnalyse); + modeMenu->addAction(m_autoAnalyse); action = new QAction(tr("&Analyse Now!"), this); action->setStatusTip(tr("Trigger analysis of pitches and notes. (This will delete all existing pitches and notes.)")); @@ -828,7 +843,7 @@ MainWindow::resetAnalyseOptions() QSettings settings; settings.beginGroup("Analyser"); - settings.setValue("auto-analysis", true); + setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); auto keyMap = Analyser::getAnalysisSettings(); for (auto p: keyMap) { @@ -839,17 +854,50 @@ MainWindow::resetAnalyseOptions() updateAnalyseStates(); } +MainWindow::RecordingAnalysisMode +MainWindow::getRecordingAnalysisMode() const +{ + QSettings settings; + settings.beginGroup("Analyser"); + int mode = settings.value("recording-analysis-mode", + static_cast(RecordingAnalysisMode::AfterRecording)).toInt(); + settings.endGroup(); + return static_cast(mode); +} + void -MainWindow::updateAnalyseStates() +MainWindow::setRecordingAnalysisMode(RecordingAnalysisMode mode) { QSettings settings; settings.beginGroup("Analyser"); + settings.setValue("recording-analysis-mode", static_cast(mode)); + settings.endGroup(); + updateAnalyseStates(); +} - bool autoAnalyse = settings.value("auto-analysis", true).toBool(); - m_autoAnalyse->setChecked(autoAnalyse); +void +MainWindow::updateAnalyseStates() +{ + QSettings settings; + settings.beginGroup("Analyser"); - bool analyseDuringRecord = settings.value("record-analysis", true).toBool(); - m_analyseDuringRecord->setChecked(analyseDuringRecord); + // Handle recording analysis mode with enum-based approach + RecordingAnalysisMode mode = getRecordingAnalysisMode(); + + switch (mode) { + case RecordingAnalysisMode::AfterRecording: + m_analyseAfterRecord->setChecked(true); + m_analyseDuringRecord->setChecked(false); + m_autoAnalyse->setChecked(true); + m_autoAnalyse->setEnabled(true); + break; + case RecordingAnalysisMode::DuringRecording: + m_analyseDuringRecord->setChecked(true); + m_analyseAfterRecord->setChecked(false); + m_autoAnalyse->setChecked(false); + m_autoAnalyse->setEnabled(false); + break; + } std::map actions { { "precision-analysis", m_precise }, @@ -880,14 +928,21 @@ MainWindow::recordAnalysisToggled() if (!a) return; bool set = a->isChecked(); + if (set) { + setRecordingAnalysisMode(RecordingAnalysisMode::DuringRecording); + } +} - QSettings settings; - settings.beginGroup("Analyser"); - settings.setValue("record-analysis", set); - settings.endGroup(); +void +MainWindow::analyseAfterRecordToggled() +{ + QAction *a = qobject_cast(sender()); + if (!a) return; - // make result visible explicitly, in case e.g. we just set the wrong key - updateAnalyseStates(); + bool set = a->isChecked(); + if (set) { + setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); + } } void diff --git a/main/MainWindow.h b/main/MainWindow.h index 21feff6..48e2db2 100644 --- a/main/MainWindow.h +++ b/main/MainWindow.h @@ -31,6 +31,11 @@ class MainWindow : public sv::MainWindowBase Q_OBJECT public: + enum class RecordingAnalysisMode { + AfterRecording = 0, // Default + DuringRecording = 1 + }; + MainWindow(AudioMode audioMode, bool withSonification = true, bool withSpectrogram = true); @@ -93,6 +98,7 @@ protected slots: virtual void analyseDuringRecordingRunner(); virtual void resetAnalyseOptions(); virtual void recordAnalysisToggled(); + virtual void analyseAfterRecordToggled(); virtual void autoAnalysisToggled(); virtual void precisionAnalysisToggled(); @@ -212,6 +218,7 @@ protected slots: QAction *m_autoAnalyse; QAction *m_analyseDuringRecord; + QAction *m_analyseAfterRecord; QAction *m_precise; QAction *m_lowamp; QAction *m_onset; @@ -254,6 +261,9 @@ protected slots: virtual void setupHelpMenu(); virtual void setupToolbars(); + RecordingAnalysisMode getRecordingAnalysisMode() const; + void setRecordingAnalysisMode(RecordingAnalysisMode mode); + virtual void octaveShift(bool up); virtual void auxSnapNotes(sv::Selection s); From 08892ecdc587c53fab2200754f999f586921ecdf Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 5 Sep 2025 09:04:18 +0300 Subject: [PATCH 14/24] use layer pool --- main/Analyser.cpp | 49 +++++++++++++++++++++++++++++++++++++++++------ main/Analyser.h | 1 + 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index df4aea8..d70da08 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -69,6 +69,16 @@ Analyser::Analyser() : Analyser::~Analyser() { + // Clean up any remaining realtime analysis layers + if (m_document && m_pane) { + for (auto* layer : m_realtimeAnalysisLayers) { + if (layer) { + m_document->removeLayerFromView(m_pane, layer); + m_document->deleteLayer(layer); + } + } + } + m_realtimeAnalysisLayers.clear(); } std::map @@ -192,6 +202,18 @@ void Analyser::fileClosed() { cerr << "Analyser::fileClosed" << endl; + + // Clean up any remaining realtime analysis layers + if (m_document && m_pane) { + for (auto* layer : m_realtimeAnalysisLayers) { + if (layer) { + m_document->removeLayerFromView(m_pane, layer); + m_document->deleteLayer(layer); + } + } + } + m_realtimeAnalysisLayers.clear(); + m_layers.clear(); m_reAnalysisCandidates.clear(); m_currentCandidate = -1; @@ -603,8 +625,8 @@ void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* // Generalized helper function to process layers template -void processLayer(LayerType* layer, LayerType* targetLayer, std::function, std::shared_ptr)> customProcessing) { - QObject::connect(layer, &TimeValueLayer::modelCompletionChanged, [layer, targetLayer, customProcessing](ModelId modelId) { +void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* document, sv::Pane* pane, std::vector* trackingVector, std::function, std::shared_ptr)> customProcessing) { + QObject::connect(layer, &LayerType::modelCompletionChanged, [layer, targetLayer, document, pane, trackingVector, customProcessing](ModelId modelId) { auto model = ModelById::getAs(modelId); if (model->getCompletion() == 100) { @@ -618,8 +640,18 @@ void processLayer(LayerType* layer, LayerType* targetLayer, std::functionadd(p); } - QObject::disconnect(layer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); - // TODO (alnovi): remove the layer. + QObject::disconnect(layer, &LayerType::modelCompletionChanged, nullptr, nullptr); + + // Clean up the temporary layer + if (document && pane) { + document->removeLayerFromView(pane, layer); + document->deleteLayer(layer); + } + + // Remove from tracking vector + if (trackingVector) { + trackingVector->erase(std::remove(trackingVector->begin(), trackingVector->end(), layer), trackingVector->end()); + } } }); } @@ -728,6 +760,11 @@ Analyser::analyseRecording(Selection sel) ColourDatabase* cdb = ColourDatabase::getInstance(); + // Track the temporary layers for cleanup + for (auto* layer : layers) { + m_realtimeAnalysisLayers.push_back(layer); + } + for (auto* layer : layers) { FlexiNoteLayer* tempNoteLayer = qobject_cast(layer); @@ -735,7 +772,7 @@ Analyser::analyseRecording(Selection sel) if (tempPitchLayer) { setBaseColour(tempPitchLayer, tr("Black"), cdb); - processLayer(tempPitchLayer, pitchLayer, [](std::shared_ptr model, std::shared_ptr) { + processLayer(tempPitchLayer, pitchLayer, m_document, m_pane, &m_realtimeAnalysisLayers, [](std::shared_ptr model, std::shared_ptr) { // TODO (alnovi): remove all events from toModel which ends after contextStart return model->getAllEvents(); }); @@ -743,7 +780,7 @@ Analyser::analyseRecording(Selection sel) if (tempNoteLayer) { setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); - processLayer(tempNoteLayer, noteLayer, std::bind(processNoteModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); + processLayer(tempNoteLayer, noteLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processNoteModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); } } diff --git a/main/Analyser.h b/main/Analyser.h index 04c710d..1db40b8 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -262,6 +262,7 @@ protected slots: sv::sv_frame_t m_analysedFrames = 0; FrequencyRange m_reAnalysingRange; std::vector m_reAnalysisCandidates; + std::vector m_realtimeAnalysisLayers; // Track temporary layers for cleanup int m_currentCandidate; bool m_candidatesVisible; sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; From 2818440d401fa7f9715d1ae9c5e1bb1cb8693169 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 5 Sep 2025 09:21:01 +0300 Subject: [PATCH 15/24] remove duplicated events --- main/Analyser.cpp | 110 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index d70da08..7467803 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -656,6 +656,63 @@ void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* docume }); } +// Custom processing logic for pitch track (SparseTimeValueModel) +static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { + auto allEvents = toModel->getAllEvents(); + auto points = fromModel->getAllEvents(); + + // Add context start timestamp to all points from the new analysis + std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { + return point.withFrame(point.getFrame() + contextStart); + }); + + // Remove all events from toModel that extend beyond contextStart to prevent overlaps + EventVector eventsToRemove; + for (const auto& event : allEvents) { + if (event.getFrame() >= contextStart) { + eventsToRemove.push_back(event); + } + } + + for (const auto& event : eventsToRemove) { + toModel->remove(event); + } + + // After cleanup, get the remaining events for overlap processing + allEvents = toModel->getAllEvents(); + + // Handle potential overlaps between the last existing event and first new event + if (!allEvents.empty() && !points.empty()) { + auto& lastExistingEvent = allEvents.back(); + auto& firstNewEvent = points.front(); + + // Check if there's a gap or overlap between last existing and first new event + auto gapFrames = firstNewEvent.getFrame() - lastExistingEvent.getFrame(); + + // If events are very close (within ~11ms at 44.1kHz), interpolate between them + const sv_frame_t interpolationThreshold = 512; // ~11ms at 44.1kHz + + if (gapFrames > 0 && gapFrames <= interpolationThreshold) { + // Small gap - add interpolated point if pitch values are similar + if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { + auto lastValue = lastExistingEvent.getValue(); + auto firstValue = firstNewEvent.getValue(); + auto valueDiff = std::abs(lastValue - firstValue) / lastValue; + + // Only interpolate if pitch values are within 10% of each other + if (valueDiff <= 0.1) { + auto midFrame = lastExistingEvent.getFrame() + gapFrames / 2; + auto midValue = (lastValue + firstValue) / 2.0; + Event interpolatedEvent = Event(midFrame, midValue, "interpolated"); + toModel->add(interpolatedEvent); + } + } + } + } + + return points; +} + // Custom processing logic for FlexiNoteLayer static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { auto allEvents = toModel->getAllEvents(); @@ -666,18 +723,52 @@ static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr contextStart) { + eventsToRemove.push_back(event); + } + } + + for (const auto& event : eventsToRemove) { + toModel->remove(event); + } + + // After cleanup, get the remaining events for overlap processing + allEvents = toModel->getAllEvents(); + if (!allEvents.empty() && !points.empty()) { auto& prevEvent = allEvents.back(); auto& nextEvent = points.front(); - // Merge events but don't be too greedy - if (nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration() && nextEvent.getFrame() > prevEvent.getFrame()) { - auto overallDuration = prevEvent.getDuration() + nextEvent.getDuration(); - auto overlapDuration = prevEvent.getFrame() + prevEvent.getDuration() - nextEvent.getFrame(); - points[0] = prevEvent.withDuration(overallDuration - overlapDuration); + // Handle various overlap scenarios + if (nextEvent.getFrame() >= prevEvent.getFrame() && + nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { - // TODO (alnovi): remove all events from toModel which ends after contextStart - toModel->remove(prevEvent); + // Calculate overlap parameters + auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); + auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); + auto overlapStart = nextEvent.getFrame(); + auto overlapDuration = std::min(prevEnd, nextEnd) - overlapStart; + + // Choose merge strategy based on overlap characteristics + if (overlapDuration < prevEvent.getDuration() * 0.5 && + overlapDuration < nextEvent.getDuration() * 0.5) { + // Small overlap: merge by extending the earlier event + auto mergedDuration = nextEnd - prevEvent.getFrame(); + points[0] = prevEvent.withDuration(mergedDuration); + toModel->remove(prevEvent); + } else { + // Significant overlap: keep the longer event, adjust timing + if (prevEvent.getDuration() > nextEvent.getDuration()) { + // Keep previous event, adjust next event start + points[0] = nextEvent.withFrame(prevEnd); + } else { + // Keep next event, remove previous + toModel->remove(prevEvent); + } + } } } @@ -772,10 +863,7 @@ Analyser::analyseRecording(Selection sel) if (tempPitchLayer) { setBaseColour(tempPitchLayer, tr("Black"), cdb); - processLayer(tempPitchLayer, pitchLayer, m_document, m_pane, &m_realtimeAnalysisLayers, [](std::shared_ptr model, std::shared_ptr) { - // TODO (alnovi): remove all events from toModel which ends after contextStart - return model->getAllEvents(); - }); + processLayer(tempPitchLayer, pitchLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processPitchModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); } if (tempNoteLayer) { From 0402064ae6f98c3ee339bf8223413dcb05183951 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 5 Sep 2025 09:25:18 +0300 Subject: [PATCH 16/24] intelligent overlap --- main/Analyser.cpp | 91 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 7467803..463a4b3 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -713,6 +713,57 @@ static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr 0.0f) return nextFreq; + if (nextFreq <= 0.0f && prevFreq > 0.0f) return prevFreq; + if (prevFreq <= 0.0f && nextFreq <= 0.0f) return 0.0f; + + // Calculate duration-based weights for each event's contribution to overlap + auto prevStart = prevEvent.getFrame(); + auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); + auto nextStart = nextEvent.getFrame(); + auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); + auto overlapEnd = overlapStart + overlapDuration; + + // Calculate overlapping portions for each event + auto prevOverlapStart = std::max(prevStart, overlapStart); + auto prevOverlapEnd = std::min(prevEnd, overlapEnd); + auto prevOverlapContrib = std::max(0LL, prevOverlapEnd - prevOverlapStart); + + auto nextOverlapStart = std::max(nextStart, overlapStart); + auto nextOverlapEnd = std::min(nextEnd, overlapEnd); + auto nextOverlapContrib = std::max(0LL, nextOverlapEnd - nextOverlapStart); + + // Calculate weights based on duration contributions + double totalContrib = prevOverlapContrib + nextOverlapContrib; + if (totalContrib <= 0) { + // Fallback to simple average if no clear contribution + return (prevFreq + nextFreq) / 2.0f; + } + + double prevWeight = prevOverlapContrib / totalContrib; + double nextWeight = nextOverlapContrib / totalContrib; + + // Calculate weighted frequency - use logarithmic averaging for better musical accuracy + if (prevFreq > 0.0f && nextFreq > 0.0f) { + // Geometric mean weighted by duration (better for frequency averaging) + double logPrevFreq = std::log(prevFreq); + double logNextFreq = std::log(nextFreq); + double weightedLogFreq = logPrevFreq * prevWeight + logNextFreq * nextWeight; + return std::exp(weightedLogFreq); + } else { + // Linear average for edge cases + return prevFreq * prevWeight + nextFreq * nextWeight; + } +} + // Custom processing logic for FlexiNoteLayer static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { auto allEvents = toModel->getAllEvents(); @@ -752,22 +803,44 @@ static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr 0.0f) { + mergedEvent = mergedEvent.withValue(weightedFreq); + } + points[0] = mergedEvent; toModel->remove(prevEvent); } else { - // Significant overlap: keep the longer event, adjust timing - if (prevEvent.getDuration() > nextEvent.getDuration()) { - // Keep previous event, adjust next event start - points[0] = nextEvent.withFrame(prevEnd); - } else { - // Keep next event, remove previous - toModel->remove(prevEvent); + // Significant overlap: create merged event with weighted frequency and optimal timing + auto mergedStart = prevEvent.getFrame(); + auto mergedDuration = std::max(prevEnd, nextEnd) - mergedStart; + + // Use the event with better timing characteristics (earlier start) + Event mergedEvent = prevEvent.withDuration(mergedDuration); + if (weightedFreq > 0.0f) { + mergedEvent = mergedEvent.withValue(weightedFreq); } + + // Preserve additional properties from the longer event + if (nextEvent.getDuration() > prevEvent.getDuration()) { + // Copy label and other properties from longer event if available + if (nextEvent.hasLabel() && !prevEvent.hasLabel()) { + mergedEvent = mergedEvent.withLabel(nextEvent.getLabel()); + } + if (nextEvent.hasLevel() && !prevEvent.hasLevel()) { + mergedEvent = mergedEvent.withLevel(nextEvent.getLevel()); + } + } + + points[0] = mergedEvent; + toModel->remove(prevEvent); } } } From ff43c0952a6bc8b52eb38b2d072deaebb6908dfb Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 5 Sep 2025 11:04:59 +0300 Subject: [PATCH 17/24] multievent overlaps --- build.log | 1 + changes.log | 2325 +++++++++++++++++++++++++++++++++++++++++++++ main/Analyser.cpp | 357 +++++-- 3 files changed, 2588 insertions(+), 95 deletions(-) create mode 100644 build.log create mode 100644 changes.log diff --git a/build.log b/build.log new file mode 100644 index 0000000..87240c0 --- /dev/null +++ b/build.log @@ -0,0 +1 @@ +[535/535] Linking target test-svcore-data-fileio diff --git a/changes.log b/changes.log new file mode 100644 index 0000000..7f0b69b --- /dev/null +++ b/changes.log @@ -0,0 +1,2325 @@ +diff --git a/main/Analyser.cpp b/main/Analyser.cpp +index 957bfe6..463a4b3 100644 +--- a/main/Analyser.cpp ++++ b/main/Analyser.cpp +@@ -5,7 +5,7 @@ + An intonation analysis and annotation tool + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006-2012 Chris Cannam and QMUL. +- ++ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the +@@ -69,6 +69,16 @@ Analyser::Analyser() : + + Analyser::~Analyser() + { ++ // Clean up any remaining realtime analysis layers ++ if (m_document && m_pane) { ++ for (auto* layer : m_realtimeAnalysisLayers) { ++ if (layer) { ++ m_document->removeLayerFromView(m_pane, layer); ++ m_document->deleteLayer(layer); ++ } ++ } ++ } ++ m_realtimeAnalysisLayers.clear(); + } + + std::map +@@ -83,7 +93,7 @@ Analyser::getAnalysisSettings() + + QString + Analyser::newFileLoaded(Document *doc, ModelId model, +- PaneStack *paneStack, Pane *pane) ++ PaneStack *paneStack, Pane *pane) + { + m_document = doc; + m_fileModel = model; +@@ -93,7 +103,7 @@ Analyser::newFileLoaded(Document *doc, ModelId model, + if (!ModelById::isa(m_fileModel)) { + return "Internal error: Analyser::newFileLoaded() called with no model, or a non-WaveFileModel"; + } +- ++ + connect(doc, SIGNAL(layerAboutToBeDeleted(Layer *)), + this, SLOT(layerAboutToBeDeleted(Layer *))); + +@@ -113,7 +123,7 @@ Analyser::analyseExistingFile() + if (!m_pane) return "Internal error: Analyser::analyseExistingFile() called with no pane present"; + + if (m_fileModel.isNone()) return "Internal error: Analyser::analyseExistingFile() called with no model present"; +- ++ + if (m_layers[PitchTrack]) { + m_document->removeLayerFromView(m_pane, m_layers[PitchTrack]); + m_layers[PitchTrack] = 0; +@@ -126,6 +136,28 @@ Analyser::analyseExistingFile() + return doAllAnalyses(true); + } + ++QString ++Analyser::analyseRecordingToEnd(sv_frame_t record_duration) ++{ ++ if (!m_document) return "Internal error: Analyser::analyseRecordingToEnd() called with no document present"; ++ ++ if (!m_pane) return "Internal error: Analyser::analyseRecordingToEnd() called with no pane present"; ++ ++ if (m_fileModel.isNone()) return "Internal error: Analyser::analyseRecordingToEnd() called with no model present"; ++ ++ // We start with a 2500-frame overlap to ensure we capture instrument attacks in time (~56ms) ++ sv_frame_t overlap = 2500; ++ auto startPosition = std::max(m_analysedFrames - overlap, 0LL); ++ auto endPosition = record_duration; ++ Selection analysingSelection = Selection(startPosition, endPosition); ++ ++ this->analyseRecording(analysingSelection); ++ ++ m_analysedFrames = endPosition; ++ ++ return ""; ++} ++ + QString + Analyser::doAllAnalyses(bool withPitchTrack) + { +@@ -170,6 +202,18 @@ void + Analyser::fileClosed() + { + cerr << "Analyser::fileClosed" << endl; ++ ++ // Clean up any remaining realtime analysis layers ++ if (m_document && m_pane) { ++ for (auto* layer : m_realtimeAnalysisLayers) { ++ if (layer) { ++ m_document->removeLayerFromView(m_pane, layer); ++ m_document->deleteLayer(layer); ++ } ++ } ++ } ++ m_realtimeAnalysisLayers.clear(); ++ + m_layers.clear(); + m_reAnalysisCandidates.clear(); + m_currentCandidate = -1; +@@ -204,7 +248,7 @@ Analyser::getInitialAnalysisCompletion() + int c = m_layers[Notes]->getCompletion(m_pane); + if (c < completion) completion = c; + } +- ++ + return completion; + } + +@@ -227,7 +271,7 @@ Analyser::layerCompletionChanged(ModelId) + + auto audioModel = ModelById::get(m_layers[Audio]->getModel()); + sv_frame_t endFrame = audioModel->getEndFrame(); +- ++ + if (m_layers[PitchTrack]) { + auto model = ModelById::getAs + (m_layers[PitchTrack]->getModel()); +@@ -255,7 +299,7 @@ Analyser::addVisualisations() + + /* This is roughly what we'd do for a constant-Q spectrogram, but it + currently has issues with y-axis alignment +- ++ + TransformFactory *tf = TransformFactory::getInstance(); + + QString name = "Constant-Q"; +@@ -264,7 +308,7 @@ Analyser::addVisualisations() + + QString notFound = tr("Transform \"%1\" not found, spectrogram will not be enabled.

Is the %2 Vamp plugin correctly installed?"); + if (!tf->haveTransform(base + out)) { +- return notFound.arg(base + out).arg(name); ++ return notFound.arg(base + out).arg(name); + } + + Transform transform = tf->getDefaultTransformFor +@@ -275,7 +319,7 @@ Analyser::addVisualisations() + (m_document->createDerivedLayer(transform, m_fileModel)); + + if (!spectrogram) return tr("Transform \"%1\" did not run correctly (no layer or wrong layer type returned)").arg(base + out); +-*/ ++*/ + + // As with all the visualisation layers, if we already have one in + // the pane we do not create another, just record its +@@ -338,13 +382,83 @@ Analyser::addWaveform() + params->setPlayPan(-1); + params->setPlayGain(1); + } +- ++ + m_document->addLayerToView(m_pane, waveform); + + m_layers[Audio] = waveform; + return ""; + } + ++std::map getAnalysisSettingsFromSettings() ++{ ++ std::map analysisSettings; ++ ++ QSettings settings; ++ settings.beginGroup("Analyser"); ++ ++ analysisSettings["precision-analysis"] = settings.value("precision-analysis", false).toBool(); ++ analysisSettings["lowamp-analysis"] = settings.value("lowamp-analysis", true).toBool(); ++ analysisSettings["onset-analysis"] = settings.value("onset-analysis", true).toBool(); ++ analysisSettings["prune-analysis"] = settings.value("prune-analysis", true).toBool(); ++ ++ settings.endGroup(); ++ ++ return analysisSettings; ++} ++ ++static void setAnalysisSettings(Transform& transform) ++{ ++ const auto analysisSettings = getAnalysisSettingsFromSettings(); ++ ++ if (analysisSettings.count("precision-analysis") > 0) { ++ bool precise = analysisSettings.at("precision-analysis"); ++ if (precise) { ++ cerr << "setting parameters for precise mode" << endl; ++ transform.setParameter("precisetime", 1); ++ } ++ else { ++ cerr << "setting parameters for vague mode" << endl; ++ transform.setParameter("precisetime", 0); ++ } ++ } ++ ++ if (analysisSettings.count("lowamp-analysis") > 0) { ++ bool lowamp = analysisSettings.at("lowamp-analysis"); ++ if (lowamp) { ++ cerr << "setting parameters for lowamp suppression" << endl; ++ transform.setParameter("lowampsuppression", 0.2f); ++ } ++ else { ++ cerr << "setting parameters for no lowamp suppression" << endl; ++ transform.setParameter("lowampsuppression", 0.0f); ++ } ++ } ++ ++ if (analysisSettings.count("onset-analysis") > 0) { ++ bool onset = analysisSettings.at("onset-analysis"); ++ if (onset) { ++ cerr << "setting parameters for increased onset sensitivity" << endl; ++ transform.setParameter("onsetsensitivity", 0.7f); ++ } ++ else { ++ cerr << "setting parameters for non-increased onset sensitivity" << endl; ++ transform.setParameter("onsetsensitivity", 0.0f); ++ } ++ } ++ ++ if (analysisSettings.count("prune-analysis") > 0) { ++ bool prune = analysisSettings.at("prune-analysis"); ++ if (prune) { ++ cerr << "setting parameters for duration pruning" << endl; ++ transform.setParameter("prunethresh", 0.1f); ++ } ++ else { ++ cerr << "setting parameters for no duration pruning" << endl; ++ transform.setParameter("prunethresh", 0.0f); ++ } ++ } ++} ++ + QString + Analyser::addAnalyses() + { +@@ -352,7 +466,7 @@ Analyser::addAnalyses() + if (!waveFileModel) { + return "Internal error: Analyser::addAnalyses() called with no model present"; + } +- ++ + // As with the spectrogram above, if these layers exist we use + // them + TimeValueLayer *existingPitch = 0; +@@ -382,11 +496,6 @@ Analyser::addAnalyses() + } + + TransformFactory *tf = TransformFactory::getInstance(); +- +- QString plugname = "pYIN"; +- QString base = "vamp:pyin:pyin:"; +- QString f0out = "smoothedpitchtrack"; +- QString noteout = "notes"; + + Transforms transforms; + +@@ -401,81 +510,28 @@ Analyser::addAnalyses() + m_document->addLayerToView(m_pane, lx); + } + */ ++ auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); ++ auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); + + QString notFound = tr("Transform \"%1\" not found. Unable to analyse audio file.

Is the %2 Vamp plugin correctly installed?"); +- if (!tf->haveTransform(base + f0out)) { +- return notFound.arg(base + f0out).arg(plugname); ++ if (!tf->haveTransform(f0_transform)) { ++ return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); + } +- if (!tf->haveTransform(base + noteout)) { +- return notFound.arg(base + noteout).arg(plugname); +- } +- +- QSettings settings; +- settings.beginGroup("Analyser"); +- +- bool precise = false, lowamp = true, onset = true, prune = true; +- +- std::map flags { +- { "precision-analysis", precise }, +- { "lowamp-analysis", lowamp }, +- { "onset-analysis", onset }, +- { "prune-analysis", prune } +- }; +- +- auto keyMap = getAnalysisSettings(); +- +- for (auto p: flags) { +- auto ki = keyMap.find(p.first); +- if (ki != keyMap.end()) { +- p.second = settings.value(ki->first, ki->second).toBool(); +- } else { +- throw std::logic_error("Internal error: One or more analysis settings keys not found in map: check addAnalyses and getAnalysisSettings"); +- } ++ if (!tf->haveTransform(note_transform)) { ++ return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); + } + +- settings.endGroup(); +- + Transform t = tf->getDefaultTransformFor +- (base + f0out, waveFileModel->getSampleRate()); ++ (f0_transform, waveFileModel->getSampleRate()); + t.setStepSize(256); + t.setBlockSize(2048); + +- if (precise) { +- cerr << "setting parameters for precise mode" << endl; +- t.setParameter("precisetime", 1); +- } else { +- cerr << "setting parameters for vague mode" << endl; +- t.setParameter("precisetime", 0); +- } +- +- if (lowamp) { +- cerr << "setting parameters for lowamp suppression" << endl; +- t.setParameter("lowampsuppression", 0.2f); +- } else { +- cerr << "setting parameters for no lowamp suppression" << endl; +- t.setParameter("lowampsuppression", 0.0f); +- } +- +- if (onset) { +- cerr << "setting parameters for increased onset sensitivity" << endl; +- t.setParameter("onsetsensitivity", 0.7f); +- } else { +- cerr << "setting parameters for non-increased onset sensitivity" << endl; +- t.setParameter("onsetsensitivity", 0.0f); +- } +- +- if (prune) { +- cerr << "setting parameters for duration pruning" << endl; +- t.setParameter("prunethresh", 0.1f); +- } else { +- cerr << "setting parameters for no duration pruning" << endl; +- t.setParameter("prunethresh", 0.0f); +- } ++ setAnalysisSettings(t); + + transforms.push_back(t); + +- t.setOutput(noteout); +- ++ t.setOutput(PYIN_NOTE_OUT); ++ + transforms.push_back(t); + + std::vector layers = +@@ -485,16 +541,20 @@ Analyser::addAnalyses() + + FlexiNoteLayer *f = qobject_cast(layers[i]); + TimeValueLayer *t = qobject_cast(layers[i]); +- +- if (f) m_layers[Notes] = f; +- if (t) m_layers[PitchTrack] = t; +- ++ ++ if (f) { ++ m_layers[Notes] = f; ++ } ++ else if (t) { ++ m_layers[PitchTrack] = t; ++ } ++ + m_document->addLayerToView(m_pane, layers[i]); + } +- ++ + ColourDatabase *cdb = ColourDatabase::getInstance(); +- +- TimeValueLayer *pitchLayer = ++ ++ TimeValueLayer *pitchLayer = + qobject_cast(m_layers[PitchTrack]); + if (pitchLayer) { + pitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); +@@ -506,8 +566,8 @@ Analyser::addAnalyses() + connect(pitchLayer, SIGNAL(modelCompletionChanged(ModelId)), + this, SLOT(layerCompletionChanged(ModelId))); + } +- +- FlexiNoteLayer *flexiNoteLayer = ++ ++ FlexiNoteLayer *flexiNoteLayer = + qobject_cast(m_layers[Notes]); + if (flexiNoteLayer) { + flexiNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); +@@ -523,7 +583,7 @@ Analyser::addAnalyses() + connect(flexiNoteLayer, SIGNAL(materialiseReAnalysis()), + this, SLOT(materialiseReAnalysis())); + } +- ++ + return ""; + } + +@@ -544,6 +604,351 @@ Analyser::materialiseReAnalysis() + switchPitchCandidate(m_reAnalysingSelection, true); // or false, doesn't matter + } + ++void ++Analyser::updatePitchTrack(sv::ModelId) ++{ ++ // Implementation for updatePitchTrack slot ++ emit layersChanged(); ++} ++ ++void ++Analyser::updateNoteLayer(sv::ModelId) ++{ ++ // Implementation for updateNoteLayer slot ++ emit layersChanged(); ++} ++ ++template ++void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* cdb) { ++ layer->setBaseColour(cdb->getColourIndex(colourName)); ++} ++ ++// Generalized helper function to process layers ++template ++void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* document, sv::Pane* pane, std::vector* trackingVector, std::function, std::shared_ptr)> customProcessing) { ++ QObject::connect(layer, &LayerType::modelCompletionChanged, [layer, targetLayer, document, pane, trackingVector, customProcessing](ModelId modelId) { ++ auto model = ModelById::getAs(modelId); ++ ++ if (model->getCompletion() == 100) { ++ auto toModel = ModelById::getAs(targetLayer->getModel()); ++ EventVector points = model->getAllEvents(); ++ ++ // Custom processing logic ++ points = customProcessing(model, toModel); ++ ++ for (const Event& p : points) { ++ toModel->add(p); ++ } ++ ++ QObject::disconnect(layer, &LayerType::modelCompletionChanged, nullptr, nullptr); ++ ++ // Clean up the temporary layer ++ if (document && pane) { ++ document->removeLayerFromView(pane, layer); ++ document->deleteLayer(layer); ++ } ++ ++ // Remove from tracking vector ++ if (trackingVector) { ++ trackingVector->erase(std::remove(trackingVector->begin(), trackingVector->end(), layer), trackingVector->end()); ++ } ++ } ++ }); ++} ++ ++// Custom processing logic for pitch track (SparseTimeValueModel) ++static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { ++ auto allEvents = toModel->getAllEvents(); ++ auto points = fromModel->getAllEvents(); ++ ++ // Add context start timestamp to all points from the new analysis ++ std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { ++ return point.withFrame(point.getFrame() + contextStart); ++ }); ++ ++ // Remove all events from toModel that extend beyond contextStart to prevent overlaps ++ EventVector eventsToRemove; ++ for (const auto& event : allEvents) { ++ if (event.getFrame() >= contextStart) { ++ eventsToRemove.push_back(event); ++ } ++ } ++ ++ for (const auto& event : eventsToRemove) { ++ toModel->remove(event); ++ } ++ ++ // After cleanup, get the remaining events for overlap processing ++ allEvents = toModel->getAllEvents(); ++ ++ // Handle potential overlaps between the last existing event and first new event ++ if (!allEvents.empty() && !points.empty()) { ++ auto& lastExistingEvent = allEvents.back(); ++ auto& firstNewEvent = points.front(); ++ ++ // Check if there's a gap or overlap between last existing and first new event ++ auto gapFrames = firstNewEvent.getFrame() - lastExistingEvent.getFrame(); ++ ++ // If events are very close (within ~11ms at 44.1kHz), interpolate between them ++ const sv_frame_t interpolationThreshold = 512; // ~11ms at 44.1kHz ++ ++ if (gapFrames > 0 && gapFrames <= interpolationThreshold) { ++ // Small gap - add interpolated point if pitch values are similar ++ if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { ++ auto lastValue = lastExistingEvent.getValue(); ++ auto firstValue = firstNewEvent.getValue(); ++ auto valueDiff = std::abs(lastValue - firstValue) / lastValue; ++ ++ // Only interpolate if pitch values are within 10% of each other ++ if (valueDiff <= 0.1) { ++ auto midFrame = lastExistingEvent.getFrame() + gapFrames / 2; ++ auto midValue = (lastValue + firstValue) / 2.0; ++ Event interpolatedEvent = Event(midFrame, midValue, "interpolated"); ++ toModel->add(interpolatedEvent); ++ } ++ } ++ } ++ } ++ ++ return points; ++} ++ ++// Helper function to calculate weighted frequency from overlapping notes ++static float calculateWeightedFrequency(const Event& prevEvent, const Event& nextEvent, ++ sv_frame_t overlapStart, sv_frame_t overlapDuration) { ++ // Extract frequencies from both events ++ float prevFreq = prevEvent.hasValue() ? prevEvent.getValue() : 0.0f; ++ float nextFreq = nextEvent.hasValue() ? nextEvent.getValue() : 0.0f; ++ ++ // If either frequency is invalid, use the valid one ++ if (prevFreq <= 0.0f && nextFreq > 0.0f) return nextFreq; ++ if (nextFreq <= 0.0f && prevFreq > 0.0f) return prevFreq; ++ if (prevFreq <= 0.0f && nextFreq <= 0.0f) return 0.0f; ++ ++ // Calculate duration-based weights for each event's contribution to overlap ++ auto prevStart = prevEvent.getFrame(); ++ auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); ++ auto nextStart = nextEvent.getFrame(); ++ auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); ++ auto overlapEnd = overlapStart + overlapDuration; ++ ++ // Calculate overlapping portions for each event ++ auto prevOverlapStart = std::max(prevStart, overlapStart); ++ auto prevOverlapEnd = std::min(prevEnd, overlapEnd); ++ auto prevOverlapContrib = std::max(0LL, prevOverlapEnd - prevOverlapStart); ++ ++ auto nextOverlapStart = std::max(nextStart, overlapStart); ++ auto nextOverlapEnd = std::min(nextEnd, overlapEnd); ++ auto nextOverlapContrib = std::max(0LL, nextOverlapEnd - nextOverlapStart); ++ ++ // Calculate weights based on duration contributions ++ double totalContrib = prevOverlapContrib + nextOverlapContrib; ++ if (totalContrib <= 0) { ++ // Fallback to simple average if no clear contribution ++ return (prevFreq + nextFreq) / 2.0f; ++ } ++ ++ double prevWeight = prevOverlapContrib / totalContrib; ++ double nextWeight = nextOverlapContrib / totalContrib; ++ ++ // Calculate weighted frequency - use logarithmic averaging for better musical accuracy ++ if (prevFreq > 0.0f && nextFreq > 0.0f) { ++ // Geometric mean weighted by duration (better for frequency averaging) ++ double logPrevFreq = std::log(prevFreq); ++ double logNextFreq = std::log(nextFreq); ++ double weightedLogFreq = logPrevFreq * prevWeight + logNextFreq * nextWeight; ++ return std::exp(weightedLogFreq); ++ } else { ++ // Linear average for edge cases ++ return prevFreq * prevWeight + nextFreq * nextWeight; ++ } ++} ++ ++// Custom processing logic for FlexiNoteLayer ++static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { ++ auto allEvents = toModel->getAllEvents(); ++ auto points = fromModel->getAllEvents(); ++ ++ // Vamp doesn't add current timestamp for note features, so, do it manually ++ std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { ++ return point.withFrame(point.getFrame() + contextStart); ++ }); ++ ++ // Remove all events from toModel which end after contextStart to prevent overlaps ++ EventVector eventsToRemove; ++ for (const auto& event : allEvents) { ++ if (event.getFrame() + event.getDuration() > contextStart) { ++ eventsToRemove.push_back(event); ++ } ++ } ++ ++ for (const auto& event : eventsToRemove) { ++ toModel->remove(event); ++ } ++ ++ // After cleanup, get the remaining events for overlap processing ++ allEvents = toModel->getAllEvents(); ++ ++ if (!allEvents.empty() && !points.empty()) { ++ auto& prevEvent = allEvents.back(); ++ auto& nextEvent = points.front(); ++ ++ // Handle various overlap scenarios ++ if (nextEvent.getFrame() >= prevEvent.getFrame() && ++ nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { ++ ++ // Calculate overlap parameters ++ auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); ++ auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); ++ auto overlapStart = nextEvent.getFrame(); ++ auto overlapDuration = std::min(prevEnd, nextEnd) - overlapStart; ++ ++ // Calculate weighted frequency for the merged event ++ float weightedFreq = calculateWeightedFrequency(prevEvent, nextEvent, overlapStart, overlapDuration); ++ ++ // Choose merge strategy based on overlap characteristics ++ if (overlapDuration < prevEvent.getDuration() * 0.5 && ++ overlapDuration < nextEvent.getDuration() * 0.5) { ++ // Small overlap: merge by extending the earlier event with weighted frequency ++ auto mergedDuration = nextEnd - prevEvent.getFrame(); ++ Event mergedEvent = prevEvent.withDuration(mergedDuration); ++ if (weightedFreq > 0.0f) { ++ mergedEvent = mergedEvent.withValue(weightedFreq); ++ } ++ points[0] = mergedEvent; ++ toModel->remove(prevEvent); ++ } else { ++ // Significant overlap: create merged event with weighted frequency and optimal timing ++ auto mergedStart = prevEvent.getFrame(); ++ auto mergedDuration = std::max(prevEnd, nextEnd) - mergedStart; ++ ++ // Use the event with better timing characteristics (earlier start) ++ Event mergedEvent = prevEvent.withDuration(mergedDuration); ++ if (weightedFreq > 0.0f) { ++ mergedEvent = mergedEvent.withValue(weightedFreq); ++ } ++ ++ // Preserve additional properties from the longer event ++ if (nextEvent.getDuration() > prevEvent.getDuration()) { ++ // Copy label and other properties from longer event if available ++ if (nextEvent.hasLabel() && !prevEvent.hasLabel()) { ++ mergedEvent = mergedEvent.withLabel(nextEvent.getLabel()); ++ } ++ if (nextEvent.hasLevel() && !prevEvent.hasLevel()) { ++ mergedEvent = mergedEvent.withLevel(nextEvent.getLevel()); ++ } ++ } ++ ++ points[0] = mergedEvent; ++ toModel->remove(prevEvent); ++ } ++ } ++ } ++ ++ return points; ++} ++ ++QString ++Analyser::analyseRecording(Selection sel) ++{ ++ QMutexLocker locker(&m_asyncMutex); ++ ++ auto waveFileModel = ModelById::getAs(m_fileModel); ++ if (!waveFileModel) { ++ return "Internal error: Analyser::analyseRecording() called with no model present"; ++ } ++ ++ if (!m_reAnalysingSelection.isEmpty()) { ++ if (sel == m_reAnalysingSelection) { ++ cerr << "selection & range are same as current analysis, ignoring" << endl; ++ return ""; ++ } ++ } ++ ++ if (sel.isEmpty()) return ""; ++ ++ m_reAnalysingSelection = sel; ++ ++ auto* pitchLayer = qobject_cast(m_layers[PitchTrack]); ++ auto* noteLayer = qobject_cast(m_layers[Notes]); ++ ++ TransformFactory* tf = TransformFactory::getInstance(); ++ ++ Transforms transforms; ++ ++ auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); ++ auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); ++ ++ QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); ++ if (!tf->haveTransform(f0_transform)) { ++ return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); ++ } ++ ++ if (!tf->haveTransform(note_transform)) { ++ return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); ++ } ++ ++ Transform t = tf->getDefaultTransformFor ++ (f0_transform, waveFileModel->getSampleRate()); ++ t.setStepSize(256); ++ t.setBlockSize(2048); ++ ++ setAnalysisSettings(t); ++ ++ RealTime start = RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); ++ RealTime end = RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); ++ ++ RealTime duration; ++ ++ if (sel.getEndFrame() > sel.getStartFrame()) { ++ duration = end - start; ++ } ++ ++ cerr << "Analyser::analyseRecording: start " << start << " end " << end << " original selection start " << sel.getStartFrame() << " end " << sel.getEndFrame() << " duration " << duration << endl; ++ ++ if (duration <= RealTime::zeroTime) { ++ cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; ++ return ""; ++ } ++ ++ t.setStartTime(start); ++ t.setDuration(duration); ++ ++ transforms.push_back(t); ++ ++ t.setOutput(PYIN_NOTE_OUT); ++ ++ transforms.push_back(t); ++ ++ std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); ++ ++ ColourDatabase* cdb = ColourDatabase::getInstance(); ++ ++ // Track the temporary layers for cleanup ++ for (auto* layer : layers) { ++ m_realtimeAnalysisLayers.push_back(layer); ++ } ++ ++ for (auto* layer : layers) { ++ ++ FlexiNoteLayer* tempNoteLayer = qobject_cast(layer); ++ TimeValueLayer* tempPitchLayer = qobject_cast(layer); ++ ++ if (tempPitchLayer) { ++ setBaseColour(tempPitchLayer, tr("Black"), cdb); ++ processLayer(tempPitchLayer, pitchLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processPitchModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); ++ } ++ ++ if (tempNoteLayer) { ++ setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); ++ processLayer(tempNoteLayer, noteLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processNoteModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); ++ } ++ } ++ ++ return ""; ++} ++ ++ + QString + Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) + { +@@ -553,7 +958,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) + if (!waveFileModel) { + return "Internal error: Analyser::reAnalyseSelection() called with no model present"; + } +- ++ + if (!m_reAnalysingSelection.isEmpty()) { + if (sel == m_reAnalysingSelection && range == m_reAnalysingRange) { + cerr << "selection & range are same as current analysis, ignoring" << endl; +@@ -584,13 +989,13 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) + } + + TransformFactory *tf = TransformFactory::getInstance(); +- +- QString plugname1 = "pYIN"; ++ + QString plugname2 = "CHP"; + + QString base = "vamp:pyin:localcandidatepyin:"; + QString out = "pitchtrackcandidates"; + ++ + if (range.isConstrained()) { + base = "vamp:chp:constrainedharmonicpeak:"; + out = "peak"; +@@ -600,9 +1005,9 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) + + QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); + if (!tf->haveTransform(base + out)) { +- return notFound.arg(base + out).arg(plugname1).arg(plugname2); ++ return notFound.arg(base + out).arg(PYIN_PLUGIN_NAME).arg(plugname2); + } +- ++ + Transform t = tf->getDefaultTransformFor + (base + out, waveFileModel->getSampleRate()); + t.setStepSize(256); +@@ -626,7 +1031,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) + } else { + endSample -= 9*grid; // MM says: not sure what the CHP plugin does there + } +- RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); ++ RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); + RealTime end = RealTime::frame2RealTime(endSample, waveFileModel->getSampleRate()); + + RealTime duration; +@@ -641,12 +1046,12 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) + cerr << "Analyser::reAnalyseSelection: duration <= 0, not analysing" << endl; + return ""; + } +- ++ + t.setStartTime(start); + t.setDuration(duration); + + transforms.push_back(t); +- ++ + m_currentAsyncHandle = + m_document->createDerivedLayersAsync(transforms, m_fileModel, this); + +@@ -660,7 +1065,7 @@ Analyser::arePitchCandidatesShown() const + } + + void +-Analyser::showPitchCandidates(bool shown) ++Analyser::showPitchCandidates(bool shown) + { + if (m_candidatesVisible == shown) return; + +@@ -687,7 +1092,7 @@ Analyser::layersCreated(Document::LayerCreationAsyncHandle handle, + { + QMutexLocker locker(&m_asyncMutex); + +- if (handle != m_currentAsyncHandle || ++ if (handle != m_currentAsyncHandle || + m_reAnalysingSelection == Selection()) { + // We don't want these! + for (int i = 0; i < (int)primary.size(); ++i) { +@@ -750,14 +1155,14 @@ Analyser::haveHigherPitchCandidate() const + if (m_reAnalysisCandidates.empty()) return false; + return (m_currentCandidate < 0 || + (m_currentCandidate + 1 < (int)m_reAnalysisCandidates.size())); +-} ++} + + bool + Analyser::haveLowerPitchCandidate() const + { + if (m_reAnalysisCandidates.empty()) return false; + return (m_currentCandidate < 0 || m_currentCandidate >= 1); +-} ++} + + void + Analyser::switchPitchCandidate(Selection sel, bool up) +@@ -803,14 +1208,14 @@ void + Analyser::shiftOctave(Selection sel, bool up) + { + float factor = (up ? 2.f : 0.5f); +- ++ + vector actOn; + + Layer *pitchTrack = m_layers[PitchTrack]; + if (pitchTrack) actOn.push_back(pitchTrack); + + foreach (Layer *layer, actOn) { +- ++ + Clipboard clip; + layer->copy(m_pane, sel, clip); + layer->deleteSelection(sel); +@@ -824,7 +1229,7 @@ Analyser::shiftOctave(Selection sel, bool up) + shifted.addPoint(e); + } + } +- ++ + layer->paste(m_pane, shifted, 0, false); + } + } +@@ -849,7 +1254,7 @@ Analyser::abandonReAnalysis(Selection sel) + if (!myLayer) return; + myLayer->deleteSelection(sel); + myLayer->paste(m_pane, m_preAnalysis, 0, false); +-} ++} + + void + Analyser::clearReAnalysis() +@@ -880,7 +1285,7 @@ void + Analyser::layerAboutToBeDeleted(Layer *doomed) + { + cerr << "Analyser::layerAboutToBeDeleted(" << doomed << ")" << endl; +- ++ + vector notDoomed; + + foreach (Layer *layer, m_reAnalysisCandidates) { +@@ -903,7 +1308,7 @@ Analyser::takePitchTrackFrom(Layer *otherLayer) + if (!myModel || !otherModel) return; + + Clipboard clip; +- ++ + Selection sel = Selection(myModel->getStartFrame(), + myModel->getEndFrame()); + myLayer->deleteSelection(sel); +@@ -932,7 +1337,7 @@ Analyser::takePitchTrackFrom(Layer *otherLayer) + void + Analyser::getEnclosingSelectionScope(sv_frame_t f, sv_frame_t &f0, sv_frame_t &f1) + { +- FlexiNoteLayer *flexiNoteLayer = ++ FlexiNoteLayer *flexiNoteLayer = + qobject_cast(m_layers[Notes]); + + sv_frame_t f0i = f, f1i = f; +@@ -942,7 +1347,7 @@ Analyser::getEnclosingSelectionScope(sv_frame_t f, sv_frame_t &f0, sv_frame_t &f + f0 = f1 = f; + return; + } +- ++ + flexiNoteLayer->snapToFeatureFrame(m_pane, f0i, res, Layer::SnapLeft, -1); + flexiNoteLayer->snapToFeatureFrame(m_pane, f1i, res, Layer::SnapRight, -1); + +@@ -976,11 +1381,11 @@ Analyser::loadState(Component c) + } + + void +-Analyser::setIntelligentActions(bool on) ++Analyser::setIntelligentActions(bool on) + { + std::cerr << "toggle setIntelligentActions " << on << std::endl; + +- FlexiNoteLayer *flexiNoteLayer = ++ FlexiNoteLayer *flexiNoteLayer = + qobject_cast(m_layers[Notes]); + if (flexiNoteLayer) { + flexiNoteLayer->setIntelligentActions(on); +@@ -1055,7 +1460,7 @@ Analyser::getGain(Component c) const + return 1.f; + } + } +- ++ + void + Analyser::setGain(Component c, float gain) + { +@@ -1078,7 +1483,7 @@ Analyser::getPan(Component c) const + return 1.f; + } + } +- ++ + void + Analyser::setPan(Component c, float pan) + { +@@ -1089,6 +1494,3 @@ Analyser::setPan(Component c, float pan) + saveState(c); + } + } +- +- +- +diff --git a/main/Analyser.h b/main/Analyser.h +index 918d239..1db40b8 100644 +--- a/main/Analyser.h ++++ b/main/Analyser.h +@@ -5,7 +5,7 @@ + An intonation analysis and annotation tool + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006-2012 Chris Cannam and QMUL. +- ++ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the +@@ -56,9 +56,12 @@ public: + // layers; return "" on success or error string on failure + QString analyseExistingFile(); + ++ // Completes analysis from the last position to the end ++ QString analyseRecordingToEnd(sv::sv_frame_t record_duration); ++ + // Discard any layers etc associated with the current document + void fileClosed(); +- ++ + void setIntelligentActions(bool); + + bool getDisplayFrequencyExtents(double &min, double &max); +@@ -128,7 +131,14 @@ public: + * group in QSettings. + */ + static std::map getAnalysisSettings(); +- ++ ++ /** ++ * Analyse the selection and schedule asynchronous adds of ++ * candidate layers for the region it contains. Returns "" on ++ * success or a user-readable error string on failure. ++ */ ++ QString analyseRecording(sv::Selection sel); ++ + /** + * Analyse the selection and schedule asynchronous adds of + * candidate layers for the region it contains. Returns "" on +@@ -232,6 +242,8 @@ signals: + void initialAnalysisCompleted(); + + protected slots: ++ void updatePitchTrack(sv::ModelId); ++ void updateNoteLayer(sv::ModelId); + void layerAboutToBeDeleted(sv::Layer *); + void layerCompletionChanged(sv::ModelId); + void reAnalyseRegion(sv::sv_frame_t, sv::sv_frame_t, float, float); +@@ -247,8 +259,10 @@ protected: + + sv::Clipboard m_preAnalysis; + sv::Selection m_reAnalysingSelection; ++ sv::sv_frame_t m_analysedFrames = 0; + FrequencyRange m_reAnalysingRange; + std::vector m_reAnalysisCandidates; ++ std::vector m_realtimeAnalysisLayers; // Track temporary layers for cleanup + int m_currentCandidate; + bool m_candidatesVisible; + sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; +@@ -263,13 +277,21 @@ protected: + void discardPitchCandidates(); + + void stackLayers(); +- ++ + // Document::LayerCreationHandler method + void layersCreated(sv::Document::LayerCreationAsyncHandle, + std::vector, std::vector); + + void saveState(Component c) const; + void loadState(Component c); ++ ++ // TODO (alnovi): Move constexpression to a static class ++ static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; ++ static constexpr const char* PYIN_TRANSFORM_BASE = "vamp:pyin:pyin:"; ++ static constexpr const char* PYIN_F0_OUT = "smoothedpitchtrack"; ++ static constexpr const char* PYIN_NOTE_OUT = "notes"; ++ ++ + }; + + #endif +diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp +index cd9fa4a..7046805 100644 +--- a/main/MainWindow.cpp ++++ b/main/MainWindow.cpp +@@ -5,7 +5,7 @@ + An intonation analysis and annotation tool + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006-2012 Chris Cannam and QMUL. +- ++ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the +@@ -102,7 +102,7 @@ using std::endl; + using namespace sv; + + MainWindow::MainWindow(AudioMode audioMode, +- bool withSonification, ++ bool withSonification, + bool withSpectrogram) : + MainWindowBase(audioMode, + MainWindowBase::MIDI_NONE, +@@ -111,7 +111,7 @@ MainWindow::MainWindow(AudioMode audioMode, + m_overview(0), + m_mainMenusCreated(false), + m_playbackMenu(0), +- m_recentFilesMenu(0), ++ m_recentFilesMenu(0), + m_rightButtonMenu(0), + m_rightButtonPlaybackMenu(0), + m_deleteSelectedAction(0), +@@ -186,13 +186,13 @@ MainWindow::MainWindow(AudioMode audioMode, + m_viewManager->setOverlayMode(ViewManager::GlobalOverlays); + + connect(m_viewManager, SIGNAL(selectionChangedByUser()), +- this, SLOT(selectionChangedByUser())); ++ this, SLOT(selectionChangedByUser())); + + QFrame *frame = new QFrame; + setCentralWidget(frame); + + QGridLayout *layout = new QGridLayout; +- ++ + QScrollArea *scroll = new QScrollArea(frame); + scroll->setWidgetResizable(true); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); +@@ -230,7 +230,7 @@ MainWindow::MainWindow(AudioMode audioMode, + } else { + m_panLayer->setBaseColour + (ColourDatabase::getInstance()->getColourIndex(tr("Blue"))); +- } ++ } + + m_fader = new Fader(frame, false); + connect(m_fader, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); +@@ -311,7 +311,7 @@ MainWindow::MainWindow(AudioMode audioMode, + m_activityLog->hide(); + + setAudioRecordMode(RecordReplaceSession); +- ++ + newSession(); + + settings.beginGroup("MainWindow"); +@@ -350,7 +350,7 @@ MainWindow::setupMenus() + // workaround, to remove the appmenu-qt5 package, but that is + // awkward and the problem is so severe that it merits disabling + // the system menubar integration altogether. Like this: +- menuBar()->setNativeMenuBar(false); ++ menuBar()->setNativeMenuBar(false); + #endif + + m_rightButtonMenu = new QMenu(); +@@ -417,7 +417,7 @@ MainWindow::setupFileMenu() + m_keyReference->registerShortcut(action); + menu->addAction(action); + toolbar->addAction(action); +- ++ + icon = il.load("filesaveas"); + action = new QAction(icon, tr("Save Session &As..."), this); + action->setShortcut(tr("Ctrl+Shift+S")); +@@ -455,7 +455,7 @@ MainWindow::setupFileMenu() + menu->addAction(action); + + menu->addSeparator(); +- ++ + action = new QAction(tr("Browse Recorded Audio"), this); + action->setStatusTip(tr("Open the Recorded Audio folder in the system file browser")); + connect(action, SIGNAL(triggered()), this, SLOT(browseRecordedAudio())); +@@ -484,17 +484,17 @@ MainWindow::setupEditMenu() + m_keyReference->setCategory + (tr("Selection Strip Mouse Actions")); + m_keyReference->registerShortcut +- (tr("Jump"), tr("Left"), ++ (tr("Jump"), tr("Left"), + tr("Click left button to move the playback position to a time")); + m_keyReference->registerShortcut +- (tr("Select"), tr("Left"), ++ (tr("Select"), tr("Left"), + tr("Click left button and drag to select a region of time")); + m_keyReference->registerShortcut +- (tr("Select Note Duration"), tr("Double-Click Left"), ++ (tr("Select Note Duration"), tr("Double-Click Left"), + tr("Double-click left button to select the region of time corresponding to a note")); + + QToolBar *toolbar = addToolBar(tr("Tools Toolbar")); +- ++ + CommandHistory::getInstance()->registerToolbar(toolbar); + + QActionGroup *group = new QActionGroup(this); +@@ -517,18 +517,18 @@ MainWindow::setupEditMenu() + m_keyReference->setCategory + (tr("Navigate Tool Mouse Actions")); + m_keyReference->registerShortcut +- (tr("Navigate"), tr("Left"), ++ (tr("Navigate"), tr("Left"), + tr("Click left button and drag to move around")); + m_keyReference->registerShortcut +- (tr("Re-Analyse Area"), tr("Shift+Left"), ++ (tr("Re-Analyse Area"), tr("Shift+Left"), + tr("Shift-click left button and drag to define a specific pitch and time range to re-analyse")); + m_keyReference->registerShortcut +- (tr("Edit"), tr("Double-Click Left"), ++ (tr("Edit"), tr("Double-Click Left"), + tr("Double-click left button on an item to edit it")); + + m_keyReference->setCategory(tr("Tool Selection")); + action = toolbar->addAction(il.load("move"), +- tr("Edit")); ++ tr("Edit")); + action->setCheckable(true); + action->setShortcut(tr("2")); + action->setStatusTip(tr("Edit with Note Intelligence")); +@@ -540,16 +540,16 @@ MainWindow::setupEditMenu() + m_keyReference->setCategory + (tr("Note Edit Tool Mouse Actions")); + m_keyReference->registerShortcut +- (tr("Adjust Pitch"), tr("Left"), ++ (tr("Adjust Pitch"), tr("Left"), + tr("Click left button on the main part of a note and drag to move it up or down")); + m_keyReference->registerShortcut +- (tr("Split"), tr("Left"), ++ (tr("Split"), tr("Left"), + tr("Click left button on the bottom edge of a note to split it at the click point")); + m_keyReference->registerShortcut +- (tr("Resize"), tr("Left"), ++ (tr("Resize"), tr("Left"), + tr("Click left button on the left or right edge of a note and drag to change the time or duration of the note")); + m_keyReference->registerShortcut +- (tr("Erase"), tr("Shift+Left"), ++ (tr("Erase"), tr("Shift+Left"), + tr("Shift-click left button on a note to remove it")); + + +@@ -557,7 +557,7 @@ MainWindow::setupEditMenu() + + m_keyReference->setCategory(tr("Tool Selection")); + action = toolbar->addAction(il.load("notes"), +- tr("Free Edit")); ++ tr("Free Edit")); + action->setCheckable(true); + action->setShortcut(tr("3")); + action->setStatusTip(tr("Free Edit")); +@@ -567,7 +567,7 @@ MainWindow::setupEditMenu() + */ + + menu->addSeparator(); +- ++ + m_keyReference->setCategory(tr("Selection")); + + action = new QAction(tr("Select &All"), this); +@@ -593,9 +593,9 @@ MainWindow::setupEditMenu() + + menu->addSeparator(); + m_rightButtonMenu->addSeparator(); +- ++ + m_keyReference->setCategory(tr("Pitch Track")); +- ++ + action = new QAction(tr("Choose Higher Pitch"), this); + action->setShortcut(tr("Ctrl+Up")); + action->setStatusTip(tr("Move pitches up an octave, or to the next higher pitch candidate")); +@@ -604,7 +604,7 @@ MainWindow::setupEditMenu() + connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); +- ++ + action = new QAction(tr("Choose Lower Pitch"), this); + action->setShortcut(tr("Ctrl+Down")); + action->setStatusTip(tr("Move pitches down an octave, or to the next lower pitch candidate")); +@@ -622,7 +622,7 @@ MainWindow::setupEditMenu() + connect(this, SIGNAL(canClearSelection(bool)), m_showCandidatesAction, SLOT(setEnabled(bool))); + menu->addAction(m_showCandidatesAction); + m_rightButtonMenu->addAction(m_showCandidatesAction); +- ++ + action = new QAction(tr("Remove Pitches"), this); + action->setShortcut(tr("Ctrl+Backspace")); + action->setStatusTip(tr("Remove all pitch estimates within the selected region, making it unvoiced")); +@@ -634,7 +634,7 @@ MainWindow::setupEditMenu() + + menu->addSeparator(); + m_rightButtonMenu->addSeparator(); +- ++ + m_keyReference->setCategory(tr("Note Track")); + + action = new QAction(tr("Split Note"), this); +@@ -663,7 +663,7 @@ MainWindow::setupEditMenu() + connect(this, SIGNAL(canSnapNotes(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); +- ++ + action = new QAction(tr("Form Note from Selection"), this); + action->setShortcut(tr("=")); + action->setStatusTip(tr("Form a note spanning the selected region, splitting any existing notes at its boundaries")); +@@ -702,7 +702,7 @@ MainWindow::setupViewMenu() + connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); +- ++ + action = new QAction(tr("Peek &Right"), this); + action->setShortcut(tr("Alt+Right")); + action->setStatusTip(tr("Scroll the current pane to the right without changing the play position")); +@@ -723,7 +723,7 @@ MainWindow::setupViewMenu() + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); +- ++ + action = new QAction(il.load("zoom-out"), + tr("Zoom &Out"), this); + action->setShortcut(tr("Down")); +@@ -732,7 +732,7 @@ MainWindow::setupViewMenu() + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); +- ++ + action = new QAction(tr("Restore &Default Zoom"), this); + action->setStatusTip(tr("Restore the zoom level to the default")); + connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault())); +@@ -749,7 +749,7 @@ MainWindow::setupViewMenu() + menu->addAction(action); + + menu->addSeparator(); +- ++ + action = new QAction(tr("Set Displayed Fre&quency Range..."), this); + action->setStatusTip(tr("Set the minimum and maximum frequencies in the visible display")); + connect(action, SIGNAL(triggered()), this, SLOT(editDisplayExtents())); +@@ -768,11 +768,32 @@ MainWindow::setupAnalysisMenu() + QMenu *menu = menuBar()->addMenu(tr("&Analysis")); + menu->setTearOffEnabled(true); + ++ // Create a submenu for analysis mode options ++ QMenu *modeMenu = menu->addMenu(tr("Analysis &Mode")); ++ modeMenu->setTearOffEnabled(true); ++ ++ // Create a radio button group for recording analysis mode ++ QActionGroup *analysisGroup = new QActionGroup(this); ++ ++ m_analyseAfterRecord = new QAction(tr("Analyse &After Recording"), this); ++ m_analyseAfterRecord->setStatusTip(tr("Automatically trigger analysis after recording is complete.")); ++ m_analyseAfterRecord->setCheckable(true); ++ m_analyseAfterRecord->setActionGroup(analysisGroup); ++ connect(m_analyseAfterRecord, SIGNAL(triggered()), this, SLOT(analyseAfterRecordToggled())); ++ modeMenu->addAction(m_analyseAfterRecord); ++ ++ m_analyseDuringRecord = new QAction(tr("Analyse &During Recording"), this); ++ m_analyseDuringRecord->setStatusTip(tr("Automatically trigger analysis during recording.")); ++ m_analyseDuringRecord->setCheckable(true); ++ m_analyseDuringRecord->setActionGroup(analysisGroup); ++ connect(m_analyseDuringRecord, SIGNAL(triggered()), this, SLOT(recordAnalysisToggled())); ++ modeMenu->addAction(m_analyseDuringRecord); ++ + m_autoAnalyse = new QAction(tr("Auto-Analyse &New Audio"), this); + m_autoAnalyse->setStatusTip(tr("Automatically trigger analysis upon opening of a new audio file.")); + m_autoAnalyse->setCheckable(true); + connect(m_autoAnalyse, SIGNAL(triggered()), this, SLOT(autoAnalysisToggled())); +- menu->addAction(m_autoAnalyse); ++ modeMenu->addAction(m_autoAnalyse); + + action = new QAction(tr("&Analyse Now!"), this); + action->setStatusTip(tr("Trigger analysis of pitches and notes. (This will delete all existing pitches and notes.)")); +@@ -822,8 +843,8 @@ MainWindow::resetAnalyseOptions() + QSettings settings; + settings.beginGroup("Analyser"); + +- settings.setValue("auto-analysis", true); +- ++ setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); ++ + auto keyMap = Analyser::getAnalysisSettings(); + for (auto p: keyMap) { + settings.setValue(p.first, p.second); +@@ -833,14 +854,50 @@ MainWindow::resetAnalyseOptions() + updateAnalyseStates(); + } + ++MainWindow::RecordingAnalysisMode ++MainWindow::getRecordingAnalysisMode() const ++{ ++ QSettings settings; ++ settings.beginGroup("Analyser"); ++ int mode = settings.value("recording-analysis-mode", ++ static_cast(RecordingAnalysisMode::AfterRecording)).toInt(); ++ settings.endGroup(); ++ return static_cast(mode); ++} ++ ++void ++MainWindow::setRecordingAnalysisMode(RecordingAnalysisMode mode) ++{ ++ QSettings settings; ++ settings.beginGroup("Analyser"); ++ settings.setValue("recording-analysis-mode", static_cast(mode)); ++ settings.endGroup(); ++ updateAnalyseStates(); ++} ++ + void + MainWindow::updateAnalyseStates() + { + QSettings settings; + settings.beginGroup("Analyser"); + +- bool autoAnalyse = settings.value("auto-analysis", true).toBool(); +- m_autoAnalyse->setChecked(autoAnalyse); ++ // Handle recording analysis mode with enum-based approach ++ RecordingAnalysisMode mode = getRecordingAnalysisMode(); ++ ++ switch (mode) { ++ case RecordingAnalysisMode::AfterRecording: ++ m_analyseAfterRecord->setChecked(true); ++ m_analyseDuringRecord->setChecked(false); ++ m_autoAnalyse->setChecked(true); ++ m_autoAnalyse->setEnabled(true); ++ break; ++ case RecordingAnalysisMode::DuringRecording: ++ m_analyseDuringRecord->setChecked(true); ++ m_analyseAfterRecord->setChecked(false); ++ m_autoAnalyse->setChecked(false); ++ m_autoAnalyse->setEnabled(false); ++ break; ++ } + + std::map actions { + { "precision-analysis", m_precise }, +@@ -850,7 +907,7 @@ MainWindow::updateAnalyseStates() + }; + + auto keyMap = Analyser::getAnalysisSettings(); +- ++ + for (auto p: actions) { + auto ki = keyMap.find(p.first); + if (ki != keyMap.end()) { +@@ -864,6 +921,30 @@ MainWindow::updateAnalyseStates() + settings.endGroup(); + } + ++void ++MainWindow::recordAnalysisToggled() ++{ ++ QAction *a = qobject_cast(sender()); ++ if (!a) return; ++ ++ bool set = a->isChecked(); ++ if (set) { ++ setRecordingAnalysisMode(RecordingAnalysisMode::DuringRecording); ++ } ++} ++ ++void ++MainWindow::analyseAfterRecordToggled() ++{ ++ QAction *a = qobject_cast(sender()); ++ if (!a) return; ++ ++ bool set = a->isChecked(); ++ if (set) { ++ setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); ++ } ++} ++ + void + MainWindow::autoAnalysisToggled() + { +@@ -962,7 +1043,7 @@ MainWindow::setupHelpMenu() + { + QMenu *menu = menuBar()->addMenu(tr("&Help")); + menu->setTearOffEnabled(true); +- ++ + m_keyReference->setCategory(tr("Help")); + + IconLoader il; +@@ -971,9 +1052,9 @@ MainWindow::setupHelpMenu() + QAction *action; + + action = new QAction(il.load("help"), +- tr("&Help Reference"), this); ++ tr("&Help Reference"), this); + action->setShortcut(tr("F1")); +- action->setStatusTip(tr("Open the %1 reference manual").arg(name)); ++ action->setStatusTip(tr("Open the %1 reference manual").arg(name)); + connect(action, SIGNAL(triggered()), this, SLOT(help())); + m_keyReference->registerShortcut(action); + menu->addAction(action); +@@ -984,14 +1065,14 @@ MainWindow::setupHelpMenu() + connect(action, SIGNAL(triggered()), this, SLOT(keyReference())); + m_keyReference->registerShortcut(action); + menu->addAction(action); +- +- action = new QAction(tr("What's &New In This Release?"), this); +- action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); ++ ++ action = new QAction(tr("What's &New In This Release?"), this); ++ action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); + connect(action, SIGNAL(triggered()), this, SLOT(whatsNew())); + menu->addAction(action); +- +- action = new QAction(tr("&About %1").arg(name), this); +- action->setStatusTip(tr("Show information about %1").arg(name)); ++ ++ action = new QAction(tr("&About %1").arg(name), this); ++ action->setStatusTip(tr("Show information about %1").arg(name)); + connect(action, SIGNAL(triggered()), this, SLOT(about())); + menu->addAction(action); + } +@@ -1076,10 +1157,14 @@ MainWindow::setupToolbars() + recordAction->setShortcut(tr("Ctrl+Space")); + recordAction->setStatusTip(tr("Record a new audio file")); + connect(recordAction, SIGNAL(triggered()), this, SLOT(record())); ++ ++ connect(recordAction, SIGNAL(triggered()), ++ this, SLOT(analyseDuringRecordingRunner())); ++ + connect(m_recordTarget, SIGNAL(recordStatusChanged(bool)), +- recordAction, SLOT(setChecked(bool))); ++ recordAction, SLOT(setChecked(bool))); + connect(m_recordTarget, SIGNAL(recordCompleted()), +- this, SLOT(analyseNow())); ++ this, SLOT(analyseNow())); + connect(this, SIGNAL(canRecord(bool)), + recordAction, SLOT(setEnabled(bool))); + +@@ -1112,7 +1197,7 @@ MainWindow::setupToolbars() + oneLeftAction->setStatusTip(tr("Move cursor to the preceding note (or silence) onset.")); + connect(oneLeftAction, SIGNAL(triggered()), this, SLOT(moveOneNoteLeft())); + connect(this, SIGNAL(canScroll(bool)), oneLeftAction, SLOT(setEnabled(bool))); +- ++ + QAction *oneRightAction = new QAction(tr("O&ne Note Right"), this); + oneRightAction->setShortcut(tr("Ctrl+Right")); + oneRightAction->setStatusTip(tr("Move cursor to the succeeding note (or silence).")); +@@ -1124,7 +1209,7 @@ MainWindow::setupToolbars() + selectOneLeftAction->setStatusTip(tr("Select to the preceding note (or silence) onset.")); + connect(selectOneLeftAction, SIGNAL(triggered()), this, SLOT(selectOneNoteLeft())); + connect(this, SIGNAL(canScroll(bool)), selectOneLeftAction, SLOT(setEnabled(bool))); +- ++ + QAction *selectOneRightAction = new QAction(tr("S&elect One Note Right"), this); + selectOneRightAction->setShortcut(tr("Ctrl+Shift+Right")); + selectOneRightAction->setStatusTip(tr("Select to the succeeding note (or silence).")); +@@ -1186,7 +1271,7 @@ MainWindow::setupToolbars() + fastAction->setStatusTip(tr("Time-stretch playback to speed it up without changing pitch")); + connect(fastAction, SIGNAL(triggered()), this, SLOT(speedUpPlayback())); + connect(this, SIGNAL(canSpeedUpPlayback(bool)), fastAction, SLOT(setEnabled(bool))); +- ++ + QAction *slowAction = menu->addAction(tr("Slow Down")); + slowAction->setShortcut(tr("Ctrl+PgDown")); + slowAction->setStatusTip(tr("Time-stretch playback to slow it down without changing pitch")); +@@ -1234,7 +1319,7 @@ MainWindow::setupToolbars() + lpwSize = m_viewManager->scalePixelSize(26); + bigLpwSize = int(lpwSize * 2.8); + #endif +- ++ + m_audioLPW->setImageSize(lpwSize); + m_audioLPW->setBigImageSize(bigLpwSize); + toolbar->addWidget(m_audioLPW); +@@ -1303,7 +1388,7 @@ MainWindow::setupToolbars() + Pane::registerShortcuts(*m_keyReference); + + updateLayerStatuses(); +- ++ + // QTimer::singleShot(500, this, SLOT(betaReleaseWarning())); + } + +@@ -1340,7 +1425,7 @@ MainWindow::moveByOneNote(bool right, bool doSelect) + { + sv_frame_t frame = m_viewManager->getPlaybackFrame(); + cerr << "MainWindow::moveByOneNote startframe: " << frame << endl; +- ++ + bool isAtSelectionBoundary = false; + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + if (!selections.empty()) { +@@ -1360,7 +1445,7 @@ MainWindow::moveByOneNote(bool right, bool doSelect) + //!!! This seems like a strange and inefficient way to do this - + //!!! there is almost certainly a better way making use of + //!!! EventSeries api +- ++ + EventVector points = model->getAllEvents(); + if (points.empty()) return; + +@@ -1434,24 +1519,24 @@ MainWindow::updateMenuStates() + if (currentPane) currentLayer = currentPane->getSelectedLayer(); + + bool haveMainModel = +- (getMainModel() != 0); ++ (getMainModel() != 0); + bool havePlayTarget = +- (m_playTarget != 0 || m_audioIO != 0); ++ (m_playTarget != 0 || m_audioIO != 0); + bool haveCurrentPane = + (currentPane != 0); + bool haveCurrentLayer = + (haveCurrentPane && + (currentLayer != 0)); +- bool haveSelection = ++ bool haveSelection = + (m_viewManager && + !m_viewManager->getSelections().empty()); +- bool haveCurrentTimeInstantsLayer = ++ bool haveCurrentTimeInstantsLayer = + (haveCurrentLayer && + qobject_cast(currentLayer)); +- bool haveCurrentTimeValueLayer = ++ bool haveCurrentTimeValueLayer = + (haveCurrentLayer && + qobject_cast(currentLayer)); +- bool pitchCandidatesVisible = ++ bool pitchCandidatesVisible = + m_analyser->arePitchCandidatesShown(); + + emit canChangePlaybackSpeed(true); +@@ -1463,11 +1548,11 @@ MainWindow::updateMenuStates() + m_analyser->isVisible(Analyser::Audio) && + m_analyser->getLayer(Analyser::Audio); + +- bool havePitchTrack = ++ bool havePitchTrack = + m_analyser->isVisible(Analyser::PitchTrack) && + m_analyser->getLayer(Analyser::PitchTrack); + +- bool haveNotes = ++ bool haveNotes = + m_analyser->isVisible(Analyser::Notes) && + m_analyser->getLayer(Analyser::Notes); + +@@ -1611,7 +1696,7 @@ MainWindow::updateLayerStatuses() + m_audioLPW->setEnabled(m_analyser->isAudible(Analyser::Audio)); + m_audioLPW->setLevel(m_analyser->getGain(Analyser::Audio)); + m_audioLPW->setPan(m_analyser->getPan(Analyser::Audio)); +- ++ + m_showPitch->setChecked(m_analyser->isVisible(Analyser::PitchTrack)); + m_playPitch->setChecked(m_analyser->isAudible(Analyser::PitchTrack)); + m_pitchLPW->setEnabled(m_analyser->isAudible(Analyser::PitchTrack)); +@@ -1633,7 +1718,7 @@ MainWindow::editDisplayExtents() + double min, max; + double vmin = 0; + double vmax = getMainModel()->getSampleRate() /2; +- ++ + if (!m_analyser->getDisplayFrequencyExtents(min, max)) { + //!!! + return; +@@ -1691,7 +1776,7 @@ MainWindow::newSession() + + m_viewManager->setGlobalCentreFrame + (pane->getFrameForX(width() / 2)); +- ++ + connect(pane, SIGNAL(contextHelpChanged(const QString &)), + this, SLOT(contextHelpChanged(const QString &))); + +@@ -1730,7 +1815,7 @@ MainWindow::closeSession() + m_document->removeLayerFromView + (pane, pane->getLayer(pane->getLayerCount() - 1)); + } +- ++ + m_overview->unregisterView(pane); + m_paneStack->deletePane(pane); + } +@@ -1739,12 +1824,12 @@ MainWindow::closeSession() + + Pane *pane = m_paneStack->getHiddenPane + (m_paneStack->getHiddenPaneCount() - 1); +- ++ + while (pane->getLayerCount() > 0) { + m_document->removeLayerFromView + (pane, pane->getLayer(pane->getLayerCount() - 1)); + } +- ++ + m_overview->unregisterView(pane); + m_paneStack->deletePane(pane); + } +@@ -1818,7 +1903,7 @@ MainWindow::openRecentFile() + { + QObject *obj = sender(); + QAction *action = qobject_cast(obj); +- ++ + if (!action) { + cerr << "WARNING: MainWindow::openRecentFile: sender is not an action" + << endl; +@@ -1845,19 +1930,19 @@ MainWindow::paneAdded(Pane *pane) + pane->setPlaybackFollow(PlaybackScrollPage); + m_paneStack->sizePanesEqually(); + if (m_overview) m_overview->registerView(pane); +-} ++} + + void + MainWindow::paneHidden(Pane *pane) + { +- if (m_overview) m_overview->unregisterView(pane); +-} ++ if (m_overview) m_overview->unregisterView(pane); ++} + + void + MainWindow::paneAboutToBeDeleted(Pane *pane) + { +- if (m_overview) m_overview->unregisterView(pane); +-} ++ if (m_overview) m_overview->unregisterView(pane); ++} + + void + MainWindow::paneDropAccepted(Pane *pane, QStringList uriList) +@@ -1884,8 +1969,8 @@ MainWindow::paneDropAccepted(Pane *pane, QString text) + if (pane) m_paneStack->setCurrentPane(pane); + + QUrl testUrl(text); +- if (testUrl.scheme() == "file" || +- testUrl.scheme() == "http" || ++ if (testUrl.scheme() == "file" || ++ testUrl.scheme() == "http" || + testUrl.scheme() == "ftp") { + QStringList list; + list.push_back(text); +@@ -1950,7 +2035,7 @@ MainWindow::commitData(bool mayAskUser) + } else { + if (!QFileInfo(svDir).isDir()) return false; + } +- ++ + // This name doesn't have to be unguessable + #ifndef _WIN32 + QString fname = QString("tmp-%1-%2.sv") +@@ -1979,7 +2064,7 @@ MainWindow::checkSaveModified() + + if (!m_documentModified) return true; + +- int button = ++ int button = + QMessageBox::warning(this, + tr("Session modified"), + tr("The current session has been modified.\nDo you want to save it?"), +@@ -2009,7 +2094,7 @@ MainWindow::waitForInitialAnalysis() + // initial analysis is happening, because then we end up with an + // incomplete session on reload. There are certainly theoretically + // better ways to handle this... +- ++ + QSettings settings; + settings.beginGroup("Analyser"); + bool autoAnalyse = settings.value("auto-analysis", true).toBool(); +@@ -2029,7 +2114,7 @@ MainWindow::waitForInitialAnalysis() + QMessageBox::Cancel, + this); + +- connect(m_analyser, SIGNAL(initialAnalysisCompleted()), ++ connect(m_analyser, SIGNAL(initialAnalysisCompleted()), + &mb, SLOT(accept())); + + if (mb.exec() == QDialog::Accepted) { +@@ -2164,14 +2249,14 @@ MainWindow::exportToSVL(QString path, Layer *layer) + << "\n" + << "\n" + << " \n"; +- ++ + model->toXml(out, " "); +- ++ + out << " \n" + << " \n"; +- ++ + layer->toXml(out, " "); +- ++ + out << " \n" + << "\n"; + +@@ -2206,10 +2291,10 @@ MainWindow::importPitchLayer(FileSource source) + source.waitForData(); + + if (!waitForInitialAnalysis()) return FileOpenCancelled; +- ++ + QString path = source.getLocalFilename(); + +- RDFImporter::RDFDocumentType rdfType = ++ RDFImporter::RDFDocumentType rdfType = + RDFImporter::identifyDocumentType(QUrl::fromLocalFile(path).toString()); + + if (rdfType != RDFImporter::NotRDF) { +@@ -2221,12 +2306,12 @@ MainWindow::importPitchLayer(FileSource source) + (source.getExtension().toLower() == "xml" && + (SVFileReader::identifyXmlFile(source.getLocalFilename()) + == SVFileReader::SVLayerFile))) { +- ++ + //!!! + return FileOpenFailed; + + } else { +- ++ + try { + + CSVFormat format(path); +@@ -2246,7 +2331,7 @@ MainWindow::importPitchLayer(FileSource source) + + ModelId modelId = ModelById::add + (std::shared_ptr(model)); +- ++ + CommandHistory::getInstance()->startCompoundOperation + (tr("Import Pitch Track"), true); + +@@ -2272,7 +2357,7 @@ MainWindow::importPitchLayer(FileSource source) + } + } + } +- ++ + return FileOpenFailed; + } + +@@ -2292,7 +2377,7 @@ MainWindow::exportPitchLayer() + if (path == "") return; + + if (!waitForInitialAnalysis()) return; +- ++ + if (QFileInfo(path).suffix() == "") path += ".svl"; + + QString suffix = QFileInfo(path).suffix().toLower(); +@@ -2314,7 +2399,7 @@ MainWindow::exportPitchLayer() + } else { + + DataExportOptions options = DataExportFillGaps; +- ++ + CSVFileWriter writer(path, model.get(), + ((suffix == "csv") ? "," : "\t"), + options); +@@ -2358,7 +2443,7 @@ MainWindow::exportNoteLayer() + error = exportToSVL(path, layer); + + } else if (suffix == "mid" || suffix == "midi") { +- ++ + MIDIFileWriter writer(path, model.get(), model->getSampleRate()); + writer.write(); + if (!writer.isOK()) { +@@ -2376,7 +2461,7 @@ MainWindow::exportNoteLayer() + } else { + + DataExportOptions options = DataExportOmitLevel; +- ++ + CSVFileWriter writer(path, model.get(), + ((suffix == "csv") ? "," : "\t"), + options); +@@ -2411,7 +2496,7 @@ MainWindow::doubleClickSelectInvoked(sv_frame_t frame) + { + sv_frame_t f0, f1; + m_analyser->getEnclosingSelectionScope(frame, f0, f1); +- ++ + cerr << "MainWindow::doubleClickSelectInvoked(" << frame << "): [" << f0 << "," << f1 << "]" << endl; + + Selection sel(f0, f1); +@@ -2496,11 +2581,11 @@ MainWindow::regionOutlined(QRect r) + + sv_frame_t f0 = pane->getFrameForX(r.x()); + sv_frame_t f1 = pane->getFrameForX(r.x() + r.width()); +- ++ + double v0 = spectrogram->getFrequencyForY(pane, r.y() + r.height()); + double v1 = spectrogram->getFrequencyForY(pane, r.y()); + +- cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 ++ cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 + << ", frequency " << v0 << " -> " << v1 << endl; + + m_pendingConstraint = Analyser::FrequencyRange(v0, v1); +@@ -2589,7 +2674,7 @@ MainWindow::switchPitchDown() + (tr("Choose Lower Pitch Candidate"), true); + + MultiSelection::SelectionList selections = m_viewManager->getSelections(); +- ++ + for (MultiSelection::SelectionList::iterator k = selections.begin(); + k != selections.end(); ++k) { + m_analyser->switchPitchCandidate(*k, false); +@@ -2613,12 +2698,12 @@ MainWindow::snapNotesToPitches() + + CommandHistory::getInstance()->startCompoundOperation + (tr("Snap Notes to Pitches"), true); +- ++ + for (MultiSelection::SelectionList::iterator k = selections.begin(); + k != selections.end(); ++k) { + auxSnapNotes(*k); + } +- ++ + CommandHistory::getInstance()->endCompoundOperation(); + } + } +@@ -2632,7 +2717,7 @@ MainWindow::auxSnapNotes(Selection s) + if (!layer) return; + + layer->snapSelectedNotesToPitchTrack(m_analyser->getPane(), s); +-} ++} + + void + MainWindow::splitNote() +@@ -2657,12 +2742,12 @@ MainWindow::mergeNotes() + + CommandHistory::getInstance()->startCompoundOperation + (tr("Merge Notes"), true); +- ++ + for (MultiSelection::SelectionList::iterator k = selections.begin(); + k != selections.end(); ++k) { + layer->mergeNotes(m_analyser->getPane(), *k, true); + } +- ++ + CommandHistory::getInstance()->endCompoundOperation(); + } + } +@@ -2680,12 +2765,12 @@ MainWindow::deleteNotes() + + CommandHistory::getInstance()->startCompoundOperation + (tr("Delete Notes"), true); +- ++ + for (MultiSelection::SelectionList::iterator k = selections.begin(); + k != selections.end(); ++k) { + layer->deleteSelectionInclusive(*k); + } +- ++ + CommandHistory::getInstance()->endCompoundOperation(); + } + } +@@ -2703,7 +2788,7 @@ MainWindow::formNoteFromSelection() + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + + if (!selections.empty()) { +- ++ + CommandHistory::getInstance()->startCompoundOperation + (tr("Form Note from Selection"), true); + +@@ -2717,7 +2802,7 @@ MainWindow::formNoteFromSelection() + // existing pitch track if possible. This way we should + // handle all the possible cases of existing notes that + // may or may not overlap the start or end times +- ++ + sv_frame_t start = k->getStartFrame(); + sv_frame_t end = k->getEndFrame(); + +@@ -2728,18 +2813,18 @@ MainWindow::formNoteFromSelection() + if (!existing.empty()) { + defaultPitch = int(roundf(existing.begin()->getValue())); + } +- ++ + layer->splitNotesAt(pane, start); + layer->splitNotesAt(pane, end); + layer->deleteSelection(*k); +- ++ + layer->addNoteOn(start, defaultPitch, 100); + layer->addNoteOff(end, defaultPitch); +- ++ + layer->mergeNotes(pane, *k, false); + } + +- CommandHistory::getInstance()->endCompoundOperation(); ++ CommandHistory::getInstance()->endCompoundOperation(); + } + } + +@@ -2758,7 +2843,7 @@ MainWindow::playSpeedChanged(int position) + + char pcbuf[30]; + char facbuf[30]; +- ++ + if (position == centre) { + contextHelpChanged(tr("Playback speed: Normal")); + } else if (position < centre) { +@@ -2802,7 +2887,7 @@ MainWindow::playMonoToggled() + + playSpeedChanged(m_playSpeed->value()); + // TODO: pitch gain? +-} ++} + + void + MainWindow::speedUpPlayback() +@@ -2841,7 +2926,7 @@ MainWindow::audioGainChanged(float gain) + m_analyser->setGain(Analyser::Audio, gain); + } + updateMenuStates(); +-} ++} + + void + MainWindow::pitchGainChanged(float gain) +@@ -2856,7 +2941,7 @@ MainWindow::pitchGainChanged(float gain) + m_analyser->setGain(Analyser::PitchTrack, gain); + } + updateMenuStates(); +-} ++} + + void + MainWindow::notesGainChanged(float gain) +@@ -2871,7 +2956,7 @@ MainWindow::notesGainChanged(float gain) + m_analyser->setGain(Analyser::Notes, gain); + } + updateMenuStates(); +-} ++} + + void + MainWindow::audioPanChanged(float pan) +@@ -2879,7 +2964,7 @@ MainWindow::audioPanChanged(float pan) + contextHelpChanged(tr("Audio Pan: %1").arg(pan)); + m_analyser->setPan(Analyser::Audio, pan); + updateMenuStates(); +-} ++} + + void + MainWindow::pitchPanChanged(float pan) +@@ -2887,7 +2972,7 @@ MainWindow::pitchPanChanged(float pan) + contextHelpChanged(tr("Pitch Pan: %1").arg(pan)); + m_analyser->setPan(Analyser::PitchTrack, pan); + updateMenuStates(); +-} ++} + + void + MainWindow::notesPanChanged(float pan) +@@ -2895,7 +2980,7 @@ MainWindow::notesPanChanged(float pan) + contextHelpChanged(tr("Notes Pan: %1").arg(pan)); + m_analyser->setPan(Analyser::Notes, pan); + updateMenuStates(); +-} ++} + + void + MainWindow::updateVisibleRangeDisplay(Pane *p) const +@@ -2944,7 +3029,7 @@ MainWindow::updateVisibleRangeDisplay(Pane *p) const + m_myStatusMessage = tr("Visible: %1 to %2 (duration %3)") + .arg(startStr).arg(endStr).arg(durationStr); + } +- ++ + getStatusLabel()->setText(m_myStatusMessage); + } + +@@ -3045,6 +3130,30 @@ MainWindow::analyseNow() + } + } + ++void ++MainWindow::analyseDuringRecordingRunner() ++{ ++ QSettings settings; ++ settings.beginGroup("Analyser"); ++ bool recordAnalyse = settings.value("record-analysis", true).toBool(); ++ settings.endGroup(); ++ if ((recordAnalyse && this->m_recordTarget->isRecording())) ++ { ++ connect(this->m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, this, &MainWindow::analyseDuringRecording); ++ connect(this->m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, this, [this]() { ++ disconnect(this->m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, nullptr, nullptr); ++ disconnect(this->m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, nullptr, nullptr); ++ analyseDuringRecording(); ++ }); ++ } ++} ++ ++void ++MainWindow::analyseDuringRecording() ++{ ++ m_analyser->analyseRecordingToEnd(m_recordTarget->getRecordDuration()); ++} ++ + void + MainWindow::analyseNewMainModel() + { +@@ -3053,7 +3162,7 @@ MainWindow::analyseNewMainModel() + SVDEBUG << "MainWindow::analyseNewMainModel: main model is " << model << endl; + + SVDEBUG << "(document is " << m_document << ", it says main model is " << m_document->getMainModel() << ")" << endl; +- ++ + if (!model) { + cerr << "no main model!" << endl; + return; +@@ -3117,7 +3226,7 @@ MainWindow::analyseNewMainModel() + m_analyser->setAudible(Analyser::PitchTrack, false); + m_analyser->setAudible(Analyser::Notes, false); + } +- ++ + updateLayerStatuses(); + documentRestored(); + } +@@ -3265,16 +3374,16 @@ MainWindow::whatsNew() + + QDialog *d = new QDialog(this); + d->setWindowTitle(tr("What's New")); +- ++ + QGridLayout *layout = new QGridLayout; + d->setLayout(layout); + + int row = 0; +- ++ + QLabel *iconLabel = new QLabel; + iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64)); + layout->addWidget(iconLabel, row, 0); +- ++ + layout->addWidget + (new QLabel(tr("

What's New in %1

") + .arg(QApplication::applicationName())), +@@ -3287,7 +3396,7 @@ MainWindow::whatsNew() + if (m_newerVersionIs != "") { + layout->addWidget(new QLabel(tr("Note: A newer version of %1 is available.
(Version %2 is available; you are using version %3)").arg(QApplication::applicationName()).arg(m_newerVersionIs).arg(TONY_VERSION)), row++, 1, 1, 2); + } +- ++ + QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok); + layout->addWidget(bb, row++, 0, 1, 3); + connect(bb, SIGNAL(accepted()), d, SLOT(accept())); +@@ -3316,7 +3425,7 @@ MainWindow::whatsNew() + + d->setMinimumSize(m_viewManager->scalePixelSize(520), + m_viewManager->scalePixelSize(450)); +- ++ + d->exec(); + + delete d; +@@ -3360,7 +3469,7 @@ MainWindow::about() + aboutText += tr("

Using Qt framework version %1.

") + .arg(QT_VERSION_STR); + +- aboutText += ++ aboutText += + "

Copyright © 2005–2019 Chris Cannam, Queen Mary University of London, and the Tony project authors: Matthias Mauch, George Fazekas, Justin Salamon, and Rachel Bittner.

" + "

pYIN analysis plugin written by Matthias Mauch.

" + "

This program is free software; you can redistribute it and/or " +@@ -3368,18 +3477,18 @@ MainWindow::about() + "published by the Free Software Foundation; either version 2 of the " + "License, or (at your option) any later version.
See the file " + "COPYING included with this distribution for more information.

"; +- ++ + // use our own dialog so we can influence the size + + QDialog *d = new QDialog(this); + + d->setWindowTitle(tr("About %1").arg(QApplication::applicationName())); +- ++ + QGridLayout *layout = new QGridLayout; + d->setLayout(layout); + + int row = 0; +- ++ + QLabel *iconLabel = new QLabel; + iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64)); + layout->addWidget(iconLabel, row, 0, Qt::AlignTop); +@@ -3402,7 +3511,7 @@ MainWindow::about() + + d->setMinimumSize(m_viewManager->scalePixelSize(420), + m_viewManager->scalePixelSize(200)); +- ++ + d->exec(); + + delete d; +@@ -3418,7 +3527,7 @@ void + MainWindow::newerVersionAvailable(QString version) + { + m_newerVersionIs = version; +- ++ + //!!! nicer URL would be nicer + QSettings settings; + settings.beginGroup("NewerVersionWarning"); +@@ -3442,9 +3551,9 @@ MainWindow::ffwd() + + sv_samplerate_t sr = getMainModel()->getSampleRate(); + +- // The step is supposed to scale and be as wide as a step of ++ // The step is supposed to scale and be as wide as a step of + // m_defaultFfwdRwdStep seconds at zoom level 720 and sr = 44100 +- ++ + ZoomLevel zoom = m_viewManager->getGlobalZoom(); + double framesPerPixel = 1.0; + if (zoom.zone == ZoomLevel::FramesPerPixel) { +@@ -3455,20 +3564,20 @@ MainWindow::ffwd() + double defaultFramesPerPixel = (720 * 44100) / sr; + double scaler = framesPerPixel / defaultFramesPerPixel; + RealTime step = m_defaultFfwdRwdStep * scaler; +- ++ + frame = RealTime::realTime2Frame + (RealTime::frame2RealTime(frame, sr) + step, sr); + + if (frame > getMainModel()->getEndFrame()) { + frame = getMainModel()->getEndFrame(); + } +- ++ + if (frame < 0) frame = 0; + + if (m_viewManager->getPlaySelectionMode()) { + frame = m_viewManager->constrainFrameToSelection(frame); + } +- ++ + m_viewManager->setPlaybackFrame(frame); + + if (frame == getMainModel()->getEndFrame() && +@@ -3489,7 +3598,7 @@ MainWindow::rewind() + + sv_samplerate_t sr = getMainModel()->getSampleRate(); + +- // The step is supposed to scale and be as wide as a step of ++ // The step is supposed to scale and be as wide as a step of + // m_defaultFfwdRwdStep seconds at zoom level 720 and sr = 44100 + + ZoomLevel zoom = m_viewManager->getGlobalZoom(); +@@ -3505,7 +3614,7 @@ MainWindow::rewind() + + frame = RealTime::realTime2Frame + (RealTime::frame2RealTime(frame, sr) - step, sr); +- ++ + if (frame < getMainModel()->getStartFrame()) { + frame = getMainModel()->getStartFrame(); + } +diff --git a/main/MainWindow.h b/main/MainWindow.h +index f14d5ca..48e2db2 100644 +--- a/main/MainWindow.h ++++ b/main/MainWindow.h +@@ -1,3 +1,4 @@ ++#pragma once + /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + + /* +@@ -5,7 +6,7 @@ + An intonation analysis and annotation tool + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006-2012 Chris Cannam and QMUL. +- ++ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the +@@ -30,8 +31,13 @@ class MainWindow : public sv::MainWindowBase + Q_OBJECT + + public: ++ enum class RecordingAnalysisMode { ++ AfterRecording = 0, // Default ++ DuringRecording = 1 ++ }; ++ + MainWindow(AudioMode audioMode, +- bool withSonification = true, ++ bool withSonification = true, + bool withSpectrogram = true); + virtual ~MainWindow(); + +@@ -87,7 +93,13 @@ protected slots: + virtual void editDisplayExtents(); + + virtual void analyseNow(); ++ ++ virtual void analyseDuringRecording(); ++ virtual void analyseDuringRecordingRunner(); + virtual void resetAnalyseOptions(); ++ virtual void recordAnalysisToggled(); ++ virtual void analyseAfterRecordToggled(); ++ + virtual void autoAnalysisToggled(); + virtual void precisionAnalysisToggled(); + virtual void lowampAnalysisToggled(); +@@ -164,7 +176,7 @@ protected slots: + virtual void whatsNew(); + + virtual void betaReleaseWarning(); +- ++ + virtual void newerVersionAvailable(QString); + + virtual void selectionChangedByUser(); +@@ -205,11 +217,13 @@ protected: + bool m_intelligentActionOn; // GF: !!! temporary + + QAction *m_autoAnalyse; ++ QAction *m_analyseDuringRecord; ++ QAction *m_analyseAfterRecord; + QAction *m_precise; + QAction *m_lowamp; + QAction *m_onset; + QAction *m_prune; +- ++ + QAction *m_showAudio; + QAction *m_showSpect; + QAction *m_showPitch; +@@ -224,6 +238,7 @@ protected: + sv::ActivityLog *m_activityLog; + sv::KeyReference *m_keyReference; + sv::VersionTester *m_versionTester; ++ + QString m_newerVersionIs; + + sv::sv_frame_t m_selectionAnchor; +@@ -246,6 +261,9 @@ protected: + virtual void setupHelpMenu(); + virtual void setupToolbars(); + ++ RecordingAnalysisMode getRecordingAnalysisMode() const; ++ void setRecordingAnalysisMode(RecordingAnalysisMode mode); ++ + virtual void octaveShift(bool up); + + virtual void auxSnapNotes(sv::Selection s); diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 463a4b3..a7ba1b7 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -713,58 +713,186 @@ static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr findOverlapGroups(const EventVector& events) { + std::vector groups; + std::vector processed(events.size(), false); + + for (size_t i = 0; i < events.size(); ++i) { + if (processed[i]) continue; + + EventVector currentGroup; + currentGroup.push_back(events[i]); + processed[i] = true; + + // Find all events that overlap with any event in the current group + bool foundOverlap; + do { + foundOverlap = false; + for (size_t j = 0; j < events.size(); ++j) { + if (processed[j]) continue; + + // Check if event j overlaps with any event in the current group + for (const auto& groupEvent : currentGroup) { + auto eventStart = events[j].getFrame(); + auto eventEnd = events[j].getFrame() + events[j].getDuration(); + auto groupStart = groupEvent.getFrame(); + auto groupEnd = groupEvent.getFrame() + groupEvent.getDuration(); + + // Check for temporal overlap + if (eventStart < groupEnd && eventEnd > groupStart) { + currentGroup.push_back(events[j]); + processed[j] = true; + foundOverlap = true; + break; + } + } + if (foundOverlap) break; + } + } while (foundOverlap); + + // Only create groups for actual overlaps (more than one event) + if (currentGroup.size() > 1) { + groups.emplace_back(currentGroup); + } + } - // If either frequency is invalid, use the valid one - if (prevFreq <= 0.0f && nextFreq > 0.0f) return nextFreq; - if (nextFreq <= 0.0f && prevFreq > 0.0f) return prevFreq; - if (prevFreq <= 0.0f && nextFreq <= 0.0f) return 0.0f; + return groups; +} + +// Enhanced helper function to calculate weighted frequency from multiple overlapping notes +static float calculateWeightedFrequency(const EventVector& overlappingEvents, + sv_frame_t overlapStart, sv_frame_t overlapDuration) { + if (overlappingEvents.empty()) return 0.0f; + if (overlappingEvents.size() == 1) { + return overlappingEvents[0].hasValue() ? overlappingEvents[0].getValue() : 0.0f; + } - // Calculate duration-based weights for each event's contribution to overlap - auto prevStart = prevEvent.getFrame(); - auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); - auto nextStart = nextEvent.getFrame(); - auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); + // Calculate weighted contributions from all events + std::vector frequencies; + std::vector weights; auto overlapEnd = overlapStart + overlapDuration; - // Calculate overlapping portions for each event - auto prevOverlapStart = std::max(prevStart, overlapStart); - auto prevOverlapEnd = std::min(prevEnd, overlapEnd); - auto prevOverlapContrib = std::max(0LL, prevOverlapEnd - prevOverlapStart); + for (const auto& event : overlappingEvents) { + float freq = event.hasValue() ? event.getValue() : 0.0f; + if (freq <= 0.0f) continue; // Skip invalid frequencies + + // Calculate this event's contribution to the overlap + auto eventStart = event.getFrame(); + auto eventEnd = event.getFrame() + event.getDuration(); + + auto eventOverlapStart = std::max(eventStart, overlapStart); + auto eventOverlapEnd = std::min(eventEnd, overlapEnd); + auto eventOverlapContrib = std::max(0LL, eventOverlapEnd - eventOverlapStart); + + if (eventOverlapContrib > 0) { + frequencies.push_back(freq); + weights.push_back(static_cast(eventOverlapContrib)); + } + } + + if (frequencies.empty()) return 0.0f; + if (frequencies.size() == 1) return frequencies[0]; + + // Normalize weights + double totalWeight = 0.0; + for (double weight : weights) totalWeight += weight; + if (totalWeight <= 0.0) { + // Fallback to simple geometric mean + double logSum = 0.0; + for (float freq : frequencies) { + logSum += std::log(freq); + } + return std::exp(logSum / frequencies.size()); + } - auto nextOverlapStart = std::max(nextStart, overlapStart); - auto nextOverlapEnd = std::min(nextEnd, overlapEnd); - auto nextOverlapContrib = std::max(0LL, nextOverlapEnd - nextOverlapStart); + for (double& weight : weights) weight /= totalWeight; - // Calculate weights based on duration contributions - double totalContrib = prevOverlapContrib + nextOverlapContrib; - if (totalContrib <= 0) { - // Fallback to simple average if no clear contribution - return (prevFreq + nextFreq) / 2.0f; + // Calculate weighted geometric mean for better musical accuracy + double weightedLogSum = 0.0; + for (size_t i = 0; i < frequencies.size(); ++i) { + weightedLogSum += std::log(frequencies[i]) * weights[i]; } - double prevWeight = prevOverlapContrib / totalContrib; - double nextWeight = nextOverlapContrib / totalContrib; + return std::exp(weightedLogSum); +} + +// Helper function to merge a group of overlapping events into a single event +static Event mergeOverlapGroup(const OverlapGroup& group) { + if (group.events.empty()) { + return Event(0, 0.0f, ""); + } - // Calculate weighted frequency - use logarithmic averaging for better musical accuracy - if (prevFreq > 0.0f && nextFreq > 0.0f) { - // Geometric mean weighted by duration (better for frequency averaging) - double logPrevFreq = std::log(prevFreq); - double logNextFreq = std::log(nextFreq); - double weightedLogFreq = logPrevFreq * prevWeight + logNextFreq * nextWeight; - return std::exp(weightedLogFreq); - } else { - // Linear average for edge cases - return prevFreq * prevWeight + nextFreq * nextWeight; + if (group.events.size() == 1) { + return group.events[0]; + } + + // Calculate merged event properties + auto mergedStart = group.startFrame; + auto mergedEnd = group.endFrame; + auto mergedDuration = mergedEnd - mergedStart; + + // Calculate weighted frequency for the entire overlap + float weightedFreq = calculateWeightedFrequency(group.events, mergedStart, mergedDuration); + + // Use properties from the event with the longest duration as base + const Event* longestEvent = &group.events[0]; + for (const auto& event : group.events) { + if (event.getDuration() > longestEvent->getDuration()) { + longestEvent = &event; + } + } + + // Create merged event + Event mergedEvent = longestEvent->withFrame(mergedStart) + .withDuration(mergedDuration); + + if (weightedFreq > 0.0f) { + mergedEvent = mergedEvent.withValue(weightedFreq); + } + + // Preserve label if available + if (longestEvent->hasLabel()) { + mergedEvent = mergedEvent.withLabel(longestEvent->getLabel()); + } + + // Preserve level if available + if (longestEvent->hasLevel()) { + mergedEvent = mergedEvent.withLevel(longestEvent->getLevel()); } + + return mergedEvent; +} + +// Legacy two-event version for backward compatibility +static float calculateWeightedFrequency(const Event& prevEvent, const Event& nextEvent, + sv_frame_t overlapStart, sv_frame_t overlapDuration) { + EventVector events = {prevEvent, nextEvent}; + return calculateWeightedFrequency(events, overlapStart, overlapDuration); } -// Custom processing logic for FlexiNoteLayer +// Enhanced custom processing logic for FlexiNoteLayer with multi-note overlap support static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { auto allEvents = toModel->getAllEvents(); auto points = fromModel->getAllEvents(); @@ -774,78 +902,117 @@ static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr contextStart) { + bool potentialOverlap = false; + for (const auto& newEvent : points) { + // Check if existing event could potentially overlap with any new event + auto existingStart = event.getFrame(); + auto existingEnd = event.getFrame() + event.getDuration(); + auto newStart = newEvent.getFrame(); + auto newEnd = newEvent.getFrame() + newEvent.getDuration(); + + if (existingStart < newEnd && existingEnd > newStart) { + potentialOverlap = true; + break; + } + } + + if (potentialOverlap) { eventsToRemove.push_back(event); + } else { + remainingEvents.push_back(event); } } + // Remove potentially overlapping events from the model for (const auto& event : eventsToRemove) { toModel->remove(event); } - // After cleanup, get the remaining events for overlap processing - allEvents = toModel->getAllEvents(); + // Create combined event set for overlap detection + EventVector combinedEvents; + + // Add remaining non-overlapping events + for (const auto& event : remainingEvents) { + combinedEvents.push_back(event); + } + + // Add previously removed events that might need merging + for (const auto& event : eventsToRemove) { + combinedEvents.push_back(event); + } + + // Add new incoming events + for (const auto& event : points) { + combinedEvents.push_back(event); + } - if (!allEvents.empty() && !points.empty()) { - auto& prevEvent = allEvents.back(); - auto& nextEvent = points.front(); + // Find all overlap groups in the combined event set + auto overlapGroups = findOverlapGroups(combinedEvents); - // Handle various overlap scenarios - if (nextEvent.getFrame() >= prevEvent.getFrame() && - nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { - - // Calculate overlap parameters - auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); - auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); - auto overlapStart = nextEvent.getFrame(); - auto overlapDuration = std::min(prevEnd, nextEnd) - overlapStart; - - // Calculate weighted frequency for the merged event - float weightedFreq = calculateWeightedFrequency(prevEvent, nextEvent, overlapStart, overlapDuration); - - // Choose merge strategy based on overlap characteristics - if (overlapDuration < prevEvent.getDuration() * 0.5 && - overlapDuration < nextEvent.getDuration() * 0.5) { - // Small overlap: merge by extending the earlier event with weighted frequency - auto mergedDuration = nextEnd - prevEvent.getFrame(); - Event mergedEvent = prevEvent.withDuration(mergedDuration); - if (weightedFreq > 0.0f) { - mergedEvent = mergedEvent.withValue(weightedFreq); - } - points[0] = mergedEvent; - toModel->remove(prevEvent); - } else { - // Significant overlap: create merged event with weighted frequency and optimal timing - auto mergedStart = prevEvent.getFrame(); - auto mergedDuration = std::max(prevEnd, nextEnd) - mergedStart; - - // Use the event with better timing characteristics (earlier start) - Event mergedEvent = prevEvent.withDuration(mergedDuration); - if (weightedFreq > 0.0f) { - mergedEvent = mergedEvent.withValue(weightedFreq); - } - - // Preserve additional properties from the longer event - if (nextEvent.getDuration() > prevEvent.getDuration()) { - // Copy label and other properties from longer event if available - if (nextEvent.hasLabel() && !prevEvent.hasLabel()) { - mergedEvent = mergedEvent.withLabel(nextEvent.getLabel()); - } - if (nextEvent.hasLevel() && !prevEvent.hasLevel()) { - mergedEvent = mergedEvent.withLevel(nextEvent.getLevel()); - } + // Process each overlap group + EventVector finalEvents; + std::vector processed(combinedEvents.size(), false); + + // Process overlap groups first + for (const auto& group : overlapGroups) { + Event mergedEvent = mergeOverlapGroup(group); + finalEvents.push_back(mergedEvent); + + // Mark all events in this group as processed + for (const auto& groupEvent : group.events) { + for (size_t i = 0; i < combinedEvents.size(); ++i) { + if (!processed[i] && + combinedEvents[i].getFrame() == groupEvent.getFrame() && + combinedEvents[i].getDuration() == groupEvent.getDuration()) { + processed[i] = true; } - - points[0] = mergedEvent; - toModel->remove(prevEvent); } } } + + // Add non-overlapping events + for (size_t i = 0; i < combinedEvents.size(); ++i) { + if (!processed[i]) { + finalEvents.push_back(combinedEvents[i]); + } + } + + // Filter to return only the new/modified events that should be added + EventVector resultEvents; + for (const auto& event : finalEvents) { + bool isNew = false; + + // Check if this event is derived from new analysis or is a merge result + for (const auto& originalNew : points) { + if (event.getFrame() >= originalNew.getFrame() - 1000 && // Allow some tolerance + event.getFrame() <= originalNew.getFrame() + originalNew.getDuration() + 1000) { + isNew = true; + break; + } + } + + // Also include events that are merge results (modified existing events) + bool isMergeResult = false; + for (const auto& removedEvent : eventsToRemove) { + if (event.getFrame() >= removedEvent.getFrame() - 1000 && + event.getFrame() <= removedEvent.getFrame() + removedEvent.getDuration() + 1000) { + isMergeResult = true; + break; + } + } + + if (isNew || isMergeResult) { + resultEvents.push_back(event); + } + } - return points; + return resultEvents; } QString From bf5c4bee287185d5b856062ada819c56357e156d Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 5 Sep 2025 12:08:25 +0300 Subject: [PATCH 18/24] refactor overlapping --- build.log | 1 - changes.log | 2325 ------------------------------------- main/Analyser.cpp | 363 +----- main/OverlapProcessor.cpp | 395 +++++++ main/OverlapProcessor.h | 107 ++ meson.build | 1 + 6 files changed, 513 insertions(+), 2679 deletions(-) delete mode 100644 build.log delete mode 100644 changes.log create mode 100644 main/OverlapProcessor.cpp create mode 100644 main/OverlapProcessor.h diff --git a/build.log b/build.log deleted file mode 100644 index 87240c0..0000000 --- a/build.log +++ /dev/null @@ -1 +0,0 @@ -[535/535] Linking target test-svcore-data-fileio diff --git a/changes.log b/changes.log deleted file mode 100644 index 7f0b69b..0000000 --- a/changes.log +++ /dev/null @@ -1,2325 +0,0 @@ -diff --git a/main/Analyser.cpp b/main/Analyser.cpp -index 957bfe6..463a4b3 100644 ---- a/main/Analyser.cpp -+++ b/main/Analyser.cpp -@@ -5,7 +5,7 @@ - An intonation analysis and annotation tool - Centre for Digital Music, Queen Mary, University of London. - This file copyright 2006-2012 Chris Cannam and QMUL. -- -+ - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of the -@@ -69,6 +69,16 @@ Analyser::Analyser() : - - Analyser::~Analyser() - { -+ // Clean up any remaining realtime analysis layers -+ if (m_document && m_pane) { -+ for (auto* layer : m_realtimeAnalysisLayers) { -+ if (layer) { -+ m_document->removeLayerFromView(m_pane, layer); -+ m_document->deleteLayer(layer); -+ } -+ } -+ } -+ m_realtimeAnalysisLayers.clear(); - } - - std::map -@@ -83,7 +93,7 @@ Analyser::getAnalysisSettings() - - QString - Analyser::newFileLoaded(Document *doc, ModelId model, -- PaneStack *paneStack, Pane *pane) -+ PaneStack *paneStack, Pane *pane) - { - m_document = doc; - m_fileModel = model; -@@ -93,7 +103,7 @@ Analyser::newFileLoaded(Document *doc, ModelId model, - if (!ModelById::isa(m_fileModel)) { - return "Internal error: Analyser::newFileLoaded() called with no model, or a non-WaveFileModel"; - } -- -+ - connect(doc, SIGNAL(layerAboutToBeDeleted(Layer *)), - this, SLOT(layerAboutToBeDeleted(Layer *))); - -@@ -113,7 +123,7 @@ Analyser::analyseExistingFile() - if (!m_pane) return "Internal error: Analyser::analyseExistingFile() called with no pane present"; - - if (m_fileModel.isNone()) return "Internal error: Analyser::analyseExistingFile() called with no model present"; -- -+ - if (m_layers[PitchTrack]) { - m_document->removeLayerFromView(m_pane, m_layers[PitchTrack]); - m_layers[PitchTrack] = 0; -@@ -126,6 +136,28 @@ Analyser::analyseExistingFile() - return doAllAnalyses(true); - } - -+QString -+Analyser::analyseRecordingToEnd(sv_frame_t record_duration) -+{ -+ if (!m_document) return "Internal error: Analyser::analyseRecordingToEnd() called with no document present"; -+ -+ if (!m_pane) return "Internal error: Analyser::analyseRecordingToEnd() called with no pane present"; -+ -+ if (m_fileModel.isNone()) return "Internal error: Analyser::analyseRecordingToEnd() called with no model present"; -+ -+ // We start with a 2500-frame overlap to ensure we capture instrument attacks in time (~56ms) -+ sv_frame_t overlap = 2500; -+ auto startPosition = std::max(m_analysedFrames - overlap, 0LL); -+ auto endPosition = record_duration; -+ Selection analysingSelection = Selection(startPosition, endPosition); -+ -+ this->analyseRecording(analysingSelection); -+ -+ m_analysedFrames = endPosition; -+ -+ return ""; -+} -+ - QString - Analyser::doAllAnalyses(bool withPitchTrack) - { -@@ -170,6 +202,18 @@ void - Analyser::fileClosed() - { - cerr << "Analyser::fileClosed" << endl; -+ -+ // Clean up any remaining realtime analysis layers -+ if (m_document && m_pane) { -+ for (auto* layer : m_realtimeAnalysisLayers) { -+ if (layer) { -+ m_document->removeLayerFromView(m_pane, layer); -+ m_document->deleteLayer(layer); -+ } -+ } -+ } -+ m_realtimeAnalysisLayers.clear(); -+ - m_layers.clear(); - m_reAnalysisCandidates.clear(); - m_currentCandidate = -1; -@@ -204,7 +248,7 @@ Analyser::getInitialAnalysisCompletion() - int c = m_layers[Notes]->getCompletion(m_pane); - if (c < completion) completion = c; - } -- -+ - return completion; - } - -@@ -227,7 +271,7 @@ Analyser::layerCompletionChanged(ModelId) - - auto audioModel = ModelById::get(m_layers[Audio]->getModel()); - sv_frame_t endFrame = audioModel->getEndFrame(); -- -+ - if (m_layers[PitchTrack]) { - auto model = ModelById::getAs - (m_layers[PitchTrack]->getModel()); -@@ -255,7 +299,7 @@ Analyser::addVisualisations() - - /* This is roughly what we'd do for a constant-Q spectrogram, but it - currently has issues with y-axis alignment -- -+ - TransformFactory *tf = TransformFactory::getInstance(); - - QString name = "Constant-Q"; -@@ -264,7 +308,7 @@ Analyser::addVisualisations() - - QString notFound = tr("Transform \"%1\" not found, spectrogram will not be enabled.

Is the %2 Vamp plugin correctly installed?"); - if (!tf->haveTransform(base + out)) { -- return notFound.arg(base + out).arg(name); -+ return notFound.arg(base + out).arg(name); - } - - Transform transform = tf->getDefaultTransformFor -@@ -275,7 +319,7 @@ Analyser::addVisualisations() - (m_document->createDerivedLayer(transform, m_fileModel)); - - if (!spectrogram) return tr("Transform \"%1\" did not run correctly (no layer or wrong layer type returned)").arg(base + out); --*/ -+*/ - - // As with all the visualisation layers, if we already have one in - // the pane we do not create another, just record its -@@ -338,13 +382,83 @@ Analyser::addWaveform() - params->setPlayPan(-1); - params->setPlayGain(1); - } -- -+ - m_document->addLayerToView(m_pane, waveform); - - m_layers[Audio] = waveform; - return ""; - } - -+std::map getAnalysisSettingsFromSettings() -+{ -+ std::map analysisSettings; -+ -+ QSettings settings; -+ settings.beginGroup("Analyser"); -+ -+ analysisSettings["precision-analysis"] = settings.value("precision-analysis", false).toBool(); -+ analysisSettings["lowamp-analysis"] = settings.value("lowamp-analysis", true).toBool(); -+ analysisSettings["onset-analysis"] = settings.value("onset-analysis", true).toBool(); -+ analysisSettings["prune-analysis"] = settings.value("prune-analysis", true).toBool(); -+ -+ settings.endGroup(); -+ -+ return analysisSettings; -+} -+ -+static void setAnalysisSettings(Transform& transform) -+{ -+ const auto analysisSettings = getAnalysisSettingsFromSettings(); -+ -+ if (analysisSettings.count("precision-analysis") > 0) { -+ bool precise = analysisSettings.at("precision-analysis"); -+ if (precise) { -+ cerr << "setting parameters for precise mode" << endl; -+ transform.setParameter("precisetime", 1); -+ } -+ else { -+ cerr << "setting parameters for vague mode" << endl; -+ transform.setParameter("precisetime", 0); -+ } -+ } -+ -+ if (analysisSettings.count("lowamp-analysis") > 0) { -+ bool lowamp = analysisSettings.at("lowamp-analysis"); -+ if (lowamp) { -+ cerr << "setting parameters for lowamp suppression" << endl; -+ transform.setParameter("lowampsuppression", 0.2f); -+ } -+ else { -+ cerr << "setting parameters for no lowamp suppression" << endl; -+ transform.setParameter("lowampsuppression", 0.0f); -+ } -+ } -+ -+ if (analysisSettings.count("onset-analysis") > 0) { -+ bool onset = analysisSettings.at("onset-analysis"); -+ if (onset) { -+ cerr << "setting parameters for increased onset sensitivity" << endl; -+ transform.setParameter("onsetsensitivity", 0.7f); -+ } -+ else { -+ cerr << "setting parameters for non-increased onset sensitivity" << endl; -+ transform.setParameter("onsetsensitivity", 0.0f); -+ } -+ } -+ -+ if (analysisSettings.count("prune-analysis") > 0) { -+ bool prune = analysisSettings.at("prune-analysis"); -+ if (prune) { -+ cerr << "setting parameters for duration pruning" << endl; -+ transform.setParameter("prunethresh", 0.1f); -+ } -+ else { -+ cerr << "setting parameters for no duration pruning" << endl; -+ transform.setParameter("prunethresh", 0.0f); -+ } -+ } -+} -+ - QString - Analyser::addAnalyses() - { -@@ -352,7 +466,7 @@ Analyser::addAnalyses() - if (!waveFileModel) { - return "Internal error: Analyser::addAnalyses() called with no model present"; - } -- -+ - // As with the spectrogram above, if these layers exist we use - // them - TimeValueLayer *existingPitch = 0; -@@ -382,11 +496,6 @@ Analyser::addAnalyses() - } - - TransformFactory *tf = TransformFactory::getInstance(); -- -- QString plugname = "pYIN"; -- QString base = "vamp:pyin:pyin:"; -- QString f0out = "smoothedpitchtrack"; -- QString noteout = "notes"; - - Transforms transforms; - -@@ -401,81 +510,28 @@ Analyser::addAnalyses() - m_document->addLayerToView(m_pane, lx); - } - */ -+ auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); -+ auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); - - QString notFound = tr("Transform \"%1\" not found. Unable to analyse audio file.

Is the %2 Vamp plugin correctly installed?"); -- if (!tf->haveTransform(base + f0out)) { -- return notFound.arg(base + f0out).arg(plugname); -+ if (!tf->haveTransform(f0_transform)) { -+ return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); - } -- if (!tf->haveTransform(base + noteout)) { -- return notFound.arg(base + noteout).arg(plugname); -- } -- -- QSettings settings; -- settings.beginGroup("Analyser"); -- -- bool precise = false, lowamp = true, onset = true, prune = true; -- -- std::map flags { -- { "precision-analysis", precise }, -- { "lowamp-analysis", lowamp }, -- { "onset-analysis", onset }, -- { "prune-analysis", prune } -- }; -- -- auto keyMap = getAnalysisSettings(); -- -- for (auto p: flags) { -- auto ki = keyMap.find(p.first); -- if (ki != keyMap.end()) { -- p.second = settings.value(ki->first, ki->second).toBool(); -- } else { -- throw std::logic_error("Internal error: One or more analysis settings keys not found in map: check addAnalyses and getAnalysisSettings"); -- } -+ if (!tf->haveTransform(note_transform)) { -+ return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); - } - -- settings.endGroup(); -- - Transform t = tf->getDefaultTransformFor -- (base + f0out, waveFileModel->getSampleRate()); -+ (f0_transform, waveFileModel->getSampleRate()); - t.setStepSize(256); - t.setBlockSize(2048); - -- if (precise) { -- cerr << "setting parameters for precise mode" << endl; -- t.setParameter("precisetime", 1); -- } else { -- cerr << "setting parameters for vague mode" << endl; -- t.setParameter("precisetime", 0); -- } -- -- if (lowamp) { -- cerr << "setting parameters for lowamp suppression" << endl; -- t.setParameter("lowampsuppression", 0.2f); -- } else { -- cerr << "setting parameters for no lowamp suppression" << endl; -- t.setParameter("lowampsuppression", 0.0f); -- } -- -- if (onset) { -- cerr << "setting parameters for increased onset sensitivity" << endl; -- t.setParameter("onsetsensitivity", 0.7f); -- } else { -- cerr << "setting parameters for non-increased onset sensitivity" << endl; -- t.setParameter("onsetsensitivity", 0.0f); -- } -- -- if (prune) { -- cerr << "setting parameters for duration pruning" << endl; -- t.setParameter("prunethresh", 0.1f); -- } else { -- cerr << "setting parameters for no duration pruning" << endl; -- t.setParameter("prunethresh", 0.0f); -- } -+ setAnalysisSettings(t); - - transforms.push_back(t); - -- t.setOutput(noteout); -- -+ t.setOutput(PYIN_NOTE_OUT); -+ - transforms.push_back(t); - - std::vector layers = -@@ -485,16 +541,20 @@ Analyser::addAnalyses() - - FlexiNoteLayer *f = qobject_cast(layers[i]); - TimeValueLayer *t = qobject_cast(layers[i]); -- -- if (f) m_layers[Notes] = f; -- if (t) m_layers[PitchTrack] = t; -- -+ -+ if (f) { -+ m_layers[Notes] = f; -+ } -+ else if (t) { -+ m_layers[PitchTrack] = t; -+ } -+ - m_document->addLayerToView(m_pane, layers[i]); - } -- -+ - ColourDatabase *cdb = ColourDatabase::getInstance(); -- -- TimeValueLayer *pitchLayer = -+ -+ TimeValueLayer *pitchLayer = - qobject_cast(m_layers[PitchTrack]); - if (pitchLayer) { - pitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); -@@ -506,8 +566,8 @@ Analyser::addAnalyses() - connect(pitchLayer, SIGNAL(modelCompletionChanged(ModelId)), - this, SLOT(layerCompletionChanged(ModelId))); - } -- -- FlexiNoteLayer *flexiNoteLayer = -+ -+ FlexiNoteLayer *flexiNoteLayer = - qobject_cast(m_layers[Notes]); - if (flexiNoteLayer) { - flexiNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); -@@ -523,7 +583,7 @@ Analyser::addAnalyses() - connect(flexiNoteLayer, SIGNAL(materialiseReAnalysis()), - this, SLOT(materialiseReAnalysis())); - } -- -+ - return ""; - } - -@@ -544,6 +604,351 @@ Analyser::materialiseReAnalysis() - switchPitchCandidate(m_reAnalysingSelection, true); // or false, doesn't matter - } - -+void -+Analyser::updatePitchTrack(sv::ModelId) -+{ -+ // Implementation for updatePitchTrack slot -+ emit layersChanged(); -+} -+ -+void -+Analyser::updateNoteLayer(sv::ModelId) -+{ -+ // Implementation for updateNoteLayer slot -+ emit layersChanged(); -+} -+ -+template -+void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* cdb) { -+ layer->setBaseColour(cdb->getColourIndex(colourName)); -+} -+ -+// Generalized helper function to process layers -+template -+void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* document, sv::Pane* pane, std::vector* trackingVector, std::function, std::shared_ptr)> customProcessing) { -+ QObject::connect(layer, &LayerType::modelCompletionChanged, [layer, targetLayer, document, pane, trackingVector, customProcessing](ModelId modelId) { -+ auto model = ModelById::getAs(modelId); -+ -+ if (model->getCompletion() == 100) { -+ auto toModel = ModelById::getAs(targetLayer->getModel()); -+ EventVector points = model->getAllEvents(); -+ -+ // Custom processing logic -+ points = customProcessing(model, toModel); -+ -+ for (const Event& p : points) { -+ toModel->add(p); -+ } -+ -+ QObject::disconnect(layer, &LayerType::modelCompletionChanged, nullptr, nullptr); -+ -+ // Clean up the temporary layer -+ if (document && pane) { -+ document->removeLayerFromView(pane, layer); -+ document->deleteLayer(layer); -+ } -+ -+ // Remove from tracking vector -+ if (trackingVector) { -+ trackingVector->erase(std::remove(trackingVector->begin(), trackingVector->end(), layer), trackingVector->end()); -+ } -+ } -+ }); -+} -+ -+// Custom processing logic for pitch track (SparseTimeValueModel) -+static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { -+ auto allEvents = toModel->getAllEvents(); -+ auto points = fromModel->getAllEvents(); -+ -+ // Add context start timestamp to all points from the new analysis -+ std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { -+ return point.withFrame(point.getFrame() + contextStart); -+ }); -+ -+ // Remove all events from toModel that extend beyond contextStart to prevent overlaps -+ EventVector eventsToRemove; -+ for (const auto& event : allEvents) { -+ if (event.getFrame() >= contextStart) { -+ eventsToRemove.push_back(event); -+ } -+ } -+ -+ for (const auto& event : eventsToRemove) { -+ toModel->remove(event); -+ } -+ -+ // After cleanup, get the remaining events for overlap processing -+ allEvents = toModel->getAllEvents(); -+ -+ // Handle potential overlaps between the last existing event and first new event -+ if (!allEvents.empty() && !points.empty()) { -+ auto& lastExistingEvent = allEvents.back(); -+ auto& firstNewEvent = points.front(); -+ -+ // Check if there's a gap or overlap between last existing and first new event -+ auto gapFrames = firstNewEvent.getFrame() - lastExistingEvent.getFrame(); -+ -+ // If events are very close (within ~11ms at 44.1kHz), interpolate between them -+ const sv_frame_t interpolationThreshold = 512; // ~11ms at 44.1kHz -+ -+ if (gapFrames > 0 && gapFrames <= interpolationThreshold) { -+ // Small gap - add interpolated point if pitch values are similar -+ if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { -+ auto lastValue = lastExistingEvent.getValue(); -+ auto firstValue = firstNewEvent.getValue(); -+ auto valueDiff = std::abs(lastValue - firstValue) / lastValue; -+ -+ // Only interpolate if pitch values are within 10% of each other -+ if (valueDiff <= 0.1) { -+ auto midFrame = lastExistingEvent.getFrame() + gapFrames / 2; -+ auto midValue = (lastValue + firstValue) / 2.0; -+ Event interpolatedEvent = Event(midFrame, midValue, "interpolated"); -+ toModel->add(interpolatedEvent); -+ } -+ } -+ } -+ } -+ -+ return points; -+} -+ -+// Helper function to calculate weighted frequency from overlapping notes -+static float calculateWeightedFrequency(const Event& prevEvent, const Event& nextEvent, -+ sv_frame_t overlapStart, sv_frame_t overlapDuration) { -+ // Extract frequencies from both events -+ float prevFreq = prevEvent.hasValue() ? prevEvent.getValue() : 0.0f; -+ float nextFreq = nextEvent.hasValue() ? nextEvent.getValue() : 0.0f; -+ -+ // If either frequency is invalid, use the valid one -+ if (prevFreq <= 0.0f && nextFreq > 0.0f) return nextFreq; -+ if (nextFreq <= 0.0f && prevFreq > 0.0f) return prevFreq; -+ if (prevFreq <= 0.0f && nextFreq <= 0.0f) return 0.0f; -+ -+ // Calculate duration-based weights for each event's contribution to overlap -+ auto prevStart = prevEvent.getFrame(); -+ auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); -+ auto nextStart = nextEvent.getFrame(); -+ auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); -+ auto overlapEnd = overlapStart + overlapDuration; -+ -+ // Calculate overlapping portions for each event -+ auto prevOverlapStart = std::max(prevStart, overlapStart); -+ auto prevOverlapEnd = std::min(prevEnd, overlapEnd); -+ auto prevOverlapContrib = std::max(0LL, prevOverlapEnd - prevOverlapStart); -+ -+ auto nextOverlapStart = std::max(nextStart, overlapStart); -+ auto nextOverlapEnd = std::min(nextEnd, overlapEnd); -+ auto nextOverlapContrib = std::max(0LL, nextOverlapEnd - nextOverlapStart); -+ -+ // Calculate weights based on duration contributions -+ double totalContrib = prevOverlapContrib + nextOverlapContrib; -+ if (totalContrib <= 0) { -+ // Fallback to simple average if no clear contribution -+ return (prevFreq + nextFreq) / 2.0f; -+ } -+ -+ double prevWeight = prevOverlapContrib / totalContrib; -+ double nextWeight = nextOverlapContrib / totalContrib; -+ -+ // Calculate weighted frequency - use logarithmic averaging for better musical accuracy -+ if (prevFreq > 0.0f && nextFreq > 0.0f) { -+ // Geometric mean weighted by duration (better for frequency averaging) -+ double logPrevFreq = std::log(prevFreq); -+ double logNextFreq = std::log(nextFreq); -+ double weightedLogFreq = logPrevFreq * prevWeight + logNextFreq * nextWeight; -+ return std::exp(weightedLogFreq); -+ } else { -+ // Linear average for edge cases -+ return prevFreq * prevWeight + nextFreq * nextWeight; -+ } -+} -+ -+// Custom processing logic for FlexiNoteLayer -+static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { -+ auto allEvents = toModel->getAllEvents(); -+ auto points = fromModel->getAllEvents(); -+ -+ // Vamp doesn't add current timestamp for note features, so, do it manually -+ std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { -+ return point.withFrame(point.getFrame() + contextStart); -+ }); -+ -+ // Remove all events from toModel which end after contextStart to prevent overlaps -+ EventVector eventsToRemove; -+ for (const auto& event : allEvents) { -+ if (event.getFrame() + event.getDuration() > contextStart) { -+ eventsToRemove.push_back(event); -+ } -+ } -+ -+ for (const auto& event : eventsToRemove) { -+ toModel->remove(event); -+ } -+ -+ // After cleanup, get the remaining events for overlap processing -+ allEvents = toModel->getAllEvents(); -+ -+ if (!allEvents.empty() && !points.empty()) { -+ auto& prevEvent = allEvents.back(); -+ auto& nextEvent = points.front(); -+ -+ // Handle various overlap scenarios -+ if (nextEvent.getFrame() >= prevEvent.getFrame() && -+ nextEvent.getFrame() < prevEvent.getFrame() + prevEvent.getDuration()) { -+ -+ // Calculate overlap parameters -+ auto prevEnd = prevEvent.getFrame() + prevEvent.getDuration(); -+ auto nextEnd = nextEvent.getFrame() + nextEvent.getDuration(); -+ auto overlapStart = nextEvent.getFrame(); -+ auto overlapDuration = std::min(prevEnd, nextEnd) - overlapStart; -+ -+ // Calculate weighted frequency for the merged event -+ float weightedFreq = calculateWeightedFrequency(prevEvent, nextEvent, overlapStart, overlapDuration); -+ -+ // Choose merge strategy based on overlap characteristics -+ if (overlapDuration < prevEvent.getDuration() * 0.5 && -+ overlapDuration < nextEvent.getDuration() * 0.5) { -+ // Small overlap: merge by extending the earlier event with weighted frequency -+ auto mergedDuration = nextEnd - prevEvent.getFrame(); -+ Event mergedEvent = prevEvent.withDuration(mergedDuration); -+ if (weightedFreq > 0.0f) { -+ mergedEvent = mergedEvent.withValue(weightedFreq); -+ } -+ points[0] = mergedEvent; -+ toModel->remove(prevEvent); -+ } else { -+ // Significant overlap: create merged event with weighted frequency and optimal timing -+ auto mergedStart = prevEvent.getFrame(); -+ auto mergedDuration = std::max(prevEnd, nextEnd) - mergedStart; -+ -+ // Use the event with better timing characteristics (earlier start) -+ Event mergedEvent = prevEvent.withDuration(mergedDuration); -+ if (weightedFreq > 0.0f) { -+ mergedEvent = mergedEvent.withValue(weightedFreq); -+ } -+ -+ // Preserve additional properties from the longer event -+ if (nextEvent.getDuration() > prevEvent.getDuration()) { -+ // Copy label and other properties from longer event if available -+ if (nextEvent.hasLabel() && !prevEvent.hasLabel()) { -+ mergedEvent = mergedEvent.withLabel(nextEvent.getLabel()); -+ } -+ if (nextEvent.hasLevel() && !prevEvent.hasLevel()) { -+ mergedEvent = mergedEvent.withLevel(nextEvent.getLevel()); -+ } -+ } -+ -+ points[0] = mergedEvent; -+ toModel->remove(prevEvent); -+ } -+ } -+ } -+ -+ return points; -+} -+ -+QString -+Analyser::analyseRecording(Selection sel) -+{ -+ QMutexLocker locker(&m_asyncMutex); -+ -+ auto waveFileModel = ModelById::getAs(m_fileModel); -+ if (!waveFileModel) { -+ return "Internal error: Analyser::analyseRecording() called with no model present"; -+ } -+ -+ if (!m_reAnalysingSelection.isEmpty()) { -+ if (sel == m_reAnalysingSelection) { -+ cerr << "selection & range are same as current analysis, ignoring" << endl; -+ return ""; -+ } -+ } -+ -+ if (sel.isEmpty()) return ""; -+ -+ m_reAnalysingSelection = sel; -+ -+ auto* pitchLayer = qobject_cast(m_layers[PitchTrack]); -+ auto* noteLayer = qobject_cast(m_layers[Notes]); -+ -+ TransformFactory* tf = TransformFactory::getInstance(); -+ -+ Transforms transforms; -+ -+ auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); -+ auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); -+ -+ QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); -+ if (!tf->haveTransform(f0_transform)) { -+ return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); -+ } -+ -+ if (!tf->haveTransform(note_transform)) { -+ return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); -+ } -+ -+ Transform t = tf->getDefaultTransformFor -+ (f0_transform, waveFileModel->getSampleRate()); -+ t.setStepSize(256); -+ t.setBlockSize(2048); -+ -+ setAnalysisSettings(t); -+ -+ RealTime start = RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); -+ RealTime end = RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); -+ -+ RealTime duration; -+ -+ if (sel.getEndFrame() > sel.getStartFrame()) { -+ duration = end - start; -+ } -+ -+ cerr << "Analyser::analyseRecording: start " << start << " end " << end << " original selection start " << sel.getStartFrame() << " end " << sel.getEndFrame() << " duration " << duration << endl; -+ -+ if (duration <= RealTime::zeroTime) { -+ cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; -+ return ""; -+ } -+ -+ t.setStartTime(start); -+ t.setDuration(duration); -+ -+ transforms.push_back(t); -+ -+ t.setOutput(PYIN_NOTE_OUT); -+ -+ transforms.push_back(t); -+ -+ std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); -+ -+ ColourDatabase* cdb = ColourDatabase::getInstance(); -+ -+ // Track the temporary layers for cleanup -+ for (auto* layer : layers) { -+ m_realtimeAnalysisLayers.push_back(layer); -+ } -+ -+ for (auto* layer : layers) { -+ -+ FlexiNoteLayer* tempNoteLayer = qobject_cast(layer); -+ TimeValueLayer* tempPitchLayer = qobject_cast(layer); -+ -+ if (tempPitchLayer) { -+ setBaseColour(tempPitchLayer, tr("Black"), cdb); -+ processLayer(tempPitchLayer, pitchLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processPitchModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); -+ } -+ -+ if (tempNoteLayer) { -+ setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); -+ processLayer(tempNoteLayer, noteLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processNoteModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); -+ } -+ } -+ -+ return ""; -+} -+ -+ - QString - Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) - { -@@ -553,7 +958,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) - if (!waveFileModel) { - return "Internal error: Analyser::reAnalyseSelection() called with no model present"; - } -- -+ - if (!m_reAnalysingSelection.isEmpty()) { - if (sel == m_reAnalysingSelection && range == m_reAnalysingRange) { - cerr << "selection & range are same as current analysis, ignoring" << endl; -@@ -584,13 +989,13 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) - } - - TransformFactory *tf = TransformFactory::getInstance(); -- -- QString plugname1 = "pYIN"; -+ - QString plugname2 = "CHP"; - - QString base = "vamp:pyin:localcandidatepyin:"; - QString out = "pitchtrackcandidates"; - -+ - if (range.isConstrained()) { - base = "vamp:chp:constrainedharmonicpeak:"; - out = "peak"; -@@ -600,9 +1005,9 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) - - QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); - if (!tf->haveTransform(base + out)) { -- return notFound.arg(base + out).arg(plugname1).arg(plugname2); -+ return notFound.arg(base + out).arg(PYIN_PLUGIN_NAME).arg(plugname2); - } -- -+ - Transform t = tf->getDefaultTransformFor - (base + out, waveFileModel->getSampleRate()); - t.setStepSize(256); -@@ -626,7 +1031,7 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) - } else { - endSample -= 9*grid; // MM says: not sure what the CHP plugin does there - } -- RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); -+ RealTime start = RealTime::frame2RealTime(startSample, waveFileModel->getSampleRate()); - RealTime end = RealTime::frame2RealTime(endSample, waveFileModel->getSampleRate()); - - RealTime duration; -@@ -641,12 +1046,12 @@ Analyser::reAnalyseSelection(Selection sel, FrequencyRange range) - cerr << "Analyser::reAnalyseSelection: duration <= 0, not analysing" << endl; - return ""; - } -- -+ - t.setStartTime(start); - t.setDuration(duration); - - transforms.push_back(t); -- -+ - m_currentAsyncHandle = - m_document->createDerivedLayersAsync(transforms, m_fileModel, this); - -@@ -660,7 +1065,7 @@ Analyser::arePitchCandidatesShown() const - } - - void --Analyser::showPitchCandidates(bool shown) -+Analyser::showPitchCandidates(bool shown) - { - if (m_candidatesVisible == shown) return; - -@@ -687,7 +1092,7 @@ Analyser::layersCreated(Document::LayerCreationAsyncHandle handle, - { - QMutexLocker locker(&m_asyncMutex); - -- if (handle != m_currentAsyncHandle || -+ if (handle != m_currentAsyncHandle || - m_reAnalysingSelection == Selection()) { - // We don't want these! - for (int i = 0; i < (int)primary.size(); ++i) { -@@ -750,14 +1155,14 @@ Analyser::haveHigherPitchCandidate() const - if (m_reAnalysisCandidates.empty()) return false; - return (m_currentCandidate < 0 || - (m_currentCandidate + 1 < (int)m_reAnalysisCandidates.size())); --} -+} - - bool - Analyser::haveLowerPitchCandidate() const - { - if (m_reAnalysisCandidates.empty()) return false; - return (m_currentCandidate < 0 || m_currentCandidate >= 1); --} -+} - - void - Analyser::switchPitchCandidate(Selection sel, bool up) -@@ -803,14 +1208,14 @@ void - Analyser::shiftOctave(Selection sel, bool up) - { - float factor = (up ? 2.f : 0.5f); -- -+ - vector actOn; - - Layer *pitchTrack = m_layers[PitchTrack]; - if (pitchTrack) actOn.push_back(pitchTrack); - - foreach (Layer *layer, actOn) { -- -+ - Clipboard clip; - layer->copy(m_pane, sel, clip); - layer->deleteSelection(sel); -@@ -824,7 +1229,7 @@ Analyser::shiftOctave(Selection sel, bool up) - shifted.addPoint(e); - } - } -- -+ - layer->paste(m_pane, shifted, 0, false); - } - } -@@ -849,7 +1254,7 @@ Analyser::abandonReAnalysis(Selection sel) - if (!myLayer) return; - myLayer->deleteSelection(sel); - myLayer->paste(m_pane, m_preAnalysis, 0, false); --} -+} - - void - Analyser::clearReAnalysis() -@@ -880,7 +1285,7 @@ void - Analyser::layerAboutToBeDeleted(Layer *doomed) - { - cerr << "Analyser::layerAboutToBeDeleted(" << doomed << ")" << endl; -- -+ - vector notDoomed; - - foreach (Layer *layer, m_reAnalysisCandidates) { -@@ -903,7 +1308,7 @@ Analyser::takePitchTrackFrom(Layer *otherLayer) - if (!myModel || !otherModel) return; - - Clipboard clip; -- -+ - Selection sel = Selection(myModel->getStartFrame(), - myModel->getEndFrame()); - myLayer->deleteSelection(sel); -@@ -932,7 +1337,7 @@ Analyser::takePitchTrackFrom(Layer *otherLayer) - void - Analyser::getEnclosingSelectionScope(sv_frame_t f, sv_frame_t &f0, sv_frame_t &f1) - { -- FlexiNoteLayer *flexiNoteLayer = -+ FlexiNoteLayer *flexiNoteLayer = - qobject_cast(m_layers[Notes]); - - sv_frame_t f0i = f, f1i = f; -@@ -942,7 +1347,7 @@ Analyser::getEnclosingSelectionScope(sv_frame_t f, sv_frame_t &f0, sv_frame_t &f - f0 = f1 = f; - return; - } -- -+ - flexiNoteLayer->snapToFeatureFrame(m_pane, f0i, res, Layer::SnapLeft, -1); - flexiNoteLayer->snapToFeatureFrame(m_pane, f1i, res, Layer::SnapRight, -1); - -@@ -976,11 +1381,11 @@ Analyser::loadState(Component c) - } - - void --Analyser::setIntelligentActions(bool on) -+Analyser::setIntelligentActions(bool on) - { - std::cerr << "toggle setIntelligentActions " << on << std::endl; - -- FlexiNoteLayer *flexiNoteLayer = -+ FlexiNoteLayer *flexiNoteLayer = - qobject_cast(m_layers[Notes]); - if (flexiNoteLayer) { - flexiNoteLayer->setIntelligentActions(on); -@@ -1055,7 +1460,7 @@ Analyser::getGain(Component c) const - return 1.f; - } - } -- -+ - void - Analyser::setGain(Component c, float gain) - { -@@ -1078,7 +1483,7 @@ Analyser::getPan(Component c) const - return 1.f; - } - } -- -+ - void - Analyser::setPan(Component c, float pan) - { -@@ -1089,6 +1494,3 @@ Analyser::setPan(Component c, float pan) - saveState(c); - } - } -- -- -- -diff --git a/main/Analyser.h b/main/Analyser.h -index 918d239..1db40b8 100644 ---- a/main/Analyser.h -+++ b/main/Analyser.h -@@ -5,7 +5,7 @@ - An intonation analysis and annotation tool - Centre for Digital Music, Queen Mary, University of London. - This file copyright 2006-2012 Chris Cannam and QMUL. -- -+ - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of the -@@ -56,9 +56,12 @@ public: - // layers; return "" on success or error string on failure - QString analyseExistingFile(); - -+ // Completes analysis from the last position to the end -+ QString analyseRecordingToEnd(sv::sv_frame_t record_duration); -+ - // Discard any layers etc associated with the current document - void fileClosed(); -- -+ - void setIntelligentActions(bool); - - bool getDisplayFrequencyExtents(double &min, double &max); -@@ -128,7 +131,14 @@ public: - * group in QSettings. - */ - static std::map getAnalysisSettings(); -- -+ -+ /** -+ * Analyse the selection and schedule asynchronous adds of -+ * candidate layers for the region it contains. Returns "" on -+ * success or a user-readable error string on failure. -+ */ -+ QString analyseRecording(sv::Selection sel); -+ - /** - * Analyse the selection and schedule asynchronous adds of - * candidate layers for the region it contains. Returns "" on -@@ -232,6 +242,8 @@ signals: - void initialAnalysisCompleted(); - - protected slots: -+ void updatePitchTrack(sv::ModelId); -+ void updateNoteLayer(sv::ModelId); - void layerAboutToBeDeleted(sv::Layer *); - void layerCompletionChanged(sv::ModelId); - void reAnalyseRegion(sv::sv_frame_t, sv::sv_frame_t, float, float); -@@ -247,8 +259,10 @@ protected: - - sv::Clipboard m_preAnalysis; - sv::Selection m_reAnalysingSelection; -+ sv::sv_frame_t m_analysedFrames = 0; - FrequencyRange m_reAnalysingRange; - std::vector m_reAnalysisCandidates; -+ std::vector m_realtimeAnalysisLayers; // Track temporary layers for cleanup - int m_currentCandidate; - bool m_candidatesVisible; - sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; -@@ -263,13 +277,21 @@ protected: - void discardPitchCandidates(); - - void stackLayers(); -- -+ - // Document::LayerCreationHandler method - void layersCreated(sv::Document::LayerCreationAsyncHandle, - std::vector, std::vector); - - void saveState(Component c) const; - void loadState(Component c); -+ -+ // TODO (alnovi): Move constexpression to a static class -+ static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; -+ static constexpr const char* PYIN_TRANSFORM_BASE = "vamp:pyin:pyin:"; -+ static constexpr const char* PYIN_F0_OUT = "smoothedpitchtrack"; -+ static constexpr const char* PYIN_NOTE_OUT = "notes"; -+ -+ - }; - - #endif -diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp -index cd9fa4a..7046805 100644 ---- a/main/MainWindow.cpp -+++ b/main/MainWindow.cpp -@@ -5,7 +5,7 @@ - An intonation analysis and annotation tool - Centre for Digital Music, Queen Mary, University of London. - This file copyright 2006-2012 Chris Cannam and QMUL. -- -+ - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of the -@@ -102,7 +102,7 @@ using std::endl; - using namespace sv; - - MainWindow::MainWindow(AudioMode audioMode, -- bool withSonification, -+ bool withSonification, - bool withSpectrogram) : - MainWindowBase(audioMode, - MainWindowBase::MIDI_NONE, -@@ -111,7 +111,7 @@ MainWindow::MainWindow(AudioMode audioMode, - m_overview(0), - m_mainMenusCreated(false), - m_playbackMenu(0), -- m_recentFilesMenu(0), -+ m_recentFilesMenu(0), - m_rightButtonMenu(0), - m_rightButtonPlaybackMenu(0), - m_deleteSelectedAction(0), -@@ -186,13 +186,13 @@ MainWindow::MainWindow(AudioMode audioMode, - m_viewManager->setOverlayMode(ViewManager::GlobalOverlays); - - connect(m_viewManager, SIGNAL(selectionChangedByUser()), -- this, SLOT(selectionChangedByUser())); -+ this, SLOT(selectionChangedByUser())); - - QFrame *frame = new QFrame; - setCentralWidget(frame); - - QGridLayout *layout = new QGridLayout; -- -+ - QScrollArea *scroll = new QScrollArea(frame); - scroll->setWidgetResizable(true); - scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); -@@ -230,7 +230,7 @@ MainWindow::MainWindow(AudioMode audioMode, - } else { - m_panLayer->setBaseColour - (ColourDatabase::getInstance()->getColourIndex(tr("Blue"))); -- } -+ } - - m_fader = new Fader(frame, false); - connect(m_fader, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); -@@ -311,7 +311,7 @@ MainWindow::MainWindow(AudioMode audioMode, - m_activityLog->hide(); - - setAudioRecordMode(RecordReplaceSession); -- -+ - newSession(); - - settings.beginGroup("MainWindow"); -@@ -350,7 +350,7 @@ MainWindow::setupMenus() - // workaround, to remove the appmenu-qt5 package, but that is - // awkward and the problem is so severe that it merits disabling - // the system menubar integration altogether. Like this: -- menuBar()->setNativeMenuBar(false); -+ menuBar()->setNativeMenuBar(false); - #endif - - m_rightButtonMenu = new QMenu(); -@@ -417,7 +417,7 @@ MainWindow::setupFileMenu() - m_keyReference->registerShortcut(action); - menu->addAction(action); - toolbar->addAction(action); -- -+ - icon = il.load("filesaveas"); - action = new QAction(icon, tr("Save Session &As..."), this); - action->setShortcut(tr("Ctrl+Shift+S")); -@@ -455,7 +455,7 @@ MainWindow::setupFileMenu() - menu->addAction(action); - - menu->addSeparator(); -- -+ - action = new QAction(tr("Browse Recorded Audio"), this); - action->setStatusTip(tr("Open the Recorded Audio folder in the system file browser")); - connect(action, SIGNAL(triggered()), this, SLOT(browseRecordedAudio())); -@@ -484,17 +484,17 @@ MainWindow::setupEditMenu() - m_keyReference->setCategory - (tr("Selection Strip Mouse Actions")); - m_keyReference->registerShortcut -- (tr("Jump"), tr("Left"), -+ (tr("Jump"), tr("Left"), - tr("Click left button to move the playback position to a time")); - m_keyReference->registerShortcut -- (tr("Select"), tr("Left"), -+ (tr("Select"), tr("Left"), - tr("Click left button and drag to select a region of time")); - m_keyReference->registerShortcut -- (tr("Select Note Duration"), tr("Double-Click Left"), -+ (tr("Select Note Duration"), tr("Double-Click Left"), - tr("Double-click left button to select the region of time corresponding to a note")); - - QToolBar *toolbar = addToolBar(tr("Tools Toolbar")); -- -+ - CommandHistory::getInstance()->registerToolbar(toolbar); - - QActionGroup *group = new QActionGroup(this); -@@ -517,18 +517,18 @@ MainWindow::setupEditMenu() - m_keyReference->setCategory - (tr("Navigate Tool Mouse Actions")); - m_keyReference->registerShortcut -- (tr("Navigate"), tr("Left"), -+ (tr("Navigate"), tr("Left"), - tr("Click left button and drag to move around")); - m_keyReference->registerShortcut -- (tr("Re-Analyse Area"), tr("Shift+Left"), -+ (tr("Re-Analyse Area"), tr("Shift+Left"), - tr("Shift-click left button and drag to define a specific pitch and time range to re-analyse")); - m_keyReference->registerShortcut -- (tr("Edit"), tr("Double-Click Left"), -+ (tr("Edit"), tr("Double-Click Left"), - tr("Double-click left button on an item to edit it")); - - m_keyReference->setCategory(tr("Tool Selection")); - action = toolbar->addAction(il.load("move"), -- tr("Edit")); -+ tr("Edit")); - action->setCheckable(true); - action->setShortcut(tr("2")); - action->setStatusTip(tr("Edit with Note Intelligence")); -@@ -540,16 +540,16 @@ MainWindow::setupEditMenu() - m_keyReference->setCategory - (tr("Note Edit Tool Mouse Actions")); - m_keyReference->registerShortcut -- (tr("Adjust Pitch"), tr("Left"), -+ (tr("Adjust Pitch"), tr("Left"), - tr("Click left button on the main part of a note and drag to move it up or down")); - m_keyReference->registerShortcut -- (tr("Split"), tr("Left"), -+ (tr("Split"), tr("Left"), - tr("Click left button on the bottom edge of a note to split it at the click point")); - m_keyReference->registerShortcut -- (tr("Resize"), tr("Left"), -+ (tr("Resize"), tr("Left"), - tr("Click left button on the left or right edge of a note and drag to change the time or duration of the note")); - m_keyReference->registerShortcut -- (tr("Erase"), tr("Shift+Left"), -+ (tr("Erase"), tr("Shift+Left"), - tr("Shift-click left button on a note to remove it")); - - -@@ -557,7 +557,7 @@ MainWindow::setupEditMenu() - - m_keyReference->setCategory(tr("Tool Selection")); - action = toolbar->addAction(il.load("notes"), -- tr("Free Edit")); -+ tr("Free Edit")); - action->setCheckable(true); - action->setShortcut(tr("3")); - action->setStatusTip(tr("Free Edit")); -@@ -567,7 +567,7 @@ MainWindow::setupEditMenu() - */ - - menu->addSeparator(); -- -+ - m_keyReference->setCategory(tr("Selection")); - - action = new QAction(tr("Select &All"), this); -@@ -593,9 +593,9 @@ MainWindow::setupEditMenu() - - menu->addSeparator(); - m_rightButtonMenu->addSeparator(); -- -+ - m_keyReference->setCategory(tr("Pitch Track")); -- -+ - action = new QAction(tr("Choose Higher Pitch"), this); - action->setShortcut(tr("Ctrl+Up")); - action->setStatusTip(tr("Move pitches up an octave, or to the next higher pitch candidate")); -@@ -604,7 +604,7 @@ MainWindow::setupEditMenu() - connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool))); - menu->addAction(action); - m_rightButtonMenu->addAction(action); -- -+ - action = new QAction(tr("Choose Lower Pitch"), this); - action->setShortcut(tr("Ctrl+Down")); - action->setStatusTip(tr("Move pitches down an octave, or to the next lower pitch candidate")); -@@ -622,7 +622,7 @@ MainWindow::setupEditMenu() - connect(this, SIGNAL(canClearSelection(bool)), m_showCandidatesAction, SLOT(setEnabled(bool))); - menu->addAction(m_showCandidatesAction); - m_rightButtonMenu->addAction(m_showCandidatesAction); -- -+ - action = new QAction(tr("Remove Pitches"), this); - action->setShortcut(tr("Ctrl+Backspace")); - action->setStatusTip(tr("Remove all pitch estimates within the selected region, making it unvoiced")); -@@ -634,7 +634,7 @@ MainWindow::setupEditMenu() - - menu->addSeparator(); - m_rightButtonMenu->addSeparator(); -- -+ - m_keyReference->setCategory(tr("Note Track")); - - action = new QAction(tr("Split Note"), this); -@@ -663,7 +663,7 @@ MainWindow::setupEditMenu() - connect(this, SIGNAL(canSnapNotes(bool)), action, SLOT(setEnabled(bool))); - menu->addAction(action); - m_rightButtonMenu->addAction(action); -- -+ - action = new QAction(tr("Form Note from Selection"), this); - action->setShortcut(tr("=")); - action->setStatusTip(tr("Form a note spanning the selected region, splitting any existing notes at its boundaries")); -@@ -702,7 +702,7 @@ MainWindow::setupViewMenu() - connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); - m_keyReference->registerShortcut(action); - menu->addAction(action); -- -+ - action = new QAction(tr("Peek &Right"), this); - action->setShortcut(tr("Alt+Right")); - action->setStatusTip(tr("Scroll the current pane to the right without changing the play position")); -@@ -723,7 +723,7 @@ MainWindow::setupViewMenu() - connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); - m_keyReference->registerShortcut(action); - menu->addAction(action); -- -+ - action = new QAction(il.load("zoom-out"), - tr("Zoom &Out"), this); - action->setShortcut(tr("Down")); -@@ -732,7 +732,7 @@ MainWindow::setupViewMenu() - connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); - m_keyReference->registerShortcut(action); - menu->addAction(action); -- -+ - action = new QAction(tr("Restore &Default Zoom"), this); - action->setStatusTip(tr("Restore the zoom level to the default")); - connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault())); -@@ -749,7 +749,7 @@ MainWindow::setupViewMenu() - menu->addAction(action); - - menu->addSeparator(); -- -+ - action = new QAction(tr("Set Displayed Fre&quency Range..."), this); - action->setStatusTip(tr("Set the minimum and maximum frequencies in the visible display")); - connect(action, SIGNAL(triggered()), this, SLOT(editDisplayExtents())); -@@ -768,11 +768,32 @@ MainWindow::setupAnalysisMenu() - QMenu *menu = menuBar()->addMenu(tr("&Analysis")); - menu->setTearOffEnabled(true); - -+ // Create a submenu for analysis mode options -+ QMenu *modeMenu = menu->addMenu(tr("Analysis &Mode")); -+ modeMenu->setTearOffEnabled(true); -+ -+ // Create a radio button group for recording analysis mode -+ QActionGroup *analysisGroup = new QActionGroup(this); -+ -+ m_analyseAfterRecord = new QAction(tr("Analyse &After Recording"), this); -+ m_analyseAfterRecord->setStatusTip(tr("Automatically trigger analysis after recording is complete.")); -+ m_analyseAfterRecord->setCheckable(true); -+ m_analyseAfterRecord->setActionGroup(analysisGroup); -+ connect(m_analyseAfterRecord, SIGNAL(triggered()), this, SLOT(analyseAfterRecordToggled())); -+ modeMenu->addAction(m_analyseAfterRecord); -+ -+ m_analyseDuringRecord = new QAction(tr("Analyse &During Recording"), this); -+ m_analyseDuringRecord->setStatusTip(tr("Automatically trigger analysis during recording.")); -+ m_analyseDuringRecord->setCheckable(true); -+ m_analyseDuringRecord->setActionGroup(analysisGroup); -+ connect(m_analyseDuringRecord, SIGNAL(triggered()), this, SLOT(recordAnalysisToggled())); -+ modeMenu->addAction(m_analyseDuringRecord); -+ - m_autoAnalyse = new QAction(tr("Auto-Analyse &New Audio"), this); - m_autoAnalyse->setStatusTip(tr("Automatically trigger analysis upon opening of a new audio file.")); - m_autoAnalyse->setCheckable(true); - connect(m_autoAnalyse, SIGNAL(triggered()), this, SLOT(autoAnalysisToggled())); -- menu->addAction(m_autoAnalyse); -+ modeMenu->addAction(m_autoAnalyse); - - action = new QAction(tr("&Analyse Now!"), this); - action->setStatusTip(tr("Trigger analysis of pitches and notes. (This will delete all existing pitches and notes.)")); -@@ -822,8 +843,8 @@ MainWindow::resetAnalyseOptions() - QSettings settings; - settings.beginGroup("Analyser"); - -- settings.setValue("auto-analysis", true); -- -+ setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); -+ - auto keyMap = Analyser::getAnalysisSettings(); - for (auto p: keyMap) { - settings.setValue(p.first, p.second); -@@ -833,14 +854,50 @@ MainWindow::resetAnalyseOptions() - updateAnalyseStates(); - } - -+MainWindow::RecordingAnalysisMode -+MainWindow::getRecordingAnalysisMode() const -+{ -+ QSettings settings; -+ settings.beginGroup("Analyser"); -+ int mode = settings.value("recording-analysis-mode", -+ static_cast(RecordingAnalysisMode::AfterRecording)).toInt(); -+ settings.endGroup(); -+ return static_cast(mode); -+} -+ -+void -+MainWindow::setRecordingAnalysisMode(RecordingAnalysisMode mode) -+{ -+ QSettings settings; -+ settings.beginGroup("Analyser"); -+ settings.setValue("recording-analysis-mode", static_cast(mode)); -+ settings.endGroup(); -+ updateAnalyseStates(); -+} -+ - void - MainWindow::updateAnalyseStates() - { - QSettings settings; - settings.beginGroup("Analyser"); - -- bool autoAnalyse = settings.value("auto-analysis", true).toBool(); -- m_autoAnalyse->setChecked(autoAnalyse); -+ // Handle recording analysis mode with enum-based approach -+ RecordingAnalysisMode mode = getRecordingAnalysisMode(); -+ -+ switch (mode) { -+ case RecordingAnalysisMode::AfterRecording: -+ m_analyseAfterRecord->setChecked(true); -+ m_analyseDuringRecord->setChecked(false); -+ m_autoAnalyse->setChecked(true); -+ m_autoAnalyse->setEnabled(true); -+ break; -+ case RecordingAnalysisMode::DuringRecording: -+ m_analyseDuringRecord->setChecked(true); -+ m_analyseAfterRecord->setChecked(false); -+ m_autoAnalyse->setChecked(false); -+ m_autoAnalyse->setEnabled(false); -+ break; -+ } - - std::map actions { - { "precision-analysis", m_precise }, -@@ -850,7 +907,7 @@ MainWindow::updateAnalyseStates() - }; - - auto keyMap = Analyser::getAnalysisSettings(); -- -+ - for (auto p: actions) { - auto ki = keyMap.find(p.first); - if (ki != keyMap.end()) { -@@ -864,6 +921,30 @@ MainWindow::updateAnalyseStates() - settings.endGroup(); - } - -+void -+MainWindow::recordAnalysisToggled() -+{ -+ QAction *a = qobject_cast(sender()); -+ if (!a) return; -+ -+ bool set = a->isChecked(); -+ if (set) { -+ setRecordingAnalysisMode(RecordingAnalysisMode::DuringRecording); -+ } -+} -+ -+void -+MainWindow::analyseAfterRecordToggled() -+{ -+ QAction *a = qobject_cast(sender()); -+ if (!a) return; -+ -+ bool set = a->isChecked(); -+ if (set) { -+ setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); -+ } -+} -+ - void - MainWindow::autoAnalysisToggled() - { -@@ -962,7 +1043,7 @@ MainWindow::setupHelpMenu() - { - QMenu *menu = menuBar()->addMenu(tr("&Help")); - menu->setTearOffEnabled(true); -- -+ - m_keyReference->setCategory(tr("Help")); - - IconLoader il; -@@ -971,9 +1052,9 @@ MainWindow::setupHelpMenu() - QAction *action; - - action = new QAction(il.load("help"), -- tr("&Help Reference"), this); -+ tr("&Help Reference"), this); - action->setShortcut(tr("F1")); -- action->setStatusTip(tr("Open the %1 reference manual").arg(name)); -+ action->setStatusTip(tr("Open the %1 reference manual").arg(name)); - connect(action, SIGNAL(triggered()), this, SLOT(help())); - m_keyReference->registerShortcut(action); - menu->addAction(action); -@@ -984,14 +1065,14 @@ MainWindow::setupHelpMenu() - connect(action, SIGNAL(triggered()), this, SLOT(keyReference())); - m_keyReference->registerShortcut(action); - menu->addAction(action); -- -- action = new QAction(tr("What's &New In This Release?"), this); -- action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); -+ -+ action = new QAction(tr("What's &New In This Release?"), this); -+ action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); - connect(action, SIGNAL(triggered()), this, SLOT(whatsNew())); - menu->addAction(action); -- -- action = new QAction(tr("&About %1").arg(name), this); -- action->setStatusTip(tr("Show information about %1").arg(name)); -+ -+ action = new QAction(tr("&About %1").arg(name), this); -+ action->setStatusTip(tr("Show information about %1").arg(name)); - connect(action, SIGNAL(triggered()), this, SLOT(about())); - menu->addAction(action); - } -@@ -1076,10 +1157,14 @@ MainWindow::setupToolbars() - recordAction->setShortcut(tr("Ctrl+Space")); - recordAction->setStatusTip(tr("Record a new audio file")); - connect(recordAction, SIGNAL(triggered()), this, SLOT(record())); -+ -+ connect(recordAction, SIGNAL(triggered()), -+ this, SLOT(analyseDuringRecordingRunner())); -+ - connect(m_recordTarget, SIGNAL(recordStatusChanged(bool)), -- recordAction, SLOT(setChecked(bool))); -+ recordAction, SLOT(setChecked(bool))); - connect(m_recordTarget, SIGNAL(recordCompleted()), -- this, SLOT(analyseNow())); -+ this, SLOT(analyseNow())); - connect(this, SIGNAL(canRecord(bool)), - recordAction, SLOT(setEnabled(bool))); - -@@ -1112,7 +1197,7 @@ MainWindow::setupToolbars() - oneLeftAction->setStatusTip(tr("Move cursor to the preceding note (or silence) onset.")); - connect(oneLeftAction, SIGNAL(triggered()), this, SLOT(moveOneNoteLeft())); - connect(this, SIGNAL(canScroll(bool)), oneLeftAction, SLOT(setEnabled(bool))); -- -+ - QAction *oneRightAction = new QAction(tr("O&ne Note Right"), this); - oneRightAction->setShortcut(tr("Ctrl+Right")); - oneRightAction->setStatusTip(tr("Move cursor to the succeeding note (or silence).")); -@@ -1124,7 +1209,7 @@ MainWindow::setupToolbars() - selectOneLeftAction->setStatusTip(tr("Select to the preceding note (or silence) onset.")); - connect(selectOneLeftAction, SIGNAL(triggered()), this, SLOT(selectOneNoteLeft())); - connect(this, SIGNAL(canScroll(bool)), selectOneLeftAction, SLOT(setEnabled(bool))); -- -+ - QAction *selectOneRightAction = new QAction(tr("S&elect One Note Right"), this); - selectOneRightAction->setShortcut(tr("Ctrl+Shift+Right")); - selectOneRightAction->setStatusTip(tr("Select to the succeeding note (or silence).")); -@@ -1186,7 +1271,7 @@ MainWindow::setupToolbars() - fastAction->setStatusTip(tr("Time-stretch playback to speed it up without changing pitch")); - connect(fastAction, SIGNAL(triggered()), this, SLOT(speedUpPlayback())); - connect(this, SIGNAL(canSpeedUpPlayback(bool)), fastAction, SLOT(setEnabled(bool))); -- -+ - QAction *slowAction = menu->addAction(tr("Slow Down")); - slowAction->setShortcut(tr("Ctrl+PgDown")); - slowAction->setStatusTip(tr("Time-stretch playback to slow it down without changing pitch")); -@@ -1234,7 +1319,7 @@ MainWindow::setupToolbars() - lpwSize = m_viewManager->scalePixelSize(26); - bigLpwSize = int(lpwSize * 2.8); - #endif -- -+ - m_audioLPW->setImageSize(lpwSize); - m_audioLPW->setBigImageSize(bigLpwSize); - toolbar->addWidget(m_audioLPW); -@@ -1303,7 +1388,7 @@ MainWindow::setupToolbars() - Pane::registerShortcuts(*m_keyReference); - - updateLayerStatuses(); -- -+ - // QTimer::singleShot(500, this, SLOT(betaReleaseWarning())); - } - -@@ -1340,7 +1425,7 @@ MainWindow::moveByOneNote(bool right, bool doSelect) - { - sv_frame_t frame = m_viewManager->getPlaybackFrame(); - cerr << "MainWindow::moveByOneNote startframe: " << frame << endl; -- -+ - bool isAtSelectionBoundary = false; - MultiSelection::SelectionList selections = m_viewManager->getSelections(); - if (!selections.empty()) { -@@ -1360,7 +1445,7 @@ MainWindow::moveByOneNote(bool right, bool doSelect) - //!!! This seems like a strange and inefficient way to do this - - //!!! there is almost certainly a better way making use of - //!!! EventSeries api -- -+ - EventVector points = model->getAllEvents(); - if (points.empty()) return; - -@@ -1434,24 +1519,24 @@ MainWindow::updateMenuStates() - if (currentPane) currentLayer = currentPane->getSelectedLayer(); - - bool haveMainModel = -- (getMainModel() != 0); -+ (getMainModel() != 0); - bool havePlayTarget = -- (m_playTarget != 0 || m_audioIO != 0); -+ (m_playTarget != 0 || m_audioIO != 0); - bool haveCurrentPane = - (currentPane != 0); - bool haveCurrentLayer = - (haveCurrentPane && - (currentLayer != 0)); -- bool haveSelection = -+ bool haveSelection = - (m_viewManager && - !m_viewManager->getSelections().empty()); -- bool haveCurrentTimeInstantsLayer = -+ bool haveCurrentTimeInstantsLayer = - (haveCurrentLayer && - qobject_cast(currentLayer)); -- bool haveCurrentTimeValueLayer = -+ bool haveCurrentTimeValueLayer = - (haveCurrentLayer && - qobject_cast(currentLayer)); -- bool pitchCandidatesVisible = -+ bool pitchCandidatesVisible = - m_analyser->arePitchCandidatesShown(); - - emit canChangePlaybackSpeed(true); -@@ -1463,11 +1548,11 @@ MainWindow::updateMenuStates() - m_analyser->isVisible(Analyser::Audio) && - m_analyser->getLayer(Analyser::Audio); - -- bool havePitchTrack = -+ bool havePitchTrack = - m_analyser->isVisible(Analyser::PitchTrack) && - m_analyser->getLayer(Analyser::PitchTrack); - -- bool haveNotes = -+ bool haveNotes = - m_analyser->isVisible(Analyser::Notes) && - m_analyser->getLayer(Analyser::Notes); - -@@ -1611,7 +1696,7 @@ MainWindow::updateLayerStatuses() - m_audioLPW->setEnabled(m_analyser->isAudible(Analyser::Audio)); - m_audioLPW->setLevel(m_analyser->getGain(Analyser::Audio)); - m_audioLPW->setPan(m_analyser->getPan(Analyser::Audio)); -- -+ - m_showPitch->setChecked(m_analyser->isVisible(Analyser::PitchTrack)); - m_playPitch->setChecked(m_analyser->isAudible(Analyser::PitchTrack)); - m_pitchLPW->setEnabled(m_analyser->isAudible(Analyser::PitchTrack)); -@@ -1633,7 +1718,7 @@ MainWindow::editDisplayExtents() - double min, max; - double vmin = 0; - double vmax = getMainModel()->getSampleRate() /2; -- -+ - if (!m_analyser->getDisplayFrequencyExtents(min, max)) { - //!!! - return; -@@ -1691,7 +1776,7 @@ MainWindow::newSession() - - m_viewManager->setGlobalCentreFrame - (pane->getFrameForX(width() / 2)); -- -+ - connect(pane, SIGNAL(contextHelpChanged(const QString &)), - this, SLOT(contextHelpChanged(const QString &))); - -@@ -1730,7 +1815,7 @@ MainWindow::closeSession() - m_document->removeLayerFromView - (pane, pane->getLayer(pane->getLayerCount() - 1)); - } -- -+ - m_overview->unregisterView(pane); - m_paneStack->deletePane(pane); - } -@@ -1739,12 +1824,12 @@ MainWindow::closeSession() - - Pane *pane = m_paneStack->getHiddenPane - (m_paneStack->getHiddenPaneCount() - 1); -- -+ - while (pane->getLayerCount() > 0) { - m_document->removeLayerFromView - (pane, pane->getLayer(pane->getLayerCount() - 1)); - } -- -+ - m_overview->unregisterView(pane); - m_paneStack->deletePane(pane); - } -@@ -1818,7 +1903,7 @@ MainWindow::openRecentFile() - { - QObject *obj = sender(); - QAction *action = qobject_cast(obj); -- -+ - if (!action) { - cerr << "WARNING: MainWindow::openRecentFile: sender is not an action" - << endl; -@@ -1845,19 +1930,19 @@ MainWindow::paneAdded(Pane *pane) - pane->setPlaybackFollow(PlaybackScrollPage); - m_paneStack->sizePanesEqually(); - if (m_overview) m_overview->registerView(pane); --} -+} - - void - MainWindow::paneHidden(Pane *pane) - { -- if (m_overview) m_overview->unregisterView(pane); --} -+ if (m_overview) m_overview->unregisterView(pane); -+} - - void - MainWindow::paneAboutToBeDeleted(Pane *pane) - { -- if (m_overview) m_overview->unregisterView(pane); --} -+ if (m_overview) m_overview->unregisterView(pane); -+} - - void - MainWindow::paneDropAccepted(Pane *pane, QStringList uriList) -@@ -1884,8 +1969,8 @@ MainWindow::paneDropAccepted(Pane *pane, QString text) - if (pane) m_paneStack->setCurrentPane(pane); - - QUrl testUrl(text); -- if (testUrl.scheme() == "file" || -- testUrl.scheme() == "http" || -+ if (testUrl.scheme() == "file" || -+ testUrl.scheme() == "http" || - testUrl.scheme() == "ftp") { - QStringList list; - list.push_back(text); -@@ -1950,7 +2035,7 @@ MainWindow::commitData(bool mayAskUser) - } else { - if (!QFileInfo(svDir).isDir()) return false; - } -- -+ - // This name doesn't have to be unguessable - #ifndef _WIN32 - QString fname = QString("tmp-%1-%2.sv") -@@ -1979,7 +2064,7 @@ MainWindow::checkSaveModified() - - if (!m_documentModified) return true; - -- int button = -+ int button = - QMessageBox::warning(this, - tr("Session modified"), - tr("The current session has been modified.\nDo you want to save it?"), -@@ -2009,7 +2094,7 @@ MainWindow::waitForInitialAnalysis() - // initial analysis is happening, because then we end up with an - // incomplete session on reload. There are certainly theoretically - // better ways to handle this... -- -+ - QSettings settings; - settings.beginGroup("Analyser"); - bool autoAnalyse = settings.value("auto-analysis", true).toBool(); -@@ -2029,7 +2114,7 @@ MainWindow::waitForInitialAnalysis() - QMessageBox::Cancel, - this); - -- connect(m_analyser, SIGNAL(initialAnalysisCompleted()), -+ connect(m_analyser, SIGNAL(initialAnalysisCompleted()), - &mb, SLOT(accept())); - - if (mb.exec() == QDialog::Accepted) { -@@ -2164,14 +2249,14 @@ MainWindow::exportToSVL(QString path, Layer *layer) - << "\n" - << "\n" - << " \n"; -- -+ - model->toXml(out, " "); -- -+ - out << " \n" - << " \n"; -- -+ - layer->toXml(out, " "); -- -+ - out << " \n" - << "\n"; - -@@ -2206,10 +2291,10 @@ MainWindow::importPitchLayer(FileSource source) - source.waitForData(); - - if (!waitForInitialAnalysis()) return FileOpenCancelled; -- -+ - QString path = source.getLocalFilename(); - -- RDFImporter::RDFDocumentType rdfType = -+ RDFImporter::RDFDocumentType rdfType = - RDFImporter::identifyDocumentType(QUrl::fromLocalFile(path).toString()); - - if (rdfType != RDFImporter::NotRDF) { -@@ -2221,12 +2306,12 @@ MainWindow::importPitchLayer(FileSource source) - (source.getExtension().toLower() == "xml" && - (SVFileReader::identifyXmlFile(source.getLocalFilename()) - == SVFileReader::SVLayerFile))) { -- -+ - //!!! - return FileOpenFailed; - - } else { -- -+ - try { - - CSVFormat format(path); -@@ -2246,7 +2331,7 @@ MainWindow::importPitchLayer(FileSource source) - - ModelId modelId = ModelById::add - (std::shared_ptr(model)); -- -+ - CommandHistory::getInstance()->startCompoundOperation - (tr("Import Pitch Track"), true); - -@@ -2272,7 +2357,7 @@ MainWindow::importPitchLayer(FileSource source) - } - } - } -- -+ - return FileOpenFailed; - } - -@@ -2292,7 +2377,7 @@ MainWindow::exportPitchLayer() - if (path == "") return; - - if (!waitForInitialAnalysis()) return; -- -+ - if (QFileInfo(path).suffix() == "") path += ".svl"; - - QString suffix = QFileInfo(path).suffix().toLower(); -@@ -2314,7 +2399,7 @@ MainWindow::exportPitchLayer() - } else { - - DataExportOptions options = DataExportFillGaps; -- -+ - CSVFileWriter writer(path, model.get(), - ((suffix == "csv") ? "," : "\t"), - options); -@@ -2358,7 +2443,7 @@ MainWindow::exportNoteLayer() - error = exportToSVL(path, layer); - - } else if (suffix == "mid" || suffix == "midi") { -- -+ - MIDIFileWriter writer(path, model.get(), model->getSampleRate()); - writer.write(); - if (!writer.isOK()) { -@@ -2376,7 +2461,7 @@ MainWindow::exportNoteLayer() - } else { - - DataExportOptions options = DataExportOmitLevel; -- -+ - CSVFileWriter writer(path, model.get(), - ((suffix == "csv") ? "," : "\t"), - options); -@@ -2411,7 +2496,7 @@ MainWindow::doubleClickSelectInvoked(sv_frame_t frame) - { - sv_frame_t f0, f1; - m_analyser->getEnclosingSelectionScope(frame, f0, f1); -- -+ - cerr << "MainWindow::doubleClickSelectInvoked(" << frame << "): [" << f0 << "," << f1 << "]" << endl; - - Selection sel(f0, f1); -@@ -2496,11 +2581,11 @@ MainWindow::regionOutlined(QRect r) - - sv_frame_t f0 = pane->getFrameForX(r.x()); - sv_frame_t f1 = pane->getFrameForX(r.x() + r.width()); -- -+ - double v0 = spectrogram->getFrequencyForY(pane, r.y() + r.height()); - double v1 = spectrogram->getFrequencyForY(pane, r.y()); - -- cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 -+ cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 - << ", frequency " << v0 << " -> " << v1 << endl; - - m_pendingConstraint = Analyser::FrequencyRange(v0, v1); -@@ -2589,7 +2674,7 @@ MainWindow::switchPitchDown() - (tr("Choose Lower Pitch Candidate"), true); - - MultiSelection::SelectionList selections = m_viewManager->getSelections(); -- -+ - for (MultiSelection::SelectionList::iterator k = selections.begin(); - k != selections.end(); ++k) { - m_analyser->switchPitchCandidate(*k, false); -@@ -2613,12 +2698,12 @@ MainWindow::snapNotesToPitches() - - CommandHistory::getInstance()->startCompoundOperation - (tr("Snap Notes to Pitches"), true); -- -+ - for (MultiSelection::SelectionList::iterator k = selections.begin(); - k != selections.end(); ++k) { - auxSnapNotes(*k); - } -- -+ - CommandHistory::getInstance()->endCompoundOperation(); - } - } -@@ -2632,7 +2717,7 @@ MainWindow::auxSnapNotes(Selection s) - if (!layer) return; - - layer->snapSelectedNotesToPitchTrack(m_analyser->getPane(), s); --} -+} - - void - MainWindow::splitNote() -@@ -2657,12 +2742,12 @@ MainWindow::mergeNotes() - - CommandHistory::getInstance()->startCompoundOperation - (tr("Merge Notes"), true); -- -+ - for (MultiSelection::SelectionList::iterator k = selections.begin(); - k != selections.end(); ++k) { - layer->mergeNotes(m_analyser->getPane(), *k, true); - } -- -+ - CommandHistory::getInstance()->endCompoundOperation(); - } - } -@@ -2680,12 +2765,12 @@ MainWindow::deleteNotes() - - CommandHistory::getInstance()->startCompoundOperation - (tr("Delete Notes"), true); -- -+ - for (MultiSelection::SelectionList::iterator k = selections.begin(); - k != selections.end(); ++k) { - layer->deleteSelectionInclusive(*k); - } -- -+ - CommandHistory::getInstance()->endCompoundOperation(); - } - } -@@ -2703,7 +2788,7 @@ MainWindow::formNoteFromSelection() - MultiSelection::SelectionList selections = m_viewManager->getSelections(); - - if (!selections.empty()) { -- -+ - CommandHistory::getInstance()->startCompoundOperation - (tr("Form Note from Selection"), true); - -@@ -2717,7 +2802,7 @@ MainWindow::formNoteFromSelection() - // existing pitch track if possible. This way we should - // handle all the possible cases of existing notes that - // may or may not overlap the start or end times -- -+ - sv_frame_t start = k->getStartFrame(); - sv_frame_t end = k->getEndFrame(); - -@@ -2728,18 +2813,18 @@ MainWindow::formNoteFromSelection() - if (!existing.empty()) { - defaultPitch = int(roundf(existing.begin()->getValue())); - } -- -+ - layer->splitNotesAt(pane, start); - layer->splitNotesAt(pane, end); - layer->deleteSelection(*k); -- -+ - layer->addNoteOn(start, defaultPitch, 100); - layer->addNoteOff(end, defaultPitch); -- -+ - layer->mergeNotes(pane, *k, false); - } - -- CommandHistory::getInstance()->endCompoundOperation(); -+ CommandHistory::getInstance()->endCompoundOperation(); - } - } - -@@ -2758,7 +2843,7 @@ MainWindow::playSpeedChanged(int position) - - char pcbuf[30]; - char facbuf[30]; -- -+ - if (position == centre) { - contextHelpChanged(tr("Playback speed: Normal")); - } else if (position < centre) { -@@ -2802,7 +2887,7 @@ MainWindow::playMonoToggled() - - playSpeedChanged(m_playSpeed->value()); - // TODO: pitch gain? --} -+} - - void - MainWindow::speedUpPlayback() -@@ -2841,7 +2926,7 @@ MainWindow::audioGainChanged(float gain) - m_analyser->setGain(Analyser::Audio, gain); - } - updateMenuStates(); --} -+} - - void - MainWindow::pitchGainChanged(float gain) -@@ -2856,7 +2941,7 @@ MainWindow::pitchGainChanged(float gain) - m_analyser->setGain(Analyser::PitchTrack, gain); - } - updateMenuStates(); --} -+} - - void - MainWindow::notesGainChanged(float gain) -@@ -2871,7 +2956,7 @@ MainWindow::notesGainChanged(float gain) - m_analyser->setGain(Analyser::Notes, gain); - } - updateMenuStates(); --} -+} - - void - MainWindow::audioPanChanged(float pan) -@@ -2879,7 +2964,7 @@ MainWindow::audioPanChanged(float pan) - contextHelpChanged(tr("Audio Pan: %1").arg(pan)); - m_analyser->setPan(Analyser::Audio, pan); - updateMenuStates(); --} -+} - - void - MainWindow::pitchPanChanged(float pan) -@@ -2887,7 +2972,7 @@ MainWindow::pitchPanChanged(float pan) - contextHelpChanged(tr("Pitch Pan: %1").arg(pan)); - m_analyser->setPan(Analyser::PitchTrack, pan); - updateMenuStates(); --} -+} - - void - MainWindow::notesPanChanged(float pan) -@@ -2895,7 +2980,7 @@ MainWindow::notesPanChanged(float pan) - contextHelpChanged(tr("Notes Pan: %1").arg(pan)); - m_analyser->setPan(Analyser::Notes, pan); - updateMenuStates(); --} -+} - - void - MainWindow::updateVisibleRangeDisplay(Pane *p) const -@@ -2944,7 +3029,7 @@ MainWindow::updateVisibleRangeDisplay(Pane *p) const - m_myStatusMessage = tr("Visible: %1 to %2 (duration %3)") - .arg(startStr).arg(endStr).arg(durationStr); - } -- -+ - getStatusLabel()->setText(m_myStatusMessage); - } - -@@ -3045,6 +3130,30 @@ MainWindow::analyseNow() - } - } - -+void -+MainWindow::analyseDuringRecordingRunner() -+{ -+ QSettings settings; -+ settings.beginGroup("Analyser"); -+ bool recordAnalyse = settings.value("record-analysis", true).toBool(); -+ settings.endGroup(); -+ if ((recordAnalyse && this->m_recordTarget->isRecording())) -+ { -+ connect(this->m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, this, &MainWindow::analyseDuringRecording); -+ connect(this->m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, this, [this]() { -+ disconnect(this->m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, nullptr, nullptr); -+ disconnect(this->m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, nullptr, nullptr); -+ analyseDuringRecording(); -+ }); -+ } -+} -+ -+void -+MainWindow::analyseDuringRecording() -+{ -+ m_analyser->analyseRecordingToEnd(m_recordTarget->getRecordDuration()); -+} -+ - void - MainWindow::analyseNewMainModel() - { -@@ -3053,7 +3162,7 @@ MainWindow::analyseNewMainModel() - SVDEBUG << "MainWindow::analyseNewMainModel: main model is " << model << endl; - - SVDEBUG << "(document is " << m_document << ", it says main model is " << m_document->getMainModel() << ")" << endl; -- -+ - if (!model) { - cerr << "no main model!" << endl; - return; -@@ -3117,7 +3226,7 @@ MainWindow::analyseNewMainModel() - m_analyser->setAudible(Analyser::PitchTrack, false); - m_analyser->setAudible(Analyser::Notes, false); - } -- -+ - updateLayerStatuses(); - documentRestored(); - } -@@ -3265,16 +3374,16 @@ MainWindow::whatsNew() - - QDialog *d = new QDialog(this); - d->setWindowTitle(tr("What's New")); -- -+ - QGridLayout *layout = new QGridLayout; - d->setLayout(layout); - - int row = 0; -- -+ - QLabel *iconLabel = new QLabel; - iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64)); - layout->addWidget(iconLabel, row, 0); -- -+ - layout->addWidget - (new QLabel(tr("

What's New in %1

") - .arg(QApplication::applicationName())), -@@ -3287,7 +3396,7 @@ MainWindow::whatsNew() - if (m_newerVersionIs != "") { - layout->addWidget(new QLabel(tr("Note: A newer version of %1 is available.
(Version %2 is available; you are using version %3)").arg(QApplication::applicationName()).arg(m_newerVersionIs).arg(TONY_VERSION)), row++, 1, 1, 2); - } -- -+ - QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok); - layout->addWidget(bb, row++, 0, 1, 3); - connect(bb, SIGNAL(accepted()), d, SLOT(accept())); -@@ -3316,7 +3425,7 @@ MainWindow::whatsNew() - - d->setMinimumSize(m_viewManager->scalePixelSize(520), - m_viewManager->scalePixelSize(450)); -- -+ - d->exec(); - - delete d; -@@ -3360,7 +3469,7 @@ MainWindow::about() - aboutText += tr("

Using Qt framework version %1.

") - .arg(QT_VERSION_STR); - -- aboutText += -+ aboutText += - "

Copyright © 2005–2019 Chris Cannam, Queen Mary University of London, and the Tony project authors: Matthias Mauch, George Fazekas, Justin Salamon, and Rachel Bittner.

" - "

pYIN analysis plugin written by Matthias Mauch.

" - "

This program is free software; you can redistribute it and/or " -@@ -3368,18 +3477,18 @@ MainWindow::about() - "published by the Free Software Foundation; either version 2 of the " - "License, or (at your option) any later version.
See the file " - "COPYING included with this distribution for more information.

"; -- -+ - // use our own dialog so we can influence the size - - QDialog *d = new QDialog(this); - - d->setWindowTitle(tr("About %1").arg(QApplication::applicationName())); -- -+ - QGridLayout *layout = new QGridLayout; - d->setLayout(layout); - - int row = 0; -- -+ - QLabel *iconLabel = new QLabel; - iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64)); - layout->addWidget(iconLabel, row, 0, Qt::AlignTop); -@@ -3402,7 +3511,7 @@ MainWindow::about() - - d->setMinimumSize(m_viewManager->scalePixelSize(420), - m_viewManager->scalePixelSize(200)); -- -+ - d->exec(); - - delete d; -@@ -3418,7 +3527,7 @@ void - MainWindow::newerVersionAvailable(QString version) - { - m_newerVersionIs = version; -- -+ - //!!! nicer URL would be nicer - QSettings settings; - settings.beginGroup("NewerVersionWarning"); -@@ -3442,9 +3551,9 @@ MainWindow::ffwd() - - sv_samplerate_t sr = getMainModel()->getSampleRate(); - -- // The step is supposed to scale and be as wide as a step of -+ // The step is supposed to scale and be as wide as a step of - // m_defaultFfwdRwdStep seconds at zoom level 720 and sr = 44100 -- -+ - ZoomLevel zoom = m_viewManager->getGlobalZoom(); - double framesPerPixel = 1.0; - if (zoom.zone == ZoomLevel::FramesPerPixel) { -@@ -3455,20 +3564,20 @@ MainWindow::ffwd() - double defaultFramesPerPixel = (720 * 44100) / sr; - double scaler = framesPerPixel / defaultFramesPerPixel; - RealTime step = m_defaultFfwdRwdStep * scaler; -- -+ - frame = RealTime::realTime2Frame - (RealTime::frame2RealTime(frame, sr) + step, sr); - - if (frame > getMainModel()->getEndFrame()) { - frame = getMainModel()->getEndFrame(); - } -- -+ - if (frame < 0) frame = 0; - - if (m_viewManager->getPlaySelectionMode()) { - frame = m_viewManager->constrainFrameToSelection(frame); - } -- -+ - m_viewManager->setPlaybackFrame(frame); - - if (frame == getMainModel()->getEndFrame() && -@@ -3489,7 +3598,7 @@ MainWindow::rewind() - - sv_samplerate_t sr = getMainModel()->getSampleRate(); - -- // The step is supposed to scale and be as wide as a step of -+ // The step is supposed to scale and be as wide as a step of - // m_defaultFfwdRwdStep seconds at zoom level 720 and sr = 44100 - - ZoomLevel zoom = m_viewManager->getGlobalZoom(); -@@ -3505,7 +3614,7 @@ MainWindow::rewind() - - frame = RealTime::realTime2Frame - (RealTime::frame2RealTime(frame, sr) - step, sr); -- -+ - if (frame < getMainModel()->getStartFrame()) { - frame = getMainModel()->getStartFrame(); - } -diff --git a/main/MainWindow.h b/main/MainWindow.h -index f14d5ca..48e2db2 100644 ---- a/main/MainWindow.h -+++ b/main/MainWindow.h -@@ -1,3 +1,4 @@ -+#pragma once - /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ - - /* -@@ -5,7 +6,7 @@ - An intonation analysis and annotation tool - Centre for Digital Music, Queen Mary, University of London. - This file copyright 2006-2012 Chris Cannam and QMUL. -- -+ - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of the -@@ -30,8 +31,13 @@ class MainWindow : public sv::MainWindowBase - Q_OBJECT - - public: -+ enum class RecordingAnalysisMode { -+ AfterRecording = 0, // Default -+ DuringRecording = 1 -+ }; -+ - MainWindow(AudioMode audioMode, -- bool withSonification = true, -+ bool withSonification = true, - bool withSpectrogram = true); - virtual ~MainWindow(); - -@@ -87,7 +93,13 @@ protected slots: - virtual void editDisplayExtents(); - - virtual void analyseNow(); -+ -+ virtual void analyseDuringRecording(); -+ virtual void analyseDuringRecordingRunner(); - virtual void resetAnalyseOptions(); -+ virtual void recordAnalysisToggled(); -+ virtual void analyseAfterRecordToggled(); -+ - virtual void autoAnalysisToggled(); - virtual void precisionAnalysisToggled(); - virtual void lowampAnalysisToggled(); -@@ -164,7 +176,7 @@ protected slots: - virtual void whatsNew(); - - virtual void betaReleaseWarning(); -- -+ - virtual void newerVersionAvailable(QString); - - virtual void selectionChangedByUser(); -@@ -205,11 +217,13 @@ protected: - bool m_intelligentActionOn; // GF: !!! temporary - - QAction *m_autoAnalyse; -+ QAction *m_analyseDuringRecord; -+ QAction *m_analyseAfterRecord; - QAction *m_precise; - QAction *m_lowamp; - QAction *m_onset; - QAction *m_prune; -- -+ - QAction *m_showAudio; - QAction *m_showSpect; - QAction *m_showPitch; -@@ -224,6 +238,7 @@ protected: - sv::ActivityLog *m_activityLog; - sv::KeyReference *m_keyReference; - sv::VersionTester *m_versionTester; -+ - QString m_newerVersionIs; - - sv::sv_frame_t m_selectionAnchor; -@@ -246,6 +261,9 @@ protected: - virtual void setupHelpMenu(); - virtual void setupToolbars(); - -+ RecordingAnalysisMode getRecordingAnalysisMode() const; -+ void setRecordingAnalysisMode(RecordingAnalysisMode mode); -+ - virtual void octaveShift(bool up); - - virtual void auxSnapNotes(sv::Selection s); diff --git a/main/Analyser.cpp b/main/Analyser.cpp index a7ba1b7..e9b7a3a 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -14,6 +14,10 @@ */ #include "Analyser.h" +#include "OverlapProcessor.h" + +#include +#include #include "transform/TransformFactory.h" #include "transform/ModelTransformer.h" @@ -656,363 +660,16 @@ void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* docume }); } -// Custom processing logic for pitch track (SparseTimeValueModel) -static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { - auto allEvents = toModel->getAllEvents(); - auto points = fromModel->getAllEvents(); - - // Add context start timestamp to all points from the new analysis - std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { - return point.withFrame(point.getFrame() + contextStart); - }); - - // Remove all events from toModel that extend beyond contextStart to prevent overlaps - EventVector eventsToRemove; - for (const auto& event : allEvents) { - if (event.getFrame() >= contextStart) { - eventsToRemove.push_back(event); - } - } - - for (const auto& event : eventsToRemove) { - toModel->remove(event); - } - - // After cleanup, get the remaining events for overlap processing - allEvents = toModel->getAllEvents(); - - // Handle potential overlaps between the last existing event and first new event - if (!allEvents.empty() && !points.empty()) { - auto& lastExistingEvent = allEvents.back(); - auto& firstNewEvent = points.front(); - - // Check if there's a gap or overlap between last existing and first new event - auto gapFrames = firstNewEvent.getFrame() - lastExistingEvent.getFrame(); - - // If events are very close (within ~11ms at 44.1kHz), interpolate between them - const sv_frame_t interpolationThreshold = 512; // ~11ms at 44.1kHz - - if (gapFrames > 0 && gapFrames <= interpolationThreshold) { - // Small gap - add interpolated point if pitch values are similar - if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { - auto lastValue = lastExistingEvent.getValue(); - auto firstValue = firstNewEvent.getValue(); - auto valueDiff = std::abs(lastValue - firstValue) / lastValue; - - // Only interpolate if pitch values are within 10% of each other - if (valueDiff <= 0.1) { - auto midFrame = lastExistingEvent.getFrame() + gapFrames / 2; - auto midValue = (lastValue + firstValue) / 2.0; - Event interpolatedEvent = Event(midFrame, midValue, "interpolated"); - toModel->add(interpolatedEvent); - } - } - } - } - - return points; -} - -// Structure to represent a group of overlapping events -struct OverlapGroup { - EventVector events; - sv_frame_t startFrame; - sv_frame_t endFrame; - - OverlapGroup(const EventVector& evts) : events(evts) { - if (!events.empty()) { - startFrame = events.front().getFrame(); - endFrame = events.front().getFrame() + events.front().getDuration(); - - for (const auto& event : events) { - startFrame = std::min(startFrame, event.getFrame()); - endFrame = std::max(endFrame, event.getFrame() + event.getDuration()); - } - } else { - startFrame = endFrame = 0; - } - } -}; - -// Helper function to find all overlap groups among a set of events -static std::vector findOverlapGroups(const EventVector& events) { - std::vector groups; - std::vector processed(events.size(), false); - - for (size_t i = 0; i < events.size(); ++i) { - if (processed[i]) continue; - - EventVector currentGroup; - currentGroup.push_back(events[i]); - processed[i] = true; - - // Find all events that overlap with any event in the current group - bool foundOverlap; - do { - foundOverlap = false; - for (size_t j = 0; j < events.size(); ++j) { - if (processed[j]) continue; - - // Check if event j overlaps with any event in the current group - for (const auto& groupEvent : currentGroup) { - auto eventStart = events[j].getFrame(); - auto eventEnd = events[j].getFrame() + events[j].getDuration(); - auto groupStart = groupEvent.getFrame(); - auto groupEnd = groupEvent.getFrame() + groupEvent.getDuration(); - - // Check for temporal overlap - if (eventStart < groupEnd && eventEnd > groupStart) { - currentGroup.push_back(events[j]); - processed[j] = true; - foundOverlap = true; - break; - } - } - if (foundOverlap) break; - } - } while (foundOverlap); - - // Only create groups for actual overlaps (more than one event) - if (currentGroup.size() > 1) { - groups.emplace_back(currentGroup); - } - } - - return groups; -} - -// Enhanced helper function to calculate weighted frequency from multiple overlapping notes -static float calculateWeightedFrequency(const EventVector& overlappingEvents, - sv_frame_t overlapStart, sv_frame_t overlapDuration) { - if (overlappingEvents.empty()) return 0.0f; - if (overlappingEvents.size() == 1) { - return overlappingEvents[0].hasValue() ? overlappingEvents[0].getValue() : 0.0f; - } - - // Calculate weighted contributions from all events - std::vector frequencies; - std::vector weights; - auto overlapEnd = overlapStart + overlapDuration; - - for (const auto& event : overlappingEvents) { - float freq = event.hasValue() ? event.getValue() : 0.0f; - if (freq <= 0.0f) continue; // Skip invalid frequencies - - // Calculate this event's contribution to the overlap - auto eventStart = event.getFrame(); - auto eventEnd = event.getFrame() + event.getDuration(); - - auto eventOverlapStart = std::max(eventStart, overlapStart); - auto eventOverlapEnd = std::min(eventEnd, overlapEnd); - auto eventOverlapContrib = std::max(0LL, eventOverlapEnd - eventOverlapStart); - - if (eventOverlapContrib > 0) { - frequencies.push_back(freq); - weights.push_back(static_cast(eventOverlapContrib)); - } - } - - if (frequencies.empty()) return 0.0f; - if (frequencies.size() == 1) return frequencies[0]; - - // Normalize weights - double totalWeight = 0.0; - for (double weight : weights) totalWeight += weight; - if (totalWeight <= 0.0) { - // Fallback to simple geometric mean - double logSum = 0.0; - for (float freq : frequencies) { - logSum += std::log(freq); - } - return std::exp(logSum / frequencies.size()); - } - - for (double& weight : weights) weight /= totalWeight; - - // Calculate weighted geometric mean for better musical accuracy - double weightedLogSum = 0.0; - for (size_t i = 0; i < frequencies.size(); ++i) { - weightedLogSum += std::log(frequencies[i]) * weights[i]; - } - - return std::exp(weightedLogSum); -} - -// Helper function to merge a group of overlapping events into a single event -static Event mergeOverlapGroup(const OverlapGroup& group) { - if (group.events.empty()) { - return Event(0, 0.0f, ""); - } - - if (group.events.size() == 1) { - return group.events[0]; - } - - // Calculate merged event properties - auto mergedStart = group.startFrame; - auto mergedEnd = group.endFrame; - auto mergedDuration = mergedEnd - mergedStart; - - // Calculate weighted frequency for the entire overlap - float weightedFreq = calculateWeightedFrequency(group.events, mergedStart, mergedDuration); - - // Use properties from the event with the longest duration as base - const Event* longestEvent = &group.events[0]; - for (const auto& event : group.events) { - if (event.getDuration() > longestEvent->getDuration()) { - longestEvent = &event; - } - } - - // Create merged event - Event mergedEvent = longestEvent->withFrame(mergedStart) - .withDuration(mergedDuration); - - if (weightedFreq > 0.0f) { - mergedEvent = mergedEvent.withValue(weightedFreq); - } - - // Preserve label if available - if (longestEvent->hasLabel()) { - mergedEvent = mergedEvent.withLabel(longestEvent->getLabel()); - } - - // Preserve level if available - if (longestEvent->hasLevel()) { - mergedEvent = mergedEvent.withLevel(longestEvent->getLevel()); - } - - return mergedEvent; -} +// Global overlap processor instance for efficient processing +static OverlapProcessor s_overlapProcessor; -// Legacy two-event version for backward compatibility -static float calculateWeightedFrequency(const Event& prevEvent, const Event& nextEvent, - sv_frame_t overlapStart, sv_frame_t overlapDuration) { - EventVector events = {prevEvent, nextEvent}; - return calculateWeightedFrequency(events, overlapStart, overlapDuration); +// Wrapper functions for backward compatibility +static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { + return s_overlapProcessor.processPitchModel(contextStart, fromModel, toModel); } -// Enhanced custom processing logic for FlexiNoteLayer with multi-note overlap support static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { - auto allEvents = toModel->getAllEvents(); - auto points = fromModel->getAllEvents(); - - // Vamp doesn't add current timestamp for note features, so, do it manually - std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { - return point.withFrame(point.getFrame() + contextStart); - }); - - // Enhanced cleanup strategy: more intelligent overlap-aware removal - EventVector eventsToRemove; - EventVector remainingEvents; - - // Identify events that might overlap with incoming analysis - for (const auto& event : allEvents) { - bool potentialOverlap = false; - for (const auto& newEvent : points) { - // Check if existing event could potentially overlap with any new event - auto existingStart = event.getFrame(); - auto existingEnd = event.getFrame() + event.getDuration(); - auto newStart = newEvent.getFrame(); - auto newEnd = newEvent.getFrame() + newEvent.getDuration(); - - if (existingStart < newEnd && existingEnd > newStart) { - potentialOverlap = true; - break; - } - } - - if (potentialOverlap) { - eventsToRemove.push_back(event); - } else { - remainingEvents.push_back(event); - } - } - - // Remove potentially overlapping events from the model - for (const auto& event : eventsToRemove) { - toModel->remove(event); - } - - // Create combined event set for overlap detection - EventVector combinedEvents; - - // Add remaining non-overlapping events - for (const auto& event : remainingEvents) { - combinedEvents.push_back(event); - } - - // Add previously removed events that might need merging - for (const auto& event : eventsToRemove) { - combinedEvents.push_back(event); - } - - // Add new incoming events - for (const auto& event : points) { - combinedEvents.push_back(event); - } - - // Find all overlap groups in the combined event set - auto overlapGroups = findOverlapGroups(combinedEvents); - - // Process each overlap group - EventVector finalEvents; - std::vector processed(combinedEvents.size(), false); - - // Process overlap groups first - for (const auto& group : overlapGroups) { - Event mergedEvent = mergeOverlapGroup(group); - finalEvents.push_back(mergedEvent); - - // Mark all events in this group as processed - for (const auto& groupEvent : group.events) { - for (size_t i = 0; i < combinedEvents.size(); ++i) { - if (!processed[i] && - combinedEvents[i].getFrame() == groupEvent.getFrame() && - combinedEvents[i].getDuration() == groupEvent.getDuration()) { - processed[i] = true; - } - } - } - } - - // Add non-overlapping events - for (size_t i = 0; i < combinedEvents.size(); ++i) { - if (!processed[i]) { - finalEvents.push_back(combinedEvents[i]); - } - } - - // Filter to return only the new/modified events that should be added - EventVector resultEvents; - for (const auto& event : finalEvents) { - bool isNew = false; - - // Check if this event is derived from new analysis or is a merge result - for (const auto& originalNew : points) { - if (event.getFrame() >= originalNew.getFrame() - 1000 && // Allow some tolerance - event.getFrame() <= originalNew.getFrame() + originalNew.getDuration() + 1000) { - isNew = true; - break; - } - } - - // Also include events that are merge results (modified existing events) - bool isMergeResult = false; - for (const auto& removedEvent : eventsToRemove) { - if (event.getFrame() >= removedEvent.getFrame() - 1000 && - event.getFrame() <= removedEvent.getFrame() + removedEvent.getDuration() + 1000) { - isMergeResult = true; - break; - } - } - - if (isNew || isMergeResult) { - resultEvents.push_back(event); - } - } - - return resultEvents; + return s_overlapProcessor.processNoteModel(contextStart, fromModel, toModel); } QString diff --git a/main/OverlapProcessor.cpp b/main/OverlapProcessor.cpp new file mode 100644 index 0000000..916b73f --- /dev/null +++ b/main/OverlapProcessor.cpp @@ -0,0 +1,395 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Tony + An intonation analysis and annotation tool + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006-2012 Chris Cannam and QMUL. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#include "OverlapProcessor.h" +#include "data/model/Event.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/NoteModel.h" +#include +#include +#include + +using std::cerr; +using std::endl; + +// OverlapGroup implementation +OverlapGroup::OverlapGroup(const EventVector& evts) : events(evts) { + if (!events.empty()) { + startFrame = events.front().getFrame(); + endFrame = events.front().getFrame() + events.front().getDuration(); + + for (const auto& event : events) { + startFrame = std::min(startFrame, event.getFrame()); + endFrame = std::max(endFrame, event.getFrame() + event.getDuration()); + } + } else { + startFrame = endFrame = 0; + } +} + +// OverlapProcessor implementation +OverlapProcessor::OverlapProcessor(const OverlapConfig& config) : m_config(config) { +} + +std::vector OverlapProcessor::findOverlapGroups(const EventVector& events) const { + std::vector groups; + std::vector processed(events.size(), false); + + for (size_t i = 0; i < events.size(); ++i) { + if (processed[i]) continue; + + EventVector currentGroup; + currentGroup.push_back(events[i]); + processed[i] = true; + + // Find all events that overlap with any event in the current group + bool foundOverlap; + do { + foundOverlap = false; + for (size_t j = 0; j < events.size(); ++j) { + if (processed[j]) continue; + + // Check if event j overlaps with any event in the current group + for (const auto& groupEvent : currentGroup) { + if (eventsOverlap(events[j], groupEvent)) { + currentGroup.push_back(events[j]); + processed[j] = true; + foundOverlap = true; + break; + } + } + if (foundOverlap) break; + } + } while (foundOverlap); + + // Only create groups for actual overlaps (more than one event) + if (currentGroup.size() > 1) { + groups.emplace_back(currentGroup); + } + } + + return groups; +} + +float OverlapProcessor::calculateWeightedFrequency(const EventVector& overlappingEvents, + sv_frame_t overlapStart, + sv_frame_t overlapDuration) const { + if (overlappingEvents.empty()) return 0.0f; + if (overlappingEvents.size() == 1) { + return overlappingEvents[0].hasValue() ? overlappingEvents[0].getValue() : 0.0f; + } + + // Calculate weighted contributions from all events + std::vector frequencies; + std::vector weights; + auto overlapEnd = overlapStart + overlapDuration; + + for (const auto& event : overlappingEvents) { + float freq = event.hasValue() ? event.getValue() : 0.0f; + if (freq <= 0.0f) continue; // Skip invalid frequencies + + // Calculate this event's contribution to the overlap + auto eventStart = event.getFrame(); + auto eventEnd = event.getFrame() + event.getDuration(); + + auto eventOverlapStart = std::max(eventStart, overlapStart); + auto eventOverlapEnd = std::min(eventEnd, overlapEnd); + auto eventOverlapContrib = std::max(0LL, eventOverlapEnd - eventOverlapStart); + + if (eventOverlapContrib > 0) { + frequencies.push_back(freq); + weights.push_back(static_cast(eventOverlapContrib)); + } + } + + if (frequencies.empty()) return 0.0f; + if (frequencies.size() == 1) return frequencies[0]; + + // Normalize weights + double totalWeight = 0.0; + for (double weight : weights) totalWeight += weight; + if (totalWeight <= 0.0) { + // Fallback to simple geometric mean + double logSum = 0.0; + for (float freq : frequencies) { + logSum += std::log(freq); + } + return std::exp(logSum / frequencies.size()); + } + + for (double& weight : weights) weight /= totalWeight; + + // Calculate weighted geometric mean for better musical accuracy + double weightedLogSum = 0.0; + for (size_t i = 0; i < frequencies.size(); ++i) { + weightedLogSum += std::log(frequencies[i]) * weights[i]; + } + + return std::exp(weightedLogSum); +} + +float OverlapProcessor::calculateWeightedFrequency(const Event& prevEvent, + const Event& nextEvent, + sv_frame_t overlapStart, + sv_frame_t overlapDuration) const { + EventVector events = {prevEvent, nextEvent}; + return calculateWeightedFrequency(events, overlapStart, overlapDuration); +} + +Event OverlapProcessor::mergeOverlapGroup(const OverlapGroup& group) const { + if (group.isEmpty()) { + return Event(0, 0.0f, ""); + } + + if (group.size() == 1) { + return group.events[0]; + } + + // Calculate merged event properties + auto mergedStart = group.startFrame; + auto mergedEnd = group.endFrame; + auto mergedDuration = mergedEnd - mergedStart; + + // Calculate weighted frequency for the entire overlap + float weightedFreq = calculateWeightedFrequency(group.events, mergedStart, mergedDuration); + + // Use properties from the event with the longest duration as base + const Event* longestEvent = findLongestEvent(group.events); + + // Create merged event + Event mergedEvent = longestEvent->withFrame(mergedStart) + .withDuration(mergedDuration); + + if (weightedFreq > 0.0f) { + mergedEvent = mergedEvent.withValue(weightedFreq); + } + + // Preserve label if available + if (longestEvent->hasLabel()) { + mergedEvent = mergedEvent.withLabel(longestEvent->getLabel()); + } + + // Preserve level if available + if (longestEvent->hasLevel()) { + mergedEvent = mergedEvent.withLevel(longestEvent->getLevel()); + } + + return mergedEvent; +} + +EventVector OverlapProcessor::processPitchModel(sv_frame_t contextStart, + std::shared_ptr fromModel, + std::shared_ptr toModel) const { + auto allEvents = toModel->getAllEvents(); + auto points = fromModel->getAllEvents(); + + // Add context start timestamp to all points from the new analysis + std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { + return point.withFrame(point.getFrame() + contextStart); + }); + + // Remove all events from toModel that extend beyond contextStart to prevent overlaps + EventVector eventsToRemove; + for (const auto& event : allEvents) { + if (event.getFrame() >= contextStart) { + eventsToRemove.push_back(event); + } + } + + for (const auto& event : eventsToRemove) { + toModel->remove(event); + } + + // After cleanup, get the remaining events for overlap processing + allEvents = toModel->getAllEvents(); + + // Handle potential overlaps between the last existing event and first new event + if (!allEvents.empty() && !points.empty()) { + auto& lastExistingEvent = allEvents.back(); + auto& firstNewEvent = points.front(); + + // Check if there's a gap or overlap between last existing and first new event + auto gapFrames = firstNewEvent.getFrame() - lastExistingEvent.getFrame(); + + // If events are very close (within interpolation threshold), interpolate between them + if (gapFrames > 0 && gapFrames <= m_config.interpolationThreshold) { + // Small gap - add interpolated point if pitch values are similar + if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { + auto lastValue = lastExistingEvent.getValue(); + auto firstValue = firstNewEvent.getValue(); + auto valueDiff = std::abs(lastValue - firstValue) / lastValue; + + // Only interpolate if pitch values are within similarity threshold + if (valueDiff <= m_config.pitchSimilarityThreshold) { + auto midFrame = lastExistingEvent.getFrame() + gapFrames / 2; + auto midValue = (lastValue + firstValue) / 2.0; + Event interpolatedEvent = Event(midFrame, midValue, "interpolated"); + toModel->add(interpolatedEvent); + } + } + } + } + + return points; +} + +EventVector OverlapProcessor::processNoteModel(sv_frame_t contextStart, + std::shared_ptr fromModel, + std::shared_ptr toModel) const { + auto allEvents = toModel->getAllEvents(); + auto points = fromModel->getAllEvents(); + + // Vamp doesn't add current timestamp for note features, so, do it manually + std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { + return point.withFrame(point.getFrame() + contextStart); + }); + + // Enhanced cleanup strategy: more intelligent overlap-aware removal + EventVector eventsToRemove; + EventVector remainingEvents; + + // Categorize events based on potential overlap with incoming analysis + categorizeEvents(allEvents, points, eventsToRemove, remainingEvents); + + // Remove potentially overlapping events from the model + for (const auto& event : eventsToRemove) { + toModel->remove(event); + } + + // Create combined event set for overlap detection + EventVector combinedEvents; + + // Add remaining non-overlapping events + for (const auto& event : remainingEvents) { + combinedEvents.push_back(event); + } + + // Add previously removed events that might need merging + for (const auto& event : eventsToRemove) { + combinedEvents.push_back(event); + } + + // Add new incoming events + for (const auto& event : points) { + combinedEvents.push_back(event); + } + + // Find all overlap groups in the combined event set + auto overlapGroups = findOverlapGroups(combinedEvents); + + // Process each overlap group + EventVector finalEvents; + std::vector processed(combinedEvents.size(), false); + + // Process overlap groups first + for (const auto& group : overlapGroups) { + Event mergedEvent = mergeOverlapGroup(group); + finalEvents.push_back(mergedEvent); + + // Mark all events in this group as processed + for (const auto& groupEvent : group.events) { + for (size_t i = 0; i < combinedEvents.size(); ++i) { + if (!processed[i] && + combinedEvents[i].getFrame() == groupEvent.getFrame() && + combinedEvents[i].getDuration() == groupEvent.getDuration()) { + processed[i] = true; + } + } + } + } + + // Add non-overlapping events + for (size_t i = 0; i < combinedEvents.size(); ++i) { + if (!processed[i]) { + finalEvents.push_back(combinedEvents[i]); + } + } + + // Filter to return only the new/modified events that should be added + EventVector resultEvents; + for (const auto& event : finalEvents) { + bool isNew = false; + + // Check if this event is derived from new analysis or is a merge result + for (const auto& originalNew : points) { + if (event.getFrame() >= originalNew.getFrame() - m_config.overlapTolerance && + event.getFrame() <= originalNew.getFrame() + originalNew.getDuration() + m_config.overlapTolerance) { + isNew = true; + break; + } + } + + // Also include events that are merge results (modified existing events) + bool isMergeResult = false; + for (const auto& removedEvent : eventsToRemove) { + if (event.getFrame() >= removedEvent.getFrame() - m_config.overlapTolerance && + event.getFrame() <= removedEvent.getFrame() + removedEvent.getDuration() + m_config.overlapTolerance) { + isMergeResult = true; + break; + } + } + + if (isNew || isMergeResult) { + resultEvents.push_back(event); + } + } + + return resultEvents; +} + +// Private helper methods +bool OverlapProcessor::eventsOverlap(const Event& a, const Event& b) const { + auto aStart = a.getFrame(); + auto aEnd = a.getFrame() + a.getDuration(); + auto bStart = b.getFrame(); + auto bEnd = b.getFrame() + b.getDuration(); + + // Check for temporal overlap + return aStart < bEnd && aEnd > bStart; +} + +const Event* OverlapProcessor::findLongestEvent(const EventVector& events) const { + if (events.empty()) return nullptr; + + const Event* longestEvent = &events[0]; + for (const auto& event : events) { + if (event.getDuration() > longestEvent->getDuration()) { + longestEvent = &event; + } + } + return longestEvent; +} + +void OverlapProcessor::categorizeEvents(const EventVector& allEvents, + const EventVector& newEvents, + EventVector& eventsToRemove, + EventVector& remainingEvents) const { + // Identify events that might overlap with incoming analysis + for (const auto& event : allEvents) { + bool potentialOverlap = false; + for (const auto& newEvent : newEvents) { + if (eventsOverlap(event, newEvent)) { + potentialOverlap = true; + break; + } + } + + if (potentialOverlap) { + eventsToRemove.push_back(event); + } else { + remainingEvents.push_back(event); + } + } +} diff --git a/main/OverlapProcessor.h b/main/OverlapProcessor.h new file mode 100644 index 0000000..6760eaa --- /dev/null +++ b/main/OverlapProcessor.h @@ -0,0 +1,107 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Tony + An intonation analysis and annotation tool + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006-2012 Chris Cannam and QMUL. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef OVERLAP_PROCESSOR_H +#define OVERLAP_PROCESSOR_H + +#include +#include + +// Forward declarations - includes will be in the .cpp file +namespace sv { + class Event; + typedef std::vector EventVector; + class SparseTimeValueModel; + class NoteModel; + typedef int64_t sv_frame_t; +} + +using namespace sv; + +/** + * Configuration parameters for overlap processing algorithms + */ +struct OverlapConfig { + sv_frame_t interpolationThreshold = 512; // ~11ms at 44.1kHz + double pitchSimilarityThreshold = 0.1; // 10% pitch difference threshold + sv_frame_t overlapTolerance = 1000; // Tolerance for event matching + + OverlapConfig() = default; + OverlapConfig(sv_frame_t interpThresh, double pitchThresh, sv_frame_t tolerance) + : interpolationThreshold(interpThresh) + , pitchSimilarityThreshold(pitchThresh) + , overlapTolerance(tolerance) {} +}; + +/** + * Structure to represent a group of overlapping events + */ +struct OverlapGroup { + EventVector events; + sv_frame_t startFrame; + sv_frame_t endFrame; + + explicit OverlapGroup(const EventVector& evts); + bool isEmpty() const { return events.empty(); } + size_t size() const { return events.size(); } +}; + +/** + * Main overlap processing class that handles detection and merging of overlapping events + */ +class OverlapProcessor { +public: + explicit OverlapProcessor(const OverlapConfig& config = OverlapConfig()); + + // Core overlap detection and processing + std::vector findOverlapGroups(const EventVector& events) const; + Event mergeOverlapGroup(const OverlapGroup& group) const; + + // Frequency calculation methods + float calculateWeightedFrequency(const EventVector& overlappingEvents, + sv_frame_t overlapStart, + sv_frame_t overlapDuration) const; + + float calculateWeightedFrequency(const Event& prevEvent, + const Event& nextEvent, + sv_frame_t overlapStart, + sv_frame_t overlapDuration) const; + + // Main processing methods for different model types + EventVector processPitchModel(sv_frame_t contextStart, + std::shared_ptr fromModel, + std::shared_ptr toModel) const; + + EventVector processNoteModel(sv_frame_t contextStart, + std::shared_ptr fromModel, + std::shared_ptr toModel) const; + + // Configuration access + const OverlapConfig& getConfig() const { return m_config; } + void setConfig(const OverlapConfig& config) { m_config = config; } + +private: + OverlapConfig m_config; + + // Helper methods + bool eventsOverlap(const Event& a, const Event& b) const; + const Event* findLongestEvent(const EventVector& events) const; + void categorizeEvents(const EventVector& allEvents, + const EventVector& newEvents, + EventVector& eventsToRemove, + EventVector& remainingEvents) const; +}; + +#endif // OVERLAP_PROCESSOR_H diff --git a/meson.build b/meson.build index 41c9392..19eb9d5 100644 --- a/meson.build +++ b/meson.build @@ -1021,6 +1021,7 @@ chp_plugin = shared_library( tony_main_files = [ 'main/main.cpp', 'main/Analyser.cpp', + 'main/OverlapProcessor.cpp', 'main/MainWindow.cpp', 'main/NetworkPermissionTester.cpp', ] From c3c76ef7e543993d7d5965cc1657c93663f5eace Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Sat, 14 Mar 2026 11:58:32 +0300 Subject: [PATCH 19/24] safe pointer --- main/Analyser.cpp | 118 +++++++++++++++++++++++++++++++++++++- main/OverlapProcessor.cpp | 1 - 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index e9b7a3a..cd17f6f 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "transform/TransformFactory.h" #include "transform/ModelTransformer.h" @@ -630,15 +631,42 @@ void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* // Generalized helper function to process layers template void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* document, sv::Pane* pane, std::vector* trackingVector, std::function, std::shared_ptr)> customProcessing) { + // DIAGNOSTIC: Log layer creation + cerr << "processLayer: Creating async handler for layer " << layer << ", targetLayer " << targetLayer << endl; + QObject::connect(layer, &LayerType::modelCompletionChanged, [layer, targetLayer, document, pane, trackingVector, customProcessing](ModelId modelId) { + // DIAGNOSTIC: Log callback entry + cerr << "processLayer callback: layer=" << layer << ", targetLayer=" << targetLayer + << ", document=" << document << ", pane=" << pane << endl; + + // SAFETY CHECK: Validate pointers before use + if (!document || !pane || !targetLayer) { + cerr << "ERROR: processLayer callback invoked with null pointers! " + << "document=" << document << ", pane=" << pane << ", targetLayer=" << targetLayer << endl; + return; + } + auto model = ModelById::getAs(modelId); + if (!model) { + cerr << "ERROR: processLayer callback - model is null for modelId" << endl; + return; + } if (model->getCompletion() == 100) { + cerr << "processLayer: Model completion reached 100%, processing events" << endl; + auto toModel = ModelById::getAs(targetLayer->getModel()); + if (!toModel) { + cerr << "ERROR: processLayer - target model is null" << endl; + return; + } + EventVector points = model->getAllEvents(); + cerr << "processLayer: Retrieved " << points.size() << " events from source model" << endl; // Custom processing logic points = customProcessing(model, toModel); + cerr << "processLayer: After processing, have " << points.size() << " events to add" << endl; for (const Event& p : points) { toModel->add(p); @@ -647,6 +675,7 @@ void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* docume QObject::disconnect(layer, &LayerType::modelCompletionChanged, nullptr, nullptr); // Clean up the temporary layer + cerr << "processLayer: Cleaning up temporary layer " << layer << endl; if (document && pane) { document->removeLayerFromView(pane, layer); document->deleteLayer(layer); @@ -656,6 +685,7 @@ void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* docume if (trackingVector) { trackingVector->erase(std::remove(trackingVector->begin(), trackingVector->end(), layer), trackingVector->end()); } + cerr << "processLayer: Cleanup complete" << endl; } }); } @@ -760,12 +790,96 @@ Analyser::analyseRecording(Selection sel) if (tempPitchLayer) { setBaseColour(tempPitchLayer, tr("Black"), cdb); - processLayer(tempPitchLayer, pitchLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processPitchModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); + // SAFETY: Use QPointer to track object lifetime and prevent dangling pointer access + QPointer safeTempLayer(tempPitchLayer); + QPointer safePitchLayer(pitchLayer); + QPointer safeDocument(m_document); + QPointer safePane(m_pane); + + QObject::connect(tempPitchLayer, &TimeValueLayer::modelCompletionChanged, + [this, safeTempLayer, safePitchLayer, safeDocument, safePane, sel](ModelId modelId) { + // SAFETY CHECK: Verify all pointers are still valid + if (!safeTempLayer || !safePitchLayer || !safeDocument || !safePane) { + cerr << "WARNING: analyseRecording pitch callback - objects deleted, skipping" << endl; + return; + } + + auto model = ModelById::getAs(modelId); + if (!model || model->getCompletion() != 100) { + return; + } + + cerr << "analyseRecording: Processing pitch track completion" << endl; + auto toModel = ModelById::getAs(safePitchLayer->getModel()); + if (!toModel) { + cerr << "ERROR: Target pitch model is null" << endl; + return; + } + + EventVector points = processPitchModel(sel.getStartFrame(), model, toModel); + + for (const Event& p : points) { + toModel->add(p); + } + + QObject::disconnect(safeTempLayer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); + + { + QMutexLocker locker(&m_asyncMutex); + m_realtimeAnalysisLayers.erase( + std::remove(m_realtimeAnalysisLayers.begin(), m_realtimeAnalysisLayers.end(), safeTempLayer.data()), + m_realtimeAnalysisLayers.end()); + } + + safeDocument->removeLayerFromView(safePane, safeTempLayer); + safeDocument->deleteLayer(safeTempLayer); + }); } if (tempNoteLayer) { setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); - processLayer(tempNoteLayer, noteLayer, m_document, m_pane, &m_realtimeAnalysisLayers, std::bind(processNoteModel, sel.getStartFrame(), std::placeholders::_1, std::placeholders::_2)); + // SAFETY: Use QPointer to track object lifetime and prevent dangling pointer access + QPointer safeTempLayer(tempNoteLayer); + QPointer safeNoteLayer(noteLayer); + QPointer safeDocument(m_document); + QPointer safePane(m_pane); + + // Capture the layer pointer before it might be deleted + Layer* layerPtr = safeTempLayer.data(); + + QObject::connect(tempNoteLayer, &FlexiNoteLayer::modelCompletionChanged, + [layerPtr, safeTempLayer, safeNoteLayer, safeDocument, safePane, sel](ModelId modelId) { + // SAFETY CHECK: Verify all pointers are still valid + if (!safeTempLayer || !safeNoteLayer || !safeDocument || !safePane) { + cerr << "WARNING: analyseRecording note callback - objects deleted, skipping" << endl; + return; + } + + auto model = ModelById::getAs(modelId); + if (!model || model->getCompletion() != 100) { + return; + } + + cerr << "analyseRecording: Processing note layer completion" << endl; + auto toModel = ModelById::getAs(safeNoteLayer->getModel()); + if (!toModel) { + cerr << "ERROR: Target note model is null" << endl; + return; + } + + EventVector points = processNoteModel(sel.getStartFrame(), model, toModel); + + for (const Event& p : points) { + toModel->add(p); + } + + QObject::disconnect(safeTempLayer, &FlexiNoteLayer::modelCompletionChanged, nullptr, nullptr); + + // Clean up temporary layer - no need to access m_realtimeAnalysisLayers + // as the layer will be automatically removed when deleted + safeDocument->removeLayerFromView(safePane, safeTempLayer); + safeDocument->deleteLayer(safeTempLayer); + }); } } diff --git a/main/OverlapProcessor.cpp b/main/OverlapProcessor.cpp index 916b73f..a21c87e 100644 --- a/main/OverlapProcessor.cpp +++ b/main/OverlapProcessor.cpp @@ -14,7 +14,6 @@ */ #include "OverlapProcessor.h" -#include "data/model/Event.h" #include "data/model/SparseTimeValueModel.h" #include "data/model/NoteModel.h" #include From f8d24580e7817a166ab68469feb138738d8817e8 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Sat, 14 Mar 2026 12:47:56 +0300 Subject: [PATCH 20/24] safer --- main/Analyser.cpp | 434 +++++++++++++++++++++++++--------------------- main/Analyser.h | 10 +- 2 files changed, 244 insertions(+), 200 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index cd17f6f..6972593 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -16,7 +16,6 @@ #include "Analyser.h" #include "OverlapProcessor.h" -#include #include #include @@ -54,7 +53,9 @@ Analyser::Analyser() : m_pane(0), m_currentCandidate(-1), m_candidatesVisible(false), - m_currentAsyncHandle(0) + m_currentAsyncHandle(0), + m_realtimeAnalysisInFlight(false), + m_havePendingRealtimeSelection(false) { QSettings settings; settings.beginGroup("LayerDefaults"); @@ -74,16 +75,72 @@ Analyser::Analyser() : Analyser::~Analyser() { - // Clean up any remaining realtime analysis layers - if (m_document && m_pane) { - for (auto* layer : m_realtimeAnalysisLayers) { - if (layer) { - m_document->removeLayerFromView(m_pane, layer); - m_document->deleteLayer(layer); - } + cleanupRealtimeAnalysisLayers(); +} + +void +Analyser::cleanupRealtimeAnalysisLayers() +{ + std::vector> layersToClean; + + { + QMutexLocker locker(&m_asyncMutex); + layersToClean.swap(m_realtimeAnalysisLayers); + } + + if (!m_document || !m_pane) { + return; + } + + for (const auto &layerPtr : layersToClean) { + Layer *layer = layerPtr.data(); + if (!layer) { + continue; } + + m_document->removeLayerFromView(m_pane, layer); + m_document->deleteLayer(layer); + } +} + +void +Analyser::untrackRealtimeAnalysisLayer(Layer *layer) +{ + QMutexLocker locker(&m_asyncMutex); + + m_realtimeAnalysisLayers.erase( + std::remove_if(m_realtimeAnalysisLayers.begin(), + m_realtimeAnalysisLayers.end(), + [layer](const QPointer &p) { + return p.isNull() || p.data() == layer; + }), + m_realtimeAnalysisLayers.end()); +} + +void +Analyser::finishRealtimeAnalysisChunk() +{ + Selection pending; + bool havePending = false; + + { + QMutexLocker locker(&m_asyncMutex); + + m_realtimeAnalysisInFlight = false; + m_reAnalysingSelection = Selection(); + + if (m_havePendingRealtimeSelection) { + pending = m_pendingRealtimeSelection; + m_pendingRealtimeSelection = Selection(); + m_havePendingRealtimeSelection = false; + havePending = true; + } + } + + if (havePending) { + cerr << "Analyser::finishRealtimeAnalysisChunk: starting pending realtime selection" << endl; + (void)analyseRecording(pending); } - m_realtimeAnalysisLayers.clear(); } std::map @@ -100,6 +157,10 @@ QString Analyser::newFileLoaded(Document *doc, ModelId model, PaneStack *paneStack, Pane *pane) { + if (m_document && m_document != doc) { + disconnect(m_document, nullptr, this, nullptr); + } + m_document = doc; m_fileModel = model; m_paneStack = paneStack; @@ -109,8 +170,9 @@ Analyser::newFileLoaded(Document *doc, ModelId model, return "Internal error: Analyser::newFileLoaded() called with no model, or a non-WaveFileModel"; } - connect(doc, SIGNAL(layerAboutToBeDeleted(Layer *)), - this, SLOT(layerAboutToBeDeleted(Layer *))); + connect(doc, &Document::layerAboutToBeDeleted, + this, &Analyser::layerAboutToBeDeleted, + Qt::UniqueConnection); QSettings settings; settings.beginGroup("Analyser"); @@ -207,22 +269,30 @@ void Analyser::fileClosed() { cerr << "Analyser::fileClosed" << endl; - - // Clean up any remaining realtime analysis layers - if (m_document && m_pane) { - for (auto* layer : m_realtimeAnalysisLayers) { - if (layer) { - m_document->removeLayerFromView(m_pane, layer); - m_document->deleteLayer(layer); - } - } + + if (m_currentAsyncHandle && m_document) { + m_document->cancelAsyncLayerCreation(m_currentAsyncHandle); } - m_realtimeAnalysisLayers.clear(); - + m_currentAsyncHandle = 0; + + cleanupRealtimeAnalysisLayers(); + m_layers.clear(); m_reAnalysisCandidates.clear(); m_currentCandidate = -1; m_reAnalysingSelection = Selection(); + m_reAnalysingRange = FrequencyRange(); + m_candidatesVisible = false; + m_analysedFrames = 0; + + m_realtimeAnalysisInFlight = false; + m_havePendingRealtimeSelection = false; + m_pendingRealtimeSelection = Selection(); + + m_document = 0; + m_paneStack = 0; + m_pane = 0; + m_fileModel = ModelId(); } bool @@ -628,68 +698,6 @@ void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* layer->setBaseColour(cdb->getColourIndex(colourName)); } -// Generalized helper function to process layers -template -void processLayer(LayerType* layer, LayerType* targetLayer, sv::Document* document, sv::Pane* pane, std::vector* trackingVector, std::function, std::shared_ptr)> customProcessing) { - // DIAGNOSTIC: Log layer creation - cerr << "processLayer: Creating async handler for layer " << layer << ", targetLayer " << targetLayer << endl; - - QObject::connect(layer, &LayerType::modelCompletionChanged, [layer, targetLayer, document, pane, trackingVector, customProcessing](ModelId modelId) { - // DIAGNOSTIC: Log callback entry - cerr << "processLayer callback: layer=" << layer << ", targetLayer=" << targetLayer - << ", document=" << document << ", pane=" << pane << endl; - - // SAFETY CHECK: Validate pointers before use - if (!document || !pane || !targetLayer) { - cerr << "ERROR: processLayer callback invoked with null pointers! " - << "document=" << document << ", pane=" << pane << ", targetLayer=" << targetLayer << endl; - return; - } - - auto model = ModelById::getAs(modelId); - if (!model) { - cerr << "ERROR: processLayer callback - model is null for modelId" << endl; - return; - } - - if (model->getCompletion() == 100) { - cerr << "processLayer: Model completion reached 100%, processing events" << endl; - - auto toModel = ModelById::getAs(targetLayer->getModel()); - if (!toModel) { - cerr << "ERROR: processLayer - target model is null" << endl; - return; - } - - EventVector points = model->getAllEvents(); - cerr << "processLayer: Retrieved " << points.size() << " events from source model" << endl; - - // Custom processing logic - points = customProcessing(model, toModel); - cerr << "processLayer: After processing, have " << points.size() << " events to add" << endl; - - for (const Event& p : points) { - toModel->add(p); - } - - QObject::disconnect(layer, &LayerType::modelCompletionChanged, nullptr, nullptr); - - // Clean up the temporary layer - cerr << "processLayer: Cleaning up temporary layer " << layer << endl; - if (document && pane) { - document->removeLayerFromView(pane, layer); - document->deleteLayer(layer); - } - - // Remove from tracking vector - if (trackingVector) { - trackingVector->erase(std::remove(trackingVector->begin(), trackingVector->end(), layer), trackingVector->end()); - } - cerr << "processLayer: Cleanup complete" << endl; - } - }); -} - // Global overlap processor instance for efficient processing static OverlapProcessor s_overlapProcessor; @@ -705,35 +713,42 @@ static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr(m_fileModel); if (!waveFileModel) { return "Internal error: Analyser::analyseRecording() called with no model present"; } - if (!m_reAnalysingSelection.isEmpty()) { - if (sel == m_reAnalysingSelection) { - cerr << "selection & range are same as current analysis, ignoring" << endl; - return ""; - } + if (!m_document || !m_pane) { + return "Internal error: Analyser::analyseRecording() called with no document or pane present"; } - if (sel.isEmpty()) return ""; + auto *pitchLayer = qobject_cast(m_layers[PitchTrack]); + auto *noteLayer = qobject_cast(m_layers[Notes]); - m_reAnalysingSelection = sel; + if (!pitchLayer || !noteLayer) { + return "Internal error: Analyser::analyseRecording() called with no target pitch/note layers present"; + } - auto* pitchLayer = qobject_cast(m_layers[PitchTrack]); - auto* noteLayer = qobject_cast(m_layers[Notes]); + if (sel.isEmpty()) return ""; - TransformFactory* tf = TransformFactory::getInstance(); + { + QMutexLocker locker(&m_asyncMutex); + if (!m_reAnalysingSelection.isEmpty() && sel == m_reAnalysingSelection) { + cerr << "selection is same as current analysis, ignoring" << endl; + return ""; + } + m_reAnalysingSelection = sel; + } - Transforms transforms; + TransformFactory *tf = TransformFactory::getInstance(); - auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); + auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); - QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis.

Are the %2 and %3 Vamp plugins correctly installed?"); + QString notFound = + tr("Transform \"%1\" not found. Unable to perform interactive analysis." + "

Are the %2 and %3 Vamp plugins correctly installed?"); + if (!tf->haveTransform(f0_transform)) { return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); } @@ -742,23 +757,28 @@ Analyser::analyseRecording(Selection sel) return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); } - Transform t = tf->getDefaultTransformFor - (f0_transform, waveFileModel->getSampleRate()); + Transform t = tf->getDefaultTransformFor(f0_transform, + waveFileModel->getSampleRate()); t.setStepSize(256); t.setBlockSize(2048); setAnalysisSettings(t); - - RealTime start = RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); - RealTime end = RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); - RealTime duration; + RealTime start = + RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); + RealTime end = + RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); + RealTime duration; if (sel.getEndFrame() > sel.getStartFrame()) { duration = end - start; } - cerr << "Analyser::analyseRecording: start " << start << " end " << end << " original selection start " << sel.getStartFrame() << " end " << sel.getEndFrame() << " duration " << duration << endl; + cerr << "Analyser::analyseRecording: start " << start + << " end " << end + << " original selection start " << sel.getStartFrame() + << " end " << sel.getEndFrame() + << " duration " << duration << endl; if (duration <= RealTime::zeroTime) { cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; @@ -768,121 +788,137 @@ Analyser::analyseRecording(Selection sel) t.setStartTime(start); t.setDuration(duration); + Transforms transforms; transforms.push_back(t); t.setOutput(PYIN_NOTE_OUT); - transforms.push_back(t); - std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); + std::vector layers = + m_document->createDerivedLayers(transforms, m_fileModel); - ColourDatabase* cdb = ColourDatabase::getInstance(); + ColourDatabase *cdb = ColourDatabase::getInstance(); - // Track the temporary layers for cleanup - for (auto* layer : layers) { - m_realtimeAnalysisLayers.push_back(layer); + { + QMutexLocker locker(&m_asyncMutex); + for (auto *layer : layers) { + m_realtimeAnalysisLayers.push_back(QPointer(layer)); + } } - for (auto* layer : layers) { + for (auto *layer : layers) { - FlexiNoteLayer* tempNoteLayer = qobject_cast(layer); - TimeValueLayer* tempPitchLayer = qobject_cast(layer); + if (auto *tempPitchLayer = qobject_cast(layer)) { - if (tempPitchLayer) { setBaseColour(tempPitchLayer, tr("Black"), cdb); - // SAFETY: Use QPointer to track object lifetime and prevent dangling pointer access + QPointer safeTempLayer(tempPitchLayer); - QPointer safePitchLayer(pitchLayer); + QPointer safeTargetLayer(pitchLayer); QPointer safeDocument(m_document); QPointer safePane(m_pane); - - QObject::connect(tempPitchLayer, &TimeValueLayer::modelCompletionChanged, - [this, safeTempLayer, safePitchLayer, safeDocument, safePane, sel](ModelId modelId) { - // SAFETY CHECK: Verify all pointers are still valid - if (!safeTempLayer || !safePitchLayer || !safeDocument || !safePane) { - cerr << "WARNING: analyseRecording pitch callback - objects deleted, skipping" << endl; - return; - } - - auto model = ModelById::getAs(modelId); - if (!model || model->getCompletion() != 100) { - return; - } - - cerr << "analyseRecording: Processing pitch track completion" << endl; - auto toModel = ModelById::getAs(safePitchLayer->getModel()); - if (!toModel) { - cerr << "ERROR: Target pitch model is null" << endl; - return; - } - - EventVector points = processPitchModel(sel.getStartFrame(), model, toModel); - - for (const Event& p : points) { - toModel->add(p); - } - - QObject::disconnect(safeTempLayer, &TimeValueLayer::modelCompletionChanged, nullptr, nullptr); - - { - QMutexLocker locker(&m_asyncMutex); - m_realtimeAnalysisLayers.erase( - std::remove(m_realtimeAnalysisLayers.begin(), m_realtimeAnalysisLayers.end(), safeTempLayer.data()), - m_realtimeAnalysisLayers.end()); - } - - safeDocument->removeLayerFromView(safePane, safeTempLayer); - safeDocument->deleteLayer(safeTempLayer); - }); + + QObject::connect( + tempPitchLayer, + &TimeValueLayer::modelCompletionChanged, + this, + [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel](ModelId modelId) { + + if (!safeTempLayer || !safeTargetLayer || !safeDocument || !safePane) { + cerr << "WARNING: analyseRecording pitch callback - objects deleted, skipping" << endl; + return; + } + + auto fromModel = ModelById::getAs(modelId); + if (!fromModel || fromModel->getCompletion() != 100) { + return; + } + + cerr << "analyseRecording: Processing pitch track completion" << endl; + + auto toModel = + ModelById::getAs(safeTargetLayer->getModel()); + if (!toModel) { + cerr << "ERROR: analyseRecording pitch callback - target model is null" << endl; + return; + } + + EventVector points = + processPitchModel(sel.getStartFrame(), fromModel, toModel); + + for (const Event &p : points) { + toModel->add(p); + } + + Layer *layerToDelete = safeTempLayer.data(); + if (layerToDelete) { + untrackRealtimeAnalysisLayer(layerToDelete); + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } + } + + emit layersChanged(); + }, + Qt::QueuedConnection); } - if (tempNoteLayer) { + if (auto *tempNoteLayer = qobject_cast(layer)) { + setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); - // SAFETY: Use QPointer to track object lifetime and prevent dangling pointer access + QPointer safeTempLayer(tempNoteLayer); - QPointer safeNoteLayer(noteLayer); + QPointer safeTargetLayer(noteLayer); QPointer safeDocument(m_document); QPointer safePane(m_pane); - - // Capture the layer pointer before it might be deleted - Layer* layerPtr = safeTempLayer.data(); - - QObject::connect(tempNoteLayer, &FlexiNoteLayer::modelCompletionChanged, - [layerPtr, safeTempLayer, safeNoteLayer, safeDocument, safePane, sel](ModelId modelId) { - // SAFETY CHECK: Verify all pointers are still valid - if (!safeTempLayer || !safeNoteLayer || !safeDocument || !safePane) { - cerr << "WARNING: analyseRecording note callback - objects deleted, skipping" << endl; - return; - } - - auto model = ModelById::getAs(modelId); - if (!model || model->getCompletion() != 100) { - return; - } - - cerr << "analyseRecording: Processing note layer completion" << endl; - auto toModel = ModelById::getAs(safeNoteLayer->getModel()); - if (!toModel) { - cerr << "ERROR: Target note model is null" << endl; - return; - } - - EventVector points = processNoteModel(sel.getStartFrame(), model, toModel); - - for (const Event& p : points) { - toModel->add(p); - } - - QObject::disconnect(safeTempLayer, &FlexiNoteLayer::modelCompletionChanged, nullptr, nullptr); - - // Clean up temporary layer - no need to access m_realtimeAnalysisLayers - // as the layer will be automatically removed when deleted - safeDocument->removeLayerFromView(safePane, safeTempLayer); - safeDocument->deleteLayer(safeTempLayer); - }); + + QObject::connect( + tempNoteLayer, + &FlexiNoteLayer::modelCompletionChanged, + this, + [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel](ModelId modelId) { + + if (!safeTempLayer || !safeTargetLayer || !safeDocument || !safePane) { + cerr << "WARNING: analyseRecording note callback - objects deleted, skipping" << endl; + return; + } + + auto fromModel = ModelById::getAs(modelId); + if (!fromModel || fromModel->getCompletion() != 100) { + return; + } + + cerr << "analyseRecording: Processing note layer completion" << endl; + + auto toModel = + ModelById::getAs(safeTargetLayer->getModel()); + if (!toModel) { + cerr << "ERROR: analyseRecording note callback - target model is null" << endl; + return; + } + + EventVector points = + processNoteModel(sel.getStartFrame(), fromModel, toModel); + + for (const Event &p : points) { + toModel->add(p); + } + + Layer *layerToDelete = safeTempLayer.data(); + if (layerToDelete) { + untrackRealtimeAnalysisLayer(layerToDelete); + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } + } + + emit layersChanged(); + }, + Qt::QueuedConnection); } } - + return ""; } diff --git a/main/Analyser.h b/main/Analyser.h index 1db40b8..77c4f48 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -17,6 +17,7 @@ #define ANALYSER_H #include +#include #include #include @@ -262,7 +263,11 @@ protected slots: sv::sv_frame_t m_analysedFrames = 0; FrequencyRange m_reAnalysingRange; std::vector m_reAnalysisCandidates; - std::vector m_realtimeAnalysisLayers; // Track temporary layers for cleanup + std::vector> m_realtimeAnalysisLayers; // Track temporary layers for cleanup + + bool m_realtimeAnalysisInFlight; + bool m_havePendingRealtimeSelection; + sv::Selection m_pendingRealtimeSelection; int m_currentCandidate; bool m_candidatesVisible; sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; @@ -284,6 +289,9 @@ protected slots: void saveState(Component c) const; void loadState(Component c); + void cleanupRealtimeAnalysisLayers(); + void untrackRealtimeAnalysisLayer(sv::Layer *layer); + void finishRealtimeAnalysisChunk(); // TODO (alnovi): Move constexpression to a static class static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; From 860121fe5e060f0621589617818aa25453b5fae3 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Sun, 15 Mar 2026 13:39:59 +0300 Subject: [PATCH 21/24] safer --- main/Analyser.cpp | 192 ++++++++++++++++++++++++++++++++++++---------- main/Analyser.h | 2 + 2 files changed, 155 insertions(+), 39 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 6972593..8856da3 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -55,7 +55,8 @@ Analyser::Analyser() : m_candidatesVisible(false), m_currentAsyncHandle(0), m_realtimeAnalysisInFlight(false), - m_havePendingRealtimeSelection(false) + m_havePendingRealtimeSelection(false), + m_realtimeGeneration(0) { QSettings settings; settings.beginGroup("LayerDefaults"); @@ -127,7 +128,6 @@ Analyser::finishRealtimeAnalysisChunk() QMutexLocker locker(&m_asyncMutex); m_realtimeAnalysisInFlight = false; - m_reAnalysingSelection = Selection(); if (m_havePendingRealtimeSelection) { pending = m_pendingRealtimeSelection; @@ -166,6 +166,11 @@ Analyser::newFileLoaded(Document *doc, ModelId model, m_paneStack = paneStack; m_pane = pane; + { + QMutexLocker locker(&m_asyncMutex); + ++m_realtimeGeneration; + } + if (!ModelById::isa(m_fileModel)) { return "Internal error: Analyser::newFileLoaded() called with no model, or a non-WaveFileModel"; } @@ -288,6 +293,10 @@ Analyser::fileClosed() m_realtimeAnalysisInFlight = false; m_havePendingRealtimeSelection = false; m_pendingRealtimeSelection = Selection(); + { + QMutexLocker locker(&m_asyncMutex); + ++m_realtimeGeneration; + } m_document = 0; m_paneStack = 0; @@ -713,6 +722,8 @@ static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr(m_fileModel); if (!waveFileModel) { return "Internal error: Analyser::analyseRecording() called with no model present"; @@ -731,13 +742,20 @@ Analyser::analyseRecording(Selection sel) if (sel.isEmpty()) return ""; + quint64 generation = 0; { QMutexLocker locker(&m_asyncMutex); - if (!m_reAnalysingSelection.isEmpty() && sel == m_reAnalysingSelection) { - cerr << "selection is same as current analysis, ignoring" << endl; + + if (m_realtimeAnalysisInFlight) { + m_pendingRealtimeSelection = sel; + m_havePendingRealtimeSelection = true; + cerr << "Analyser::analyseRecording: realtime analysis already in flight, replacing pending selection" << endl; return ""; } - m_reAnalysingSelection = sel; + + m_realtimeAnalysisInFlight = true; + startedRealtimeChunk = true; + generation = m_realtimeGeneration; } TransformFactory *tf = TransformFactory::getInstance(); @@ -750,10 +768,16 @@ Analyser::analyseRecording(Selection sel) "

Are the %2 and %3 Vamp plugins correctly installed?"); if (!tf->haveTransform(f0_transform)) { + if (startedRealtimeChunk) { + finishRealtimeAnalysisChunk(); + } return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); } if (!tf->haveTransform(note_transform)) { + if (startedRealtimeChunk) { + finishRealtimeAnalysisChunk(); + } return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); } @@ -782,6 +806,9 @@ Analyser::analyseRecording(Selection sel) if (duration <= RealTime::zeroTime) { cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; + if (startedRealtimeChunk) { + finishRealtimeAnalysisChunk(); + } return ""; } @@ -797,6 +824,14 @@ Analyser::analyseRecording(Selection sel) std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); + if (layers.empty()) { + cerr << "WARNING: analyseRecording: no layers returned from createDerivedLayers" << endl; + if (startedRealtimeChunk) { + finishRealtimeAnalysisChunk(); + } + return ""; + } + ColourDatabase *cdb = ColourDatabase::getInstance(); { @@ -806,10 +841,13 @@ Analyser::analyseRecording(Selection sel) } } + auto remainingParts = std::make_shared(0); + for (auto *layer : layers) { if (auto *tempPitchLayer = qobject_cast(layer)) { + ++(*remainingParts); setBaseColour(tempPitchLayer, tr("Black"), cdb); QPointer safeTempLayer(tempPitchLayer); @@ -821,12 +859,7 @@ Analyser::analyseRecording(Selection sel) tempPitchLayer, &TimeValueLayer::modelCompletionChanged, this, - [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel](ModelId modelId) { - - if (!safeTempLayer || !safeTargetLayer || !safeDocument || !safePane) { - cerr << "WARNING: analyseRecording pitch callback - objects deleted, skipping" << endl; - return; - } + [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel, remainingParts, generation](ModelId modelId) { auto fromModel = ModelById::getAs(modelId); if (!fromModel || fromModel->getCompletion() != 100) { @@ -835,36 +868,77 @@ Analyser::analyseRecording(Selection sel) cerr << "analyseRecording: Processing pitch track completion" << endl; - auto toModel = - ModelById::getAs(safeTargetLayer->getModel()); - if (!toModel) { - cerr << "ERROR: analyseRecording pitch callback - target model is null" << endl; + bool stale = false; + { + QMutexLocker locker(&m_asyncMutex); + stale = (generation != m_realtimeGeneration); + } + + if (stale) { + cerr << "analyseRecording: Ignoring stale pitch callback from old generation" << endl; + + Layer *layerToDelete = safeTempLayer.data(); + if (layerToDelete) { + untrackRealtimeAnalysisLayer(layerToDelete); + + if (safeDocument && safePane) { + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } + } + } + + --(*remainingParts); + if (*remainingParts == 0) { + finishRealtimeAnalysisChunk(); + } return; } - EventVector points = - processPitchModel(sel.getStartFrame(), fromModel, toModel); + if (safeTargetLayer) { + auto toModel = + ModelById::getAs(safeTargetLayer->getModel()); + + if (toModel) { + EventVector points = + processPitchModel(sel.getStartFrame(), fromModel, toModel); - for (const Event &p : points) { - toModel->add(p); + for (const Event &p : points) { + toModel->add(p); + } + } else { + cerr << "ERROR: analyseRecording pitch callback - target model is null" << endl; + } + } else { + cerr << "WARNING: analyseRecording pitch callback - target layer deleted" << endl; } Layer *layerToDelete = safeTempLayer.data(); if (layerToDelete) { untrackRealtimeAnalysisLayer(layerToDelete); - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); + + if (safeDocument && safePane) { + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } } } emit layersChanged(); + + --(*remainingParts); + if (*remainingParts == 0) { + finishRealtimeAnalysisChunk(); + } }, Qt::QueuedConnection); } if (auto *tempNoteLayer = qobject_cast(layer)) { + ++(*remainingParts); setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); QPointer safeTempLayer(tempNoteLayer); @@ -876,12 +950,7 @@ Analyser::analyseRecording(Selection sel) tempNoteLayer, &FlexiNoteLayer::modelCompletionChanged, this, - [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel](ModelId modelId) { - - if (!safeTempLayer || !safeTargetLayer || !safeDocument || !safePane) { - cerr << "WARNING: analyseRecording note callback - objects deleted, skipping" << endl; - return; - } + [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel, remainingParts, generation](ModelId modelId) { auto fromModel = ModelById::getAs(modelId); if (!fromModel || fromModel->getCompletion() != 100) { @@ -890,35 +959,80 @@ Analyser::analyseRecording(Selection sel) cerr << "analyseRecording: Processing note layer completion" << endl; - auto toModel = - ModelById::getAs(safeTargetLayer->getModel()); - if (!toModel) { - cerr << "ERROR: analyseRecording note callback - target model is null" << endl; + bool stale = false; + { + QMutexLocker locker(&m_asyncMutex); + stale = (generation != m_realtimeGeneration); + } + + if (stale) { + cerr << "analyseRecording: Ignoring stale note callback from old generation" << endl; + + Layer *layerToDelete = safeTempLayer.data(); + if (layerToDelete) { + untrackRealtimeAnalysisLayer(layerToDelete); + + if (safeDocument && safePane) { + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } + } + } + + --(*remainingParts); + if (*remainingParts == 0) { + finishRealtimeAnalysisChunk(); + } return; } - EventVector points = - processNoteModel(sel.getStartFrame(), fromModel, toModel); + if (safeTargetLayer) { + auto toModel = + ModelById::getAs(safeTargetLayer->getModel()); + + if (toModel) { + EventVector points = + processNoteModel(sel.getStartFrame(), fromModel, toModel); - for (const Event &p : points) { - toModel->add(p); + for (const Event &p : points) { + toModel->add(p); + } + } else { + cerr << "ERROR: analyseRecording note callback - target model is null" << endl; + } + } else { + cerr << "WARNING: analyseRecording note callback - target layer deleted" << endl; } Layer *layerToDelete = safeTempLayer.data(); if (layerToDelete) { untrackRealtimeAnalysisLayer(layerToDelete); - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); + + if (safeDocument && safePane) { + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } } } emit layersChanged(); + + --(*remainingParts); + if (*remainingParts == 0) { + finishRealtimeAnalysisChunk(); + } }, Qt::QueuedConnection); } } + if (*remainingParts == 0) { + cerr << "WARNING: analyseRecording: no recognised temp layers created" << endl; + finishRealtimeAnalysisChunk(); + } + return ""; } diff --git a/main/Analyser.h b/main/Analyser.h index 77c4f48..229f72b 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -268,6 +269,7 @@ protected slots: bool m_realtimeAnalysisInFlight; bool m_havePendingRealtimeSelection; sv::Selection m_pendingRealtimeSelection; + quint64 m_realtimeGeneration; int m_currentCandidate; bool m_candidatesVisible; sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; From e10a0deadb961a56c106ecde543075f565bab911 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Mon, 16 Mar 2026 08:43:19 +0300 Subject: [PATCH 22/24] c++17 --- main/Analyser.cpp | 222 ++++++++++++++++++---------------------------- main/Analyser.h | 7 +- 2 files changed, 92 insertions(+), 137 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 8856da3..864c805 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -17,6 +17,7 @@ #include "OverlapProcessor.h" #include +#include #include #include "transform/TransformFactory.h" @@ -48,14 +49,13 @@ using std::endl; using namespace sv; Analyser::Analyser() : - m_document(0), - m_paneStack(0), - m_pane(0), + m_document(nullptr), + m_paneStack(nullptr), + m_pane(nullptr), m_currentCandidate(-1), m_candidatesVisible(false), m_currentAsyncHandle(0), m_realtimeAnalysisInFlight(false), - m_havePendingRealtimeSelection(false), m_realtimeGeneration(0) { QSettings settings; @@ -121,28 +121,28 @@ Analyser::untrackRealtimeAnalysisLayer(Layer *layer) void Analyser::finishRealtimeAnalysisChunk() { - Selection pending; - bool havePending = false; + std::optional pending; { QMutexLocker locker(&m_asyncMutex); m_realtimeAnalysisInFlight = false; - - if (m_havePendingRealtimeSelection) { - pending = m_pendingRealtimeSelection; - m_pendingRealtimeSelection = Selection(); - m_havePendingRealtimeSelection = false; - havePending = true; - } + pending = std::exchange(m_pendingRealtimeSelection, std::nullopt); } - if (havePending) { + if (pending) { cerr << "Analyser::finishRealtimeAnalysisChunk: starting pending realtime selection" << endl; - (void)analyseRecording(pending); + (void)analyseRecording(*pending); } } +bool +Analyser::isStaleRealtimeGeneration(quint64 generation) const +{ + QMutexLocker locker(&m_asyncMutex); + return generation != m_realtimeGeneration; +} + std::map Analyser::getAnalysisSettings() { @@ -198,11 +198,11 @@ Analyser::analyseExistingFile() if (m_layers[PitchTrack]) { m_document->removeLayerFromView(m_pane, m_layers[PitchTrack]); - m_layers[PitchTrack] = 0; + m_layers[PitchTrack] = nullptr; } if (m_layers[Notes]) { m_document->removeLayerFromView(m_pane, m_layers[Notes]); - m_layers[Notes] = 0; + m_layers[Notes] = nullptr; } return doAllAnalyses(true); @@ -291,16 +291,15 @@ Analyser::fileClosed() m_analysedFrames = 0; m_realtimeAnalysisInFlight = false; - m_havePendingRealtimeSelection = false; - m_pendingRealtimeSelection = Selection(); + m_pendingRealtimeSelection = std::nullopt; { QMutexLocker locker(&m_asyncMutex); ++m_realtimeGeneration; } - m_document = 0; - m_paneStack = 0; - m_pane = 0; + m_document = nullptr; + m_paneStack = nullptr; + m_pane = nullptr; m_fileModel = ModelId(); } @@ -571,11 +570,11 @@ Analyser::addAnalyses() } else { if (existingPitch) { m_document->removeLayerFromView(m_pane, existingPitch); - m_layers[PitchTrack] = 0; + m_layers[PitchTrack] = nullptr; } if (existingNotes) { m_document->removeLayerFromView(m_pane, existingNotes); - m_layers[Notes] = 0; + m_layers[Notes] = nullptr; } } @@ -724,7 +723,7 @@ Analyser::analyseRecording(Selection sel) { bool startedRealtimeChunk = false; - auto waveFileModel = ModelById::getAs(m_fileModel); + const auto waveFileModel = ModelById::getAs(m_fileModel); if (!waveFileModel) { return "Internal error: Analyser::analyseRecording() called with no model present"; } @@ -748,7 +747,6 @@ Analyser::analyseRecording(Selection sel) if (m_realtimeAnalysisInFlight) { m_pendingRealtimeSelection = sel; - m_havePendingRealtimeSelection = true; cerr << "Analyser::analyseRecording: realtime analysis already in flight, replacing pending selection" << endl; return ""; } @@ -758,26 +756,54 @@ Analyser::analyseRecording(Selection sel) generation = m_realtimeGeneration; } + auto finishIfStarted = [this, &startedRealtimeChunk]() { + if (startedRealtimeChunk) { + finishRealtimeAnalysisChunk(); + } + }; + + auto cleanupTempLayer = [this](auto safeTempLayer, auto safeDocument, auto safePane) { + Layer *layerToDelete = safeTempLayer.data(); + if (!layerToDelete) return; + + untrackRealtimeAnalysisLayer(layerToDelete); + + if (safeDocument && safePane) { + safeDocument->removeLayerFromView(safePane.data(), layerToDelete); + if (safeTempLayer) { + safeDocument->deleteLayer(layerToDelete); + } + } + }; + + struct RealtimeChunkState { + int remainingParts = 0; + }; + auto state = std::make_shared(); + + auto completePart = [this, state]() { + --state->remainingParts; + if (state->remainingParts == 0) { + finishRealtimeAnalysisChunk(); + } + }; + TransformFactory *tf = TransformFactory::getInstance(); - auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); - auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); + const auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); + const auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); QString notFound = tr("Transform \"%1\" not found. Unable to perform interactive analysis." "

Are the %2 and %3 Vamp plugins correctly installed?"); if (!tf->haveTransform(f0_transform)) { - if (startedRealtimeChunk) { - finishRealtimeAnalysisChunk(); - } + finishIfStarted(); return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); } if (!tf->haveTransform(note_transform)) { - if (startedRealtimeChunk) { - finishRealtimeAnalysisChunk(); - } + finishIfStarted(); return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); } @@ -788,9 +814,9 @@ Analyser::analyseRecording(Selection sel) setAnalysisSettings(t); - RealTime start = + const RealTime start = RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); - RealTime end = + const RealTime end = RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); RealTime duration; @@ -806,9 +832,7 @@ Analyser::analyseRecording(Selection sel) if (duration <= RealTime::zeroTime) { cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; - if (startedRealtimeChunk) { - finishRealtimeAnalysisChunk(); - } + finishIfStarted(); return ""; } @@ -821,14 +845,12 @@ Analyser::analyseRecording(Selection sel) t.setOutput(PYIN_NOTE_OUT); transforms.push_back(t); - std::vector layers = + const std::vector layers = m_document->createDerivedLayers(transforms, m_fileModel); if (layers.empty()) { cerr << "WARNING: analyseRecording: no layers returned from createDerivedLayers" << endl; - if (startedRealtimeChunk) { - finishRealtimeAnalysisChunk(); - } + finishIfStarted(); return ""; } @@ -841,13 +863,11 @@ Analyser::analyseRecording(Selection sel) } } - auto remainingParts = std::make_shared(0); - for (auto *layer : layers) { if (auto *tempPitchLayer = qobject_cast(layer)) { - ++(*remainingParts); + ++state->remainingParts; setBaseColour(tempPitchLayer, tr("Black"), cdb); QPointer safeTempLayer(tempPitchLayer); @@ -859,49 +879,29 @@ Analyser::analyseRecording(Selection sel) tempPitchLayer, &TimeValueLayer::modelCompletionChanged, this, - [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel, remainingParts, generation](ModelId modelId) { + [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, + sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { - auto fromModel = ModelById::getAs(modelId); + const auto fromModel = ModelById::getAs(modelId); if (!fromModel || fromModel->getCompletion() != 100) { return; } cerr << "analyseRecording: Processing pitch track completion" << endl; - bool stale = false; - { - QMutexLocker locker(&m_asyncMutex); - stale = (generation != m_realtimeGeneration); - } - - if (stale) { + if (isStaleRealtimeGeneration(generation)) { cerr << "analyseRecording: Ignoring stale pitch callback from old generation" << endl; - - Layer *layerToDelete = safeTempLayer.data(); - if (layerToDelete) { - untrackRealtimeAnalysisLayer(layerToDelete); - - if (safeDocument && safePane) { - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); - } - } - } - - --(*remainingParts); - if (*remainingParts == 0) { - finishRealtimeAnalysisChunk(); - } + cleanupTempLayer(safeTempLayer, safeDocument, safePane); + completePart(); return; } if (safeTargetLayer) { - auto toModel = + const auto toModel = ModelById::getAs(safeTargetLayer->getModel()); if (toModel) { - EventVector points = + const EventVector points = processPitchModel(sel.getStartFrame(), fromModel, toModel); for (const Event &p : points) { @@ -914,31 +914,18 @@ Analyser::analyseRecording(Selection sel) cerr << "WARNING: analyseRecording pitch callback - target layer deleted" << endl; } - Layer *layerToDelete = safeTempLayer.data(); - if (layerToDelete) { - untrackRealtimeAnalysisLayer(layerToDelete); - - if (safeDocument && safePane) { - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); - } - } - } + cleanupTempLayer(safeTempLayer, safeDocument, safePane); emit layersChanged(); - --(*remainingParts); - if (*remainingParts == 0) { - finishRealtimeAnalysisChunk(); - } + completePart(); }, Qt::QueuedConnection); } if (auto *tempNoteLayer = qobject_cast(layer)) { - ++(*remainingParts); + ++state->remainingParts; setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); QPointer safeTempLayer(tempNoteLayer); @@ -950,49 +937,29 @@ Analyser::analyseRecording(Selection sel) tempNoteLayer, &FlexiNoteLayer::modelCompletionChanged, this, - [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, sel, remainingParts, generation](ModelId modelId) { + [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, + sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { - auto fromModel = ModelById::getAs(modelId); + const auto fromModel = ModelById::getAs(modelId); if (!fromModel || fromModel->getCompletion() != 100) { return; } cerr << "analyseRecording: Processing note layer completion" << endl; - bool stale = false; - { - QMutexLocker locker(&m_asyncMutex); - stale = (generation != m_realtimeGeneration); - } - - if (stale) { + if (isStaleRealtimeGeneration(generation)) { cerr << "analyseRecording: Ignoring stale note callback from old generation" << endl; - - Layer *layerToDelete = safeTempLayer.data(); - if (layerToDelete) { - untrackRealtimeAnalysisLayer(layerToDelete); - - if (safeDocument && safePane) { - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); - } - } - } - - --(*remainingParts); - if (*remainingParts == 0) { - finishRealtimeAnalysisChunk(); - } + cleanupTempLayer(safeTempLayer, safeDocument, safePane); + completePart(); return; } if (safeTargetLayer) { - auto toModel = + const auto toModel = ModelById::getAs(safeTargetLayer->getModel()); if (toModel) { - EventVector points = + const EventVector points = processNoteModel(sel.getStartFrame(), fromModel, toModel); for (const Event &p : points) { @@ -1005,30 +972,17 @@ Analyser::analyseRecording(Selection sel) cerr << "WARNING: analyseRecording note callback - target layer deleted" << endl; } - Layer *layerToDelete = safeTempLayer.data(); - if (layerToDelete) { - untrackRealtimeAnalysisLayer(layerToDelete); - - if (safeDocument && safePane) { - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); - } - } - } + cleanupTempLayer(safeTempLayer, safeDocument, safePane); emit layersChanged(); - --(*remainingParts); - if (*remainingParts == 0) { - finishRealtimeAnalysisChunk(); - } + completePart(); }, Qt::QueuedConnection); } } - if (*remainingParts == 0) { + if (state->remainingParts == 0) { cerr << "WARNING: analyseRecording: no recognised temp layers created" << endl; finishRealtimeAnalysisChunk(); } diff --git a/main/Analyser.h b/main/Analyser.h index 229f72b..c063584 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -24,6 +24,7 @@ #include #include +#include #include "framework/Document.h" #include "base/Selection.h" @@ -267,13 +268,12 @@ protected slots: std::vector> m_realtimeAnalysisLayers; // Track temporary layers for cleanup bool m_realtimeAnalysisInFlight; - bool m_havePendingRealtimeSelection; - sv::Selection m_pendingRealtimeSelection; + std::optional m_pendingRealtimeSelection; quint64 m_realtimeGeneration; int m_currentCandidate; bool m_candidatesVisible; sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; - QMutex m_asyncMutex; + mutable QMutex m_asyncMutex; QString doAllAnalyses(bool withPitchTrack); @@ -294,6 +294,7 @@ protected slots: void cleanupRealtimeAnalysisLayers(); void untrackRealtimeAnalysisLayer(sv::Layer *layer); void finishRealtimeAnalysisChunk(); + bool isStaleRealtimeGeneration(quint64 generation) const; // TODO (alnovi): Move constexpression to a static class static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; From d926db1c88380966eb1c98c382a59de6290f99dd Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Mon, 16 Mar 2026 16:06:16 +0300 Subject: [PATCH 23/24] realtime analyser --- main/Analyser.cpp | 398 +++------------------------ main/Analyser.h | 15 +- main/RealtimeAnalyser.cpp | 555 ++++++++++++++++++++++++++++++++++++++ main/RealtimeAnalyser.h | 96 +++++++ meson.build | 5 +- 5 files changed, 697 insertions(+), 372 deletions(-) create mode 100644 main/RealtimeAnalyser.cpp create mode 100644 main/RealtimeAnalyser.h diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 864c805..9b62104 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -14,11 +14,9 @@ */ #include "Analyser.h" -#include "OverlapProcessor.h" +#include "RealtimeAnalyser.h" #include -#include -#include #include "transform/TransformFactory.h" #include "transform/ModelTransformer.h" @@ -54,9 +52,7 @@ Analyser::Analyser() : m_pane(nullptr), m_currentCandidate(-1), m_candidatesVisible(false), - m_currentAsyncHandle(0), - m_realtimeAnalysisInFlight(false), - m_realtimeGeneration(0) + m_currentAsyncHandle(0) { QSettings settings; settings.beginGroup("LayerDefaults"); @@ -72,77 +68,20 @@ Analyser::Analyser() : QString("") .arg(int(FlexiNoteLayer::AutoAlignScale))); settings.endGroup(); -} - -Analyser::~Analyser() -{ - cleanupRealtimeAnalysisLayers(); -} - -void -Analyser::cleanupRealtimeAnalysisLayers() -{ - std::vector> layersToClean; - - { - QMutexLocker locker(&m_asyncMutex); - layersToClean.swap(m_realtimeAnalysisLayers); - } - - if (!m_document || !m_pane) { - return; - } - for (const auto &layerPtr : layersToClean) { - Layer *layer = layerPtr.data(); - if (!layer) { - continue; - } - - m_document->removeLayerFromView(m_pane, layer); - m_document->deleteLayer(layer); - } + m_realtimeAnalyser = new RealtimeAnalyser(this); + connect(m_realtimeAnalyser, &RealtimeAnalyser::layersChanged, + this, &Analyser::layersChanged, + Qt::QueuedConnection); } -void -Analyser::untrackRealtimeAnalysisLayer(Layer *layer) -{ - QMutexLocker locker(&m_asyncMutex); - - m_realtimeAnalysisLayers.erase( - std::remove_if(m_realtimeAnalysisLayers.begin(), - m_realtimeAnalysisLayers.end(), - [layer](const QPointer &p) { - return p.isNull() || p.data() == layer; - }), - m_realtimeAnalysisLayers.end()); -} - -void -Analyser::finishRealtimeAnalysisChunk() +Analyser::~Analyser() { - std::optional pending; - - { - QMutexLocker locker(&m_asyncMutex); - - m_realtimeAnalysisInFlight = false; - pending = std::exchange(m_pendingRealtimeSelection, std::nullopt); - } - - if (pending) { - cerr << "Analyser::finishRealtimeAnalysisChunk: starting pending realtime selection" << endl; - (void)analyseRecording(*pending); + if (m_realtimeAnalyser) { + m_realtimeAnalyser->cleanup(); } } -bool -Analyser::isStaleRealtimeGeneration(quint64 generation) const -{ - QMutexLocker locker(&m_asyncMutex); - return generation != m_realtimeGeneration; -} - std::map Analyser::getAnalysisSettings() { @@ -161,14 +100,20 @@ Analyser::newFileLoaded(Document *doc, ModelId model, disconnect(m_document, nullptr, this, nullptr); } + // Clean up any in-flight realtime analysis layers/callbacks tied to the previous document + if (m_realtimeAnalyser) { + m_realtimeAnalyser->cleanup(); + } + m_document = doc; m_fileModel = model; m_paneStack = paneStack; m_pane = pane; - { - QMutexLocker locker(&m_asyncMutex); - ++m_realtimeGeneration; + if (m_realtimeAnalyser) { + // Targets are (re)created later in addAnalyses(); set minimal context now + m_realtimeAnalyser->setContext(m_document, m_fileModel, m_pane, nullptr, nullptr); + m_realtimeAnalyser->invalidateGeneration(); } if (!ModelById::isa(m_fileModel)) { @@ -280,7 +225,11 @@ Analyser::fileClosed() } m_currentAsyncHandle = 0; - cleanupRealtimeAnalysisLayers(); + if (m_realtimeAnalyser) { + m_realtimeAnalyser->cleanup(); + m_realtimeAnalyser->clearContext(); + m_realtimeAnalyser->invalidateGeneration(); + } m_layers.clear(); m_reAnalysisCandidates.clear(); @@ -290,13 +239,6 @@ Analyser::fileClosed() m_candidatesVisible = false; m_analysedFrames = 0; - m_realtimeAnalysisInFlight = false; - m_pendingRealtimeSelection = std::nullopt; - { - QMutexLocker locker(&m_asyncMutex); - ++m_realtimeGeneration; - } - m_document = nullptr; m_paneStack = nullptr; m_pane = nullptr; @@ -545,6 +487,11 @@ static void setAnalysisSettings(Transform& transform) QString Analyser::addAnalyses() { + if (m_realtimeAnalyser) { + // Prevent stale callbacks touching soon-to-be-replaced pitch/note layers + m_realtimeAnalyser->invalidateGeneration(); + m_realtimeAnalyser->cleanup(); + } auto waveFileModel = ModelById::getAs(m_fileModel); if (!waveFileModel) { return "Internal error: Analyser::addAnalyses() called with no model present"; @@ -667,6 +614,12 @@ Analyser::addAnalyses() this, SLOT(materialiseReAnalysis())); } + if (m_realtimeAnalyser) { + auto *pitchLayer = qobject_cast(m_layers[PitchTrack]); + auto *noteLayer = qobject_cast(m_layers[Notes]); + m_realtimeAnalyser->setContext(m_document, m_fileModel, m_pane, pitchLayer, noteLayer); + } + return ""; } @@ -701,293 +654,18 @@ Analyser::updateNoteLayer(sv::ModelId) emit layersChanged(); } -template -void setBaseColour(LayerType* layer, const QString& colourName, ColourDatabase* cdb) { - layer->setBaseColour(cdb->getColourIndex(colourName)); -} - -// Global overlap processor instance for efficient processing -static OverlapProcessor s_overlapProcessor; - -// Wrapper functions for backward compatibility -static EventVector processPitchModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { - return s_overlapProcessor.processPitchModel(contextStart, fromModel, toModel); -} - -static EventVector processNoteModel(sv_frame_t contextStart, std::shared_ptr fromModel, std::shared_ptr toModel) { - return s_overlapProcessor.processNoteModel(contextStart, fromModel, toModel); -} - QString Analyser::analyseRecording(Selection sel) { - bool startedRealtimeChunk = false; - - const auto waveFileModel = ModelById::getAs(m_fileModel); - if (!waveFileModel) { - return "Internal error: Analyser::analyseRecording() called with no model present"; - } - - if (!m_document || !m_pane) { - return "Internal error: Analyser::analyseRecording() called with no document or pane present"; + if (!m_realtimeAnalyser) { + return "Internal error: Analyser::analyseRecording() called with no realtime analyser present"; } auto *pitchLayer = qobject_cast(m_layers[PitchTrack]); auto *noteLayer = qobject_cast(m_layers[Notes]); - if (!pitchLayer || !noteLayer) { - return "Internal error: Analyser::analyseRecording() called with no target pitch/note layers present"; - } - - if (sel.isEmpty()) return ""; - - quint64 generation = 0; - { - QMutexLocker locker(&m_asyncMutex); - - if (m_realtimeAnalysisInFlight) { - m_pendingRealtimeSelection = sel; - cerr << "Analyser::analyseRecording: realtime analysis already in flight, replacing pending selection" << endl; - return ""; - } - - m_realtimeAnalysisInFlight = true; - startedRealtimeChunk = true; - generation = m_realtimeGeneration; - } - - auto finishIfStarted = [this, &startedRealtimeChunk]() { - if (startedRealtimeChunk) { - finishRealtimeAnalysisChunk(); - } - }; - - auto cleanupTempLayer = [this](auto safeTempLayer, auto safeDocument, auto safePane) { - Layer *layerToDelete = safeTempLayer.data(); - if (!layerToDelete) return; - - untrackRealtimeAnalysisLayer(layerToDelete); - - if (safeDocument && safePane) { - safeDocument->removeLayerFromView(safePane.data(), layerToDelete); - if (safeTempLayer) { - safeDocument->deleteLayer(layerToDelete); - } - } - }; - - struct RealtimeChunkState { - int remainingParts = 0; - }; - auto state = std::make_shared(); - - auto completePart = [this, state]() { - --state->remainingParts; - if (state->remainingParts == 0) { - finishRealtimeAnalysisChunk(); - } - }; - - TransformFactory *tf = TransformFactory::getInstance(); - - const auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); - const auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); - - QString notFound = - tr("Transform \"%1\" not found. Unable to perform interactive analysis." - "

Are the %2 and %3 Vamp plugins correctly installed?"); - - if (!tf->haveTransform(f0_transform)) { - finishIfStarted(); - return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); - } - - if (!tf->haveTransform(note_transform)) { - finishIfStarted(); - return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); - } - - Transform t = tf->getDefaultTransformFor(f0_transform, - waveFileModel->getSampleRate()); - t.setStepSize(256); - t.setBlockSize(2048); - - setAnalysisSettings(t); - - const RealTime start = - RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); - const RealTime end = - RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); - - RealTime duration; - if (sel.getEndFrame() > sel.getStartFrame()) { - duration = end - start; - } - - cerr << "Analyser::analyseRecording: start " << start - << " end " << end - << " original selection start " << sel.getStartFrame() - << " end " << sel.getEndFrame() - << " duration " << duration << endl; - - if (duration <= RealTime::zeroTime) { - cerr << "Analyser::analyseRecording: duration <= 0, not analysing" << endl; - finishIfStarted(); - return ""; - } - - t.setStartTime(start); - t.setDuration(duration); - - Transforms transforms; - transforms.push_back(t); - - t.setOutput(PYIN_NOTE_OUT); - transforms.push_back(t); - - const std::vector layers = - m_document->createDerivedLayers(transforms, m_fileModel); - - if (layers.empty()) { - cerr << "WARNING: analyseRecording: no layers returned from createDerivedLayers" << endl; - finishIfStarted(); - return ""; - } - - ColourDatabase *cdb = ColourDatabase::getInstance(); - - { - QMutexLocker locker(&m_asyncMutex); - for (auto *layer : layers) { - m_realtimeAnalysisLayers.push_back(QPointer(layer)); - } - } - - for (auto *layer : layers) { - - if (auto *tempPitchLayer = qobject_cast(layer)) { - - ++state->remainingParts; - setBaseColour(tempPitchLayer, tr("Black"), cdb); - - QPointer safeTempLayer(tempPitchLayer); - QPointer safeTargetLayer(pitchLayer); - QPointer safeDocument(m_document); - QPointer safePane(m_pane); - - QObject::connect( - tempPitchLayer, - &TimeValueLayer::modelCompletionChanged, - this, - [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, - sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { - - const auto fromModel = ModelById::getAs(modelId); - if (!fromModel || fromModel->getCompletion() != 100) { - return; - } - - cerr << "analyseRecording: Processing pitch track completion" << endl; - - if (isStaleRealtimeGeneration(generation)) { - cerr << "analyseRecording: Ignoring stale pitch callback from old generation" << endl; - cleanupTempLayer(safeTempLayer, safeDocument, safePane); - completePart(); - return; - } - - if (safeTargetLayer) { - const auto toModel = - ModelById::getAs(safeTargetLayer->getModel()); - - if (toModel) { - const EventVector points = - processPitchModel(sel.getStartFrame(), fromModel, toModel); - - for (const Event &p : points) { - toModel->add(p); - } - } else { - cerr << "ERROR: analyseRecording pitch callback - target model is null" << endl; - } - } else { - cerr << "WARNING: analyseRecording pitch callback - target layer deleted" << endl; - } - - cleanupTempLayer(safeTempLayer, safeDocument, safePane); - - emit layersChanged(); - - completePart(); - }, - Qt::QueuedConnection); - } - - if (auto *tempNoteLayer = qobject_cast(layer)) { - - ++state->remainingParts; - setBaseColour(tempNoteLayer, tr("Bright Blue"), cdb); - - QPointer safeTempLayer(tempNoteLayer); - QPointer safeTargetLayer(noteLayer); - QPointer safeDocument(m_document); - QPointer safePane(m_pane); - - QObject::connect( - tempNoteLayer, - &FlexiNoteLayer::modelCompletionChanged, - this, - [this, safeTempLayer, safeTargetLayer, safeDocument, safePane, - sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { - - const auto fromModel = ModelById::getAs(modelId); - if (!fromModel || fromModel->getCompletion() != 100) { - return; - } - - cerr << "analyseRecording: Processing note layer completion" << endl; - - if (isStaleRealtimeGeneration(generation)) { - cerr << "analyseRecording: Ignoring stale note callback from old generation" << endl; - cleanupTempLayer(safeTempLayer, safeDocument, safePane); - completePart(); - return; - } - - if (safeTargetLayer) { - const auto toModel = - ModelById::getAs(safeTargetLayer->getModel()); - - if (toModel) { - const EventVector points = - processNoteModel(sel.getStartFrame(), fromModel, toModel); - - for (const Event &p : points) { - toModel->add(p); - } - } else { - cerr << "ERROR: analyseRecording note callback - target model is null" << endl; - } - } else { - cerr << "WARNING: analyseRecording note callback - target layer deleted" << endl; - } - - cleanupTempLayer(safeTempLayer, safeDocument, safePane); - - emit layersChanged(); - - completePart(); - }, - Qt::QueuedConnection); - } - } - - if (state->remainingParts == 0) { - cerr << "WARNING: analyseRecording: no recognised temp layers created" << endl; - finishRealtimeAnalysisChunk(); - } - - return ""; + m_realtimeAnalyser->setContext(m_document, m_fileModel, m_pane, pitchLayer, noteLayer); + return m_realtimeAnalyser->analyseChunk(sel); } diff --git a/main/Analyser.h b/main/Analyser.h index c063584..0364daa 100644 --- a/main/Analyser.h +++ b/main/Analyser.h @@ -17,14 +17,12 @@ #define ANALYSER_H #include -#include #include #include #include #include #include -#include #include "framework/Document.h" #include "base/Selection.h" @@ -39,6 +37,8 @@ class TimeValueLayer; class Layer; } +class RealtimeAnalyser; + class Analyser : public QObject, public sv::Document::LayerCreationHandler { @@ -265,11 +265,9 @@ protected slots: sv::sv_frame_t m_analysedFrames = 0; FrequencyRange m_reAnalysingRange; std::vector m_reAnalysisCandidates; - std::vector> m_realtimeAnalysisLayers; // Track temporary layers for cleanup - bool m_realtimeAnalysisInFlight; - std::optional m_pendingRealtimeSelection; - quint64 m_realtimeGeneration; + RealtimeAnalyser *m_realtimeAnalyser = nullptr; + int m_currentCandidate; bool m_candidatesVisible; sv::Document::LayerCreationAsyncHandle m_currentAsyncHandle; @@ -291,12 +289,7 @@ protected slots: void saveState(Component c) const; void loadState(Component c); - void cleanupRealtimeAnalysisLayers(); - void untrackRealtimeAnalysisLayer(sv::Layer *layer); - void finishRealtimeAnalysisChunk(); - bool isStaleRealtimeGeneration(quint64 generation) const; - // TODO (alnovi): Move constexpression to a static class static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; static constexpr const char* PYIN_TRANSFORM_BASE = "vamp:pyin:pyin:"; static constexpr const char* PYIN_F0_OUT = "smoothedpitchtrack"; diff --git a/main/RealtimeAnalyser.cpp b/main/RealtimeAnalyser.cpp new file mode 100644 index 0000000..97bf496 --- /dev/null +++ b/main/RealtimeAnalyser.cpp @@ -0,0 +1,555 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Tony + Realtime analysis helper (extracted from Analyser) +*/ + +#include "RealtimeAnalyser.h" + +#include "OverlapProcessor.h" + +#include +#include +#include +#include + +#include +#include + +#include + +#include "transform/TransformFactory.h" +#include "framework/Document.h" +#include "data/model/WaveFileModel.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/NoteModel.h" + +#include "view/Pane.h" +#include "layer/Layer.h" +#include "layer/TimeValueLayer.h" +#include "layer/FlexiNoteLayer.h" +#include "layer/ColourDatabase.h" + +using std::cerr; +using std::endl; + +using namespace sv; + +// Global overlap processor instance for efficient processing (same as previously in Analyser.cpp) +static OverlapProcessor s_overlapProcessor; + +// Wrapper functions +static EventVector processPitchModel(sv_frame_t contextStart, + std::shared_ptr fromModel, + std::shared_ptr toModel) +{ + return s_overlapProcessor.processPitchModel(contextStart, std::move(fromModel), std::move(toModel)); +} + +static EventVector processNoteModel(sv_frame_t contextStart, + std::shared_ptr fromModel, + std::shared_ptr toModel) +{ + return s_overlapProcessor.processNoteModel(contextStart, std::move(fromModel), std::move(toModel)); +} + +static std::map getAnalysisSettingsFromSettings() +{ + std::map analysisSettings; + + QSettings settings; + settings.beginGroup("Analyser"); + + analysisSettings["precision-analysis"] = settings.value("precision-analysis", false).toBool(); + analysisSettings["lowamp-analysis"] = settings.value("lowamp-analysis", true).toBool(); + analysisSettings["onset-analysis"] = settings.value("onset-analysis", true).toBool(); + analysisSettings["prune-analysis"] = settings.value("prune-analysis", true).toBool(); + + settings.endGroup(); + + return analysisSettings; +} + +static void setAnalysisSettings(Transform &transform) +{ + const auto analysisSettings = getAnalysisSettingsFromSettings(); + + if (analysisSettings.count("precision-analysis") > 0) { + bool precise = analysisSettings.at("precision-analysis"); + if (precise) { + cerr << "setting parameters for precise mode" << endl; + transform.setParameter("precisetime", 1); + } else { + cerr << "setting parameters for vague mode" << endl; + transform.setParameter("precisetime", 0); + } + } + + if (analysisSettings.count("lowamp-analysis") > 0) { + bool lowamp = analysisSettings.at("lowamp-analysis"); + if (lowamp) { + cerr << "setting parameters for lowamp suppression" << endl; + transform.setParameter("lowampsuppression", 0.2f); + } else { + cerr << "setting parameters for no lowamp suppression" << endl; + transform.setParameter("lowampsuppression", 0.0f); + } + } + + if (analysisSettings.count("onset-analysis") > 0) { + bool onset = analysisSettings.at("onset-analysis"); + if (onset) { + cerr << "setting parameters for increased onset sensitivity" << endl; + transform.setParameter("onsetsensitivity", 0.7f); + } else { + cerr << "setting parameters for non-increased onset sensitivity" << endl; + transform.setParameter("onsetsensitivity", 0.0f); + } + } + + if (analysisSettings.count("prune-analysis") > 0) { + bool prune = analysisSettings.at("prune-analysis"); + if (prune) { + cerr << "setting parameters for duration pruning" << endl; + transform.setParameter("prunethresh", 0.1f); + } else { + cerr << "setting parameters for no duration pruning" << endl; + transform.setParameter("prunethresh", 0.0f); + } + } +} + +RealtimeAnalyser::RealtimeAnalyser(QObject *parent) : + QObject(parent) +{ +} + +RealtimeAnalyser::~RealtimeAnalyser() +{ + cleanup(); +} + +void +RealtimeAnalyser::setContext(Document *document, + ModelId fileModel, + Pane *pane, + TimeValueLayer *targetPitchLayer, + FlexiNoteLayer *targetNoteLayer) +{ + QMutexLocker locker(&m_mutex); + + m_ctx.document = document; + m_ctx.fileModel = fileModel; + m_ctx.pane = pane; + m_ctx.targetPitchLayer = targetPitchLayer; + m_ctx.targetNoteLayer = targetNoteLayer; +} + +void +RealtimeAnalyser::clearContext() +{ + QMutexLocker locker(&m_mutex); + + m_ctx.document = nullptr; + m_ctx.fileModel = ModelId(); + m_ctx.pane = nullptr; + m_ctx.targetPitchLayer = nullptr; + m_ctx.targetNoteLayer = nullptr; +} + +void +RealtimeAnalyser::cleanup() +{ + std::vector> layersToClean; + QPointer doc; + QPointer pane; + + { + QMutexLocker locker(&m_mutex); + + layersToClean.swap(m_tempLayers); + + // Ensure we never block future work after cleanup + m_inFlight = false; + m_pendingSelection = std::nullopt; + + ++m_generation; + + doc = m_ctx.document; + pane = m_ctx.pane; + } + + cleanupTempLayers(std::move(layersToClean), doc, pane); +} + +void +RealtimeAnalyser::invalidateGeneration() +{ + QMutexLocker locker(&m_mutex); + + // If we invalidate generation, any existing callbacks become stale. + // Also ensure we don't keep "in-flight" latched forever. + ++m_generation; + m_inFlight = false; + m_pendingSelection = std::nullopt; +} + +QString +RealtimeAnalyser::analyseChunk(Selection sel) +{ + bool startedChunk = false; + + if (sel.isEmpty()) return ""; + + quint64 generation = 0; + QPointer safeDocument; + QPointer safePane; + QPointer safeTargetPitchLayer; + QPointer safeTargetNoteLayer; + ModelId fileModel; + std::shared_ptr waveFileModel; + + { + QMutexLocker locker(&m_mutex); + + if (m_inFlight) { + m_pendingSelection = sel; + cerr << "RealtimeAnalyser::analyseChunk: already in flight, replacing pending selection" << endl; + return ""; + } + + if (!m_ctx.document || !m_ctx.pane) { + return "Internal error: RealtimeAnalyser::analyseChunk() called with no document or pane present"; + } + + if (m_ctx.fileModel.isNone()) { + return "Internal error: RealtimeAnalyser::analyseChunk() called with no model present"; + } + + if (!m_ctx.targetPitchLayer || !m_ctx.targetNoteLayer) { + return "Internal error: RealtimeAnalyser::analyseChunk() called with no target pitch/note layers present"; + } + + m_inFlight = true; + startedChunk = true; + generation = m_generation; + + safeDocument = m_ctx.document; + safePane = m_ctx.pane; + safeTargetPitchLayer = m_ctx.targetPitchLayer; + safeTargetNoteLayer = m_ctx.targetNoteLayer; + fileModel = m_ctx.fileModel; + } + + waveFileModel = ModelById::getAs(fileModel); + if (!waveFileModel) { + if (startedChunk) finishChunk(); + return "Internal error: RealtimeAnalyser::analyseChunk() called with no WaveFileModel"; + } + + auto finishIfStarted = [this, startedChunk]() { + if (startedChunk) { + finishChunk(); + } + }; + + auto cleanupTempLayer = [this](QPointer safeTempLayer, + QPointer doc, + QPointer pane) { + Layer *layerToDelete = safeTempLayer.data(); + if (!layerToDelete) return; + + { + QMutexLocker locker(&m_mutex); + untrackTempLayerLocked(layerToDelete); + } + + if (doc && pane) { + doc->removeLayerFromView(pane.data(), layerToDelete); + if (safeTempLayer) { + doc->deleteLayer(layerToDelete); + } + } + }; + + auto state = std::make_shared(); + + auto completePart = [this, state](bool canFinish) { + --state->remainingParts; + if (canFinish && state->remainingParts == 0) { + finishChunk(); + } + }; + + TransformFactory *tf = TransformFactory::getInstance(); + + const auto f0_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_F0_OUT); + const auto note_transform = QString(PYIN_TRANSFORM_BASE) + QString(PYIN_NOTE_OUT); + + QString notFound = + tr("Transform \"%1\" not found. Unable to perform interactive analysis." + "

Are the %2 and %3 Vamp plugins correctly installed?"); + + if (!tf->haveTransform(f0_transform)) { + finishIfStarted(); + return notFound.arg(f0_transform).arg(PYIN_PLUGIN_NAME); + } + + if (!tf->haveTransform(note_transform)) { + finishIfStarted(); + return notFound.arg(note_transform).arg(PYIN_PLUGIN_NAME); + } + + Transform t = tf->getDefaultTransformFor(f0_transform, waveFileModel->getSampleRate()); + t.setStepSize(256); + t.setBlockSize(2048); + + setAnalysisSettings(t); + + const RealTime start = + RealTime::frame2RealTime(sel.getStartFrame(), waveFileModel->getSampleRate()); + const RealTime end = + RealTime::frame2RealTime(sel.getEndFrame(), waveFileModel->getSampleRate()); + + RealTime duration; + if (sel.getEndFrame() > sel.getStartFrame()) { + duration = end - start; + } + + cerr << "RealtimeAnalyser::analyseChunk: start " << start + << " end " << end + << " original selection start " << sel.getStartFrame() + << " end " << sel.getEndFrame() + << " duration " << duration << endl; + + if (duration <= RealTime::zeroTime) { + cerr << "RealtimeAnalyser::analyseChunk: duration <= 0, not analysing" << endl; + finishIfStarted(); + return ""; + } + + t.setStartTime(start); + t.setDuration(duration); + + Transforms transforms; + transforms.push_back(t); + + t.setOutput(PYIN_NOTE_OUT); + transforms.push_back(t); + + if (!safeDocument) { + finishIfStarted(); + return "Internal error: RealtimeAnalyser::analyseChunk() document deleted during scheduling"; + } + + const std::vector layers = safeDocument->createDerivedLayers(transforms, fileModel); + + if (layers.empty()) { + cerr << "WARNING: RealtimeAnalyser::analyseChunk: no layers returned from createDerivedLayers" << endl; + finishIfStarted(); + return ""; + } + + { + QMutexLocker locker(&m_mutex); + for (auto *layer : layers) { + m_tempLayers.push_back(QPointer(layer)); + } + } + + ColourDatabase *cdb = ColourDatabase::getInstance(); + + for (auto *layer : layers) { + + if (auto *tempPitchLayer = qobject_cast(layer)) { + + ++state->remainingParts; + tempPitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); + + QPointer safeTempLayer(tempPitchLayer); + + QObject::connect( + tempPitchLayer, + &TimeValueLayer::modelCompletionChanged, + this, + [this, safeTempLayer, safeTargetPitchLayer, safeDocument, safePane, + sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { + + const auto fromModel = ModelById::getAs(modelId); + if (!fromModel || fromModel->getCompletion() != 100) { + return; + } + + cerr << "RealtimeAnalyser::analyseChunk: Processing pitch track completion" << endl; + + bool stale = false; + { + QMutexLocker locker(&m_mutex); + stale = (generation != m_generation); + } + + if (stale) { + cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale pitch callback from old generation" << endl; + cleanupTempLayer(safeTempLayer, safeDocument, safePane); + completePart(false); + return; + } + + if (safeTargetPitchLayer) { + const auto toModel = + ModelById::getAs(safeTargetPitchLayer->getModel()); + + if (toModel) { + const EventVector points = + processPitchModel(sel.getStartFrame(), fromModel, toModel); + + for (const Event &p : points) { + toModel->add(p); + } + } else { + cerr << "ERROR: RealtimeAnalyser pitch callback - target model is null" << endl; + } + } else { + cerr << "WARNING: RealtimeAnalyser pitch callback - target layer deleted" << endl; + } + + cleanupTempLayer(safeTempLayer, safeDocument, safePane); + + emit layersChanged(); + + completePart(true); + }, + Qt::QueuedConnection); + } + + if (auto *tempNoteLayer = qobject_cast(layer)) { + + ++state->remainingParts; + tempNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); + + QPointer safeTempLayer(tempNoteLayer); + + QObject::connect( + tempNoteLayer, + &FlexiNoteLayer::modelCompletionChanged, + this, + [this, safeTempLayer, safeTargetNoteLayer, safeDocument, safePane, + sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { + + const auto fromModel = ModelById::getAs(modelId); + if (!fromModel || fromModel->getCompletion() != 100) { + return; + } + + cerr << "RealtimeAnalyser::analyseChunk: Processing note layer completion" << endl; + + bool stale = false; + { + QMutexLocker locker(&m_mutex); + stale = (generation != m_generation); + } + + if (stale) { + cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale note callback from old generation" << endl; + cleanupTempLayer(safeTempLayer, safeDocument, safePane); + completePart(false); + return; + } + + if (safeTargetNoteLayer) { + const auto toModel = + ModelById::getAs(safeTargetNoteLayer->getModel()); + + if (toModel) { + const EventVector points = + processNoteModel(sel.getStartFrame(), fromModel, toModel); + + for (const Event &p : points) { + toModel->add(p); + } + } else { + cerr << "ERROR: RealtimeAnalyser note callback - target model is null" << endl; + } + } else { + cerr << "WARNING: RealtimeAnalyser note callback - target layer deleted" << endl; + } + + cleanupTempLayer(safeTempLayer, safeDocument, safePane); + + emit layersChanged(); + + completePart(true); + }, + Qt::QueuedConnection); + } + } + + if (state->remainingParts == 0) { + cerr << "WARNING: RealtimeAnalyser::analyseChunk: no recognised temp layers created" << endl; + finishIfStarted(); + } + + return ""; +} + +bool +RealtimeAnalyser::hasValidContextLocked() const +{ + return (m_ctx.document && m_ctx.pane && + !m_ctx.fileModel.isNone() && + m_ctx.targetPitchLayer && + m_ctx.targetNoteLayer); +} + +bool +RealtimeAnalyser::isStaleGenerationLocked(quint64 generation) const +{ + return generation != m_generation; +} + +void +RealtimeAnalyser::untrackTempLayerLocked(Layer *layer) +{ + m_tempLayers.erase( + std::remove_if(m_tempLayers.begin(), + m_tempLayers.end(), + [layer](const QPointer &p) { + return p.isNull() || p.data() == layer; + }), + m_tempLayers.end()); +} + +void +RealtimeAnalyser::cleanupTempLayers(std::vector> layersToClean, + const QPointer &doc, + const QPointer &pane) +{ + if (!doc || !pane) return; + + for (const auto &layerPtr : layersToClean) { + Layer *layer = layerPtr.data(); + if (!layer) continue; + + doc->removeLayerFromView(pane.data(), layer); + doc->deleteLayer(layer); + } +} + +void +RealtimeAnalyser::finishChunk() +{ + std::optional pending; + + { + QMutexLocker locker(&m_mutex); + + m_inFlight = false; + pending = std::exchange(m_pendingSelection, std::nullopt); + } + + if (pending) { + cerr << "RealtimeAnalyser::finishChunk: starting pending realtime selection" << endl; + (void)analyseChunk(*pending); + } +} \ No newline at end of file diff --git a/main/RealtimeAnalyser.h b/main/RealtimeAnalyser.h new file mode 100644 index 0000000..05ae368 --- /dev/null +++ b/main/RealtimeAnalyser.h @@ -0,0 +1,96 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + + /* + Tony + Realtime analysis helper (extracted from Analyser) + */ + +#ifndef REALTIMEANALYSER_H +#define REALTIMEANALYSER_H + +#include +#include +#include +#include +#include + +#include +#include + +#include "framework/Document.h" +#include "base/Selection.h" + +namespace sv { +class Pane; +class Layer; +class TimeValueLayer; +class FlexiNoteLayer; +} + +class RealtimeAnalyser : public QObject +{ + Q_OBJECT + +public: + explicit RealtimeAnalyser(QObject *parent = nullptr); + ~RealtimeAnalyser() override; + + void setContext(sv::Document *document, + sv::ModelId fileModel, + sv::Pane *pane, + sv::TimeValueLayer *targetPitchLayer, + sv::FlexiNoteLayer *targetNoteLayer); + + void clearContext(); + + // Cancels in-flight work, clears pending selection, removes temp layers, bumps generation + void cleanup(); + + // Ignore stale callbacks after external state changes; does not delete layers + void invalidateGeneration(); + + // One-in-flight realtime analysis: if a chunk is already running, replaces the pending selection + QString analyseChunk(sv::Selection sel); + +signals: + void layersChanged(); + +private: + struct Context { + QPointer document; + sv::ModelId fileModel; + QPointer pane; + QPointer targetPitchLayer; + QPointer targetNoteLayer; + }; + + struct RealtimeChunkState { + int remainingParts = 0; + }; + + static constexpr const char* PYIN_PLUGIN_NAME = "pYIN"; + static constexpr const char* PYIN_TRANSFORM_BASE = "vamp:pyin:pyin:"; + static constexpr const char* PYIN_F0_OUT = "smoothedpitchtrack"; + static constexpr const char* PYIN_NOTE_OUT = "notes"; + + bool hasValidContextLocked() const; + bool isStaleGenerationLocked(quint64 generation) const; + + void untrackTempLayerLocked(sv::Layer *layer); + void cleanupTempLayers(std::vector> layersToClean, + const QPointer &doc, + const QPointer &pane); + + void finishChunk(); + +private: + mutable QMutex m_mutex; + Context m_ctx; + std::vector> m_tempLayers; + + bool m_inFlight = false; + std::optional m_pendingSelection; + quint64 m_generation = 0; +}; + +#endif \ No newline at end of file diff --git a/meson.build b/meson.build index 19eb9d5..6512bb5 100644 --- a/meson.build +++ b/meson.build @@ -186,7 +186,8 @@ elif system == 'darwin' rc = [] general_defines += [ - '-mmacosx-version-min=10.15' + '-mmacosx-version-min=10.15', + '-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_NONE' ] general_link_args += [ '-mmacosx-version-min=10.15' @@ -1021,6 +1022,7 @@ chp_plugin = shared_library( tony_main_files = [ 'main/main.cpp', 'main/Analyser.cpp', + 'main/RealtimeAnalyser.cpp', 'main/OverlapProcessor.cpp', 'main/MainWindow.cpp', 'main/NetworkPermissionTester.cpp', @@ -1030,6 +1032,7 @@ tony_main_moc_files = qt.preprocess( moc_headers: [ 'main/MainWindow.h', 'main/Analyser.h', + 'main/RealtimeAnalyser.h', ]) qt_resource_files = qt.preprocess( From bd7a622552206a4eaa568d1f5f2af6753c3beeda Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Mon, 16 Mar 2026 16:26:23 +0300 Subject: [PATCH 24/24] improve OverlapProcessor --- main/OverlapProcessor.cpp | 451 ++++++++++++++++++++------------------ main/OverlapProcessor.h | 63 +++--- main/RealtimeAnalyser.cpp | 38 ++-- 3 files changed, 289 insertions(+), 263 deletions(-) diff --git a/main/OverlapProcessor.cpp b/main/OverlapProcessor.cpp index a21c87e..cb41083 100644 --- a/main/OverlapProcessor.cpp +++ b/main/OverlapProcessor.cpp @@ -14,56 +14,109 @@ */ #include "OverlapProcessor.h" -#include "data/model/SparseTimeValueModel.h" -#include "data/model/NoteModel.h" #include #include -#include -using std::cerr; -using std::endl; +namespace { + +bool lessEventForDedup(const Event &a, const Event &b) +{ + if (a.getFrame() != b.getFrame()) return a.getFrame() < b.getFrame(); + if (a.getDuration() != b.getDuration()) return a.getDuration() < b.getDuration(); + + if (a.hasValue() != b.hasValue()) return a.hasValue() < b.hasValue(); + if (a.hasValue() && a.getValue() != b.getValue()) return a.getValue() < b.getValue(); + + if (a.hasLabel() != b.hasLabel()) return a.hasLabel() < b.hasLabel(); + if (a.hasLabel() && a.getLabel() != b.getLabel()) return a.getLabel() < b.getLabel(); + + if (a.hasLevel() != b.hasLevel()) return a.hasLevel() < b.hasLevel(); + if (a.hasLevel() && a.getLevel() != b.getLevel()) return a.getLevel() < b.getLevel(); + + return false; +} + +bool equalEventForDedup(const Event &a, const Event &b) +{ + return a.getFrame() == b.getFrame() && + a.getDuration() == b.getDuration() && + a.hasValue() == b.hasValue() && + (!a.hasValue() || a.getValue() == b.getValue()) && + a.hasLabel() == b.hasLabel() && + (!a.hasLabel() || a.getLabel() == b.getLabel()) && + a.hasLevel() == b.hasLevel() && + (!a.hasLevel() || a.getLevel() == b.getLevel()); +} + +void sortAndDedupeEvents(EventVector &events) +{ + std::sort(events.begin(), events.end(), lessEventForDedup); + events.erase(std::unique(events.begin(), events.end(), equalEventForDedup), + events.end()); +} + +EventVector shiftedBy(const EventVector &events, sv_frame_t offset) +{ + EventVector shifted = events; + std::transform(shifted.begin(), shifted.end(), shifted.begin(), + [offset](const auto &event) { + return event.withFrame(event.getFrame() + offset); + }); + return shifted; +} +} // OverlapGroup implementation -OverlapGroup::OverlapGroup(const EventVector& evts) : events(evts) { - if (!events.empty()) { - startFrame = events.front().getFrame(); - endFrame = events.front().getFrame() + events.front().getDuration(); - - for (const auto& event : events) { - startFrame = std::min(startFrame, event.getFrame()); - endFrame = std::max(endFrame, event.getFrame() + event.getDuration()); - } +OverlapGroup::OverlapGroup() : startFrame(0), endFrame(0) +{ +} + +OverlapGroup::OverlapGroup(size_t index, const Event &event) : + indices{index}, + startFrame(event.getFrame()), + endFrame(event.getFrame() + event.getDuration()) +{ +} + +void OverlapGroup::addEvent(size_t index, const Event &event) +{ + if (indices.empty()) { + startFrame = event.getFrame(); + endFrame = event.getFrame() + event.getDuration(); } else { - startFrame = endFrame = 0; + startFrame = std::min(startFrame, event.getFrame()); + endFrame = std::max(endFrame, event.getFrame() + event.getDuration()); } + + indices.push_back(index); } // OverlapProcessor implementation -OverlapProcessor::OverlapProcessor(const OverlapConfig& config) : m_config(config) { +OverlapProcessor::OverlapProcessor(const OverlapConfig &config) : + m_config(config) +{ } std::vector OverlapProcessor::findOverlapGroups(const EventVector& events) const { std::vector groups; - std::vector processed(events.size(), false); - + std::vector processed(events.size(), 0); + for (size_t i = 0; i < events.size(); ++i) { if (processed[i]) continue; - - EventVector currentGroup; - currentGroup.push_back(events[i]); + + OverlapGroup currentGroup(i, events[i]); processed[i] = true; - + // Find all events that overlap with any event in the current group bool foundOverlap; do { foundOverlap = false; for (size_t j = 0; j < events.size(); ++j) { if (processed[j]) continue; - - // Check if event j overlaps with any event in the current group - for (const auto& groupEvent : currentGroup) { - if (eventsOverlap(events[j], groupEvent)) { - currentGroup.push_back(events[j]); + + for (size_t groupIndex : currentGroup.indices) { + if (eventsOverlap(events[j], events[groupIndex])) { + currentGroup.addEvent(j, events[j]); processed[j] = true; foundOverlap = true; break; @@ -72,298 +125,258 @@ std::vector OverlapProcessor::findOverlapGroups(const EventVector& if (foundOverlap) break; } } while (foundOverlap); - - // Only create groups for actual overlaps (more than one event) + if (currentGroup.size() > 1) { - groups.emplace_back(currentGroup); + groups.push_back(currentGroup); } } - + return groups; } -float OverlapProcessor::calculateWeightedFrequency(const EventVector& overlappingEvents, - sv_frame_t overlapStart, - sv_frame_t overlapDuration) const { +float OverlapProcessor::calculateWeightedFrequency(const EventVector& overlappingEvents, + sv_frame_t overlapStart, + sv_frame_t overlapDuration) const { if (overlappingEvents.empty()) return 0.0f; if (overlappingEvents.size() == 1) { return overlappingEvents[0].hasValue() ? overlappingEvents[0].getValue() : 0.0f; } - - // Calculate weighted contributions from all events + std::vector frequencies; std::vector weights; - auto overlapEnd = overlapStart + overlapDuration; - + frequencies.reserve(overlappingEvents.size()); + weights.reserve(overlappingEvents.size()); + + const auto overlapEnd = overlapStart + overlapDuration; + for (const auto& event : overlappingEvents) { - float freq = event.hasValue() ? event.getValue() : 0.0f; - if (freq <= 0.0f) continue; // Skip invalid frequencies - - // Calculate this event's contribution to the overlap - auto eventStart = event.getFrame(); - auto eventEnd = event.getFrame() + event.getDuration(); - - auto eventOverlapStart = std::max(eventStart, overlapStart); - auto eventOverlapEnd = std::min(eventEnd, overlapEnd); - auto eventOverlapContrib = std::max(0LL, eventOverlapEnd - eventOverlapStart); - + const float freq = event.hasValue() ? event.getValue() : 0.0f; + if (freq <= 0.0f) continue; + + const auto eventStart = event.getFrame(); + const auto eventEnd = event.getFrame() + event.getDuration(); + + const auto eventOverlapStart = std::max(eventStart, overlapStart); + const auto eventOverlapEnd = std::min(eventEnd, overlapEnd); + const auto eventOverlapContrib = std::max(0, eventOverlapEnd - eventOverlapStart); + if (eventOverlapContrib > 0) { frequencies.push_back(freq); weights.push_back(static_cast(eventOverlapContrib)); } } - + if (frequencies.empty()) return 0.0f; if (frequencies.size() == 1) return frequencies[0]; - - // Normalize weights + double totalWeight = 0.0; for (double weight : weights) totalWeight += weight; if (totalWeight <= 0.0) { - // Fallback to simple geometric mean double logSum = 0.0; for (float freq : frequencies) { logSum += std::log(freq); } return std::exp(logSum / frequencies.size()); } - + for (double& weight : weights) weight /= totalWeight; - - // Calculate weighted geometric mean for better musical accuracy + double weightedLogSum = 0.0; for (size_t i = 0; i < frequencies.size(); ++i) { weightedLogSum += std::log(frequencies[i]) * weights[i]; } - - return std::exp(weightedLogSum); -} -float OverlapProcessor::calculateWeightedFrequency(const Event& prevEvent, - const Event& nextEvent, - sv_frame_t overlapStart, - sv_frame_t overlapDuration) const { - EventVector events = {prevEvent, nextEvent}; - return calculateWeightedFrequency(events, overlapStart, overlapDuration); + return std::exp(weightedLogSum); } -Event OverlapProcessor::mergeOverlapGroup(const OverlapGroup& group) const { +std::optional OverlapProcessor::mergeOverlapGroup(const OverlapGroup& group, const EventVector& events) const { if (group.isEmpty()) { - return Event(0, 0.0f, ""); + return std::nullopt; } - + if (group.size() == 1) { - return group.events[0]; + return events[group.indices[0]]; + } + + const auto mergedStart = group.startFrame; + const auto mergedEnd = group.endFrame; + const auto mergedDuration = mergedEnd - mergedStart; + + EventVector overlappingEvents; + overlappingEvents.reserve(group.indices.size()); + for (size_t index : group.indices) { + overlappingEvents.push_back(events[index]); + } + + const float weightedFreq = + calculateWeightedFrequency(overlappingEvents, mergedStart, mergedDuration); + + const Event* longestEvent = findLongestEvent(group, events); + if (!longestEvent) { + return std::nullopt; } - - // Calculate merged event properties - auto mergedStart = group.startFrame; - auto mergedEnd = group.endFrame; - auto mergedDuration = mergedEnd - mergedStart; - - // Calculate weighted frequency for the entire overlap - float weightedFreq = calculateWeightedFrequency(group.events, mergedStart, mergedDuration); - - // Use properties from the event with the longest duration as base - const Event* longestEvent = findLongestEvent(group.events); - - // Create merged event + Event mergedEvent = longestEvent->withFrame(mergedStart) .withDuration(mergedDuration); - + if (weightedFreq > 0.0f) { mergedEvent = mergedEvent.withValue(weightedFreq); } - - // Preserve label if available + if (longestEvent->hasLabel()) { mergedEvent = mergedEvent.withLabel(longestEvent->getLabel()); } - - // Preserve level if available + if (longestEvent->hasLevel()) { mergedEvent = mergedEvent.withLevel(longestEvent->getLevel()); } - + return mergedEvent; } -EventVector OverlapProcessor::processPitchModel(sv_frame_t contextStart, - std::shared_ptr fromModel, - std::shared_ptr toModel) const { - auto allEvents = toModel->getAllEvents(); - auto points = fromModel->getAllEvents(); +OverlapProcessor::EventPatch OverlapProcessor::processPitchEvents(sv_frame_t contextStart, + const EventVector& incomingEvents, + const EventVector& existingEvents) const { + EventPatch patch; + EventVector shiftedIncoming = shiftedBy(incomingEvents, contextStart); - // Add context start timestamp to all points from the new analysis - std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { - return point.withFrame(point.getFrame() + contextStart); - }); + EventVector remainingEvents; + patch.remove.reserve(existingEvents.size()); + remainingEvents.reserve(existingEvents.size()); - // Remove all events from toModel that extend beyond contextStart to prevent overlaps - EventVector eventsToRemove; - for (const auto& event : allEvents) { - if (event.getFrame() >= contextStart) { - eventsToRemove.push_back(event); + for (const auto& event : existingEvents) { + const auto eventStart = event.getFrame(); + const auto eventEnd = eventStart + event.getDuration(); + if (eventStart >= contextStart || eventEnd > contextStart) { + patch.remove.push_back(event); + } else { + remainingEvents.push_back(event); } } - - for (const auto& event : eventsToRemove) { - toModel->remove(event); - } - // After cleanup, get the remaining events for overlap processing - allEvents = toModel->getAllEvents(); + sortAndDedupeEvents(patch.remove); + + patch.add = shiftedIncoming; - // Handle potential overlaps between the last existing event and first new event - if (!allEvents.empty() && !points.empty()) { - auto& lastExistingEvent = allEvents.back(); - auto& firstNewEvent = points.front(); + if (!remainingEvents.empty() && !shiftedIncoming.empty()) { + const auto& lastExistingEvent = remainingEvents.back(); + const auto& firstNewEvent = shiftedIncoming.front(); + + const auto lastExistingEnd = + lastExistingEvent.getFrame() + lastExistingEvent.getDuration(); + const auto gapFrames = firstNewEvent.getFrame() - lastExistingEnd; - // Check if there's a gap or overlap between last existing and first new event - auto gapFrames = firstNewEvent.getFrame() - lastExistingEvent.getFrame(); - - // If events are very close (within interpolation threshold), interpolate between them if (gapFrames > 0 && gapFrames <= m_config.interpolationThreshold) { - // Small gap - add interpolated point if pitch values are similar if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { - auto lastValue = lastExistingEvent.getValue(); - auto firstValue = firstNewEvent.getValue(); - auto valueDiff = std::abs(lastValue - firstValue) / lastValue; - - // Only interpolate if pitch values are within similarity threshold - if (valueDiff <= m_config.pitchSimilarityThreshold) { - auto midFrame = lastExistingEvent.getFrame() + gapFrames / 2; - auto midValue = (lastValue + firstValue) / 2.0; - Event interpolatedEvent = Event(midFrame, midValue, "interpolated"); - toModel->add(interpolatedEvent); + const auto lastValue = lastExistingEvent.getValue(); + const auto firstValue = firstNewEvent.getValue(); + + if (lastValue > 0.0f && firstValue > 0.0f) { + const auto valueDiff = std::abs(lastValue - firstValue) / lastValue; + + if (valueDiff <= m_config.pitchSimilarityThreshold) { + const auto midFrame = lastExistingEnd + gapFrames / 2; + const auto midValue = (lastValue + firstValue) / 2.0f; + patch.add.push_back(Event(midFrame, midValue, "interpolated")); + } } } } } - return points; -} + sortAndDedupeEvents(patch.add); -EventVector OverlapProcessor::processNoteModel(sv_frame_t contextStart, - std::shared_ptr fromModel, - std::shared_ptr toModel) const { - auto allEvents = toModel->getAllEvents(); - auto points = fromModel->getAllEvents(); + return patch; +} - // Vamp doesn't add current timestamp for note features, so, do it manually - std::transform(points.begin(), points.end(), points.begin(), [&](const auto& point) { - return point.withFrame(point.getFrame() + contextStart); - }); +OverlapProcessor::EventPatch OverlapProcessor::processNoteEvents(sv_frame_t contextStart, + const EventVector& incomingEvents, + const EventVector& existingEvents) const { + EventPatch patch; + EventVector shiftedIncoming = shiftedBy(incomingEvents, contextStart); - // Enhanced cleanup strategy: more intelligent overlap-aware removal - EventVector eventsToRemove; EventVector remainingEvents; - - // Categorize events based on potential overlap with incoming analysis - categorizeEvents(allEvents, points, eventsToRemove, remainingEvents); - - // Remove potentially overlapping events from the model - for (const auto& event : eventsToRemove) { - toModel->remove(event); - } + categorizeEvents(existingEvents, shiftedIncoming, patch.remove, remainingEvents); + sortAndDedupeEvents(patch.remove); - // Create combined event set for overlap detection EventVector combinedEvents; - - // Add remaining non-overlapping events - for (const auto& event : remainingEvents) { - combinedEvents.push_back(event); - } - - // Add previously removed events that might need merging - for (const auto& event : eventsToRemove) { - combinedEvents.push_back(event); - } - - // Add new incoming events - for (const auto& event : points) { - combinedEvents.push_back(event); - } + combinedEvents.reserve(remainingEvents.size() + patch.remove.size() + shiftedIncoming.size()); + combinedEvents.insert(combinedEvents.end(), remainingEvents.begin(), remainingEvents.end()); + combinedEvents.insert(combinedEvents.end(), patch.remove.begin(), patch.remove.end()); + combinedEvents.insert(combinedEvents.end(), shiftedIncoming.begin(), shiftedIncoming.end()); - // Find all overlap groups in the combined event set - auto overlapGroups = findOverlapGroups(combinedEvents); + const auto overlapGroups = findOverlapGroups(combinedEvents); - // Process each overlap group EventVector finalEvents; - std::vector processed(combinedEvents.size(), false); - - // Process overlap groups first + finalEvents.reserve(combinedEvents.size()); + std::vector processed(combinedEvents.size(), 0); + for (const auto& group : overlapGroups) { - Event mergedEvent = mergeOverlapGroup(group); - finalEvents.push_back(mergedEvent); - - // Mark all events in this group as processed - for (const auto& groupEvent : group.events) { - for (size_t i = 0; i < combinedEvents.size(); ++i) { - if (!processed[i] && - combinedEvents[i].getFrame() == groupEvent.getFrame() && - combinedEvents[i].getDuration() == groupEvent.getDuration()) { - processed[i] = true; - } + if (auto merged = mergeOverlapGroup(group, combinedEvents)) { + finalEvents.push_back(*merged); + } + + for (size_t index : group.indices) { + if (index < processed.size()) { + processed[index] = true; } } } - - // Add non-overlapping events + for (size_t i = 0; i < combinedEvents.size(); ++i) { if (!processed[i]) { finalEvents.push_back(combinedEvents[i]); } } - // Filter to return only the new/modified events that should be added - EventVector resultEvents; + patch.add.reserve(finalEvents.size()); for (const auto& event : finalEvents) { - bool isNew = false; - - // Check if this event is derived from new analysis or is a merge result - for (const auto& originalNew : points) { - if (event.getFrame() >= originalNew.getFrame() - m_config.overlapTolerance && - event.getFrame() <= originalNew.getFrame() + originalNew.getDuration() + m_config.overlapTolerance) { - isNew = true; + bool include = false; + + for (const auto& originalNew : shiftedIncoming) { + if (eventsOverlap(event, originalNew)) { + include = true; break; } } - - // Also include events that are merge results (modified existing events) - bool isMergeResult = false; - for (const auto& removedEvent : eventsToRemove) { - if (event.getFrame() >= removedEvent.getFrame() - m_config.overlapTolerance && - event.getFrame() <= removedEvent.getFrame() + removedEvent.getDuration() + m_config.overlapTolerance) { - isMergeResult = true; - break; + + if (!include) { + for (const auto& removedEvent : patch.remove) { + if (eventsOverlap(event, removedEvent)) { + include = true; + break; + } } } - - if (isNew || isMergeResult) { - resultEvents.push_back(event); + + if (include) { + patch.add.push_back(event); } } - return resultEvents; + sortAndDedupeEvents(patch.add); + + return patch; } // Private helper methods bool OverlapProcessor::eventsOverlap(const Event& a, const Event& b) const { - auto aStart = a.getFrame(); - auto aEnd = a.getFrame() + a.getDuration(); - auto bStart = b.getFrame(); - auto bEnd = b.getFrame() + b.getDuration(); - - // Check for temporal overlap + const auto aStart = a.getFrame(); + const auto aEnd = a.getFrame() + a.getDuration(); + const auto bStart = b.getFrame(); + const auto bEnd = b.getFrame() + b.getDuration(); + + // Intentionally strict overlap test: both grouping and patch inclusion + // only consider events that truly overlap in time. return aStart < bEnd && aEnd > bStart; } -const Event* OverlapProcessor::findLongestEvent(const EventVector& events) const { - if (events.empty()) return nullptr; - - const Event* longestEvent = &events[0]; - for (const auto& event : events) { +const Event* OverlapProcessor::findLongestEvent(const OverlapGroup& group, const EventVector& events) const { + if (group.indices.empty()) return nullptr; + + const Event* longestEvent = &events[group.indices[0]]; + for (size_t index : group.indices) { + const Event &event = events[index]; if (event.getDuration() > longestEvent->getDuration()) { longestEvent = &event; } @@ -371,11 +384,13 @@ const Event* OverlapProcessor::findLongestEvent(const EventVector& events) const return longestEvent; } -void OverlapProcessor::categorizeEvents(const EventVector& allEvents, +void OverlapProcessor::categorizeEvents(const EventVector& allEvents, const EventVector& newEvents, EventVector& eventsToRemove, EventVector& remainingEvents) const { - // Identify events that might overlap with incoming analysis + eventsToRemove.reserve(allEvents.size()); + remainingEvents.reserve(allEvents.size()); + for (const auto& event : allEvents) { bool potentialOverlap = false; for (const auto& newEvent : newEvents) { @@ -384,7 +399,7 @@ void OverlapProcessor::categorizeEvents(const EventVector& allEvents, break; } } - + if (potentialOverlap) { eventsToRemove.push_back(event); } else { diff --git a/main/OverlapProcessor.h b/main/OverlapProcessor.h index 6760eaa..d66979b 100644 --- a/main/OverlapProcessor.h +++ b/main/OverlapProcessor.h @@ -17,14 +17,13 @@ #define OVERLAP_PROCESSOR_H #include -#include +#include +#include + +#include "base/Event.h" -// Forward declarations - includes will be in the .cpp file namespace sv { - class Event; typedef std::vector EventVector; - class SparseTimeValueModel; - class NoteModel; typedef int64_t sv_frame_t; } @@ -49,13 +48,15 @@ struct OverlapConfig { * Structure to represent a group of overlapping events */ struct OverlapGroup { - EventVector events; + std::vector indices; sv_frame_t startFrame; sv_frame_t endFrame; - - explicit OverlapGroup(const EventVector& evts); - bool isEmpty() const { return events.empty(); } - size_t size() const { return events.size(); } + + OverlapGroup(); + explicit OverlapGroup(size_t index, const Event &event); + bool isEmpty() const { return indices.empty(); } + size_t size() const { return indices.size(); } + void addEvent(size_t index, const Event &event); }; /** @@ -66,27 +67,27 @@ class OverlapProcessor { explicit OverlapProcessor(const OverlapConfig& config = OverlapConfig()); // Core overlap detection and processing + struct EventPatch { + EventVector remove; + EventVector add; + }; + std::vector findOverlapGroups(const EventVector& events) const; - Event mergeOverlapGroup(const OverlapGroup& group) const; + std::optional mergeOverlapGroup(const OverlapGroup& group, const EventVector& events) const; // Frequency calculation methods - float calculateWeightedFrequency(const EventVector& overlappingEvents, - sv_frame_t overlapStart, - sv_frame_t overlapDuration) const; - - float calculateWeightedFrequency(const Event& prevEvent, - const Event& nextEvent, - sv_frame_t overlapStart, - sv_frame_t overlapDuration) const; + float calculateWeightedFrequency(const EventVector& overlappingEvents, + sv_frame_t overlapStart, + sv_frame_t overlapDuration) const; // Main processing methods for different model types - EventVector processPitchModel(sv_frame_t contextStart, - std::shared_ptr fromModel, - std::shared_ptr toModel) const; - - EventVector processNoteModel(sv_frame_t contextStart, - std::shared_ptr fromModel, - std::shared_ptr toModel) const; + EventPatch processPitchEvents(sv_frame_t contextStart, + const EventVector& incomingEvents, + const EventVector& existingEvents) const; + + EventPatch processNoteEvents(sv_frame_t contextStart, + const EventVector& incomingEvents, + const EventVector& existingEvents) const; // Configuration access const OverlapConfig& getConfig() const { return m_config; } @@ -97,11 +98,11 @@ class OverlapProcessor { // Helper methods bool eventsOverlap(const Event& a, const Event& b) const; - const Event* findLongestEvent(const EventVector& events) const; - void categorizeEvents(const EventVector& allEvents, - const EventVector& newEvents, - EventVector& eventsToRemove, - EventVector& remainingEvents) const; + const Event* findLongestEvent(const OverlapGroup& group, const EventVector& events) const; + void categorizeEvents(const EventVector& allEvents, + const EventVector& newEvents, + EventVector& eventsToRemove, + EventVector& remainingEvents) const; }; #endif // OVERLAP_PROCESSOR_H diff --git a/main/RealtimeAnalyser.cpp b/main/RealtimeAnalyser.cpp index 97bf496..4b16178 100644 --- a/main/RealtimeAnalyser.cpp +++ b/main/RealtimeAnalyser.cpp @@ -40,18 +40,22 @@ using namespace sv; static OverlapProcessor s_overlapProcessor; // Wrapper functions -static EventVector processPitchModel(sv_frame_t contextStart, - std::shared_ptr fromModel, - std::shared_ptr toModel) +static OverlapProcessor::EventPatch processPitchEvents(sv_frame_t contextStart, + const std::shared_ptr &fromModel, + const std::shared_ptr &toModel) { - return s_overlapProcessor.processPitchModel(contextStart, std::move(fromModel), std::move(toModel)); + return s_overlapProcessor.processPitchEvents(contextStart, + fromModel->getAllEvents(), + toModel->getAllEvents()); } -static EventVector processNoteModel(sv_frame_t contextStart, - std::shared_ptr fromModel, - std::shared_ptr toModel) +static OverlapProcessor::EventPatch processNoteEvents(sv_frame_t contextStart, + const std::shared_ptr &fromModel, + const std::shared_ptr &toModel) { - return s_overlapProcessor.processNoteModel(contextStart, std::move(fromModel), std::move(toModel)); + return s_overlapProcessor.processNoteEvents(contextStart, + fromModel->getAllEvents(), + toModel->getAllEvents()); } static std::map getAnalysisSettingsFromSettings() @@ -401,10 +405,13 @@ RealtimeAnalyser::analyseChunk(Selection sel) ModelById::getAs(safeTargetPitchLayer->getModel()); if (toModel) { - const EventVector points = - processPitchModel(sel.getStartFrame(), fromModel, toModel); + const auto patch = + processPitchEvents(sel.getStartFrame(), fromModel, toModel); - for (const Event &p : points) { + for (const Event &p : patch.remove) { + toModel->remove(p); + } + for (const Event &p : patch.add) { toModel->add(p); } } else { @@ -462,10 +469,13 @@ RealtimeAnalyser::analyseChunk(Selection sel) ModelById::getAs(safeTargetNoteLayer->getModel()); if (toModel) { - const EventVector points = - processNoteModel(sel.getStartFrame(), fromModel, toModel); + const auto patch = + processNoteEvents(sel.getStartFrame(), fromModel, toModel); - for (const Event &p : points) { + for (const Event &p : patch.remove) { + toModel->remove(p); + } + for (const Event &p : patch.add) { toModel->add(p); } } else {