changeset 423:93d4b4a98305

Merge from branch pitch-align, with the groundwork for pitch-based alignment...
author Chris Cannam
date Wed, 13 May 2020 14:11:43 +0100
parents 011d39dc7350 (current diff) eb4a9dca0acd (diff)
children d9b544de4c8a
files
diffstat 13 files changed, 345 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Apr 01 11:03:02 2020 +0100
+++ b/.hgignore	Wed May 13 14:11:43 2020 +0100
@@ -61,3 +61,4 @@
 qm-vamp-plugins
 ..*
 deploy/linux/docker/output
+*.AppImage
--- a/main/MainWindow.cpp	Wed Apr 01 11:03:02 2020 +0100
+++ b/main/MainWindow.cpp	Wed May 13 14:11:43 2020 +0100
@@ -58,7 +58,6 @@
 #include "audio/AudioCallbackRecordTarget.h"
 #include "audio/PlaySpeedRangeMapper.h"
 #include "data/fileio/DataFileReaderFactory.h"
-#include "data/fileio/PlaylistFileReader.h"
 #include "data/fileio/WavFileWriter.h"
 #include "data/fileio/CSVFileWriter.h"
 #include "data/fileio/BZipFileDevice.h"
@@ -391,16 +390,7 @@
 
     NetworkPermissionTester tester;
     m_networkPermission = tester.havePermission();
-
-    if (!reopenLastSession()) {
-        QTimer::singleShot(400, this, SLOT(introDialog()));
-    } else {
-        // Do this here only if not showing the intro dialog -
-        // otherwise the introDialog function will do this after it
-        // has shown the dialog, so we don't end up with both at once
-        checkForNewerVersion();
-    }
-                       
+        
 //    QTimer::singleShot(500, this, SLOT(betaReleaseWarning()));
 }
 
@@ -2488,17 +2478,6 @@
 }
 
 void
-MainWindow::audioTimeStretchMultiChannelDisabled()
-{
-    static bool shownOnce = false;
-    if (shownOnce) return;
-    QMessageBox::information
-        (this, tr("Audio processing overload"),
-         tr("<b>Overloaded</b><p>Audio playback speed processing has been reduced to a single channel, due to a processing overload."));
-    shownOnce = true;
-}
-
-void
 MainWindow::introDialog()
 {
     IntroDialog::show(this);
@@ -2906,7 +2885,7 @@
 }
 
 void
