From 17d3204b47ec517cbfd16e38d5a300baa694c3cb Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Fri, 8 Sep 2023 23:16:03 -0300 Subject: [PATCH 01/27] 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 957bfe63..9669057f 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 918d239c..2db3463c 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 cd9fa4af..879ef48a 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 f14d5ca9..da7f6054 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/27] 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 9669057f..44d5d33b 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 2db3463c..b53f4e0a 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 879ef48a..30e7e0ee 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/27] 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 30e7e0ee..b9295cf0 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/27] merge notes --- main/MainWindow.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index b9295cf0..eb4f3ee2 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/27] layer creation comment --- main/Analyser.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 44d5d33b..17db173f 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/27] 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 17db173f..5c011d28 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 b53f4e0a..51dac157 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 eb4f3ee2..0da7485c 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/27] 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 5c011d28..5148c5b7 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 0da7485c..cfead358 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/27] 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 5148c5b7..fd88312a 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 cfead358..45fb44f9 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/27] 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 fd88312a..9d7bb2a5 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 51dac157..3132c8bb 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 45fb44f9..6226fcf2 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/27] 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 9d7bb2a5..9d2a9907 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 3132c8bb..66f9cb3e 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 6226fcf2..6eacc67a 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/27] comments --- main/Analyser.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 9d2a9907..55c15687 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/27] 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 55c15687..df4aea8d 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 66f9cb3e..04c710d2 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 6eacc67a..1b17ed1a 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 da7f6054..21feff62 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/27] 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 1b17ed1a..7046805d 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 21feff62..48e2db27 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/27] 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 df4aea8d..d70da08f 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 04c710d2..1db40b8f 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/27] 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 d70da08f..74678038 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/27] intelligent overlap --- main/Analyser.cpp | 91 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 74678038..463a4b31 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/27] 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 00000000..87240c00 --- /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 00000000..7f0b69b2 --- /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 463a4b31..a7ba1b7e 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/27] 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 87240c00..00000000 --- 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 7f0b69b2..00000000 --- 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 a7ba1b7e..e9b7a3a0 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 00000000..916b73f9 --- /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 00000000..6760eaad --- /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 41c9392d..19eb9d53 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/27] 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 e9b7a3a0..cd17f6fd 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 916b73f9..a21c87e2 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/27] 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 cd17f6fd..6972593a 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 1db40b8f..77c4f48d 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/27] 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 6972593a..8856da37 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 77c4f48d..229f72bc 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/27] 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 8856da37..864c8056 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 229f72bc..c0635843 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/27] 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 864c8056..9b62104e 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 c0635843..0364daa1 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 00000000..97bf496a --- /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 00000000..05ae3689 --- /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 19eb9d53..6512bb56 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/27] 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 a21c87e2..cb41083f 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 6760eaad..d66979be 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 97bf496a..4b161784 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 { From 19192a05312d61562b1b33b4689bc9a9bb70ef68 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 6 Aug 2026 12:30:08 +0200 Subject: [PATCH 25/27] continue --- main/RealtimeAnalyser.cpp | 102 ++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 11 deletions(-) diff --git a/main/RealtimeAnalyser.cpp b/main/RealtimeAnalyser.cpp index 4b161784..c8a10b9b 100644 --- a/main/RealtimeAnalyser.cpp +++ b/main/RealtimeAnalyser.cpp @@ -39,6 +39,26 @@ using namespace sv; // Global overlap processor instance for efficient processing (same as previously in Analyser.cpp) static OverlapProcessor s_overlapProcessor; +static void logPatchFrameRange(const char *label, + const EventVector &events) +{ + if (events.empty()) { + cerr << label << ": empty" << endl; + return; + } + + auto minFrame = events.front().getFrame(); + auto maxFrame = events.front().getFrame() + events.front().getDuration(); + for (const auto &event : events) { + minFrame = std::min(minFrame, event.getFrame()); + maxFrame = std::max(maxFrame, event.getFrame() + event.getDuration()); + } + + cerr << label << ": count=" << events.size() + << " start=" << minFrame + << " end=" << maxFrame << endl; +} + // Wrapper functions static OverlapProcessor::EventPatch processPitchEvents(sv_frame_t contextStart, const std::shared_ptr &fromModel, @@ -168,23 +188,48 @@ RealtimeAnalyser::cleanup() std::vector> layersToClean; QPointer doc; QPointer pane; + std::optional pendingToRestart; + quint64 newGeneration = 0; + bool retiredInFlight = false; { QMutexLocker locker(&m_mutex); layersToClean.swap(m_tempLayers); - // Ensure we never block future work after cleanup - m_inFlight = false; - m_pendingSelection = std::nullopt; + // Invalidate current callbacks and discard any queued follow-up work. + // If we are destroying temp layers for an in-flight run, that run can + // no longer complete naturally, so retire it here. + if (m_inFlight && !layersToClean.empty()) { + retiredInFlight = true; + m_inFlight = false; + pendingToRestart = std::exchange(m_pendingSelection, std::nullopt); + } else { + m_pendingSelection = std::nullopt; + } ++m_generation; + newGeneration = m_generation; doc = m_ctx.document; pane = m_ctx.pane; + + cerr << "RealtimeAnalyser::cleanup: generation=" << newGeneration + << " inFlight=" << m_inFlight + << " retiredInFlight=" << retiredInFlight + << " pending=" << (m_pendingSelection.has_value() ? "yes" : "no") + << " restartPending=" << (pendingToRestart.has_value() ? "yes" : "no") + << " tempLayers=" << layersToClean.size() << endl; } cleanupTempLayers(std::move(layersToClean), doc, pane); + + if (pendingToRestart) { + cerr << "RealtimeAnalyser::cleanup: restarting pending selection after forced retirement" + << " start=" << pendingToRestart->getStartFrame() + << " end=" << pendingToRestart->getEndFrame() << endl; + (void)analyseChunk(*pendingToRestart); + } } void @@ -193,10 +238,14 @@ 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. + // Do not clear m_inFlight here: the owning run must retire itself via + // completion so we don't break the in-flight/pending state machine. ++m_generation; - m_inFlight = false; m_pendingSelection = std::nullopt; + + cerr << "RealtimeAnalyser::invalidateGeneration: generation=" << m_generation + << " inFlight=" << m_inFlight + << " pending=" << (m_pendingSelection.has_value() ? "yes" : "no") << endl; } QString @@ -219,7 +268,10 @@ RealtimeAnalyser::analyseChunk(Selection sel) if (m_inFlight) { m_pendingSelection = sel; - cerr << "RealtimeAnalyser::analyseChunk: already in flight, replacing pending selection" << endl; + cerr << "RealtimeAnalyser::analyseChunk: already in flight, replacing pending selection" + << " generation=" << m_generation + << " pendingStart=" << sel.getStartFrame() + << " pendingEnd=" << sel.getEndFrame() << endl; return ""; } @@ -244,6 +296,11 @@ RealtimeAnalyser::analyseChunk(Selection sel) safeTargetPitchLayer = m_ctx.targetPitchLayer; safeTargetNoteLayer = m_ctx.targetNoteLayer; fileModel = m_ctx.fileModel; + + cerr << "RealtimeAnalyser::analyseChunk: acquired in-flight slot" + << " generation=" << generation + << " start=" << sel.getStartFrame() + << " end=" << sel.getEndFrame() << endl; } waveFileModel = ModelById::getAs(fileModel); @@ -279,9 +336,14 @@ RealtimeAnalyser::analyseChunk(Selection sel) auto state = std::make_shared(); - auto completePart = [this, state](bool canFinish) { + auto completePart = [this, state, generation](bool canFinish) { --state->remainingParts; - if (canFinish && state->remainingParts == 0) { + cerr << "RealtimeAnalyser::completePart: generation=" << generation + << " canFinish=" << canFinish + << " remainingParts=" << state->remainingParts << endl; + if (state->remainingParts == 0) { + cerr << "RealtimeAnalyser::completePart: retiring chunk for generation=" << generation + << " finishAllowed=" << canFinish << endl; finishChunk(); } }; @@ -394,7 +456,8 @@ RealtimeAnalyser::analyseChunk(Selection sel) } if (stale) { - cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale pitch callback from old generation" << endl; + cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale pitch callback from old generation" + << " callbackGeneration=" << generation << endl; cleanupTempLayer(safeTempLayer, safeDocument, safePane); completePart(false); return; @@ -408,6 +471,11 @@ RealtimeAnalyser::analyseChunk(Selection sel) const auto patch = processPitchEvents(sel.getStartFrame(), fromModel, toModel); + cerr << "RealtimeAnalyser::pitchPatch: selectionStart=" << sel.getStartFrame() + << " selectionEnd=" << sel.getEndFrame() << endl; + logPatchFrameRange("RealtimeAnalyser::pitchPatch.remove", patch.remove); + logPatchFrameRange("RealtimeAnalyser::pitchPatch.add", patch.add); + for (const Event &p : patch.remove) { toModel->remove(p); } @@ -423,6 +491,10 @@ RealtimeAnalyser::analyseChunk(Selection sel) cleanupTempLayer(safeTempLayer, safeDocument, safePane); + if (safeTargetPitchLayer && safePane) { + safeTargetPitchLayer->layerParametersChanged(); + safePane->layerParametersChanged(); + } emit layersChanged(); completePart(true); @@ -458,7 +530,8 @@ RealtimeAnalyser::analyseChunk(Selection sel) } if (stale) { - cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale note callback from old generation" << endl; + cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale note callback from old generation" + << " callbackGeneration=" << generation << endl; cleanupTempLayer(safeTempLayer, safeDocument, safePane); completePart(false); return; @@ -550,16 +623,23 @@ void RealtimeAnalyser::finishChunk() { std::optional pending; + quint64 generation = 0; { QMutexLocker locker(&m_mutex); m_inFlight = false; pending = std::exchange(m_pendingSelection, std::nullopt); + generation = m_generation; + + cerr << "RealtimeAnalyser::finishChunk: generation=" << generation + << " pending=" << (pending.has_value() ? "yes" : "no") << endl; } if (pending) { - cerr << "RealtimeAnalyser::finishChunk: starting pending realtime selection" << endl; + cerr << "RealtimeAnalyser::finishChunk: starting pending realtime selection" + << " start=" << pending->getStartFrame() + << " end=" << pending->getEndFrame() << endl; (void)analyseChunk(*pending); } } \ No newline at end of file From e2254d0f6c002144f6f2a1ec12dcef63801ca21e Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 6 Aug 2026 13:22:15 +0200 Subject: [PATCH 26/27] Fix intermittent failures in realtime recording analysis Undo after any realtime chunk was a use-after-free: temp analysis layers are never added to a view, but cleanup called removeLayerFromView() on them, which pushes a RemoveLayerCommand holding a raw Layer* onto the undo stack, and then deleted the layer. Delete directly with deleteLayer(force) instead. This also stops recording filling the undo stack with bogus entries and marking the document dirty. MainWindow's recordCompleted handler used disconnect(sender, &signal, nullptr, nullptr), which removes *all* receivers: it was permanently destroying MainWindowBase's record-duration display and the recordCompleted -> analyseNow() connection. Only the first recording worked. Disconnect by stored Connection handle instead. The runner read the never-written "record-analysis" key rather than the "recording-analysis-mode" the menu writes, so realtime analysis ran regardless of mode, and on completion both it and the full re-analysis raced over the same layers. Route recordCompleted through analyseAfterRecording(), which stands down while the realtime handler is live. m_analysedFrames was only reset in fileClosed(), which recording a second time does not always reach, so the next recording's first chunk started from the previous recording's end frame -- silently swapped by Selection into a large backwards range that wiped the new pitch track. Reset it in newFileLoaded() and refuse empty ranges. Other correctness fixes: - RealtimeAnalyser::cleanup() no longer restarts pending work; every caller is tearing state down, and from a destructor it connected signals to a half-destroyed object. - finishChunk() is generation-aware, so a stale callback can no longer release the in-flight slot while a newer chunk is running. - Pending selections merge rather than replace, so the frames between a running chunk and the newest request are no longer skipped. - Completion handlers also fire if the model is already complete when we connect; otherwise a fast transform left m_inFlight stuck true and realtime analysis silently dead for the session. - processNoteEvents() removes existing notes absorbed into a merge transitively, not just those directly overlapping the incoming ones, which used to leave duplicates stacked under the merged note. - findOverlapGroups() is a sort-and-sweep rather than a rescan on every match, which was cubic over the whole note track once per chunk. - std::max(..., 0LL) would not compile where int64_t is long. - Restore libc++ hardening on macOS. - Decouple "Auto-Analyse New Audio" from the recording mode again. Add test-tony-main covering OverlapProcessor: grouping semantics against a brute-force transitive-closure reference, a scaling guard for the old cubic loop, merging and weighted frequency, and both patch paths including idempotence across repeated overlapping chunks. Co-Authored-By: Claude Opus 5 (1M context) --- main/Analyser.cpp | 29 +- main/MainWindow.cpp | 106 ++++-- main/MainWindow.h | 10 + main/OverlapProcessor.cpp | 216 ++++++----- main/OverlapProcessor.h | 122 +++--- main/RealtimeAnalyser.cpp | 253 ++++++++----- main/RealtimeAnalyser.h | 34 +- main/test/TestOverlapProcessor.h | 618 +++++++++++++++++++++++++++++++ main/test/tony-main-test.cpp | 46 +++ meson.build | 34 +- 10 files changed, 1164 insertions(+), 304 deletions(-) create mode 100644 main/test/TestOverlapProcessor.h create mode 100644 main/test/tony-main-test.cpp diff --git a/main/Analyser.cpp b/main/Analyser.cpp index 9b62104e..634845d6 100644 --- a/main/Analyser.cpp +++ b/main/Analyser.cpp @@ -110,6 +110,14 @@ Analyser::newFileLoaded(Document *doc, ModelId model, m_paneStack = paneStack; m_pane = pane; + // Realtime analysis progress is relative to this model. Recording a + // second time reaches us through here rather than through + // fileClosed(), so without this reset the first chunk of the new + // recording would be analysed from the *previous* recording's end + // frame -- which Selection silently turns into a large backwards + // range, wiping the new pitch track. + m_analysedFrames = 0; + 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); @@ -163,12 +171,20 @@ Analyser::analyseRecordingToEnd(sv_frame_t record_duration) 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); + const sv_frame_t overlap = 2500; + const sv_frame_t startPosition = std::max(m_analysedFrames - overlap, + sv_frame_t(0)); + const sv_frame_t endPosition = record_duration; + + if (endPosition <= startPosition) { + // Nothing new to analyse. Selection would silently swap the two + // frames and hand us a large backwards range, so bail out + // instead of leaving that to be discovered downstream. + return ""; + } - this->analyseRecording(analysingSelection); + QString error = analyseRecording(Selection(startPosition, endPosition)); + if (error != "") return error; m_analysedFrames = endPosition; @@ -226,9 +242,10 @@ Analyser::fileClosed() m_currentAsyncHandle = 0; if (m_realtimeAnalyser) { + // cleanup() already abandons outstanding work and bumps the + // generation, so it is safe to drop the context straight after m_realtimeAnalyser->cleanup(); m_realtimeAnalyser->clearContext(); - m_realtimeAnalyser->invalidateGeneration(); } m_layers.clear(); diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp index 7046805d..bea4780d 100644 --- a/main/MainWindow.cpp +++ b/main/MainWindow.cpp @@ -118,6 +118,9 @@ MainWindow::MainWindow(AudioMode audioMode, m_ffwdAction(0), m_rwdAction(0), m_intelligentActionOn(true), //GF: !!! temporary + m_autoAnalyse(nullptr), + m_analyseDuringRecord(nullptr), + m_analyseAfterRecord(nullptr), m_activityLog(new ActivityLog()), m_keyReference(new KeyReference()), m_selectionAnchor(0), @@ -843,7 +846,9 @@ MainWindow::resetAnalyseOptions() QSettings settings; settings.beginGroup("Analyser"); - setRecordingAnalysisMode(RecordingAnalysisMode::AfterRecording); + settings.setValue("auto-analysis", true); + settings.setValue("recording-analysis-mode", + static_cast(RecordingAnalysisMode::AfterRecording)); auto keyMap = Analyser::getAnalysisSettings(); for (auto p: keyMap) { @@ -883,22 +888,24 @@ MainWindow::updateAnalyseStates() // 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; } + // Auto-analysis of newly opened files is independent of the + // recording analysis mode: newFileLoaded() reads this key directly, + // so deriving the checkbox from the mode instead would leave the two + // permanently out of step. + m_autoAnalyse->setChecked(settings.value("auto-analysis", true).toBool()); + std::map actions { { "precision-analysis", m_precise }, { "lowamp-analysis", m_lowamp }, @@ -1159,12 +1166,12 @@ MainWindow::setupToolbars() connect(recordAction, SIGNAL(triggered()), this, SLOT(record())); connect(recordAction, SIGNAL(triggered()), - this, SLOT(analyseDuringRecordingRunner())); + this, SLOT(analyseDuringRecordingRunner())); connect(m_recordTarget, SIGNAL(recordStatusChanged(bool)), recordAction, SLOT(setChecked(bool))); connect(m_recordTarget, SIGNAL(recordCompleted()), - this, SLOT(analyseNow())); + this, SLOT(analyseAfterRecording())); connect(this, SIGNAL(canRecord(bool)), recordAction, SLOT(setEnabled(bool))); @@ -3130,28 +3137,85 @@ MainWindow::analyseNow() } } +void +MainWindow::disconnectRecordingAnalysis() +{ + if (m_recordDurationConnection) { + disconnect(m_recordDurationConnection); + m_recordDurationConnection = QMetaObject::Connection(); + } + if (m_recordCompletedConnection) { + disconnect(m_recordCompletedConnection); + m_recordCompletedConnection = QMetaObject::Connection(); + } +} + 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(); - }); + if (!m_recordTarget) return; + if (!m_recordTarget->isRecording()) return; + + if (getRecordingAnalysisMode() != RecordingAnalysisMode::DuringRecording) { + return; } + + // Defensive: the record action can only be triggered once per + // recording, but re-running would otherwise stack up duplicate + // connections + disconnectRecordingAnalysis(); + + m_recordDurationConnection = + connect(m_recordTarget, &AudioCallbackRecordTarget::recordDurationChanged, + this, &MainWindow::analyseDuringRecording); + + m_recordCompletedConnection = + connect(m_recordTarget, &AudioCallbackRecordTarget::recordCompleted, + this, [this]() { + // Only our own two connections, by handle. Passing + // nullptr receivers to disconnect() here used to take + // out MainWindowBase's record-duration display and + // the recordCompleted -> analyseAfterRecording() + // connection along with them, for the rest of the + // session. + disconnectRecordingAnalysis(); + analyseDuringRecording(); + }); } void MainWindow::analyseDuringRecording() { - m_analyser->analyseRecordingToEnd(m_recordTarget->getRecordDuration()); + if (!m_recordTarget || !m_analyser) return; + + QString error = m_analyser->analyseRecordingToEnd + (m_recordTarget->getRecordDuration()); + + // This runs several times a second while recording, so report + // failures to the log rather than to a modal dialog + if (error != "") { + SVCERR << "MainWindow::analyseDuringRecording: " << error << endl; + } +} + +void +MainWindow::analyseAfterRecording() +{ + // If realtime analysis is running, the final chunk is analysed by + // the recordCompleted handler installed in + // analyseDuringRecordingRunner(). Running a full re-analysis as well + // would replace the pitch and note layers out from under the chunk + // being scheduled at the same moment. + // + // This tests the live connection rather than re-reading the mode, + // so that switching mode part-way through a recording can't leave + // both paths active. We run before that handler does (we were + // connected first), so the connection is still valid here. + if (m_recordCompletedConnection) { + return; + } + + analyseNow(); } void diff --git a/main/MainWindow.h b/main/MainWindow.h index 48e2db27..82bc43b9 100644 --- a/main/MainWindow.h +++ b/main/MainWindow.h @@ -96,6 +96,7 @@ protected slots: virtual void analyseDuringRecording(); virtual void analyseDuringRecordingRunner(); + virtual void analyseAfterRecording(); virtual void resetAnalyseOptions(); virtual void recordAnalysisToggled(); virtual void analyseAfterRecordToggled(); @@ -264,6 +265,15 @@ protected slots: RecordingAnalysisMode getRecordingAnalysisMode() const; void setRecordingAnalysisMode(RecordingAnalysisMode mode); + // Tear down only the connections analyseDuringRecordingRunner() + // made. A blanket disconnect on m_recordTarget's signals would also + // remove MainWindowBase's record-duration handler and our own + // recordCompleted handler, permanently. + void disconnectRecordingAnalysis(); + + QMetaObject::Connection m_recordDurationConnection; + QMetaObject::Connection m_recordCompletedConnection; + virtual void octaveShift(bool up); virtual void auxSnapNotes(sv::Selection s); diff --git a/main/OverlapProcessor.cpp b/main/OverlapProcessor.cpp index cb41083f..291110a4 100644 --- a/main/OverlapProcessor.cpp +++ b/main/OverlapProcessor.cpp @@ -14,8 +14,12 @@ */ #include "OverlapProcessor.h" + #include #include +#include + +using namespace sv; namespace { @@ -57,13 +61,14 @@ void sortAndDedupeEvents(EventVector &events) 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); - }); + EventVector shifted; + shifted.reserve(events.size()); + for (const auto &event : events) { + shifted.push_back(event.withFrame(event.getFrame() + offset)); + } return shifted; } + } // OverlapGroup implementation @@ -97,40 +102,49 @@ OverlapProcessor::OverlapProcessor(const OverlapConfig &config) : { } -std::vector OverlapProcessor::findOverlapGroups(const EventVector& events) const { +std::vector +OverlapProcessor::findOverlapGroups(const EventVector &events) const +{ std::vector groups; - std::vector processed(events.size(), 0); - - for (size_t i = 0; i < events.size(); ++i) { - if (processed[i]) continue; - - 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; - - for (size_t groupIndex : currentGroup.indices) { - if (eventsOverlap(events[j], events[groupIndex])) { - currentGroup.addEvent(j, events[j]); - processed[j] = true; - foundOverlap = true; - break; - } - } - if (foundOverlap) break; - } - } while (foundOverlap); + if (events.size() < 2) return groups; + + // The connected components of an interval overlap graph are + // contiguous once the intervals are sorted by start frame, so a + // single sweep tracking the running maximum end frame finds them + // all. (The previous implementation rescanned the whole event set + // after every match, which is cubic in the worst case and was being + // run over the entire note track once per realtime chunk.) + + std::vector order(events.size()); + std::iota(order.begin(), order.end(), size_t(0)); + + std::sort(order.begin(), order.end(), + [&events](size_t a, size_t b) { + if (events[a].getFrame() != events[b].getFrame()) { + return events[a].getFrame() < events[b].getFrame(); + } + return a < b; + }); - if (currentGroup.size() > 1) { - groups.push_back(currentGroup); + OverlapGroup current(order[0], events[order[0]]); + + for (size_t i = 1; i < order.size(); ++i) { + + const size_t index = order[i]; + const Event &event = events[index]; + + // Equivalent to eventsOverlap() against some group member, + // given that no later event can start before this one does. + if (event.getFrame() < current.endFrame) { + current.addEvent(index, event); + } else { + if (current.size() > 1) groups.push_back(current); + current = OverlapGroup(index, event); } } + if (current.size() > 1) groups.push_back(current); + return groups; } @@ -176,7 +190,7 @@ float OverlapProcessor::calculateWeightedFrequency(const EventVector& overlappin for (float freq : frequencies) { logSum += std::log(freq); } - return std::exp(logSum / frequencies.size()); + return float(std::exp(logSum / double(frequencies.size()))); } for (double& weight : weights) weight /= totalWeight; @@ -186,7 +200,7 @@ float OverlapProcessor::calculateWeightedFrequency(const EventVector& overlappin weightedLogSum += std::log(frequencies[i]) * weights[i]; } - return std::exp(weightedLogSum); + return float(std::exp(weightedLogSum)); } std::optional OverlapProcessor::mergeOverlapGroup(const OverlapGroup& group, const EventVector& events) const { @@ -240,17 +254,24 @@ OverlapProcessor::EventPatch OverlapProcessor::processPitchEvents(sv_frame_t con EventPatch patch; EventVector shiftedIncoming = shiftedBy(incomingEvents, contextStart); - EventVector remainingEvents; + // Everything from contextStart onwards is superseded by the fresh + // analysis. We need the latest-ending event before contextStart in + // order to bridge the seam, but not a copy of the whole preceding + // track -- this runs once per realtime chunk over the entire pitch + // track, which grows for the length of the recording. + const Event *lastBefore = nullptr; + patch.remove.reserve(existingEvents.size()); - remainingEvents.reserve(existingEvents.size()); 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); + } else if (!lastBefore || + eventEnd > lastBefore->getFrame() + lastBefore->getDuration()) { + lastBefore = &event; } } @@ -258,17 +279,17 @@ OverlapProcessor::EventPatch OverlapProcessor::processPitchEvents(sv_frame_t con patch.add = shiftedIncoming; - if (!remainingEvents.empty() && !shiftedIncoming.empty()) { - const auto& lastExistingEvent = remainingEvents.back(); + if (lastBefore && !shiftedIncoming.empty()) { + const auto& firstNewEvent = shiftedIncoming.front(); const auto lastExistingEnd = - lastExistingEvent.getFrame() + lastExistingEvent.getDuration(); + lastBefore->getFrame() + lastBefore->getDuration(); const auto gapFrames = firstNewEvent.getFrame() - lastExistingEnd; if (gapFrames > 0 && gapFrames <= m_config.interpolationThreshold) { - if (lastExistingEvent.hasValue() && firstNewEvent.hasValue()) { - const auto lastValue = lastExistingEvent.getValue(); + if (lastBefore->hasValue() && firstNewEvent.hasValue()) { + const auto lastValue = lastBefore->getValue(); const auto firstValue = firstNewEvent.getValue(); if (lastValue > 0.0f && firstValue > 0.0f) { @@ -293,67 +314,62 @@ OverlapProcessor::EventPatch OverlapProcessor::processNoteEvents(sv_frame_t cont const EventVector& incomingEvents, const EventVector& existingEvents) const { EventPatch patch; - EventVector shiftedIncoming = shiftedBy(incomingEvents, contextStart); - EventVector remainingEvents; - categorizeEvents(existingEvents, shiftedIncoming, patch.remove, remainingEvents); - sortAndDedupeEvents(patch.remove); + const EventVector shiftedIncoming = shiftedBy(incomingEvents, contextStart); + if (shiftedIncoming.empty()) return patch; - EventVector combinedEvents; - 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()); + // Single index space: [0, existingCount) are existing events, the + // rest are incoming ones. Overlap grouping is transitive, so an + // existing note can be pulled into a merge by way of another + // existing note even when it does not itself overlap anything + // incoming. It is still superseded by the merged event and must be + // removed, or it would be left behind as a duplicate underneath it. + const size_t existingCount = existingEvents.size(); - const auto overlapGroups = findOverlapGroups(combinedEvents); + EventVector combined; + combined.reserve(existingCount + shiftedIncoming.size()); + combined.insert(combined.end(), existingEvents.begin(), existingEvents.end()); + combined.insert(combined.end(), shiftedIncoming.begin(), shiftedIncoming.end()); - EventVector finalEvents; - finalEvents.reserve(combinedEvents.size()); - std::vector processed(combinedEvents.size(), 0); + const auto groups = findOverlapGroups(combined); - for (const auto& group : overlapGroups) { - if (auto merged = mergeOverlapGroup(group, combinedEvents)) { - finalEvents.push_back(*merged); - } + std::vector grouped(combined.size(), 0); + for (const auto& group : groups) { + + bool touchesIncoming = false; for (size_t index : group.indices) { - if (index < processed.size()) { - processed[index] = true; - } + grouped[index] = 1; + if (index >= existingCount) touchesIncoming = true; } - } - for (size_t i = 0; i < combinedEvents.size(); ++i) { - if (!processed[i]) { - finalEvents.push_back(combinedEvents[i]); + if (!touchesIncoming) { + // Pre-existing notes that overlap only each other, outside + // the region we just analysed: leave them exactly as they + // are. + continue; } - } - - patch.add.reserve(finalEvents.size()); - for (const auto& event : finalEvents) { - bool include = false; - for (const auto& originalNew : shiftedIncoming) { - if (eventsOverlap(event, originalNew)) { - include = true; - break; + for (size_t index : group.indices) { + if (index < existingCount) { + patch.remove.push_back(combined[index]); } } - if (!include) { - for (const auto& removedEvent : patch.remove) { - if (eventsOverlap(event, removedEvent)) { - include = true; - break; - } - } + if (auto merged = mergeOverlapGroup(group, combined)) { + patch.add.push_back(*merged); } + } - if (include) { - patch.add.push_back(event); - } + // Incoming notes that overlapped nothing at all + for (size_t i = existingCount; i < combined.size(); ++i) { + if (!grouped[i]) patch.add.push_back(combined[i]); } + // Groups are disjoint, so remove holds each existing event at most + // once already; sort for determinism but don't dedupe, so that a + // model containing genuine duplicates has both instances removed. + std::sort(patch.remove.begin(), patch.remove.end(), lessEventForDedup); sortAndDedupeEvents(patch.add); return patch; @@ -383,27 +399,3 @@ const Event* OverlapProcessor::findLongestEvent(const OverlapGroup& group, const } return longestEvent; } - -void OverlapProcessor::categorizeEvents(const EventVector& allEvents, - const EventVector& newEvents, - EventVector& eventsToRemove, - EventVector& remainingEvents) const { - eventsToRemove.reserve(allEvents.size()); - remainingEvents.reserve(allEvents.size()); - - 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 index d66979be..02d7bf80 100644 --- a/main/OverlapProcessor.h +++ b/main/OverlapProcessor.h @@ -20,89 +20,111 @@ #include #include +#include "base/BaseTypes.h" #include "base/Event.h" -namespace sv { - typedef std::vector EventVector; - 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 - + sv::sv_frame_t interpolationThreshold = 512; // ~11ms at 44.1kHz + double pitchSimilarityThreshold = 0.1; // 10% pitch difference threshold + OverlapConfig() = default; - OverlapConfig(sv_frame_t interpThresh, double pitchThresh, sv_frame_t tolerance) + OverlapConfig(sv::sv_frame_t interpThresh, double pitchThresh) : interpolationThreshold(interpThresh) - , pitchSimilarityThreshold(pitchThresh) - , overlapTolerance(tolerance) {} + , pitchSimilarityThreshold(pitchThresh) {} }; /** - * Structure to represent a group of overlapping events + * Structure to represent a group of overlapping events. The indices + * refer to positions within the event vector the group was derived + * from; startFrame and endFrame span the whole group. */ struct OverlapGroup { std::vector indices; - sv_frame_t startFrame; - sv_frame_t endFrame; + sv::sv_frame_t startFrame; + sv::sv_frame_t endFrame; OverlapGroup(); - explicit OverlapGroup(size_t index, const Event &event); + explicit OverlapGroup(size_t index, const sv::Event &event); bool isEmpty() const { return indices.empty(); } size_t size() const { return indices.size(); } - void addEvent(size_t index, const Event &event); + void addEvent(size_t index, const sv::Event &event); }; /** - * Main overlap processing class that handles detection and merging of overlapping events + * Main overlap processing class that handles detection and merging of + * overlapping events. + * + * The process*Events methods are pure functions of their inputs: they + * return the set of events to remove from, and add to, the target + * model, without touching any model themselves. */ class OverlapProcessor { public: explicit OverlapProcessor(const OverlapConfig& config = OverlapConfig()); - - // Core overlap detection and processing + struct EventPatch { - EventVector remove; - EventVector add; + sv::EventVector remove; + sv::EventVector add; }; - std::vector findOverlapGroups(const EventVector& events) 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; - - // Main processing methods for different model types - 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 + /** + * Return the connected components of the overlap graph over the + * given events, omitting any component with only one member. Two + * events overlap if their half-open frame ranges intersect; + * grouping is transitive, so A-B and B-C yields one group {A,B,C}. + */ + std::vector findOverlapGroups(const sv::EventVector& events) const; + + /** + * Collapse a group into a single event spanning the whole group, + * taking its label and level from the longest member and its value + * from the duration-weighted geometric mean of the members. + */ + std::optional mergeOverlapGroup(const OverlapGroup& group, + const sv::EventVector& events) const; + + float calculateWeightedFrequency(const sv::EventVector& overlappingEvents, + sv::sv_frame_t overlapStart, + sv::sv_frame_t overlapDuration) const; + + /** + * Produce the patch that replaces the pitch track from + * contextStart onwards with incomingEvents (whose frames are + * relative to contextStart). + */ + EventPatch processPitchEvents(sv::sv_frame_t contextStart, + const sv::EventVector& incomingEvents, + const sv::EventVector& existingEvents) const; + + /** + * Produce the patch that merges incomingEvents (whose frames are + * relative to contextStart) into existingEvents. Existing notes + * absorbed into a merge are included in the remove list, and + * existing notes untouched by the incoming ones are left alone. + */ + EventPatch processNoteEvents(sv::sv_frame_t contextStart, + const sv::EventVector& incomingEvents, + const sv::EventVector& existingEvents) const; + + /** + * The pairwise overlap predicate that grouping is the transitive + * closure of: true if the half-open ranges [frame, frame+duration) + * of a and b intersect. Events that merely touch end-to-end do not + * overlap. + */ + bool eventsOverlap(const sv::Event& a, const sv::Event& b) const; + 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 OverlapGroup& group, const EventVector& events) const; - void categorizeEvents(const EventVector& allEvents, - const EventVector& newEvents, - EventVector& eventsToRemove, - EventVector& remainingEvents) const; + + const sv::Event* findLongestEvent(const OverlapGroup& group, + const sv::EventVector& events) const; }; #endif // OVERLAP_PROCESSOR_H diff --git a/main/RealtimeAnalyser.cpp b/main/RealtimeAnalyser.cpp index c8a10b9b..36626173 100644 --- a/main/RealtimeAnalyser.cpp +++ b/main/RealtimeAnalyser.cpp @@ -10,6 +10,7 @@ #include "OverlapProcessor.h" #include +#include #include #include #include @@ -187,49 +188,40 @@ RealtimeAnalyser::cleanup() { std::vector> layersToClean; QPointer doc; - QPointer pane; - std::optional pendingToRestart; quint64 newGeneration = 0; - bool retiredInFlight = false; + bool wasInFlight = false; + bool hadPending = false; { QMutexLocker locker(&m_mutex); layersToClean.swap(m_tempLayers); - // Invalidate current callbacks and discard any queued follow-up work. - // If we are destroying temp layers for an in-flight run, that run can - // no longer complete naturally, so retire it here. - if (m_inFlight && !layersToClean.empty()) { - retiredInFlight = true; - m_inFlight = false; - pendingToRestart = std::exchange(m_pendingSelection, std::nullopt); - } else { - m_pendingSelection = std::nullopt; - } + wasInFlight = m_inFlight; + hadPending = m_pendingSelection.has_value(); + + // Every caller of cleanup() is reacting to the ground moving -- + // the document closing, a new file loading, or the target layers + // being replaced. Restarting the queued chunk here would schedule + // work against state that is being torn down (and, from the + // destructor, would connect signals to a half-destroyed object), + // so all outstanding work is simply abandoned. Bumping the + // generation makes any callback already queued for delivery a + // no-op. + m_inFlight = false; + m_pendingSelection = std::nullopt; - ++m_generation; - newGeneration = m_generation; + newGeneration = ++m_generation; doc = m_ctx.document; - pane = m_ctx.pane; cerr << "RealtimeAnalyser::cleanup: generation=" << newGeneration - << " inFlight=" << m_inFlight - << " retiredInFlight=" << retiredInFlight - << " pending=" << (m_pendingSelection.has_value() ? "yes" : "no") - << " restartPending=" << (pendingToRestart.has_value() ? "yes" : "no") + << " wasInFlight=" << wasInFlight + << " hadPending=" << hadPending << " tempLayers=" << layersToClean.size() << endl; } - cleanupTempLayers(std::move(layersToClean), doc, pane); - - if (pendingToRestart) { - cerr << "RealtimeAnalyser::cleanup: restarting pending selection after forced retirement" - << " start=" << pendingToRestart->getStartFrame() - << " end=" << pendingToRestart->getEndFrame() << endl; - (void)analyseChunk(*pendingToRestart); - } + deleteTempLayers(std::move(layersToClean), doc); } void @@ -244,8 +236,7 @@ RealtimeAnalyser::invalidateGeneration() m_pendingSelection = std::nullopt; cerr << "RealtimeAnalyser::invalidateGeneration: generation=" << m_generation - << " inFlight=" << m_inFlight - << " pending=" << (m_pendingSelection.has_value() ? "yes" : "no") << endl; + << " inFlight=" << m_inFlight << endl; } QString @@ -267,11 +258,26 @@ RealtimeAnalyser::analyseChunk(Selection sel) QMutexLocker locker(&m_mutex); if (m_inFlight) { - m_pendingSelection = sel; - cerr << "RealtimeAnalyser::analyseChunk: already in flight, replacing pending selection" + + // Merge rather than replace: recordDurationChanged fires + // several times during a single chunk, and simply keeping + // the newest selection would leave the frames between the + // running chunk's end and the newest chunk's start + // permanently unanalysed. + if (m_pendingSelection) { + m_pendingSelection = Selection + (std::min(m_pendingSelection->getStartFrame(), + sel.getStartFrame()), + std::max(m_pendingSelection->getEndFrame(), + sel.getEndFrame())); + } else { + m_pendingSelection = sel; + } + + cerr << "RealtimeAnalyser::analyseChunk: already in flight, merged into pending selection" << " generation=" << m_generation - << " pendingStart=" << sel.getStartFrame() - << " pendingEnd=" << sel.getEndFrame() << endl; + << " pendingStart=" << m_pendingSelection->getStartFrame() + << " pendingEnd=" << m_pendingSelection->getEndFrame() << endl; return ""; } @@ -303,21 +309,20 @@ RealtimeAnalyser::analyseChunk(Selection sel) << " end=" << sel.getEndFrame() << endl; } + auto finishIfStarted = [this, startedChunk, generation]() { + if (startedChunk) { + finishChunk(generation); + } + }; + waveFileModel = ModelById::getAs(fileModel); if (!waveFileModel) { - if (startedChunk) finishChunk(); + finishIfStarted(); 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) { + QPointer doc) { Layer *layerToDelete = safeTempLayer.data(); if (!layerToDelete) return; @@ -326,11 +331,13 @@ RealtimeAnalyser::analyseChunk(Selection sel) untrackTempLayerLocked(layerToDelete); } - if (doc && pane) { - doc->removeLayerFromView(pane.data(), layerToDelete); - if (safeTempLayer) { - doc->deleteLayer(layerToDelete); - } + // Temp layers are never added to a view, so removeLayerFromView() + // must not be used here: it would push an undoable + // RemoveLayerCommand holding a raw pointer to a layer we are + // about to delete, leaving the undo stack full of commands that + // dereference freed memory on undo. Delete directly instead. + if (doc) { + doc->deleteLayer(layerToDelete, true); } }; @@ -344,7 +351,43 @@ RealtimeAnalyser::analyseChunk(Selection sel) if (state->remainingParts == 0) { cerr << "RealtimeAnalyser::completePart: retiring chunk for generation=" << generation << " finishAllowed=" << canFinish << endl; - finishChunk(); + finishChunk(generation); + } + }; + + // Registers a completion handler for one temp layer. The handler + // runs at most once, whether it is reached through the layer's + // signal or through the immediate check below -- if the transform + // has already finished by the time we get here the signal has been + // and gone, and without the check nothing would ever retire the + // chunk, leaving m_inFlight stuck true and realtime analysis + // silently dead for the rest of the session. + auto registerPart = [this, state](Layer *layer, + std::function handler) { + + ++state->remainingParts; + + auto done = std::make_shared(false); + auto once = [done, handler](ModelId modelId) { + if (*done) return; + *done = true; + handler(modelId); + }; + + QObject::connect(layer, &Layer::modelCompletionChanged, this, + [once](ModelId modelId) { + auto model = ModelById::get(modelId); + if (!model || model->getCompletion() != 100) return; + once(modelId); + }, + Qt::QueuedConnection); + + const ModelId modelId = layer->getModel(); + auto model = ModelById::get(modelId); + if (model && model->getCompletion() == 100) { + QMetaObject::invokeMethod(this, + [once, modelId]() { once(modelId); }, + Qt::QueuedConnection); } }; @@ -426,24 +469,25 @@ RealtimeAnalyser::analyseChunk(Selection sel) ColourDatabase *cdb = ColourDatabase::getInstance(); + std::vector unrecognised; + for (auto *layer : layers) { if (auto *tempPitchLayer = qobject_cast(layer)) { - ++state->remainingParts; tempPitchLayer->setBaseColour(cdb->getColourIndex(tr("Black"))); - QPointer safeTempLayer(tempPitchLayer); + QPointer safeTempLayer(tempPitchLayer); - QObject::connect( - tempPitchLayer, - &TimeValueLayer::modelCompletionChanged, - this, - [this, safeTempLayer, safeTargetPitchLayer, safeDocument, safePane, - sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { + registerPart + (tempPitchLayer, + [this, safeTempLayer, safeTargetPitchLayer, safeDocument, safePane, + sel, generation, cleanupTempLayer, completePart](ModelId modelId) { const auto fromModel = ModelById::getAs(modelId); - if (!fromModel || fromModel->getCompletion() != 100) { + if (!fromModel) { + cleanupTempLayer(safeTempLayer, safeDocument); + completePart(false); return; } @@ -458,7 +502,7 @@ RealtimeAnalyser::analyseChunk(Selection sel) if (stale) { cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale pitch callback from old generation" << " callbackGeneration=" << generation << endl; - cleanupTempLayer(safeTempLayer, safeDocument, safePane); + cleanupTempLayer(safeTempLayer, safeDocument); completePart(false); return; } @@ -489,7 +533,7 @@ RealtimeAnalyser::analyseChunk(Selection sel) cerr << "WARNING: RealtimeAnalyser pitch callback - target layer deleted" << endl; } - cleanupTempLayer(safeTempLayer, safeDocument, safePane); + cleanupTempLayer(safeTempLayer, safeDocument); if (safeTargetPitchLayer && safePane) { safeTargetPitchLayer->layerParametersChanged(); @@ -498,26 +542,23 @@ RealtimeAnalyser::analyseChunk(Selection sel) emit layersChanged(); completePart(true); - }, - Qt::QueuedConnection); - } + }); - if (auto *tempNoteLayer = qobject_cast(layer)) { + } else if (auto *tempNoteLayer = qobject_cast(layer)) { - ++state->remainingParts; tempNoteLayer->setBaseColour(cdb->getColourIndex(tr("Bright Blue"))); - QPointer safeTempLayer(tempNoteLayer); + QPointer safeTempLayer(tempNoteLayer); - QObject::connect( - tempNoteLayer, - &FlexiNoteLayer::modelCompletionChanged, - this, - [this, safeTempLayer, safeTargetNoteLayer, safeDocument, safePane, - sel, state, generation, cleanupTempLayer, completePart](ModelId modelId) { + registerPart + (tempNoteLayer, + [this, safeTempLayer, safeTargetNoteLayer, safeDocument, + sel, generation, cleanupTempLayer, completePart](ModelId modelId) { const auto fromModel = ModelById::getAs(modelId); - if (!fromModel || fromModel->getCompletion() != 100) { + if (!fromModel) { + cleanupTempLayer(safeTempLayer, safeDocument); + completePart(false); return; } @@ -532,7 +573,7 @@ RealtimeAnalyser::analyseChunk(Selection sel) if (stale) { cerr << "RealtimeAnalyser::analyseChunk: Ignoring stale note callback from old generation" << " callbackGeneration=" << generation << endl; - cleanupTempLayer(safeTempLayer, safeDocument, safePane); + cleanupTempLayer(safeTempLayer, safeDocument); completePart(false); return; } @@ -545,6 +586,9 @@ RealtimeAnalyser::analyseChunk(Selection sel) const auto patch = processNoteEvents(sel.getStartFrame(), fromModel, toModel); + logPatchFrameRange("RealtimeAnalyser::notePatch.remove", patch.remove); + logPatchFrameRange("RealtimeAnalyser::notePatch.add", patch.add); + for (const Event &p : patch.remove) { toModel->remove(p); } @@ -558,16 +602,25 @@ RealtimeAnalyser::analyseChunk(Selection sel) cerr << "WARNING: RealtimeAnalyser note callback - target layer deleted" << endl; } - cleanupTempLayer(safeTempLayer, safeDocument, safePane); + cleanupTempLayer(safeTempLayer, safeDocument); emit layersChanged(); completePart(true); - }, - Qt::QueuedConnection); + }); + + } else { + unrecognised.push_back(layer); } } + // Anything we can't drive to completion would otherwise sit in + // m_tempLayers until the next cleanup() + for (auto *layer : unrecognised) { + cerr << "WARNING: RealtimeAnalyser::analyseChunk: discarding unrecognised temp layer" << endl; + cleanupTempLayer(QPointer(layer), safeDocument); + } + if (state->remainingParts == 0) { cerr << "WARNING: RealtimeAnalyser::analyseChunk: no recognised temp layers created" << endl; finishIfStarted(); @@ -576,21 +629,6 @@ RealtimeAnalyser::analyseChunk(Selection sel) 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) { @@ -604,33 +642,44 @@ RealtimeAnalyser::untrackTempLayerLocked(Layer *layer) } void -RealtimeAnalyser::cleanupTempLayers(std::vector> layersToClean, - const QPointer &doc, - const QPointer &pane) +RealtimeAnalyser::deleteTempLayers(std::vector> layersToClean, + const QPointer &doc) { - if (!doc || !pane) return; + // If the document has already gone it owns and destroys its layers, + // so there is nothing left for us to release. + if (!doc) return; for (const auto &layerPtr : layersToClean) { Layer *layer = layerPtr.data(); if (!layer) continue; - doc->removeLayerFromView(pane.data(), layer); - doc->deleteLayer(layer); + // See cleanupTempLayer(): these layers were never added to a + // view, so they must not go through removeLayerFromView(). + doc->deleteLayer(layer, true); } } void -RealtimeAnalyser::finishChunk() +RealtimeAnalyser::finishChunk(quint64 generation) { std::optional pending; - quint64 generation = 0; { QMutexLocker locker(&m_mutex); + if (generation != m_generation) { + // A cleanup() or invalidateGeneration() has superseded this + // chunk, so it no longer owns the in-flight slot. Clearing + // m_inFlight here would release a slot that a newer chunk + // may already have taken, letting two chunks write to the + // same models at once. + cerr << "RealtimeAnalyser::finishChunk: ignoring retirement of stale generation=" + << generation << " current=" << m_generation << endl; + return; + } + m_inFlight = false; pending = std::exchange(m_pendingSelection, std::nullopt); - generation = m_generation; cerr << "RealtimeAnalyser::finishChunk: generation=" << generation << " pending=" << (pending.has_value() ? "yes" : "no") << endl; @@ -642,4 +691,4 @@ RealtimeAnalyser::finishChunk() << " end=" << pending->getEndFrame() << endl; (void)analyseChunk(*pending); } -} \ No newline at end of file +} diff --git a/main/RealtimeAnalyser.h b/main/RealtimeAnalyser.h index 05ae3689..a75b1a98 100644 --- a/main/RealtimeAnalyser.h +++ b/main/RealtimeAnalyser.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -43,13 +44,28 @@ class RealtimeAnalyser : public QObject void clearContext(); - // Cancels in-flight work, clears pending selection, removes temp layers, bumps generation + /** + * Abandon all in-flight and pending work, delete any temporary + * layers, and bump the generation so that callbacks still queued + * for delivery are ignored. Never starts new work, so it is safe to + * call from a destructor or while tearing down a document. + */ void cleanup(); - // Ignore stale callbacks after external state changes; does not delete layers + /** + * Ignore stale callbacks after external state changes, and drop any + * pending follow-up chunk. Does not delete layers, and does not + * retire the in-flight chunk: that chunk still owns the in-flight + * slot and must retire itself through completion. + */ void invalidateGeneration(); - // One-in-flight realtime analysis: if a chunk is already running, replaces the pending selection + /** + * One-in-flight realtime analysis. If a chunk is already running, + * the selection is merged into the pending one -- rather than + * replacing it, which would drop the region between them -- and is + * analysed when the running chunk retires. + */ QString analyseChunk(sv::Selection sel); signals: @@ -73,15 +89,11 @@ class RealtimeAnalyser : public QObject 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 deleteTempLayers(std::vector> layersToClean, + const QPointer &doc); - void finishChunk(); + void finishChunk(quint64 generation); private: mutable QMutex m_mutex; @@ -93,4 +105,4 @@ class RealtimeAnalyser : public QObject quint64 m_generation = 0; }; -#endif \ No newline at end of file +#endif diff --git a/main/test/TestOverlapProcessor.h b/main/test/TestOverlapProcessor.h new file mode 100644 index 00000000..eb562eba --- /dev/null +++ b/main/test/TestOverlapProcessor.h @@ -0,0 +1,618 @@ +/* -*- 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 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 TEST_OVERLAP_PROCESSOR_H +#define TEST_OVERLAP_PROCESSOR_H + +#include "../OverlapProcessor.h" + +#include +#include + +#include +#include +#include +#include + +using namespace sv; + +class TestOverlapProcessor : public QObject +{ + Q_OBJECT + +private: + static Event note(sv_frame_t frame, sv_frame_t duration, float value) { + return Event(frame, value, duration, QString()); + } + + static Event pitch(sv_frame_t frame, float value) { + return Event(frame, value, QString()); + } + + /** Apply a patch the way RealtimeAnalyser does: remove first, then + * add. Removing an event drops a single matching instance, as + * EventSeries::remove does. */ + static EventVector applyPatch(EventVector model, + const OverlapProcessor::EventPatch &patch) { + for (const auto &e: patch.remove) { + auto itr = std::find(model.begin(), model.end(), e); + if (itr != model.end()) model.erase(itr); + } + for (const auto &e: patch.add) { + model.push_back(e); + } + std::sort(model.begin(), model.end()); + return model; + } + + /** Brute-force reference implementation of grouping: the transitive + * closure of the pairwise overlap predicate. */ + static std::vector> + referenceGroups(const OverlapProcessor &p, const EventVector &events) { + + const size_t n = events.size(); + std::vector component(n); + for (size_t i = 0; i < n; ++i) component[i] = i; + + bool changed = true; + while (changed) { + changed = false; + for (size_t i = 0; i < n; ++i) { + for (size_t j = i + 1; j < n; ++j) { + if (!p.eventsOverlap(events[i], events[j])) continue; + const size_t a = std::min(component[i], component[j]); + const size_t b = std::max(component[i], component[j]); + if (a == b) continue; + for (size_t k = 0; k < n; ++k) { + if (component[k] == b) component[k] = a; + } + changed = true; + } + } + } + + std::vector> groups; + for (size_t root = 0; root < n; ++root) { + std::set members; + for (size_t k = 0; k < n; ++k) { + if (component[k] == root) members.insert(k); + } + if (members.size() > 1) groups.push_back(members); + } + return groups; + } + + static std::vector> + actualGroups(const OverlapProcessor &p, const EventVector &events) { + std::vector> groups; + for (const auto &g: p.findOverlapGroups(events)) { + groups.push_back(std::set(g.indices.begin(), + g.indices.end())); + } + std::sort(groups.begin(), groups.end()); + return groups; + } + + /** True if any two distinct events in the vector overlap. */ + static bool hasAnyOverlap(const OverlapProcessor &p, + const EventVector &events) { + for (size_t i = 0; i < events.size(); ++i) { + for (size_t j = i + 1; j < events.size(); ++j) { + if (p.eventsOverlap(events[i], events[j])) return true; + } + } + return false; + } + +private slots: + + // ---- findOverlapGroups ------------------------------------------ + + void groupsEmptyAndSingle() { + + OverlapProcessor p; + + QCOMPARE(int(p.findOverlapGroups(EventVector()).size()), 0); + + EventVector one { note(0, 100, 440.f) }; + QCOMPARE(int(p.findOverlapGroups(one).size()), 0); + } + + void groupsIgnoreNonOverlapping() { + + OverlapProcessor p; + + EventVector events { + note(0, 100, 440.f), + note(200, 100, 450.f), + note(400, 100, 460.f) + }; + + QCOMPARE(int(p.findOverlapGroups(events).size()), 0); + } + + void groupsIgnoreTouchingEndToEnd() { + + // [0,100) and [100,200) share a boundary but do not overlap + OverlapProcessor p; + + EventVector events { + note(0, 100, 440.f), + note(100, 100, 450.f) + }; + + QCOMPARE(int(p.findOverlapGroups(events).size()), 0); + QCOMPARE(p.eventsOverlap(events[0], events[1]), false); + } + + void groupsAreTransitive() { + + // A and C do not overlap each other, but both overlap B + OverlapProcessor p; + + EventVector events { + note(0, 100, 440.f), // A + note(90, 100, 450.f), // B + note(180, 100, 460.f) // C + }; + + auto groups = p.findOverlapGroups(events); + QCOMPARE(int(groups.size()), 1); + QCOMPARE(int(groups[0].size()), 3); + QCOMPARE(groups[0].startFrame, sv_frame_t(0)); + QCOMPARE(groups[0].endFrame, sv_frame_t(280)); + QCOMPARE(p.eventsOverlap(events[0], events[2]), false); + } + + void groupsSpanIsNotBrokenByAShortMember() { + + // The short event in the middle must not end the run: the + // running span is the maximum end frame of the group so far, + // not the end frame of the previous event. + OverlapProcessor p; + + EventVector events { + note(0, 1000, 440.f), // long + note(10, 5, 450.f), // short, entirely inside the long one + note(900, 200, 460.f) // overlaps the long one only + }; + + auto groups = p.findOverlapGroups(events); + QCOMPARE(int(groups.size()), 1); + QCOMPARE(int(groups[0].size()), 3); + QCOMPARE(groups[0].endFrame, sv_frame_t(1100)); + } + + void groupsSeparateRuns() { + + OverlapProcessor p; + + EventVector events { + note(0, 100, 440.f), + note(50, 100, 450.f), + note(1000, 100, 460.f), + note(1050, 100, 470.f) + }; + + auto groups = p.findOverlapGroups(events); + QCOMPARE(int(groups.size()), 2); + QCOMPARE(int(groups[0].size()), 2); + QCOMPARE(int(groups[1].size()), 2); + } + + void groupsHandleZeroDurationEvents() { + + // Pitch-style events have no duration and so never overlap each + // other, not even at the same frame + OverlapProcessor p; + + EventVector events { + pitch(100, 440.f), + pitch(100, 441.f), + pitch(200, 442.f) + }; + + QCOMPARE(int(p.findOverlapGroups(events).size()), 0); + + // ...but a durationed event does overlap the point events it + // spans, and only those: [50,150) covers the two at frame 100 + // but not the one at frame 200. + events.push_back(note(50, 100, 443.f)); + auto groups = p.findOverlapGroups(events); + QCOMPARE(int(groups.size()), 1); + QCOMPARE(groups[0].indices, std::vector({ 3, 0, 1 })); + } + + void groupsMatchTransitiveClosureReference() { + + // The sweep implementation must agree with a brute-force + // transitive closure of eventsOverlap() on unsorted, nested, + // duplicated and zero-duration input. + OverlapProcessor p; + + EventVector events { + note(500, 100, 440.f), + note(0, 50, 441.f), + note(40, 5, 442.f), + note(490, 400, 443.f), + pitch(45, 444.f), + note(1000, 0, 445.f), + note(880, 130, 446.f), + note(0, 50, 441.f), + note(2000, 10, 447.f), + note(300, 100, 448.f) + }; + + auto expected = referenceGroups(p, events); + std::sort(expected.begin(), expected.end()); + + QCOMPARE(actualGroups(p, events), expected); + } + + void groupsScaleToLongTracks() { + + // Regression guard for the old rescan-on-every-match loop, which + // was cubic in the worst case and ran over the whole note track + // once per realtime chunk. 20000 chained notes would not finish + // in any reasonable time under that implementation. + OverlapProcessor p; + + const int count = 20000; + EventVector events; + events.reserve(count); + for (int i = 0; i < count; ++i) { + events.push_back(note(i * 100, 150, 440.f)); // each overlaps the next + } + + QElapsedTimer timer; + timer.start(); + auto groups = p.findOverlapGroups(events); + const qint64 elapsed = timer.elapsed(); + + QCOMPARE(int(groups.size()), 1); + QCOMPARE(int(groups[0].size()), count); + QVERIFY2(elapsed < 5000, + QString("findOverlapGroups took %1 ms for %2 events") + .arg(elapsed).arg(count).toLocal8Bit().data()); + } + + // ---- mergeOverlapGroup / weighted frequency --------------------- + + void weightedFrequencyIsDurationWeightedGeometricMean() { + + OverlapProcessor p; + + EventVector events { + note(0, 300, 400.f), + note(0, 100, 800.f) + }; + + // weights 3/4 and 1/4 => exp(0.75*ln400 + 0.25*ln800) + const double expected = + std::exp(0.75 * std::log(400.0) + 0.25 * std::log(800.0)); + + const float actual = p.calculateWeightedFrequency(events, 0, 300); + QVERIFY(std::fabs(double(actual) - expected) < 0.01); + } + + void weightedFrequencyIgnoresUnvoicedEvents() { + + OverlapProcessor p; + + EventVector events { + note(0, 100, 440.f), + note(0, 100, 0.f) // unvoiced, must not drag the mean to zero + }; + + QCOMPARE(p.calculateWeightedFrequency(events, 0, 100), 440.f); + } + + void mergeSpansWholeGroupAndTakesLabelFromLongest() { + + OverlapProcessor p; + + EventVector events { + Event(0, 440.f, 100, "short"), + Event(50, 450.f, 400, "long") + }; + + OverlapGroup group(0, events[0]); + group.addEvent(1, events[1]); + + auto merged = p.mergeOverlapGroup(group, events); + QVERIFY(merged.has_value()); + QCOMPARE(merged->getFrame(), sv_frame_t(0)); + QCOMPARE(merged->getDuration(), sv_frame_t(450)); + QCOMPARE(merged->getLabel(), QString("long")); + } + + // ---- processNoteEvents ------------------------------------------ + + void notesEmptyIncomingIsANoOp() { + + OverlapProcessor p; + + EventVector existing { note(0, 100, 440.f) }; + + auto patch = p.processNoteEvents(1000, EventVector(), existing); + QCOMPARE(int(patch.remove.size()), 0); + QCOMPARE(int(patch.add.size()), 0); + } + + void notesIncomingIsShiftedByContextStart() { + + OverlapProcessor p; + + EventVector incoming { note(0, 100, 440.f), note(200, 100, 450.f) }; + + auto patch = p.processNoteEvents(5000, incoming, EventVector()); + + QCOMPARE(int(patch.remove.size()), 0); + QCOMPARE(int(patch.add.size()), 2); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(5000)); + QCOMPARE(patch.add[1].getFrame(), sv_frame_t(5200)); + } + + void notesLeaveUntouchedExistingAlone() { + + // An existing group far from the analysed region must be left + // exactly as it is -- neither removed nor re-added. + OverlapProcessor p; + + EventVector existing { + note(0, 100, 440.f), + note(50, 100, 441.f) // overlaps its neighbour, but not the incoming + }; + + EventVector incoming { note(0, 100, 460.f) }; + + auto patch = p.processNoteEvents(5000, incoming, existing); + + QCOMPARE(int(patch.remove.size()), 0); + QCOMPARE(int(patch.add.size()), 1); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(5000)); + + auto result = applyPatch(existing, patch); + QCOMPARE(int(result.size()), 3); + } + + void notesOverlappingExistingIsRemovedAndMerged() { + + OverlapProcessor p; + + EventVector existing { note(100, 150, 440.f) }; // [100,250) + EventVector incoming { note(0, 200, 445.f) }; // shifted to [200,400) + + auto patch = p.processNoteEvents(200, incoming, existing); + + QCOMPARE(int(patch.remove.size()), 1); + QCOMPARE(patch.remove[0].getFrame(), sv_frame_t(100)); + + QCOMPARE(int(patch.add.size()), 1); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(100)); + QCOMPARE(patch.add[0].getFrame() + patch.add[0].getDuration(), + sv_frame_t(400)); + + auto result = applyPatch(existing, patch); + QCOMPARE(int(result.size()), 1); + } + + void notesChainedExistingIsAlsoRemoved() { + + // Regression test. + // + // R overlaps D, and D overlaps the incoming note N, but R does + // not overlap N. Grouping is transitive, so {N,D,R} merges into + // a single event -- which means R is superseded too. Removing + // only D (the one that directly overlapped N) left R behind + // underneath the merged note, producing two overlapping notes + // covering the same span. + OverlapProcessor p; + + EventVector existing { + note(150, 100, 440.f), // D: [150,250) + note(240, 60, 441.f) // R: [240,300) + }; + EventVector incoming { note(0, 100, 450.f) }; // N: [100,200) + + QCOMPARE(p.eventsOverlap(existing[1], + incoming[0].withFrame(100)), false); + + auto patch = p.processNoteEvents(100, incoming, existing); + + QCOMPARE(int(patch.remove.size()), 2); + QCOMPARE(int(patch.add.size()), 1); + + auto result = applyPatch(existing, patch); + QCOMPARE(int(result.size()), 1); + QCOMPARE(result[0].getFrame(), sv_frame_t(100)); + QCOMPARE(result[0].getFrame() + result[0].getDuration(), + sv_frame_t(300)); + QCOMPARE(hasAnyOverlap(p, result), false); + } + + void notesLeaveNoOverlapsBehindInAChain() { + + // A longer version of the same shape: a chain of existing notes + // reached only through their neighbours. Whatever the merge + // decides to produce, the resulting model must not contain two + // notes overlapping each other. + OverlapProcessor p; + + EventVector existing; + for (int i = 0; i < 8; ++i) { + existing.push_back(note(300 + i * 90, 100, 440.f + i)); + } + + EventVector incoming { note(0, 100, 500.f) }; // -> [350,450) + + auto patch = p.processNoteEvents(350, incoming, existing); + auto result = applyPatch(existing, patch); + + QCOMPARE(hasAnyOverlap(p, result), false); + } + + void notesRepeatedChunksDoNotAccumulate() { + + // Realtime chunks overlap each other by design, so the same + // audio is analysed more than once. Re-applying an identical + // chunk must not grow the model. + OverlapProcessor p; + + EventVector incoming { + note(0, 100, 440.f), + note(200, 100, 450.f) + }; + + EventVector model; + model = applyPatch(model, p.processNoteEvents(1000, incoming, model)); + const int afterFirst = int(model.size()); + + for (int i = 0; i < 5; ++i) { + model = applyPatch(model, + p.processNoteEvents(1000, incoming, model)); + } + + QCOMPARE(int(model.size()), afterFirst); + QCOMPARE(hasAnyOverlap(p, model), false); + } + + // ---- processPitchEvents ----------------------------------------- + + void pitchReplacesFromContextStartOnly() { + + OverlapProcessor p; + + EventVector existing { + pitch(0, 400.f), + pitch(100, 401.f), + pitch(1000, 402.f), + pitch(1100, 403.f) + }; + + EventVector incoming { pitch(0, 500.f), pitch(100, 501.f) }; + + auto patch = p.processPitchEvents(1000, incoming, existing); + + QCOMPARE(int(patch.remove.size()), 2); + QCOMPARE(patch.remove[0].getFrame(), sv_frame_t(1000)); + QCOMPARE(patch.remove[1].getFrame(), sv_frame_t(1100)); + + auto result = applyPatch(existing, patch); + QCOMPARE(int(result.size()), 4); + QCOMPARE(result[0].getValue(), 400.f); + QCOMPARE(result[1].getValue(), 401.f); + QCOMPARE(result[2].getFrame(), sv_frame_t(1000)); + QCOMPARE(result[2].getValue(), 500.f); + QCOMPARE(result[3].getFrame(), sv_frame_t(1100)); + QCOMPARE(result[3].getValue(), 501.f); + } + + void pitchInterpolatesAcrossASmallSimilarGap() { + + OverlapProcessor p; // threshold 512 frames, 10% pitch difference + + EventVector existing { pitch(900, 440.f) }; + EventVector incoming { pitch(100, 445.f) }; // -> frame 1100 + + auto patch = p.processPitchEvents(1000, incoming, existing); + + QCOMPARE(int(patch.add.size()), 2); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(1000)); // midpoint + QCOMPARE(patch.add[0].getLabel(), QString("interpolated")); + QCOMPARE(patch.add[0].getValue(), (440.f + 445.f) / 2.f); + QCOMPARE(patch.add[1].getFrame(), sv_frame_t(1100)); + } + + void pitchDoesNotInterpolateAcrossALargeGap() { + + OverlapProcessor p; + + EventVector existing { pitch(0, 440.f) }; + EventVector incoming { pitch(100, 445.f) }; // gap of 1100 frames + + auto patch = p.processPitchEvents(1000, incoming, existing); + + QCOMPARE(int(patch.add.size()), 1); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(1100)); + } + + void pitchDoesNotInterpolateAcrossADissimilarPitch() { + + OverlapProcessor p; + + EventVector existing { pitch(900, 440.f) }; + EventVector incoming { pitch(100, 880.f) }; // an octave up + + auto patch = p.processPitchEvents(1000, incoming, existing); + + QCOMPARE(int(patch.add.size()), 1); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(1100)); + } + + void pitchSeamUsesLatestEventBeforeContextStart() { + + // The seam must be measured from the last event before the + // analysed region, whichever order the existing events arrive in + EventVector existing { + pitch(100, 300.f), + pitch(900, 440.f), + pitch(500, 350.f) + }; + + OverlapProcessor p; + EventVector incoming { pitch(100, 445.f) }; + + auto patch = p.processPitchEvents(1000, incoming, existing); + + QCOMPARE(int(patch.add.size()), 2); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(1000)); + QCOMPARE(patch.add[0].getValue(), (440.f + 445.f) / 2.f); + } + + void pitchEmptyIncomingStillClearsTheRegion() { + + // An analysis that found no pitch at all should clear whatever + // the previous chunk left behind in that region + OverlapProcessor p; + + EventVector existing { pitch(0, 400.f), pitch(1000, 401.f) }; + + auto patch = p.processPitchEvents(1000, EventVector(), existing); + + QCOMPARE(int(patch.remove.size()), 1); + QCOMPARE(patch.remove[0].getFrame(), sv_frame_t(1000)); + QCOMPARE(int(patch.add.size()), 0); + } + + void pitchRepeatedChunksDoNotAccumulate() { + + OverlapProcessor p; + + EventVector incoming { pitch(0, 440.f), pitch(100, 441.f) }; + + EventVector model; + model = applyPatch(model, p.processPitchEvents(1000, incoming, model)); + const int afterFirst = int(model.size()); + + for (int i = 0; i < 5; ++i) { + model = applyPatch(model, + p.processPitchEvents(1000, incoming, model)); + } + + QCOMPARE(int(model.size()), afterFirst); + } +}; + +#endif diff --git a/main/test/tony-main-test.cpp b/main/test/tony-main-test.cpp new file mode 100644 index 00000000..2b57fce9 --- /dev/null +++ b/main/test/tony-main-test.cpp @@ -0,0 +1,46 @@ +/* -*- 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 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 "TestOverlapProcessor.h" + +#include "system/Init.h" + +#include + +int main(int argc, char *argv[]) +{ + int good = 0, bad = 0; + + svSystemSpecificInitialisation(); + + QCoreApplication app(argc, argv); + app.setOrganizationName("sonic-visualiser"); + app.setApplicationName("test-tony-main"); + + { + TestOverlapProcessor t; + if (QTest::qExec(&t, argc, argv) == 0) ++good; + else ++bad; + } + + (void)good; + + if (bad > 0) { + SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl; + return 1; + } else { + SVCERR << "All tests passed" << endl; + return 0; + } +} diff --git a/meson.build b/meson.build index 6512bb56..0a73137a 100644 --- a/meson.build +++ b/meson.build @@ -186,8 +186,7 @@ elif system == 'darwin' rc = [] general_defines += [ - '-mmacosx-version-min=10.15', - '-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_NONE' + '-mmacosx-version-min=10.15' ] general_link_args += [ '-mmacosx-version-min=10.15' @@ -758,6 +757,11 @@ svcore_data_fileio_test_moc_files = qt.preprocess( 'svcore/data/fileio/test/CSVStreamWriterTest.h', ]) +tony_main_test_moc_files = qt.preprocess( + moc_headers: [ + 'main/test/TestOverlapProcessor.h', +]) + svgui_files = [ 'svgui/layer/Colour3DPlotExporter.cpp', 'svgui/layer/Colour3DPlotLayer.cpp', @@ -1203,6 +1207,31 @@ svcore_data_fileio_test_exe = executable( win_subsystem: 'console' ) +tony_main_test_exe = executable( + 'test-tony-main', + tony_main_test_moc_files, + 'main/OverlapProcessor.cpp', + 'main/test/tony-main-test.cpp', + include_directories: [ + 'main', + ], + dependencies: [ + svcore_dep, + qt_dep, + feature_dependencies, + dl_dep, + ], + cpp_args: [ + feature_defines, + general_defines, + ], + link_args: [ + feature_additional_libs, + general_link_args, + ], + win_subsystem: 'console' +) + test('svcore-base', svcore_base_test_exe) test('svcore-system', svcore_system_test_exe) test('svcore-data-model', svcore_data_model_test_exe) @@ -1210,6 +1239,7 @@ test('svcore-data-fileio', svcore_data_fileio_test_exe, args: [ '--testdir', meson.current_source_dir() / 'svcore/data/fileio/test' ]) +test('tony-main', tony_main_test_exe) summary({'prefix': get_option('prefix'), 'bindir': get_option('bindir'), From 38ab4f52d6ffc42afb6b09b9b14f44ce19548817 Mon Sep 17 00:00:00 2001 From: Alexander Novikov Date: Thu, 6 Aug 2026 16:41:20 +0200 Subject: [PATCH 27/27] Fix live pitch track appearing at roughly twice its correct frame pYIN's two outputs disagree about what their timestamps mean, and the realtime path was treating them identically. The host feeds absolute block timestamps to the plugin (FeatureExtractionModelTransformer.cpp, m_plugin->process with frame2RealTime(blockFrame)). pYIN's smoothedpitchtrack is a FixedSampleRate output that echoes that timestamp back verbatim (PYinVamp.cpp:592), and the transformer's FixedSampleRate branch reconstructs the frame from it, so those frames are already absolute. Its notes output is VariableSampleRate and derives timestamps from a frame index counting from zero, ignoring the host timestamps (PYinVamp.cpp:695-699), so those frames are relative. processPitchEvents and processNoteEvents both added contextStart, which is correct for notes and doubles the offset for pitch. During recording the pitch track therefore drifted off past the record head, further the longer the take ran, and only looked right afterwards because Document::refreshModel regenerates the derived layers when recording stops. processPitchEvents no longer shifts. contextStart remains the boundary of the region being replaced. The differing frame conventions are now documented on both methods and pinned by tests. Co-Authored-By: Claude Opus 5 --- main/OverlapProcessor.cpp | 22 +++++++++--- main/OverlapProcessor.h | 23 +++++++++---- main/test/TestOverlapProcessor.h | 59 ++++++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/main/OverlapProcessor.cpp b/main/OverlapProcessor.cpp index 291110a4..35f29e4e 100644 --- a/main/OverlapProcessor.cpp +++ b/main/OverlapProcessor.cpp @@ -252,7 +252,21 @@ OverlapProcessor::EventPatch OverlapProcessor::processPitchEvents(sv_frame_t con const EventVector& incomingEvents, const EventVector& existingEvents) const { EventPatch patch; - EventVector shiftedIncoming = shiftedBy(incomingEvents, contextStart); + + // Incoming pitch frames are NOT shifted, unlike note frames. + // + // pYIN's smoothedpitchtrack is a FixedSampleRate output, and its + // features carry the host-supplied block timestamp verbatim + // (PYinVamp.cpp: f.timestamp = m_timestamp[iFrame]). The host feeds + // absolute block timestamps, and FeatureExtractionModelTransformer's + // FixedSampleRate branch reconstructs the frame from that timestamp, + // so these frames are already absolute. + // + // Its notes output behaves differently -- it counts frames from zero + // and ignores the host timestamps -- so processNoteEvents() does have + // to shift. Adding contextStart here as well was what put the live + // pitch track at roughly twice its correct frame. + const EventVector &incoming = incomingEvents; // Everything from contextStart onwards is superseded by the fresh // analysis. We need the latest-ending event before contextStart in @@ -277,11 +291,11 @@ OverlapProcessor::EventPatch OverlapProcessor::processPitchEvents(sv_frame_t con sortAndDedupeEvents(patch.remove); - patch.add = shiftedIncoming; + patch.add = incoming; - if (lastBefore && !shiftedIncoming.empty()) { + if (lastBefore && !incoming.empty()) { - const auto& firstNewEvent = shiftedIncoming.front(); + const auto& firstNewEvent = incoming.front(); const auto lastExistingEnd = lastBefore->getFrame() + lastBefore->getDuration(); diff --git a/main/OverlapProcessor.h b/main/OverlapProcessor.h index 02d7bf80..e36c8bdf 100644 --- a/main/OverlapProcessor.h +++ b/main/OverlapProcessor.h @@ -92,18 +92,29 @@ class OverlapProcessor { /** * Produce the patch that replaces the pitch track from - * contextStart onwards with incomingEvents (whose frames are - * relative to contextStart). + * contextStart onwards with incomingEvents. + * + * incomingEvents are expected to carry ABSOLUTE frames, as pYIN's + * smoothedpitchtrack output does: it is a FixedSampleRate output + * whose features carry the host's block timestamp verbatim, and the + * transformer reconstructs the frame from that timestamp. Here + * contextStart is only the boundary of the region being replaced, + * not an offset to add. */ EventPatch processPitchEvents(sv::sv_frame_t contextStart, const sv::EventVector& incomingEvents, const sv::EventVector& existingEvents) const; /** - * Produce the patch that merges incomingEvents (whose frames are - * relative to contextStart) into existingEvents. Existing notes - * absorbed into a merge are included in the remove list, and - * existing notes untouched by the incoming ones are left alone. + * Produce the patch that merges incomingEvents into existingEvents. + * Existing notes absorbed into a merge are included in the remove + * list, and existing notes untouched by the incoming ones are left + * alone. + * + * Unlike processPitchEvents(), incomingEvents are expected to carry + * frames RELATIVE to contextStart, as pYIN's notes output does: it + * derives its timestamps from a frame index counting from zero and + * ignores the host's block timestamps. contextStart is added to them. */ EventPatch processNoteEvents(sv::sv_frame_t contextStart, const sv::EventVector& incomingEvents, diff --git a/main/test/TestOverlapProcessor.h b/main/test/TestOverlapProcessor.h index eb562eba..2db3bc87 100644 --- a/main/test/TestOverlapProcessor.h +++ b/main/test/TestOverlapProcessor.h @@ -489,6 +489,53 @@ private slots: } // ---- processPitchEvents ----------------------------------------- + // + // Note the asymmetry with processNoteEvents throughout this section: + // incoming PITCH frames are absolute and are not shifted, because + // pYIN's smoothedpitchtrack echoes the host's absolute block + // timestamp, whereas its notes output counts frames from zero. See + // pitchAndNoteFrameOriginsDiffer() below. + + void pitchIncomingIsNotShiftedByContextStart() { + + // Regression test. Adding contextStart to the incoming pitch + // frames -- correct for notes, wrong for pitch -- put the live + // pitch track at roughly twice its true frame, so during + // recording it drifted off past the record head. + OverlapProcessor p; + + EventVector incoming { pitch(5000, 440.f), pitch(5100, 441.f) }; + + auto patch = p.processPitchEvents(5000, incoming, EventVector()); + + QCOMPARE(int(patch.add.size()), 2); + QCOMPARE(patch.add[0].getFrame(), sv_frame_t(5000)); + QCOMPARE(patch.add[1].getFrame(), sv_frame_t(5100)); + } + + void pitchAndNoteFrameOriginsDiffer() { + + // Pins the contract that the two entry points disagree on + // purpose. Same contextStart, same nominal event position, two + // different frame conventions. + OverlapProcessor p; + + const sv_frame_t contextStart = 5000; + + auto pitchPatch = p.processPitchEvents + (contextStart, EventVector { pitch(5000, 440.f) }, EventVector()); + + auto notePatch = p.processNoteEvents + (contextStart, EventVector { note(0, 100, 440.f) }, EventVector()); + + QCOMPARE(int(pitchPatch.add.size()), 1); + QCOMPARE(int(notePatch.add.size()), 1); + + // Absolute in, absolute out -- unchanged + QCOMPARE(pitchPatch.add[0].getFrame(), sv_frame_t(5000)); + // Relative in, shifted to absolute out + QCOMPARE(notePatch.add[0].getFrame(), sv_frame_t(5000)); + } void pitchReplacesFromContextStartOnly() { @@ -501,7 +548,7 @@ private slots: pitch(1100, 403.f) }; - EventVector incoming { pitch(0, 500.f), pitch(100, 501.f) }; + EventVector incoming { pitch(1000, 500.f), pitch(1100, 501.f) }; auto patch = p.processPitchEvents(1000, incoming, existing); @@ -524,7 +571,7 @@ private slots: OverlapProcessor p; // threshold 512 frames, 10% pitch difference EventVector existing { pitch(900, 440.f) }; - EventVector incoming { pitch(100, 445.f) }; // -> frame 1100 + EventVector incoming { pitch(1100, 445.f) }; // 200-frame gap auto patch = p.processPitchEvents(1000, incoming, existing); @@ -540,7 +587,7 @@ private slots: OverlapProcessor p; EventVector existing { pitch(0, 440.f) }; - EventVector incoming { pitch(100, 445.f) }; // gap of 1100 frames + EventVector incoming { pitch(1100, 445.f) }; // gap of 1100 frames auto patch = p.processPitchEvents(1000, incoming, existing); @@ -553,7 +600,7 @@ private slots: OverlapProcessor p; EventVector existing { pitch(900, 440.f) }; - EventVector incoming { pitch(100, 880.f) }; // an octave up + EventVector incoming { pitch(1100, 880.f) }; // an octave up auto patch = p.processPitchEvents(1000, incoming, existing); @@ -572,7 +619,7 @@ private slots: }; OverlapProcessor p; - EventVector incoming { pitch(100, 445.f) }; + EventVector incoming { pitch(1100, 445.f) }; auto patch = p.processPitchEvents(1000, incoming, existing); @@ -600,7 +647,7 @@ private slots: OverlapProcessor p; - EventVector incoming { pitch(0, 440.f), pitch(100, 441.f) }; + EventVector incoming { pitch(1000, 440.f), pitch(1100, 441.f) }; EventVector model; model = applyPatch(model, p.processPitchEvents(1000, incoming, model));