-MainWindow::alignmentFailed(QString message)
+MainWindow::alignmentFailed(ModelId, QString message)
 {
     QMessageBox::warning
         (this,
--- a/main/MainWindow.h	Wed Apr 01 11:03:02 2020 +0100
+++ b/main/MainWindow.h	Wed May 13 14:11:43 2020 +0100
@@ -75,6 +75,7 @@
 
 public slots:
     void openSmallSession(const SmallSession &);
+    bool reopenLastSession();
 
     void preferenceChanged(PropertyContainer::PropertyName) override;
     bool commitData(bool mayAskUser); // on session shutdown
@@ -84,6 +85,9 @@
 
     void selectMainPane();
 
+    void introDialog();
+    void checkForNewerVersion();
+
 protected slots:
     virtual void openFiles();
     virtual void openLocation();
@@ -93,7 +97,6 @@
     virtual void newSession();
     virtual void preferences();
 
-    bool reopenLastSession();
     void closeSession() override;
 
     void outlineWaveformModeSelected();
@@ -129,15 +132,11 @@
     virtual void restoreNormalPlayback();
 
     void monitoringLevelsChanged(float, float) override;
-
-    void introDialog();
-    void checkForNewerVersion();
     
     void betaReleaseWarning();
 
     void sampleRateMismatch(sv_samplerate_t, sv_samplerate_t, bool) override;
     void audioOverloadPluginDisabled() override;
-    void audioTimeStretchMultiChannelDisabled() override;
 
     void documentModified() override;
     void documentRestored() override;
@@ -161,7 +160,7 @@
     void modelRegenerationWarning(QString, QString, QString) override;
 
     void alignmentComplete(ModelId) override;
-    void alignmentFailed(QString) override;
+    void alignmentFailed(ModelId, QString) override;
 
     virtual void salientLayerCompletionChanged(ModelId);
 
--- a/main/main.cpp	Wed Apr 01 11:03:02 2020 +0100
+++ b/main/main.cpp	Wed May 13 14:11:43 2020 +0100
@@ -19,6 +19,7 @@
 #include "base/TempDirectory.h"
 #include "base/PropertyContainer.h"
 #include "base/Preferences.h"
+#include "data/fileio/PlaylistFileReader.h"
 #include "widgets/TipDialog.h"
 #include "svcore/plugin/PluginScan.h"
 
@@ -302,16 +303,57 @@
 
     SmallSession session;
     bool haveSession = false;
+
+    QStringList filePaths;
     
     for (QStringList::iterator i = args.begin(); i != args.end(); ++i) {
 
         if (i == args.begin()) continue;
         if (i->startsWith('-')) continue;
 
+        QString arg = *i;
+
+        // If an arg is a playlist file, we can streamline things and
+        // make sure we get the proper absolute paths by expanding it
+        // here, rather than adding it to the session and waiting for
+        // it to be expanded in the main application logic. (That
+        // would work too, it's just not so clean a user experience.)
+
+        if (PlaylistFileReader::isSupported(arg)) {
+            PlaylistFileReader reader(arg);
+            if (!reader.isOK()) {
+                // But if we can't open the playlist file, add it to
+                // the session as if it were just any old file and let
+                // the main application worry about it later - we
+                // don't want to be popping up dialogs before the app
+                // has been exec'd
+                filePaths.push_back(arg);
+            } else {
+                auto playlist = reader.load();
+                for (auto entry: playlist) {
+                    filePaths.push_back(entry);
+                }
+            }
+        } else {
+            filePaths.push_back(arg);
+        }
+    }
+
+    for (auto filePath: filePaths) {
+
+        // Add the argument to our session as a file path or URL to be
+        // opened. We want to avoid relative file paths, but to do so
+        // we must first check that they are not absolute URLs.
+        
+        QUrl url(filePath);
+        if (url.isRelative()) {
+            filePath = QFileInfo(filePath).absoluteFilePath();
+        }
+        
         if (session.mainFile == "") {
-            session.mainFile = *i;
+            session.mainFile = filePath;
         } else {
-            session.additionalFiles.push_back(*i);
+            session.additionalFiles.push_back(filePath);
         }
 
         haveSession = true;
@@ -319,6 +361,13 @@
 
     if (haveSession) {
         gui->openSmallSession(session);
+    } else if (!gui->reopenLastSession()) {
+        QTimer::singleShot(400, gui, SLOT(introDialog()));
+    } else {
+        // Do this here only if not showing the intro dialog -
+        // otherwise the introDialog function will do this after it
+        // has shown the dialog, so we don't end up with both at once
+        gui->checkForNewerVersion();
     }
 
     int rv = application.exec();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/Makefile	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,13 @@
+
+SCRIPTS	:= ../../sml-buildscripts
+
+pitch-track-align:	pitch-track-align.mlb pitch-track-align.deps
+	mlton pitch-track-align.mlb
+
+pitch-track-align.deps: pitch-track-align.mlb 
+	${SCRIPTS}/mlb-dependencies $^ > $@
+
+clean:
+	rm -f pitch-track-align *.deps
+
+-include *.deps
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/main.sml	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,1 @@
+val _ = main ()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/note-track.ttl	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,7 @@
+@prefix xsd:      <http://www.w3.org/2001/XMLSchema#> .
+@prefix vamp:     <http://purl.org/ontology/vamp/> .
+@prefix :         <#> .
+
+:transform a vamp:Transform ;
+    vamp:plugin <http://vamp-plugins.org/rdf/plugins/pyin#pyin> ;
+    vamp:output <http://vamp-plugins.org/rdf/plugins/pyin#pyin_output_notes> .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/pitch-track-align.mlb	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,3 @@
+$(SML_LIB)/basis/basis.mlb
+pitch-track-align.sml
+main.sml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/pitch-track-align.sh	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+mydir=$(dirname "$0")
+
+set -eu
+
+file1="$1"
+file2="$2"
+
+export PATH=$PATH:"$mydir"/../../sonic-annotator
+
+tmproot=/tmp/pitch-track-align-"$$"
+trap "rm -f $tmproot.a $tmproot.b" 0
+
+#sonic-annotator -t "$mydir/pitch-track.ttl" "$file1" -w csv --csv-one-file "$tmproot.a" --csv-omit-filename --csv-force
+#sonic-annotator -t "$mydir/pitch-track.ttl" "$file2" -w csv --csv-one-file "$tmproot.b" --csv-omit-filename --csv-force
+
+sonic-annotator -t "$mydir/note-track.ttl" "$file1" -w csv --csv-omit-filename --csv-stdout | awk -F, '{ print $1 "," $3 }' > "$tmproot.a"
+sonic-annotator -t "$mydir/note-track.ttl" "$file2" -w csv --csv-omit-filename --csv-stdout | awk -F, '{ print $1 "," $3 }' > "$tmproot.b"
+
+echo 1>&2
+echo "First track:" 1>&2
+cat "$tmproot.a" 1>&2
+
+echo 1>&2
+echo "Second track:" 1>&2
+cat "$tmproot.b" 1>&2
+
+echo "0,0"
+
+"$mydir"/pitch-track-align "$tmproot.a" "$tmproot.b"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/pitch-track-align.sml	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,209 @@
+
+datatype pitch_direction =
+         PITCH_NONE |
+         PITCH_UP of real |
+         PITCH_DOWN of real
+
+type value = pitch_direction
+type cost = real
+
+fun choose costs =
+    case costs of
+        (NONE,   NONE,   _) => 0.0
+      | (SOME a, NONE,   _) => a
+      | (NONE,   SOME b, _) => b
+      | (SOME _, SOME _, NONE) => raise Fail "Internal error"
+      | (SOME a, SOME b, SOME both) =>
+        if a < b then
+            if both <= a then both else a
+        else
+            if both <= b then both else b
+
+fun cost (p1, p2) =
+    let fun together a b = let val diff = Real.abs (a - b) in 
+                               if diff < 1.0 then ~1.0
+                               else if diff > 3.0 then 1.0
+                               else 0.0 
+                           end
+        fun opposing a b = let val diff = a + b in
+                               if diff < 2.0 then 1.0
+                               else 2.0
+                           end
+    in
+        case (p1, p2) of
+            (PITCH_NONE, PITCH_NONE) => 0.0
+          | (PITCH_UP a, PITCH_UP b) => together a b
+          | (PITCH_UP a, PITCH_DOWN b) => opposing a b
+          | (PITCH_DOWN a, PITCH_UP b) => opposing a b
+          | (PITCH_DOWN a, PITCH_DOWN b) => together a b
+          | _ => 1.0
+    end
+       
+fun costSeries (s1 : value vector) (s2 : value vector) : cost vector vector =
+    let open Vector
+
+        fun costSeries' (rowAcc : cost vector list) j =
+            if j = length s1
+            then fromList (rev rowAcc)
+            else costSeries' (costRow' rowAcc j [] 0 :: rowAcc) (j+1)
+
+        and costRow' (rowAcc : cost vector list) j (colAcc : cost list) i =
+            if i = length s2
+            then fromList (rev colAcc)
+            else let val c = cost (sub (s1, j), sub (s2, i))
+                     val options =
+                         (if null rowAcc
+                          then NONE
+                          else SOME (c + sub (hd rowAcc, i)),
+                          if i = 0
+                          then NONE
+                          else SOME (c + hd colAcc),
+                          if null rowAcc orelse i = 0
+                          then NONE
+                          else SOME (c + sub (hd rowAcc, i-1)))
+                 in
+                     costRow' rowAcc j (choose options :: colAcc) (i+1)
+                 end
+    in
+        costSeries' [] 0
+    end
+
+fun alignSeries s1 s2 =
+    let val cumulativeCosts = costSeries s1 s2
+        fun cost (j, i) = Vector.sub (Vector.sub (cumulativeCosts, j), i)
+        fun trace (j, i) acc =
+            if i = 0
+            then if j = 0
+                 then i :: acc
+                 else trace (j-1, i) (i :: acc)
+            else if j = 0
+            then trace (j, i-1) acc
+            else let val (a, b, both) =
+                         (cost (j-1, i), cost (j, i-1), cost (j-1, i-1))
+                 in
+                     if a < b then
+                         if both <= a
+                         then trace (j-1, i-1) (i :: acc)
+                         else trace (j-1, i) (i :: acc)
+                     else
+                         if both <= b
+                         then trace (j-1, i-1) (i :: acc)
+                         else trace (j, i-1) acc
+                 end
+
+        val sj = Vector.length s1
+        val si = Vector.length s2
+    in
+        Vector.fromList
+            (if si = 0 orelse sj = 0
+             then []
+             else trace (sj-1, si-1) [])
+    end
+
+fun preprocess (times : real list, frequencies : real list) :
+    real vector * value vector * real vector =
+    let val pitches =
+            map (fn f =>
+                    if f < 0.0
+                    then 0.0
+                    else Real.realRound (12.0 * (Math.log10(f / 220.0) /
+                                                 Math.log10(2.0)) + 57.0))
+                frequencies
+        val values =
+            let val acc =
+                    foldl (fn (p, (acc, prev)) =>
+                              if p <= 0.0 then (PITCH_NONE :: acc, prev)
+                              else if prev <= 0.0
+                              then (PITCH_UP 0.0 :: acc, p)
+                              else if p >= prev
+                              then (PITCH_UP (p - prev) :: acc, p)
+                              else (PITCH_DOWN (prev - p) :: acc, p))
+                          ([], 0.0)
+                          pitches
+            in
+                rev (#1 acc)
+            end
+        val _ =
+            app (fn (text, p) =>
+                    TextIO.output (TextIO.stdErr, ("[" ^ text ^ "] -> " ^
+                                                   Real.toString p ^ "\n")))
+                (ListPair.map (fn (PITCH_NONE, p) => (" ", p)
+                                | (PITCH_UP d, p) => ("+", p)
+                                | (PITCH_DOWN d, p) => ("-", p))
+                              (values, pitches))
+    in
+        (Vector.fromList times,
+         Vector.fromList values,
+         Vector.fromList pitches)
+    end
+    
+fun read csvFile =
+    let fun toNumberPair line =
+            case String.fields (fn c => c = #",") line of
+                a::b::_ => (case (Real.fromString a, Real.fromString b) of
+                                (SOME r1, SOME r2) => (r1, r2)
+                              | _ => raise Fail ("Failed to parse numbers: " ^
+                                                 line))
+              | _ => raise Fail ("Not enough columns: " ^ line)
+        fun read' s acc =
+            case TextIO.inputLine s of
+                SOME line =>
+                let val pair = toNumberPair
+                                   (String.substring
+                                        (line, 0, String.size line - 1))
+                in
+                    read' s (pair :: acc)
+                end
+              | NONE => rev acc
+        val stream = TextIO.openIn csvFile
+        val (timeList, freqList) = ListPair.unzip (read' stream [])
+        val _ = TextIO.closeIn stream
+    in
+        preprocess (timeList, freqList)
+    end
+
+fun meanDiff pitches1 pitches2 mapping =
+    let open Vector
+        val n = length mapping
+        val sumDiff =
+            foldli (fn (i, j, acc) => acc +
+                                      sub (pitches1, i) -
+                                      sub (pitches2, j))
+                   0.0 mapping
+    in
+        if n = 0 then 0.0
+        else sumDiff / Real.fromInt n
+    end
+        
+fun alignFiles csv1 csv2 =
+    let val (times1, values1, pitches1) = read csv1
+        val (times2, values2, pitches2) = read csv2
+        (* raw alignment returns the index into pitches2 for each
+           element in pitches1 *)
+        val raw = alignSeries values1 values2
+        val _ = TextIO.output (TextIO.stdErr,
+                               "Mean pitch difference: reference " ^
+                               Real.toString (meanDiff pitches1 pitches2 raw)
+                               ^ " semitones higher than other track\n")
+    in
+        List.tabulate (Vector.length raw,
+                       fn i => (Vector.sub (times1, i),
+                                Vector.sub (times2, Vector.sub (raw, i))))
+    end
+
+fun printAlignment alignment =
+    app (fn (from, to) =>
+            print (Real.toString from ^ "," ^ Real.toString to ^ "\n"))
+        alignment
+        
+fun usage () =
+    TextIO.output (TextIO.stdErr,
+                   "Usage: pitch-track-align pitch1.csv pitch2.csv\n")
+
+fun main () =
+    (case CommandLine.arguments () of
+         [csv1, csv2] => printAlignment (alignFiles csv1 csv2)
+       | _ => usage ())
+    handle exn => 
+           (TextIO.output (TextIO.stdErr, "Error: " ^ (exnMessage exn) ^ "\n");
+            OS.Process.exit OS.Process.failure)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pitch-track-align/pitch-track.ttl	Wed May 13 14:11:43 2020 +0100
@@ -0,0 +1,11 @@
+@prefix xsd:      <http://www.w3.org/2001/XMLSchema#> .
+@prefix vamp:     <http://purl.org/ontology/vamp/> .
+@prefix :         <#> .
+
+:transform a vamp:Transform ;
+    vamp:plugin <http://vamp-plugins.org/rdf/plugins/pyin#pyin> ;
+    vamp:parameter_binding [
+        vamp:parameter [ vamp:identifier "outputunvoiced" ] ;
+        vamp:value "2"^^xsd:float ;
+    ] ;
+    vamp:output <http://vamp-plugins.org/rdf/plugins/pyin#pyin_output_smoothedpitchtrack> .
--- a/repoint-lock.json	Wed Apr 01 11:03:02 2020 +0100
+++ b/repoint-lock.json	Wed May 13 14:11:43 2020 +0100
@@ -1,25 +1,25 @@
 {
   "libraries": {
     "vamp-plugin-sdk": {
-      "pin": "74c5b0bfa108"
+      "pin": "8ffb8985ae8f"
     },
     "svcore": {
-      "pin": "498ed1e86f92"
+      "pin": "f36fef97ac81"
     },
     "svgui": {
-      "pin": "27ea5d61b402"
+      "pin": "52d4bfba5b3d"
     },
     "svapp": {
-      "pin": "7b1d30af4b38"
+      "pin": "6429a164b7e1"
     },
     "checker": {
-      "pin": "ef64b3f171d9"
+      "pin": "e839338d3869"
     },
     "piper": {
-      "pin": "f5a04ffe4d5a0ae01e77018a86a59b48a425e674"
+      "pin": "3a742c556ac1f2bf9823f30b937c71c690e1f6ae"
     },
     "piper-vamp-cpp": {
-      "pin": "f381235a4ba88eac2fa31fc1f7f613cca9707f63"
+      "pin": "f0d3ab2952b21d287b481759bda986427df10ef7"
     },
     "dataquay": {
       "pin": "35098262cadd"
@@ -61,7 +61,7 @@
       "pin": "a7d9c6142f8f"
     },
     "nnls-chroma": {
-      "pin": "1efe67570b75"
+      "pin": "82d5d11b68d7"
     },
     "qm-vamp-plugins": {
       "pin": "06933fecfa33aec41a07cc865098be7545ffcca3"
--- a/repoint-project.json	Wed Apr 01 11:03:02 2020 +0100
+++ b/repoint-project.json	Wed May 13 14:11:43 2020 +0100
@@ -24,7 +24,8 @@
         },
         "svapp": {
             "vcs": "hg",
-	    "service": "soundsoftware"
+	    "service": "soundsoftware",
+            "branch": "pitch-align"
         },
         "checker": {
             "vcs": "hg",