From 7007d18a554d305f63f5fdde916798426698b70a Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Thu, 5 Sep 2024 00:37:27 -0700
Subject: [PATCH 01/11] Deg positioning lost below placement as a default.

---
 src/iohumdrum.cpp | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/iohumdrum.cpp b/src/iohumdrum.cpp
index 15137e742f3..7eb039bc283 100644
--- a/src/iohumdrum.cpp
+++ b/src/iohumdrum.cpp
@@ -11105,10 +11105,7 @@ void HumdrumInput::addHarmFloatsForMeasure(int startline, int endline)
                 place = "above";
             }
             else {
-                int belowQ = token->getValueInt("auto", "below");
-                if (belowQ) {
-                    place = "below";
-                }
+                place = "below";
             }
             if (place.size() > 0) {
                 setPlaceRelStaff(harm, place, false);

From e49dce26b0c2991a24ef52f37568de2f61f888aa Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Thu, 5 Sep 2024 09:03:39 -0700
Subject: [PATCH 02/11] Added automatic identifcation EsAC input data.

---
 src/toolkit.cpp | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/toolkit.cpp b/src/toolkit.cpp
index 7b4edae8ea2..cfb0720caaf 100644
--- a/src/toolkit.cpp
+++ b/src/toolkit.cpp
@@ -298,11 +298,17 @@ FileFormat Toolkit::IdentifyInputFrom(const std::string &data)
         return UNKNOWN;
     }
     if (initial.find("\n!!") != std::string::npos) {
+        // Case where there are empty lines before content in Humdrum files.
         return HUMDRUM;
     }
     if (initial.find("\n**") != std::string::npos) {
+        // Case where there are empty lines before content in Humdrum files.
         return HUMDRUM;
     }
+    if (initial.find("\nCUT[") != std::string::npos) {
+        // Title record for a melody in EsAC format.
+        return ESAC;
+    }
 
     // Assume that the input is MEI if other input types were not detected.
     // This means that DARMS cannot be auto-detected.

From 6c4fdf3363108bee0fe68989b234a14ad40f4302 Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Sat, 7 Sep 2024 06:56:43 -0700
Subject: [PATCH 03/11] Humlib updates (esp. EsAC-to-Humdrum converter)

---
 include/hum/humlib.h |   332 +-
 src/hum/humlib.cpp   | 66453 ++++++++++++++++++++++-------------------
 2 files changed, 35983 insertions(+), 30802 deletions(-)

diff --git a/include/hum/humlib.h b/include/hum/humlib.h
index 5dfe597853f..374437551e4 100644
--- a/include/hum/humlib.h
+++ b/include/hum/humlib.h
@@ -1,7 +1,7 @@
 //
 // Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
 // Creation Date: Sat Aug  8 12:24:49 PDT 2015
-// Last Modified: Thu Aug  8 21:55:11 PDT 2024
+// Last Modified: Thu Sep  5 14:41:50 PDT 2024
 // Filename:      min/humlib.h
 // URL:           https://github.com/craigsapp/humlib/blob/master/min/humlib.h
 // Syntax:        C++11
@@ -964,9 +964,9 @@ class HumInstrument {
 		int         setGM               (const std::string& Hname, int aValue);
 
 	private:
-		int                            index;
-		static std::vector<_HumInstrument>  data;
-		static int                     classcount;
+		int                            m_index;
+		static std::vector<_HumInstrument>  m_data;
+		static int                     m_classcount;
 
 	protected:
 		void       initialize          (void);
@@ -2043,6 +2043,7 @@ class HumdrumFileBase : public HumHash {
 		bool          isRhythmAnalyzed         (void);
 		bool          areStrandsAnalyzed       (void);
 		bool          areStrophesAnalyzed      (void);
+		void          setFilenameFromSegment   (void);
 
     	template <class TYPE>
 		   void       initializeArray          (std::vector<std::vector<TYPE>>& array, TYPE value);
@@ -7358,6 +7359,213 @@ class Tool_double : public HumTool {
 };
 
 
+class Tool_esac2hum : public HumTool {
+	public:
+
+		class Note {
+			public:
+				std::vector<std::string> m_errors;
+				std::string esac;
+				int    m_dots        = 0;
+				int    m_underscores = 0;
+				int    m_octave      = 0;
+				int    m_degree      = 0;  // scale degree (wrt major key)
+				int    m_b40degree   = 0;  // scale degree as b40 interval
+				int    m_alter       = 0;  // chromatic alteration of degree (flats/sharp from major scale degrees)
+				double m_ticks       = 0.0;
+				bool   m_tieBegin    = false;
+				bool   m_tieEnd      = false;
+				bool   m_phraseBegin = false;
+				bool   m_phraseEnd   = false;
+				std::string m_humdrum; // **kern conversion of EsAC note
+				int    m_b40         = 0;  // absolute b40 pitch (-1000 = rest);
+				int    m_b12         = 0;  // MIDI note number (-1000 = rest);
+				HumNum m_factor      = 1;  // for triplet which is 2/3 duration
+
+				void calculateRhythms(int minrhy);
+				void calculatePitches(int tonic);
+				bool parseNote(const std::string& note, HumNum factor);
+				void generateHumdrum(int minrhy, int b40tonic);
+				bool isPitch(void);
+				bool isRest(void);
+				std::string getScaleDegree(void);
+		};
+
+		class Measure : public std::vector<Tool_esac2hum::Note> {
+			public:
+				std::vector<std::string> m_errors;
+				std::string esac;
+				int m_barnum = -1000; // -1000 == unassigned bar number for this measure
+				// m_barnum = -1 == invisible barline (between two partial measures)
+				// m_barnum =  0 == pickup measure (partial measure at start of music)
+				double m_ticks = 0.0;
+				double m_tsticks = 0.0;
+				// m_measureTimeSignature is a **kern time signature
+				// (change) to display in the converted score.
+				std::string m_measureTimeSignature = "";
+				bool m_partialBegin = false;  // start of an incomplete measure
+				bool m_partialEnd = false;    // end of an incomplete measure (pickup)
+				bool m_complete = false;      // a complste measure
+				void calculateRhythms(int minrhy);
+				void calculatePitches(int tonic);
+				bool parseMeasure(const std::string& measure);
+				bool isUnassigned(void);
+				void setComplete(void);
+				bool isComplete(void);
+				void setPartialBegin(void);
+				bool isPartialBegin(void);
+				void setPartialEnd(void);
+				bool isPartialEnd(void);
+		};
+
+		class Phrase : public std::vector<Tool_esac2hum::Measure> {
+			public:
+				std::vector<std::string> m_errors;
+				double m_ticks = 0.0;
+				std::string esac;
+				void calculateRhythms(int minrhy);
+				void calculatePitches(int tonic);
+				bool parsePhrase(const std::string& phrase);
+				std::string getLastScaleDegree();
+				void getNoteList(std::vector<Tool_esac2hum::Note*>& notelist);
+				std::string getNO_REP(void);
+				int getFullMeasureCount(void);
+		};
+
+		class Score : public std::vector<Tool_esac2hum::Phrase> {
+			public:
+				int m_b40tonic = 0;
+				int m_minrhy   = 0;
+				std::string m_clef;
+				std::string m_keysignature;
+				std::string m_keydesignation;
+				std::string m_timesig;
+				std::map<std::string, std::string> m_params;
+				std::vector<std::string> m_errors;
+				bool m_finalBarline = false;
+				bool hasFinalBarline(void) { return m_finalBarline; }
+				void calculateRhythms(int minrhy);
+				void calculatePitches(int tonic);
+				bool parseMel(const std::string& mel);
+				void analyzeTies(void);
+				void analyzePhrases(void);
+				void getNoteList(std::vector<Tool_esac2hum::Note*>& notelist);
+				void getMeasureList(std::vector<Tool_esac2hum::Measure*>& measurelist);
+				void getPhraseNoteList(std::vector<Tool_esac2hum::Note*>& notelist, int index);
+				void generateHumdrumNotes(void);
+				void calculateClef(void);
+				void calculateKeyInformation(void);
+				void calculateTimeSignatures(void);
+				void setAllTimesigTicks(double ticks);
+				void assignFreeMeasureNumbers(void);
+				void assignSingleMeasureNumbers(void);
+				void prepareMultipleTimeSignatures(const std::string& ts);
+
+				void doAnalyses(void);
+				void analyzeMEL_SEM(void);
+				void analyzeMEL_RAW(void);
+				void analyzeNO_REP(void);
+				void analyzeRTM(void);
+				void analyzeSCL_DEG(void);
+				void analyzeSCL_SEM(void);
+				void analyzePHR_NO(void);
+				void analyzePHR_BARS(void);
+				void analyzePHR_CAD(void);
+				void analyzeACC(void);
+		};
+
+		            Tool_esac2hum    (void);
+		           ~Tool_esac2hum    () {};
+
+		bool       convertFile          (std::ostream& out, const std::string& filename);
+		bool       convert              (std::ostream& out, const std::string& input);
+		bool       convert              (std::ostream& out, std::istream& input);
+
+
+	protected:
+		void        initialize          (void);
+
+		void        convertEsacToHumdrum(std::ostream& output, std::istream& infile);
+		bool        getSong             (std::vector<std::string>& song, std::istream& infile);
+		void        convertSong         (std::ostream& output, std::vector<std::string>& infile);
+		static std::string trimSpaces   (const std::string& input);
+		void        printHeader         (std::ostream& output);
+		void        printFooter         (std::ostream& output, std::vector<std::string>& infile);
+		void        printConversionDate (std::ostream& output);
+		void        printPdfLinks       (std::ostream& output);
+		void        printParameters     (void);
+		void        printPageNumbers    (std::ostream& output);
+		void        getParameters       (std::vector<std::string>& infile);
+		void        cleanText           (std::string& buffer);
+		std::string createFilename      (void);
+		void        printBemComment     (std::ostream& output);
+		void        processSong         (void);
+		void        printScoreContents  (std::ostream& output);
+		void        embedAnalyses       (std::ostream& output);
+
+	private:
+		bool        m_debugQ     = false;  // used with --debug option
+		bool        m_verboseQ   = false;  // used with --verbose option
+		std::string m_verbose;             //    p = print EsAC phrases, m = print measures, n = print notes.
+		                                   //    t after p, m, or n means print tick info
+		bool        m_embedEsacQ = true;   // used with -E option
+		bool        m_dwokQ      = false;  // true if source is Oskar Kolberg: Dzieła Wszystkie
+		                                   // (Oskar Kolberg: Complete Works)
+		                                   // determined automatically if header line or TRD source contains "DWOK" string.
+		bool        m_analysisQ  = false;  // used with -a option
+
+		int         m_inputline = 0;       // used to keep track if the EsAC input line.
+
+		std::string m_filePrefix;
+		std::string m_filePostfix = ".krn";
+		bool m_fileTitleQ = false;
+
+		std::string m_prevline;
+		std::string m_cutline;
+		std::vector<std::string> m_globalComments;
+
+		int m_minrhy = 0;
+
+		Tool_esac2hum::Score m_score;
+
+		std::map<std::string, std::string> m_bem_translation = {
+			{"Czwarta zwrotka pieśni Niech będzie Jezus Chrystus pochwalony", "Fourth verse of the song \"Let Jesus Christ be praised\""},
+			{"Do oczepin, zakodować w A?", "For the unveiling, encode in A?"},
+			{"Do oczepin", "For the unveiling"},
+			{"Druga zwrotka poprzedniej pieśni", "Second verse of the previous song"},
+			{"Gdy zdejmują wianek", "When they remove the wreath"},
+			{"Jak do ślubu odjeżdżają (na wozie)", "As they depart for the wedding (on a wagon)"},
+			{"Krakowiak", "Krakowiak"},
+			{"Marsz (konfederatów Barskich) Przygrywka na trąbie", "March (of the Bar Confederates) Prelude on trumpet"},
+			{"Marsz konfederatów Barskich", "March of the Bar Confederates"},
+			{"Mazur", "Mazur"},
+			{"Na przodziek", "At the front"},
+			{"Owczarek gładki, szybko tańczony", "Smooth shepherd's dance, danced quickly"},
+			{"Owczarek", "Shepherd's dance"},
+			{"Piosenka żniwiarska", "Harvest song"},
+			{"Polonez (pokutującego wojaka)", "Polonaise (of the penitent soldier)"},
+			{"Polonez", "Polonaise"},
+			{"Polski chodzony", "Polish walking dance"},
+			{"Prawdopodobnie ośmiomiar 2+3+3", "Probably an eight-measure 2+3+3"},
+			{"Prawdopodobnie rytm jambiczny", "Probably iambic rhythm"},
+			{"Przy przenosinach", "During the moving"},
+			{"Tańczy na przodziek (na weselu)", "Dances at the front (at the wedding)"},
+			{"W sobotę gdy wianki wiją", "On Saturday when they weave the wreaths"},
+			{"Wesele do ślubu", "Wedding to the church"},
+			{"Wesele", "Wedding"},
+			{"Weselna gdy zbierają składkę", "Wedding song when collecting contributions"},
+			{"Weselna", "Wedding song"},
+			{"Wielkanoc", "Easter"},
+			{"Wieniec", "Wreath"},
+			{"Zakodować w C?", "Encode in C?"},
+			{"w t. 10 wpisany tryl dziadowski 43b21", "in measure 10, a beggar's trill written 43b21"},
+			{"żniwiarska", "Harvest song"}
+		};
+
+
+};
+
+
 
 #define ND_NOTE 0  /* notes or rests + text and phrase markings */
 #define ND_BAR  1  /* explicit barlines */
@@ -7387,10 +7595,10 @@ class NoteData {
 
 
 
-class Tool_esac2hum : public HumTool {
+class Tool_esac2humold : public HumTool {
 	public:
-		         Tool_esac2hum         (void);
-		        ~Tool_esac2hum         () {};
+		        Tool_esac2humold    (void);
+		       ~Tool_esac2humold    () {};
 
 		bool    convertFile          (std::ostream& out, const std::string& filename);
 		bool    convert              (std::ostream& out, const std::string& input);
@@ -7438,16 +7646,18 @@ class Tool_esac2hum : public HumTool {
 		void      printHumdrumFooterInfo(std::ostream& out, std::vector<std::string>& song);
 
 	private:
-		int            debugQ = 0;        // used with --debug option
-		int            verboseQ = 0;      // used with -v option
-		int            splitQ = 0;        // used with -s option
-		int            firstfilenum = 1;  // used with -f option
-		std::vector<std::string> header;            // used with -h option
-		std::vector<std::string> trailer;           // used with -t option
-		std::string         fileextension;     // used with -x option
-		std::string         namebase;          // used with -s option
-
-		std::vector<int>    chartable;  // used printChars() & printSpecialChars()
+		int            debugQ = 0;         // used with --debug option
+		int            verboseQ = 0;       // used with -v option
+		int            splitQ = 0;         // used with -s option
+		int            firstfilenum = 1;   // used with -f option
+		std::vector<std::string> header;   // used with -h option
+		std::vector<std::string> trailer;  // used with -t option
+		std::string         fileextension; // used with -x option
+		std::string         namebase;      // used with -s option
+
+		// Modern ESaC files use UTF-8 characters, older ESaC files use
+		//  ASCII encodings of non-UTF7 characters:
+		std::vector<int>    chartable;  // used in printChars() & printSpecialChars()
 		int inputline = 0;
 
 };
@@ -10939,6 +11149,94 @@ class Tool_tabber : public HumTool {
 
 
 
+class Tool_tandeminfo : public HumTool {
+	public:
+	class Entry {
+		public:
+			HTp token = NULL;
+			std::string description;
+			int count = 0;
+	};
+
+		         Tool_tandeminfo   (void);
+		        ~Tool_tandeminfo   () {};
+
+		bool     run               (HumdrumFileSet& infiles);
+		bool     run               (HumdrumFile& infile);
+		bool     run               (const std::string& indata, std::ostream& out);
+		bool     run               (HumdrumFile& infile, std::ostream& out);
+
+
+	protected:
+		void     initialize        (void);
+		void     processFile       (HumdrumFile& infile);
+		void     printEntries      (HumdrumFile& infile);
+		void     printEntriesHtml  (HumdrumFile& infile);
+		void     printEntriesText  (HumdrumFile& infile);
+		void     doCountAnalysis   (void);
+
+		std::string getDescription         (HTp token);
+		std::string checkForKeySignature   (const std::string& tok);
+		std::string checkForKeyDesignation (const std::string& tok);
+		std::string checkForInstrumentInfo (const std::string& tok);
+		std::string checkForLabelInfo      (const std::string& tok);
+		std::string checkForTimeSignature  (const std::string& tok);
+		std::string checkForMeter          (const std::string& tok);
+		std::string checkForTempoMarking   (const std::string& tok);
+		std::string checkForClef           (const std::string& tok);
+		std::string checkForStaffPartGroup (const std::string& tok);
+		std::string checkForTuplet         (const std::string& tok);
+		std::string checkForHands          (const std::string& tok);
+		std::string checkForPosition       (const std::string& tok);
+		std::string checkForCue            (const std::string& tok);
+		std::string checkForFlip           (const std::string& tok);
+		std::string checkForTremolo        (const std::string& tok);
+		std::string checkForOttava         (const std::string& tok);
+		std::string checkForPedal          (const std::string& tok);
+		std::string checkForBracket        (const std::string& tok);
+		std::string checkForRscale         (const std::string& tok);
+		std::string checkForTimebase       (const std::string& tok);
+		std::string checkForTransposition  (const std::string& tok);
+		std::string checkForGrp            (const std::string& tok);
+		std::string checkForStria          (const std::string& tok);
+		std::string checkForFont           (const std::string& tok);
+		std::string checkForVerseLabels    (const std::string& tok);
+		std::string checkForLanguage       (const std::string& tok);
+		std::string checkForStemInfo       (const std::string& tok);
+		std::string checkForXywh           (const std::string& tok);
+		std::string checkForCustos         (const std::string& tok);
+		std::string checkForTextInterps    (const std::string& tok);
+		std::string checkForRep            (const std::string& tok);
+		std::string checkForPline          (const std::string& tok);
+		std::string checkForTacet          (const std::string& tok);
+		std::string checkForFb             (const std::string& tok);
+		std::string checkForColor          (const std::string& tok);
+		std::string checkForThru           (const std::string& tok);
+
+	private:
+		bool m_exclusiveQ   = true;   // used with -X option (don't print exclusive interpretation)
+		bool m_unknownQ     = false;  // used with -u option (print only unknown tandem interpretations)
+		bool m_filenameQ    = false;  // used with -f option (print only unknown tandem interpretations)
+		bool m_descriptionQ = false;  // used with -m option (print description of interpretation)
+		bool m_locationQ    = false;  // used with -l option (print location of interpretation in file)
+		bool m_zeroQ        = false;  // used with -z option (location address by 0-index)
+		bool m_tableQ       = false;  // used with -t option (display results as HTML table)
+		bool m_closeQ       = false;  // used with --close option (HTML details closed by default)
+		bool m_sortQ        = false;  // used with -s (sort entries alphabetically by tandem interpretation)
+		bool m_headerOnlyQ  = false;  // used with -h option (process only header interpretations)
+		bool m_bodyOnlyQ    = false;  // used with -H option (process only body interpretations)
+		bool m_countQ       = false;  // used with -c option (only show unique list with counts);
+		bool m_sortByCountQ = false;  // used with -c and -n options (sort from low to high count)
+		bool m_sortByReverseCountQ = false;  // used with -c and -N options (sort from high to low count)
+		bool m_humdrumQ     = false;  // used with --humdrum option (output data formatted with Humdrum syntax)
+
+		std::string m_unknown = "unknown";
+
+		std::vector<Tool_tandeminfo::Entry> m_entries;
+		std::map<std::string, int> m_count;
+};
+
+
 class Tool_tassoize : public HumTool {
 	public:
 		         Tool_tassoize   (void);
diff --git a/src/hum/humlib.cpp b/src/hum/humlib.cpp
index 1b5e14d1d75..0e37f29e2d9 100644
--- a/src/hum/humlib.cpp
+++ b/src/hum/humlib.cpp
@@ -1,7 +1,7 @@
 //
 // Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
 // Creation Date: Sat Aug  8 12:24:49 PDT 2015
-// Last Modified: Thu Aug  8 21:55:11 PDT 2024
+// Last Modified: Thu Sep  5 14:41:50 PDT 2024
 // Filename:      min/humlib.cpp
 // URL:           https://github.com/craigsapp/humlib/blob/master/min/humlib.cpp
 // Syntax:        C++11
@@ -4255,721 +4255,721 @@ string Convert::getLanguageName(const string& abbreviation) {
 		if (abbreviation[i] == '@') {
 			continue;
 		}
-		code.push_back(tolower(abbreviation[i]));
+		code.push_back(toupper(abbreviation[i]));
 	}
 
 	if (code.size() == 2) {
 		// ISO 639-1 language codes
 
-		if (code == "aa") { return "Afar"; }
-		if (code == "ab") { return "Abkhazian"; }
-		if (code == "ae") { return "Avestan"; }
-		if (code == "af") { return "Afrikaans"; }
-		if (code == "ak") { return "Akan"; }
-		if (code == "am") { return "Amharic"; }
-		if (code == "an") { return "Aragonese"; }
-		if (code == "ar") { return "Arabic"; }
-		if (code == "as") { return "Assamese"; }
-		if (code == "av") { return "Avaric"; }
-		if (code == "ay") { return "Aymara"; }
-		if (code == "az") { return "Azerbaijani"; }
-		if (code == "ba") { return "Bashkir"; }
-		if (code == "be") { return "Belarusian"; }
-		if (code == "bg") { return "Bulgarian"; }
-		if (code == "bh") { return "Bihari languages"; }
-		if (code == "bi") { return "Bislama"; }
-		if (code == "bm") { return "Bambara"; }
-		if (code == "bn") { return "Bengali"; }
-		if (code == "bo") { return "Tibetan"; }
-		if (code == "br") { return "Breton"; }
-		if (code == "bs") { return "Bosnian"; }
-		if (code == "ca") { return "Catalan"; }
-		if (code == "ce") { return "Chechen"; }
-		if (code == "ch") { return "Chamorro"; }
-		if (code == "co") { return "Corsican"; }
-		if (code == "cr") { return "Cree"; }
-		if (code == "cs") { return "Czech"; }
-		if (code == "cs") { return "Czech"; }
-		if (code == "cu") { return "Church Slavic"; }
-		if (code == "cv") { return "Chuvash"; }
-		if (code == "cy") { return "Welsh"; }
-		if (code == "cy") { return "Welsh"; }
-		if (code == "da") { return "Danish"; }
-		if (code == "de") { return "German"; }
-		if (code == "dv") { return "Divehi"; }
-		if (code == "dz") { return "Dzongkha"; }
-		if (code == "ee") { return "Ewe"; }
-		if (code == "el") { return "Greek, Modern (1453-)"; }
-		if (code == "en") { return "English"; }
-		if (code == "eo") { return "Esperanto"; }
-		if (code == "es") { return "Spanish"; }
-		if (code == "et") { return "Estonian"; }
-		if (code == "eu") { return "Basque"; }
-		if (code == "eu") { return "Basque"; }
-		if (code == "fa") { return "Persian"; }
-		if (code == "ff") { return "Fulah"; }
-		if (code == "fi") { return "Finnish"; }
-		if (code == "fj") { return "Fijian"; }
-		if (code == "fo") { return "Faroese"; }
-		if (code == "fr") { return "French"; }
-		if (code == "fy") { return "Western Frisian"; }
-		if (code == "ga") { return "Irish"; }
-		if (code == "gd") { return "Gaelic"; }
-		if (code == "gl") { return "Galician"; }
-		if (code == "gn") { return "Guarani"; }
-		if (code == "gu") { return "Gujarati"; }
-		if (code == "gv") { return "Manx"; }
-		if (code == "ha") { return "Hausa"; }
-		if (code == "he") { return "Hebrew"; }
-		if (code == "hi") { return "Hindi"; }
-		if (code == "ho") { return "Hiri Motu"; }
-		if (code == "hr") { return "Croatian"; }
-		if (code == "ht") { return "Haitian"; }
-		if (code == "hu") { return "Hungarian"; }
-		if (code == "hy") { return "Armenian"; }
-		if (code == "hz") { return "Herero"; }
-		if (code == "ia") { return "Interlingua"; }
-		if (code == "id") { return "Indonesian"; }
-		if (code == "ie") { return "Interlingue"; }
-		if (code == "ig") { return "Igbo"; }
-		if (code == "ii") { return "Sichuan Yi"; }
-		if (code == "ik") { return "Inupiaq"; }
-		if (code == "io") { return "Ido"; }
-		if (code == "is") { return "Icelandic"; }
-		if (code == "it") { return "Italian"; }
-		if (code == "iu") { return "Inuktitut"; }
-		if (code == "ja") { return "Japanese"; }
-		if (code == "jv") { return "Javanese"; }
-		if (code == "ka") { return "Georgian"; }
-		if (code == "kg") { return "Kongo"; }
-		if (code == "ki") { return "Kikuyu"; }
-		if (code == "kj") { return "Kuanyama"; }
-		if (code == "kk") { return "Kazakh"; }
-		if (code == "kl") { return "Greenlandic"; }
-		if (code == "km") { return "Central Khmer"; }
-		if (code == "kn") { return "Kannada"; }
-		if (code == "ko") { return "Korean"; }
-		if (code == "kr") { return "Kanuri"; }
-		if (code == "ks") { return "Kashmiri"; }
-		if (code == "ku") { return "Kurdish"; }
-		if (code == "kv") { return "Komi"; }
-		if (code == "kw") { return "Cornish"; }
-		if (code == "ky") { return "Kirghiz"; }
-		if (code == "la") { return "Latin"; }
-		if (code == "lb") { return "Luxembourgish"; }
-		if (code == "lg") { return "Ganda"; }
-		if (code == "li") { return "Limburgan"; }
-		if (code == "ln") { return "Lingala"; }
-		if (code == "lo") { return "Lao"; }
-		if (code == "lt") { return "Lithuanian"; }
-		if (code == "lu") { return "Luba-Katanga"; }
-		if (code == "lv") { return "Latvian"; }
-		if (code == "mg") { return "Malagasy"; }
-		if (code == "mh") { return "Marshallese"; }
-		if (code == "mi") { return "Maori"; }
-		if (code == "mk") { return "Macedonian"; }
-		if (code == "mk") { return "Macedonian"; }
-		if (code == "ml") { return "Malayalam"; }
-		if (code == "mn") { return "Mongolian"; }
-		if (code == "mr") { return "Marathi"; }
-		if (code == "ms") { return "Malay"; }
-		if (code == "mt") { return "Maltese"; }
-		if (code == "my") { return "Burmese"; }
-		if (code == "my") { return "Burmese"; }
-		if (code == "na") { return "Nauru"; }
-		if (code == "nb") { return "Bokmål, Norwegian"; }
-		if (code == "nd") { return "Ndebele, North"; }
-		if (code == "ne") { return "Nepali"; }
-		if (code == "ng") { return "Ndonga"; }
-		if (code == "nl") { return "Dutch"; }
-		if (code == "nl") { return "Dutch"; }
-		if (code == "nn") { return "Norwegian Nynorsk"; }
-		if (code == "no") { return "Norwegian"; }
-		if (code == "nr") { return "Ndebele, South"; }
-		if (code == "nv") { return "Navajo"; }
-		if (code == "ny") { return "Chichewa"; }
-		if (code == "oc") { return "Occitan (post 1500)"; }
-		if (code == "oj") { return "Ojibwa"; }
-		if (code == "om") { return "Oromo"; }
-		if (code == "or") { return "Oriya"; }
-		if (code == "os") { return "Ossetian"; }
-		if (code == "pa") { return "Panjabi"; }
-		if (code == "pi") { return "Pali"; }
-		if (code == "pl") { return "Polish"; }
-		if (code == "ps") { return "Pushto"; }
-		if (code == "pt") { return "Portuguese"; }
-		if (code == "qu") { return "Quechua"; }
-		if (code == "rm") { return "Romansh"; }
-		if (code == "rn") { return "Rundi"; }
-		if (code == "ro") { return "Romanian"; }
-		if (code == "ru") { return "Russian"; }
-		if (code == "rw") { return "Kinyarwanda"; }
-		if (code == "sa") { return "Sanskrit"; }
-		if (code == "sc") { return "Sardinian"; }
-		if (code == "sd") { return "Sindhi"; }
-		if (code == "se") { return "Northern Sami"; }
-		if (code == "sg") { return "Sango"; }
-		if (code == "si") { return "Sinhala"; }
-		if (code == "sl") { return "Slovenian"; }
-		if (code == "sm") { return "Samoan"; }
-		if (code == "sn") { return "Shona"; }
-		if (code == "so") { return "Somali"; }
-		if (code == "sq") { return "Albanian"; }
-		if (code == "sr") { return "Serbian"; }
-		if (code == "ss") { return "Swati"; }
-		if (code == "st") { return "Sotho, Southern"; }
-		if (code == "su") { return "Sundanese"; }
-		if (code == "sv") { return "Swedish"; }
-		if (code == "sw") { return "Swahili"; }
-		if (code == "ta") { return "Tamil"; }
-		if (code == "te") { return "Telugu"; }
-		if (code == "tg") { return "Tajik"; }
-		if (code == "th") { return "Thai"; }
-		if (code == "ti") { return "Tigrinya"; }
-		if (code == "tk") { return "Turkmen"; }
-		if (code == "tl") { return "Tagalog"; }
-		if (code == "tn") { return "Tswana"; }
-		if (code == "to") { return "Tonga (Tonga Islands)"; }
-		if (code == "tr") { return "Turkish"; }
-		if (code == "ts") { return "Tsonga"; }
-		if (code == "tt") { return "Tatar"; }
-		if (code == "tw") { return "Twi"; }
-		if (code == "ty") { return "Tahitian"; }
-		if (code == "ug") { return "Uighur"; }
-		if (code == "uk") { return "Ukrainian"; }
-		if (code == "ur") { return "Urdu"; }
-		if (code == "uz") { return "Uzbek"; }
-		if (code == "ve") { return "Venda"; }
-		if (code == "vi") { return "Vietnamese"; }
-		if (code == "vo") { return "Volapük"; }
-		if (code == "wa") { return "Walloon"; }
-		if (code == "wo") { return "Wolof"; }
-		if (code == "xh") { return "Xhosa"; }
-		if (code == "yi") { return "Yiddish"; }
-		if (code == "yo") { return "Yoruba"; }
-		if (code == "za") { return "Zhuang"; }
-		if (code == "zh") { return "Chinese"; }
-		if (code == "zu") { return "Zulu"; }
+		if (code == "AA") { return "Afar"; }
+		if (code == "AB") { return "Abkhazian"; }
+		if (code == "AE") { return "Avestan"; }
+		if (code == "AF") { return "Afrikaans"; }
+		if (code == "AK") { return "Akan"; }
+		if (code == "AM") { return "Amharic"; }
+		if (code == "AN") { return "Aragonese"; }
+		if (code == "AR") { return "Arabic"; }
+		if (code == "AS") { return "Assamese"; }
+		if (code == "AV") { return "Avaric"; }
+		if (code == "AY") { return "Aymara"; }
+		if (code == "AZ") { return "Azerbaijani"; }
+		if (code == "BA") { return "Bashkir"; }
+		if (code == "BE") { return "Belarusian"; }
+		if (code == "BG") { return "Bulgarian"; }
+		if (code == "BH") { return "Bihari languages"; }
+		if (code == "BI") { return "Bislama"; }
+		if (code == "BM") { return "Bambara"; }
+		if (code == "BN") { return "Bengali"; }
+		if (code == "BO") { return "Tibetan"; }
+		if (code == "BR") { return "Breton"; }
+		if (code == "BS") { return "Bosnian"; }
+		if (code == "CA") { return "Catalan"; }
+		if (code == "CE") { return "Chechen"; }
+		if (code == "CH") { return "Chamorro"; }
+		if (code == "CO") { return "Corsican"; }
+		if (code == "CR") { return "Cree"; }
+		if (code == "CS") { return "Czech"; }
+		if (code == "CS") { return "Czech"; }
+		if (code == "CU") { return "Church Slavic"; }
+		if (code == "CV") { return "Chuvash"; }
+		if (code == "CY") { return "Welsh"; }
+		if (code == "CY") { return "Welsh"; }
+		if (code == "DA") { return "Danish"; }
+		if (code == "DE") { return "German"; }
+		if (code == "DV") { return "Divehi"; }
+		if (code == "DZ") { return "Dzongkha"; }
+		if (code == "EE") { return "Ewe"; }
+		if (code == "EL") { return "Greek, Modern (1453-)"; }
+		if (code == "EN") { return "English"; }
+		if (code == "EO") { return "Esperanto"; }
+		if (code == "ES") { return "Spanish"; }
+		if (code == "ET") { return "Estonian"; }
+		if (code == "EU") { return "Basque"; }
+		if (code == "EU") { return "Basque"; }
+		if (code == "FA") { return "Persian"; }
+		if (code == "FF") { return "Fulah"; }
+		if (code == "FI") { return "Finnish"; }
+		if (code == "FJ") { return "Fijian"; }
+		if (code == "FO") { return "Faroese"; }
+		if (code == "FR") { return "French"; }
+		if (code == "FY") { return "Western Frisian"; }
+		if (code == "GA") { return "Irish"; }
+		if (code == "GD") { return "Gaelic"; }
+		if (code == "GL") { return "Galician"; }
+		if (code == "GN") { return "Guarani"; }
+		if (code == "GU") { return "Gujarati"; }
+		if (code == "GV") { return "Manx"; }
+		if (code == "HA") { return "Hausa"; }
+		if (code == "HE") { return "Hebrew"; }
+		if (code == "HI") { return "Hindi"; }
+		if (code == "HO") { return "Hiri Motu"; }
+		if (code == "HR") { return "Croatian"; }
+		if (code == "HT") { return "Haitian"; }
+		if (code == "HU") { return "Hungarian"; }
+		if (code == "HY") { return "Armenian"; }
+		if (code == "HZ") { return "Herero"; }
+		if (code == "IA") { return "Interlingua"; }
+		if (code == "ID") { return "Indonesian"; }
+		if (code == "IE") { return "Interlingue"; }
+		if (code == "IG") { return "Igbo"; }
+		if (code == "II") { return "Sichuan Yi"; }
+		if (code == "IK") { return "Inupiaq"; }
+		if (code == "IO") { return "Ido"; }
+		if (code == "IS") { return "Icelandic"; }
+		if (code == "IT") { return "Italian"; }
+		if (code == "IU") { return "Inuktitut"; }
+		if (code == "JA") { return "Japanese"; }
+		if (code == "JV") { return "Javanese"; }
+		if (code == "KA") { return "Georgian"; }
+		if (code == "KG") { return "Kongo"; }
+		if (code == "KI") { return "Kikuyu"; }
+		if (code == "KJ") { return "Kuanyama"; }
+		if (code == "KK") { return "Kazakh"; }
+		if (code == "KL") { return "Greenlandic"; }
+		if (code == "KM") { return "Central Khmer"; }
+		if (code == "KN") { return "Kannada"; }
+		if (code == "KO") { return "Korean"; }
+		if (code == "KR") { return "Kanuri"; }
+		if (code == "KS") { return "Kashmiri"; }
+		if (code == "KU") { return "Kurdish"; }
+		if (code == "KV") { return "Komi"; }
+		if (code == "KW") { return "Cornish"; }
+		if (code == "KY") { return "Kirghiz"; }
+		if (code == "LA") { return "Latin"; }
+		if (code == "LB") { return "Luxembourgish"; }
+		if (code == "LG") { return "Ganda"; }
+		if (code == "LI") { return "Limburgan"; }
+		if (code == "LN") { return "Lingala"; }
+		if (code == "LO") { return "Lao"; }
+		if (code == "LT") { return "Lithuanian"; }
+		if (code == "LU") { return "Luba-Katanga"; }
+		if (code == "LV") { return "Latvian"; }
+		if (code == "MG") { return "Malagasy"; }
+		if (code == "MH") { return "Marshallese"; }
+		if (code == "MI") { return "Maori"; }
+		if (code == "MK") { return "Macedonian"; }
+		if (code == "MK") { return "Macedonian"; }
+		if (code == "ML") { return "Malayalam"; }
+		if (code == "MN") { return "Mongolian"; }
+		if (code == "MR") { return "Marathi"; }
+		if (code == "MS") { return "Malay"; }
+		if (code == "MT") { return "Maltese"; }
+		if (code == "MY") { return "Burmese"; }
+		if (code == "MY") { return "Burmese"; }
+		if (code == "NA") { return "Nauru"; }
+		if (code == "NB") { return "Bokmål, Norwegian"; }
+		if (code == "ND") { return "Ndebele, North"; }
+		if (code == "NE") { return "Nepali"; }
+		if (code == "NG") { return "Ndonga"; }
+		if (code == "NL") { return "Dutch"; }
+		if (code == "NL") { return "Dutch"; }
+		if (code == "NN") { return "Norwegian Nynorsk"; }
+		if (code == "NO") { return "Norwegian"; }
+		if (code == "NR") { return "Ndebele, South"; }
+		if (code == "NV") { return "Navajo"; }
+		if (code == "NY") { return "Chichewa"; }
+		if (code == "OC") { return "Occitan (post 1500)"; }
+		if (code == "OJ") { return "Ojibwa"; }
+		if (code == "OM") { return "Oromo"; }
+		if (code == "OR") { return "Oriya"; }
+		if (code == "OS") { return "Ossetian"; }
+		if (code == "PA") { return "Panjabi"; }
+		if (code == "PI") { return "Pali"; }
+		if (code == "PL") { return "Polish"; }
+		if (code == "PS") { return "Pushto"; }
+		if (code == "PT") { return "Portuguese"; }
+		if (code == "QU") { return "Quechua"; }
+		if (code == "RM") { return "Romansh"; }
+		if (code == "RN") { return "Rundi"; }
+		if (code == "RO") { return "Romanian"; }
+		if (code == "RU") { return "Russian"; }
+		if (code == "RW") { return "Kinyarwanda"; }
+		if (code == "SA") { return "Sanskrit"; }
+		if (code == "SC") { return "Sardinian"; }
+		if (code == "SD") { return "Sindhi"; }
+		if (code == "SE") { return "Northern Sami"; }
+		if (code == "SG") { return "Sango"; }
+		if (code == "SI") { return "Sinhala"; }
+		if (code == "SL") { return "Slovenian"; }
+		if (code == "SM") { return "Samoan"; }
+		if (code == "SN") { return "Shona"; }
+		if (code == "SO") { return "Somali"; }
+		if (code == "SQ") { return "Albanian"; }
+		if (code == "SR") { return "Serbian"; }
+		if (code == "SS") { return "Swati"; }
+		if (code == "ST") { return "Sotho, Southern"; }
+		if (code == "SU") { return "Sundanese"; }
+		if (code == "SV") { return "Swedish"; }
+		if (code == "SW") { return "Swahili"; }
+		if (code == "TA") { return "Tamil"; }
+		if (code == "TE") { return "Telugu"; }
+		if (code == "TG") { return "Tajik"; }
+		if (code == "TH") { return "Thai"; }
+		if (code == "TI") { return "Tigrinya"; }
+		if (code == "TK") { return "Turkmen"; }
+		if (code == "TL") { return "Tagalog"; }
+		if (code == "TN") { return "Tswana"; }
+		if (code == "TO") { return "Tonga (Tonga Islands)"; }
+		if (code == "TR") { return "Turkish"; }
+		if (code == "TS") { return "Tsonga"; }
+		if (code == "TT") { return "Tatar"; }
+		if (code == "TW") { return "Twi"; }
+		if (code == "TY") { return "Tahitian"; }
+		if (code == "UG") { return "Uighur"; }
+		if (code == "UK") { return "Ukrainian"; }
+		if (code == "UR") { return "Urdu"; }
+		if (code == "UZ") { return "Uzbek"; }
+		if (code == "VE") { return "Venda"; }
+		if (code == "VI") { return "Vietnamese"; }
+		if (code == "VO") { return "Volapük"; }
+		if (code == "WA") { return "Walloon"; }
+		if (code == "WO") { return "Wolof"; }
+		if (code == "XH") { return "Xhosa"; }
+		if (code == "YI") { return "Yiddish"; }
+		if (code == "YO") { return "Yoruba"; }
+		if (code == "ZA") { return "Zhuang"; }
+		if (code == "ZH") { return "Chinese"; }
+		if (code == "ZU") { return "Zulu"; }
 
 	} else if (code.size() == 3) {
 		// ISO 639-2 language codes
 
-		if (code == "aar") { return "Afar"; }
-		if (code == "abk") { return "Abkhazian"; }
-		if (code == "ace") { return "Achinese"; }
-		if (code == "ach") { return "Acoli"; }
-		if (code == "ada") { return "Adangme"; }
-		if (code == "ady") { return "Adyghe"; }
-		if (code == "afa") { return "Afro-Asiatic languages"; }
-		if (code == "afh") { return "Afrihili"; }
-		if (code == "afr") { return "Afrikaans"; }
-		if (code == "ain") { return "Ainu"; }
-		if (code == "aka") { return "Akan"; }
-		if (code == "akk") { return "Akkadian"; }
-		if (code == "alb") { return "Albanian"; }
-		if (code == "ale") { return "Aleut"; }
-		if (code == "alg") { return "Algonquian languages"; }
-		if (code == "alt") { return "Southern Altai"; }
-		if (code == "amh") { return "Amharic"; }
-		if (code == "ang") { return "English, Old (ca.450-1100)"; }
-		if (code == "anp") { return "Angika"; }
-		if (code == "apa") { return "Apache languages"; }
-		if (code == "ara") { return "Arabic"; }
-		if (code == "arc") { return "Aramaic (700-300 BCE)"; }
-		if (code == "arg") { return "Aragonese"; }
-		if (code == "arm") { return "Armenian"; }
-		if (code == "arn") { return "Mapudungun"; }
-		if (code == "arp") { return "Arapaho"; }
-		if (code == "art") { return "Artificial languages"; }
-		if (code == "arw") { return "Arawak"; }
-		if (code == "asm") { return "Assamese"; }
-		if (code == "ast") { return "Asturian"; }
-		if (code == "ath") { return "Athapascan languages"; }
-		if (code == "aus") { return "Australian languages"; }
-		if (code == "ava") { return "Avaric"; }
-		if (code == "ave") { return "Avestan"; }
-		if (code == "awa") { return "Awadhi"; }
-		if (code == "aym") { return "Aymara"; }
-		if (code == "aze") { return "Azerbaijani"; }
-		if (code == "bad") { return "Banda languages"; }
-		if (code == "bai") { return "Bamileke languages"; }
-		if (code == "bak") { return "Bashkir"; }
-		if (code == "bal") { return "Baluchi"; }
-		if (code == "bam") { return "Bambara"; }
-		if (code == "ban") { return "Balinese"; }
-		if (code == "baq") { return "Basque"; }
-		if (code == "baq") { return "Basque"; }
-		if (code == "bas") { return "Basa"; }
-		if (code == "bat") { return "Baltic languages"; }
-		if (code == "bej") { return "Beja"; }
-		if (code == "bel") { return "Belarusian"; }
-		if (code == "bem") { return "Bemba"; }
-		if (code == "ben") { return "Bengali"; }
-		if (code == "ber") { return "Berber languages"; }
-		if (code == "bho") { return "Bhojpuri"; }
-		if (code == "bih") { return "Bihari languages"; }
-		if (code == "bik") { return "Bikol"; }
-		if (code == "bin") { return "Bini"; }
-		if (code == "bis") { return "Bislama"; }
-		if (code == "bla") { return "Siksika"; }
-		if (code == "bnt") { return "Bantu languages"; }
-		if (code == "bod") { return "Tibetan"; }
-		if (code == "bos") { return "Bosnian"; }
-		if (code == "bra") { return "Braj"; }
-		if (code == "bre") { return "Breton"; }
-		if (code == "btk") { return "Batak languages"; }
-		if (code == "bua") { return "Buriat"; }
-		if (code == "bug") { return "Buginese"; }
-		if (code == "bul") { return "Bulgarian"; }
-		if (code == "bur") { return "Burmese"; }
-		if (code == "bur") { return "Burmese"; }
-		if (code == "byn") { return "Blin"; }
-		if (code == "cad") { return "Caddo"; }
-		if (code == "cai") { return "Central American Indian languages"; }
-		if (code == "car") { return "Galibi Carib"; }
-		if (code == "cat") { return "Catalan"; }
-		if (code == "cau") { return "Caucasian languages"; }
-		if (code == "ceb") { return "Cebuano"; }
-		if (code == "cel") { return "Celtic languages"; }
-		if (code == "ces") { return "Czech"; }
-		if (code == "ces") { return "Czech"; }
-		if (code == "cha") { return "Chamorro"; }
-		if (code == "chb") { return "Chibcha"; }
-		if (code == "che") { return "Chechen"; }
-		if (code == "chg") { return "Chagatai"; }
-		if (code == "chi") { return "Chinese"; }
-		if (code == "chk") { return "Chuukese"; }
-		if (code == "chm") { return "Mari"; }
-		if (code == "chn") { return "Chinook jargon"; }
-		if (code == "cho") { return "Choctaw"; }
-		if (code == "chp") { return "Chipewyan"; }
-		if (code == "chr") { return "Cherokee"; }
-		if (code == "chu") { return "Church Slavic"; }
-		if (code == "chv") { return "Chuvash"; }
-		if (code == "chy") { return "Cheyenne"; }
-		if (code == "cmc") { return "Chamic languages"; }
-		if (code == "cnr") { return "Montenegrin"; }
-		if (code == "cop") { return "Coptic"; }
-		if (code == "cor") { return "Cornish"; }
-		if (code == "cos") { return "Corsican"; }
-		if (code == "cpe") { return "Creoles and pidgins, English based"; }
-		if (code == "cpf") { return "Creoles and pidgins, French-based"; }
-		if (code == "cpp") { return "Creoles and pidgins, Portuguese-based"; }
-		if (code == "cre") { return "Cree"; }
-		if (code == "crh") { return "Crimean Tatar"; }
-		if (code == "crp") { return "Creoles and pidgins"; }
-		if (code == "csb") { return "Kashubian"; }
-		if (code == "cus") { return "Cushitic languages"; }
-		if (code == "cym") { return "Welsh"; }
-		if (code == "cym") { return "Welsh"; }
-		if (code == "cze") { return "Czech"; }
-		if (code == "cze") { return "Czech"; }
-		if (code == "dak") { return "Dakota"; }
-		if (code == "dan") { return "Danish"; }
-		if (code == "dar") { return "Dargwa"; }
-		if (code == "day") { return "Land Dayak languages"; }
-		if (code == "del") { return "Delaware"; }
-		if (code == "den") { return "Slave (Athapascan)"; }
-		if (code == "deu") { return "German"; }
-		if (code == "dgr") { return "Dogrib"; }
-		if (code == "din") { return "Dinka"; }
-		if (code == "div") { return "Divehi"; }
-		if (code == "doi") { return "Dogri"; }
-		if (code == "dra") { return "Dravidian languages"; }
-		if (code == "dsb") { return "Lower Sorbian"; }
-		if (code == "dua") { return "Duala"; }
-		if (code == "dum") { return "Dutch, Middle (ca.1050-1350)"; }
-		if (code == "dut") { return "Dutch"; }
-		if (code == "dut") { return "Dutch"; }
-		if (code == "dyu") { return "Dyula"; }
-		if (code == "dzo") { return "Dzongkha"; }
-		if (code == "efi") { return "Efik"; }
-		if (code == "egy") { return "Egyptian (Ancient)"; }
-		if (code == "eka") { return "Ekajuk"; }
-		if (code == "ell") { return "Greek, Modern (1453-)"; }
-		if (code == "elx") { return "Elamite"; }
-		if (code == "eng") { return "English"; }
-		if (code == "enm") { return "English, Middle (1100-1500)"; }
-		if (code == "epo") { return "Esperanto"; }
-		if (code == "est") { return "Estonian"; }
-		if (code == "eus") { return "Basque"; }
-		if (code == "eus") { return "Basque"; }
-		if (code == "ewe") { return "Ewe"; }
-		if (code == "ewo") { return "Ewondo"; }
-		if (code == "fan") { return "Fang"; }
-		if (code == "fao") { return "Faroese"; }
-		if (code == "fas") { return "Persian"; }
-		if (code == "fat") { return "Fanti"; }
-		if (code == "fij") { return "Fijian"; }
-		if (code == "fil") { return "Filipino"; }
-		if (code == "fin") { return "Finnish"; }
-		if (code == "fiu") { return "Finno-Ugrian languages"; }
-		if (code == "fon") { return "Fon"; }
-		if (code == "fra") { return "French"; }
-		if (code == "fre") { return "French"; }
-		if (code == "frm") { return "French, Middle (ca.1400-1600)"; }
-		if (code == "fro") { return "French, Old (842-ca.1400)"; }
-		if (code == "frr") { return "Northern Frisian"; }
-		if (code == "frs") { return "Eastern Frisian"; }
-		if (code == "fry") { return "Western Frisian"; }
-		if (code == "ful") { return "Fulah"; }
-		if (code == "fur") { return "Friulian"; }
-		if (code == "gaa") { return "Ga"; }
-		if (code == "gay") { return "Gayo"; }
-		if (code == "gba") { return "Gbaya"; }
-		if (code == "gem") { return "Germanic languages"; }
-		if (code == "geo") { return "Georgin"; }
-		if (code == "ger") { return "German"; }
-		if (code == "gez") { return "Geez"; }
-		if (code == "gil") { return "Gilbertese"; }
-		if (code == "gla") { return "Gaelic"; }
-		if (code == "gle") { return "Irish"; }
-		if (code == "glg") { return "Galician"; }
-		if (code == "glv") { return "Manx"; }
-		if (code == "gmh") { return "German, Middle High (ca.1050-1500)"; }
-		if (code == "goh") { return "German, Old High (ca.750-1050)"; }
-		if (code == "gon") { return "Gondi"; }
-		if (code == "gor") { return "Gorontalo"; }
-		if (code == "got") { return "Gothic"; }
-		if (code == "grb") { return "Grebo"; }
-		if (code == "grc") { return "Greek, Ancient (to 1453)"; }
-		if (code == "gre") { return "Greek"; }
-		if (code == "grn") { return "Guarani"; }
-		if (code == "gsw") { return "Swiss German"; }
-		if (code == "guj") { return "Gujarati"; }
-		if (code == "gwi") { return "Gwich'in"; }
-		if (code == "hai") { return "Haida"; }
-		if (code == "hat") { return "Haitian"; }
-		if (code == "hau") { return "Hausa"; }
-		if (code == "haw") { return "Hawaiian"; }
-		if (code == "heb") { return "Hebrew"; }
-		if (code == "her") { return "Herero"; }
-		if (code == "hil") { return "Hiligaynon"; }
-		if (code == "him") { return "Himachali languages"; }
-		if (code == "hin") { return "Hindi"; }
-		if (code == "hit") { return "Hittite"; }
-		if (code == "hmn") { return "Hmong"; }
-		if (code == "hmo") { return "Hiri Motu"; }
-		if (code == "hrv") { return "Croatian"; }
-		if (code == "hsb") { return "Upper Sorbian"; }
-		if (code == "hun") { return "Hungarian"; }
-		if (code == "hup") { return "Hupa"; }
-		if (code == "hye") { return "Armenian"; }
-		if (code == "iba") { return "Iban"; }
-		if (code == "ibo") { return "Igbo"; }
-		if (code == "ice") { return "Icelandic"; }
-		if (code == "ido") { return "Ido"; }
-		if (code == "iii") { return "Sichuan Yi"; }
-		if (code == "ijo") { return "Ijo languages"; }
-		if (code == "iku") { return "Inuktitut"; }
-		if (code == "ile") { return "Interlingue"; }
-		if (code == "ilo") { return "Iloko"; }
-		if (code == "ina") { return "Interlingua)"; }
-		if (code == "inc") { return "Indic languages"; }
-		if (code == "ind") { return "Indonesian"; }
-		if (code == "ine") { return "Indo-European languages"; }
-		if (code == "inh") { return "Ingush"; }
-		if (code == "ipk") { return "Inupiaq"; }
-		if (code == "ira") { return "Iranian languages"; }
-		if (code == "iro") { return "Iroquoian languages"; }
-		if (code == "isl") { return "Icelandic"; }
-		if (code == "ita") { return "Italian"; }
-		if (code == "jav") { return "Javanese"; }
-		if (code == "jbo") { return "Lojban"; }
-		if (code == "jpn") { return "Japanese"; }
-		if (code == "jpr") { return "Judeo-Persian"; }
-		if (code == "jrb") { return "Judeo-Arabic"; }
-		if (code == "kaa") { return "Kara-Kalpak"; }
-		if (code == "kab") { return "Kabyle"; }
-		if (code == "kac") { return "Kachin"; }
-		if (code == "kal") { return "Greenlandic"; }
-		if (code == "kam") { return "Kamba"; }
-		if (code == "kan") { return "Kannada"; }
-		if (code == "kar") { return "Karen languages"; }
-		if (code == "kas") { return "Kashmiri"; }
-		if (code == "kat") { return "Georgian"; }
-		if (code == "kau") { return "Kanuri"; }
-		if (code == "kaw") { return "Kawi"; }
-		if (code == "kaz") { return "Kazakh"; }
-		if (code == "kbd") { return "Kabardian"; }
-		if (code == "kha") { return "Khasi"; }
-		if (code == "khi") { return "Khoisan languages"; }
-		if (code == "khm") { return "Central Khmer"; }
-		if (code == "kho") { return "Khotanese"; }
-		if (code == "kik") { return "Kikuyu"; }
-		if (code == "kin") { return "Kinyarwanda"; }
-		if (code == "kir") { return "Kirghiz"; }
-		if (code == "kmb") { return "Kimbundu"; }
-		if (code == "kok") { return "Konkani"; }
-		if (code == "kom") { return "Komi"; }
-		if (code == "kon") { return "Kongo"; }
-		if (code == "kor") { return "Korean"; }
-		if (code == "kos") { return "Kosraean"; }
-		if (code == "kpe") { return "Kpelle"; }
-		if (code == "krc") { return "Karachay-Balkar"; }
-		if (code == "krl") { return "Karelian"; }
-		if (code == "kro") { return "Kru languages"; }
-		if (code == "kru") { return "Kurukh"; }
-		if (code == "kua") { return "Kuanyama"; }
-		if (code == "kum") { return "Kumyk"; }
-		if (code == "kur") { return "Kurdish"; }
-		if (code == "kut") { return "Kutenai"; }
-		if (code == "lad") { return "Ladino"; }
-		if (code == "lah") { return "Lahnda"; }
-		if (code == "lam") { return "Lamba"; }
-		if (code == "lao") { return "Lao"; }
-		if (code == "lat") { return "Latin"; }
-		if (code == "lav") { return "Latvian"; }
-		if (code == "lez") { return "Lezghian"; }
-		if (code == "lim") { return "Limburgan"; }
-		if (code == "lin") { return "Lingala"; }
-		if (code == "lit") { return "Lithuanian"; }
-		if (code == "lol") { return "Mongo"; }
-		if (code == "loz") { return "Lozi"; }
-		if (code == "ltz") { return "Luxembourgish"; }
-		if (code == "lua") { return "Luba-Lulua"; }
-		if (code == "lub") { return "Luba-Katanga"; }
-		if (code == "lug") { return "Ganda"; }
-		if (code == "lui") { return "Luiseno"; }
-		if (code == "lun") { return "Lunda"; }
-		if (code == "luo") { return "Luo (Kenya and Tanzania)"; }
-		if (code == "lus") { return "Lushai"; }
-		if (code == "mac") { return "Macedonian"; }
-		if (code == "mac") { return "Macedonian"; }
-		if (code == "mad") { return "Madurese"; }
-		if (code == "mag") { return "Magahi"; }
-		if (code == "mah") { return "Marshallese"; }
-		if (code == "mai") { return "Maithili"; }
-		if (code == "mak") { return "Makasar"; }
-		if (code == "mal") { return "Malayalam"; }
-		if (code == "man") { return "Mandingo"; }
-		if (code == "mao") { return "Maori"; }
-		if (code == "map") { return "Austronesian languages"; }
-		if (code == "mar") { return "Marathi"; }
-		if (code == "mas") { return "Masai"; }
-		if (code == "may") { return "Malay"; }
-		if (code == "mdf") { return "Moksha"; }
-		if (code == "mdr") { return "Mandar"; }
-		if (code == "men") { return "Mende"; }
-		if (code == "mga") { return "Irish, Middle (900-1200)"; }
-		if (code == "mic") { return "Mi'kmaq"; }
-		if (code == "min") { return "Minangkabau"; }
-		if (code == "mis") { return "Uncoded languages"; }
-		if (code == "mkd") { return "Macedonian"; }
-		if (code == "mkd") { return "Macedonian"; }
-		if (code == "mkh") { return "Mon-Khmer languages"; }
-		if (code == "mlg") { return "Malagasy"; }
-		if (code == "mlt") { return "Maltese"; }
-		if (code == "mnc") { return "Manchu"; }
-		if (code == "mni") { return "Manipuri"; }
-		if (code == "mno") { return "Manobo languages"; }
-		if (code == "moh") { return "Mohawk"; }
-		if (code == "mon") { return "Mongolian"; }
-		if (code == "mos") { return "Mossi"; }
-		if (code == "mri") { return "Maori"; }
-		if (code == "msa") { return "Malay"; }
-		if (code == "mul") { return "Multiple languages"; }
-		if (code == "mun") { return "Munda languages"; }
-		if (code == "mus") { return "Creek"; }
-		if (code == "mwl") { return "Mirandese"; }
-		if (code == "mwr") { return "Marwari"; }
-		if (code == "mya") { return "Burmese"; }
-		if (code == "mya") { return "Burmese"; }
-		if (code == "myn") { return "Mayan languages"; }
-		if (code == "myv") { return "Erzya"; }
-		if (code == "nah") { return "Nahuatl languages"; }
-		if (code == "nai") { return "North American Indian languages"; }
-		if (code == "nap") { return "Neapolitan"; }
-		if (code == "nau") { return "Nauru"; }
-		if (code == "nav") { return "Navajo"; }
-		if (code == "nbl") { return "Ndebele, South"; }
-		if (code == "nde") { return "Ndebele, North"; }
-		if (code == "ndo") { return "Ndonga"; }
-		if (code == "nds") { return "Low German"; }
-		if (code == "nep") { return "Nepali"; }
-		if (code == "new") { return "Nepal Bhasa"; }
-		if (code == "nia") { return "Nias"; }
-		if (code == "nic") { return "Niger-Kordofanian languages"; }
-		if (code == "niu") { return "Niuean"; }
-		if (code == "nld") { return "Dutch"; }
-		if (code == "nld") { return "Dutch"; }
-		if (code == "nno") { return "Norwegian Nynorsk"; }
-		if (code == "nob") { return "Bokmål, Norwegian"; }
-		if (code == "nog") { return "Nogai"; }
-		if (code == "non") { return "Norse, Old"; }
-		if (code == "nor") { return "Norwegian"; }
-		if (code == "nqo") { return "N'Ko"; }
-		if (code == "nso") { return "Pedi"; }
-		if (code == "nub") { return "Nubian languages"; }
-		if (code == "nwc") { return "Classical Newari"; }
-		if (code == "nya") { return "Chichewa"; }
-		if (code == "nym") { return "Nyamwezi"; }
-		if (code == "nyn") { return "Nyankole"; }
-		if (code == "nyo") { return "Nyoro"; }
-		if (code == "nzi") { return "Nzima"; }
-		if (code == "oci") { return "Occitan (post 1500)"; }
-		if (code == "oji") { return "Ojibwa"; }
-		if (code == "ori") { return "Oriya"; }
-		if (code == "orm") { return "Oromo"; }
-		if (code == "osa") { return "Osage"; }
-		if (code == "oss") { return "Ossetian"; }
-		if (code == "ota") { return "Turkish, Ottoman (1500-1928)"; }
-		if (code == "oto") { return "Otomian languages"; }
-		if (code == "paa") { return "Papuan languages"; }
-		if (code == "pag") { return "Pangasinan"; }
-		if (code == "pal") { return "Pahlavi"; }
-		if (code == "pam") { return "Pampanga"; }
-		if (code == "pan") { return "Panjabi"; }
-		if (code == "pap") { return "Papiamento"; }
-		if (code == "pau") { return "Palauan"; }
-		if (code == "peo") { return "Persian, Old (ca.600-400 B.C.)"; }
-		if (code == "per") { return "Persian"; }
-		if (code == "phi") { return "Philippine languages"; }
-		if (code == "phn") { return "Phoenician"; }
-		if (code == "pli") { return "Pali"; }
-		if (code == "pol") { return "Polish"; }
-		if (code == "pon") { return "Pohnpeian"; }
-		if (code == "por") { return "Portuguese"; }
-		if (code == "pra") { return "Prakrit languages"; }
-		if (code == "pro") { return "Provençal, Old (to 1500)"; }
-		if (code == "pus") { return "Pushto"; }
-		if (code == "que") { return "Quechua"; }
-		if (code == "raj") { return "Rajasthani"; }
-		if (code == "rap") { return "Rapanui"; }
-		if (code == "rar") { return "Rarotongan"; }
-		if (code == "roa") { return "Romance languages"; }
-		if (code == "roh") { return "Romansh"; }
-		if (code == "rom") { return "Romany"; }
-		if (code == "ron") { return "Romanian"; }
-		if (code == "rum") { return "Romanian"; }
-		if (code == "run") { return "Rundi"; }
-		if (code == "rup") { return "Aromanian"; }
-		if (code == "rus") { return "Russian"; }
-		if (code == "sad") { return "Sandawe"; }
-		if (code == "sag") { return "Sango"; }
-		if (code == "sah") { return "Yakut"; }
-		if (code == "sai") { return "South American Indian languages"; }
-		if (code == "sal") { return "Salishan languages"; }
-		if (code == "sam") { return "Samaritan Aramaic"; }
-		if (code == "san") { return "Sanskrit"; }
-		if (code == "sas") { return "Sasak"; }
-		if (code == "sat") { return "Santali"; }
-		if (code == "scn") { return "Sicilian"; }
-		if (code == "sco") { return "Scots"; }
-		if (code == "sel") { return "Selkup"; }
-		if (code == "sem") { return "Semitic languages"; }
-		if (code == "sga") { return "Irish, Old (to 900)"; }
-		if (code == "sgn") { return "Sign Languages"; }
-		if (code == "shn") { return "Shan"; }
-		if (code == "sid") { return "Sidamo"; }
-		if (code == "sin") { return "Sinhala"; }
-		if (code == "sio") { return "Siouan languages"; }
-		if (code == "sit") { return "Sino-Tibetan languages"; }
-		if (code == "sla") { return "Slavic languages"; }
-		if (code == "slo") { return "Slovak"; }
-		if (code == "slv") { return "Slovenian"; }
-		if (code == "sma") { return "Southern Sami"; }
-		if (code == "sme") { return "Northern Sami"; }
-		if (code == "smi") { return "Sami languages"; }
-		if (code == "smj") { return "Lule Sami"; }
-		if (code == "smn") { return "Inari Sami"; }
-		if (code == "smo") { return "Samoan"; }
-		if (code == "sms") { return "Skolt Sami"; }
-		if (code == "sna") { return "Shona"; }
-		if (code == "snd") { return "Sindhi"; }
-		if (code == "snk") { return "Soninke"; }
-		if (code == "sog") { return "Sogdian"; }
-		if (code == "som") { return "Somali"; }
-		if (code == "son") { return "Songhai languages"; }
-		if (code == "sot") { return "Sotho, Southern"; }
-		if (code == "spa") { return "Spanish"; }
-		if (code == "sqi") { return "Albanian"; }
-		if (code == "srd") { return "Sardinian"; }
-		if (code == "srn") { return "Sranan Tongo"; }
-		if (code == "srp") { return "Serbian"; }
-		if (code == "srr") { return "Serer"; }
-		if (code == "ssa") { return "Nilo-Saharan languages"; }
-		if (code == "ssw") { return "Swati"; }
-		if (code == "suk") { return "Sukuma"; }
-		if (code == "sun") { return "Sundanese"; }
-		if (code == "sus") { return "Susu"; }
-		if (code == "sux") { return "Sumerian"; }
-		if (code == "swa") { return "Swahili"; }
-		if (code == "swe") { return "Swedish"; }
-		if (code == "syc") { return "Classical Syriac"; }
-		if (code == "syr") { return "Syriac"; }
-		if (code == "tah") { return "Tahitian"; }
-		if (code == "tai") { return "Tai languages"; }
-		if (code == "tam") { return "Tamil"; }
-		if (code == "tat") { return "Tatar"; }
-		if (code == "tel") { return "Telugu"; }
-		if (code == "tem") { return "Timne"; }
-		if (code == "ter") { return "Tereno"; }
-		if (code == "tet") { return "Tetum"; }
-		if (code == "tgk") { return "Tajik"; }
-		if (code == "tgl") { return "Tagalog"; }
-		if (code == "tha") { return "Thai"; }
-		if (code == "tib") { return "Tibetian"; }
-		if (code == "tig") { return "Tigre"; }
-		if (code == "tir") { return "Tigrinya"; }
-		if (code == "tiv") { return "Tiv"; }
-		if (code == "tkl") { return "Tokelau"; }
-		if (code == "tlh") { return "Klingon"; }
-		if (code == "tli") { return "Tlingit"; }
-		if (code == "tmh") { return "Tamashek"; }
-		if (code == "tog") { return "Tonga (Nyasa)"; }
-		if (code == "ton") { return "Tonga (Tonga Islands)"; }
-		if (code == "tpi") { return "Tok Pisin"; }
-		if (code == "tsi") { return "Tsimshian"; }
-		if (code == "tsn") { return "Tswana"; }
-		if (code == "tso") { return "Tsonga"; }
-		if (code == "tuk") { return "Turkmen"; }
-		if (code == "tum") { return "Tumbuka"; }
-		if (code == "tup") { return "Tupi languages"; }
-		if (code == "tur") { return "Turkish"; }
-		if (code == "tut") { return "Altaic languages"; }
-		if (code == "tvl") { return "Tuvalu"; }
-		if (code == "twi") { return "Twi"; }
-		if (code == "tyv") { return "Tuvinian"; }
-		if (code == "udm") { return "Udmurt"; }
-		if (code == "uga") { return "Ugaritic"; }
-		if (code == "uig") { return "Uighur"; }
-		if (code == "ukr") { return "Ukrainian"; }
-		if (code == "umb") { return "Umbundu"; }
-		if (code == "und") { return "Undetermined"; }
-		if (code == "urd") { return "Urdu"; }
-		if (code == "uzb") { return "Uzbek"; }
-		if (code == "vai") { return "Vai"; }
-		if (code == "ven") { return "Venda"; }
-		if (code == "vie") { return "Vietnamese"; }
-		if (code == "vol") { return "Volapük"; }
-		if (code == "vot") { return "Votic"; }
-		if (code == "wak") { return "Wakashan languages"; }
-		if (code == "wal") { return "Wolaitta"; }
-		if (code == "war") { return "Waray"; }
-		if (code == "was") { return "Washo"; }
-		if (code == "wel") { return "Welsh"; }
-		if (code == "wel") { return "Welsh"; }
-		if (code == "wen") { return "Sorbian languages"; }
-		if (code == "wln") { return "Walloon"; }
-		if (code == "wol") { return "Wolof"; }
-		if (code == "xal") { return "Kalmyk"; }
-		if (code == "xho") { return "Xhosa"; }
-		if (code == "yao") { return "Yao"; }
-		if (code == "yap") { return "Yapese"; }
-		if (code == "yid") { return "Yiddish"; }
-		if (code == "yor") { return "Yoruba"; }
-		if (code == "ypk") { return "Yupik languages"; }
-		if (code == "zap") { return "Zapotec"; }
-		if (code == "zbl") { return "Blissymbols"; }
-		if (code == "zen") { return "Zenaga"; }
-		if (code == "zgh") { return "Moroccan"; }
-		if (code == "zha") { return "Zhuang"; }
-		if (code == "zho") { return "Chinese"; }
-		if (code == "znd") { return "Zande languages"; }
-		if (code == "zul") { return "Zulu"; }
-		if (code == "zun") { return "Zuni"; }
-		if (code == "zza") { return "Zaza"; }
+		if (code == "AAR") { return "Afar"; }
+		if (code == "ABK") { return "Abkhazian"; }
+		if (code == "ACE") { return "Achinese"; }
+		if (code == "ACH") { return "Acoli"; }
+		if (code == "ADA") { return "Adangme"; }
+		if (code == "ADY") { return "Adyghe"; }
+		if (code == "AFA") { return "Afro-Asiatic languages"; }
+		if (code == "AFH") { return "Afrihili"; }
+		if (code == "AFR") { return "Afrikaans"; }
+		if (code == "AIN") { return "Ainu"; }
+		if (code == "AKA") { return "Akan"; }
+		if (code == "AKK") { return "Akkadian"; }
+		if (code == "ALB") { return "Albanian"; }
+		if (code == "ALE") { return "Aleut"; }
+		if (code == "ALG") { return "Algonquian languages"; }
+		if (code == "ALT") { return "Southern Altai"; }
+		if (code == "AMH") { return "Amharic"; }
+		if (code == "ANG") { return "English, Old (ca.450-1100)"; }
+		if (code == "ANP") { return "Angika"; }
+		if (code == "APA") { return "Apache languages"; }
+		if (code == "ARA") { return "Arabic"; }
+		if (code == "ARC") { return "Aramaic (700-300 BCE)"; }
+		if (code == "ARG") { return "Aragonese"; }
+		if (code == "ARM") { return "Armenian"; }
+		if (code == "ARN") { return "Mapudungun"; }
+		if (code == "ARP") { return "Arapaho"; }
+		if (code == "ART") { return "Artificial languages"; }
+		if (code == "ARW") { return "Arawak"; }
+		if (code == "ASM") { return "Assamese"; }
+		if (code == "AST") { return "Asturian"; }
+		if (code == "ATH") { return "Athapascan languages"; }
+		if (code == "AUS") { return "Australian languages"; }
+		if (code == "AVA") { return "Avaric"; }
+		if (code == "AVE") { return "Avestan"; }
+		if (code == "AWA") { return "Awadhi"; }
+		if (code == "AYM") { return "Aymara"; }
+		if (code == "AZE") { return "Azerbaijani"; }
+		if (code == "BAD") { return "Banda languages"; }
+		if (code == "BAI") { return "Bamileke languages"; }
+		if (code == "BAK") { return "Bashkir"; }
+		if (code == "BAL") { return "Baluchi"; }
+		if (code == "BAM") { return "Bambara"; }
+		if (code == "BAN") { return "Balinese"; }
+		if (code == "BAQ") { return "Basque"; }
+		if (code == "BAQ") { return "Basque"; }
+		if (code == "BAS") { return "Basa"; }
+		if (code == "BAT") { return "Baltic languages"; }
+		if (code == "BEJ") { return "Beja"; }
+		if (code == "BEL") { return "Belarusian"; }
+		if (code == "BEM") { return "Bemba"; }
+		if (code == "BEN") { return "Bengali"; }
+		if (code == "BER") { return "Berber languages"; }
+		if (code == "BHO") { return "Bhojpuri"; }
+		if (code == "BIH") { return "Bihari languages"; }
+		if (code == "BIK") { return "Bikol"; }
+		if (code == "BIN") { return "Bini"; }
+		if (code == "BIS") { return "Bislama"; }
+		if (code == "BLA") { return "Siksika"; }
+		if (code == "BNT") { return "Bantu languages"; }
+		if (code == "BOD") { return "Tibetan"; }
+		if (code == "BOS") { return "Bosnian"; }
+		if (code == "BRA") { return "Braj"; }
+		if (code == "BRE") { return "Breton"; }
+		if (code == "BTK") { return "Batak languages"; }
+		if (code == "BUA") { return "Buriat"; }
+		if (code == "BUG") { return "Buginese"; }
+		if (code == "BUL") { return "Bulgarian"; }
+		if (code == "BUR") { return "Burmese"; }
+		if (code == "BUR") { return "Burmese"; }
+		if (code == "BYN") { return "Blin"; }
+		if (code == "CAD") { return "Caddo"; }
+		if (code == "CAI") { return "Central American Indian languages"; }
+		if (code == "CAR") { return "Galibi Carib"; }
+		if (code == "CAT") { return "Catalan"; }
+		if (code == "CAU") { return "Caucasian languages"; }
+		if (code == "CEB") { return "Cebuano"; }
+		if (code == "CEL") { return "Celtic languages"; }
+		if (code == "CES") { return "Czech"; }
+		if (code == "CES") { return "Czech"; }
+		if (code == "CHA") { return "Chamorro"; }
+		if (code == "CHB") { return "Chibcha"; }
+		if (code == "CHE") { return "Chechen"; }
+		if (code == "CHG") { return "Chagatai"; }
+		if (code == "CHI") { return "Chinese"; }
+		if (code == "CHK") { return "Chuukese"; }
+		if (code == "CHM") { return "Mari"; }
+		if (code == "CHN") { return "Chinook jargon"; }
+		if (code == "CHO") { return "Choctaw"; }
+		if (code == "CHP") { return "Chipewyan"; }
+		if (code == "CHR") { return "Cherokee"; }
+		if (code == "CHU") { return "Church Slavic"; }
+		if (code == "CHV") { return "Chuvash"; }
+		if (code == "CHY") { return "Cheyenne"; }
+		if (code == "CMC") { return "Chamic languages"; }
+		if (code == "CNR") { return "Montenegrin"; }
+		if (code == "COP") { return "Coptic"; }
+		if (code == "COR") { return "Cornish"; }
+		if (code == "COS") { return "Corsican"; }
+		if (code == "CPE") { return "Creoles and pidgins, English based"; }
+		if (code == "CPF") { return "Creoles and pidgins, French-based"; }
+		if (code == "CPP") { return "Creoles and pidgins, Portuguese-based"; }
+		if (code == "CRE") { return "Cree"; }
+		if (code == "CRH") { return "Crimean Tatar"; }
+		if (code == "CRP") { return "Creoles and pidgins"; }
+		if (code == "CSB") { return "Kashubian"; }
+		if (code == "CUS") { return "Cushitic languages"; }
+		if (code == "CYM") { return "Welsh"; }
+		if (code == "CYM") { return "Welsh"; }
+		if (code == "CZE") { return "Czech"; }
+		if (code == "CZE") { return "Czech"; }
+		if (code == "DAK") { return "Dakota"; }
+		if (code == "DAN") { return "Danish"; }
+		if (code == "DAR") { return "Dargwa"; }
+		if (code == "DAY") { return "Land Dayak languages"; }
+		if (code == "DEL") { return "Delaware"; }
+		if (code == "DEN") { return "Slave (Athapascan)"; }
+		if (code == "DEU") { return "German"; }
+		if (code == "DGR") { return "Dogrib"; }
+		if (code == "DIN") { return "Dinka"; }
+		if (code == "DIV") { return "Divehi"; }
+		if (code == "DOI") { return "Dogri"; }
+		if (code == "DRA") { return "Dravidian languages"; }
+		if (code == "DSB") { return "Lower Sorbian"; }
+		if (code == "DUA") { return "Duala"; }
+		if (code == "DUM") { return "Dutch, Middle (ca.1050-1350)"; }
+		if (code == "DUT") { return "Dutch"; }
+		if (code == "DUT") { return "Dutch"; }
+		if (code == "DYU") { return "Dyula"; }
+		if (code == "DZO") { return "Dzongkha"; }
+		if (code == "EFI") { return "Efik"; }
+		if (code == "EGY") { return "Egyptian (Ancient)"; }
+		if (code == "EKA") { return "Ekajuk"; }
+		if (code == "ELL") { return "Greek, Modern (1453-)"; }
+		if (code == "ELX") { return "Elamite"; }
+		if (code == "ENG") { return "English"; }
+		if (code == "ENM") { return "English, Middle (1100-1500)"; }
+		if (code == "EPO") { return "Esperanto"; }
+		if (code == "EST") { return "Estonian"; }
+		if (code == "EUS") { return "Basque"; }
+		if (code == "EUS") { return "Basque"; }
+		if (code == "EWE") { return "Ewe"; }
+		if (code == "EWO") { return "Ewondo"; }
+		if (code == "FAN") { return "Fang"; }
+		if (code == "FAO") { return "Faroese"; }
+		if (code == "FAS") { return "Persian"; }
+		if (code == "FAT") { return "Fanti"; }
+		if (code == "FIJ") { return "Fijian"; }
+		if (code == "FIL") { return "Filipino"; }
+		if (code == "FIN") { return "Finnish"; }
+		if (code == "FIU") { return "Finno-Ugrian languages"; }
+		if (code == "FON") { return "Fon"; }
+		if (code == "FRA") { return "French"; }
+		if (code == "FRE") { return "French"; }
+		if (code == "FRM") { return "French, Middle (ca.1400-1600)"; }
+		if (code == "FRO") { return "French, Old (842-ca.1400)"; }
+		if (code == "FRR") { return "Northern Frisian"; }
+		if (code == "FRS") { return "Eastern Frisian"; }
+		if (code == "FRY") { return "Western Frisian"; }
+		if (code == "FUL") { return "Fulah"; }
+		if (code == "FUR") { return "Friulian"; }
+		if (code == "GAA") { return "Ga"; }
+		if (code == "GAY") { return "Gayo"; }
+		if (code == "GBA") { return "Gbaya"; }
+		if (code == "GEM") { return "Germanic languages"; }
+		if (code == "GEO") { return "Georgin"; }
+		if (code == "GER") { return "German"; }
+		if (code == "GEZ") { return "Geez"; }
+		if (code == "GIL") { return "Gilbertese"; }
+		if (code == "GLA") { return "Gaelic"; }
+		if (code == "GLE") { return "Irish"; }
+		if (code == "GLG") { return "Galician"; }
+		if (code == "GLV") { return "Manx"; }
+		if (code == "GMH") { return "German, Middle High (ca.1050-1500)"; }
+		if (code == "GOH") { return "German, Old High (ca.750-1050)"; }
+		if (code == "GON") { return "Gondi"; }
+		if (code == "GOR") { return "Gorontalo"; }
+		if (code == "GOT") { return "Gothic"; }
+		if (code == "GRB") { return "Grebo"; }
+		if (code == "GRC") { return "Greek, Ancient (to 1453)"; }
+		if (code == "GRE") { return "Greek"; }
+		if (code == "GRN") { return "Guarani"; }
+		if (code == "GSW") { return "Swiss German"; }
+		if (code == "GUJ") { return "Gujarati"; }
+		if (code == "GWI") { return "Gwich'in"; }
+		if (code == "HAI") { return "Haida"; }
+		if (code == "HAT") { return "Haitian"; }
+		if (code == "HAU") { return "Hausa"; }
+		if (code == "HAW") { return "Hawaiian"; }
+		if (code == "HEB") { return "Hebrew"; }
+		if (code == "HER") { return "Herero"; }
+		if (code == "HIL") { return "Hiligaynon"; }
+		if (code == "HIM") { return "Himachali languages"; }
+		if (code == "HIN") { return "Hindi"; }
+		if (code == "HIT") { return "Hittite"; }
+		if (code == "HMN") { return "Hmong"; }
+		if (code == "HMO") { return "Hiri Motu"; }
+		if (code == "HRV") { return "Croatian"; }
+		if (code == "HSB") { return "Upper Sorbian"; }
+		if (code == "HUN") { return "Hungarian"; }
+		if (code == "HUP") { return "Hupa"; }
+		if (code == "HYE") { return "Armenian"; }
+		if (code == "IBA") { return "Iban"; }
+		if (code == "IBO") { return "Igbo"; }
+		if (code == "ICE") { return "Icelandic"; }
+		if (code == "IDO") { return "Ido"; }
+		if (code == "III") { return "Sichuan Yi"; }
+		if (code == "IJO") { return "Ijo languages"; }
+		if (code == "IKU") { return "Inuktitut"; }
+		if (code == "ILE") { return "Interlingue"; }
+		if (code == "ILO") { return "Iloko"; }
+		if (code == "INA") { return "Interlingua)"; }
+		if (code == "INC") { return "Indic languages"; }
+		if (code == "IND") { return "Indonesian"; }
+		if (code == "INE") { return "Indo-European languages"; }
+		if (code == "INH") { return "Ingush"; }
+		if (code == "IPK") { return "Inupiaq"; }
+		if (code == "IRA") { return "Iranian languages"; }
+		if (code == "IRO") { return "Iroquoian languages"; }
+		if (code == "ISL") { return "Icelandic"; }
+		if (code == "ITA") { return "Italian"; }
+		if (code == "JAV") { return "Javanese"; }
+		if (code == "JBO") { return "Lojban"; }
+		if (code == "JPN") { return "Japanese"; }
+		if (code == "JPR") { return "Judeo-Persian"; }
+		if (code == "JRB") { return "Judeo-Arabic"; }
+		if (code == "KAA") { return "Kara-Kalpak"; }
+		if (code == "KAB") { return "Kabyle"; }
+		if (code == "KAC") { return "Kachin"; }
+		if (code == "KAL") { return "Greenlandic"; }
+		if (code == "KAM") { return "Kamba"; }
+		if (code == "KAN") { return "Kannada"; }
+		if (code == "KAR") { return "Karen languages"; }
+		if (code == "KAS") { return "Kashmiri"; }
+		if (code == "KAT") { return "Georgian"; }
+		if (code == "KAU") { return "Kanuri"; }
+		if (code == "KAW") { return "Kawi"; }
+		if (code == "KAZ") { return "Kazakh"; }
+		if (code == "KBD") { return "Kabardian"; }
+		if (code == "KHA") { return "Khasi"; }
+		if (code == "KHI") { return "Khoisan languages"; }
+		if (code == "KHM") { return "Central Khmer"; }
+		if (code == "KHO") { return "Khotanese"; }
+		if (code == "KIK") { return "Kikuyu"; }
+		if (code == "KIN") { return "Kinyarwanda"; }
+		if (code == "KIR") { return "Kirghiz"; }
+		if (code == "KMB") { return "Kimbundu"; }
+		if (code == "KOK") { return "Konkani"; }
+		if (code == "KOM") { return "Komi"; }
+		if (code == "KON") { return "Kongo"; }
+		if (code == "KOR") { return "Korean"; }
+		if (code == "KOS") { return "Kosraean"; }
+		if (code == "KPE") { return "Kpelle"; }
+		if (code == "KRC") { return "Karachay-Balkar"; }
+		if (code == "KRL") { return "Karelian"; }
+		if (code == "KRO") { return "Kru languages"; }
+		if (code == "KRU") { return "Kurukh"; }
+		if (code == "KUA") { return "Kuanyama"; }
+		if (code == "KUM") { return "Kumyk"; }
+		if (code == "KUR") { return "Kurdish"; }
+		if (code == "KUT") { return "Kutenai"; }
+		if (code == "LAD") { return "Ladino"; }
+		if (code == "LAH") { return "Lahnda"; }
+		if (code == "LAM") { return "Lamba"; }
+		if (code == "LAO") { return "Lao"; }
+		if (code == "LAT") { return "Latin"; }
+		if (code == "LAV") { return "Latvian"; }
+		if (code == "LEZ") { return "Lezghian"; }
+		if (code == "LIM") { return "Limburgan"; }
+		if (code == "LIN") { return "Lingala"; }
+		if (code == "LIT") { return "Lithuanian"; }
+		if (code == "LOL") { return "Mongo"; }
+		if (code == "LOZ") { return "Lozi"; }
+		if (code == "LTZ") { return "Luxembourgish"; }
+		if (code == "LUA") { return "Luba-Lulua"; }
+		if (code == "LUB") { return "Luba-Katanga"; }
+		if (code == "LUG") { return "Ganda"; }
+		if (code == "LUI") { return "Luiseno"; }
+		if (code == "LUN") { return "Lunda"; }
+		if (code == "LUO") { return "Luo (Kenya and Tanzania)"; }
+		if (code == "LUS") { return "Lushai"; }
+		if (code == "MAC") { return "Macedonian"; }
+		if (code == "MAC") { return "Macedonian"; }
+		if (code == "MAD") { return "Madurese"; }
+		if (code == "MAG") { return "Magahi"; }
+		if (code == "MAH") { return "Marshallese"; }
+		if (code == "MAI") { return "Maithili"; }
+		if (code == "MAK") { return "Makasar"; }
+		if (code == "MAL") { return "Malayalam"; }
+		if (code == "MAN") { return "Mandingo"; }
+		if (code == "MAO") { return "Maori"; }
+		if (code == "MAP") { return "Austronesian languages"; }
+		if (code == "MAR") { return "Marathi"; }
+		if (code == "MAS") { return "Masai"; }
+		if (code == "MAY") { return "Malay"; }
+		if (code == "MDF") { return "Moksha"; }
+		if (code == "MDR") { return "Mandar"; }
+		if (code == "MEN") { return "Mende"; }
+		if (code == "MGA") { return "Irish, Middle (900-1200)"; }
+		if (code == "MIC") { return "Mi'kmaq"; }
+		if (code == "MIN") { return "Minangkabau"; }
+		if (code == "MIS") { return "Uncoded languages"; }
+		if (code == "MKD") { return "Macedonian"; }
+		if (code == "MKD") { return "Macedonian"; }
+		if (code == "MKH") { return "Mon-Khmer languages"; }
+		if (code == "MLG") { return "Malagasy"; }
+		if (code == "MLT") { return "Maltese"; }
+		if (code == "MNC") { return "Manchu"; }
+		if (code == "MNI") { return "Manipuri"; }
+		if (code == "MNO") { return "Manobo languages"; }
+		if (code == "MOH") { return "Mohawk"; }
+		if (code == "MON") { return "Mongolian"; }
+		if (code == "MOS") { return "Mossi"; }
+		if (code == "MRI") { return "Maori"; }
+		if (code == "MSA") { return "Malay"; }
+		if (code == "MUL") { return "Multiple languages"; }
+		if (code == "MUN") { return "Munda languages"; }
+		if (code == "MUS") { return "Creek"; }
+		if (code == "MWL") { return "Mirandese"; }
+		if (code == "MWR") { return "Marwari"; }
+		if (code == "MYA") { return "Burmese"; }
+		if (code == "MYA") { return "Burmese"; }
+		if (code == "MYN") { return "Mayan languages"; }
+		if (code == "MYV") { return "Erzya"; }
+		if (code == "NAH") { return "Nahuatl languages"; }
+		if (code == "NAI") { return "North American Indian languages"; }
+		if (code == "NAP") { return "Neapolitan"; }
+		if (code == "NAU") { return "Nauru"; }
+		if (code == "NAV") { return "Navajo"; }
+		if (code == "NBL") { return "Ndebele, South"; }
+		if (code == "NDE") { return "Ndebele, North"; }
+		if (code == "NDO") { return "Ndonga"; }
+		if (code == "NDS") { return "Low German"; }
+		if (code == "NEP") { return "Nepali"; }
+		if (code == "NEW") { return "Nepal Bhasa"; }
+		if (code == "NIA") { return "Nias"; }
+		if (code == "NIC") { return "Niger-Kordofanian languages"; }
+		if (code == "NIU") { return "Niuean"; }
+		if (code == "NLD") { return "Dutch"; }
+		if (code == "NLD") { return "Dutch"; }
+		if (code == "NNO") { return "Norwegian Nynorsk"; }
+		if (code == "NOB") { return "Bokmål, Norwegian"; }
+		if (code == "NOG") { return "Nogai"; }
+		if (code == "NON") { return "Norse, Old"; }
+		if (code == "NOR") { return "Norwegian"; }
+		if (code == "NQO") { return "N'Ko"; }
+		if (code == "NSO") { return "Pedi"; }
+		if (code == "NUB") { return "Nubian languages"; }
+		if (code == "NWC") { return "Classical Newari"; }
+		if (code == "NYA") { return "Chichewa"; }
+		if (code == "NYM") { return "Nyamwezi"; }
+		if (code == "NYN") { return "Nyankole"; }
+		if (code == "NYO") { return "Nyoro"; }
+		if (code == "NZI") { return "Nzima"; }
+		if (code == "OCI") { return "Occitan (post 1500)"; }
+		if (code == "OJI") { return "Ojibwa"; }
+		if (code == "ORI") { return "Oriya"; }
+		if (code == "ORM") { return "Oromo"; }
+		if (code == "OSA") { return "Osage"; }
+		if (code == "OSS") { return "Ossetian"; }
+		if (code == "OTA") { return "Turkish, Ottoman (1500-1928)"; }
+		if (code == "OTO") { return "Otomian languages"; }
+		if (code == "PAA") { return "Papuan languages"; }
+		if (code == "PAG") { return "Pangasinan"; }
+		if (code == "PAL") { return "Pahlavi"; }
+		if (code == "PAM") { return "Pampanga"; }
+		if (code == "PAN") { return "Panjabi"; }
+		if (code == "PAP") { return "Papiamento"; }
+		if (code == "PAU") { return "Palauan"; }
+		if (code == "PEO") { return "Persian, Old (ca.600-400 B.C.)"; }
+		if (code == "PER") { return "Persian"; }
+		if (code == "PHI") { return "Philippine languages"; }
+		if (code == "PHN") { return "Phoenician"; }
+		if (code == "PLI") { return "Pali"; }
+		if (code == "POL") { return "Polish"; }
+		if (code == "PON") { return "Pohnpeian"; }
+		if (code == "POR") { return "Portuguese"; }
+		if (code == "PRA") { return "Prakrit languages"; }
+		if (code == "PRO") { return "Provençal, Old (to 1500)"; }
+		if (code == "PUS") { return "Pushto"; }
+		if (code == "QUE") { return "Quechua"; }
+		if (code == "RAJ") { return "Rajasthani"; }
+		if (code == "RAP") { return "Rapanui"; }
+		if (code == "RAR") { return "Rarotongan"; }
+		if (code == "ROA") { return "Romance languages"; }
+		if (code == "ROH") { return "Romansh"; }
+		if (code == "ROM") { return "Romany"; }
+		if (code == "RON") { return "Romanian"; }
+		if (code == "RUM") { return "Romanian"; }
+		if (code == "RUN") { return "Rundi"; }
+		if (code == "RUP") { return "Aromanian"; }
+		if (code == "RUS") { return "Russian"; }
+		if (code == "SAD") { return "Sandawe"; }
+		if (code == "SAG") { return "Sango"; }
+		if (code == "SAH") { return "Yakut"; }
+		if (code == "SAI") { return "South American Indian languages"; }
+		if (code == "SAL") { return "Salishan languages"; }
+		if (code == "SAM") { return "Samaritan Aramaic"; }
+		if (code == "SAN") { return "Sanskrit"; }
+		if (code == "SAS") { return "Sasak"; }
+		if (code == "SAT") { return "Santali"; }
+		if (code == "SCN") { return "Sicilian"; }
+		if (code == "SCO") { return "Scots"; }
+		if (code == "SEL") { return "Selkup"; }
+		if (code == "SEM") { return "Semitic languages"; }
+		if (code == "SGA") { return "Irish, Old (to 900)"; }
+		if (code == "SGN") { return "Sign Languages"; }
+		if (code == "SHN") { return "Shan"; }
+		if (code == "SID") { return "Sidamo"; }
+		if (code == "SIN") { return "Sinhala"; }
+		if (code == "SIO") { return "Siouan languages"; }
+		if (code == "SIT") { return "Sino-Tibetan languages"; }
+		if (code == "SLA") { return "Slavic languages"; }
+		if (code == "SLO") { return "Slovak"; }
+		if (code == "SLV") { return "Slovenian"; }
+		if (code == "SMA") { return "Southern Sami"; }
+		if (code == "SME") { return "Northern Sami"; }
+		if (code == "SMI") { return "Sami languages"; }
+		if (code == "SMJ") { return "Lule Sami"; }
+		if (code == "SMN") { return "Inari Sami"; }
+		if (code == "SMO") { return "Samoan"; }
+		if (code == "SMS") { return "Skolt Sami"; }
+		if (code == "SNA") { return "Shona"; }
+		if (code == "SND") { return "Sindhi"; }
+		if (code == "SNK") { return "Soninke"; }
+		if (code == "SOG") { return "Sogdian"; }
+		if (code == "SOM") { return "Somali"; }
+		if (code == "SON") { return "Songhai languages"; }
+		if (code == "SOT") { return "Sotho, Southern"; }
+		if (code == "SPA") { return "Spanish"; }
+		if (code == "SQI") { return "Albanian"; }
+		if (code == "SRD") { return "Sardinian"; }
+		if (code == "SRN") { return "Sranan Tongo"; }
+		if (code == "SRP") { return "Serbian"; }
+		if (code == "SRR") { return "Serer"; }
+		if (code == "SSA") { return "Nilo-Saharan languages"; }
+		if (code == "SSW") { return "Swati"; }
+		if (code == "SUK") { return "Sukuma"; }
+		if (code == "SUN") { return "Sundanese"; }
+		if (code == "SUS") { return "Susu"; }
+		if (code == "SUX") { return "Sumerian"; }
+		if (code == "SWA") { return "Swahili"; }
+		if (code == "SWE") { return "Swedish"; }
+		if (code == "SYC") { return "Classical Syriac"; }
+		if (code == "SYR") { return "Syriac"; }
+		if (code == "TAH") { return "Tahitian"; }
+		if (code == "TAI") { return "Tai languages"; }
+		if (code == "TAM") { return "Tamil"; }
+		if (code == "TAT") { return "Tatar"; }
+		if (code == "TEL") { return "Telugu"; }
+		if (code == "TEM") { return "Timne"; }
+		if (code == "TER") { return "Tereno"; }
+		if (code == "TET") { return "Tetum"; }
+		if (code == "TGK") { return "Tajik"; }
+		if (code == "TGL") { return "Tagalog"; }
+		if (code == "THA") { return "Thai"; }
+		if (code == "TIB") { return "Tibetian"; }
+		if (code == "TIG") { return "Tigre"; }
+		if (code == "TIR") { return "Tigrinya"; }
+		if (code == "TIV") { return "Tiv"; }
+		if (code == "TKL") { return "Tokelau"; }
+		if (code == "TLH") { return "Klingon"; }
+		if (code == "TLI") { return "Tlingit"; }
+		if (code == "TMH") { return "Tamashek"; }
+		if (code == "TOG") { return "Tonga (Nyasa)"; }
+		if (code == "TON") { return "Tonga (Tonga Islands)"; }
+		if (code == "TPI") { return "Tok Pisin"; }
+		if (code == "TSI") { return "Tsimshian"; }
+		if (code == "TSN") { return "Tswana"; }
+		if (code == "TSO") { return "Tsonga"; }
+		if (code == "TUK") { return "Turkmen"; }
+		if (code == "TUM") { return "Tumbuka"; }
+		if (code == "TUP") { return "Tupi languages"; }
+		if (code == "TUR") { return "Turkish"; }
+		if (code == "TUT") { return "Altaic languages"; }
+		if (code == "TVL") { return "Tuvalu"; }
+		if (code == "TWI") { return "Twi"; }
+		if (code == "TYV") { return "Tuvinian"; }
+		if (code == "UDM") { return "Udmurt"; }
+		if (code == "UGA") { return "Ugaritic"; }
+		if (code == "UIG") { return "Uighur"; }
+		if (code == "UKR") { return "Ukrainian"; }
+		if (code == "UMB") { return "Umbundu"; }
+		if (code == "UND") { return "Undetermined"; }
+		if (code == "URD") { return "Urdu"; }
+		if (code == "UZB") { return "Uzbek"; }
+		if (code == "VAI") { return "Vai"; }
+		if (code == "VEN") { return "Venda"; }
+		if (code == "VIE") { return "Vietnamese"; }
+		if (code == "VOL") { return "Volapük"; }
+		if (code == "VOT") { return "Votic"; }
+		if (code == "WAK") { return "Wakashan languages"; }
+		if (code == "WAL") { return "Wolaitta"; }
+		if (code == "WAR") { return "Waray"; }
+		if (code == "WAS") { return "Washo"; }
+		if (code == "WEL") { return "Welsh"; }
+		if (code == "WEL") { return "Welsh"; }
+		if (code == "WEN") { return "Sorbian languages"; }
+		if (code == "WLN") { return "Walloon"; }
+		if (code == "WOL") { return "Wolof"; }
+		if (code == "XAL") { return "Kalmyk"; }
+		if (code == "XHO") { return "Xhosa"; }
+		if (code == "YAO") { return "Yao"; }
+		if (code == "YAP") { return "Yapese"; }
+		if (code == "YID") { return "Yiddish"; }
+		if (code == "YOR") { return "Yoruba"; }
+		if (code == "YPK") { return "Yupik languages"; }
+		if (code == "ZAP") { return "Zapotec"; }
+		if (code == "ZBL") { return "Blissymbols"; }
+		if (code == "ZEN") { return "Zenaga"; }
+		if (code == "ZGH") { return "Moroccan"; }
+		if (code == "ZHA") { return "Zhuang"; }
+		if (code == "ZHO") { return "Chinese"; }
+		if (code == "ZND") { return "Zande languages"; }
+		if (code == "ZUL") { return "Zulu"; }
+		if (code == "ZUN") { return "Zuni"; }
+		if (code == "ZZA") { return "Zaza"; }
 	}
 	return code;
 }
@@ -14507,8 +14507,8 @@ ostream& operator<<(ostream& out, HumHash* hash) {
 typedef unsigned long long TEMP64BITFIX;
 
 // declare static variables
-vector<_HumInstrument> HumInstrument::data;
-int HumInstrument::classcount = 0;
+vector<_HumInstrument> HumInstrument::m_data;
+int HumInstrument::m_classcount = 0;
 
 
 //////////////////////////////
@@ -14517,11 +14517,11 @@ int HumInstrument::classcount = 0;
 //
 
 HumInstrument::HumInstrument(void) {
-	if (classcount == 0) {
+	if (m_classcount == 0) {
 		initialize();
 	}
-	classcount++;
-	index = -1;
+	m_classcount++;
+	m_index = -1;
 }
 
 
@@ -14532,11 +14532,11 @@ HumInstrument::HumInstrument(void) {
 //
 
 HumInstrument::HumInstrument(const string& Hname) {
-	if (classcount == 0) {
+	if (m_classcount == 0) {
 		initialize();
 	}
 
-	index = find(Hname);
+	m_index = find(Hname);
 }
 
 
@@ -14547,7 +14547,7 @@ HumInstrument::HumInstrument(const string& Hname) {
 //
 
 HumInstrument::~HumInstrument() {
-	index = -1;
+	m_index = -1;
 }
 
 
@@ -14558,8 +14558,8 @@ HumInstrument::~HumInstrument() {
 //
 
 int HumInstrument::getGM(void) {
-	if (index > 0) {
-		return data[index].gm;
+	if (m_index > 0) {
+		return m_data[m_index].gm;
 	} else {
 		return -1;
 	}
@@ -14581,7 +14581,7 @@ int HumInstrument::getGM(const string& Hname) {
 	}
 
 	if (tindex > 0) {
-		return data[tindex].gm;
+		return m_data[tindex].gm;
 	} else {
 		return -1;
 	}
@@ -14595,8 +14595,8 @@ int HumInstrument::getGM(const string& Hname) {
 //
 
 string HumInstrument::getName(void) {
-	if (index > 0) {
-		return data[index].name;
+	if (m_index > 0) {
+		return m_data[m_index].name;
 	} else {
 		return "";
 	}
@@ -14617,7 +14617,7 @@ string HumInstrument::getName(const string& Hname) {
 		tindex = find(Hname);
 	}
 	if (tindex > 0) {
-		return data[tindex].name;
+		return m_data[tindex].name;
 	} else {
 		return "";
 	}
@@ -14631,8 +14631,8 @@ string HumInstrument::getName(const string& Hname) {
 //
 
 string HumInstrument::getHumdrum(void) {
-	if (index > 0) {
-		return data[index].humdrum;
+	if (m_index > 0) {
+		return m_data[m_index].humdrum;
 	} else {
 		return "";
 	}
@@ -14651,7 +14651,7 @@ int HumInstrument::setGM(const string& Hname, int aValue) {
 	}
 	int rindex = find(Hname);
 	if (rindex > 0) {
-		data[rindex].gm = aValue;
+		m_data[rindex].gm = aValue;
 	} else {
 		afi(Hname.c_str(), aValue, Hname.c_str());
 		sortData();
@@ -14668,11 +14668,11 @@ int HumInstrument::setGM(const string& Hname, int aValue) {
 
 void HumInstrument::setHumdrum(const string& Hname) {
 	if (Hname.compare(0, 2, "*I") == 0) {
-		index = find(Hname.substr(2));
+		m_index = find(Hname.substr(2));
 	} else {
-		index = find(Hname);
+		m_index = find(Hname);
 	}
-}
+                                             }
 
 
 
@@ -14686,171 +14686,223 @@ void HumInstrument::setHumdrum(const string& Hname) {
 //
 // HumInstrument::initialize --
 //
-
 void HumInstrument::initialize(void) {
-	data.reserve(500);
-	afi("accor",	GM_ACCORDION,	"accordion");
-	afi("alto",		GM_RECORDER,	"alto");
-	afi("archl",	GM_ACOUSTIC_GUITAR_NYLON,	"archlute");
-	afi("armon",	GM_HARMONICA,	"harmonica");
-	afi("arpa",		GM_ORCHESTRAL_HARP,	"harp");
-	afi("bagpI",	GM_BAGPIPE,	"bagpipe (Irish)");
-	afi("bagpS",	GM_BAGPIPE,	"bagpipe (Scottish)");
-	afi("banjo",	GM_BANJO,	"banjo");
-	afi("barit",	GM_CHOIR_AAHS,	"baritone");
-	afi("baset",	GM_CLARINET,	"bassett horn");
-	afi("bass",		GM_CHOIR_AAHS,	"bass");
-	afi("bdrum",	GM_TAIKO_DRUM,	"bass drum (kit)");
-	afi("bguit",	GM_ELECTRIC_BASS_FINGER,	"electric bass guitar");
-	afi("biwa",		GM_FLUTE,	"biwa");
-	afi("bscan",	GM_CHOIR_AAHS,	"basso cantante");
-	afi("bspro",	GM_CHOIR_AAHS,	"basso profondo");
-	afi("calam",	GM_OBOE,	"chalumeau");
-	afi("calpe",	GM_LEAD_CALLIOPE,	"calliope");
-	afi("calto",	GM_CHOIR_AAHS,	"contralto");
-	afi("campn",	GM_TUBULAR_BELLS,	"bell");
-	afi("cangl",	GM_ENGLISH_HORN,	"english horn");
-	afi("caril",	GM_TUBULAR_BELLS,	"carillon");
-	afi("castr",	GM_CHOIR_AAHS,	"castrato");
-	afi("casts",	GM_WOODBLOCKS,	"castanets");
-	afi("cbass",	GM_CONTRABASS,	"contrabass");
-	afi("cello",	GM_CELLO,	"violoncello");
-	afi("cemba",	GM_HARPSICHORD,	"harpsichord");
-	afi("cetra",	GM_VIOLIN,	"cittern");
-	afi("chime",	GM_TUBULAR_BELLS,	"chimes");
-	afi("chlma",	GM_BASSOON,	"alto shawm");
-	afi("chlms",	GM_BASSOON,	"soprano shawm");
-	afi("chlmt",	GM_BASSOON,	"tenor shawm");
-	afi("clara",	GM_CLARINET,	"alto clarinet (in E-flat)");
-	afi("clarb",	GM_CLARINET,	"bass clarinet (in B-flat)");
-	afi("clarp",	GM_CLARINET,	"piccolo clarinet");
-	afi("clars",	GM_CLARINET,	"soprano clarinet");
-	afi("clavi",	GM_CLAVI,	"clavichord");
-	afi("clest",	GM_CELESTA,	"celesta");
-	afi("colsp",	GM_FLUTE,	"coloratura soprano");
-	afi("cor",		GM_FRENCH_HORN,	"horn");
-	afi("cornm",	GM_BAGPIPE,	"French bagpipe");
-	afi("corno",	GM_TRUMPET,	"cornett");
-	afi("cornt",	GM_TRUMPET,	"cornet");
-	afi("crshc",	GM_REVERSE_CYMBAL,	"crash cymbal (kit)");
-	afi("ctenor",	GM_CHOIR_AAHS,	"counter-tenor");
-	afi("ctina",	GM_ACCORDION,	"concertina");
-	afi("drmsp",	GM_FLUTE,	"dramatic soprano");
-	afi("dulc",		GM_DULCIMER,	"dulcimer");
-	afi("eguit",	GM_ELECTRIC_GUITAR_CLEAN,	"electric guitar");
-	afi("fag_c",	GM_BASSOON,	"contrabassoon");
-	afi("fagot",	GM_BASSOON,	"bassoon");
-	afi("false",	GM_RECORDER,	"falsetto");
-	afi("feme",		GM_CHOIR_AAHS,	"female voice");
-	afi("fife",		GM_BLOWN_BOTTLE,	"fife");
-	afi("fingc",	GM_REVERSE_CYMBAL,	"finger cymbal");
-	afi("flt",		GM_FLUTE,	"flute");
-	afi("flt_a",	GM_FLUTE,	"alto flute");
-	afi("flt_b",	GM_FLUTE,	"bass flute");
-	afi("fltda",	GM_RECORDER,	"alto recorder");
-	afi("fltdb",	GM_RECORDER,	"bass recorder");
-	afi("fltdn",	GM_RECORDER,	"sopranino recorder");
-	afi("fltds",	GM_RECORDER,	"soprano recorder");
-	afi("fltdt",	GM_RECORDER,	"tenor recorder");
-	afi("flugh",	GM_FRENCH_HORN,	"flugelhorn");
-	afi("forte",	GM_HONKYTONK_PIANO,	"fortepiano");
-	afi("glock",	GM_GLOCKENSPIEL,	"glockenspiel");
-	afi("gong", 	GM_STEEL_DRUMS,	"gong");
-	afi("guitr",	GM_ACOUSTIC_GUITAR_NYLON,	"guitar");
-	afi("hammd",	GM_DRAWBAR_ORGAN,	"Hammond electronic organ");
-	afi("heltn",	GM_CHOIR_AAHS,	"Heldentenor");
-	afi("hichi",	GM_OBOE,	"hichiriki");
-	afi("hurdy",	GM_LEAD_CALLIOPE,	"hurdy-gurdy");
-	afi("kit",		GM_SYNTH_DRUM,	"drum kit");
-	afi("kokyu",	GM_FIDDLE,	"kokyu (Japanese spike fiddle)");
-	afi("komun",	GM_KOTO,	"komun'go (Korean long zither)");
-	afi("koto",		GM_KOTO,	"koto (Japanese long zither)");
-	afi("kruma",	GM_TRUMPET,	"alto crumhorn");
-	afi("krumb",	GM_TRUMPET,	"bass crumhorn");
-	afi("krums",	GM_TRUMPET,	"soprano crumhorn");
-	afi("krumt",	GM_TRUMPET,	"tenor crumhorn");
-	afi("liuto",	GM_ACOUSTIC_GUITAR_NYLON,	"lute");
-	afi("lyrsp",	GM_FLUTE,	"lyric soprano");
-	afi("lyrtn",	GM_FRENCH_HORN,	"lyric tenor");
-	afi("male",		GM_CHOIR_AAHS,  	"male voice");
-	afi("mando",	GM_ACOUSTIC_GUITAR_NYLON,	"mandolin");
-	afi("marac",	GM_AGOGO,	"maracas");
-	afi("marim",	GM_MARIMBA,	"marimba");
-	afi("mezzo",	GM_CHOIR_AAHS,  	"mezzo soprano");
-	afi("nfant",	GM_CHOIR_AAHS,  	"child's voice");
-	afi("nokan",	GM_SHAKUHACHI,	"nokan (a Japanese flute)");
-	afi("oboeD",	GM_ENGLISH_HORN,	"oboe d'amore");
-	afi("oboe",		GM_OBOE,	"oboe");
-	afi("ocari",	GM_OCARINA,	"ocarina");
-	afi("organ",	GM_CHURCH_ORGAN,	"pipe organ");
-	afi("panpi",	GM_PAN_FLUTE,	"panpipe");
-	afi("piano",	GM_ACOUSTIC_GRAND_PIANO,	"pianoforte");
-	afi("piatt",	GM_REVERSE_CYMBAL,	"cymbals");
-	afi("picco",	GM_PICCOLO,	"piccolo");
-	afi("pipa",		GM_ACOUSTIC_GUITAR_NYLON,	"Chinese lute");
-	afi("porta",	GM_TANGO_ACCORDION,	"portative organ");
-	afi("psalt",	GM_CLAVI,	"psaltery (box zither)");
-	afi("qin",		GM_CLAVI,	"qin, ch'in (Chinese zither)");
-	afi("quitr",	GM_ACOUSTIC_GUITAR_NYLON,	"gittern");
-	afi("rackt",	GM_TRUMPET,	"racket");
-	afi("rebec",	GM_ACOUSTIC_GUITAR_NYLON,	"rebec");
-	afi("recit",	GM_CHOIR_AAHS,  	"recitativo");
-	afi("reedo",	GM_REED_ORGAN,	"reed organ");
-	afi("rhode",	GM_ELECTRIC_PIANO_1,	"Fender-Rhodes electric piano");
-	afi("ridec",	GM_REVERSE_CYMBAL,	"ride cymbal (kit)");
-	afi("sarod",	GM_SITAR,	"sarod");
-	afi("sarus",	GM_TUBA,	"sarrusophone");
-	afi("saxA",		GM_ALTO_SAX,	"E-flat alto saxophone");
-	afi("saxB",		GM_BARITONE_SAX,	"B-flat bass saxophone");
-	afi("saxC",		GM_BARITONE_SAX,	"E-flat contrabass saxophone");
-	afi("saxN",		GM_SOPRANO_SAX,	"E-flat sopranino saxophone");
-	afi("saxR",		GM_BARITONE_SAX,	"E-flat baritone saxophone");
-	afi("saxS",		GM_SOPRANO_SAX,	"B-flat soprano saxophone");
-	afi("saxT",		GM_TENOR_SAX,	"B-flat tenor saxophone");
-	afi("sdrum",	GM_SYNTH_DRUM,	"snare drum (kit)");
-	afi("shaku",	GM_SHAKUHACHI,	"shakuhachi");
-	afi("shami",	GM_SHAMISEN,	"shamisen (Japanese fretless lute)");
-	afi("sheng",	GM_SHANAI,	"mouth organ (Chinese)");
-	afi("sho",		GM_SHANAI,	"mouth organ (Japanese)");
-	afi("sitar",	GM_SITAR,	"sitar");
-	afi("soprn",	GM_CHOIR_AAHS,  	"soprano");
-	afi("spshc",	GM_REVERSE_CYMBAL,	"splash cymbal (kit)");
-	afi("steel",	GM_STEEL_DRUMS,	"steel-drum");
-	afi("sxhA",		GM_ALTO_SAX,	"E-flat alto saxhorn");
-	afi("sxhB",		GM_BARITONE_SAX,	"B-flat bass saxhorn");
-	afi("sxhC",		GM_BARITONE_SAX,	"E-flat contrabass saxhorn");
-	afi("sxhR",		GM_BARITONE_SAX,	"E-flat baritone saxhorn");
-	afi("sxhS",		GM_SOPRANO_SAX,	"B-flat soprano saxhorn");
-	afi("sxhT",		GM_TENOR_SAX,	"B-flat tenor saxhorn");
-	afi("synth",	GM_ELECTRIC_PIANO_2,	"keyboard synthesizer");
-	afi("tabla",	GM_MELODIC_DRUM,	"tabla");
-	afi("tambn",	GM_TINKLE_BELL,	"tambourine");
-	afi("tambu",	GM_MELODIC_DRUM,	"tambura");
-	afi("tanbr",	GM_MELODIC_DRUM,	"tanbur");
-	afi("tenor",	GM_CHOIR_AAHS,	"tenor");
-	afi("timpa",	GM_MELODIC_DRUM,	"timpani");
-	afi("tiorb",	GM_ACOUSTIC_GUITAR_NYLON,	"theorbo");
-	afi("tom",		GM_TAIKO_DRUM,	"tom-tom drum");
-	afi("trngl",	GM_TINKLE_BELL,	"triangle");
-	afi("tromb",	GM_TROMBONE,	"bass trombone");
-	afi("tromp",	GM_TRUMPET,	"trumpet");
-	afi("tromt",	GM_TROMBONE,	"tenor trombone");
-	afi("tuba",		GM_TUBA,	"tuba");
-	afi("ud",		GM_ACOUSTIC_GUITAR_NYLON,	"ud");
-	afi("ukule",	GM_ACOUSTIC_GUITAR_NYLON,	"ukulele");
-	afi("vibra",	GM_VIBRAPHONE,	"vibraphone");
-	afi("vina",		GM_SITAR,	"vina");
-	afi("viola",	GM_VIOLA,	"viola");
-	afi("violb",	GM_CONTRABASS,	"bass viola da gamba");
-	afi("viold",	GM_VIOLA,	"viola d'amore");
-	afi("violn",	GM_VIOLIN,	"violin");
-	afi("violp",	GM_VIOLIN,	"piccolo violin");
-	afi("viols",	GM_VIOLIN,	"treble viola da gamba");
-	afi("violt",	GM_CELLO,	"tenor viola da gamba");
-	afi("vox",		GM_CHOIR_AAHS,  	"generic voice");
-	afi("xylo",		GM_XYLOPHONE,	"xylophone");
-	afi("zithr",	GM_CLAVI,	"zither");
-	afi("zurna",	GM_ACOUSTIC_GUITAR_NYLON,	"zurna");
+   m_data.reserve(500);
+
+	// List has to be sorted by first parameter.  Maybe put in map.
+   afi("accor",   GM_ACCORDION,             "accordion");
+   afi("alto",    GM_RECORDER,              "alto");
+   afi("anvil",   GM_TINKLE_BELL,           "anvil");
+   afi("archl",   GM_ACOUSTIC_GUITAR_NYLON, "archlute");
+   afi("armon",   GM_HARMONICA,             "harmonica");
+   afi("arpa",    GM_ORCHESTRAL_HARP,       "harp");
+   afi("bagpI",   GM_BAGPIPE,               "bagpipe (Irish)");
+   afi("bagpS",   GM_BAGPIPE,               "bagpipe (Scottish)");
+   afi("banjo",   GM_BANJO,                 "banjo");
+   afi("bansu",   GM_FLUTE,                 "bansuri");
+   afi("barit",   GM_CHOIR_AAHS,            "baritone");
+   afi("baset",   GM_CLARINET,              "bassett horn");
+   afi("bass",    GM_CHOIR_AAHS,            "bass");
+   afi("bdrum",   GM_TAIKO_DRUM,            "bass drum");
+   afi("bguit",   GM_ELECTRIC_BASS_FINGER,  "electric bass guitar");
+   afi("biwa",    GM_FLUTE,                 "biwa");
+   afi("bongo",   GM_TAIKO_DRUM,            "bongo");
+   afi("brush",   GM_BREATH_NOISE,          "brush");
+   afi("bscan",   GM_CHOIR_AAHS,            "basso cantante");
+   afi("bspro",   GM_CHOIR_AAHS,            "basso profondo");
+   afi("bugle",   GM_TRUMPET,               "bugle");
+   afi("calam",   GM_OBOE,                  "chalumeau");
+   afi("calpe",   GM_LEAD_CALLIOPE,         "calliope");
+   afi("calto",   GM_CHOIR_AAHS,            "contralto");
+   afi("campn",   GM_TUBULAR_BELLS,         "bell");
+   afi("cangl",   GM_ENGLISH_HORN,          "english horn");
+   afi("canto",   GM_CHOIR_AAHS,            "canto");
+   afi("caril",   GM_TUBULAR_BELLS,         "carillon");
+   afi("castr",   GM_CHOIR_AAHS,            "castrato");
+   afi("casts",   GM_WOODBLOCKS,            "castanets");
+   afi("cbass",   GM_CONTRABASS,            "contrabass");
+   afi("cello",   GM_CELLO,                 "violoncello");
+   afi("cemba",   GM_HARPSICHORD,           "harpsichord");
+   afi("cetra",   GM_VIOLIN,                "cittern");
+   afi("chain",   GM_TINKLE_BELL,           "chains");
+   afi("chcym",   GM_REVERSE_CYMBAL,        "China cymbal");
+   afi("chime",   GM_TUBULAR_BELLS,         "chimes");
+   afi("chlma",   GM_BASSOON,               "alto shawm");
+   afi("chlms",   GM_BASSOON,               "soprano shawm");
+   afi("chlmt",   GM_BASSOON,               "tenor shawm");
+   afi("clap",    GM_GUNSHOT,               "hand clapping");
+   afi("clara",   GM_CLARINET,              "alto clarinet");
+   afi("clarb",   GM_CLARINET,              "bass clarinet");
+   afi("clarp",   GM_CLARINET,              "piccolo clarinet");
+   afi("clars",   GM_CLARINET,              "clarinet");
+   afi("clave",   GM_AGOGO,                 "claves");
+   afi("clavi",   GM_CLAVI,                 "clavichord");
+   afi("clest",   GM_CELESTA,               "celesta");
+   afi("clrno",   GM_TRUMPET,               "clarino");
+   afi("colsp",   GM_FLUTE,                 "coloratura soprano");
+   afi("conga",   GM_TAIKO_DRUM,            "conga");
+   afi("cor",     GM_FRENCH_HORN,           "horn");
+   afi("cornm",   GM_BAGPIPE,               "French bagpipe");
+   afi("corno",   GM_TRUMPET,               "cornett");
+   afi("cornt",   GM_TRUMPET,               "cornet");
+   afi("coro",    GM_CHOIR_AAHS,            "chorus");
+   afi("crshc",   GM_REVERSE_CYMBAL,        "crash cymbal");
+   afi("ctenor",  GM_CHOIR_AAHS,            "counter-tenor");
+   afi("ctina",   GM_ACCORDION,             "concertina");
+   afi("drmsp",   GM_FLUTE,                 "dramatic soprano");
+   afi("drum",    GM_SYNTH_DRUM,            "drum");
+   afi("drumP",   GM_SYNTH_DRUM,            "small drum");
+   afi("dulc",    GM_DULCIMER,              "dulcimer");
+   afi("eguit",   GM_ELECTRIC_GUITAR_CLEAN, "electric guitar");
+   afi("fag_c",   GM_BASSOON,               "contrabassoon");
+   afi("fagot",   GM_BASSOON,               "bassoon");
+   afi("false",   GM_RECORDER,              "falsetto");
+   afi("fdrum",   GM_TAIKO_DRUM,            "frame drum");
+   afi("feme",    GM_CHOIR_AAHS,            "female voice");
+   afi("fife",    GM_BLOWN_BOTTLE,          "fife");
+   afi("fingc",   GM_REVERSE_CYMBAL,        "finger cymbal");
+   afi("flt",     GM_FLUTE,                 "flute");
+   afi("flt_a",   GM_FLUTE,                 "alto flute");
+   afi("flt_b",   GM_FLUTE,                 "bass flute");
+   afi("fltda",   GM_RECORDER,              "alto recorder");
+   afi("fltdb",   GM_RECORDER,              "bass recorder");
+   afi("fltdn",   GM_RECORDER,              "sopranino recorder");
+   afi("fltds",   GM_RECORDER,              "soprano recorder");
+   afi("fltdt",   GM_RECORDER,              "tenor recorder");
+   afi("flugh",   GM_FRENCH_HORN,           "flugelhorn");
+   afi("forte",   GM_HONKYTONK_PIANO,       "fortepiano");
+   afi("gen",     GM_ACOUSTIC_GRAND_PIANO,  "generic instrument");
+   afi("genB",    GM_ACOUSTIC_GRAND_PIANO,  "generic bass instrument");
+   afi("genT",    GM_ACOUSTIC_GRAND_PIANO,  "generic treble instrument");
+   afi("glock",   GM_GLOCKENSPIEL,          "glockenspiel");
+   afi("gong",    GM_REVERSE_CYMBAL,        "gong");
+   afi("guitr",   GM_ACOUSTIC_GUITAR_NYLON, "guitar");
+   afi("hammd",   GM_DRAWBAR_ORGAN,         "Hammond electronic organ");
+   afi("hbell",   GM_TINKLE_BELL,           "handbell");
+   afi("hbell",   GM_TINKLE_BELL,           "handbell");
+   afi("heck",    GM_BASSOON,               "heckelphone");
+   afi("heltn",   GM_CHOIR_AAHS,            "Heldentenor");
+   afi("hichi",   GM_OBOE,                  "hichiriki");
+   afi("hurdy",   GM_LEAD_CALLIOPE,         "hurdy-gurdy");
+   afi("kitv",    GM_VIOLIN,                "kit violin");
+   afi("klav",    GM_ACOUSTIC_GRAND_PIANO,  "keyboard");
+   afi("kokyu",   GM_FIDDLE,                "kokyu");
+   afi("komun",   GM_KOTO,                  "komun'go");
+   afi("koto",    GM_KOTO,                  "koto");
+   afi("kruma",   GM_TRUMPET,               "alto crumhorn");
+   afi("krumb",   GM_TRUMPET,               "bass crumhorn");
+   afi("krums",   GM_TRUMPET,               "soprano crumhorn");
+   afi("krumt",   GM_TRUMPET,               "tenor crumhorn");
+   afi("lion",    GM_AGOGO,                 "lion's roar");
+   afi("liuto",   GM_ACOUSTIC_GUITAR_NYLON, "lute");
+   afi("lyrsp",   GM_FLUTE,                 "lyric soprano");
+   afi("lyrtn",   GM_FRENCH_HORN,           "lyric tenor");
+   afi("male",    GM_CHOIR_AAHS,            "male voice");
+   afi("mando",   GM_ACOUSTIC_GUITAR_NYLON, "mandolin");
+   afi("marac",   GM_AGOGO,                 "maracas");
+   afi("marim",   GM_MARIMBA,               "marimba");
+   afi("mbari",   GM_CHOIR_AAHS,            "high baritone");
+   afi("mezzo",   GM_CHOIR_AAHS,            "mezzo soprano");
+   afi("nfant",   GM_CHOIR_AAHS,            "child's voice");
+   afi("nokan",   GM_SHAKUHACHI,            "nokan");
+   afi("oboe",    GM_OBOE,                  "oboe");
+   afi("oboeD",   GM_ENGLISH_HORN,          "oboe d'amore");
+   afi("ocari",   GM_OCARINA,               "ocarina");
+   afi("ondes",   GM_PAD_SWEEP,             "ondes Martenot");
+   afi("ophic",   GM_TUBA,                  "ophicleide");
+   afi("organ",   GM_CHURCH_ORGAN,          "pipe organ");
+   afi("oud",     GM_ACOUSTIC_GUITAR_NYLON, "oud");
+   afi("paila",   GM_AGOGO,                 "timbales");
+   afi("panpi",   GM_PAN_FLUTE,             "panpipe");
+   afi("pbell",   GM_TUBULAR_BELLS,         "bell plate");
+   afi("pguit",   GM_ACOUSTIC_GUITAR_NYLON, "Portuguese guitar");
+   afi("physh",   GM_REED_ORGAN,            "physharmonica");
+   afi("piano",   GM_ACOUSTIC_GRAND_PIANO,  "pianoforte");
+   afi("piatt",   GM_REVERSE_CYMBAL,        "cymbals");
+   afi("picco",   GM_PICCOLO,               "piccolo");
+   afi("pipa",    GM_ACOUSTIC_GUITAR_NYLON, "Chinese lute");
+   afi("porta",   GM_TANGO_ACCORDION,       "portative organ");
+   afi("psalt",   GM_CLAVI,                 "psaltery");
+   afi("qin",     GM_CLAVI,                 "qin");
+   afi("quinto",  GM_CHOIR_AAHS,            "quinto");
+   afi("quitr",   GM_ACOUSTIC_GUITAR_NYLON, "gittern");
+   afi("rackt",   GM_TRUMPET,               "racket");
+   afi("ratl",    GM_WOODBLOCKS,            "rattle");
+   afi("rebec",   GM_ACOUSTIC_GUITAR_NYLON, "rebec");
+   afi("recit",   GM_CHOIR_AAHS,            "recitativo");
+   afi("reedo",   GM_REED_ORGAN,            "reed organ");
+   afi("rhode",   GM_ELECTRIC_PIANO_1,      "Fender-Rhodes electric piano");
+   afi("ridec",   GM_REVERSE_CYMBAL,        "ride cymbal");
+   afi("sarod",   GM_SITAR,                 "sarod");
+   afi("sarus",   GM_TUBA,                  "sarrusophone");
+   afi("saxA",    GM_ALTO_SAX,              "alto saxophone");
+   afi("saxB",    GM_BARITONE_SAX,          "bass saxophone");
+   afi("saxC",    GM_BARITONE_SAX,          "contrabass saxophone");
+   afi("saxN",    GM_SOPRANO_SAX,           "sopranino saxophone");
+   afi("saxR",    GM_BARITONE_SAX,          "baritone saxophone");
+   afi("saxS",    GM_SOPRANO_SAX,           "soprano saxophone");
+   afi("saxT",    GM_TENOR_SAX,             "tenor saxophone");
+   afi("sbell",   GM_TINKLE_BELL,           "sleigh bells");
+   afi("sdrum",   GM_SYNTH_DRUM,            "snare drum (kit)");
+   afi("shaku",   GM_SHAKUHACHI,            "shakuhachi");
+   afi("shami",   GM_SHAMISEN,              "shamisen");
+   afi("sheng",   GM_SHANAI,                "sheng");
+   afi("sho",     GM_SHANAI,                "sho");
+   afi("siren",   GM_FX_SCI_FI,             "siren");
+   afi("sitar",   GM_SITAR,                 "sitar");
+   afi("slap",    GM_GUNSHOT,               "slapstick");
+   afi("soprn",   GM_CHOIR_AAHS,            "soprano");
+   afi("spshc",   GM_REVERSE_CYMBAL,        "splash cymbal");
+   afi("steel",   GM_STEEL_DRUMS,           "steel-drum");
+   afi("stim",    GM_SEASHORE,              "Sprechstimme");
+   afi("stimA",   GM_SEASHORE,              "Sprechstimme, alto");
+   afi("stimB",   GM_SEASHORE,              "Sprechstimme, bass");
+   afi("stimC",   GM_SEASHORE,              "Sprechstimme, contralto");
+   afi("stimR",   GM_SEASHORE,              "Sprechstimme, baritone");
+   afi("stimS",   GM_SEASHORE,              "Sprechstimme, soprano");
+   afi("strdr",   GM_AGOGO,                 "string drum");
+   afi("sxhA",    GM_ALTO_SAX,              "alto saxhorn");
+   afi("sxhB",    GM_BARITONE_SAX,          "bass saxhorn");
+   afi("sxhC",    GM_BARITONE_SAX,          "contrabass saxhorn");
+   afi("sxhR",    GM_BARITONE_SAX,          "baritone saxhorn");
+   afi("sxhS",    GM_SOPRANO_SAX,           "soprano saxhorn");
+   afi("sxhT",    GM_TENOR_SAX,             "tenor saxhorn");
+   afi("synth",   GM_ELECTRIC_PIANO_2,      "keyboard synthesizer");
+   afi("tabla",   GM_MELODIC_DRUM,          "tabla");
+   afi("tambn",   GM_TINKLE_BELL,           "tambourine");
+   afi("tambu",   GM_MELODIC_DRUM,          "tambura");
+   afi("tanbr",   GM_MELODIC_DRUM,          "tanbur");
+   afi("tblok",   GM_WOODBLOCKS,            "temple blocks");
+   afi("tdrum",   GM_SYNTH_DRUM,            "tenor drum");
+   afi("tenor",   GM_CHOIR_AAHS,            "tenor");
+   afi("timpa",   GM_MELODIC_DRUM,          "timpani");
+   afi("tiorb",   GM_ACOUSTIC_GUITAR_NYLON, "theorbo");
+   afi("tom",     GM_TAIKO_DRUM,            "tom-tom drum");
+   afi("trngl",   GM_TINKLE_BELL,           "triangle");
+   afi("tromb",   GM_TROMBONE,              "bass trombone");
+   afi("tromp",   GM_TRUMPET,               "trumpet");
+   afi("tromt",   GM_TROMBONE,              "tenor trombone");
+   afi("tuba",    GM_TUBA,                  "tuba");
+   afi("tubaB",   GM_TUBA,                  "bass tuba");
+   afi("tubaC",   GM_TUBA,                  "contrabass tuba");
+   afi("tubaT",   GM_TUBA,                  "tenor tuba");
+   afi("tubaU",   GM_TUBA,                  "subcontra tuba");
+   afi("ukule",   GM_ACOUSTIC_GUITAR_NYLON, "ukulele");
+   afi("vibra",   GM_VIBRAPHONE,            "vibraphone");
+   afi("vina",    GM_SITAR,                 "vina");
+   afi("viola",   GM_VIOLA,                 "viola");
+   afi("violb",   GM_CONTRABASS,            "bass viola da gamba");
+   afi("viold",   GM_VIOLA,                 "viola d'amore");
+   afi("violn",   GM_VIOLIN,                "violin");
+   afi("violp",   GM_VIOLIN,                "piccolo violin");
+   afi("viols",   GM_VIOLIN,                "treble viola da gamba");
+   afi("violt",   GM_CELLO,                 "tenor viola da gamba");
+   afi("vox",     GM_CHOIR_AAHS,            "generic voice");
+   afi("wblok",   GM_WOODBLOCKS,            "woodblock");
+   afi("xylo",    GM_XYLOPHONE,             "xylophone");
+   afi("zithr",   GM_CLAVI,                 "zither");
+   afi("zurna",   GM_ACOUSTIC_GUITAR_NYLON, "zurna");
+
 }
 
 
@@ -14867,7 +14919,7 @@ void HumInstrument::afi(const char* humdrum_name, int midinum,
 	x.humdrum = humdrum_name;
 	x.gm = midinum;
 
-	data.push_back(x);
+	m_data.push_back(x);
 }
 
 
@@ -14884,14 +14936,14 @@ int HumInstrument::find(const string& Hname) {
 	key.name = "";
 	key.gm = 0;
 
-	searchResult = bsearch(&key, data.data(),
-			data.size(), sizeof(_HumInstrument),
+	searchResult = bsearch(&key, m_data.data(),
+			m_data.size(), sizeof(_HumInstrument),
 			&data_compare_by_humdrum_name);
 
 	if (searchResult == NULL) {
 		return -1;
 	} else {
-		return (int)(((TEMP64BITFIX)(searchResult)) - ((TEMP64BITFIX)(data.data())))/
+		return (int)(((TEMP64BITFIX)(searchResult)) - ((TEMP64BITFIX)(m_data.data())))/
 			sizeof(_HumInstrument);
 	}
 }
@@ -14917,7 +14969,7 @@ int HumInstrument::data_compare_by_humdrum_name(const void* a,
 //
 
 void HumInstrument::sortData(void) {
-	qsort(data.data(), data.size(), sizeof(_HumInstrument),
+	qsort(m_data.data(), m_data.size(), sizeof(_HumInstrument),
 		&HumInstrument::data_compare_by_humdrum_name);
 }
 
@@ -20488,24 +20540,16 @@ bool HumdrumFileBase::read(const char* filename) {
 
 
 bool HumdrumFileBase::read(istream& contents) {
-	clear();
-	m_displayError = true;
-	char buffer[123123] = {0};
-	HLp s;
-	while (contents.getline(buffer, sizeof(buffer), '\n')) {
-		s = new HumdrumLine(buffer);
-		s->setOwner(this);
-		m_lines.push_back(s);
-	}
-	return analyzeBaseFromLines();
-/*
-	if (!analyzeTokens()) { return isValid(); }
-	if (!analyzeLines() ) { return isValid(); }
-	if (!analyzeSpines()) { return isValid(); }
-	if (!analyzeLinks() ) { return isValid(); }
-	if (!analyzeTracks()) { return isValid(); }
-	return isValid();
-*/
+   clear();
+   m_displayError = true;
+   std::string buffer;
+   HLp s;
+   while (std::getline(contents, buffer)) {
+      s = new HumdrumLine(buffer);
+      s->setOwner(this);
+      m_lines.push_back(s);
+   }
+   return analyzeBaseFromLines();
 }
 
 
@@ -20569,6 +20613,39 @@ bool HumdrumFileBase::analyzeBaseFromLines(void)  {
 
 
 
+//////////////////////////////
+//
+// HumdrumFileBase::setFilenameFromSegment -- Update filename based on any
+//      !!!!SEGMENT: line at the top of the file.
+//
+
+void HumdrumFileBase::setFilenameFromSegment(void) {
+	HumdrumFileBase& infile = *this;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isEmpty()) {
+			continue;
+		}
+		if (!infile[i].isCommentUniversal()) {
+			break;
+		}
+		if (!infile[i].isUniversalReference()) {
+			break;
+		}
+		string key = infile[i].getUniversalReferenceKey();
+		if (key != "SEGMENT") {
+			// consider segment levels as well...
+			continue;
+		}
+		string value = infile[i].getUniversalReferenceValue();
+		if (!value.empty()) {
+			infile.setFilename(value);
+			break;
+		}
+	}
+}
+
+
+
 //////////////////////////////
 //
 // HumdrumFileBase::analyzeBaseFromTokens --
@@ -27598,6 +27675,13 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 
 restarting:
 
+	stringstream buffer;
+	string templine;
+	if (!m_newfilebuffer.empty()) {
+		buffer << m_newfilebuffer << endl;
+		m_newfilebuffer = "";
+	}
+
 	newinput = NULL;
 
 	if (m_urlbuffer.eof()) {
@@ -27630,21 +27714,21 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 	// (3) If ifstream is closed but there is a file to be processed,
 	// load it into the ifstream and start processing it immediately.
 	else if (((int)m_filelist.size() > 0) &&
-			(m_curfile < (int)m_filelist.size()-1)) {
+	      (m_curfile < (int)m_filelist.size()-1)) {
 		m_curfile++;
 		if (m_instream.is_open()) {
 			m_instream.close();
 		}
-		if (strstr(m_filelist[m_curfile].c_str(), "://") != NULL) {
+		if (m_filelist[m_curfile].find("://") != string::npos) {
 			// The next file to read is a URL/URI, so buffer the
 			// data from the internet and start reading that instead
 			// of reading from a file on the hard disk.
-			fillUrlBuffer(m_urlbuffer, m_filelist[m_curfile].c_str());
-			infile.setFilename(m_filelist[m_curfile].c_str());
+			fillUrlBuffer(m_urlbuffer, m_filelist[m_curfile]);
+			infile.setFilename(m_filelist[m_curfile]);
 			goto restarting;
 		}
-		m_instream.open(m_filelist[m_curfile].c_str());
-		infile.setFilename(m_filelist[m_curfile].c_str());
+		m_instream.open(m_filelist[m_curfile]);
+		infile.setFilename(m_filelist[m_curfile]);
 		if (!m_instream.is_open()) {
 			// file does not exist or cannot be opened close
 			// the file and try luck with next file in the list
@@ -27671,17 +27755,16 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 	if (m_newfilebuffer.size() > 0) {
 		// store the filename for the current HumdrumFile being read:
 		HumRegex hre;
-		if (hre.search(m_newfilebuffer,
-				R"(^!!!!SEGMENT\s*([+-]?\d+)?\s*:\s*(.*)\s*$)")) {
+		if (hre.search(m_newfilebuffer, R"(^!!!!SEGMENT\s*([+-]?\d+)?\s*:\s*(.*)\s*$)")) {
 			if (hre.getMatchLength(1) > 0) {
-				infile.setSegmentLevel(atoi(hre.getMatch(1).c_str()));
+				infile.setSegmentLevel(hre.getMatchInt(1));
 			} else {
 				infile.setSegmentLevel(0);
 			}
 			infile.setFilename(hre.getMatch(2));
 		} else if ((m_curfile >=0) && (m_curfile < (int)m_filelist.size())
-				&& (m_filelist.size() > 0)) {
-			infile.setFilename(m_filelist[m_curfile].c_str());
+		      && (m_filelist.size() > 0)) {
+			infile.setFilename(m_filelist[m_curfile]);
 		} else {
 			// reading from standard input, but no name.
 		}
@@ -27692,7 +27775,6 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 		return 0;
 	}
 
-	stringstream buffer;
 	int foundUniversalQ = 0;
 
 	// Start reading the input stream.  If !!!!SEGMENT: universal comment
@@ -27700,15 +27782,13 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 	// newly read HumdrumFile.  If other universal comments are found, then
 	// overwrite the old universal comments here.
 
-	int addedFilename = 0;
-	//int searchName = 0;
+	// int addedFilename = 0;
 	int dataFoundQ = 0;
 	int starstarFoundQ = 0;
 	int starminusFoundQ = 0;
 	if (m_newfilebuffer.size() < 4) {
 		//searchName = 1;
 	}
-	char templine[123123] = {0};
 
 	if (newinput->eof()) {
 		if (m_curfile < (int)m_filelist.size()-1) {
@@ -27723,85 +27803,49 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 
 	// if the previous line from the last read starts with "**"
 	// then treat it as part of the current file.
-	if ((m_newfilebuffer.size() > 1) &&
-		 (strncmp(m_newfilebuffer.c_str(), "**", 2)) == 0) {
+	if ((m_newfilebuffer.size() > 1) && (m_newfilebuffer.compare(0, 2, "**") == 0)) {
 		buffer << m_newfilebuffer << "\n";
 		m_newfilebuffer = "";
 		starstarFoundQ = 1;
 	}
 
 	while (!input.eof()) {
-		input.getline(templine, 123123, '\n');
-		if ((!dataFoundQ) &&
-				(strncmp(templine, "!!!!SEGMENT", strlen("!!!!SEGMENT")) == 0)) {
-			string tempstring;
-			tempstring = templine;
-			HumRegex hre;
-			if (hre.search(tempstring,
-					"^!!!!SEGMENT\\s*([+-]?\\d+)?\\s*:\\s*(.*)\\s*$")) {
-				if (hre.getMatchLength(1) > 0) {
-					infile.setSegmentLevel(atoi(hre.getMatch(1).c_str()));
-				} else {
-					infile.setSegmentLevel(0);
-				}
-				infile.setFilename(hre.getMatch(2));
+		getline(input, templine);
+		if (templine.compare(0, strlen("!!!!SEGMENT"), "!!!!SEGMENT") == 0) {
+			// Store the current segment line in the buffer before breaking.
+			if (!buffer.str().empty()) {
+				m_newfilebuffer = templine;
+				break;
 			}
+			m_newfilebuffer = templine;
 		}
 
-		if (strncmp(templine, "**", 2) == 0) {
+		if (templine.compare(0, 2, "**") == 0) {
 			if (starstarFoundQ == 1) {
 				m_newfilebuffer = templine;
 				// already found a **, so this one is defined as a file
 				// segment.  Exit from the loop and process the previous
-				// content, waiting until the next read for to start with
+				// content, waiting until the next read to start with
 				// this line.
 				break;
 			}
 			starstarFoundQ = 1;
 		}
 
-		if (input.eof() && (strcmp(templine, "") == 0)) {
+		if (input.eof() && templine.empty()) {
 			// No more data coming from current stream, so this is
 			// the end of the HumdrumFile.  Break from the while loop
 			// and then store the read contents of the stream in the
 			// HumdrumFile.
 			break;
 		}
-		// (1) Does the line start with "!!!!SEGMENT"?  If so, then
-		// this is either the name of the current or next file to process.
-		// (1a) this is the name of the current file to process if no
-		// data has yet been found,
-		// (1b) or a name is being actively searched for.
-		if (strncmp(templine, "!!!!SEGMENT", strlen("!!!!SEGMENT")) == 0) {
-			m_newfilebuffer = templine;
-			if (dataFoundQ) {
-				// this new filename is for the next chunk to process in the
-				// current file stream, not this one, so stop reading the
-				// HumdrumFile content and send what has already been read back
-				// out with new contents.
-			}  else {
-				// !!!!SEGMENT: came before any real data was read, so
-				// it is most likely the name of the current file
-				// (i.e., it comes at the start of the file stream and
-				// is the name of the first HumdrumFile in the stream).
-				HumRegex hre;
-				if (hre.search(m_newfilebuffer,
-						R"(^!!!!SEGMENT\s*([+-]?\d+)?\s:\s*(.*)\s*$)")) {
-					if (hre.getMatchLength(1) > 0) {
-						infile.setSegmentLevel(atoi(hre.getMatch(1).c_str()));
-					} else {
-						infile.setSegmentLevel(0);
-					}
-					infile.setFilename(hre.getMatch(2));
-				}
-			}
-		}
-		int len = (int)strlen(templine);
-		if ((len > 4) && (strncmp(templine, "!!!!", 4) == 0) &&
-				(templine[4] != '!') &&
-				(dataFoundQ == 0) &&
-				(strncmp(templine, "!!!!filter:", strlen("!!!!filter:")) != 0) &&
-				(strncmp(templine, "!!!!SEGMENT:", strlen("!!!!SEGMENT:")) != 0)) {
+
+		int len = templine.length();
+		if ((len > 4) && (templine.compare(0, 4, "!!!!") == 0) &&
+		    (templine[4] != '!') &&
+		    (dataFoundQ == 0) &&
+		    (templine.compare(0, strlen("!!!!filter:"), "!!!!filter:") != 0) &&
+		    (templine.compare(0, strlen("!!!!SEGMENT:"), "!!!!SEGMENT:") != 0)) {
 			// This is a universal comment.  Should it be appended
 			// to the list or should the current list be erased and
 			// this record placed into the first entry?
@@ -27819,39 +27863,33 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 			continue;
 		}
 
-		if (strncmp(templine, "*-", 2) == 0) {
+		if (templine.compare(0, 2, "*-") == 0) {
 			starminusFoundQ = 1;
 		}
 
-		// if before first ** in a data file or after *-, and the line
-		// does not start with '!' or '*', then assume that it is a file
-		// name which should be added to the file list to read.
-		if (((starminusFoundQ == 1) || (starstarFoundQ == 0))
-				&& (templine[0] != '*') && (templine[0] != '!')) {
-			if ((templine[0] != '\0') && (templine[0] != ' ')) {
-				// The file can only be added once in this manner
-				// so that infinite loops are prevented.
+		if (((starminusFoundQ == 1) || (starstarFoundQ == 0)) && (templine[0] != '*') && (templine[0] != '!')) {
+			if ((!templine.empty()) && (templine[0] != ' ')) {
 				int found = 0;
-				for (int mm=0; mm<(int)m_filelist.size(); mm++) {
-					if (strcmp(m_filelist[mm].c_str(), templine) == 0) {
+				for (int mm = 0; mm < (int)m_filelist.size(); mm++) {
+					if (m_filelist[mm] == templine) {
 						found = 1;
 					}
 				}
 				if (!found) {
 					m_filelist.push_back(templine);
-					addedFilename = 1;
+					// addedFilename = 1;
 				}
 				continue;
 			}
 		}
 
 		dataFoundQ = 1; // found something other than universal comments
-		// should empty lines be treated somewhat as universal comments?
 
 		// store the data line for later parsing into HumdrumFile record:
 		buffer << templine << "\n";
 	}
 
+/*
 	if (dataFoundQ == 0) {
 		// never found anything for some strange reason.
 		if (addedFilename) {
@@ -27859,6 +27897,7 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 		}
 		return 0;
 	}
+*/
 
 	// Arriving here means that reading of the data stream is complete.
 	// The string stream variable "buffer" contains the HumdrumFile
@@ -27870,29 +27909,33 @@ int HumdrumFileStream::getFile(HumdrumFile& infile) {
 	contents.str(""); // empty any contents in buffer
 	contents.clear(); // reset error flags in buffer
 
-	for (int i=0; i<(int)m_universals.size(); i++) {
-		// Convert universals reference records to globals, but do not demote !!!!filter:
+	for (int i=0; i < (int)m_universals.size(); i++) {
 		if (m_universals[i].compare(0, 11, "!!!!filter:") == 0) {
 			continue;
 		}
 		contents << &(m_universals[i][1]) << "\n";
 	}
+
 	contents << buffer.str();
-	string filename = infile.getFilename();
+	string oldfilename = infile.getFilename();
 	infile.readNoRhythm(contents);
-	if (!filename.empty()) {
-		infile.setFilename(filename);
+	string newfilename = infile.getFilename();
+	if (newfilename.empty() && !oldfilename.empty()) {
+		infile.setFilename(oldfilename);
 	}
+	infile.setFilenameFromSegment();
+
 	return 1;
 }
 
 
+
+
 //////////////////////////////
 //
 // HumdrumFileStream::fillUrlBuffer --
 //
 
-
 void HumdrumFileStream::fillUrlBuffer(stringstream& uribuffer,
 		const string& uriname) {
 	#ifdef USING_URI
@@ -31327,7 +31370,7 @@ int HumdrumLine::createTokensFromLine(void) {
 		token = new HumdrumToken();
 		token->setOwner(this);
 		m_tokens.push_back(token);
-		m_tokens.push_back(0);
+		m_tabs.push_back(0);
 	} else if (this->compare(0, 2, "!!") == 0) {
 		token = new HumdrumToken(this->c_str());
 		token->setOwner(this);
@@ -66283,13 +66326,14 @@ void Tool_composite::addCoincidenceMarks(HumdrumFile& infile) {
 			if (token->isNull()) {
 				continue;
 			}
-			if (token->isRest()) {
-				continue;
-			}
-			if (token->isNoteAttack()) {
-				string text = *token;
-				text += m_coinMark;
-				token->setText(text);
+			for (int i=0; i<token->getSubtokenCount(); i++) {
+				string subtok = token->getSubtoken(i);
+				if (subtok.find("r") != string::npos) {
+					continue;
+				}
+				subtok += m_coinMark;
+				token->replaceSubtoken(i, subtok);
+				// Maybe highlight only if note attack?
 			}
 		}
 	}
@@ -66689,6 +66733,11 @@ void Tool_composite::getAnalysisOutputLine(ostream& output, HumdrumFile& infile,
 				tempout << "/";
 			}
 		}
+		if (m_coinMarkQ) {
+			if (value.find("R") != string::npos) {
+				tempout << m_coinMark;
+			}
+		}
 		if (processedQ) {
 			tempout << "\t";
 		}
@@ -68965,7 +69014,7 @@ void Tool_composite::addMeterSignatureChanges(HumdrumFile& output, HumdrumFile&
 
 //////////////////////////////
 //
-// adjustBadCoincidenceRests --  Sometimes coincidence rests are not so great, particularly
+// adjustBadCoincidenceRests -- Sometimes coincidence rests are not so great, particularly
 //    when they are long and there is a small note that will add to it to fill in a measure
 //    (such as a 5 eighth-note rest in 6/8).  Try to simplify such case in this function
 //    (more can be added on a case-by-case basis).
@@ -72909,7 +72958,7 @@ Tool_deg::Tool_deg(void) {
 	define("kern=b",                                     "prefix composite rhythm **kern spine with -I option");
 	define("k|kern-tracks=s",                            "process only the specified kern spines");
 	define("kd|dk|key-default|default-key=s",            "default (initial) key if none specified in data");
-	define("kf|fk|key-force|force-key=s",                "use the given key for analysing deg data (ignore modulations)");
+	define("kf|fk|key-force|force-key|forced-key=s",     "use the given key for analysing deg data (ignore modulations)");
 	define("o|octave|octaves|degree=b",                  "encode octave information int **degree spines");
 	define("r|recip=b",                                  "prefix output data with **recip spine with -I option");
 	define("t|ties=b",                                   "include scale degrees for tied notes");
@@ -78192,17 +78241,10 @@ void Tool_double::doubleRhythms(HumdrumFile& infile) {
 //
 
 Tool_esac2hum::Tool_esac2hum(void) {
-	define("debug=b",            "print debug information");
-	define("v|verbose=b",        "verbose output");
-	define("h|header=s:",        "header filename for placement in output");
-	define("t|trailer=s:",       "trailer filename for placement in output");
-	define("s|split=s:file",     "split song info into separate files");
-	define("x|extension=s:.krn", "split filename extension");
-	define("f|first=i:1",        "number of first split filename");
-	define("author=b",           "author of program");
-	define("version=b",          "compilation info");
-	define("example=b",          "example usages");
-	define("help=b",             "short description");
+	define("debug=b", "Print debugging statements");
+	define("v|verbose=s", "Print verbose messages");
+	define("e|embed-esac=b", "Eembed EsAC data in output");
+	define("a|analyses|analysis=b", "Generate EsAC analysis fields");
 }
 
 
@@ -78214,13 +78256,12 @@ Tool_esac2hum::Tool_esac2hum(void) {
 //
 
 bool Tool_esac2hum::convertFile(ostream& out, const string& filename) {
-	ifstream file(filename);
-	stringstream s;
-	if (file) {
-		s << file.rdbuf();
-		file.close();
-	}
-	return convert(out, s.str());
+	initialize();
+   ifstream file(filename);
+   if (file) {
+      return convert(out, file);
+   }
+   return false;
 }
 
 
@@ -78238,87 +78279,57 @@ bool Tool_esac2hum::convert(ostream& out, const string& input) {
 }
 
 
-
-
 //////////////////////////////
 //
 // Tool_esac2hum::initialize --
 //
 
-bool Tool_esac2hum::initialize(void) {
-	// handle basic options:
-	if (getBoolean("author")) {
-		cerr << "Written by Craig Stuart Sapp, "
-			  << "craig@ccrma.stanford.edu, March 2002" << endl;
-		return false;
-	} else if (getBoolean("version")) {
-		cerr << getCommand() << ", version: 6 June 2017" << endl;
-		cerr << "compiled: " << __DATE__ << endl;
-		return false;
-	} else if (getBoolean("help")) {
-		usage(getCommand());
-		return false;
-	} else if (getBoolean("example")) {
-		example();
-		return false;
-	}
-
-	debugQ   = getBoolean("debug");
-	verboseQ = getBoolean("verbose");
-
-	if (getBoolean("header")) {
-		if (!getFileContents(header, getString("header"))) {
-			return false;
-		}
-	} else {
-		header.resize(0);
-	}
-	if (getBoolean("trailer")) {
-		if (!getFileContents(trailer, getString("trailer"))) {
-			return false;
-		}
-	} else {
-		trailer.resize(0);
-	}
-
-	if (getBoolean("split")) {
-		splitQ = 1;
+void Tool_esac2hum::initialize(void) {
+	m_debugQ     = getBoolean("debug");      // print debugging information
+	m_verboseQ   = getBoolean("verbose");    // print input EsAC MEL[] data when true
+	m_verbose    = getString("verbose");     // p = phrase, m=measure, n=note
+	m_embedEsacQ = getBoolean("embed-esac"); // don't print input EsAC data
+	m_analysisQ  = getBoolean("analyses");   // embed analysis in EsAC data
+	if (m_analysisQ) {
+		m_embedEsacQ = true;
 	}
-	namebase = getString("split");
-	fileextension = getString("extension");
-	firstfilenum = getInteger("first");
-	return true;
 }
 
 
 
-//////////////////////////////////////////////////////////////////////////
-
-
 //////////////////////////////
 //
 // Tool_esac2hum::convertEsacToHumdrum --
 //
 
 void Tool_esac2hum::convertEsacToHumdrum(ostream& output, istream& infile) {
-	initialize();
-	vector<string> song;
-	song.reserve(400);
-	int init = 0;
-	// int filecounter = firstfilenum;
-	string outfilename;
-	string numberstring;
-	// ofstream outfile;
+	m_inputline = 0;
+	m_prevline = "";
+
+	vector<string> song;  // contents of one EsAC song, extracted from input stream
+	song.reserve(1000);
+
 	while (!infile.eof()) {
-		if (debugQ) {
+		if (m_debugQ) {
 			cerr << "Getting a song..." << endl;
 		}
-		getSong(song, infile, init);
-		if (debugQ) {
+		bool status = getSong(song, infile);
+		if (!status) {
+			cerr << "Error getting a song" << endl;
+			continue;
+		}
+		if (m_debugQ) {
 			cerr << "Got a song ..." << endl;
 		}
-		init = 1;
-		convertSong(song, output);
+		if (song.empty()) {
+			cerr << "Song is empty" << endl;
+			continue;
+		}
+		if (song.size() < 4) {
+			cerr << "Song is too short" << endl;
+			continue;
+		}
+		convertSong(output, song);
 	}
 }
 
@@ -78326,49 +78337,127 @@ void Tool_esac2hum::convertEsacToHumdrum(ostream& output, istream& infile) {
 
 //////////////////////////////
 //
-// Tool_esac2hum::getSong -- get a song from the EsAC file
+// Tool_esac2hum::getSong -- get a song from a multiple-song EsAC file.
+//     Search for a CUT[] line which indicates the first line of the data.
+//     There will/can be some text above the CUT[] line.  The CUT[] field
+//     may contain newlnes, so searching only for CUT[ to also handle these
+//     cases.
 //
 
-bool Tool_esac2hum::getSong(vector<string>& song, istream& infile, int init) {
-	string holdbuffer;
+bool Tool_esac2hum::getSong(vector<string>& song, istream& infile) {
 	song.resize(0);
-	if (init) {
-		// do nothing holdbuffer has the CUT[] information
-	} else {
-		while (!infile.eof() && holdbuffer.compare(0, 4, "CUT[") != 0) {
-			getline(infile, holdbuffer);
-			if (verboseQ) {
-				cerr << "Contents: " << holdbuffer << endl;
+	m_globalComments.clear();
+
+	HumRegex hre;
+	string buffer;
+
+	// First find the next CUT[] line in the input which indcates
+	// the start of a song.  There typically is a non-empty line just above CUT[]
+	// containing information about the collection.
+	if (m_cutline.empty()) {
+		while (!infile.eof()) {
+			getline(infile, buffer);
+
+			if (hre.search(buffer, "^[!#]{2,}")) {
+				hre.search(buffer, "^([!#]{2,})(.*)$");
+				string prefix = hre.getMatch(1);
+				string postfix = hre.getMatch(2);
+				hre.replaceDestructive(prefix, "!", "#", "g");
+				string comment = prefix + postfix;
+				m_globalComments.push_back(comment);
+				continue;
 			}
-			if (holdbuffer.compare(0, 2, "!!") == 0) {
-				song.push_back(holdbuffer);
+
+			cleanText(buffer);
+			m_inputline++;
+			if (buffer.compare(0, 4, "CUT[") == 0) {
+				m_cutline = buffer;
+				break;
+			} else {
+				m_prevline = buffer;
+				continue;
 			}
 		}
-		if (infile.eof()) {
-			return false;
-		}
 	}
 
-	if (!infile.eof()) {
-		song.push_back(holdbuffer);
-	} else {
+	if (m_cutline.empty()) {
 		return false;
 	}
 
-	getline(infile, holdbuffer);
-	chopExtraInfo(holdbuffer);
-	inputline++;
-	if (verboseQ) {
-		cerr << "READ LINE: " << holdbuffer << endl;
+	if (infile.eof()) {
+		return false;
 	}
-	while (!infile.eof() && (holdbuffer.compare(0, 4, "CUT[", 4) != 0)) {
-		song.push_back(holdbuffer);
-		getline(infile, holdbuffer);
-		chopExtraInfo(holdbuffer);
-		inputline++;
-		if (verboseQ) {
-			cerr << "READ ANOTHER LINE: " << holdbuffer << endl;
+
+	if (!hre.search(m_prevline, "^\\s*$")) {
+		song.push_back(m_prevline);
+	}
+	song.push_back(m_cutline);
+
+	m_prevline.clear();
+	m_cutline.clear();
+
+	bool expectingCloseQ = false;
+
+	while (!infile.eof()) {
+		getline(infile, buffer);
+
+		if (hre.search(buffer, "^#{2,}")) {
+			hre.search(buffer, "^(#{2,})(.*)$");
+			string prefix = hre.getMatch(1);
+			string postfix = hre.getMatch(2);
+			hre.replaceDestructive(prefix, "!", "#", "g");
+			string comment = prefix + postfix;
+			m_globalComments.push_back(comment);
+			continue;
+		}
+
+		cleanText(buffer);
+		m_inputline++;
+		if (m_debugQ) {
+			cerr << "READ LINE: " << buffer << endl;
+		}
+		if (expectingCloseQ) {
+			if (buffer.find("[") != string::npos) {
+				cerr << "Strange error on line " << m_inputline << ": " << buffer << endl;
+				continue;
+			} else if (!hre.search(buffer, "[\\[\\]]")) {
+				// intermediate parameter line (not starting or ending)
+				song.push_back(buffer);
+				continue;
+			}
+
+			if (hre.search(buffer, "^[^\\]]*\\]\\s*$")) {
+				// closing bracket
+				expectingCloseQ = 0;
+				song.push_back(buffer);
+				continue;
+			} else {
+				cerr << "STRANGE CASE HERE " << buffer << endl;
+			}
+			continue;
+		}
+
+		if (hre.search(buffer, "^\\s*$")) {
+			continue;
+		}
+
+		if (hre.search(buffer, "^[A-Za-z][^\\[\\]]*$")) {
+			// collection line
+			m_prevline = buffer;
+			continue;
+		}
+
+		if (hre.search(buffer, "^[A-Za-z]+\\s*\\[[^\\]]*\\s*$")) {
+			// parameter with opening [
+			expectingCloseQ = true;
+		} else {
 		}
+
+		song.push_back(buffer);
+	}
+
+	if (expectingCloseQ) {
+		cerr << "Strange case: expecting closing of a song parameter around line " << m_inputline++ << endl;
 	}
 
 	return true;
@@ -78378,945 +78467,1244 @@ bool Tool_esac2hum::getSong(vector<string>& song, istream& infile, int init) {
 
 //////////////////////////////
 //
-// Tool_esac2hum::chopExtraInfo -- remove phrase number information from Luxembourg data.
+// Tool_esac2hum::cleanText -- remove \x88 and \x98 bytes from string (should not affect UTF-8 encodings)
+//     since those bytes do not seem to be involved with any UTF-8 characters.
 //
 
-void Tool_esac2hum::chopExtraInfo(string& buffer) {
+void Tool_esac2hum::cleanText(std::string& buffer) {
 	HumRegex hre;
-	hre.replaceDestructive(buffer, "", "^\\s+");
-	hre.replaceDestructive(buffer, "", "\\s+$");
+
+	// Fix UTF-8 double encodings (related to editing with Windows-1252 or ISO-8859-2 programs):
+
+	// Ą: c3 84 c2 84 - c4 84
+	hre.replaceDestructive(buffer, "\xc4\x84", "\xc3\x84\xc2\x84", "g");
+
+	// ą: c3 84 c2 85 - c4 85
+	hre.replaceDestructive(buffer, "\xc4\x85", "\xc3\x84\xc2\x85", "g");
+
+	// Ć: c3 84 c2 86 -> c4 86
+	hre.replaceDestructive(buffer, "\xc4\x86", "\xc3\x84\xc2\x86", "g");
+
+	// ć: c3 84 c2 87 -> c4 87
+	hre.replaceDestructive(buffer, "\xc4\x87", "\xc3\x84\xc2\x87", "g");
+
+	// Ę: c3 84 c2 98 -> c4 98
+	hre.replaceDestructive(buffer, "\xc4\x98", "\xc3\x84\xc2\x98", "g");
+
+	// ę: c3 84 c2 99 -> c4 99
+	hre.replaceDestructive(buffer, "\xc4\x99", "\xc3\x84\xc2\x99", "g");
+
+	// Ł: c4 b9 c2 81 -> c5 81
+	hre.replaceDestructive(buffer, "\xc5\x81", "\xc4\xb9\xc2\x81", "g");
+
+	// ł: c4 b9 c2 82 -> c5 82
+	hre.replaceDestructive(buffer, "\xc5\x82", "\xc4\xb9\xc2\x82", "g");
+
+	// Ń: c4 b9 c2 83 -> c5 83
+	hre.replaceDestructive(buffer, "\xc5\x83", "\xc4\xb9\xc2\x83", "g");
+
+	// ń: c4 b9 c2 84 -> c5 84
+	hre.replaceDestructive(buffer, "\xc5\x84", "\xc4\xb9\xc2\x84", "g");
+
+	// Ó: c4 82 c5 93 -> c3 93 (note: not sequential with ó)
+	hre.replaceDestructive(buffer, "\xc3\x93", "\xc4\x82\xc5\x93", "g");
+
+	// ó: c4 82 c5 82 -> c3 b3 (note: not sequential with Ó)
+	hre.replaceDestructive(buffer, "\xc3\xb3", "\xc4\x82\xc5\x82", "g");
+
+	// Ś: c4 b9 c2 9a -> c5 9a
+	hre.replaceDestructive(buffer, "\xc5\x9a", "\xc4\xb9\xc2\x9b", "g");
+
+	// ś: c4 b9 c2 9b -> c5 9b
+	hre.replaceDestructive(buffer, "\xc5\x9b", "\xc4\xb9\xc2\x9b", "g");
+
+	// Ź: c4 b9 c5 9a -> c5 b9
+	hre.replaceDestructive(buffer, "\xc5\xb9", "\xc4\xb9\xc5\x9a", "g");
+
+	// ź: c4 b9 c5 9f -> c5 ba
+	hre.replaceDestructive(buffer, "\xc5\xba", "\xc4\xb9\xc5\x9f", "g");
+
+	// Ż: c4 b9 c5 a5 -> c5 bb
+	hre.replaceDestructive(buffer, "\xc5\xbb", "\xc4\xb9\xc5\xa5", "g");
+	
+	// ż:  c4 b9 c5 ba -> c5 bc
+	hre.replaceDestructive(buffer, "\xc5\xbc", "\xc4\xb9\xc5\xba", "g");
+
+
+	// Random leftover characters from some character conversion:
+	hre.replaceDestructive(buffer, "", "[\x88\x98]", "g");
+
+	// Remove MS-DOS newline character at ends of lines:
+	if (!buffer.empty()) {
+		if (buffer.back() == 0x0d) {
+			// windows newline piece
+			buffer.resize(buffer.size() - 1);
+		}
+	}
+	// In VHV, when saving content to the local computer in EsAC mode, the 0x0d character should be added back.
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::printHumdrumHeaderInfo --
+// Tool_esac2hum::trimSpaces -- remove any trailing or leading spaces.
 //
 
-void Tool_esac2hum::printHumdrumHeaderInfo(ostream& out, vector<string>& song) {
-	for (int i=0; i<(int)song.size(); i++) {
-		if (song[i].size() == 0) {
-			continue;
-		}
-		if (song[i].compare(0, 2, "!!") == 0) {
-			out << song[i] << "\n";
-			continue;
-		}
-		if ((song[i][0] == ' ') || (song[i][0] == '\t')) {
-			continue;
-		}
-		break;
-	}
+string Tool_esac2hum::trimSpaces(const string& input) {
+	string output = input;
+	HumRegex hre;
+	hre.replaceDestructive(output, "", "^\\s+");
+	hre.replaceDestructive(output, "", "\\s+$");
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::printHumdrumFooterInfo --
+// Tool_esac2hum::convertSong --
 //
 
-void Tool_esac2hum::printHumdrumFooterInfo(ostream& out, vector<string>& song) {
-	int i = 0;
-	for (i=0; i<(int)song.size(); i++) {
-		if (song[i].size() == 0) {
-			continue;
-		}
-		if (song[i].compare(0, 2, "!!") == 0) {
-			continue;
-		}
-		if ((song[i][0] == ' ') || (song[i][0] == '\t')) {
-			continue;
-		}
-		break;
-	}
-	int j = i;
-	for (j=i; j<(int)song.size(); j++) {
-		if (song[j].compare(0, 2, "!!") == 0) {
-			out << song[j] << "\n";
-		}
-	}
+void Tool_esac2hum::convertSong(ostream& output, vector<string>& infile) {
+	getParameters(infile);
+	processSong();
+	// printParameters();
+	printHeader(output);
+	printScoreContents(output);
+	printFooter(output, infile);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::convertSong --
+// Tool_esac2hum::processSong -- parse and preliminary conversion to Humdrum.
 //
 
-void Tool_esac2hum::convertSong(vector<string>& song, ostream& out) {
+void Tool_esac2hum::processSong(void) {
+	string mel = m_score.m_params["MEL"];
+	m_score.parseMel(mel);
+}
 
-	int i;
-	if (verboseQ) {
-		for (i=0; i<(int)song.size(); i++) {
-			out << song[i] << "\n";
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::printScoreContents --
+//
+
+void Tool_esac2hum::printScoreContents(ostream& output) {
+
+	vector<string>& errors = m_score.m_errors;
+	if (!errors.empty()) {
+		for (int z=0; z<(int)errors.size(); z++) {
+			output << "!!" << errors.at(z) << endl;
 		}
 	}
 
-	printHumdrumHeaderInfo(out, song);
+	if (!m_score.m_clef.empty()) {
+		output << m_score.m_clef << endl;
+	}
+	if (!m_score.m_keysignature.empty()) {
+		output << m_score.m_keysignature << endl;
+	}
+	if (!m_score.m_keydesignation.empty()) {
+		output << m_score.m_keydesignation << endl;
+	}
+	if (!m_score.m_timesig.empty()) {
+		output << m_score.m_timesig << endl;
+	}
 
-	string key;
-	double mindur = 1.0;
-	string meter;
-	int tonic = 0;
-	getKeyInfo(song, key, mindur, tonic, meter, out);
+	for (int i=0; i<(int)m_score.size(); i++) {
+		Tool_esac2hum::Phrase& phrase = m_score.at(i);
+		if (m_verbose.find("p") != string::npos) {
+			output << "!!esac-phrase: " << phrase.esac;
+			if (m_verbose.find("pi") != string::npos) {
+				output << " [";
+				output << "ticks:" << phrase.m_ticks;
+				output << "]";
+			}
+			vector<string>& errors = phrase.m_errors;
+			if (!errors.empty()) {
+				for (int z=0; z<(int)errors.size(); z++) {
+					output << "!!" << errors.at(z) << endl;
+				}
+			}
+			output << endl;
+		}
 
-	vector<NoteData> songdata;
-	songdata.resize(0);
-	songdata.reserve(1000);
-	getNoteList(song, songdata, mindur, tonic);
-	placeLyrics(song, songdata);
+		for (int j=0; j<(int)phrase.size(); j++) {
 
-	vector<int> numerator;
-	vector<int> denominator;
-	getMeterInfo(meter, numerator, denominator);
+			Tool_esac2hum::Measure& measure = phrase.at(j);
+			if ((j == 0) && (i > 0)) {
+				output << "!!LO:LB:g=esac" << endl;
+			}
+			if (measure.m_barnum != 0) { // don't print barline if first is pickup
+				output << "=";
+				if (measure.m_barnum > 0) {
+					output << measure.m_barnum;
+				} else if (measure.m_barnum == -1) {
+					output << "-"; // "non-controlling" barline.
+				} else {
+					// visible barline, but not assigned a measure
+					// number (probably need more analysis to assign
+					// a measure number to this barline).
+				}
+				output  << endl;
+			}
+			if (m_verbose.find("m") != string::npos) {
+				output << "!!esac-measure: " << measure.esac;
+				if (m_verbose.find("mi") != string::npos) {
+					output << " [";
+					output << "ticks:" << measure.m_ticks;
+					if (measure.isComplete())  {
+						output << "; CM";
+					}
+					if (measure.isPartialBegin())  {
+						output << "; PB";
+					}
+					if (measure.isPartialEnd())  {
+						output << "; PE";
+					}
+					if (measure.isUnassigned())  {
+						output << "; UN";
+					}
+					output << "]";
+				}
+				output << endl;
+			}
+			vector<string>& errors = measure.m_errors;
+			if (!errors.empty()) {
+				for (int z=0; z<(int)errors.size(); z++) {
+					output << "!!" << errors.at(z) << endl;
+				}
+			}
 
-	postProcessSongData(songdata, numerator, denominator);
+			// print time signature change
+			if (!measure.m_measureTimeSignature.empty()) {
+				output << measure.m_measureTimeSignature << endl;
+			}
 
-	printTitleInfo(song, out);
-	out << "!!!id: "    << key  << "\n";
+			for (int k=0; k<(int)measure.size(); k++) {
 
-	// check for presence of lyrics
-	int textQ = 0;
-	for (i=0; i<(int)songdata.size(); i++) {
-		if (songdata[i].text !=  "") {
-			textQ = 1;
-			break;
+				Tool_esac2hum::Note& note = measure.at(k);
+				if (m_verbose.find("n") != string::npos) {
+					output << "!!esac-note: " << note.esac;
+					if (m_verbose.find("ni") != string::npos) {
+						output << " [";
+						output << "ticks:" << note.m_ticks;
+						output << ", deg:" << note.m_degree;
+						output << ", alt:" << note.m_alter;
+						output << ", oct:" << note.m_octave;
+						output << "]";
+					}
+					vector<string>& errors = note.m_errors;
+					if (!errors.empty()) {
+						for (int z=0; z<(int)errors.size(); z++) {
+							output << "!!" << errors.at(z) << endl;
+						}
+					}
+					output << endl;
+				}
+				output << note.m_humdrum << endl;
+
+			}
 		}
 	}
 
-	for (i=0; i<(int)header.size(); i++) {
-		out << header[i] << "\n";
+	if (m_score.hasFinalBarline()) {
+		output << "==" << endl;
+	} else {
+		output << "=" << endl;
 	}
+}
 
-	out << "**kern";
-	if (textQ) {
-		out << "\t**text";
-	}
-	out << "\n";
 
-	printKeyInfo(songdata, tonic, textQ, out);
-	for (i=0; i<(int)songdata.size(); i++) {
-		printNoteData(songdata[i], textQ, out);
-	}
-	out << "*-";
-	if (textQ) {
-		out << "\t*-";
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::parseMel --
+//
+
+bool Tool_esac2hum::Score::parseMel(const string& mel) {
+	clear();
+	reserve(100);
+
+	HumRegex hre;
+	if (hre.search(mel, "^\\s*$")) {
+		// no data;
+		cerr << "ERROR: MEL parameter is empty or non-existent" << endl;
+		return false;
 	}
-	out << "\n";
 
-	out << "!!!minrhy: ";
-	out << Convert::durationFloatToRecip(mindur)<<"\n";
-	out << "!!!meter";
-	if (numerator.size() > 1) {
-		out << "s";
+	vector<string> lines;
+	string line;
+
+	stringstream linestream;
+	linestream << mel;
+
+	int lineNumber = 0;
+	while (std::getline(linestream, line)) {
+		lineNumber++;
+		if (hre.search(line, "^\\s*$")) {
+			// Skip blank lines
+			continue;
+		}
+		string unknown = line;
+		hre.replaceDestructive(unknown, "", "[\\^0-9b\\s/._#()+-]+", "g");
+		if (!unknown.empty()) {
+			cerr << "Unknown characters " << ">>" << unknown << "<< " << " on mel line " << lineNumber << ": " << line << endl;
+		}
+		line = Tool_esac2hum::trimSpaces(line);
+		lines.push_back(line);
 	}
-	out << ": "  << meter;
-	if ((meter == "frei") || (meter == "Frei")) {
-		out << " [unmetered]";
-	} else if (meter.find('/') == string::npos) {
-		out << " interpreted as [";
-		for (i=0; i<(int)numerator.size(); i++) {
-			out << numerator[i] << "/" << denominator[i];
-			if (i < (int)numerator.size()-1) {
-				out << ", ";
+
+	m_finalBarline = false;
+	for (int i=0; i<(int)lines.size(); i++) {
+		string line = lines[i];
+		if (i == (int)lines.size() - 1) {
+			if (hre.search(line, "^(.*)\\s*//\\s*$")) {
+				m_finalBarline = true;
+				lines.back() = hre.getMatch(1);
 			}
 		}
-		out << "]";
 	}
-	out << "\n";
-
-	printBibInfo(song, out);
-	printSpecialChars(out);
-
-	for (i=0; i<(int)songdata.size(); i++) {
-		if (songdata[i].lyricerr) {
-			out << "!!!RWG: Lyric placement mismatch "
-				  << "in phrase (too many syllables) " << songdata[i].phnum << " ["
-				  << key << "]\n";
-			break;
+	// remove the last line if it is only "//":
+	if (!lines.empty()) {
+		if (hre.search(lines.back(), "^\\s*$")) {
+			lines.resize(lines.size() - 1);
 		}
 	}
+	if (lines.empty()) {
+		cerr << "ERROR: No notes in MEL data" << endl;
+		return false;
+	}
 
-	for (i=0; i<(int)trailer.size(); i++) {
-		out << trailer[i] << "\n";
+	for (int i=0; i<(int)lines.size(); i++) {
+		resize(size() + 1);
+		back().parsePhrase(lines[i]);
 	}
 
-	printHumdrumFooterInfo(out, song);
+	analyzeTies();
+	analyzePhrases();
+ 	generateHumdrumNotes();
+	calculateClef();
+	calculateKeyInformation();
+	calculateTimeSignatures();
 
-/*
-	if (!splitQ) {
-		out << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::assignFreeMeasureNumbers -- The time signature
+//    is "FREI", so assign a measure number to eavery barline, not checking
+//    for pickup or partial measures.
+//
+
+void Tool_esac2hum::Score::assignFreeMeasureNumbers(void) {
+	vector<Tool_esac2hum::Measure*> measurelist;
+	getMeasureList(measurelist);
+
+	int barnum = 1;
+	for (int i=0; i<(int)measurelist.size(); i++) {
+		measurelist[i]->m_barnum = barnum++;
+		measurelist[i]->m_partialBegin = false;
+		measurelist[i]->m_partialEnd = false;
+		measurelist[i]->m_complete = true;
 	}
-*/
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::placeLyrics -- extract lyrics (if any) and place on correct notes
+// Tool_esac2hum::Score::assignSingleMeasureNumbers -- There is a
+//    single time signature for the entire melody, so identify full
+//    and unfull measures, marking full that match the time signature
+//    duration as complete, and then try to pair measures and look
+//    for a pickup measure at the start of the music.
+//    The Measure::tsticks is the expected duration of the measure
+//    according to the time signature.
 //
 
-bool Tool_esac2hum::placeLyrics(vector<string>& song, vector<NoteData>& songdata) {
-	int start = -1;
-	int stop = -1;
-	getLineRange(song, "TXT", start, stop);
-	if (start < 0) {
-		// no TXT[] field, so don't do anything
-		return true;
+void Tool_esac2hum::Score::assignSingleMeasureNumbers(void) {
+	vector<Tool_esac2hum::Measure*> measurelist;
+	getMeasureList(measurelist);
+
+	if (measurelist.empty()) {
+		// strange error: no measures;
+		return;
 	}
-	int line = 0;
-	vector<string> lyrics;
-	string buffer;
-	for (line=0; line<=stop-start; line++) {
-		if (song[line+start].size() <= 4) {
-			cerr << "Error: lyric line is too short!: "
-				  << song[line+start] << endl;
-			return false;
+
+	// first identify complete measures:
+	for (int i=0; i<(int)measurelist.size(); i++) {
+		if (measurelist[i]->m_tsticks == measurelist[i]->m_ticks) {
+			measurelist[i]->setComplete();
 		}
-		buffer = song[line+start].substr(4);
-		if (line == stop - start) {
-			auto loc = buffer.rfind(']');
-			if (loc != string::npos) {
-				buffer.resize(loc);
-			}
+	}
+
+	// check for pickup measure at beginning of music
+	if (measurelist[0]->m_ticks < measurelist[0]->m_tsticks) {
+		measurelist[0]->setPartialEnd();
+		// check for partial measure at end that matches end measure
+		if (measurelist.back()->m_ticks < measurelist.back()->m_tsticks) {
+			measurelist.back()->setPartialBegin();
 		}
-		if (buffer == "") {
+	}
+
+	// search for pairs of partial measures
+	for (int i=1; i<(int)measurelist.size(); i++) {
+		if (!measurelist[i]->isUnassigned()) {
 			continue;
 		}
-		getLyrics(lyrics, buffer);
-		cleanupLyrics(lyrics);
-		placeLyricPhrase(songdata, lyrics, line);
+		if (!measurelist[i-1]->isUnassigned()) {
+			continue;
+		}
+		double ticks1 = measurelist[i-1]->m_ticks;
+		double ticks2 = measurelist[i]->m_ticks;
+		double tsticks1 = measurelist[i-1]->m_tsticks;
+		double tsticks2 = measurelist[i]->m_tsticks;
+		if (tsticks1 != tsticks2) {
+			// strange error;
+			continue;
+		}
+		if (ticks1 + ticks2 == tsticks2) {
+			measurelist[i-1]->setPartialBegin();
+			measurelist[i]->setPartialEnd();
+		}
 	}
 
-	return true;
+	// Now assign barlines to measures. that are complete or
+	// partial starts.
+	int barnum = 1;
+	for (int i=0; i<(int)measurelist.size(); i++) {
+		if (measurelist[i]->isComplete()) {
+			measurelist[i]->m_barnum = barnum++;
+		} else if (measurelist[i]->isPartialBegin()) {
+			measurelist[i]->m_barnum = barnum++;
+		} else if (measurelist[i]->isPartialEnd()) {
+			measurelist[i]->m_barnum = -1;
+		}
+	}
+	if (measurelist[0]->isPartialEnd()) {
+		measurelist[0]->m_barnum = 0; // pickup: don't add barline on first measure
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::cleanupLyrics -- add preceeding dashes, avoid starting *'s if any,
-//    and convert _'s to spaces.
+// Tool_esac2hum::Measure::isUnassigned --
 //
 
-void Tool_esac2hum::cleanupLyrics(vector<string>& lyrics) {
-	int length;
-	int length2;
-	int i, j, m;
-	int lastsyl = 0;
-	for (i=0; i<(int)lyrics.size(); i++) {
-		length = (int)lyrics[i].size();
-		for (j=0; j<length; j++) {
-			if (lyrics[i][j] == '_') {
-				lyrics[i][j] = ' ';
-			}
-		}
+bool Tool_esac2hum::Measure::isUnassigned(void) {
+	return !(m_complete || m_partialBegin || m_partialEnd);
+}
 
-		if (i > 0) {
-			if ((lyrics[i] != ".") &&
-				 (lyrics[i] != "")  &&
-				 (lyrics[i] != "%") &&
-				 (lyrics[i] != "^") &&
-				 (lyrics[i] != "|") &&
-				 (lyrics[i] != " ")) {
-				lastsyl = -1;
-				for (m=i-1; m>=0; m--) {
-					if ((lyrics[m] != ".") &&
-						 (lyrics[m] != "")  &&
-						 (lyrics[m] != "%") &&
-						 (lyrics[i] != "^") &&
-						 (lyrics[m] != "|") &&
-						 (lyrics[m] != " ")) {
-						lastsyl = m;
-						break;
-					}
-				}
-				if (lastsyl >= 0) {
-					length2 = (int)lyrics[lastsyl].size();
-					if (lyrics[lastsyl][length2-1] == '-') {
-						for (j=0; j<=length; j++) {
-							lyrics[i][length - j + 1] = lyrics[i][length - j];
-						}
-						lyrics[i][0] = '-';
-					}
-				}
-			}
-		}
 
-		// avoid *'s on the start of lyrics by placing a space before
-		// them if they exist.
-		if (lyrics[i][0] == '*') {
-			length = (int)lyrics[i].size();
-			for (j=0; j<=length; j++) {
-				lyrics[i][length - j + 1] = lyrics[i][length - j];
-			}
-			lyrics[i][0] = ' ';
-		}
+//////////////////////////////
+//
+// Tool_esac2hum::Measure::setComplete --
+//
 
-		// avoid !'s on the start of lyrics by placing a space before
-		// them if they exist.
-		if (lyrics[i][0] == '!') {
-			length = (int)lyrics[i].size();
-			for (j=0; j<=length; j++) {
-				lyrics[i][length - j + 1] = lyrics[i][length - j];
-			}
-			lyrics[i][0] = ' ';
-		}
+void Tool_esac2hum::Measure::setComplete(void) {
+	m_complete     = true;
+	m_partialBegin = false;
+	m_partialEnd   = false;
+}
 
-	}
 
+
+//////////////////////////////
+//
+// Tool_esac2hum::Measure::isComplete --
+//
+
+bool Tool_esac2hum::Measure::isComplete(void) {
+	return m_complete;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_esac2hum::getLyrics -- extract the lyrics from the text string.
+// Tool_esac2hum::Measure::setPartialBegin --
 //
 
-void Tool_esac2hum::getLyrics(vector<string>& lyrics, const string& buffer) {
-	lyrics.resize(0);
-	int zero1 = 0;
-	string current;
-	int zero2 = 0;
-	zero2 = zero1 + zero2;
+void Tool_esac2hum::Measure::setPartialBegin(void) {
+	m_complete     = false;
+	m_partialBegin = true;
+	m_partialEnd   = false;
+}
 
-	int length = (int)buffer.size();
-	int i;
 
-	i = 0;
-	while (i<length) {
-		current = "";
-		if (buffer[i] == ' ') {
-			current = ".";
-			lyrics.push_back(current);
-			i++;
-			continue;
-		}
 
-		while (i < length && buffer[i] != ' ') {
-			current += buffer[i++];
-		}
-		lyrics.push_back(current);
-		i++;
-	}
+//////////////////////////////
+//
+// Tool_esac2hum::Measure::isPartialBegin --
+//
 
+bool Tool_esac2hum::Measure::isPartialBegin(void) {
+	return m_partialBegin;
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_esac2hum::placeLyricPhrase -- match lyrics from a phrase to the songdata.
+// Tool_esac2hum::Measure::setPartialEnd --
 //
 
-bool Tool_esac2hum::placeLyricPhrase(vector<NoteData>& songdata, vector<string>& lyrics, int line) {
-	int i = 0;
-	int start = 0;
-	int found = 0;
+void Tool_esac2hum::Measure::setPartialEnd(void) {
+	m_complete     = false;
+	m_partialBegin = false;
+	m_partialEnd   = true;
+}
 
-	if (lyrics.empty()) {
-		return true;
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Measure::isPartialEnd --
+//
+
+bool Tool_esac2hum::Measure::isPartialEnd(void) {
+	return m_partialEnd;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::calculateTimeSignatures --
+//
+
+void Tool_esac2hum::Score::calculateTimeSignatures(void) {
+	string ts = m_params["_time"];
+	ts = trimSpaces(ts);
+	if (ts.find("FREI") != string::npos) {
+		m_timesig = "*MX";
+		setAllTimesigTicks(0.0);
+		assignFreeMeasureNumbers();
+		return;
 	}
 
-	// find the phrase to which the lyrics belongs
-	for (i=0; i<(int)songdata.size(); i++) {
-		if (songdata[i].phnum == line) {
-			found = 1;
-			break;
+
+	HumRegex hre;
+	if (hre.search(ts, "^(\\d+)/(\\d+)$")) {
+		m_timesig = "*M" + ts;
+		int top = hre.getMatchInt(1);
+		int bot = hre.getMatchInt(2);
+		// check if bot is a power of two?
+		double tsticks = top * m_minrhy / bot;
+		setAllTimesigTicks(tsticks);
+		assignSingleMeasureNumbers();
+		return;
+	} else if (hre.search(ts, "^(\\d+/\\d+(?:\\s+|$)){2,}$")) {
+		prepareMultipleTimeSignatures(ts);
+	}
+
+	// Complicated case where the time signature changes
+	vector<string> timesigs;
+	hre.split(timesigs, ts, "\\s+");
+	if (timesigs.size() < 2) {
+		m_errors.push_back("ERROR: strange format for time signatures.");
+		return;
+	}
+
+/* ggg
+	vector<double> bticks(timesigs.size(), 0);
+	for (int i=0; i<(int)bticks
+*/
+
+
+}
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::prepareMultipleTimeSignatures --
+//    N.B.: Will have problems when the duration of time siganture
+//    in a list are the same such as "4/4 2/2".
+//
+
+void Tool_esac2hum::Score::prepareMultipleTimeSignatures(const string& ts) {
+	vector<string> tss;
+	HumRegex hre;
+	string timesigs = ts;
+	hre.split(tss, timesigs, "\\s+");
+	if (tss.size() < 2) {
+		cerr << "Time sigs: " << ts << " needs to have at least two time signatures" << endl;
+	}
+
+	// Calculate tick duration of time signature in list:
+	vector<double> tsticks(tss.size(), 0);
+	for (int i=0; i<(int)tss.size(); i++) {
+		if (!hre.search(tss[i], "^(\\d+)/(\\d+)$")) {
+			continue;
 		}
+		int top = hre.getMatchInt(1);
+		int bot = hre.getMatchInt(2);
+		double ticks = top * m_minrhy / bot;
+		tsticks[i] = ticks;
 	}
-	start = i;
 
-	if (!found) {
-		cerr << "Error: cannot find music for lyrics line " << line << endl;
-		cerr << "Error near input data line: " << inputline << endl;
-		return false;
+	//cerr << "\nMultiple time signatures in melody: " << endl;
+	//for (int i=0; i<(int)tss.size(); i++) {
+	//	cerr << "(" << i+1 << "): " << tss[i] << "\tticks:" << tsticks[i] << endl;
+	//}
+	//cerr << endl;
+
+	// First assign a time signature to every inner measure in a phrase, which
+	// is presumed to be a complete measure:
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		for (int j=1; j<(int)phrase.size()-1; j++) {
+			Tool_esac2hum::Measure& measure = phrase.at(j);
+			for (int k=0; k<(int)tss.size(); k++) {
+				if (tsticks[k] == measure.m_ticks) {
+					measure.m_measureTimeSignature = "*M" + tss[k];
+					measure.setComplete();
+				}
+			}
+			
+		}
 	}
 
-	for (i=0; i<(int)lyrics.size() && i+start < (int)songdata.size(); i++) {
-		if ((lyrics[i] == " ") || (lyrics[i] == ".") || (lyrics[i] == "")) {
-			if (songdata[i+start].pitch < 0) {
-				lyrics[i] = "%";
-			} else {
-				lyrics[i] = "|";
+	// Now check if the measure at the end and beginning
+	// of the next phrase are both complete.  If not then
+	// calculate partial measure pairs.
+	for (int i=0; i<(int)size()-1; i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		Tool_esac2hum::Phrase& nextphrase = at(i+1);
+      if (phrase.size() < 2) {
+			// deal with phrases with a single measure later
+			continue;
+		}
+      if (nextphrase.size() < 2) {
+			// deal with phrases with a single measure later
+			continue;
+		}
+		Tool_esac2hum::Measure& measure = phrase.back();
+		Tool_esac2hum::Measure& nextmeasure = nextphrase.at(0);
+
+		int mticks  = measure.m_ticks;
+		int nmticks = nextmeasure.m_ticks;
+
+		int found1 = -1;
+		int found2 = -1;
+
+		for (int j=(int)tss.size() - 1; j>=0; j--) {
+			if (tsticks.at(j) == mticks) {
+				found1 = j;
+			}
+			if (tsticks.at(j) == nmticks) {
+				found2 = j;
 			}
-			// lyrics[i] = ".";
 		}
-		songdata[i+start].text = lyrics[i];
-		songdata[i+start].lyricnum = line;
-		if (line != songdata[i+start].phnum) {
-			songdata[i+start].lyricerr = 1;   // lyric does not line up with music
+		if ((found1 >= 0) && (found2 >= 0)) {
+			// The two measures are complete
+			measure.m_measureTimeSignature = "*M" + tss[found1];
+			nextmeasure.m_measureTimeSignature = "*M" + tss[found2];
+			measure.setComplete();
+			nextmeasure.setComplete();
+		} else {
+			// See if the sum of the two measures match
+			// a listed time signature.  if so, then they
+			// form two partial measures.
+			int ticksum = mticks + nmticks;
+			for (int z=0; z<(int)tsticks.size(); z++) {
+				if (tsticks.at(z) == ticksum) {
+					nextmeasure.m_barnum = -1;
+					measure.m_measureTimeSignature = "*M" + tss.at(z);
+					nextmeasure.m_measureTimeSignature = "*M" + tss.at(z);
+					measure.setPartialBegin();
+					nextmeasure.setPartialEnd();
+				}
+			}
 		}
 	}
 
-	return true;
-}
+	// Check if the first measure is a complete time signature in duration.
+	// If not then mark as pickup measure.  If incomplete and last measure
+	// is incomplete, then merge into a single measure (partial start for
+	// last measure and partial end for first measure.
+	if (empty()) {
+		// no data
+	} else if ((size() == 1) && (at(0).size() <= 1)) {
+		// single measure in melody
+	} else {
+		Tool_esac2hum::Measure& firstmeasure = at(0).at(0);
+		Tool_esac2hum::Measure& lastmeasure  = back().back();
 
+		double firstticks = firstmeasure.m_ticks;
+		double lastticks = lastmeasure.m_ticks;
 
+		int foundfirst = -1;
+		int foundlast  = -1;
 
-//////////////////////////////
-//
-// Tool_esac2hum::printSpecialChars -- print high ASCII character table
-//
+		for (int i=(int)tss.size() - 1; i>=0; i--) {
+			if (tsticks.at(i) == firstticks) {
+				foundfirst = i;
+			}
+			if (tsticks.at(i) == lastticks) {
+				foundlast = i;
+			}
+		}
 
-void Tool_esac2hum::printSpecialChars(ostream& out) {
-	int i;
-	for (i=0; i<(int)chartable.size(); i++) {
-		if (chartable[i]) {
-		switch (i) {
-			case 129:   out << "!!!RNB" << ": symbol: &uuml;  = u umlaut (UTF-8: "
-							     << (char)0xc3 << (char)0xb3 << ")\n";    break;
-			case 130:   out << "!!!RNB" << ": symbol: &eacute;= e acute  (UTF-8: "
-							     << (char)0xc3 << (char)0xa9 << ")\n";    break;
-			case 132:   out << "!!!RNB" << ": symbol: &auml;  = a umlaut (UTF-8: "
-							     << (char)0xc3 << (char)0xa4 << ")\n";    break;
-			case 134:   out << "!!!RNB" << ": symbol: $c      = c acute  (UTF-8: "
-							     << (char)0xc4 << (char)0x87 << ")\n";    break;
-			case 136:   out << "!!!RNB" << ": symbol: $l      = l slash  (UTF-8: "
-							     << (char)0xc5 << (char)0x82 << ")\n";    break;
-			case 140:   out << "!!!RNB" << ": symbol: &icirc; = i circumflex (UTF-8: "
-							     << (char)0xc3 << (char)0xaf << ")\n";    break;
-			case 141:   out << "!!!RNB" << ": symbol: $X      = Z acute  (UTF-8: "
-							     << (char)0xc5 << (char)0xb9 << ")\n";    break;
-			case 142:   out << "!!!RNB" << ": symbol: &auml;  = a umlaut (UTF-8: "
-							     << (char)0xc3 << (char)0xa4 << ")\n";    break;
-			case 143:   out << "!!!RNB" << ": symbol: $C      = C acute  (UTF-8: "
-							     << (char)0xc4 << (char)0x86 << ")\n";    break;
-			case 148:   out << "!!!RNB" << ": symbol: &ouml;  = o umlaut (UTF-8: "
-							     << (char)0xc3 << (char)0xb6 << ")\n";    break;
-			case 151:   out << "!!!RNB" << ": symbol: $S      = S acute  (UTF-8: "
-							     << (char)0xc5 << (char)0x9a << ")\n";    break;
-			case 152:   out << "!!!RNB" << ": symbol: $s      = s acute  (UTF-8: "
-							     << (char)0xc5 << (char)0x9b << ")\n";    break;
-			case 156:   out << "!!!RNB" << ": symbol: $s      = s acute  (UTF-8: "
-							     << (char)0xc5 << (char)0x9b << ")\n";    break;
-			case 157:   out << "!!!RNB" << ": symbol: $L      = L slash  (UTF-8: "
-							     << (char)0xc5 << (char)0x81 << ")\n";    break;
-			case 159:   out << "!!!RNB" << ": symbol: $vc     = c hachek (UTF-8: "
-							     << (char)0xc4 << (char)0x8d << ")\n";    break;
-			case 162:   out << "!!!RNB" << ": symbol: &oacute;= o acute  (UTF-8: "
-							     << (char)0xc3 << (char)0xb3 << ")\n";    break;
-			case 163:   out << "!!!RNB" << ": symbol: &uacute;= u acute  (UTF-8: "
-							     << (char)0xc3 << (char)0xba << ")\n";    break;
-			case 165:   out << "!!!RNB" << ": symbol: $a      = a hook   (UTF-8: "
-							     << (char)0xc4 << (char)0x85 << ")\n";    break;
-			case 169:   out << "!!!RNB" << ": symbol: $e      = e hook   (UTF-8: "
-							     << (char)0xc4 << (char)0x99 << ")\n";    break;
-			case 171:   out << "!!!RNB" << ": symbol: $y      = z acute  (UTF-8: "
-							     << (char)0xc5 << (char)0xba << ")\n";    break;
-			case 175:   out << "!!!RNB" << ": symbol: $Z      = Z dot    (UTF-8: "
-							     << (char)0xc5 << (char)0xbb << ")\n";    break;
-			case 179:   out << "!!!RNB" << ": symbol: $l      = l slash  (UTF-8: "
-							     << (char)0xc5 << (char)0x82 << ")\n";    break;
-			case 185:   out << "!!!RNB" << ": symbol: $a      = a hook   (UTF-8: "
-							     << (char)0xc4 << (char)0x85 << ")\n";    break;
-			case 189:   out << "!!!RNB" << ": symbol: $Z      = Z dot    (UTF-8: "
-							     << (char)0xc5 << (char)0xbb << ")\n";    break;
-			case 190:   out << "!!!RNB" << ": symbol: $z      = z dot    (UTF-8: "
-							     << (char)0xc5 << (char)0xbc << ")\n";    break;
-			case 191:   out << "!!!RNB" << ": symbol: $z      = z dot    (UTF-8: "
-							     << (char)0xc5 << (char)0xbc << ")\n";    break;
-			case 224:   out << "!!!RNB" << ": symbol: &Oacute;= O acute  (UTF-8: "
-							     << (char)0xc3 << (char)0x93 << ")\n";    break;
-			case 225:   out << "!!!RNB" << ": symbol: &szlig; = sz ligature (UTF-8: "
-							     << (char)0xc3 << (char)0x9f << ")\n";    break;
-			case 0xdf:  out << "!!!RNB" << ": symbol: &szlig; = sz ligature (UTF-8: "
-							     << (char)0xc3 << (char)0x9f << ")\n";    break;
-// Polish version:
-//         case 228:   out << "!!!RNB" << ": symbol: $n      = n acute  (UTF-8: "
-//                          << (char)0xc5 << (char)0x84 << ")\n";    break;
-// Luxembourg version for some reason...:
-			case 228:   out << "!!!RNB" << ": symbol: &auml;      = a umlaut  (UTF-8: "
-							     << (char)0xc5 << (char)0x84 << ")\n";    break;
-			case 230:   out << "!!!RNB" << ": symbol: c       = c\n";           break;
-			case 231:   out << "!!!RNB" << ": symbol: $vs     = s hachek (UTF-8: "
-							     << (char)0xc5 << (char)0xa1 << ")\n";    break;
-			case 234:   out << "!!!RNB" << ": symbol: $e      = e hook   (UTF-8: "
-							     << (char)0xc4 << (char)0x99 << ")\n";    break;
-			case 241:   out << "!!!RNB" << ": symbol: $n      = n acute  (UTF-8: "
-							     << (char)0xc5 << (char)0x84 << ")\n";    break;
-			case 243:   out << "!!!RNB" << ": symbol: &oacute;= o acute  (UTF-8: "
-							     << (char)0xc3 << (char)0xb3 << ")\n";    break;
-			case 252:   out << "!!!RNB" << ": symbol: &uuml;  = u umlaut (UTF-8: "
-							     << (char)0xc3 << (char)0xbc << ")\n";    break;
-//         default:
+		if ((foundfirst >= 0) && (foundlast >= 0)) {
+			// first and last measures are both complete
+			firstmeasure.m_measureTimeSignature = "*M" + tss.at(foundfirst);
+			lastmeasure.m_measureTimeSignature = "*M" + tss.at(foundlast);
+			firstmeasure.setComplete();
+			lastmeasure.setComplete();
+		} else {
+			// if both sum to a time signature than assigned that time signature to both
+			double sumticks = firstticks + lastticks;
+			int sumfound = -1;
+			for (int i=0; i<(int)tsticks.size(); i++) {
+				if (tsticks[i] == sumticks) {
+					sumfound = i;
+					break;
+				}
+			}
+			if (sumfound >= 0) {
+				// First and last meatures match a time signture, so
+				// use that time signture for both, mark firt measure
+				// last pickup (barnum -> 0), and mark last as partial
+				// measure start
+				firstmeasure.m_measureTimeSignature = "*M" + tss.at(sumfound);
+				lastmeasure.m_measureTimeSignature = "*M" + tss.at(sumfound);
+				firstmeasure.m_barnum = 0;
+				firstmeasure.setPartialEnd();
+				lastmeasure.setPartialBegin();
+			} else if ((foundfirst >= 0) && (foundlast < 0)) {
+				firstmeasure.setComplete();
+				lastmeasure.setPartialBegin();
+			} else if ((foundfirst < 0) && (foundlast >= 0)) {
+				firstmeasure.setPartialEnd();
+				lastmeasure.setComplete();
+			}
+		}
+	}
+
+
+	// Now assign bar numbers
+	// First probalby check for pairs of uncategorized measure durations (deal with that later).
+	vector<Tool_esac2hum::Measure*> measurelist;
+	getMeasureList(measurelist);
+	int barnum = 1;
+	for (int i=0; i<(int)measurelist.size(); i++) {
+		if ((i == 0) && measurelist.at(i)->isPartialEnd()) {
+			measurelist.at(i)->m_barnum = 0;
+			continue;
 		}
+		if (measurelist.at(i)->isComplete()) {
+			measurelist.at(i)->m_barnum = barnum++;
+		} else if (measurelist.at(i)->isPartialBegin()) {
+			measurelist.at(i)->m_barnum = barnum++;
+		} else if (measurelist.at(i)->isPartialEnd()) {
+			measurelist.at(i)->m_barnum = -1;
+		} else {
+			measurelist.at(i)->m_errors.push_back("UNCATEGORIZED MEASURE");
+		}
+	}
+
+	// Now remove duplicate time signatures
+	string current = "";
+	for (int i=0; i<(int)measurelist.size(); i++) {
+		if (measurelist.at(i)->m_measureTimeSignature == current) {
+			measurelist.at(i)->m_measureTimeSignature = "";
+		} else {
+			current = measurelist.at(i)->m_measureTimeSignature;
 		}
-		chartable[i] = 0;
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::printTitleInfo -- print the first line of the CUT[] field.
+// Tool_esac2hum::Score::setAllTimeSigTicks -- Used for calculating bar numbers;
 //
 
-bool Tool_esac2hum::printTitleInfo(vector<string>& song, ostream& out) {
-	int start = -1;
-	int stop = -1;
-	getLineRange(song, "CUT", start, stop);
-	if (start == -1) {
-		cerr << "Error: cannot find CUT[] field in song: " << song[0] << endl;
-		return false;
-	}
-
-	string buffer;
-	buffer = song[start].substr(4);
-	if (buffer.back() == ']') {
-		buffer.resize((int)buffer.size() - 1);
-	}
+void Tool_esac2hum::Score::setAllTimesigTicks(double ticks) {
+	vector<Tool_esac2hum::Measure*> measurelist;
+	getMeasureList(measurelist);
 
-	out << "!!!OTL: ";
-	for (int i=0; i<(int)buffer.size(); i++) {
-		printChar(buffer[i], out);
+	for (int i=0; i<(int)measurelist.size(); i++) {
+		measurelist[i]->m_tsticks = ticks;
 	}
-	out << "\n";
-
-	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::printChar -- print text characters, translating high-bit data
-//    if required.
+// Tool_esac2hum::Score::calculateKeyInformation --
 //
 
-void Tool_esac2hum::printChar(unsigned char c, ostream& out) {
-	out << c;
-/*
-	if (c < 128) {
-		out << c;
-	} else {
-		chartable[c]++;
-		switch (c) {
-			case 129:   out << "&uuml;";    break;
-			case 130:   out << "&eacute;";  break;
-			case 132:   out << "&auml;";    break;
-			case 134:   out << "$c";        break;
-			case 136:   out << "$l";        break;
-			case 140:   out << "&icirc;";   break;
-			case 141:   out << "$X";        break;   // Z acute
-			case 142:   out << "&auml;";    break;   // ?
-			case 143:   out << "$C";        break;
-			case 148:   out << "&ouml;";    break;
-			case 151:   out << "$S";        break;
-			case 152:   out << "$s";        break;
-			case 156:   out << "$s";        break;  // 1250 encoding
-			case 157:   out << "$L";        break;
-			case 159:   out << "$vc";       break;  // Cech c with v accent
-			case 162:   out << "&oacute;";  break;
-			case 163:   out << "&uacute;";  break;
-			case 165:   out << "$a";        break;
-			case 169:   out << "$e";        break;
-			case 171:   out << "$y";        break;
-			case 175:   out << "$Z";        break;  // 1250 encoding
-			case 179:   out << "$l";        break;  // 1250 encoding
-			case 185:   out << "$a";        break;  // 1250 encoding
-			case 189:   out << "$Z";        break;  // Z dot
-			case 190:   out << "$z";        break;  // z dot
-			case 191:   out << "$z";        break;  // 1250 encoding
-			case 224:   out << "&Oacute;";  break;
-			case 225:   out << "&szlig;";   break;
-			case 0xdf:  out << "&szlig;";   break;
-			// Polish version:
-			// case 228:   out << "$n";        break;
-			// Luxembourg version (for some reason...)
-			case 228:   out << "&auml;";        break;
-			case 230:   out << "c";         break;  // ?
-			case 231:   out << "$vs";       break;  // Cech s with v accent
-			case 234:   out << "$e";        break;  // 1250 encoding
-			case 241:   out << "$n";        break;  // 1250 encoding
-			case 243:   out << "&oacute;";  break;  // 1250 encoding
-			case 252:   out << "&uuml;";    break;
-			default:    out << c;
+void Tool_esac2hum::Score::calculateKeyInformation(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+
+	vector<int> b40pcs(40, 0);
+	for (int i=0; i<(int)notelist.size(); i++) {
+		int pc = notelist[i]->m_b40degree;
+		if ((pc >= 0) && (pc < 40)) {
+			b40pcs.at(pc)++;
+		}
+	}
+
+	string tonic = m_params["_tonic"];
+	if (tonic.empty()) {
+		// no tonic for some strange reason
+		// error will be reported when calculating Humdrum pitches.
+		return;
+	}
+	char letter = std::toupper(tonic[0]);
+
+	// Compare counts of third and sixth pitch classes:
+	int majorsum = b40pcs.at(12) + b40pcs.at(29);
+	int minorsum = b40pcs.at(11) + b40pcs.at(28);
+	if (minorsum > majorsum) {
+		letter = std::tolower(letter);
+	}
+	string flats;
+	string sharps;
+	for (int i=1; i<(int)tonic.size(); i++) {
+		if (tonic[i] == 'b') {
+			flats += "-";
+		} else if (tonic[i] == '#') {
+			sharps += "#";
+		}
+	}
+
+	m_keydesignation = "*";
+	m_keydesignation += letter;
+
+	if (!flats.empty() && !sharps.empty()) {
+		m_errors.push_back("ERROR: tonic note cannot include both sharps and flats.");
+	}
+	if (!flats.empty()) {
+		m_keydesignation += flats;
+	} else {
+		m_keydesignation += sharps;
+	}
+	m_keydesignation += ":";
+
+	if (std::isupper(letter)) {
+
+		// major key signature
+		if (m_keydesignation == "*C:") {
+			m_keysignature = "*k[]";
+		} else if (m_keydesignation == "*G:") {
+			m_keysignature = "*k[f#]";
+		} else if (m_keydesignation == "*D:") {
+			m_keysignature = "*k[f#c#]";
+		} else if (m_keydesignation == "*A:") {
+			m_keysignature = "*k[f#c#g#]";
+		} else if (m_keydesignation == "*E:") {
+			m_keysignature = "*k[f#c#g#d#]";
+		} else if (m_keydesignation == "*B:") {
+			m_keysignature = "*k[f#c#g#d#a#]";
+		} else if (m_keydesignation == "*F#:") {
+			m_keysignature = "*k[f#c#g#d#a#e#]";
+		} else if (m_keydesignation == "*C#:") {
+			m_keysignature = "*k[f#c#g#d#a#e#b#]";
+		} else if (m_keydesignation == "*F:") {
+			m_keysignature = "*k[b-]";
+		} else if (m_keydesignation == "*B-:") {
+			m_keysignature = "*k[b-e-]";
+		} else if (m_keydesignation == "*E-:") {
+			m_keysignature = "*k[b-e-a-]";
+		} else if (m_keydesignation == "*A-:") {
+			m_keysignature = "*k[b-e-a-d-]";
+		} else if (m_keydesignation == "*D-:") {
+			m_keysignature = "*k[b-e-a-d-g-]";
+		} else if (m_keydesignation == "*G-:") {
+			m_keysignature = "*k[b-e-a-d-g-c-]";
+		} else if (m_keydesignation == "*C-:") {
+			m_keysignature = "*k[b-e-a-d-g-f-]";
+		} else {
+			m_errors.push_back("ERROR: invalid/exotic key signature required.");
+		}
+
+	} else  {
+
+		// minor key signature
+		if (m_keydesignation == "*a:") {
+			m_keysignature = "*k[]";
+		} else if (m_keydesignation == "*e:") {
+			m_keysignature = "*k[f#]";
+		} else if (m_keydesignation == "*b:") {
+			m_keysignature = "*k[f#c#]";
+		} else if (m_keydesignation == "*f#:") {
+			m_keysignature = "*k[f#c#g$]";
+		} else if (m_keydesignation == "*c#:") {
+			m_keysignature = "*k[f#c#g$d#]";
+		} else if (m_keydesignation == "*g#:") {
+			m_keysignature = "*k[f#c#g$d#a#]";
+		} else if (m_keydesignation == "*d#:") {
+			m_keysignature = "*k[f#c#g$d#a#e#]";
+		} else if (m_keydesignation == "*a#:") {
+			m_keysignature = "*k[f#c#g$d#a#e#b#]";
+		} else if (m_keydesignation == "*d:") {
+			m_keysignature = "*k[b-]";
+		} else if (m_keydesignation == "*g:") {
+			m_keysignature = "*k[b-e-]";
+		} else if (m_keydesignation == "*c:") {
+			m_keysignature = "*k[b-e-a-]";
+		} else if (m_keydesignation == "*f:") {
+			m_keysignature = "*k[b-e-a-d-]";
+		} else if (m_keydesignation == "*b-:") {
+			m_keysignature = "*k[b-e-a-d-g-]";
+		} else if (m_keydesignation == "*e-:") {
+			m_keysignature = "*k[b-e-a-d-g-c-]";
+		} else if (m_keydesignation == "*a-:") {
+			m_keysignature = "*k[b-e-a-d-g-f-]";
+		} else {
+			m_errors.push_back("ERROR: invalid/exotic key signature required.");
 		}
 	}
-*/
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::printKeyInfo --
+// Tool_esac2hum::Score::calculateClef --
 //
 
-void Tool_esac2hum::printKeyInfo(vector<NoteData>& songdata, int tonic, int textQ,
-		ostream& out) {
-	vector<int> pitches(40, 0);
-	int pitchsum = 0;
-	int pitchcount = 0;
-	int i;
-	for (i=0; i<(int)songdata.size(); i++) {
-		if (songdata[i].pitch >= 0) {
-			pitches[songdata[i].pitch % 40]++;
-			pitchsum += Convert::base40ToMidiNoteNumber(songdata[i].pitch);
-			pitchcount++;
+void Tool_esac2hum::Score::calculateClef(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+
+	double sum = 0;
+	double count = 0;
+	int min12 = 1000;
+	int max12 = -1000;
+
+	for (int i=0; i<(int)notelist.size(); i++) {
+		int b12 = notelist[i]->m_b12;
+		if (b12 > 0) {
+			sum += b12;
+			count++;
+			if (b12 < min12) {
+				min12 = b12;
+			}
+			if (b12 > max12) {
+				max12 = b12;
+			}
 		}
 	}
+	double average = sum / count;
 
-	// generate a clef, choosing either treble or bass clef depending
-	// on the average pitch.
-	double averagepitch = pitchsum * 1.0 / pitchcount;
-	if (averagepitch > 60.0) {
-		out << "*clefG2";
-		if (textQ) {
-			out << "\t*clefG2";
-		}
-		out << "\n";
+
+	if ((min12 > 54) && (average >= 60.0)) {
+		m_clef = "*clefG2";
+	} else if ((max12 < 67) && (average < 60.0)) {
+		m_clef = "*clefF4";
+	} else if ((min12 > 47) && (min12 <= 57) && (max12 < 77) && (max12 >= 65)) {
+		m_clef = "*clefGv2";
+	} else if (average < 60.0) {
+		m_clef = "*clefF2";
 	} else {
-		out << "*clefF4";
-		if (textQ) {
-			out << "\t*clefF4";
-		}
-		out << "\n";
+		m_clef = "*clefG2";
 	}
+}
 
-	// generate a key signature
-	vector<int> diatonic(7, 0);
-	diatonic[0] = getAccidentalMax(pitches[1], pitches[2], pitches[3]);
-	diatonic[1] = getAccidentalMax(pitches[7], pitches[8], pitches[9]);
-	diatonic[2] = getAccidentalMax(pitches[13], pitches[14], pitches[15]);
-	diatonic[3] = getAccidentalMax(pitches[18], pitches[19], pitches[20]);
-	diatonic[4] = getAccidentalMax(pitches[24], pitches[25], pitches[26]);
-	diatonic[5] = getAccidentalMax(pitches[30], pitches[31], pitches[32]);
-	diatonic[6] = getAccidentalMax(pitches[36], pitches[37], pitches[38]);
 
-	int flatcount = 0;
-	int sharpcount = 0;
-	int naturalcount = 0;
-	for (i=0; i<7; i++) {
-		switch (diatonic[i]) {
-			case -1:   flatcount++;      break;
-			case  0:   naturalcount++;   break;
-			case +1:   sharpcount++;     break;
+
+//////////////////////////////
+//
+// Tool_esac2hum::generateHumdrumNotes --
+//
+
+void Tool_esac2hum::Score::generateHumdrumNotes(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+
+	string tonic = m_params["_tonic"];
+	if (tonic.empty()) {
+		m_errors.push_back("Error: cannot find KEY[] tonic pitch");
+		return;
+	}
+	char letter = std::tolower(tonic[0]);
+	m_b40tonic = 40 * 4 + 2;  // start with middle C
+	switch (letter) {
+		case 'd': m_b40tonic +=  6; break;
+		case 'e': m_b40tonic += 12; break;
+		case 'f': m_b40tonic += 17; break;
+		case 'g': m_b40tonic += 23; break;
+		case 'a': m_b40tonic += 29; break;
+		case 'b': m_b40tonic += 35; break;
+	}
+	int flats = 0;
+	int sharps = 0;
+	for (int i=1; i<(int)tonic.size(); i++) {
+		if (tonic[i] == 'b') {
+			flats++;
+		} else if (tonic[i] == '#') {
+			sharps++;
 		}
 	}
+	if (flats > 0) {
+		m_b40tonic -= flats;
+	} else if (sharps > 0) {
+		m_b40tonic += sharps;
+	}
 
-	char kbuf[32] = {0};
-	if (naturalcount == 7) {
-		// do nothing
-	} else if (flatcount > sharpcount) {
-		// print a flat key signature
-		if (diatonic[6] == -1) strcat(kbuf, "b-"); else goto keysigend;
-		if (diatonic[2] == -1) strcat(kbuf, "e-"); else goto keysigend;
-		if (diatonic[5] == -1) strcat(kbuf, "a-"); else goto keysigend;
-		if (diatonic[1] == -1) strcat(kbuf, "d-"); else goto keysigend;
-		if (diatonic[4] == -1) strcat(kbuf, "g-"); else goto keysigend;
-		if (diatonic[0] == -1) strcat(kbuf, "c-"); else goto keysigend;
-		if (diatonic[3] == -1) strcat(kbuf, "f-"); else goto keysigend;
-	} else {
-		// print a sharp key signature
-		if (diatonic[3] == +1) strcat(kbuf, "f#"); else goto keysigend;
-		if (diatonic[0] == +1) strcat(kbuf, "c#"); else goto keysigend;
-		if (diatonic[4] == +1) strcat(kbuf, "g#"); else goto keysigend;
-		if (diatonic[1] == +1) strcat(kbuf, "d#"); else goto keysigend;
-		if (diatonic[5] == +1) strcat(kbuf, "a#"); else goto keysigend;
-		if (diatonic[2] == +1) strcat(kbuf, "e#"); else goto keysigend;
-		if (diatonic[6] == +1) strcat(kbuf, "b#"); else goto keysigend;
+	string minrhy = m_params["_minrhy"];
+	if (minrhy.empty()) {
+		m_errors.push_back("Error: cannot find KEY[] minrhy");
+		return;
 	}
 
-keysigend:
-	out << "*k[" << kbuf << "]";
-	if (textQ) {
-		out << "\t*k[" << kbuf << "]";
+	m_minrhy = std::stoi(minrhy);
+	// maybe check of power of two?
+
+	for (int i=0; i<(int)notelist.size(); i++) {
+		notelist.at(i)->generateHumdrum(m_minrhy, m_b40tonic);
 	}
-	out << "\n";
 
-	// look at the third scale degree above the tonic pitch
-	int minor = pitches[(tonic + 40 + 11) % 40];
-	int major = pitches[(tonic + 40 + 12) % 40];
+}
 
-	if (minor > major) {
-		// minor key (or related mode)
-		out  << "*" << Convert::base40ToKern(40 * 4 + tonic) << ":";
-		if (textQ) {
-			out  << "\t*" << Convert::base40ToKern(40 * 4 + tonic) << ":";
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Note::generateHumdrum -- convert EsAC note to Humdrum note token.
+//
+
+void Tool_esac2hum::Note::generateHumdrum(int minrhy, int b40tonic) {
+	string pitch;
+	if (m_degree != 0) {
+		m_b40degree = 0;
+		switch (abs(m_degree)) {
+			case 2: m_b40degree += 6;  break;
+			case 3: m_b40degree += 12; break;
+			case 4: m_b40degree += 17; break;
+			case 5: m_b40degree += 23; break;
+			case 6: m_b40degree += 29; break;
+			case 7: m_b40degree += 35; break;
+		}
+		if ((m_alter >= -2) && (m_alter <= 2)) {
+			m_b40degree += m_alter;
+		} else {
+			m_errors.push_back("Error: chromatic alteration on note too large");
 		}
-		out << "\n";
+		m_b40 = 40 * m_octave + m_b40degree + b40tonic;
+		pitch = Convert::base40ToKern(m_b40);
+		// m_b12 is used for calculating clef later on.
+		m_b12 = Convert::base40ToMidiNoteNumber(m_b40);
 	} else {
-		// major key (or related mode)
-		out  << "*" << Convert::base40ToKern(40 * 3 + tonic) << ":";
-		if (textQ) {
-			out  << "\t*" << Convert::base40ToKern(40 * 3 + tonic) << ":";
-		}
-		out << "\n";
+		pitch = "r";
+		m_b40 = -1000;
+		m_b40degree = -1000;
+	}
+
+	HumNum duration(1, minrhy);
+	int multiplier = (1 << m_underscores);
+	duration *= multiplier;
+	duration *= 4;  // convert from whole notes to quarter notes
+	duration *= m_factor;
+	string recip = Convert::durationToRecip(duration);
+	for (int i=0; i<m_dots; i++) {
+		recip += ".";
 	}
 
+	m_humdrum.clear();
+	if (m_phraseBegin) {
+		m_humdrum += "{";
+	}
+
+	if (m_tieBegin && !m_tieEnd) {
+		m_humdrum += "[";
+	}
+
+	m_humdrum += recip;
+	m_humdrum += pitch;
+
+	if (!m_tieBegin && m_tieEnd) {
+		m_humdrum += "]";
+	} else if (m_tieBegin && m_tieEnd) {
+		m_humdrum += "_";
+	}
+
+	if (m_phraseEnd) {
+		m_humdrum += "}";
+	}
 }
 
 
+
 //////////////////////////////
 //
-// Tool_esac2hum::getAccidentalMax --
+// Tool_esac2hum::Score::analyzeTies -- Create a list of notes
+//     in each phrase and then assign a phrase start to the
+//     first non-rest note, and phrase end to the last non-rest note.
 //
 
-int Tool_esac2hum::getAccidentalMax(int a, int b, int c) {
-	if (a > b && a > c) {
-		return -1;
-	} else if (c > a && c > b) {
-		return +1;
-	} else {
-		return 0;
+void Tool_esac2hum::Score::analyzeTies(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+
+	for (int i=1; i<(int)notelist.size(); i++) {
+		// negative m_degree indicates a tied note to previous note
+		if (notelist.at(i)->m_degree < 0) {
+			// Tied note, so link to previous note.
+			notelist.at(i)->m_tieEnd = true;
+			notelist.at(i-1)->m_tieBegin = true;
+			if (notelist.at(i-1)->m_degree >= 0) {
+				notelist.at(i)->m_degree = -notelist.at(i-1)->m_degree;
+				// Copy chromatic alteration and octave:
+				notelist[i]->m_alter = notelist.at(i-1)->m_alter;
+				notelist[i]->m_octave = notelist.at(i-1)->m_octave;
+			}
+		}
 	}
 }
 
 
+
 //////////////////////////////
 //
-// Tool_esac2hum::postProcessSongData -- clean up data and do some interpreting.
+// Tool_esac2hum::Score::getNoteList -- Return a list of all notes
+//      in the score.
 //
 
-void Tool_esac2hum::postProcessSongData(vector<NoteData>& songdata, vector<int>& numerator,
-		vector<int>& denominator) {
-	int i, j;
-	// move phrase start markers off of rests and onto the
-	// first note that it finds
-	for (i=0; i<(int)songdata.size()-1; i++) {
-		if (songdata[i].pitch < 0 && songdata[i].phstart) {
-			songdata[i+1].phstart = songdata[i].phstart;
-			songdata[i].phstart = 0;
+void Tool_esac2hum::Score::getNoteList(vector<Tool_esac2hum::Note*>& notelist) {
+	notelist.clear();
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		for (int j=0; j<(int)phrase.size(); j++) {
+			Tool_esac2hum::Measure& measure = phrase[j];
+			for (int k=0; k<(int)measure.size(); k++) {
+				notelist.push_back(&measure.at(k));
+			}
 		}
 	}
+}
 
-	// move phrase ending markers off of rests and onto the
-	// previous note that it finds
-	for (i=(int)songdata.size()-1; i>0; i--) {
-		if (songdata[i].pitch < 0 && songdata[i].phend) {
-			songdata[i-1].phend = songdata[i].phend;
-			songdata[i].phend = 0;
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::getMeasureList --
+//
+
+void Tool_esac2hum::Score::getMeasureList(vector<Tool_esac2hum::Measure*>& measurelist) {
+	measurelist.clear();
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		for (int j=0; j<(int)phrase.size(); j++) {
+			Tool_esac2hum::Measure& measure = phrase[j];
+			measurelist.push_back(&measure);
 		}
 	}
+}
 
-	// examine barline information
-	double dur = 0.0;
-	for (i=(int)songdata.size()-1; i>=0; i--) {
-		if (songdata[i].bar == 1) {
-			songdata[i].bardur = dur;
-			dur = songdata[i].duration;
-		} else {
-			dur += songdata[i].duration;
-		}
-	}
-
-	int barnum = 0;
-	double firstdur = 0.0;
-	if (numerator.size() == 1 && numerator[0] > 0) {
-		// handle single non-frei meter
-		songdata[0].num = numerator[0];
-		songdata[0].denom = denominator[0];
-		dur = 0;
-		double meterdur = 4.0 / denominator[0] * numerator[0];
-		for (i=0; i<(int)songdata.size(); i++) {
-			if (songdata[i].bar) {
-				dur = 0.0;
-			} else {
-				dur += songdata[i].duration;
-				if (fabs(dur - meterdur) < 0.001) {
-					songdata[i].bar = 1;
-					songdata[i].barinterp = 1;
-					dur = 0.0;
-				}
-			}
-		}
-
-		// readjust measure beat counts
-		dur = 0.0;
-		for (i=(int)songdata.size()-1; i>=0; i--) {
-			if (songdata[i].bar == 1) {
-				songdata[i].bardur = dur;
-				dur = songdata[i].duration;
-			} else {
-				dur += songdata[i].duration;
-			}
-		}
-		firstdur = dur;
-
-		// number the barlines
-		barnum = 0;
-		if (fabs(firstdur - meterdur) < 0.001) {
-			// music for first bar, next bar will be bar 2
-			barnum = 2;
-		} else {
-			barnum = 1;
-			// pickup-measure
-		}
-		for (i=0; i<(int)songdata.size(); i++) {
-			if (songdata[i].bar == 1) {
-				songdata[i].barnum = barnum++;
-			}
-		}
-
-	} else if (numerator.size() == 1 && numerator[0] == -1) {
-		// handle free meter
 
-		// number the barline
-		firstdur = dur;
-		barnum = 1;
-		for (i=0; i<(int)songdata.size(); i++) {
-			if (songdata[i].bar == 1) {
-				songdata[i].barnum = barnum++;
-			}
-		}
 
-	} else {
-		// handle multiple time signatures
+//////////////////////////////
+//
+// Tool_esac2hum::Score::analyzePhrases -- Create a list of notes in the score
+//     and then search for ^ (-1 degrees) which mean a tied continuation
+//     of the previous note.
+//
 
-		// get the duration of each type of meter:
-		vector<double> meterdurs;
-		meterdurs.resize(numerator.size());
-		for (i=0; i<(int)meterdurs.size(); i++) {
-			meterdurs[i] = 4.0 / denominator[i] * numerator[i];
-		}
+void Tool_esac2hum::Score::analyzePhrases(void) {
+	// first create a list of the notes in the score
+	vector<Tool_esac2hum::Note*> notelist;
+	for (int i=0; i<(int)size(); i++) {
+		getPhraseNoteList(notelist, i);
 
-		// measure beat counts:
-		dur = 0.0;
-		for (i=(int)songdata.size()-1; i>=0; i--) {
-			if (songdata[i].bar == 1) {
-				songdata[i].bardur = dur;
-				dur = songdata[i].duration;
-			} else {
-				dur += songdata[i].duration;
-			}
+		if (notelist.empty()) {
+			at(i).m_errors.push_back("ERROR: no notes in phrase.");
+			return;
 		}
-		firstdur = dur;
 
-		// interpret missing barlines
-		int currentmeter = 0;
-		// find first meter
-		for (i=0; i<(int)numerator.size(); i++) {
-			if (fabs(firstdur - meterdurs[i]) < 0.001) {
-				songdata[0].num = numerator[i];
-				songdata[0].denom = denominator[i];
-				currentmeter = i;
+		// Find the first non-rest note and mark with phrase start:
+		bool foundNote = false;
+		for (int j=0; j<(int)notelist.size(); j++) {
+			if (notelist.at(j)->m_degree <= 0) {
+				continue;
 			}
+			foundNote = true;
+			notelist.at(j)->m_phraseBegin = true;
+			break;
 		}
-		// now handle the meters in the rest of the music...
-		int fnd = 0;
-		dur = 0;
-		for (i=0; i<(int)songdata.size()-1; i++) {
-			if (songdata[i].bar) {
-				if (songdata[i].bardur != meterdurs[currentmeter]) {
-					// try to find the correct new meter
 
-					fnd = 0;
-					for (j=0; j<(int)numerator.size(); j++) {
-						if (j == currentmeter) {
-							continue;
-						}
-						if (fabs(songdata[i].bardur - meterdurs[j]) < 0.001) {
-							songdata[i+1].num = numerator[j];
-							songdata[i+1].denom = denominator[j];
-							currentmeter = j;
-							fnd = 1;
-						}
-					}
-					if (!fnd) {
-						for (j=0; j<(int)numerator.size(); j++) {
-							if (j == currentmeter) {
-							   continue;
-							}
-							if (fabs(songdata[i].bardur/2.0 - meterdurs[j]) < 0.001) {
-							   songdata[i+1].num = numerator[j];
-							   songdata[i+1].denom = denominator[j];
-							   currentmeter = j;
-							   fnd = 1;
-							}
-						}
-					}
-				}
-				dur = 0.0;
-			} else {
-				dur += songdata[i].duration;
-				if (fabs(dur - meterdurs[currentmeter]) < 0.001) {
-					songdata[i].bar = 1;
-					songdata[i].barinterp = 1;
-					dur = 0.0;
-				}
-			}
+		if (!foundNote) {
+			at(i).m_errors.push_back("Error: cannot find any notes in phrase.");
+			continue;
 		}
 
-		// perhaps sum duration of measures again and search for error here?
-
-		// finally, number the barlines:
-		barnum = 1;
-		for (i=0; i<(int)numerator.size(); i++) {
-			if (fabs(firstdur - meterdurs[i]) < 0.001) {
-				barnum = 2;
-				break;
-			}
-		}
-		for (i=0; i<(int)songdata.size(); i++) {
-			if (songdata[i].bar == 1) {
-				songdata[i].barnum = barnum++;
+		// Find the last non-rest note and mark with phrase end:
+		for (int j=(int)notelist.size()-1; j>=0; j--) {
+			if (notelist.at(j)->m_degree <= 0) {
+				continue;
 			}
+			notelist.at(j)->m_phraseEnd = true;
+			break;
 		}
-
-
 	}
-
 }
 
 
-
 //////////////////////////////
 //
-// Tool_esac2hum::getMeterInfo --
+// Tool_esac2hum::Score::getPhraseNoteList -- Return a list of all notes
+//      in the 0-indexed phrase
 //
 
-void Tool_esac2hum::getMeterInfo(string& meter, vector<int>& numerator,
-		vector<int>& denominator) {
-	numerator.clear();
-	denominator.clear();
-	HumRegex hre;
-	hre.replaceDestructive(meter, "", "^\\s+");
-	hre.replaceDestructive(meter, "", "\\s+$");
-	if (hre.search(meter, "^(\\d+)/(\\d+)$")) {
-		numerator.push_back(hre.getMatchInt(1));
-		denominator.push_back(hre.getMatchInt(2));
+void Tool_esac2hum::Score::getPhraseNoteList(vector<Tool_esac2hum::Note*>& notelist, int index) {
+	notelist.clear();
+	if (index < 0) {
+		m_errors.push_back("ERROR: trying to access a negative phrase index");
 		return;
 	}
-	if (hre.search(meter, "^frei$", "i")) {
-		numerator.push_back(-1);
-		denominator.push_back(-1);
+	if (index >= (int)size()) {
+		m_errors.push_back("ERROR: trying to access a phrase index that is too large");
 		return;
 	}
-	cerr << "NEED TO DEAL WITH METER: " << meter << endl;
+	Tool_esac2hum::Phrase& phrase = at(index);
+
+	for (int i=0; i<(int)phrase.size(); i++) {
+		Tool_esac2hum::Measure& measure = phrase[i];
+		for (int j=0; j<(int)measure.size(); j++) {
+			Tool_esac2hum::Note& note = measure.at(j);
+			notelist.push_back(&note);
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::getLineRange -- get the staring line and ending line of a data
-//     field.  Returns -1 if the data field was not found.
+// Tool_esac2hum::Phrase::getNoteList -- Return a list of all notes
+//      in the phrase.
 //
 
-void Tool_esac2hum::getLineRange(vector<string>& song, const string& field,
-		int& start, int& stop) {
-	string searchstring = field;;
-	searchstring += "[";
-	start = stop = -1;
-	for (int i=0; i<(int)song.size(); i++) {
-		auto loc = song[i].find(']');
-		if (song[i].compare(0, searchstring.size(), searchstring) == 0) {
-			start = i;
-			if (loc != string::npos) {
-				stop = i;
-				break;
-			}
-		} else if ((start >= 0) && (loc != string::npos)) {
-			stop = i;
-			break;
+void Tool_esac2hum::Phrase::getNoteList(vector<Tool_esac2hum::Note*>& notelist) {
+	notelist.clear();
+	Tool_esac2hum::Phrase& phrase = *this;
+
+	for (int i=0; i<(int)phrase.size(); i++) {
+		Tool_esac2hum::Measure& measure = phrase[i];
+		for (int j=0; j<(int)measure.size(); j++) {
+			Tool_esac2hum::Note& note = measure.at(j);
+			notelist.push_back(&note);
 		}
 	}
 }
@@ -79325,172 +79713,117 @@ void Tool_esac2hum::getLineRange(vector<string>& song, const string& field,
 
 //////////////////////////////
 //
-// Tool_esac2hum::getNoteList -- get a list of the notes and rests and barlines in
-//    the MEL field.
+// Tool_esac2hum::Phrase::parsePhrase --
 //
 
-bool Tool_esac2hum::getNoteList(vector<string>& song, vector<NoteData>& songdata, double mindur,
-		int tonic) {
-	songdata.resize(0);
-	NoteData tempnote;
-	int melstart = -1;
-	int melstop  = -1;
-	int i, j;
-	int octave      = 0;
-	int degree      = 0;
-	int accidental  = 0;
-	double duration = mindur;
-	int bar    = 0;
-	// int tuplet = 0;
-	int major[8] = {-1, 0, 6, 12, 17, 23, 29, 35};
-	// int oldstate  = -1;
-	int state     = -1;
-	int nextstate = -1;
-	int phend = 0;
-	int phnum = 0;
-	int phstart = 0;
-	int slend = 0;
-	int slstart = 0;
-	int tie = 0;
+bool Tool_esac2hum::Phrase::parsePhrase(const string& phrase) {
+	esac = phrase;
 
-	getLineRange(song, "MEL", melstart, melstop);
+	vector<string> bars;
 
-	for (i=melstart; i<=melstop; i++) {
-		if (song[i].size() < 4) {
-			cerr << "Error: invalid line in MEL[]: " << song[i] << endl;
-			return false;
-		}
-		j = 4;
-		phstart = 1;
-		phend = 0;
-		// Note Format: (+|-)*[0..7]_*\.*(  )?
-		// ONADB
-		// Order of data: Octave, Note, Accidental, Duration, Barline
+	HumRegex hre;
+	string newphrase = phrase;
+	newphrase = trimSpaces(newphrase);
+	hre.split(bars, newphrase, "\\s+");
+	if (bars.empty()) {
+		cerr << "Funny error with no measures" << endl;
+		return false;
+	}
+	int length = (int)bars.size();
+	for (int i=0; i<length; i++) {
+		resize(size() + 1);
+		back().parseMeasure(bars[i]);
+	}
 
-		#define STATE_SLSTART -1
-		#define STATE_OCTAVE   0
-		#define STATE_NOTE     1
-		#define STATE_ACC      2
-		#define STATE_DUR      3
-		#define STATE_BAR      4
-		#define STATE_SLEND    5
+	// Calculate ticks for phrase:
+	m_ticks = 0;
+	for (int i=0; i<(int)size(); i++) {
+		m_ticks += at(i).m_ticks;
+	}
 
-		while (j < 200 && (j < (int)song[i].size())) {
-			// oldstate = state;
-			switch (song[i][j]) {
-				// Octave information:
-				case '-': octave--; state = STATE_OCTAVE; break;
-				case '+': octave++; state = STATE_OCTAVE; break;
+	return true;
+}
 
-				// Duration information:
-				case '_': duration *= 2.0; state = STATE_DUR; break;
-				case '.': duration *= 1.5; state = STATE_DUR; break;
 
-				// Accidental information:
-				case 'b': accidental--; state = STATE_ACC;  break;
-				case '#': accidental++; state = STATE_ACC;  break;
 
-				// Note information:
-				case '0': case '1': case '2': case '3': case '4':
-				case '5': case '6': case '7':
-					degree =  major[song[i][j] - '0'];
-					state = STATE_NOTE;
-					break;
-				case 'O':
-					degree =  major[0];
-					state = STATE_NOTE;
-					break;
+//////////////////////////////
+//
+// Tool_esac2hum::Measure::parseMeasure --
+//     Also deal with () for ties.
+//     Split notes by digit.  Prefix characters attached to digit:
+//        ^: equivalent to digit, tied to previous note.
+//        -: digit is scale degree in lower octave.
+//        (: slur start
+//
 
-				// Barline information:
-				case ' ':
-					state = STATE_BAR;
-					if (song[i][j+1] == ' ') {
-						bar = 1;
-					}
-					break;
+bool Tool_esac2hum::Measure::parseMeasure(const string& measure) {
+	esac = measure;
+	vector<string> tokens;
+	vector<HumNum> factors;
+	HumNum factor = 1;
+	int length = (int)measure.size();
+	for (int i=0; i<length; i++) {
+		if (measure[i] == '(') {
+			factor = 2;
+			factor /= 3;
+		}
 
-				// Other information:
-				case '{': slstart = 1;  state = STATE_SLSTART;  break;
-				case '}': slend   = 1;  state = STATE_SLEND;    break;
-				// case '(': tuplet  = 1;        break;
-				// case ')': tuplet  = 0;        break;
-				case '/':                     break;
-				case ']':                     break;
-//            case '>':                     break;   // unknown marker
-//            case '<':                     break;   //
-				case '^': tie = 1; state = STATE_NOTE; break;
-				default : cerr << "Error: unknown character " << song[i][j]
-							      << " on the line: " << song[i] << endl;
-							 return false;
-			}
-			j++;
-			switch (song[i][j]) {
-				case '-': case '+': nextstate = STATE_OCTAVE; break;
-				case 'O':
-				case '0': case '1': case '2': case '3': case '4':
-				case '5': case '6': case '7': nextstate = STATE_NOTE; break;
-				case 'b': case '#': nextstate = STATE_ACC;    break;
-				case '_': case '.': nextstate = STATE_DUR; break;
-				case '{': nextstate = STATE_SLSTART; break;
-				case '}': nextstate = STATE_SLEND; break;
-				case '^': nextstate = STATE_NOTE; break;
-				case ' ':
-					 if (song[i][j+1] == ' ') nextstate = STATE_BAR;
-					 else if (song[i][j+1] == '/') nextstate = -2;
-					 break;
-				case '\0':
-					phend = 1;
-					break;
-				default: nextstate = -1;
-			}
+		bool marker = false;
+		if (std::isdigit(measure[i])) {
+			marker = true;
+		} else if (measure[i] == '^') {  // tie placeholder for degree digit
+			marker = true;
+		} else if (measure[i] == '(') {  // tuplet start
+			marker = true;
+		} else if (measure[i] == '-') {  // octave lower
+			marker = true;
+		} else if (measure[i] == '+') {  // octave higher
+			marker = true;
+		}
 
-			if (nextstate < state ||
-					((nextstate == STATE_NOTE) && (state == nextstate))) {
-				 tempnote.clear();
-				 if (degree < 0) { // rest
-					 tempnote.pitch = -999;
-				 } else {
-					 tempnote.pitch = degree + 40*(octave + 4) + accidental + tonic;
-				 }
-				 if (tie) {
-					 tempnote.pitch = songdata[(int)songdata.size()-1].pitch;
-					 if (songdata[(int)songdata.size()-1].tieend) {
-						 songdata[(int)songdata.size()-1].tiecont = 1;
-						 songdata[(int)songdata.size()-1].tieend = 0;
-					 } else {
-						 songdata[(int)songdata.size()-1].tiestart = 1;
-					 }
-					 tempnote.tieend = 1;
-				 }
-				 tempnote.duration = duration;
-				 tempnote.phend = phend;
-				 tempnote.bar = bar;
-				 tempnote.phstart = phstart;
-				 tempnote.slstart = slstart;
-				 tempnote.slend = slend;
-				 if (nextstate == -2) {
-					 tempnote.bar = 2;
-					 tempnote.phend = 1;
-				 }
-				 tempnote.phnum = phnum;
+		if (marker && !tokens.empty() && !tokens.back().empty()) {
+			char checkChar = tokens.back().back();
+			if (checkChar == '(') {
+				marker = false;
+			} else if (checkChar == '-') {
+				marker = false;
+			} else if (checkChar == '+') {
+				marker = false;
+			}
+		}
 
-				 songdata.push_back(tempnote);
-				 duration = mindur;
-				 degree = 0;
-				 bar = 0;
-				 tie = 0;
-				 phend = 0;
-				 phstart = 0;
-				 slend = 0;
-				 slstart = 0;
-				 octave = 0;
-				 accidental = 0;
-				 if (nextstate == -2) {
-					 return true;
-				 }
+		if (marker) {
+			tokens.resize(tokens.size() + 1);
+			tokens.back() += measure[i];
+			factors.resize(factors.size() + 1);
+			factors.back() = factor;
+		} else {
+			if (!tokens.empty()) {
+				tokens.back() += measure[i];
+			} else {
+				cerr << "!!ERROR: unknown character at start of measure: " << measure << endl;
 			}
 		}
-		phnum++;
+
+		if (measure[i] == ')') {
+			factor = 1;
+		}
+	}
+
+	if (tokens.empty()) {
+		cerr << "!!ERROR: In measure: " << measure << ": no notes to parts." << endl;
+		return false;
+	}
+
+	for (int i=0; i<(int)tokens.size(); i++) {
+		resize(size() + 1);
+		back().parseNote(tokens[i], factors[i]);
+	}
+
+	// Calculate ticks for measure:
+	m_ticks = 0;
+	for (int i=0; i<(int)size(); i++) {
+		m_ticks += at(i).m_ticks;
 	}
 
 	return true;
@@ -79500,1670 +79833,1651 @@ bool Tool_esac2hum::getNoteList(vector<string>& song, vector<NoteData>& songdata
 
 //////////////////////////////
 //
-// Tool_esac2hum::printNoteData --
+// Tool_esac2hum::Note::parseNote --
 //
 
-void Tool_esac2hum::printNoteData(NoteData& data, int textQ, ostream& out) {
+bool Tool_esac2hum::Note::parseNote(const string& note, HumNum factor) {
+	esac = note;
 
-	if (data.num > 0) {
-		out << "*M" << data.num << "/" << data.denom;
-		if (textQ) {
-			out << "\t*M" << data.num << "/" << data.denom;
+	int minus = 0;
+	int plus = 0;
+	int b = 0;
+	int s = 0;
+	m_degree = 0;
+	m_dots = 0;
+
+	for (int i=0; i<(int)note.size(); i++) {
+		if (note[i] == '.') {        // augmentation dot
+			m_dots++;
+		} else if (note[i] == '_') { // duration modifier
+			m_underscores++;
+		} else if (note[i] == '-') { // lower octave
+			minus++;
+		} else if (note[i] == '+') { // upper octave
+			plus++;
+		} else if (note[i] == 'b') { // flat
+			b++;
+		} else if (note[i] == '#') { // sharp
+			s++;
+		} else if (isdigit(note[i])) {
+			m_degree = note[i] - '0';
+		} else if (note[i] == '^') { // tied to previous note
+			m_degree = -1000;
 		}
-		out << "\n";
-	}
-	if (data.phstart == 1) {
-		out << "{";
 	}
-	if (data.slstart == 1) {
-		out << "(";
+
+	m_ticks = 1 << m_underscores;
+	if (m_dots > 0) {
+		m_ticks = m_ticks * (2.0 - 1.0/(1 << m_dots));
 	}
-	if (data.tiestart == 1) {
-		out << "[";
+
+	if (b > 2) {
+		cerr << "!! ERROR: more than double flat not parseable, note: " << esac << endl;
 	}
-	out << Convert::durationFloatToRecip(data.duration);
-	if (data.pitch < 0) {
-		out << "r";
-	} else {
-		out << Convert::base40ToKern(data.pitch);
+	if (s > 2) {
+		cerr << "!! ERROR: more than double sharp not parseable, note: " << esac << endl;
 	}
-	if (data.tiecont == 1) {
-		out << "_";
+
+	m_alter = s - b;
+ 	m_octave = plus - minus;
+
+	m_factor = factor;
+
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::printHeader --
+//
+
+void Tool_esac2hum::printHeader(ostream& output) {
+	string filename = createFilename();
+	output << "!!!!SEGMENT: " << filename << endl;
+
+	string title = m_score.m_params["_title"];
+	output << "!!!OTL:";
+	if (!title.empty()) {
+		output << " " << title;
 	}
-	if (data.tieend == 1) {
-		out << "]";
+	output << endl;
+	// sometimes CUT[] has two lines, and the sescond is the text incipit:
+	string incipit = m_score.m_params["_incipit"];
+	if (!incipit.empty()) {
+		output << "!!!TIN: " << incipit << endl;
 	}
-	if (data.slend == 1) {
-		out << ")";
+
+	string source = m_score.m_params["_source"];
+	output << "!!!source:";
+	if (!source.empty()) {
+		output << " " << source;
 	}
-	if (data.phend == 1) {
-		out << "}";
+	output << endl;
+
+	string id = m_score.m_params["_id"];
+	output << "!!!id:";
+	if (!id.empty()) {
+		output << " " << id;
 	}
+	output << endl;
 
-	if (textQ) {
-		out << "\t";
-		if (data.phstart == 1) {
-			out << "{";
-		}
-		if (data.text == "") {
-			if (data.pitch < 0) {
-				data.text = "%";
-			} else {
-				data.text = "|";
-			}
-		}
-		if (data.pitch < 0 && (data.text.find('%') == string::npos)) {
-			out << "%";
-		}
-		if (data.text == " *") {
-			if (data.pitch < 0) {
-				data.text = "%*";
-			} else {
-				data.text = "|*";
-			}
-		}
-		if (data.text == "^") {
-			data.text = "|^";
-		}
-		printString(data.text, out);
-		if (data.phend == 1) {
-			out << "}";
-		}
+	string signature = m_score.m_params["SIG"];
+	output << "!!!signature:";
+	if (!signature.empty()) {
+		output << " " << signature;
 	}
+	output << endl;
 
-	out << "\n";
+	output << "**kern" << endl;
+}
 
-	// print barline information
-	if (data.bar == 1) {
 
-		out << "=";
-		if (data.barnum > 0) {
-			out << data.barnum;
-		}
-		if (data.barinterp) {
-			// out << "yy";
-		}
-		if (debugQ) {
-			if (data.bardur > 0.0) {
-				out << "[" << data.bardur << "]";
-			}
-		}
-		if (textQ) {
-			out << "\t";
-			out << "=";
-			if (data.barnum > 0) {
-				out << data.barnum;
-			}
-			if (data.barinterp) {
-				// out << "yy";
-			}
-			if (debugQ) {
-				if (data.bardur > 0.0) {
-					out << "[" << data.bardur << "]";
+
+//////////////////////////////
+//
+// Tool_esac2hum::createFilename -- from SIG[] and CUT[], with spaces in CUT[] turned into
+//     underscores and accents removed from characters.
+//
+//     Also need to deal with decomposed accents, if necessary:
+//         0x0301: Combining acute accent
+//         0x0300: Combining grave accent
+//         0x0302: Combining circumflex accent
+//         0x0303: Combining tilde
+//         0x0308: Combining diaeresis (umlaut)
+//         0x0327: Combining cedilla
+//         0x0328: Combining ogonek
+//         0x0304: Combining macron
+//         0x0306: Combining breve
+//         0x0307: Combining dot above
+//         0x0323: Combining dot below
+//         0x030A: Combining ring above
+//         0x030B: Combining double acute accent
+//         0x030C: Combining caron
+//
+//
+//    std::unordered_map<char, char> m_accent_map = {
+//         {'á', 'a'}, {'à', 'a'}, {'ä', 'a'}, {'â', 'a'}, {'ã', 'a'}, {'å', 'a'},
+//         {'é', 'e'}, {'è', 'e'}, {'ë', 'e'}, {'ê', 'e'},
+//         {'í', 'i'}, {'ì', 'i'}, {'ï', 'i'}, {'î', 'i'},
+//         {'ó', 'o'}, {'ò', 'o'}, {'ö', 'o'}, {'ô', 'o'}, {'õ', 'o'}, {'ø', 'o'},
+//         {'ú', 'u'}, {'ù', 'u'}, {'ü', 'u'}, {'û', 'u'},
+//         {'ý', 'y'}, {'ÿ', 'y'},
+//         {'ñ', 'n'}, {'ç', 'c'},
+//         {'ą', 'a'}, {'ć', 'c'}, {'ę', 'e'}, {'ł', 'l'}, {'ń', 'n'},
+//         {'ś', 's'}, {'ź', 'z'}, {'ż', 'z'}
+//    };
+
+string Tool_esac2hum::createFilename(void) {
+	string source = m_score.m_params["_source"];
+	string prefix;
+	string sig = m_score.m_params["SIG"];
+	string title = m_score.m_params["_title"];
+	string id  = m_score.m_params["_id"];
+	if (sig.empty()) {
+		sig = id;
+	}
+
+	HumRegex hre;
+	// Should not be spaces, but just in case;
+	hre.replaceDestructive(sig, "", "\\s+", "g");
+	hre.replaceDestructive(source, "", "\\s+", "g");
+
+	if (!m_filePrefix.empty()) {
+		prefix = m_filePrefix;
+		source = "";
+	}
+
+	// Convert spaces to underscores:
+	hre.replaceDestructive(title, "_", "\\s+", "g");
+	// Remove accents:
+	hre.replaceDestructive(title, "a", "á", "g");
+	hre.replaceDestructive(title, "a", "à", "g");
+	hre.replaceDestructive(title, "a", "ä", "g");
+	hre.replaceDestructive(title, "a", "â", "g");
+	hre.replaceDestructive(title, "a", "ã", "g");
+	hre.replaceDestructive(title, "a", "å", "g");
+	hre.replaceDestructive(title, "e", "é", "g");
+	hre.replaceDestructive(title, "e", "è", "g");
+	hre.replaceDestructive(title, "e", "ë", "g");
+	hre.replaceDestructive(title, "e", "ê", "g");
+	hre.replaceDestructive(title, "i", "í", "g");
+	hre.replaceDestructive(title, "i", "ì", "g");
+	hre.replaceDestructive(title, "i", "ï", "g");
+	hre.replaceDestructive(title, "i", "î", "g");
+	hre.replaceDestructive(title, "o", "ó", "g");
+	hre.replaceDestructive(title, "o", "ò", "g");
+	hre.replaceDestructive(title, "o", "ö", "g");
+	hre.replaceDestructive(title, "o", "ô", "g");
+	hre.replaceDestructive(title, "o", "õ", "g");
+	hre.replaceDestructive(title, "o", "ø", "g");
+	hre.replaceDestructive(title, "u", "ú", "g");
+	hre.replaceDestructive(title, "u", "ù", "g");
+	hre.replaceDestructive(title, "u", "ü", "g");
+	hre.replaceDestructive(title, "u", "û", "g");
+	hre.replaceDestructive(title, "y", "ý", "g");
+	hre.replaceDestructive(title, "y", "ÿ", "g");
+	hre.replaceDestructive(title, "n", "ñ", "g");
+	hre.replaceDestructive(title, "c", "ç", "g");
+	hre.replaceDestructive(title, "a", "ą", "g");
+	hre.replaceDestructive(title, "c", "ć", "g");
+	hre.replaceDestructive(title, "e", "ę", "g");
+	hre.replaceDestructive(title, "l", "ł", "g");
+	hre.replaceDestructive(title, "n", "ń", "g");
+	hre.replaceDestructive(title, "s", "ś", "g");
+	hre.replaceDestructive(title, "z", "ź", "g");
+	hre.replaceDestructive(title, "z", "ż", "g");
+	hre.replaceDestructive(title, "", "[^a-zA-Z0-9-_.]", "g");
+
+	std::transform(title.begin(), title.end(), title.begin(),
+			[](unsigned char c) { return std::tolower(c); });
+
+	string output;
+	if (!prefix.empty()) {
+		output += prefix + "-";
+	} else if (!source.empty()) {
+		if (hre.search(source, "^DWOK(\\d+)$")) {
+			string volume = hre.getMatch(1);
+			if (volume.size() == 1) {
+				volume = "0" + volume;
+			}
+			if (!sig.empty()) {
+				if (hre.search(sig, "^(\\d\\d)")) {
+					string volume2 = hre.getMatch(1);
+					if (volume == volume2) {
+						source = "DWOK";
+						output += source;
+					}
+				} else {
+					output += source + "-";
 				}
+			} else {
+				output += source + "-";
 			}
+		} else {
+			output += source + "-";
 		}
-
-		out << "\n";
-	} else if (data.bar == 2) {
-		out << "==";
-		if (textQ) {
-			out << "\t==";
-		}
-		out << "\n";
 	}
+	output += sig;
+	if (!(sig.empty() || title.empty())) {
+		output += "-";
+	}
+	output += title;
+	if (output.empty()) {
+		output = "file";
+	}
+	output += m_filePostfix;
+
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::getKeyInfo -- look for a KEY[] entry and extract the data.
+// Tool_esac2hum::getParameters --
 //
-// ggg fix this function
-//
-
-bool Tool_esac2hum::getKeyInfo(vector<string>& song, string& key, double& mindur,
-		int& tonic, string& meter, ostream& out) {
-	int i;
-	for (i=0; i<(int)song.size(); i++) {
-		if (song[i].compare(0, 4, "KEY[") == 0) {
-			key = song[i][4]; // letter
-			key += song[i][5]; // number
-			key += song[i][6]; // number
-			key += song[i][7]; // number
-			key += song[i][8]; // number
-			if (!isspace(song[i][9])) {
-				key += song[i][9];  // optional letter (sometimes ' or ")
-			}
-			if (!isspace(song[i][10])) {
-				key += song[i][10];  // illegal but possible extra letter
-			}
-			if (song[i][10] != ' ') {
-				out << "!! Warning key field is not complete" << endl;
-				out << "!!Key field: " << song[i] << endl;
-			}
-
-			mindur = (song[i][11] - '0') * 10 + (song[i][12] - '0');
-			mindur = 4.0 / mindur;
 
-			string tonicstr;
-			if (song[i][14] != ' ') {
-				tonicstr[0] = song[i][14];
-				if (tolower(song[i][15]) == 'b') {
-					tonicstr[1] = '-';
+void Tool_esac2hum::getParameters(vector<string>& infile) {
+	m_score.m_params.clear();
+	HumRegex hre;
+	bool expectingCloseQ = false;
+	string lastKey = "";
+	for (int i=0; i<(int)infile.size(); i++) {
+		if (hre.search(infile[i], "^\\s*$")) {
+			continue;
+		}
+		if ((i == 0) && hre.search(infile[i], "^([A-Z_a-z][^\\]\\[]*)\\s*$")) {
+			m_score.m_params["_source"] = hre.getMatch(1);
+			continue;
+		}
+		if (expectingCloseQ) {
+			if (infile[i].find("[") != string::npos) {
+				cerr << "Strange case searching for close: " << infile[i] << endl;
+			} else if (infile[i].find("]") == string::npos) {
+				// continuing a parameter:
+				if (lastKey == "") {
+					cerr << "Strange case of no last key when closing parameter: " << infile[i] << endl;
 				} else {
-					tonicstr[1] = song[i][15];
+					m_score.m_params[lastKey] += "\n" + infile[i];
+				}
+			} else if (hre.search(infile[i], "^([^\\]]+)\\]\\s*$")) {
+				// closing a parameter:
+				if (lastKey == "") {
+					cerr << "Strange case B of no last key when closing parameter: " << infile[i] << endl;
+				} else {
+					string value = hre.getMatch(1);
+					m_score.m_params[lastKey] += "\n" + value;
+					expectingCloseQ = false;
+					continue;
 				}
-				tonicstr[2] = '\0';
 			} else {
-				tonicstr = song[i][15];
+				cerr << "Problem closing parameter: " << infile[i] << endl;
 			}
+			continue;
+		} else if (hre.search(infile[i], "^\\s*([A-Z_a-z]+)\\s*\\[([^\\]]*)\\]\\s*$")) {
+			// single line parameter
+			string key   = hre.getMatch(1);
+			string value = hre.getMatch(2);
 
-			// convert German notation to English for note names
-			// Hopefully all references to B will mean English B-flat.
-			if (tonicstr == "B") {
-				tonicstr = "B-";
-			}
-			if (tonicstr == "H") {
-				tonicstr = "B";
-			}
+			// Rare cases where the key has lower case letters that should not be there:
+			std::transform(key.begin(), key.end(), key.begin(),
+					[](unsigned char c) { return std::toupper(c); });
 
-			tonic = Convert::kernToBase40(tonicstr);
-			if (tonic <= 0) {
-				cerr << "Error: invalid tonic on line: " << song[i] << endl;
-				return false;
-			}
-			tonic = tonic % 40;
-			meter = song[i].substr(17);
-			if (meter.back() != ']') {
-				cerr << "Error with meter on line: " << song[i] << endl;
-				cerr << "Meter area: " << meter << endl;
-				cerr << "Expected ] as last character but found " << meter.back() << endl;
-				return false;
-			} else {
-				meter.resize((int)meter.size() - 1);
-			}
-			return true;
-		}
-	}
-	cerr << "Error: did not find a KEY field" << endl;
-	return false;
-}
+			m_score.m_params[key] = value;
+			continue;
+		} else if (hre.search(infile[i], "^\\s*([A-Z_a-z]+)\\s*\\[([^\\]]*)\\s*$")) {
+			// opening of a parameter
+			string key   = hre.getMatch(1);
+			string value = hre.getMatch(2);
 
+			// Rare cases where the key has lower case letters that should not be there:
+			std::transform(key.begin(), key.end(), key.begin(),
+					[](unsigned char c) { return std::toupper(c); });
 
+			m_score.m_params[key] = value;
+			lastKey = key;
+			expectingCloseQ = true;
+			continue;
+		} else if (hre.search(infile[i], "^#")) {
+			// Do nothing: an external comment, or embedded filter processed
+			// when filter loading the file.
+		} else {
+			cerr << "UNKNOWN CASE: " << infile[i] << endl;
+		}
+	}
 
-///////////////////////////////
-//
-// Tool_esac2hum::getFileContents -- read a file into the array.
-//
+	// The CUT[] line can be multiple lines, the first being the title and
+	// the second being the text incipit.  Split them into _title and _incipit
+	// fields (not checking if more than two lines):
+	string cut = m_score.m_params["CUT"];
+	if (hre.search(cut, "^\\s*(.*?)\\n(.*?)\\s*$", "s")) {
+		m_score.m_params["_title"]   = trimSpaces(hre.getMatch(1));
+		m_score.m_params["_incipit"] = trimSpaces(hre.getMatch(2));
+	} else {
+		// Don't know if CUT[] is title or incipit, but assign to title.
+		m_score.m_params["_title"] = trimSpaces(cut);
+		m_score.m_params["_incipit"] = "";
+	}
 
-bool Tool_esac2hum::getFileContents(vector<string>& array, const string& filename) {
-	ifstream infile(filename.c_str());
-	array.reserve(100);
-	array.resize(0);
+	string key = m_score.m_params["KEY"];
+	if (hre.search(key, "^\\s*([^\\s]+)\\s+(\\d+)\\s+([A-Gacdefg][bs]*)\\s+(.*?)\\s*$")) {
+		string id     = hre.getMatch(1);
+		string minrhy = hre.getMatch(2);
+		string tonic  = hre.getMatch(3);
+		if (tonic.size() >= 1) {
+			if (tonic[0] == 'b') {
+				cerr << "Error: key signature cannot be 'b'." << endl;
+			} else {
+				if (std::islower(tonic[0])) {
+					cerr << "Warning: Tonic note should be upper case." << endl;
+					tonic[0] = std::toupper(tonic[0]);
+				}
+			}
+		}
+		string time   = hre.getMatch(4);
+		m_score.m_params["_id"]     = id;
+		m_score.m_params["_minrhy"] = minrhy;
+		m_score.m_params["_tonic"]  = tonic;
+		m_score.m_params["_time"]   = time;
+		m_minrhy = stoi(minrhy);
+	} else {
+		cerr << "Problem parsing KEY parameter: " << key << endl;
+	}
 
-	if (!infile.is_open()) {
-		cerr << "Error: cannot open file: " << filename << endl;
-		return false;
+	string trd;
+	if (hre.search(trd, "^\\s*(.*)\\ss\\.")) {
+		m_score.m_params["_source_trd"] = hre.getMatch(1);
+	}
+	if (hre.search(trd, "s\\.\\s*(\\d+-?\\d*)")) {
+		// Could be text aftewards about the origin of the song.
+		m_score.m_params["_page"] = hre.getMatch(1);
 	}
 
-	char holdbuffer[1024] = {0};
+	if (m_debugQ) {
+		printParameters();
+	}
 
-	infile.getline(holdbuffer, 256, '\n');
-	while (!infile.eof()) {
-		array.push_back(holdbuffer);
-		infile.getline(holdbuffer, 256, '\n');
+	if (hre.search(m_score.m_params["_source_trd"], "^\\s*(DWOK\\d+)")) {
+		m_dwokQ = true;
+	} else if (hre.search(m_score.m_params["_source"], "^\\s*(DWOK\\d+)")) {
+		m_dwokQ = true;
 	}
 
-	infile.close();
-	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::example --
+// Tool_esac2hum::printParameters --
 //
 
-void Tool_esac2hum::example(void) {
-
-
+void Tool_esac2hum::printParameters(void) {
+	cerr << endl;
+	cerr << "========================================" << endl;
+    for (const auto& [key, value] : m_score.m_params) {
+        cerr << "Key: " << key << ", Value: " << value << endl;
+    }
+	cerr << "========================================" << endl;
+	cerr << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::usage --
+// Tool_esac2hum::printBemComment --
 //
 
-void Tool_esac2hum::usage(const string& command) {
-
+void Tool_esac2hum::printBemComment(ostream& output) {
+	string bem = m_score.m_params["BEM"];
+	if (bem.empty()) {
+		return;
+	}
+	string english = m_bem_translation[bem];
+	if (english.empty()) {
+		output << "!!!ONB: " << bem << endl;
+	} else {
+		output << "!!!ONB@@PL: " << bem << endl;
+		output << "!!!ONB@@EN: " << english << endl;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_esac2hum::printBibInfo --
+// Tool_esac2hum::printFooter --
 //
 
-void Tool_esac2hum::printBibInfo(vector<string>& song, ostream& out) {
-	int i, j;
-	char buffer[32] = {0};
-	int start = -1;
-	int stop  = -1;
-	int count = 0;
-	string templine;
-
-	for (i=0; i<(int)song.size(); i++) {
-		if (song[i] == "") {
-			continue;
-		}
-		if (song[i][0] != ' ') {
-			if (song[i].size() < 4 || song[i][3] != '[') {
-				if (song[i].compare(0, 2, "!!") != 0) {
-					out << "!! " << song[i] << "\n";
-				}
-				continue;
-			}
-			strncpy(buffer, song[i].c_str(), 3);
-			buffer[3] = '\0';
-			if (strcmp(buffer, "MEL") == 0) continue;
-			if (strcmp(buffer, "TXT") == 0) continue;
-			// if (strcmp(buffer, "KEY") == 0) continue;
-			getLineRange(song, buffer, start, stop);
+void Tool_esac2hum::printFooter(ostream& output, vector<string>& infile) {
+	output << "*-" << endl;
 
-			// don't print CUT field if only one line.  !!!OTL: will contain CUT[]
-			// if (strcmp(buffer, "CUT") == 0 && start == stop) continue;
+	printBemComment(output);
+	printPdfLinks(output);
+	printPageNumbers(output);
+	printConversionDate(output);
 
-			buffer[0] = tolower(buffer[0]);
-			buffer[1] = tolower(buffer[1]);
-			buffer[2] = tolower(buffer[2]);
 
-			count = 1;
-			templine = "";
-			for (j=start; j<=stop; j++) {
-				if (song[j].size() < 4) {
-					continue;
-				}
-				if (stop - start == 0) {
-					templine = song[j].substr(4);
-					auto loc = templine.find(']');
-					if (loc != string::npos) {
-						templine.resize(loc);
-					}
-					if (templine != "") {
-						out << "!!!" << buffer << ": ";
-						printString(templine, out);
-						out << "\n";
-					}
+	if (m_embedEsacQ) {
+		output << "!!@@BEGIN: ESAC" << endl;
+		output << "!!@CONTENTS:" << endl;;
+		for (int i=0; i<(int)infile.size(); i++) {
+			output << "!!" << infile[i] << endl;
+		}
+		if (m_analysisQ) {
+			embedAnalyses(output);
+		}
+		output << "!!@@END: ESAC" << endl;
+	}
 
-				} else if (j==start) {
-					out << "!!!" << buffer << count++ << ": ";
-					printString(song[j].substr(4), out);
-					out << "\n";
-				} else if (j==stop) {
-					templine = song[j].substr(4);
-					auto loc = templine.find(']');
-					if (loc != string::npos) {
-						templine.resize(loc);
-					}
-					if (templine != "") {
-						out << "!!!" << buffer << count++ << ": ";
-						printString(templine, out);
-						out << "\n";
-					}
-				} else {
-					out << "!!!" << buffer << count++ << ": ";
-					printString(&(song[j][4]), out);
-					out << "\n";
-				}
-			}
+	if (!m_globalComments.empty()) {
+		for (int i=0; i<(int)m_globalComments.size(); i++) {
+			output << m_globalComments.at(i) << endl;
 		}
 	}
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_esac2hum::printString -- print characters in string.
+// Tool_esac2hum::printPageNumbers --
 //
 
-void Tool_esac2hum::printString(const string& string, ostream& out) {
-	for (int i=0; i<(int)string.size(); i++) {
-		printChar(string[i], out);
+void Tool_esac2hum::printPageNumbers(ostream& output) {
+	HumRegex hre;
+	string trd = m_score.m_params["TRD"];
+	if (hre.search(trd, "\\bs\\.\\s*(\\d+)\\s*-\\s*(\\d+)", "i")) {
+		output << "!!!page: " << hre.getMatch(1) << "-" << hre.getMatch(2) << endl;
+	} else if (hre.search(trd, "\\bs\\.\\s*(\\d+)", "i")) {
+		output << "!!!page: " << hre.getMatch(1) << endl;
 	}
 }
 
 
 
+///////////////////////////////
+//
+// Tool_esac::embedAnalyses --
+//
+
+void Tool_esac2hum::embedAnalyses(ostream& output) {
+	m_score.doAnalyses();
+	string MEL_SEM  = m_score.m_params["MEL_SEM"];
+	string MEL_RAW  = m_score.m_params["MEL_RAW"];
+	string NO_REP   = m_score.m_params["NO_REP"];
+	string RTM      = m_score.m_params["RTM"];
+	string SCL_DEG  = m_score.m_params["SCL_DEG"];
+	string SCL_SEM  = m_score.m_params["SCL_SEM"];
+	string PHR_NO   = m_score.m_params["PHR_NO"];
+	string PHR_BARS = m_score.m_params["PHR_BARS"];
+	string PHR_CAD  = m_score.m_params["PHR_CAD"];
+	string ACC      = m_score.m_params["ACC"];
+
+	bool allEmptyQ = true;
+	if      (!MEL_SEM.empty() ) { allEmptyQ = false; }
+	else if (!MEL_RAW.empty() ) { allEmptyQ = false; }
+	else if (!NO_REP.empty()  ) { allEmptyQ = false; }
+	else if (!RTM.empty()     ) { allEmptyQ = false; }
+	else if (!SCL_DEG.empty() ) { allEmptyQ = false; }
+	else if (!SCL_SEM.empty() ) { allEmptyQ = false; }
+	else if (!PHR_NO.empty()  ) { allEmptyQ = false; }
+	else if (!PHR_BARS.empty()) { allEmptyQ = false; }
+	else if (!PHR_CAD.empty() ) { allEmptyQ = false; }
+	else if (!ACC.empty()     ) { allEmptyQ = false; }
+
+	if (allEmptyQ) {
+		// no analyses for some strange reason.
+		return;
+	}
+	output << "!!@ANALYSES:" << endl;
+	if (!MEL_SEM.empty() ) { output << "!!MEL_SEM["  << MEL_SEM  << "]" << endl; }
+	if (!MEL_RAW.empty() ) { output << "!!MEL_RAW["  << MEL_RAW  << "]" << endl; }
+	if (!NO_REP.empty()  ) { output << "!!NO_REP["   << NO_REP   << "]" << endl; }
+	if (!RTM.empty()     ) { output << "!!RTM["      << RTM      << "]" << endl; }
+	if (!SCL_DEG.empty() ) { output << "!!SCL_DEG["  << SCL_DEG  << "]" << endl; }
+	if (!SCL_SEM.empty() ) { output << "!!SCL_SEM["  << SCL_SEM  << "]" << endl; }
+	if (!PHR_NO.empty()  ) { output << "!!PHR_NO["   << PHR_NO   << "]" << endl; }
+	if (!PHR_BARS.empty()) { output << "!!PHR_BARS[" << PHR_BARS << "]" << endl; }
+	if (!PHR_CAD.empty() ) { output << "!!PHR_CAD["  << PHR_CAD  << "]" << endl; }
+	if (!ACC.empty()     ) { output << "!!ACC["      << ACC      << "]" << endl; }
+
+}
 
 
-/////////////////////////////////
+///////////////////////////////
 //
-// Tool_extract::Tool_extract -- Set the recognized options for the tool.
+// Tool_esac2hum::printPdfLinks --
 //
 
-Tool_extract::Tool_extract(void) {
-	define("P|F|S|x|exclude=s:",        "remove listed spines from output");
-	define("i=s:",                      "exclusive interpretation list to extract from input");
-	define("I=s:",                      "exclusive interpretation exclusion list");
-	define("f|p|s|field|path|spine=s:", "for extraction of particular spines");
-	define("C|count=b",                 "print a count of the number of spines in file");
-	define("c|cointerp=s:**kern",       "exclusive interpretation for cospines");
-	define("g|grep=s:",                 "extract spines which match a given regex.");
-	define("r|reverse=b",               "reverse order of spines by **kern group");
-	define("R=s:**kern",                "reverse order of spine by exinterp group");
-	define("t|trace=s:",                "use a trace file to extract data");
-	define("e|expand=b",                "expand spines with subspines");
-	define("k|kern=s",                  "extract by kern spine group");
-	define("K|reverse-kern=s",          "extract by kern spine group top to bottom numbering");
-	define("E|expand-interp=s:",        "expand subspines limited to exinterp");
-	define("m|model|method=s:d",        "method for extracting secondary spines");
-	define("M|cospine-model=s:d",       "method for extracting cospines");
-	define("Y|no-editoral-rests=b",     "do not display yy marks on interpreted rests");
-	define("n|name|b|blank=s:**blank",  "name if exinterp added with 0");
-	define("no-empty|no-empties=b",     "suppress spines with only null data tokens");
-	define("empty|empties=b",           "only keep spines with only null data tokens");
-	define("spine-list=b",              "show spine list and then exit");
-	define("no-rest|no-rests=b",        "remove **kern spines containing only rests (and their co-spines)");
+void Tool_esac2hum::printPdfLinks(ostream& output) {
+	output << "!!!URL: http://webesac.pcss.pl WebEsAC" << endl;
+
+	if (!m_dwokQ) {
+		return;
+	}
+
+	output << "!!!URL: https::kolberg.ispan.pl/dwok/tomy Oskar Kolberg: Complete Works digital edition" << endl;
+
+	string source = m_score.m_params["_source"];
+	HumRegex hre;
+	if (!hre.search(source, "^DWOK(\\d+)")) {
+		return;
+	}
+	string volume = hre.getMatch(1);
+	if (volume.size() == 1) {
+		volume = "0" + volume;
+	}
+	if (volume.size() == 2) {
+		volume = "0" + volume;
+	}
+	if (volume.size() > 3) {
+		return;
+	}
+	string nozero = volume;
+	hre.replaceDestructive(nozero, "" , "^0+");
+	// need http:// not https:// for the following PDF link:
+	output << "!!!URL-pdf: http://oskarkolberg.pl/MediaFiles/" << volume << "dwok.pdf" << " Oskar Kolberg: Complete Works, volume " << nozero << endl;
 
-	define("debug=b",                   "print debugging information");
-	define("author=b",                  "author of the program");
-	define("version=b",                 "compilation info");
-	define("example=b",                 "example usages");
-	define("h|help=b",                  "short description");
 }
 
 
 
-/////////////////////////////////
+///////////////////////////////
 //
-// Tool_extract::run -- Primary interfaces to the tool.
+// Tool_esac2hum::printCoversionDate --
 //
 
-bool Tool_extract::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
+void Tool_esac2hum::printConversionDate(ostream& output) {
+	std::time_t t = std::time(nullptr);
+	std::tm* now = std::localtime(&t);
+	output << "!!!ONB: Converted on ";
+	output << std::put_time(now, "%Y/%m/%d");
+	output << " with esac2hum" << endl;
 }
 
 
-bool Tool_extract::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::doAnalyses --
+//
+
+void Tool_esac2hum::Score::doAnalyses(void) {
+	analyzeMEL_SEM();
+	analyzeMEL_RAW();
+	analyzeNO_REP();
+	analyzeRTM();
+	analyzeSCL_DEG();
+	analyzeSCL_SEM();
+	analyzePHR_NO();
+	analyzePHR_BARS();
+	analyzePHR_CAD();
+	analyzeACC();
 }
 
 
-bool Tool_extract::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::analyzeMEL_SEM -- Current
+//   algorithm: calculate intervals across rest, ignore tied notes.
+//   values are differences between m_b12 of notes.
+//
+
+void Tool_esac2hum::Score::analyzeMEL_SEM(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+
+	vector<int> b12s;  // list of notes to calculate intervals between
+
+	for (int i=0; i<(int)notelist.size(); i++) {
+		if (notelist[i]->isRest()) {
+			continue;
+		}
+		if (notelist[i]->m_tieEnd) {
+			continue;
+		}
+		b12s.push_back(notelist[i]->m_b12);
 	}
-	return status;
+
+	string output;
+	for (int i=1; i<(int)b12s.size(); i++) {
+		int difference = b12s[i] - b12s[i-1];
+		output += to_string(difference);
+		if (i < (int)b12s.size() - 1) {
+			output += " ";
+		}
+	}
+
+	m_params["MEL_SEM"] = output;
 }
 
+
+
+//////////////////////////////
 //
-// In-place processing of file:
+// Tool_esac2hum::Score::analyzeMEL_RAW -- Remove rhythms from MEL[] data.
+//    Preserve spaces as in original MEL[];
+//    What to do with parentheses? Currently removed.
+//    What to do with tied notes?  Currently removed.
 //
 
-bool Tool_extract::run(HumdrumFile& infile) {
-	initialize(infile);
-	processFile(infile);
-	// Re-load the text for each line from their tokens.
-	// infile.createLinesFromTokens();
-	return true;
+void Tool_esac2hum::Score::analyzeMEL_RAW(void) {
+	string output = m_params["MEL"];
+	HumRegex hre;
+	hre.replaceDestructive(output, "", "[^\\d+\\sb#-]+", "g");
+	hre.replaceDestructive(output, "", "\\s*//\\s*$");
+	hre.replaceDestructive(output, "\n!!", "\n", "g");
+	m_params["MEL_RAW"] = output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::processFile --
+// Tool_esac2hum::Score::analyzeNO_REP -- Return
+//     the non-repeated notes/rests without rhythms
+//     in each phrase with a newlines between phrases
+//     and no spaces between notes or measures.
 //
 
-void Tool_extract::processFile(HumdrumFile& infile) {
-	if (countQ) {
-		m_free_text << infile.getMaxTrack() << endl;
-		return;
-	}
-	if (expandQ) {
-		expandSpines(field, subfield, model, infile, expandInterp);
-	} else if (interpQ) {
-		getInterpretationFields(field, subfield, model, infile, interps,
-				interpstate);
-	} else if (reverseQ) {
-		reverseSpines(field, subfield, model, infile, reverseInterp);
-	} else if (removerestQ) {
-		fillFieldDataByNoRest(field, subfield, model, grepString, infile,
-			interpstate);
-	} else if (grepQ) {
-		fillFieldDataByGrep(field, subfield, model, grepString, infile,
-			interpstate);
-	} else if (emptyQ) {
-		fillFieldDataByEmpty(field, subfield, model, infile, interpstate);
-	} else if (noEmptyQ) {
-		fillFieldDataByNoEmpty(field, subfield, model, infile, interpstate);
-	} else if (fieldQ || excludeQ) {
-		fillFieldData(field, subfield, model, fieldstring, infile);
+void Tool_esac2hum::Score::analyzeNO_REP(void) {
+	string output;
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		string line = phrase.getNO_REP();
+		if (i > 0) {
+			output += "\n    ";
+		}
+		output += line;
 	}
 
-	if (spineListQ) {
-		m_free_text << "-s ";
-		for (int i=0; i<(int)field.size(); i++) {
-			m_free_text << field[i];
-			if (i < (int)field.size() - 1) {
-				m_free_text << ",";
-			}
+	HumRegex hre;
+	hre.replaceDestructive(output, "\n!!", "\n", "g");
+
+	m_params["NO_REP"] = output;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Phrase::getNO_REP -- Return
+//     the non-repeated notes/rests without rhythms
+//     with no spaces between notes or measures.
+//     What to do if line starts with an ending tied note?
+//     Currently ignoring leading tied end notes.
+//
+
+string Tool_esac2hum::Phrase::getNO_REP(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+	string output;
+	int foundNonTie = false;
+	string lastitem = "";
+	for (int i=0; i<(int)notelist.size(); i++) {
+		if (!foundNonTie && notelist[i]->m_tieEnd) {
+			continue;
+		}
+		foundNonTie = true;
+		string curitem = notelist[i]->getScaleDegree();
+		if (curitem != lastitem) {
+			output += curitem;
+			lastitem = curitem;
 		}
-		m_free_text << endl;
-		return;
 	}
+	return output;
+}
 
-	if (debugQ && !traceQ) {
-		m_free_text << "!! Field Expansion List:";
-		for (int j=0; j<(int)field.size(); j++) {
-			m_free_text << " " << field[j];
-			if (subfield[j]) {
-				m_free_text << (char)subfield[j];
-			}
-			if (model[j]) {
-				m_free_text << (char)model[j];
-			}
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::analyzeRTM -- Convert pitches/rests to "x".
+//      What to do with tied notes?  Leaving ^ in for now.
+//      What to do with ()?  Removing for now.
+//
+
+void Tool_esac2hum::Score::analyzeRTM(void) {
+	string output = m_params["MEL"];
+	HumRegex hre;
+	hre.replaceDestructive(output, "", "[()]+", "g");
+	hre.replaceDestructive(output, "x", "[+-]*(\\d|\\^)[b#]*", "g");
+	hre.replaceDestructive(output, "", "\\s*//\\s*$");
+	hre.replaceDestructive(output, "\n!!", "\n", "g");
+	m_params["RTM"] = output;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::analyzeSCL_DEG -- List of scale degrees
+//     present in melody from lowest to highest with no spaces between
+//     the scale degrees.
+//
+
+void Tool_esac2hum::Score::analyzeSCL_DEG(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+	map<int, Tool_esac2hum::Note*> list;
+	for (int i=0; i<(int)notelist.size(); i++) {
+		if (notelist[i]->isRest()) {
+			continue;
 		}
-		m_free_text << endl;
+		if (notelist[i]->m_tieEnd) {
+			continue;
+		}
+		int b40 = notelist[i]->m_b40;
+		list[b40] = notelist[i];
 	}
 
-	// preserve SEGMENT filename if present (now printed in main())
-	// infile.printNonemptySegmentLabel(m_humdrum_text);
-
-	// analyze the input file according to command-line options
-	if (fieldQ || grepQ || removerestQ) {
-		extractFields(infile, field, subfield, model);
-	} else if (excludeQ) {
-		excludeFields(infile, field, subfield, model);
-	} else if (traceQ) {
-		extractTrace(infile, tracefile);
-	} else {
-		m_humdrum_text << infile;
+	string output;
+	for (const auto& pair : list) {
+		output += pair.second->getScaleDegree();
 	}
+	m_params["SCL_DEG"] = output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::getNullDataTracks --
+// Tool_esac2hum::Score::analyzeSCL_SEM -- Get the semitone
+//    between scale degrees in SCL_DEG analysis.
 //
 
-vector<int> Tool_extract::getNullDataTracks(HumdrumFile& infile) {
-	vector<int> output(infile.getMaxTrack() + 1, 1);
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
+void Tool_esac2hum::Score::analyzeSCL_SEM(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
+	map<int, Tool_esac2hum::Note*> list;
+	for (int i=0; i<(int)notelist.size(); i++) {
+		if (notelist[i]->isRest()) {
 			continue;
 		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			int track = token->getTrack();
-			if (!output[track]) {
-				continue;
-			}
-			if (!token->isNull()) {
-				output[track] = 0;
-			}
+		if (notelist[i]->m_tieEnd) {
+			continue;
 		}
-		// maybe exit here if all tracks are non-null
+		int b40 = notelist[i]->m_b40;
+		list[b40] = notelist[i];
 	}
 
-	return output;
+	string output;
+	Tool_esac2hum::Note* lastnote = nullptr;
+	for (const auto& pair : list) {
+		if (lastnote == nullptr) {
+			lastnote = pair.second;
+			continue;
+		}
+		int second = pair.second->m_b12;
+		int first = lastnote->m_b12;
+		int difference = second -first;
+		if (!output.empty()) {
+			output += " ";
+		}
+		output += to_string(difference);
+		lastnote = pair.second;
+	}
+	m_params["SCL_SEM"] = output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::fillFieldDataByEmpty -- Only keep the spines which contain only
-//    null data tokens.
+// Tool_esac2hum::Score::analyzePHR_NO --
 //
 
-void Tool_extract::fillFieldDataByEmpty(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, HumdrumFile& infile, int negate) {
+void Tool_esac2hum::Score::analyzePHR_NO(void) {
+	int phraseCount = (int)size();
+	m_params["PHR_NO"] = to_string(phraseCount);
+}
 
-	field.reserve(infile.getMaxTrack()+1);
-	subfield.reserve(infile.getMaxTrack()+1);
-	model.reserve(infile.getMaxTrack()+1);
-	field.resize(0);
-	subfield.resize(0);
-	model.resize(0);
-	vector<int> nullTrack = getNullDataTracks(infile);
 
-	int zero = 0;
-	for (int i=1; i<(int)nullTrack.size(); i++) {
-		if (negate) {
-			if (!nullTrack[i]) {
-				field.push_back(i);
-				subfield.push_back(zero);
-				model.push_back(zero);
-			}
-		} else {
-			if (nullTrack[i]) {
-				field.push_back(i);
-				subfield.push_back(zero);
-				model.push_back(zero);
-			}
+
+//////////////////////////////
+//
+// Tool_esac2hum::Score::analyzePHR_BARS -- Return the number
+//    of measures in each phrase.
+//
+
+void Tool_esac2hum::Score::analyzePHR_BARS(void) {
+	string output;
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		int barCount = phrase.getFullMeasureCount();
+		output += to_string(barCount);
+		if (i < (int)size() - 1) {
+			output += " ";
 		}
 	}
-
+	m_params["PHR_BARS"] = output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::fillFieldDataByNoEmpty -- Only keep spines which are not all
-//   null data tokens.
+// Tool_esac2hum:::Phrase::getFullMeasureCount -- Return the number
+//      of measures, but subtrack one if the first measure is a
+//      partialEnd and the last is a partialBegin.
 //
 
-void Tool_extract::fillFieldDataByNoEmpty(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, HumdrumFile& infile, int negate) {
+int Tool_esac2hum::Phrase::getFullMeasureCount(void) {
+	int measureCount = (int)size();
+	if (measureCount < 2) {
+		return measureCount;
+	}
+	if (at(0).isPartialEnd() && back().isPartialBegin()) {
+		measureCount--;
+	}
 
-	field.reserve(infile.getMaxTrack()+1);
-	subfield.reserve(infile.getMaxTrack()+1);
-	model.reserve(infile.getMaxTrack()+1);
-	field.resize(0);
-	subfield.resize(0);
-	model.resize(0);
-	vector<int> nullTrack = getNullDataTracks(infile);
-	for (int i=1; i<(int)nullTrack.size(); i++) {
-		nullTrack[i] = !nullTrack[i];
+	// if the fist is partial and the last is not, also -1
+	if (at(0).isPartialEnd() && back().isComplete()) {
+		measureCount--;
 	}
 
-	int zero = 0;
-	for (int i=1; i<(int)nullTrack.size(); i++) {
-		if (negate) {
-			if (!nullTrack[i]) {
-				field.push_back(i);
-				subfield.push_back(zero);
-				model.push_back(zero);
-			}
-		} else {
-			if (nullTrack[i]) {
-				field.push_back(i);
-				subfield.push_back(zero);
-				model.push_back(zero);
-			}
-		}
+	// if the fist is complete and the last is incomplete, also -1
+	if (at(0).isComplete() && back().isPartialBegin()) {
+		measureCount--;
 	}
+
+
+	// what to do if first measure is pickup (and maybe last measure)?
+	return measureCount;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::fillFieldDataByNoRest --  Find the spines which
-//    contain only rests and remove them.  Also remove cospines (non-kern spines
-//    to the right of the kern spine containing only rests).  If there are
-//    *part# interpretations in the data, then any spine which is all rests
-//    will not be removed if there is another **kern spine with the same
-//    part number if it is also not all rests.
+// Tool_esac2hum::Score::analyzePHR_CAD -- Give a space-delimited
+//     list of the last scale degree of each phrase.
 //
 
-void Tool_extract::fillFieldDataByNoRest(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, const string& searchstring, HumdrumFile& infile,
-		int state) {
+void Tool_esac2hum::Score::analyzePHR_CAD(void) {
+	string output;
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		output += phrase.getLastScaleDegree();
+		if (i < (int)size() - 1) {
+			output += " ";
+		}
+	}
+	m_params["PHR_CAD"] = output;
+}
 
-	field.clear();
-	subfield.clear();
-	model.clear();
 
 
-	// Check every **kern spine for any notes.  If there is a note
-	// then the tracks variable for that spine will be marked
-	// as non-zero.
-	vector<int> tracks(infile.getMaxTrack() + 1, 0);
-	int track;
-	int partline = 0;
-	bool dataQ = false;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if ((!partline) && (!dataQ) && infile[i].hasSpines()) {
+//////////////////////////////
+//
+// Tool_esac2hum::Phrase::getLastScaleDegree --
+//
+
+string Tool_esac2hum::Phrase::getLastScaleDegree(void) {
+	vector<Tool_esac2hum::Note*> notelist;
+	getNoteList(notelist);
 
-		}
-		if (!infile[i].isData()) {
-			continue;
-		}
-		dataQ = true;
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
-				continue;
-			}
-			if (token->isRest()) {
-				continue;
-			}
-			track = token->getTrack();
-			tracks[track] = 1;
+	for (int i=(int)notelist.size() - 1; i>=0; i--) {
+		if (notelist[i]->isPitch()) {
+			return notelist[i]->getScaleDegree();
 		}
 	}
 
-	// Go back and mark any empty spines as non-empty if they
-	// are in a part that contains multiple staves. I.e., only
-	// delete a staff if all staves for the part are empty.
-	// There should be a single *part# line at the start of the
-	// score.
-	if (partline > 0) {
-		vector<HTp> kerns;
-		for (int i=0; i<infile[partline].getFieldCount(); i++) {
-			HTp token = infile.token(partline, i);
-			if (!token->isKern()) {
-				continue;
-			}
-			kerns.push_back(token);
-		}
-		for (int i=0; i<(int)kerns.size(); i++) {
-			for (int j=i+1; j<(int)kerns.size(); j++) {
-				if (*kerns[i] != *kerns[j]) {
-					continue;
-				}
-				if (kerns[i]->find("*part") == string::npos) {
-					continue;
-				}
-				int track1 = kerns[i]->getTrack();
-				int track2 = kerns[j]->getTrack();
-				int state1 = tracks[track1];
-				int state2 = tracks[track2];
-				if ((state1 && !state2) || (state2 && !state1)) {
-					// Prevent empty staff from being removed
-					// from a multi-staff part:
-					tracks[track1] = 1;
-					tracks[track2] = 1;
-				}
-			}
-		}
-	}
+	return "?";
+}
 
 
-	// deal with co-spines
-	vector<HTp> sstarts;
-	infile.getSpineStartList(sstarts);
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (!sstarts[i]->isKern()) {
-			track = sstarts[i]->getTrack();
-			tracks[track] = 1;
-		}
-	}
+//////////////////////////////
+//
+// Tool_esac2hum::Note::getScaleDegree -- return the scale degree
+//     string for the note, such as: 6, -6, +7b, 5#.
+//
 
-	// remove co-spines attached to removed kern spines
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (!sstarts[i]->isKern()) {
-			continue;
-		}
-		if (tracks[sstarts[i]->getTrack()] != 0) {
-			continue;
+string Tool_esac2hum::Note::getScaleDegree(void) {
+	string output;
+	if (m_octave < 0) {
+		for (int i=0; i<-m_octave; i++) {
+			output += "-";
 		}
-		for (int j=i+1; j<(int)sstarts.size(); j++) {
-			if (sstarts[j]->isKern()) {
-				break;
-			}
-			track = sstarts[j]->getTrack();
-			tracks[track] = 0;
+	} else if (m_octave > 0) {
+		for (int i=0; i<m_octave; i++) {
+			output += "+";
 		}
 	}
-
-	int zero = 0;
-	for (int i=1; i<(int)tracks.size(); i++) {
-		if (state != 0) {
-			tracks[i] = !tracks[i];
+	output += to_string(m_degree);
+	if (m_alter < 0) {
+		for (int i=0; i<-m_alter; i++) {
+			output += "b";
 		}
-		if (tracks[i]) {
-			field.push_back(i);
-			subfield.push_back(zero);
-			model.push_back(zero);
+	} else if (m_alter > 0) {
+		for (int i=0; i<m_alter; i++) {
+			output += "#";
 		}
 	}
-
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::fillFieldDataByGrep --
+// Tool_esac2hum::Note::isPitch -- return true if scale degree is not 0.
 //
 
-void Tool_extract::fillFieldDataByGrep(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, const string& searchstring, HumdrumFile& infile,
-		int state) {
-
-	field.reserve(infile.getMaxTrack()+1);
-	subfield.reserve(infile.getMaxTrack()+1);
-	model.reserve(infile.getMaxTrack()+1);
-	field.resize(0);
-	subfield.resize(0);
-	model.resize(0);
-
-	vector<int> tracks;
-	tracks.resize(infile.getMaxTrack()+1);
-	fill(tracks.begin(), tracks.end(), 0);
-	HumRegex hre;
-	int track;
-
-	int i, j;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			continue;
-		}
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			if (hre.search(infile.token(i, j), searchstring, "")) {
-				track = infile[i].token(j)->getTrack();
-				tracks[track] = 1;
-			}
-		}
-	}
-
-	int zero = 0;
-	for (i=1; i<(int)tracks.size(); i++) {
-		if (state != 0) {
-			tracks[i] = !tracks[i];
-		}
-		if (tracks[i]) {
-			field.push_back(i);
-			subfield.push_back(zero);
-			model.push_back(zero);
-		}
-	}
+bool Tool_esac2hum::Note::isPitch(void) {
+	return (m_degree > 0);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::getInterpretationFields --
+// Tool_esac2hum::Note::isRest -- return true if scale degree is 0.
 //
 
-void Tool_extract::getInterpretationFields(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, HumdrumFile& infile, string& interps, int state) {
-	vector<string> sstrings; // search strings
-	sstrings.reserve(100);
-	sstrings.resize(0);
-
-	int i, j, k;
-	string buffer;
-	buffer = interps;
-
-	HumRegex hre;
-	hre.replaceDestructive(buffer, "", "\\s+", "g");
+bool Tool_esac2hum::Note::isRest(void) {
+	return (m_degree <= 0);
+}
 
-	int start = 0;
-	while (hre.search(buffer, start, "^([^,]+)")) {
-		sstrings.push_back(hre.getMatch(1));
-		start = hre.getMatchEndIndex(1);
-	}
 
-	if (debugQ) {
-		m_humdrum_text << "!! Interpretation strings to search for: " << endl;
-		for (i=0; i<(int)sstrings.size(); i++) {
-			m_humdrum_text << "!!\t" << sstrings[i] << endl;
-		}
-	}
 
-	vector<int> tracks;
-	tracks.resize(infile.getMaxTrack()+1);
-	fill(tracks.begin(), tracks.end(), 0);
+//////////////////////////////
+//
+// Tool_esac2hum::Score::analyzeACC --  The first scale degree
+//     of each (complete) meausre, or partial measure start.
+//     the scale degress for each phrase are placed into a word
+//     without spaces, and then a space between each phrase.
+//
+//     Todo: Deal with tied notes at starts of measures.
+//
 
-	// Algorithm below could be made more efficient by
-	// not searching the entire file...
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			for (k=0; k<(int)sstrings.size(); k++) {
-				if (sstrings[k] == *infile.token(i, j)) {
-					tracks[infile[i].token(j)->getTrack()] = 1;
-				}
+void Tool_esac2hum::Score::analyzeACC(void) {
+	string output;
+	for (int i=0; i<(int)size(); i++) {
+		Tool_esac2hum::Phrase& phrase = at(i);
+		for (int j=0; j<(int)phrase.size(); j++) {
+			Tool_esac2hum::Measure& measure = phrase.at(j);
+			if (measure.isComplete()) {
+				output += measure.at(0).getScaleDegree();
 			}
 		}
+		if (i < (int)size() -1) {
+			output += " ";
+		}
 	}
+	m_params["ACC"] =  output;
+}
 
-	field.reserve(tracks.size());
-	subfield.reserve(tracks.size());
-	model.reserve(tracks.size());
 
-	field.resize(0);
-	subfield.resize(0);
-	model.resize(0);
 
-	int zero = 0;
-	for (i=1; i<(int)tracks.size(); i++) {
-		if (state == 0) {
-			tracks[i] = !tracks[i];
-		}
-		if (tracks[i]) {
-			field.push_back(i);
-			subfield.push_back(zero);
-			model.push_back(zero);
-		}
-	}
 
+/////////////////////////////////
+//
+// Tool_esac2humold::Tool_esac2humold -- Set the recognized options for the tool.
+//
+
+Tool_esac2humold::Tool_esac2humold(void) {
+	define("debug=b",            "print debug information");
+	define("v|verbose=b",        "verbose output");
+	define("h|header=s:",        "header filename for placement in output");
+	define("t|trailer=s:",       "trailer filename for placement in output");
+	define("s|split=s:file",     "split song info into separate files");
+	define("x|extension=s:.krn", "split filename extension");
+	define("f|first=i:1",        "number of first split filename");
+	define("author=b",           "author of program");
+	define("version=b",          "compilation info");
+	define("example=b",          "example usages");
+	define("help=b",             "short description");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::expandSpines --
+// Tool_esac2humold::convert -- Convert a MusicXML file into
+//     Humdrum content.
 //
 
-void Tool_extract::expandSpines(vector<int>& field, vector<int>& subfield, vector<int>& model,
-		HumdrumFile& infile, string& interp) {
+bool Tool_esac2humold::convertFile(ostream& out, const string& filename) {
+	ifstream file(filename);
+	stringstream s;
+	if (file) {
+		s << file.rdbuf();
+		file.close();
+	}
+	return convert(out, s.str());
+}
 
-	vector<int> splits;
-	splits.resize(infile.getMaxTrack()+1);
-	fill(splits.begin(), splits.end(), 0);
 
-	int i, j;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isManipulator()) {
-			continue;
-		}
+bool Tool_esac2humold::convert(ostream& out, istream& input) {
+	convertEsacToHumdrum(out, input);
+	return true;
+}
 
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			if (strchr(infile.token(i, j)->getSpineInfo().c_str(), '(') != NULL) {
-				splits[infile[i].token(j)->getTrack()] = 1;
-			}
-		}
-	}
 
-	field.reserve(infile.getMaxTrack()*2);
-	field.resize(0);
+bool Tool_esac2humold::convert(ostream& out, const string& input) {
+	stringstream ss;
+	ss << input;
+	convertEsacToHumdrum(out, ss);
+	return true;
+}
 
-	subfield.reserve(infile.getMaxTrack()*2);
-	subfield.resize(0);
 
-	model.reserve(infile.getMaxTrack()*2);
-	model.resize(0);
 
-	bool allQ = interp.empty();
 
-	vector<int> dummyfield;
-	vector<int> dummysubfield;
-	vector<int> dummymodel;
-	getInterpretationFields(dummyfield, dummysubfield, model, infile, interp, 1);
+//////////////////////////////
+//
+// Tool_esac2humold::initialize --
+//
 
-	vector<int> interptracks;
+bool Tool_esac2humold::initialize(void) {
+	// handle basic options:
+	if (getBoolean("author")) {
+		cerr << "Written by Craig Stuart Sapp, "
+			  << "craig@ccrma.stanford.edu, March 2002" << endl;
+		return false;
+	} else if (getBoolean("version")) {
+		cerr << getCommand() << ", version: 6 June 2017" << endl;
+		cerr << "compiled: " << __DATE__ << endl;
+		return false;
+	} else if (getBoolean("help")) {
+		usage(getCommand());
+		return false;
+	} else if (getBoolean("example")) {
+		example();
+		return false;
+	}
 
-	interptracks.resize(infile.getMaxTrack()+1);
-	fill(interptracks.begin(), interptracks.end(), 0);
+	debugQ   = getBoolean("debug");
+	verboseQ = getBoolean("verbose");
 
-	for (i=0; i<(int)dummyfield.size(); i++) {
-		interptracks[dummyfield[i]] = 1;
+	if (getBoolean("header")) {
+		if (!getFileContents(header, getString("header"))) {
+			return false;
+		}
+	} else {
+		header.resize(0);
 	}
-
-	int aval = 'a';
-	int bval = 'b';
-	int zero = 0;
-	for (i=1; i<(int)splits.size(); i++) {
-		if (splits[i] && (allQ || interptracks[i])) {
-			field.push_back(i);
-			subfield.push_back(aval);
-			model.push_back(zero);
-			field.push_back(i);
-			subfield.push_back(bval);
-			model.push_back(zero);
-		} else {
-			field.push_back(i);
-			subfield.push_back(zero);
-			model.push_back(zero);
+	if (getBoolean("trailer")) {
+		if (!getFileContents(trailer, getString("trailer"))) {
+			return false;
 		}
+	} else {
+		trailer.resize(0);
 	}
 
-	if (debugQ) {
-		m_humdrum_text << "!!expand: ";
-		for (i=0; i<(int)field.size(); i++) {
-			m_humdrum_text << field[i];
-			if (subfield[i]) {
-				m_humdrum_text << (char)subfield[i];
-			}
-			if (i < (int)field.size()-1) {
-				m_humdrum_text << ",";
-			}
-		}
-		m_humdrum_text << endl;
+	if (getBoolean("split")) {
+		splitQ = 1;
 	}
+	namebase = getString("split");
+	fileextension = getString("extension");
+	firstfilenum = getInteger("first");
+	return true;
 }
 
 
 
+//////////////////////////////////////////////////////////////////////////
+
+
 //////////////////////////////
 //
-// Tool_extract::reverseSpines -- reverse the order of spines, grouped by the
-//   given exclusive interpretation.
+// Tool_esac2humold::convertEsacToHumdrum --
 //
 
-void Tool_extract::reverseSpines(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, HumdrumFile& infile, const string& exinterp) {
-
-	vector<int> target;
-	target.resize(infile.getMaxTrack()+1);
-	fill(target.begin(), target.end(), 0);
-
-	vector<HTp> trackstarts;
-	infile.getSpineStartList(trackstarts);
-
-	for (int t=0; t<(int)trackstarts.size(); t++) {
-		if (trackstarts[t]->isDataType(exinterp)) {
-			target.at(t + 1) = 1;
+void Tool_esac2humold::convertEsacToHumdrum(ostream& output, istream& infile) {
+	initialize();
+	vector<string> song;
+	song.reserve(400);
+	int init = 0;
+	// int filecounter = firstfilenum;
+	string outfilename;
+	string numberstring;
+	// ofstream outfile;
+	while (!infile.eof()) {
+		if (debugQ) {
+			cerr << "Getting a song..." << endl;
+		}
+		getSong(song, infile, init);
+		if (debugQ) {
+			cerr << "Got a song ..." << endl;
 		}
+		init = 1;
+		convertSong(song, output);
 	}
+}
 
-	field.reserve(infile.getMaxTrack()*2);
-	field.resize(0);
 
-	int lasti = (int)target.size();
-	for (int i=(int)target.size()-1; i>0; i--) {
-		if (target[i]) {
-			lasti = i;
-			field.push_back(i);
-			for (int j=i+1; j<(int)target.size(); j++) {
-				if (!target.at(j)) {
-					field.push_back(j);
-				} else {
-					break;
-				}
+
+//////////////////////////////
+//
+// Tool_esac2humold::getSong -- get a song from the EsAC file
+//
+
+bool Tool_esac2humold::getSong(vector<string>& song, istream& infile, int init) {
+	string holdbuffer;
+	song.resize(0);
+	if (init) {
+		// do nothing holdbuffer has the CUT[] information
+	} else {
+		while (!infile.eof() && holdbuffer.compare(0, 4, "CUT[") != 0) {
+			getline(infile, holdbuffer);
+			if (verboseQ) {
+				cerr << "Contents: " << holdbuffer << endl;
+			}
+			if (holdbuffer.compare(0, 2, "!!") == 0) {
+				song.push_back(holdbuffer);
 			}
 		}
+		if (infile.eof()) {
+			return false;
+		}
 	}
 
-	// if the grouping spine is not first, then preserve the
-	// locations of the pre-spines.
-	int extras = 0;
-	if (lasti != 1) {
-		extras = lasti - 1;
-		field.resize(field.size()+extras);
-		for (int i=0; i<(int)field.size()-extras; i++) {
-			field[(int)field.size()-1-i] = field[(int)field.size()-1-extras-i];
-		}
-		for (int i=0; i<extras; i++) {
-			field[i] = i+1;
-		}
+	if (!infile.eof()) {
+		song.push_back(holdbuffer);
+	} else {
+		return false;
 	}
 
-	if (debugQ) {
-		m_humdrum_text << "!!reverse: ";
-		for (int i=0; i<(int)field.size(); i++) {
-			m_humdrum_text << field[i] << " ";
+	getline(infile, holdbuffer);
+	chopExtraInfo(holdbuffer);
+	inputline++;
+	if (verboseQ) {
+		cerr << "READ LINE: " << holdbuffer << endl;
+	}
+	while (!infile.eof() && (holdbuffer.compare(0, 4, "CUT[", 4) != 0)) {
+		song.push_back(holdbuffer);
+		getline(infile, holdbuffer);
+		chopExtraInfo(holdbuffer);
+		inputline++;
+		if (verboseQ) {
+			cerr << "READ ANOTHER LINE: " << holdbuffer << endl;
 		}
-		m_humdrum_text << endl;
 	}
 
-	subfield.resize(field.size());
-	fill(subfield.begin(), subfield.end(), 0);
-
-	model.resize(field.size());
-	fill(model.begin(), model.end(), 0);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::fillFieldData --
+// Tool_esac2humold::chopExtraInfo -- remove phrase number information from Luxembourg data.
 //
 
-void Tool_extract::fillFieldData(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, string& fieldstring, HumdrumFile& infile) {
-
-	int maxtrack = infile.getMaxTrack();
-
-	field.reserve(maxtrack);
-	field.resize(0);
-
-	subfield.reserve(maxtrack);
-	subfield.resize(0);
-
-	model.reserve(maxtrack);
-	model.resize(0);
-
+void Tool_esac2humold::chopExtraInfo(string& buffer) {
 	HumRegex hre;
-	string buffer = fieldstring;
-	hre.replaceDestructive(buffer, "", "\\s", "gs");
-	int start = 0;
-	string tempstr;
-	vector<int> tempfield;
-	vector<int> tempsubfield;
-	vector<int> tempmodel;
-	while (hre.search(buffer,  start, "^([^,]+,?)")) {
-		tempfield.clear();
-		tempsubfield.clear();
-		tempmodel.clear();
-		processFieldEntry(tempfield, tempsubfield, tempmodel, hre.getMatch(1), infile);
-		start += hre.getMatchEndIndex(1);
-		field.insert(field.end(), tempfield.begin(), tempfield.end());
-		subfield.insert(subfield.end(), tempsubfield.begin(), tempsubfield.end());
-		model.insert(model.end(), tempmodel.begin(), tempmodel.end());
-	}
+	hre.replaceDestructive(buffer, "", "^\\s+");
+	hre.replaceDestructive(buffer, "", "\\s+$");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::processFieldEntry --
-//   3-6 expands to 3 4 5 6
-//   $   expands to maximum spine track
-//   $-1 expands to maximum spine track minus 1, etc.
+// Tool_esac2humold::printHumdrumHeaderInfo --
 //
 
-void Tool_extract::processFieldEntry(vector<int>& field,
-		vector<int>& subfield, vector<int>& model, const string& astring,
-		HumdrumFile& infile) {
-
-	int finitsize = (int)field.size();
-	int maxtrack = infile.getMaxTrack();
-
-	vector<HTp> ktracks;
-	infile.getKernSpineStartList(ktracks);
-	int maxkerntrack = (int)ktracks.size();
+void Tool_esac2humold::printHumdrumHeaderInfo(ostream& out, vector<string>& song) {
+	for (int i=0; i<(int)song.size(); i++) {
+		if (song[i].size() == 0) {
+			continue;
+		}
+		if (song[i].compare(0, 2, "!!") == 0) {
+			out << song[i] << "\n";
+			continue;
+		}
+		if ((song[i][0] == ' ') || (song[i][0] == '\t')) {
+			continue;
+		}
+		break;
+	}
+}
 
-	int modletter;
-	int subletter;
 
-	HumRegex hre;
-	string buffer = astring;
 
-	// remove any comma left at end of input astring (or anywhere else)
-	hre.replaceDestructive(buffer, "", ",", "g");
+//////////////////////////////
+//
+// Tool_esac2humold::printHumdrumFooterInfo --
+//
 
-	// first remove $ symbols and replace with the correct values
-	if (kernQ) {
-		removeDollarsFromString(buffer, maxkerntrack);
-	} else {
-		removeDollarsFromString(buffer, maxtrack);
-	}
-
-	int zero = 0;
-	if (hre.search(buffer, "^(\\d+)-(\\d+)$")) {
-		int firstone = hre.getMatchInt(1);
-		int lastone  = hre.getMatchInt(2);
-
-		if ((firstone < 1) && (firstone != 0)) {
-			m_error_text << "Error: range token: \"" << astring << "\""
-			             << " contains too small a number at start: " << firstone << endl;
-			m_error_text << "Minimum number allowed is " << 1 << endl;
-			return;
+void Tool_esac2humold::printHumdrumFooterInfo(ostream& out, vector<string>& song) {
+	int i = 0;
+	for (i=0; i<(int)song.size(); i++) {
+		if (song[i].size() == 0) {
+			continue;
 		}
-		if ((lastone < 1) && (lastone != 0)) {
-			m_error_text << "Error: range token: \"" << astring << "\""
-			             << " contains too small a number at end: " << lastone << endl;
-			m_error_text << "Minimum number allowed is " << 1 << endl;
-			return;
+		if (song[i].compare(0, 2, "!!") == 0) {
+			continue;
 		}
-		if (firstone > maxtrack) {
-			m_error_text << "Error: range token: \"" << astring << "\""
-			             << " contains number too large at start: " << firstone << endl;
-			m_error_text << "Maximum number allowed is " << maxtrack << endl;
-			return;
+		if ((song[i][0] == ' ') || (song[i][0] == '\t')) {
+			continue;
 		}
-		if (lastone > maxtrack) {
-			m_error_text << "Error: range token: \"" << astring << "\""
-			             << " contains number too large at end: " << lastone << endl;
-			m_error_text << "Maximum number allowed is " << maxtrack << endl;
-			return;
+		break;
+	}
+	int j = i;
+	for (j=i; j<(int)song.size(); j++) {
+		if (song[j].compare(0, 2, "!!") == 0) {
+			out << song[j] << "\n";
 		}
+	}
+}
 
-		if (firstone > lastone) {
-			for (int i=firstone; i>=lastone; i--) {
-				field.push_back(i);
-				subfield.push_back(zero);
-				model.push_back(zero);
-			}
-		} else {
-			for (int i=firstone; i<=lastone; i++) {
-				field.push_back(i);
-				subfield.push_back(zero);
-				model.push_back(zero);
-			}
-		}
-	} else if (hre.search(buffer, "^(\\d+)([a-z]*)")) {
-		int value = hre.getMatchInt(1);
-		modletter = 0;
-		subletter = 0;
-		if (hre.getMatch(2) ==  "a") {
-			subletter = 'a';
-		}
-		if (hre.getMatch(2) ==  "b") {
-			subletter = 'b';
-		}
-		if (hre.getMatch(2) ==  "c") {
-			subletter = 'c';
-		}
-		if (hre.getMatch(2) ==  "d") {
-			modletter = 'd';
-		}
-		if (hre.getMatch(2) ==  "n") {
-			modletter = 'n';
-		}
-		if (hre.getMatch(2) ==  "r") {
-			modletter = 'r';
-		}
 
-		if ((value < 1) && (value != 0)) {
-			m_error_text << "Error: range token: \"" << astring << "\""
-			             << " contains too small a number at end: " << value << endl;
-			m_error_text << "Minimum number allowed is " << 1 << endl;
-			return;
-		}
-		if (value > maxtrack) {
-			m_error_text << "Error: range token: \"" << astring << "\""
-			             << " contains number too large at start: " << value << endl;
-			m_error_text << "Maximum number allowed is " << maxtrack << endl;
-			return;
-		}
-		field.push_back(value);
-		if (value == 0) {
-			subfield.push_back(zero);
-			model.push_back(zero);
-		} else {
-			subfield.push_back(subletter);
-			model.push_back(modletter);
+
+//////////////////////////////
+//
+// Tool_esac2humold::convertSong --
+//
+
+void Tool_esac2humold::convertSong(vector<string>& song, ostream& out) {
+
+	int i;
+	if (verboseQ) {
+		for (i=0; i<(int)song.size(); i++) {
+			out << song[i] << "\n";
 		}
 	}
 
-	if (!kernQ) {
-		return;
-	}
+	printHumdrumHeaderInfo(out, song);
 
-	// Insert fields to next **kern spine.
-	vector<int> newfield;
-	vector<int> newsubfield;
-	vector<int> newmodel;
+	string key;
+	double mindur = 1.0;
+	string meter;
+	int tonic = 0;
+	getKeyInfo(song, key, mindur, tonic, meter, out);
 
-	vector<HTp> trackstarts;
-	infile.getTrackStartList(trackstarts);
-	int spine;
+	vector<NoteData> songdata;
+	songdata.resize(0);
+	songdata.reserve(1000);
+	getNoteList(song, songdata, mindur, tonic);
+	placeLyrics(song, songdata);
 
-	// convert kern tracks into spine tracks:
-	for (int i=finitsize; i<(int)field.size(); i++) {
-		if (field[i] > 0) {
-			spine = ktracks[field[i]-1]->getTrack();
-		   field[i] = spine;
-		}
-	}
+	vector<int> numerator;
+	vector<int> denominator;
+	getMeterInfo(meter, numerator, denominator);
 
-	int startspineindex, stopspineindex;
-	for (int i=0; i<(int)field.size(); i++) {
-		newfield.push_back(field[i]); // copy **kern spine index into new list
-		newsubfield.push_back(subfield[i]);
-		newmodel.push_back(model[i]);
+	postProcessSongData(songdata, numerator, denominator);
 
-		// search for non **kern spines after specified **kern spine:
-		startspineindex = field[i] + 1 - 1;
-		stopspineindex = maxtrack;
-		for (int j=startspineindex; j<stopspineindex; j++) {
-			if (trackstarts[j]->isKern()) {
-				break;
-			}
-			newfield.push_back(j+1);
-			newsubfield.push_back(zero);
-			newmodel.push_back(zero);
+	printTitleInfo(song, out);
+	out << "!!!id: "    << key  << "\n";
+
+	// check for presence of lyrics
+	int textQ = 0;
+	for (i=0; i<(int)songdata.size(); i++) {
+		if (songdata[i].text !=  "") {
+			textQ = 1;
+			break;
 		}
 	}
 
-	field    = newfield;
-	subfield = newsubfield;
-	model    = newmodel;
-}
+	for (i=0; i<(int)header.size(); i++) {
+		out << header[i] << "\n";
+	}
 
+	out << "**kern";
+	if (textQ) {
+		out << "\t**text";
+	}
+	out << "\n";
 
+	printKeyInfo(songdata, tonic, textQ, out);
+	for (i=0; i<(int)songdata.size(); i++) {
+		printNoteData(songdata[i], textQ, out);
+	}
+	out << "*-";
+	if (textQ) {
+		out << "\t*-";
+	}
+	out << "\n";
 
-//////////////////////////////
-//
-// Tool_extract::removeDollarsFromString -- substitute $ sign for maximum track count.
-//
+	out << "!!!minrhy: ";
+	out << Convert::durationFloatToRecip(mindur)<<"\n";
+	out << "!!!meter";
+	if (numerator.size() > 1) {
+		out << "s";
+	}
+	out << ": "  << meter;
+	if ((meter == "frei") || (meter == "Frei")) {
+		out << " [unmetered]";
+	} else if (meter.find('/') == string::npos) {
+		out << " interpreted as [";
+		for (i=0; i<(int)numerator.size(); i++) {
+			out << numerator[i] << "/" << denominator[i];
+			if (i < (int)numerator.size()-1) {
+				out << ", ";
+			}
+		}
+		out << "]";
+	}
+	out << "\n";
 
-void Tool_extract::removeDollarsFromString(string& buffer, int maxtrack) {
-	HumRegex hre;
-	char buf2[128] = {0};
-	int value2;
+	printBibInfo(song, out);
+	printSpecialChars(out);
 
-	if (hre.search(buffer, "\\$$")) {
-		snprintf(buf2, 128, "%d", maxtrack);
-		hre.replaceDestructive(buffer, buf2, "\\$$");
+	for (i=0; i<(int)songdata.size(); i++) {
+		if (songdata[i].lyricerr) {
+			out << "!!!RWG: Lyric placement mismatch "
+				  << "in phrase (too many syllables) " << songdata[i].phnum << " ["
+				  << key << "]\n";
+			break;
+		}
 	}
 
-	if (hre.search(buffer, "\\$(?![\\d-])")) {
-		// don't know how this case could happen, however...
-		snprintf(buf2, 128, "%d", maxtrack);
-		hre.replaceDestructive(buffer, buf2, "\\$(?![\\d-])", "g");
+	for (i=0; i<(int)trailer.size(); i++) {
+		out << trailer[i] << "\n";
 	}
 
-	if (hre.search(buffer, "\\$0")) {
-		// replace $0 with maxtrack (used for reverse orderings)
-		snprintf(buf2, 128, "%d", maxtrack);
-		hre.replaceDestructive(buffer, buf2, "\\$0", "g");
-	}
+	printHumdrumFooterInfo(out, song);
 
-	while (hre.search(buffer, "\\$(-?\\d+)")) {
-		value2 = maxtrack - abs(hre.getMatchInt(1));
-		snprintf(buf2, 128, "%d", value2);
-		hre.replaceDestructive(buffer, buf2, "\\$-?\\d+");
+/*
+	if (!splitQ) {
+		out << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
 	}
+*/
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::excludeFields -- print all spines except the ones in the list of fields.
+// Tool_esac2humold::placeLyrics -- extract lyrics (if any) and place on correct notes
 //
 
-void Tool_extract::excludeFields(HumdrumFile& infile, vector<int>& field,
-		vector<int>& subfield, vector<int>& model) {
-	int start = 0;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << '\n';
-			continue;
-		} else {
-			start = 0;
-			for (int j=0; j<infile[i].getFieldCount(); j++) {
-				if (isInList(infile[i].token(j)->getTrack(), field)) {
-					continue;
-				}
-				if (start != 0) {
-					m_humdrum_text << '\t';
-				}
-				start = 1;
-				m_humdrum_text << infile.token(i, j);
-			}
-			if (start != 0) {
-				m_humdrum_text << endl;
+bool Tool_esac2humold::placeLyrics(vector<string>& song, vector<NoteData>& songdata) {
+	int start = -1;
+	int stop = -1;
+	getLineRange(song, "TXT", start, stop);
+	if (start < 0) {
+		// no TXT[] field, so don't do anything
+		return true;
+	}
+	int line = 0;
+	vector<string> lyrics;
+	string buffer;
+	for (line=0; line<=stop-start; line++) {
+		if (song[line+start].size() <= 4) {
+			cerr << "Error: lyric line is too short!: "
+				  << song[line+start] << endl;
+			return false;
+		}
+		buffer = song[line+start].substr(4);
+		if (line == stop - start) {
+			auto loc = buffer.rfind(']');
+			if (loc != string::npos) {
+				buffer.resize(loc);
 			}
 		}
+		if (buffer == "") {
+			continue;
+		}
+		getLyrics(lyrics, buffer);
+		cleanupLyrics(lyrics);
+		placeLyricPhrase(songdata, lyrics, line);
 	}
+
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::extractFields -- print all spines in the list of fields.
+// Tool_esac2humold::cleanupLyrics -- add preceeding dashes, avoid starting *'s if any,
+//    and convert _'s to spaces.
 //
 
-void Tool_extract::extractFields(HumdrumFile& infile, vector<int>& field,
-		vector<int>& subfield, vector<int>& model) {
-
-	HumRegex hre;
-	int start = 0;
-	int target;
-	int subtarget;
-	int modeltarget;
-	string spat;
-	bool foundBarline = true;
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << '\n';
-			continue;
-		}
-
-		if (infile[i].isManipulator()) {
-			dealWithSpineManipulators(infile, i, field, subfield, model);
-			continue;
-		}
-
-		if (infile[i].isBarline()) {
-			foundBarline = true;
+void Tool_esac2humold::cleanupLyrics(vector<string>& lyrics) {
+	int length;
+	int length2;
+	int i, j, m;
+	int lastsyl = 0;
+	for (i=0; i<(int)lyrics.size(); i++) {
+		length = (int)lyrics[i].size();
+		for (j=0; j<length; j++) {
+			if (lyrics[i][j] == '_') {
+				lyrics[i][j] = ' ';
+			}
 		}
 
-		start = 0;
-		for (int t=0; t<(int)field.size(); t++) {
-			target = field[t];
-			subtarget = subfield[t];
-			modeltarget = model[t];
-			if (modeltarget == 0) {
-				switch (subtarget) {
-					case 'a':
-					case 'b':
-						modeltarget = submodel;
+		if (i > 0) {
+			if ((lyrics[i] != ".") &&
+				 (lyrics[i] != "")  &&
+				 (lyrics[i] != "%") &&
+				 (lyrics[i] != "^") &&
+				 (lyrics[i] != "|") &&
+				 (lyrics[i] != " ")) {
+				lastsyl = -1;
+				for (m=i-1; m>=0; m--) {
+					if ((lyrics[m] != ".") &&
+						 (lyrics[m] != "")  &&
+						 (lyrics[m] != "%") &&
+						 (lyrics[i] != "^") &&
+						 (lyrics[m] != "|") &&
+						 (lyrics[m] != " ")) {
+						lastsyl = m;
 						break;
-					case 'c':
-						modeltarget = comodel;
-				}
-			}
-			if (target == 0) {
-				if (start != 0) {
-					m_humdrum_text << '\t';
-				}
-				start = 1;
-				if (!infile[i].isManipulator()) {
-					if (infile[i].isLocalComment()) {
-						m_humdrum_text << "!";
-					} else if (infile[i].isBarline()) {
-						m_humdrum_text << infile[i].token(0);
-					} else if (infile[i].isData()) {
-						if (foundBarline) {
-							if (addRestsQ) {
-								HumNum dur = infile[i].getDurationToBarline();
-								m_humdrum_text << Convert::durationToRecip(dur);
-							} else {
-								m_humdrum_text << ".";
-							}
-						} else {
-							m_humdrum_text << ".";
-						}
-						// interpretations handled in dealWithSpineManipulators()
-						// [obviously not, so adding a blank one here
-					} else if (infile[i].isInterpretation()) {
-						HTp token = infile.token(i, 0);
-						if (token->isExpansionLabel()) {
-							m_humdrum_text << token;
-						} else if (token->isExpansionList()) {
-							m_humdrum_text << token;
-						} else {
-							if (addRestsQ) {
-								printInterpretationForKernSpine(infile, i);
-							} else {
-								m_humdrum_text << "*";
-							}
-						}
 					}
 				}
-			} else {
-				for (int j=0; j<infile[i].getFieldCount(); j++) {
-					if (infile[i].token(j)->getTrack() != target) {
-						continue;
-					}
-					switch (subtarget) {
-					case 'a':
-						getSearchPat(spat, target, "a");
-						if (hre.search(infile.token(i,j)->getSpineInfo(), spat) ||
-								!hre.search(infile.token(i, j)->getSpineInfo(), "\\(")) {
-							if (start != 0) {
-								m_humdrum_text << '\t';
-							}
-							start = 1;
-							m_humdrum_text << infile.token(i, j);
-						}
-						break;
-					case 'b':
-						getSearchPat(spat, target, "b");
-						if (hre.search(infile.token(i, j)->getSpineInfo(), spat)) {
-							if (start != 0) {
-								m_humdrum_text << '\t';
-							}
-							start = 1;
-							m_humdrum_text << infile.token(i, j);
-						} else if (!hre.search(infile.token(i, j)->getSpineInfo(),
-								"\\(")) {
-							if (start != 0) {
-								m_humdrum_text << '\t';
-							}
-							start = 1;
-							dealWithSecondarySubspine(field, subfield, model, t,
-									infile, i, j, modeltarget);
-						}
-						break;
-					case 'c':
-						if (start != 0) {
-							m_humdrum_text << '\t';
-						}
-						start = 1;
-						dealWithCospine(field, subfield, model, t, infile, i, j,
-								modeltarget, modeltarget, cointerp);
-						break;
-					default:
-						if (start != 0) {
-							m_humdrum_text << '\t';
+				if (lastsyl >= 0) {
+					length2 = (int)lyrics[lastsyl].size();
+					if (lyrics[lastsyl][length2-1] == '-') {
+						for (j=0; j<=length; j++) {
+							lyrics[i][length - j + 1] = lyrics[i][length - j];
 						}
-						start = 1;
-						m_humdrum_text << infile.token(i, j);
+						lyrics[i][0] = '-';
 					}
 				}
 			}
 		}
 
-		if (infile[i].isData()) {
-			foundBarline = false;
+		// avoid *'s on the start of lyrics by placing a space before
+		// them if they exist.
+		if (lyrics[i][0] == '*') {
+			length = (int)lyrics[i].size();
+			for (j=0; j<=length; j++) {
+				lyrics[i][length - j + 1] = lyrics[i][length - j];
+			}
+			lyrics[i][0] = ' ';
 		}
 
-		if (start != 0) {
-			m_humdrum_text << endl;
+		// avoid !'s on the start of lyrics by placing a space before
+		// them if they exist.
+		if (lyrics[i][0] == '!') {
+			length = (int)lyrics[i].size();
+			for (j=0; j<=length; j++) {
+				lyrics[i][length - j + 1] = lyrics[i][length - j];
+			}
+			lyrics[i][0] = ' ';
 		}
+
 	}
+
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_extract::printInterpretationForKernSpine --
+// Tool_esac2humold::getLyrics -- extract the lyrics from the text string.
 //
 
-void Tool_extract::printInterpretationForKernSpine(HumdrumFile& infile, int index) {
-	HTp kerntok = NULL;
-	for (int j=0; j<infile[index].getFieldCount(); j++) {
-		HTp token = infile.token(index, j);
-		if (!token->isKern()) {
+void Tool_esac2humold::getLyrics(vector<string>& lyrics, const string& buffer) {
+	lyrics.resize(0);
+	int zero1 = 0;
+	string current;
+	int zero2 = 0;
+	zero2 = zero1 + zero2;
+
+	int length = (int)buffer.size();
+	int i;
+
+	i = 0;
+	while (i<length) {
+		current = "";
+		if (buffer[i] == ' ') {
+			current = ".";
+			lyrics.push_back(current);
+			i++;
 			continue;
 		}
-		kerntok = token;
-		break;
+
+		while (i < length && buffer[i] != ' ') {
+			current += buffer[i++];
+		}
+		lyrics.push_back(current);
+		i++;
 	}
 
-	if (kerntok == NULL) {
-		m_humdrum_text << "*";
-		return;
-	}
-
-	if (*kerntok == "*") {
-		m_humdrum_text << kerntok;
-		return;
-	}
-
-	if (kerntok->isKeySignature()) {
-		m_humdrum_text << kerntok;
-		return;
-	}
-	if (kerntok->isKeyDesignation()) {
-		m_humdrum_text << kerntok;
-		return;
-	}
-	if (kerntok->isTimeSignature()) {
-		m_humdrum_text << kerntok;
-		return;
-	}
-	if (kerntok->isMensurationSymbol()) {
-		m_humdrum_text << kerntok;
-		return;
-	}
-	if (kerntok->isTempo()) {
-		m_humdrum_text << kerntok;
-		return;
-	}
-	if (kerntok->isInstrumentName()) {
-		m_humdrum_text << "*I\"";
-		return;
-	}
-	if (kerntok->isInstrumentAbbreviation()) {
-		m_humdrum_text << "*I'";
-		return;
-	}
-
-	m_humdrum_text << "*";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::dealWithCospine -- extract the required token(s) from a co-spine.
+// Tool_esac2humold::placeLyricPhrase -- match lyrics from a phrase to the songdata.
 //
 
-void Tool_extract::dealWithCospine(vector<int>& field, vector<int>& subfield, vector<int>& model,
-		int targetindex, HumdrumFile& infile, int line, int cospine,
-		int comodel, int submodel, const string& cointerp) {
-
-	vector<string> cotokens;
-	cotokens.reserve(50);
-
-	string buffer;
-	int i, j, k;
-	int index;
-
-	if (infile[line].isInterpretation()) {
-		m_humdrum_text << infile.token(line, cospine);
-		return;
-	}
-
-	if (infile[line].isBarline()) {
-		m_humdrum_text << infile.token(line, cospine);
-		return;
-	}
-
-	if (infile[line].isLocalComment()) {
-		m_humdrum_text << infile.token(line, cospine);
-		return;
-	}
+bool Tool_esac2humold::placeLyricPhrase(vector<NoteData>& songdata, vector<string>& lyrics, int line) {
+	int i = 0;
+	int start = 0;
+	int found = 0;
 
-	int count = infile[line].token(cospine)->getSubtokenCount();
-	for (k=0; k<count; k++) {
-		buffer = infile.token(line, cospine)->getSubtoken(k);
-		cotokens.resize(cotokens.size()+1);
-		index = (int)cotokens.size()-1;
-		cotokens[index] = buffer;
+	if (lyrics.empty()) {
+		return true;
 	}
 
-	vector<int> spineindex;
-	vector<int> subspineindex;
-
-	spineindex.reserve(infile.getMaxTrack()*2);
-	spineindex.resize(0);
-
-	subspineindex.reserve(infile.getMaxTrack()*2);
-	subspineindex.resize(0);
-
-	for (j=0; j<infile[line].getFieldCount(); j++) {
-		if (infile.token(line, j)->isDataType(cointerp)) {
-			continue;
-		}
-		if (*infile.token(line, j) == ".") {
-			continue;
-		}
-		count = infile[line].token(j)->getSubtokenCount();
-		for (k=0; k<count; k++) {
-			buffer = infile[line].token(j)->getSubtoken(k);
-			if (comodel == 'r') {
-				if (buffer == "r") {
-					continue;
-				}
-			}
-			spineindex.push_back(j);
-			subspineindex.push_back(k);
+	// find the phrase to which the lyrics belongs
+	for (i=0; i<(int)songdata.size(); i++) {
+		if (songdata[i].phnum == line) {
+			found = 1;
+			break;
 		}
 	}
+	start = i;
 
-	if (debugQ) {
-		m_humdrum_text << "\n!!codata:\n";
-		for (i=0; i<(int)cotokens.size(); i++) {
-			m_humdrum_text << "!!\t" << i << "\t" << cotokens[i];
-			if (i < (int)spineindex.size()) {
-				m_humdrum_text << "\tspine=" << spineindex[i];
-				m_humdrum_text << "\tsubspine=" << subspineindex[i];
-			} else {
-				m_humdrum_text << "\tspine=.";
-				m_humdrum_text << "\tsubspine=.";
-			}
-			m_humdrum_text << endl;
-		}
+	if (!found) {
+		cerr << "Error: cannot find music for lyrics line " << line << endl;
+		cerr << "Error near input data line: " << inputline << endl;
+		return false;
 	}
 
-	string buff;
-
-	int start = 0;
-	for (i=0; i<(int)field.size(); i++) {
-		if (infile.token(line, field[i])->isDataType(cointerp)) {
-			continue;
-		}
-
-		for (j=0; j<infile[line].getFieldCount(); j++) {
-			if (infile[line].token(j)->getTrack() != field[i]) {
-				continue;
-			}
-			if (subfield[i] == 'a') {
-				getSearchPat(buff, field[i], "a");
-				if ((strchr(infile.token(line, j)->getSpineInfo().c_str(), '(') == NULL) ||
-					(infile.token(line, j)->getSpineInfo().find(buff) != string::npos)) {
-					printCotokenInfo(start, infile, line, j, cotokens, spineindex,
-							subspineindex);
-				}
-			} else if (subfield[i] == 'b') {
-				// this section may need more work...
-				getSearchPat(buff, field[i], "b");
-				if ((strchr(infile.token(line, j)->getSpineInfo().c_str(), '(') == NULL) ||
-					(strstr(infile.token(line, j)->getSpineInfo().c_str(), buff.c_str()) != NULL)) {
-					printCotokenInfo(start, infile, line, j, cotokens, spineindex,
-							subspineindex);
-				}
+	for (i=0; i<(int)lyrics.size() && i+start < (int)songdata.size(); i++) {
+		if ((lyrics[i] == " ") || (lyrics[i] == ".") || (lyrics[i] == "")) {
+			if (songdata[i+start].pitch < 0) {
+				lyrics[i] = "%";
 			} else {
-				printCotokenInfo(start, infile, line, j, cotokens, spineindex,
-					subspineindex);
+				lyrics[i] = "|";
 			}
+			// lyrics[i] = ".";
+		}
+		songdata[i+start].text = lyrics[i];
+		songdata[i+start].lyricnum = line;
+		if (line != songdata[i+start].phnum) {
+			songdata[i+start].lyricerr = 1;   // lyric does not line up with music
 		}
 	}
+
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::printCotokenInfo --
+// Tool_esac2humold::printSpecialChars -- print high ASCII character table
 //
 
-void Tool_extract::printCotokenInfo(int& start, HumdrumFile& infile, int line, int spine,
-		vector<string>& cotokens, vector<int>& spineindex,
-		vector<int>& subspineindex) {
+void Tool_esac2humold::printSpecialChars(ostream& out) {
 	int i;
-	int found = 0;
-	for (i=0; i<(int)spineindex.size(); i++) {
-		if (spineindex[i] == spine) {
-			if (start == 0) {
-				start++;
-			} else {
-				m_humdrum_text << subtokenseparator;
-			}
-			if (i<(int)cotokens.size()) {
-				m_humdrum_text << cotokens[i];
-			} else {
-				m_humdrum_text << ".";
-			}
-		found = 1;
+	for (i=0; i<(int)chartable.size(); i++) {
+		if (chartable[i]) {
+		switch (i) {
+			case 129:   out << "!!!RNB" << ": symbol: &uuml;  = u umlaut (UTF-8: "
+							     << (char)0xc3 << (char)0xb3 << ")\n";    break;
+			case 130:   out << "!!!RNB" << ": symbol: &eacute;= e acute  (UTF-8: "
+							     << (char)0xc3 << (char)0xa9 << ")\n";    break;
+			case 132:   out << "!!!RNB" << ": symbol: &auml;  = a umlaut (UTF-8: "
+							     << (char)0xc3 << (char)0xa4 << ")\n";    break;
+			case 134:   out << "!!!RNB" << ": symbol: $c      = c acute  (UTF-8: "
+							     << (char)0xc4 << (char)0x87 << ")\n";    break;
+			case 136:   out << "!!!RNB" << ": symbol: $l      = l slash  (UTF-8: "
+							     << (char)0xc5 << (char)0x82 << ")\n";    break;
+			case 140:   out << "!!!RNB" << ": symbol: &icirc; = i circumflex (UTF-8: "
+							     << (char)0xc3 << (char)0xaf << ")\n";    break;
+			case 141:   out << "!!!RNB" << ": symbol: $X      = Z acute  (UTF-8: "
+							     << (char)0xc5 << (char)0xb9 << ")\n";    break;
+			case 142:   out << "!!!RNB" << ": symbol: &auml;  = a umlaut (UTF-8: "
+							     << (char)0xc3 << (char)0xa4 << ")\n";    break;
+			case 143:   out << "!!!RNB" << ": symbol: $C      = C acute  (UTF-8: "
+							     << (char)0xc4 << (char)0x86 << ")\n";    break;
+			case 148:   out << "!!!RNB" << ": symbol: &ouml;  = o umlaut (UTF-8: "
+							     << (char)0xc3 << (char)0xb6 << ")\n";    break;
+			case 151:   out << "!!!RNB" << ": symbol: $S      = S acute  (UTF-8: "
+							     << (char)0xc5 << (char)0x9a << ")\n";    break;
+			case 152:   out << "!!!RNB" << ": symbol: $s      = s acute  (UTF-8: "
+							     << (char)0xc5 << (char)0x9b << ")\n";    break;
+			case 156:   out << "!!!RNB" << ": symbol: $s      = s acute  (UTF-8: "
+							     << (char)0xc5 << (char)0x9b << ")\n";    break;
+			case 157:   out << "!!!RNB" << ": symbol: $L      = L slash  (UTF-8: "
+							     << (char)0xc5 << (char)0x81 << ")\n";    break;
+			case 159:   out << "!!!RNB" << ": symbol: $vc     = c hachek (UTF-8: "
+							     << (char)0xc4 << (char)0x8d << ")\n";    break;
+			case 162:   out << "!!!RNB" << ": symbol: &oacute;= o acute  (UTF-8: "
+							     << (char)0xc3 << (char)0xb3 << ")\n";    break;
+			case 163:   out << "!!!RNB" << ": symbol: &uacute;= u acute  (UTF-8: "
+							     << (char)0xc3 << (char)0xba << ")\n";    break;
+			case 165:   out << "!!!RNB" << ": symbol: $a      = a hook   (UTF-8: "
+							     << (char)0xc4 << (char)0x85 << ")\n";    break;
+			case 169:   out << "!!!RNB" << ": symbol: $e      = e hook   (UTF-8: "
+							     << (char)0xc4 << (char)0x99 << ")\n";    break;
+			case 171:   out << "!!!RNB" << ": symbol: $y      = z acute  (UTF-8: "
+							     << (char)0xc5 << (char)0xba << ")\n";    break;
+			case 175:   out << "!!!RNB" << ": symbol: $Z      = Z dot    (UTF-8: "
+							     << (char)0xc5 << (char)0xbb << ")\n";    break;
+			case 179:   out << "!!!RNB" << ": symbol: $l      = l slash  (UTF-8: "
+							     << (char)0xc5 << (char)0x82 << ")\n";    break;
+			case 185:   out << "!!!RNB" << ": symbol: $a      = a hook   (UTF-8: "
+							     << (char)0xc4 << (char)0x85 << ")\n";    break;
+			case 189:   out << "!!!RNB" << ": symbol: $Z      = Z dot    (UTF-8: "
+							     << (char)0xc5 << (char)0xbb << ")\n";    break;
+			case 190:   out << "!!!RNB" << ": symbol: $z      = z dot    (UTF-8: "
+							     << (char)0xc5 << (char)0xbc << ")\n";    break;
+			case 191:   out << "!!!RNB" << ": symbol: $z      = z dot    (UTF-8: "
+							     << (char)0xc5 << (char)0xbc << ")\n";    break;
+			case 224:   out << "!!!RNB" << ": symbol: &Oacute;= O acute  (UTF-8: "
+							     << (char)0xc3 << (char)0x93 << ")\n";    break;
+			case 225:   out << "!!!RNB" << ": symbol: &szlig; = sz ligature (UTF-8: "
+							     << (char)0xc3 << (char)0x9f << ")\n";    break;
+			case 0xdf:  out << "!!!RNB" << ": symbol: &szlig; = sz ligature (UTF-8: "
+							     << (char)0xc3 << (char)0x9f << ")\n";    break;
+// Polish version:
+//         case 228:   out << "!!!RNB" << ": symbol: $n      = n acute  (UTF-8: "
+//                          << (char)0xc5 << (char)0x84 << ")\n";    break;
+// Luxembourg version for some reason...:
+			case 228:   out << "!!!RNB" << ": symbol: &auml;      = a umlaut  (UTF-8: "
+							     << (char)0xc5 << (char)0x84 << ")\n";    break;
+			case 230:   out << "!!!RNB" << ": symbol: c       = c\n";           break;
+			case 231:   out << "!!!RNB" << ": symbol: $vs     = s hachek (UTF-8: "
+							     << (char)0xc5 << (char)0xa1 << ")\n";    break;
+			case 234:   out << "!!!RNB" << ": symbol: $e      = e hook   (UTF-8: "
+							     << (char)0xc4 << (char)0x99 << ")\n";    break;
+			case 241:   out << "!!!RNB" << ": symbol: $n      = n acute  (UTF-8: "
+							     << (char)0xc5 << (char)0x84 << ")\n";    break;
+			case 243:   out << "!!!RNB" << ": symbol: &oacute;= o acute  (UTF-8: "
+							     << (char)0xc3 << (char)0xb3 << ")\n";    break;
+			case 252:   out << "!!!RNB" << ": symbol: &uuml;  = u umlaut (UTF-8: "
+							     << (char)0xc3 << (char)0xbc << ")\n";    break;
+//         default:
 		}
-	}
-	if (!found) {
-		if (start == 0) {
-			start++;
-		} else {
-			m_humdrum_text << subtokenseparator;
 		}
-		m_humdrum_text << ".";
+		chartable[i] = 0;
 	}
 }
 
@@ -81171,937 +81485,1047 @@ void Tool_extract::printCotokenInfo(int& start, HumdrumFile& infile, int line, i
 
 //////////////////////////////
 //
-// Tool_extract::dealWithSecondarySubspine -- what to print if a secondary spine
-//     does not exist on a line.
+// Tool_esac2humold::printTitleInfo -- print the first line of the CUT[] field.
 //
 
-void Tool_extract::dealWithSecondarySubspine(vector<int>& field, vector<int>& subfield,
-		vector<int>& model, int targetindex, HumdrumFile& infile, int line,
-		int spine, int submodel) {
-
-	int& i = line;
-	int& j = spine;
+bool Tool_esac2humold::printTitleInfo(vector<string>& song, ostream& out) {
+	int start = -1;
+	int stop = -1;
+	getLineRange(song, "CUT", start, stop);
+	if (start == -1) {
+		cerr << "Error: cannot find CUT[] field in song: " << song[0] << endl;
+		return false;
+	}
 
-	HumRegex hre;
 	string buffer;
-	if (infile[line].isLocalComment()) {
-		if ((submodel == 'n') || (submodel == 'r')) {
-			m_humdrum_text << "!";
-		} else {
-			m_humdrum_text << infile.token(i, j);
-		}
-	} else if (infile[line].isBarline()) {
-		m_humdrum_text << infile.token(i, j);
-	} else if (infile[line].isInterpretation()) {
-		if ((submodel == 'n') || (submodel == 'r')) {
-			m_humdrum_text << "*";
-		} else {
-			m_humdrum_text << infile.token(i, j);
-		}
-	} else if (infile[line].isData()) {
-		if (submodel == 'n') {
-			m_humdrum_text << ".";
-		} else if (submodel == 'r') {
-			if (*infile.token(i, j) == ".") {
-				m_humdrum_text << ".";
-			} else if (infile.token(i, j)->find('q') != string::npos) {
-				m_humdrum_text << ".";
-			} else if (infile.token(i, j)->find('Q') != string::npos) {
-				m_humdrum_text << ".";
-			} else {
-				buffer = *infile.token(i, j);
-				if (hre.search(buffer, "{")) {
-					m_humdrum_text << "{";
-				}
-				// remove secondary chord notes:
-				hre.replaceDestructive(buffer, "", " .*");
-				// remove unnecessary characters (such as stem direction):
-				hre.replaceDestructive(buffer, "",
-						"[^}pPqQA-Ga-g0-9.;%#nr-]", "g");
-				// change pitch to rest:
-				hre.replaceDestructive(buffer, "[A-Ga-g#n-]+", "r");
-				// add editorial marking unless -Y option is given:
-				if (editorialInterpretation != "") {
-					if (hre.search(buffer, "rr")) {
-						 hre.replaceDestructive(buffer, editorialInterpretation, "(?<=rr)");
-						 hre.replaceDestructive(buffer, "r", "rr");
-					} else {
-						 hre.replaceDestructive(buffer, editorialInterpretation, "(?<=r)");
-					}
-				}
-				m_humdrum_text << buffer;
-			}
-		} else {
-			m_humdrum_text << infile.token(i, j);
-		}
-	} else {
-		m_error_text << "Should not get to this line of code" << endl;
-		return;
+	buffer = song[start].substr(4);
+	if (buffer.back() == ']') {
+		buffer.resize((int)buffer.size() - 1);
 	}
-}
 
+	out << "!!!OTL: ";
+	for (int i=0; i<(int)buffer.size(); i++) {
+		printChar(buffer[i], out);
+	}
+	out << "\n";
+
+	return true;
+}
 
 
 
 //////////////////////////////
 //
-// Tool_extract::getSearchPat --
+// Tool_esac2humold::printChar -- print text characters, translating high-bit data
+//    if required.
 //
 
-void Tool_extract::getSearchPat(string& spat, int target, const string& modifier) {
-	if (modifier.size() > 20) {
-		m_error_text << "Error in GetSearchPat" << endl;
-		return;
+void Tool_esac2humold::printChar(unsigned char c, ostream& out) {
+	out << c;
+/*
+	if (c < 128) {
+		out << c;
+	} else {
+		chartable[c]++;
+		switch (c) {
+			case 129:   out << "&uuml;";    break;
+			case 130:   out << "&eacute;";  break;
+			case 132:   out << "&auml;";    break;
+			case 134:   out << "$c";        break;
+			case 136:   out << "$l";        break;
+			case 140:   out << "&icirc;";   break;
+			case 141:   out << "$X";        break;   // Z acute
+			case 142:   out << "&auml;";    break;   // ?
+			case 143:   out << "$C";        break;
+			case 148:   out << "&ouml;";    break;
+			case 151:   out << "$S";        break;
+			case 152:   out << "$s";        break;
+			case 156:   out << "$s";        break;  // 1250 encoding
+			case 157:   out << "$L";        break;
+			case 159:   out << "$vc";       break;  // Cech c with v accent
+			case 162:   out << "&oacute;";  break;
+			case 163:   out << "&uacute;";  break;
+			case 165:   out << "$a";        break;
+			case 169:   out << "$e";        break;
+			case 171:   out << "$y";        break;
+			case 175:   out << "$Z";        break;  // 1250 encoding
+			case 179:   out << "$l";        break;  // 1250 encoding
+			case 185:   out << "$a";        break;  // 1250 encoding
+			case 189:   out << "$Z";        break;  // Z dot
+			case 190:   out << "$z";        break;  // z dot
+			case 191:   out << "$z";        break;  // 1250 encoding
+			case 224:   out << "&Oacute;";  break;
+			case 225:   out << "&szlig;";   break;
+			case 0xdf:  out << "&szlig;";   break;
+			// Polish version:
+			// case 228:   out << "$n";        break;
+			// Luxembourg version (for some reason...)
+			case 228:   out << "&auml;";        break;
+			case 230:   out << "c";         break;  // ?
+			case 231:   out << "$vs";       break;  // Cech s with v accent
+			case 234:   out << "$e";        break;  // 1250 encoding
+			case 241:   out << "$n";        break;  // 1250 encoding
+			case 243:   out << "&oacute;";  break;  // 1250 encoding
+			case 252:   out << "&uuml;";    break;
+			default:    out << c;
+		}
 	}
-	spat.reserve(16);
-	spat = "\\(";
-	spat += to_string(target);
-	spat += "\\)";
-	spat += modifier;
+*/
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::dealWithSpineManipulators -- check for proper Humdrum syntax of
-//     spine manipulators (**, *-, *x, *v, *^) when creating the output.
+// Tool_esac2humold::printKeyInfo --
 //
 
-void Tool_extract::dealWithSpineManipulators(HumdrumFile& infile, int line,
-		vector<int>& field, vector<int>& subfield, vector<int>& model) {
-
-	vector<int> vmanip;  // counter for *v records on line
-	vmanip.resize(infile[line].getFieldCount());
-	fill(vmanip.begin(), vmanip.end(), 0);
-
-	vector<int> xmanip; // counter for *x record on line
-	xmanip.resize(infile[line].getFieldCount());
-	fill(xmanip.begin(), xmanip.end(), 0);
-
-	int i = 0;
-	int j;
-	for (j=0; j<(int)vmanip.size(); j++) {
-		if (*infile.token(line, j) == "*v") {
-			vmanip[j] = 1;
-		}
-		if (*infile.token(line, j) == "*x") {
-			xmanip[j] = 1;
+void Tool_esac2humold::printKeyInfo(vector<NoteData>& songdata, int tonic, int textQ,
+		ostream& out) {
+	vector<int> pitches(40, 0);
+	int pitchsum = 0;
+	int pitchcount = 0;
+	int i;
+	for (i=0; i<(int)songdata.size(); i++) {
+		if (songdata[i].pitch >= 0) {
+			pitches[songdata[i].pitch % 40]++;
+			pitchsum += Convert::base40ToMidiNoteNumber(songdata[i].pitch);
+			pitchcount++;
 		}
 	}
 
-	int counter = 1;
-	for (i=1; i<(int)xmanip.size(); i++) {
-		if ((xmanip[i] == 1) && (xmanip[i-1] == 1)) {
-			xmanip[i] = counter;
-			xmanip[i-1] = counter;
-			counter++;
+	// generate a clef, choosing either treble or bass clef depending
+	// on the average pitch.
+	double averagepitch = pitchsum * 1.0 / pitchcount;
+	if (averagepitch > 60.0) {
+		out << "*clefG2";
+		if (textQ) {
+			out << "\t*clefG2";
 		}
-	}
-
-	counter = 1;
-	i = 0;
-	while (i < (int)vmanip.size()) {
-		if (vmanip[i] == 1) {
-			while ((i < (int)vmanip.size()) && (vmanip[i] == 1)) {
-				vmanip[i] = counter;
-				i++;
-			}
-			counter++;
+		out << "\n";
+	} else {
+		out << "*clefF4";
+		if (textQ) {
+			out << "\t*clefF4";
 		}
-		i++;
+		out << "\n";
 	}
 
-	vector<int> fieldoccur;  // nth occurance of an input spine in the output
-	fieldoccur.resize(field.size());
-	fill(fieldoccur.begin(), fieldoccur.end(), 0);
-
-	vector<int> trackcounter; // counter of input spines occurances in output
-	trackcounter.resize(infile.getMaxTrack()+1);
-	fill(trackcounter.begin(), trackcounter.end(), 0);
+	// generate a key signature
+	vector<int> diatonic(7, 0);
+	diatonic[0] = getAccidentalMax(pitches[1], pitches[2], pitches[3]);
+	diatonic[1] = getAccidentalMax(pitches[7], pitches[8], pitches[9]);
+	diatonic[2] = getAccidentalMax(pitches[13], pitches[14], pitches[15]);
+	diatonic[3] = getAccidentalMax(pitches[18], pitches[19], pitches[20]);
+	diatonic[4] = getAccidentalMax(pitches[24], pitches[25], pitches[26]);
+	diatonic[5] = getAccidentalMax(pitches[30], pitches[31], pitches[32]);
+	diatonic[6] = getAccidentalMax(pitches[36], pitches[37], pitches[38]);
 
-	for (i=0; i<(int)field.size(); i++) {
-		if (field[i] != 0) {
-			trackcounter[field[i]]++;
-			fieldoccur[i] = trackcounter[field[i]];
+	int flatcount = 0;
+	int sharpcount = 0;
+	int naturalcount = 0;
+	for (i=0; i<7; i++) {
+		switch (diatonic[i]) {
+			case -1:   flatcount++;      break;
+			case  0:   naturalcount++;   break;
+			case +1:   sharpcount++;     break;
 		}
 	}
 
-	vector<string> tempout;
-	vector<int> vserial;
-	vector<int> xserial;
-	vector<int> fpos;     // input column of output spine
-
-	tempout.reserve(1000);
-	tempout.resize(0);
-
-	vserial.reserve(1000);
-	vserial.resize(0);
+	char kbuf[32] = {0};
+	if (naturalcount == 7) {
+		// do nothing
+	} else if (flatcount > sharpcount) {
+		// print a flat key signature
+		if (diatonic[6] == -1) strcat(kbuf, "b-"); else goto keysigend;
+		if (diatonic[2] == -1) strcat(kbuf, "e-"); else goto keysigend;
+		if (diatonic[5] == -1) strcat(kbuf, "a-"); else goto keysigend;
+		if (diatonic[1] == -1) strcat(kbuf, "d-"); else goto keysigend;
+		if (diatonic[4] == -1) strcat(kbuf, "g-"); else goto keysigend;
+		if (diatonic[0] == -1) strcat(kbuf, "c-"); else goto keysigend;
+		if (diatonic[3] == -1) strcat(kbuf, "f-"); else goto keysigend;
+	} else {
+		// print a sharp key signature
+		if (diatonic[3] == +1) strcat(kbuf, "f#"); else goto keysigend;
+		if (diatonic[0] == +1) strcat(kbuf, "c#"); else goto keysigend;
+		if (diatonic[4] == +1) strcat(kbuf, "g#"); else goto keysigend;
+		if (diatonic[1] == +1) strcat(kbuf, "d#"); else goto keysigend;
+		if (diatonic[5] == +1) strcat(kbuf, "a#"); else goto keysigend;
+		if (diatonic[2] == +1) strcat(kbuf, "e#"); else goto keysigend;
+		if (diatonic[6] == +1) strcat(kbuf, "b#"); else goto keysigend;
+	}
 
-	xserial.reserve(1000);
-	xserial.resize(0);
+keysigend:
+	out << "*k[" << kbuf << "]";
+	if (textQ) {
+		out << "\t*k[" << kbuf << "]";
+	}
+	out << "\n";
 
-	fpos.reserve(1000);
-	fpos.resize(0);
+	// look at the third scale degree above the tonic pitch
+	int minor = pitches[(tonic + 40 + 11) % 40];
+	int major = pitches[(tonic + 40 + 12) % 40];
 
-	string spat;
-	string spinepat;
-	HumRegex hre;
-	int subtarget;
-	int modeltarget;
-	int xdebug = 0;
-	int vdebug = 0;
-	int suppress = 0;
-	int target;
-	int tval;
-	for (int t=0; t<(int)field.size(); t++) {
-		target = field[t];
-		subtarget = subfield[t];
-		modeltarget = model[t];
-		if (modeltarget == 0) {
-			switch (subtarget) {
-				case 'a':
-				case 'b':
-					modeltarget = submodel;
-					break;
-				case 'c':
-					modeltarget = comodel;
-			}
+	if (minor > major) {
+		// minor key (or related mode)
+		out  << "*" << Convert::base40ToKern(40 * 4 + tonic) << ":";
+		if (textQ) {
+			out  << "\t*" << Convert::base40ToKern(40 * 4 + tonic) << ":";
 		}
-		suppress = 0;
-		if (target == 0) {
-			if (infile.token(line, 0)->compare(0, 2, "**") == 0) {
-				storeToken(tempout, blankName);
-				tval = 0;
-				vserial.push_back(tval);
-				xserial.push_back(tval);
-				fpos.push_back(tval);
-			} else if (*infile.token(line, 0) == "*-") {
-				storeToken(tempout, "*-");
-				tval = 0;
-				vserial.push_back(tval);
-				xserial.push_back(tval);
-				fpos.push_back(tval);
-			} else {
-				storeToken(tempout, "*");
-				tval = 0;
-				vserial.push_back(tval);
-				xserial.push_back(tval);
-				fpos.push_back(tval);
-			}
-		} else {
-			for (j=0; j<infile[line].getFieldCount(); j++) {
-				if (infile[line].token(j)->getTrack() != target) {
-					continue;
-				}
-		// filter by subfield
-		if (subtarget == 'a') {
-			getSearchPat(spat, target, "b");
-			if (hre.search(infile.token(line, j)->getSpineInfo(), spat)) {
-						continue;
-			}
-		} else if (subtarget == 'b') {
-			getSearchPat(spat, target, "a");
-			if (hre.search(infile.token(line, j)->getSpineInfo(), spat)) {
-				continue;
-			}
+		out << "\n";
+	} else {
+		// major key (or related mode)
+		out  << "*" << Convert::base40ToKern(40 * 3 + tonic) << ":";
+		if (textQ) {
+			out  << "\t*" << Convert::base40ToKern(40 * 3 + tonic) << ":";
 		}
+		out << "\n";
+	}
 
-				switch (subtarget) {
-				case 'a':
-
-					if (!hre.search(infile.token(line, j)->getSpineInfo(), "\\(")) {
-						if (*infile.token(line, j)  == "*^") {
-							 storeToken(tempout, "*");
-						} else {
-							 storeToken(tempout, *infile.token(line, j));
-						}
-					} else {
-						getSearchPat(spat, target, "a");
-						spinepat =  infile.token(line, j)->getSpineInfo();
-						hre.replaceDestructive(spinepat, "\\(", "\\(", "g");
-						hre.replaceDestructive(spinepat, "\\)", "\\)", "g");
-
-						if ((*infile.token(line, j) == "*v") &&
-							    (spinepat == spat)) {
-							 storeToken(tempout, "*");
-						} else {
-							getSearchPat(spat, target, "b");
-							if ((spinepat == spat) &&
-									(*infile.token(line, j) ==  "*v")) {
-								// do nothing
-								suppress = 1;
-							} else {
-								storeToken(tempout, *infile.token(line, j));
-							}
-						}
-					}
-
-					break;
-				case 'b':
-
-					if (!hre.search(infile.token(line, j)->getSpineInfo(), "\\(")) {
-						if (*infile.token(line, j) == "*^") {
-							storeToken(tempout, "*");
-						} else {
-							storeToken(tempout, *infile.token(line, j));
-						}
-					} else {
-						getSearchPat(spat, target, "b");
-						spinepat = infile.token(line, j)->getSpineInfo();
-						hre.replaceDestructive(spinepat, "\\(", "\\(", "g");
-						hre.replaceDestructive(spinepat, "\\)", "\\)", "g");
-
-						if ((*infile.token(line, j) ==  "*v") &&
-								(spinepat == spat)) {
-							storeToken(tempout, "*");
-						} else {
-							getSearchPat(spat, target, "a");
-							if ((spinepat == spat) &&
-									(*infile.token(line, j) == "*v")) {
-								// do nothing
-								suppress = 1;
-							} else {
-								storeToken(tempout, *infile.token(line, j));
-							}
-						}
-					}
+}
 
-					break;
-				case 'c':
-					// work on later
-					storeToken(tempout, *infile.token(line, j));
-					break;
-				default:
-					storeToken(tempout, *infile.token(line, j));
-				}
 
-				if (suppress) {
-					continue;
-				}
+//////////////////////////////
+//
+// Tool_esac2humold::getAccidentalMax --
+//
 
-				if (tempout[(int)tempout.size()-1] == "*x") {
-					tval = fieldoccur[t] * 1000 + xmanip[j];
-					xserial.push_back(tval);
-					xdebug = 1;
-				} else {
-					tval = 0;
-					xserial.push_back(tval);
-				}
+int Tool_esac2humold::getAccidentalMax(int a, int b, int c) {
+	if (a > b && a > c) {
+		return -1;
+	} else if (c > a && c > b) {
+		return +1;
+	} else {
+		return 0;
+	}
+}
 
-				if (tempout[(int)tempout.size()-1] == "*v") {
-					tval = fieldoccur[t] * 1000 + vmanip[j];
-					vserial.push_back(tval);
-					vdebug = 1;
-				} else {
-					tval = 0;
-					vserial.push_back(tval);
-				}
 
-				fpos.push_back(j);
+//////////////////////////////
+//
+// Tool_esac2humold::postProcessSongData -- clean up data and do some interpreting.
+//
 
-			}
+void Tool_esac2humold::postProcessSongData(vector<NoteData>& songdata, vector<int>& numerator,
+		vector<int>& denominator) {
+	int i, j;
+	// move phrase start markers off of rests and onto the
+	// first note that it finds
+	for (i=0; i<(int)songdata.size()-1; i++) {
+		if (songdata[i].pitch < 0 && songdata[i].phstart) {
+			songdata[i+1].phstart = songdata[i].phstart;
+			songdata[i].phstart = 0;
 		}
 	}
 
-	if (debugQ && xdebug) {
-		m_humdrum_text << "!! *x serials = ";
-		for (int ii=0; ii<(int)xserial.size(); ii++) {
-			m_humdrum_text << xserial[ii] << " ";
+	// move phrase ending markers off of rests and onto the
+	// previous note that it finds
+	for (i=(int)songdata.size()-1; i>0; i--) {
+		if (songdata[i].pitch < 0 && songdata[i].phend) {
+			songdata[i-1].phend = songdata[i].phend;
+			songdata[i].phend = 0;
 		}
-		m_humdrum_text << "\n";
 	}
 
-	if (debugQ && vdebug) {
-		m_humdrum_text << "!!LINE: " << infile[line] << endl;
-		m_humdrum_text << "!! *v serials = ";
-		for (int ii=0; ii<(int)vserial.size(); ii++) {
-			m_humdrum_text << vserial[ii] << " ";
+	// examine barline information
+	double dur = 0.0;
+	for (i=(int)songdata.size()-1; i>=0; i--) {
+		if (songdata[i].bar == 1) {
+			songdata[i].bardur = dur;
+			dur = songdata[i].duration;
+		} else {
+			dur += songdata[i].duration;
 		}
-		m_humdrum_text << "\n";
 	}
 
-	// check for proper *x syntax /////////////////////////////////
-	for (i=0; i<(int)xserial.size()-1; i++) {
-		if (!xserial[i]) {
-			continue;
+	int barnum = 0;
+	double firstdur = 0.0;
+	if (numerator.size() == 1 && numerator[0] > 0) {
+		// handle single non-frei meter
+		songdata[0].num = numerator[0];
+		songdata[0].denom = denominator[0];
+		dur = 0;
+		double meterdur = 4.0 / denominator[0] * numerator[0];
+		for (i=0; i<(int)songdata.size(); i++) {
+			if (songdata[i].bar) {
+				dur = 0.0;
+			} else {
+				dur += songdata[i].duration;
+				if (fabs(dur - meterdur) < 0.001) {
+					songdata[i].bar = 1;
+					songdata[i].barinterp = 1;
+					dur = 0.0;
+				}
+			}
 		}
-		if (xserial[i] != xserial[i+1]) {
-			if (tempout[i] == "*x") {
-				xserial[i] = 0;
-				tempout[i] = "*";
+
+		// readjust measure beat counts
+		dur = 0.0;
+		for (i=(int)songdata.size()-1; i>=0; i--) {
+			if (songdata[i].bar == 1) {
+				songdata[i].bardur = dur;
+				dur = songdata[i].duration;
+			} else {
+				dur += songdata[i].duration;
 			}
-		} else {
-			i++;
 		}
-	}
+		firstdur = dur;
 
-	if ((tempout.size() == 1) || (xserial.size() == 1)) {
-		// get rid of *x if there is only one spine in output
-		if (xserial[0]) {
-			xserial[0] = 0;
-			tempout[0] = "*";
+		// number the barlines
+		barnum = 0;
+		if (fabs(firstdur - meterdur) < 0.001) {
+			// music for first bar, next bar will be bar 2
+			barnum = 2;
+		} else {
+			barnum = 1;
+			// pickup-measure
 		}
-	} else if ((int)xserial.size() > 1) {
-		// check the last item in the list
-		int index = (int)xserial.size()-1;
-		if (tempout[index] == "*x") {
-			if (xserial[index] != xserial[index-1]) {
-				xserial[index] = 0;
-				tempout[index] = "*";
+		for (i=0; i<(int)songdata.size(); i++) {
+			if (songdata[i].bar == 1) {
+				songdata[i].barnum = barnum++;
 			}
 		}
-	}
 
-	// check for proper *v syntax /////////////////////////////////
-	vector<int> vsplit;
-	vsplit.resize((int)vserial.size());
-	fill(vsplit.begin(), vsplit.end(), 0);
+	} else if (numerator.size() == 1 && numerator[0] == -1) {
+		// handle free meter
 
-	// identify necessary line splits
-	for (i=0; i<(int)vserial.size()-1; i++) {
-		if (!vserial[i]) {
-			continue;
+		// number the barline
+		firstdur = dur;
+		barnum = 1;
+		for (i=0; i<(int)songdata.size(); i++) {
+			if (songdata[i].bar == 1) {
+				songdata[i].barnum = barnum++;
+			}
 		}
-		while ((i<(int)vserial.size()-1) && (vserial[i]==vserial[i+1])) {
-			i++;
+
+	} else {
+		// handle multiple time signatures
+
+		// get the duration of each type of meter:
+		vector<double> meterdurs;
+		meterdurs.resize(numerator.size());
+		for (i=0; i<(int)meterdurs.size(); i++) {
+			meterdurs[i] = 4.0 / denominator[i] * numerator[i];
 		}
-		if ((i<(int)vserial.size()-1) && vserial[i]) {
-			if (vserial.size() > 1) {
-				if (vserial[i+1]) {
-					vsplit[i+1] = 1;
-				}
+
+		// measure beat counts:
+		dur = 0.0;
+		for (i=(int)songdata.size()-1; i>=0; i--) {
+			if (songdata[i].bar == 1) {
+				songdata[i].bardur = dur;
+				dur = songdata[i].duration;
+			} else {
+				dur += songdata[i].duration;
 			}
 		}
-	}
-
-	// remove single *v spines:
+		firstdur = dur;
 
-	for (i=0; i<(int)vsplit.size()-1; i++) {
-		if (vsplit[i] && vsplit[i+1]) {
-			if (tempout[i] == "*v") {
-				tempout[i] = "*";
-				vsplit[i] = 0;
+		// interpret missing barlines
+		int currentmeter = 0;
+		// find first meter
+		for (i=0; i<(int)numerator.size(); i++) {
+			if (fabs(firstdur - meterdurs[i]) < 0.001) {
+				songdata[0].num = numerator[i];
+				songdata[0].denom = denominator[i];
+				currentmeter = i;
 			}
 		}
-	}
+		// now handle the meters in the rest of the music...
+		int fnd = 0;
+		dur = 0;
+		for (i=0; i<(int)songdata.size()-1; i++) {
+			if (songdata[i].bar) {
+				if (songdata[i].bardur != meterdurs[currentmeter]) {
+					// try to find the correct new meter
 
-	if (debugQ) {
-		m_humdrum_text << "!!vsplit array: ";
-		for (i=0; i<(int)vsplit.size(); i++) {
-			m_humdrum_text << " " << vsplit[i];
+					fnd = 0;
+					for (j=0; j<(int)numerator.size(); j++) {
+						if (j == currentmeter) {
+							continue;
+						}
+						if (fabs(songdata[i].bardur - meterdurs[j]) < 0.001) {
+							songdata[i+1].num = numerator[j];
+							songdata[i+1].denom = denominator[j];
+							currentmeter = j;
+							fnd = 1;
+						}
+					}
+					if (!fnd) {
+						for (j=0; j<(int)numerator.size(); j++) {
+							if (j == currentmeter) {
+							   continue;
+							}
+							if (fabs(songdata[i].bardur/2.0 - meterdurs[j]) < 0.001) {
+							   songdata[i+1].num = numerator[j];
+							   songdata[i+1].denom = denominator[j];
+							   currentmeter = j;
+							   fnd = 1;
+							}
+						}
+					}
+				}
+				dur = 0.0;
+			} else {
+				dur += songdata[i].duration;
+				if (fabs(dur - meterdurs[currentmeter]) < 0.001) {
+					songdata[i].bar = 1;
+					songdata[i].barinterp = 1;
+					dur = 0.0;
+				}
+			}
 		}
-		m_humdrum_text << endl;
-	}
 
-	if (vsplit.size() > 0) {
-		if (vsplit[(int)vsplit.size()-1]) {
-			if (tempout[(int)tempout.size()-1] == "*v") {
-				tempout[(int)tempout.size()-1] = "*";
-				vsplit[(int)vsplit.size()-1] = 0;
+		// perhaps sum duration of measures again and search for error here?
+
+		// finally, number the barlines:
+		barnum = 1;
+		for (i=0; i<(int)numerator.size(); i++) {
+			if (fabs(firstdur - meterdurs[i]) < 0.001) {
+				barnum = 2;
+				break;
+			}
+		}
+		for (i=0; i<(int)songdata.size(); i++) {
+			if (songdata[i].bar == 1) {
+				songdata[i].barnum = barnum++;
 			}
 		}
-	}
 
-	int vcount = 0;
-	for (i=0; i<(int)vsplit.size(); i++) {
-		vcount += vsplit[i];
-	}
 
-	if (vcount) {
-		printMultiLines(vsplit, vserial, tempout);
 	}
 
-	int start = 0;
-	for (i=0; i<(int)tempout.size(); i++) {
-		if (tempout[i] != "") {
-			if (start != 0) {
-				m_humdrum_text << "\t";
-			}
-			m_humdrum_text << tempout[i];
-			start++;
-		}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2humold::getMeterInfo --
+//
+
+void Tool_esac2humold::getMeterInfo(string& meter, vector<int>& numerator,
+		vector<int>& denominator) {
+	numerator.clear();
+	denominator.clear();
+	HumRegex hre;
+	hre.replaceDestructive(meter, "", "^\\s+");
+	hre.replaceDestructive(meter, "", "\\s+$");
+	if (hre.search(meter, "^(\\d+)/(\\d+)$")) {
+		numerator.push_back(hre.getMatchInt(1));
+		denominator.push_back(hre.getMatchInt(2));
+		return;
 	}
-	if (start) {
-		m_humdrum_text << '\n';
+	if (hre.search(meter, "^frei$", "i")) {
+		numerator.push_back(-1);
+		denominator.push_back(-1);
+		return;
 	}
+	cerr << "NEED TO DEAL WITH METER: " << meter << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::printMultiLines -- print separate *v lines.
+// Tool_esac2humold::getLineRange -- get the staring line and ending line of a data
+//     field.  Returns -1 if the data field was not found.
 //
 
-void Tool_extract::printMultiLines(vector<int>& vsplit, vector<int>& vserial,
-		vector<string>& tempout) {
-	int i;
-
-	int splitpoint = -1;
-	for (i=0; i<(int)vsplit.size(); i++) {
-		if (vsplit[i]) {
-			splitpoint = i;
+void Tool_esac2humold::getLineRange(vector<string>& song, const string& field,
+		int& start, int& stop) {
+	string searchstring = field;;
+	searchstring += "[";
+	start = stop = -1;
+	for (int i=0; i<(int)song.size(); i++) {
+		auto loc = song[i].find(']');
+		if (song[i].compare(0, searchstring.size(), searchstring) == 0) {
+			start = i;
+			if (loc != string::npos) {
+				stop = i;
+				break;
+			}
+		} else if ((start >= 0) && (loc != string::npos)) {
+			stop = i;
 			break;
 		}
 	}
+}
 
-	if (debugQ) {
-		m_humdrum_text << "!!tempout: ";
-		for (i=0; i<(int)tempout.size(); i++) {
-			m_humdrum_text << tempout[i] << " ";
-		}
-		m_humdrum_text << endl;
-	}
 
-	if (splitpoint == -1) {
-		return;
-	}
 
-	int start = 0;
-	int printv = 0;
-	for (i=0; i<splitpoint; i++) {
-		if (tempout[i] != "") {
-			if (start) {
-				m_humdrum_text << "\t";
-			}
-			m_humdrum_text << tempout[i];
-			start = 1;
-			if (tempout[i] == "*v") {
-				if (printv) {
-					tempout[i] = "";
-				} else {
-					tempout[i] = "*";
-					printv = 1;
-				}
-			} else {
-				tempout[i] = "*";
-			}
-		}
-	}
-
-	for (i=splitpoint; i<(int)vsplit.size(); i++) {
-		if (tempout[i] != "") {
-			if (start) {
-				m_humdrum_text << "\t";
-			}
-			m_humdrum_text << "*";
-		}
-	}
+//////////////////////////////
+//
+// Tool_esac2humold::getNoteList -- get a list of the notes and rests and barlines in
+//    the MEL field.
+//
 
-	if (start) {
-		m_humdrum_text << "\n";
-	}
+bool Tool_esac2humold::getNoteList(vector<string>& song, vector<NoteData>& songdata, double mindur,
+		int tonic) {
+	songdata.resize(0);
+	NoteData tempnote;
+	int melstart = -1;
+	int melstop  = -1;
+	int i, j;
+	int octave      = 0;
+	int degree      = 0;
+	int accidental  = 0;
+	double duration = mindur;
+	int bar    = 0;
+	// int tuplet = 0;
+	int major[8] = {-1, 0, 6, 12, 17, 23, 29, 35};
+	// int oldstate  = -1;
+	int state     = -1;
+	int nextstate = -1;
+	int phend = 0;
+	int phnum = 0;
+	int phstart = 0;
+	int slend = 0;
+	int slstart = 0;
+	int tie = 0;
 
-	vsplit[splitpoint] = 0;
+	getLineRange(song, "MEL", melstart, melstop);
 
-	printMultiLines(vsplit, vserial, tempout);
-}
+	for (i=melstart; i<=melstop; i++) {
+		if (song[i].size() < 4) {
+			cerr << "Error: invalid line in MEL[]: " << song[i] << endl;
+			return false;
+		}
+		j = 4;
+		phstart = 1;
+		phend = 0;
+		// Note Format: (+|-)*[0..7]_*\.*(  )?
+		// ONADB
+		// Order of data: Octave, Note, Accidental, Duration, Barline
 
+		#define STATE_SLSTART -1
+		#define STATE_OCTAVE   0
+		#define STATE_NOTE     1
+		#define STATE_ACC      2
+		#define STATE_DUR      3
+		#define STATE_BAR      4
+		#define STATE_SLEND    5
 
+		while (j < 200 && (j < (int)song[i].size())) {
+			// oldstate = state;
+			switch (song[i][j]) {
+				// Octave information:
+				case '-': octave--; state = STATE_OCTAVE; break;
+				case '+': octave++; state = STATE_OCTAVE; break;
 
-//////////////////////////////
-//
-// Tool_extract::storeToken --
-//
+				// Duration information:
+				case '_': duration *= 2.0; state = STATE_DUR; break;
+				case '.': duration *= 1.5; state = STATE_DUR; break;
 
-void Tool_extract::storeToken(vector<string>& storage, const string& string) {
-	storage.push_back(string);
-}
+				// Accidental information:
+				case 'b': accidental--; state = STATE_ACC;  break;
+				case '#': accidental++; state = STATE_ACC;  break;
 
-void storeToken(vector<string>& storage, int index, const string& string) {
-	storage[index] = string;
-}
+				// Note information:
+				case '0': case '1': case '2': case '3': case '4':
+				case '5': case '6': case '7':
+					degree =  major[song[i][j] - '0'];
+					state = STATE_NOTE;
+					break;
+				case 'O':
+					degree =  major[0];
+					state = STATE_NOTE;
+					break;
 
+				// Barline information:
+				case ' ':
+					state = STATE_BAR;
+					if (song[i][j+1] == ' ') {
+						bar = 1;
+					}
+					break;
 
+				// Other information:
+				case '{': slstart = 1;  state = STATE_SLSTART;  break;
+				case '}': slend   = 1;  state = STATE_SLEND;    break;
+				// case '(': tuplet  = 1;        break;
+				// case ')': tuplet  = 0;        break;
+				case '/':                     break;
+				case ']':                     break;
+//            case '>':                     break;   // unknown marker
+//            case '<':                     break;   //
+				case '^': tie = 1; state = STATE_NOTE; break;
+				default : cerr << "Error: unknown character " << song[i][j]
+							      << " on the line: " << song[i] << endl;
+							 return false;
+			}
+			j++;
+			switch (song[i][j]) {
+				case '-': case '+': nextstate = STATE_OCTAVE; break;
+				case 'O':
+				case '0': case '1': case '2': case '3': case '4':
+				case '5': case '6': case '7': nextstate = STATE_NOTE; break;
+				case 'b': case '#': nextstate = STATE_ACC;    break;
+				case '_': case '.': nextstate = STATE_DUR; break;
+				case '{': nextstate = STATE_SLSTART; break;
+				case '}': nextstate = STATE_SLEND; break;
+				case '^': nextstate = STATE_NOTE; break;
+				case ' ':
+					 if (song[i][j+1] == ' ') nextstate = STATE_BAR;
+					 else if (song[i][j+1] == '/') nextstate = -2;
+					 break;
+				case '\0':
+					phend = 1;
+					break;
+				default: nextstate = -1;
+			}
 
-//////////////////////////////
-//
-// Tool_extract::isInList -- returns true if first number found in list of numbers.
-//     returns the matching index plus one.
-//
+			if (nextstate < state ||
+					((nextstate == STATE_NOTE) && (state == nextstate))) {
+				 tempnote.clear();
+				 if (degree < 0) { // rest
+					 tempnote.pitch = -999;
+				 } else {
+					 tempnote.pitch = degree + 40*(octave + 4) + accidental + tonic;
+				 }
+				 if (tie) {
+					 tempnote.pitch = songdata[(int)songdata.size()-1].pitch;
+					 if (songdata[(int)songdata.size()-1].tieend) {
+						 songdata[(int)songdata.size()-1].tiecont = 1;
+						 songdata[(int)songdata.size()-1].tieend = 0;
+					 } else {
+						 songdata[(int)songdata.size()-1].tiestart = 1;
+					 }
+					 tempnote.tieend = 1;
+				 }
+				 tempnote.duration = duration;
+				 tempnote.phend = phend;
+				 tempnote.bar = bar;
+				 tempnote.phstart = phstart;
+				 tempnote.slstart = slstart;
+				 tempnote.slend = slend;
+				 if (nextstate == -2) {
+					 tempnote.bar = 2;
+					 tempnote.phend = 1;
+				 }
+				 tempnote.phnum = phnum;
 
-int Tool_extract::isInList(int number, vector<int>& listofnum) {
-	int i;
-	for (i=0; i<(int)listofnum.size(); i++) {
-		if (listofnum[i] == number) {
-			return i+1;
+				 songdata.push_back(tempnote);
+				 duration = mindur;
+				 degree = 0;
+				 bar = 0;
+				 tie = 0;
+				 phend = 0;
+				 phstart = 0;
+				 slend = 0;
+				 slstart = 0;
+				 octave = 0;
+				 accidental = 0;
+				 if (nextstate == -2) {
+					 return true;
+				 }
+			}
 		}
+		phnum++;
 	}
-	return 0;
 
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::getTraceData --
+// Tool_esac2humold::printNoteData --
 //
 
-void Tool_extract::getTraceData(vector<int>& startline, vector<vector<int> >& fields,
-		const string& tracefile, HumdrumFile& infile) {
-	char buffer[1024] = {0};
-	HumRegex hre;
-	int linenum;
-	startline.reserve(10000);
-	startline.resize(0);
-	fields.reserve(10000);
-	fields.resize(0);
+void Tool_esac2humold::printNoteData(NoteData& data, int textQ, ostream& out) {
 
-	ifstream input;
-	input.open(tracefile.c_str());
-	if (!input.is_open()) {
-		m_error_text << "Error: cannot open file for reading: " << tracefile << endl;
-		return;
+	if (data.num > 0) {
+		out << "*M" << data.num << "/" << data.denom;
+		if (textQ) {
+			out << "\t*M" << data.num << "/" << data.denom;
+		}
+		out << "\n";
+	}
+	if (data.phstart == 1) {
+		out << "{";
+	}
+	if (data.slstart == 1) {
+		out << "(";
+	}
+	if (data.tiestart == 1) {
+		out << "[";
+	}
+	out << Convert::durationFloatToRecip(data.duration);
+	if (data.pitch < 0) {
+		out << "r";
+	} else {
+		out << Convert::base40ToKern(data.pitch);
+	}
+	if (data.tiecont == 1) {
+		out << "_";
+	}
+	if (data.tieend == 1) {
+		out << "]";
+	}
+	if (data.slend == 1) {
+		out << ")";
+	}
+	if (data.phend == 1) {
+		out << "}";
 	}
 
-	string temps;
-	vector<int> field;
-	vector<int> subfield;
-	vector<int> model;
-
-	input.getline(buffer, 1024);
-	while (!input.eof()) {
-		if (hre.search(buffer, "^\\s*$")) {
-			continue;
+	if (textQ) {
+		out << "\t";
+		if (data.phstart == 1) {
+			out << "{";
 		}
-		if (!hre.search(buffer, "(\\d+)")) {
-			continue;
+		if (data.text == "") {
+			if (data.pitch < 0) {
+				data.text = "%";
+			} else {
+				data.text = "|";
+			}
 		}
-		linenum = hre.getMatchInt(1);
-		linenum--;  // adjust so that line 0 is the first line in the file
-		temps = buffer;
-		hre.replaceDestructive(temps, "", "\\d+");
-		hre.replaceDestructive(temps, "", "[^,\\s\\d\\$\\-].*");  // remove any possible comments
-		hre.replaceDestructive(temps, "", "\\s", "g");
-		if (hre.search(temps, "^\\s*$")) {
-			// no field data to process online
-			continue;
+		if (data.pitch < 0 && (data.text.find('%') == string::npos)) {
+			out << "%";
+		}
+		if (data.text == " *") {
+			if (data.pitch < 0) {
+				data.text = "%*";
+			} else {
+				data.text = "|*";
+			}
+		}
+		if (data.text == "^") {
+			data.text = "|^";
+		}
+		printString(data.text, out);
+		if (data.phend == 1) {
+			out << "}";
 		}
-		startline.push_back(linenum);
-		string ttemp = temps;
-		fillFieldData(field, subfield, model, ttemp, infile);
-		fields.push_back(field);
-		input.getline(buffer, 1024);
 	}
 
+	out << "\n";
+
+	// print barline information
+	if (data.bar == 1) {
+
+		out << "=";
+		if (data.barnum > 0) {
+			out << data.barnum;
+		}
+		if (data.barinterp) {
+			// out << "yy";
+		}
+		if (debugQ) {
+			if (data.bardur > 0.0) {
+				out << "[" << data.bardur << "]";
+			}
+		}
+		if (textQ) {
+			out << "\t";
+			out << "=";
+			if (data.barnum > 0) {
+				out << data.barnum;
+			}
+			if (data.barinterp) {
+				// out << "yy";
+			}
+			if (debugQ) {
+				if (data.bardur > 0.0) {
+					out << "[" << data.bardur << "]";
+				}
+			}
+		}
+
+		out << "\n";
+	} else if (data.bar == 2) {
+		out << "==";
+		if (textQ) {
+			out << "\t==";
+		}
+		out << "\n";
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::extractTrace --
+// Tool_esac2humold::getKeyInfo -- look for a KEY[] entry and extract the data.
+//
+// ggg fix this function
 //
 
-void Tool_extract::extractTrace(HumdrumFile& infile, const string& tracefile) {
-	vector<int> startline;
-	vector<vector<int> > fields;
-	getTraceData(startline, fields, tracefile, infile);
-	int i, j;
-
-	if (debugQ) {
-		for (i=0; i<(int)startline.size(); i++) {
-			m_humdrum_text << "!!TRACE " << startline[i]+1 << ":\t";
-			for (j=0; j<(int)fields[i].size(); j++) {
-				m_humdrum_text << fields[i][j] << " ";
+bool Tool_esac2humold::getKeyInfo(vector<string>& song, string& key, double& mindur,
+		int& tonic, string& meter, ostream& out) {
+	int i;
+	for (i=0; i<(int)song.size(); i++) {
+		if (song[i].compare(0, 4, "KEY[") == 0) {
+			key = song[i][4]; // letter
+			key += song[i][5]; // number
+			key += song[i][6]; // number
+			key += song[i][7]; // number
+			key += song[i][8]; // number
+			if (!isspace(song[i][9])) {
+				key += song[i][9];  // optional letter (sometimes ' or ")
+			}
+			if (!isspace(song[i][10])) {
+				key += song[i][10];  // illegal but possible extra letter
+			}
+			if (song[i][10] != ' ') {
+				out << "!! Warning key field is not complete" << endl;
+				out << "!!Key field: " << song[i] << endl;
 			}
-			m_humdrum_text << "\n";
-		}
-	}
 
+			mindur = (song[i][11] - '0') * 10 + (song[i][12] - '0');
+			mindur = 4.0 / mindur;
 
-	if (startline.size() == 0) {
-		for (i=0; i<infile.getLineCount(); i++) {
-			if (!infile[i].hasSpines()) {
-				m_humdrum_text << infile[i] << '\n';
+			string tonicstr;
+			if (song[i][14] != ' ') {
+				tonicstr[0] = song[i][14];
+				if (tolower(song[i][15]) == 'b') {
+					tonicstr[1] = '-';
+				} else {
+					tonicstr[1] = song[i][15];
+				}
+				tonicstr[2] = '\0';
+			} else {
+				tonicstr = song[i][15];
 			}
-		}
-		return;
-	}
 
-	for (i=0; i<startline[0]; i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << '\n';
-		}
-	}
+			// convert German notation to English for note names
+			// Hopefully all references to B will mean English B-flat.
+			if (tonicstr == "B") {
+				tonicstr = "B-";
+			}
+			if (tonicstr == "H") {
+				tonicstr = "B";
+			}
 
-	int endline;
-	for (j=0; j<(int)startline.size(); j++) {
-		if (j == (int)startline.size()-1) {
-			endline = infile.getLineCount()-1;
-		} else {
-			endline = startline[j+1]-1;
-		}
-		for (i=startline[j]; i<endline; i++) {
-			if (!infile[i].hasSpines()) {
-				m_humdrum_text << infile[i] << '\n';
+			tonic = Convert::kernToBase40(tonicstr);
+			if (tonic <= 0) {
+				cerr << "Error: invalid tonic on line: " << song[i] << endl;
+				return false;
+			}
+			tonic = tonic % 40;
+			meter = song[i].substr(17);
+			if (meter.back() != ']') {
+				cerr << "Error with meter on line: " << song[i] << endl;
+				cerr << "Meter area: " << meter << endl;
+				cerr << "Expected ] as last character but found " << meter.back() << endl;
+				return false;
 			} else {
-				printTraceLine(infile, i, fields[j]);
+				meter.resize((int)meter.size() - 1);
 			}
+			return true;
 		}
 	}
+	cerr << "Error: did not find a KEY field" << endl;
+	return false;
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_extract::printTraceLine --
+// Tool_esac2humold::getFileContents -- read a file into the array.
 //
 
-void Tool_extract::printTraceLine(HumdrumFile& infile, int line, vector<int>& field) {
-	int j;
-	int t;
-	int start = 0;
-	int target;
+bool Tool_esac2humold::getFileContents(vector<string>& array, const string& filename) {
+	ifstream infile(filename.c_str());
+	array.reserve(100);
+	array.resize(0);
 
-	start = 0;
-	for (t=0; t<(int)field.size(); t++) {
-		target = field[t];
-		for (j=0; j<infile[line].getFieldCount(); j++) {
-			if (infile[line].token(j)->getTrack() != target) {
-				continue;
-			}
-			if (start != 0) {
-				m_humdrum_text << '\t';
-			}
-			start = 1;
-			m_humdrum_text << infile.token(line, j);
-		}
+	if (!infile.is_open()) {
+		cerr << "Error: cannot open file: " << filename << endl;
+		return false;
 	}
-	if (start != 0) {
-		m_humdrum_text << endl;
+
+	char holdbuffer[1024] = {0};
+
+	infile.getline(holdbuffer, 256, '\n');
+	while (!infile.eof()) {
+		array.push_back(holdbuffer);
+		infile.getline(holdbuffer, 256, '\n');
 	}
+
+	infile.close();
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::example -- example usage of the sonority program
+// Tool_esac2humold::example --
 //
 
-void Tool_extract::example(void) {
-	m_free_text <<
-	"					                                                          \n"
-	<< endl;
+void Tool_esac2humold::example(void) {
+
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::usage -- gives the usage statement for the sonority program
+// Tool_esac2humold::usage --
 //
 
-void Tool_extract::usage(const string& command) {
-	m_free_text <<
-	"					                                                          \n"
-	<< endl;
+void Tool_esac2humold::usage(const string& command) {
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_extract::initialize --
+// Tool_esac2humold::printBibInfo --
 //
 
-void Tool_extract::initialize(HumdrumFile& infile) {
-	// handle basic options:
-	if (getBoolean("author")) {
-		m_free_text << "Written by Craig Stuart Sapp, "
-			  << "craig@ccrma.stanford.edu, Feb 2008" << endl;
-		return;
-	} else if (getBoolean("version")) {
-		m_free_text << getArg(0) << ", version: Feb 2008" << endl;
-		m_free_text << "compiled: " << __DATE__ << endl;
-		return;
-	} else if (getBoolean("help")) {
-		usage(getCommand().c_str());
-		return;
-	} else if (getBoolean("example")) {
-		example();
-		return;
-	}
-
-	excludeQ    = getBoolean("x");
-	interpQ     = getBoolean("i");
-	interps     = getString("i");
-	kernQ       = getBoolean("k");
-	rkernQ      = getBoolean("K");
-
-	interpstate = 1;
-	if (!interpQ) {
-		interpQ = getBoolean("I");
-		interpstate = 0;
-		interps = getString("I");
-	}
-	if (interps.size() > 0) {
-		if (interps[0] != '*') {
-			// Automatically add ** if not given on exclusive interpretation
-			string tstring = "**";
-			interps = tstring + interps;
-		}
-	}
-
-	removerestQ = getBoolean("no-rest");
-	noEmptyQ    = getBoolean("no-empty");
-	emptyQ      = getBoolean("empty");
-	fieldQ      = getBoolean("f");
-	debugQ      = getBoolean("debug");
-	countQ      = getBoolean("count");
-	traceQ      = getBoolean("trace");
-	tracefile   = getString("trace");
-	reverseQ    = getBoolean("reverse");
-	expandQ     = getBoolean("expand") || getBoolean("E");
-	submodel    = getString("model").c_str()[0];
-	cointerp    = getString("cointerp");
-	comodel     = getString("cospine-model").c_str()[0];
-
-	if (getBoolean("no-editoral-rests")) {
-		editorialInterpretation = "";
-	}
-
-	if (interpQ) {
-		fieldQ = true;
-	}
-
-	if (emptyQ) {
-		fieldQ = true;
-	}
-
-	if (noEmptyQ) {
-		fieldQ = true;
-	}
-
-	if (expandQ) {
-		fieldQ = true;
-		expandInterp = getString("expand-interp");
-	}
+void Tool_esac2humold::printBibInfo(vector<string>& song, ostream& out) {
+	int i, j;
+	char buffer[32] = {0};
+	int start = -1;
+	int stop  = -1;
+	int count = 0;
+	string templine;
 
-	if (!reverseQ) {
-		reverseQ = getBoolean("R");
-		if (reverseQ) {
-			reverseInterp = getString("R");
+	for (i=0; i<(int)song.size(); i++) {
+		if (song[i] == "") {
+			continue;
 		}
-	}
+		if (song[i][0] != ' ') {
+			if (song[i].size() < 4 || song[i][3] != '[') {
+				if (song[i].compare(0, 2, "!!") != 0) {
+					out << "!! " << song[i] << "\n";
+				}
+				continue;
+			}
+			strncpy(buffer, song[i].c_str(), 3);
+			buffer[3] = '\0';
+			if (strcmp(buffer, "MEL") == 0) continue;
+			if (strcmp(buffer, "TXT") == 0) continue;
+			// if (strcmp(buffer, "KEY") == 0) continue;
+			getLineRange(song, buffer, start, stop);
 
-	if (reverseQ) {
-		fieldQ = true;
-	}
+			// don't print CUT field if only one line.  !!!OTL: will contain CUT[]
+			// if (strcmp(buffer, "CUT") == 0 && start == stop) continue;
 
-	if (excludeQ) {
-		fieldstring = getString("x");
-	} else if (fieldQ) {
-		fieldstring = getString("f");
-	} else if (kernQ) {
-		fieldstring = getString("k");
-		fieldQ = true;
-	} else if (rkernQ) {
-		fieldstring = getString("K");
-		fieldQ = true;
-		fieldstring = reverseFieldString(fieldstring, infile.getMaxTrack());
-	}
+			buffer[0] = tolower(buffer[0]);
+			buffer[1] = tolower(buffer[1]);
+			buffer[2] = tolower(buffer[2]);
 
-	spineListQ = getBoolean("spine-list");
-	grepQ      = getBoolean("grep");
-	grepString = getString("grep");
+			count = 1;
+			templine = "";
+			for (j=start; j<=stop; j++) {
+				if (song[j].size() < 4) {
+					continue;
+				}
+				if (stop - start == 0) {
+					templine = song[j].substr(4);
+					auto loc = templine.find(']');
+					if (loc != string::npos) {
+						templine.resize(loc);
+					}
+					if (templine != "") {
+						out << "!!!" << buffer << ": ";
+						printString(templine, out);
+						out << "\n";
+					}
 
-	if (getBoolean("name")) {
-		blankName = getString("name");
-		if (blankName == "") {
-			blankName = "**blank";
-		} else if (blankName.compare(0, 2, "**") != 0) {
-			if (blankName.compare(0, 1, "*") != 0) {
-				blankName = "**" + blankName;
-			} else {
-				blankName = "*" + blankName;
+				} else if (j==start) {
+					out << "!!!" << buffer << count++ << ": ";
+					printString(song[j].substr(4), out);
+					out << "\n";
+				} else if (j==stop) {
+					templine = song[j].substr(4);
+					auto loc = templine.find(']');
+					if (loc != string::npos) {
+						templine.resize(loc);
+					}
+					if (templine != "") {
+						out << "!!!" << buffer << count++ << ": ";
+						printString(templine, out);
+						out << "\n";
+					}
+				} else {
+					out << "!!!" << buffer << count++ << ": ";
+					printString(&(song[j][4]), out);
+					out << "\n";
+				}
 			}
 		}
-		if (blankName == "**kern") {
-			addRestsQ = true;
-		}
 	}
-
 }
 
 
+
 //////////////////////////////
 //
-// Tool_extract::reverseFieldString --  No dollar expansion for now.
+// Tool_esac2humold::printString -- print characters in string.
 //
 
-string Tool_extract::reverseFieldString(const string& input, int maxval) {
-	string output;
-	string number;
-	for (int i=0; i<(int)input.size(); i++) {
-		if (isdigit(input[i])) {
-			number += input[i];
-			continue;
-		} else {
-			if (!number.empty()) {
-				int value = (int)strtol(number.c_str(), NULL, 10);
-				value = maxval - value + 1;
-				output += to_string(value);
-				output += input[i];
-				number.clear();
-			}
-		}
-	}
-	if (!number.empty()) {
-		int value = (int)strtol(number.c_str(), NULL, 10);
-		value = maxval - value + 1;
-		output += to_string(value);
+void Tool_esac2humold::printString(const string& string, ostream& out) {
+	for (int i=0; i<(int)string.size(); i++) {
+		printChar(string[i], out);
 	}
-	return output;
 }
 
 
 
-//////////////////////////////
+
+
+/////////////////////////////////
 //
-// Tool_fb::Tool_fb -- Set the recognized options for the tool.
+// Tool_extract::Tool_extract -- Set the recognized options for the tool.
 //
 
-Tool_fb::Tool_fb(void) {
-	define("c|compound=b",                               "output reasonable figured bass numbers within octave");
-	define("a|accidentals|accid|acc=b",                  "display accidentals in front of the numbers");
-	define("b|base|base-track=i:1",                      "number of the base kern track (compare with -k)");
-	define("i|intervallsatz=b",                          "display numbers under their voice instead of under the base staff");
-	define("o|sort|order=b",                             "sort figured bass numbers by size");
-	define("l|lowest=b",                                 "use lowest note as base note");
-	define("n|normalize=b",                              "remove number 8 and doubled numbers; adds -co");
-	define("r|reduce|abbreviate|abbr=b",                 "use abbreviated figures; adds -nco");
-	define("t|ties=b",                                   "hide numbers without attack or changing base (needs -i)");
-	define("f|figuredbass=b",                            "shortcut for -acorn3");
-	define("3|hide-three=b",                             "hide number 3 if it has an accidental");
-	define("m|negative=b",                               "show negative numbers");
-	define("above=b",                                    "show numbers above the staff (**fba)");
-	define("rate=s:",                                    "rate to display the numbers (use a **recip value, e.g. 4, 4.)");
-	define("k|kern-tracks=s",                            "process only the specified kern spines");
-	define("s|spine-tracks|spine|spines|track|tracks=s", "Process only the specified spines");
-	define("hint=b",                                     "determine harmonic intervals with interval quality");
+Tool_extract::Tool_extract(void) {
+	define("P|F|S|x|exclude=s:",        "remove listed spines from output");
+	define("i=s:",                      "exclusive interpretation list to extract from input");
+	define("I=s:",                      "exclusive interpretation exclusion list");
+	define("f|p|s|field|path|spine=s:", "for extraction of particular spines");
+	define("C|count=b",                 "print a count of the number of spines in file");
+	define("c|cointerp=s:**kern",       "exclusive interpretation for cospines");
+	define("g|grep=s:",                 "extract spines which match a given regex.");
+	define("r|reverse=b",               "reverse order of spines by **kern group");
+	define("R=s:**kern",                "reverse order of spine by exinterp group");
+	define("t|trace=s:",                "use a trace file to extract data");
+	define("e|expand=b",                "expand spines with subspines");
+	define("k|kern=s",                  "extract by kern spine group");
+	define("K|reverse-kern=s",          "extract by kern spine group top to bottom numbering");
+	define("E|expand-interp=s:",        "expand subspines limited to exinterp");
+	define("m|model|method=s:d",        "method for extracting secondary spines");
+	define("M|cospine-model=s:d",       "method for extracting cospines");
+	define("Y|no-editoral-rests=b",     "do not display yy marks on interpreted rests");
+	define("n|name|b|blank=s:**blank",  "name if exinterp added with 0");
+	define("no-empty|no-empties=b",     "suppress spines with only null data tokens");
+	define("empty|empties=b",           "only keep spines with only null data tokens");
+	define("spine-list=b",              "show spine list and then exit");
+	define("no-rest|no-rests=b",        "remove **kern spines containing only rests (and their co-spines)");
+
+	define("debug=b",                   "print debugging information");
+	define("author=b",                  "author of the program");
+	define("version=b",                 "compilation info");
+	define("example=b",                 "example usages");
+	define("h|help=b",                  "short description");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_fb::run -- Do the main work of the tool.
+// Tool_extract::run -- Primary interfaces to the tool.
 //
 
-bool Tool_fb::run(HumdrumFileSet &infiles) {
+bool Tool_extract::run(HumdrumFileSet& infiles) {
 	bool status = true;
-	for (int i = 0; i < infiles.getCount(); i++) {
+	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
 	}
 	return status;
 }
 
-bool Tool_fb::run(const string &indata, ostream &out) {
+
+bool Tool_extract::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -82112,7 +82536,8 @@ bool Tool_fb::run(const string &indata, ostream &out) {
 	return status;
 }
 
-bool Tool_fb::run(HumdrumFile &infile, ostream &out) {
+
+bool Tool_extract::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -82122,9 +82547,15 @@ bool Tool_fb::run(HumdrumFile &infile, ostream &out) {
 	return status;
 }
 
-bool Tool_fb::run(HumdrumFile &infile) {
-	initialize();
+//
+// In-place processing of file:
+//
+
+bool Tool_extract::run(HumdrumFile& infile) {
+	initialize(infile);
 	processFile(infile);
+	// Re-load the text for each line from their tokens.
+	// infile.createLinesFromTokens();
 	return true;
 }
 
@@ -82132,55 +82563,73 @@ bool Tool_fb::run(HumdrumFile &infile) {
 
 //////////////////////////////
 //
-// Tool_fb::initialize --
+// Tool_extract::processFile --
 //
 
-void Tool_fb::initialize(void) {
-	m_compoundQ      = getBoolean("compound");
-	m_accidentalsQ   = getBoolean("accidentals");
-	m_baseTrackQ     = getInteger("base");
-	m_intervallsatzQ = getBoolean("intervallsatz");
-	m_sortQ          = getBoolean("sort");
-	m_lowestQ        = getBoolean("lowest");
-	m_normalizeQ     = getBoolean("normalize");
-	m_reduceQ        = getBoolean("reduce");
-	m_attackQ        = getBoolean("ties");
-	m_figuredbassQ   = getBoolean("figuredbass");
-	m_hideThreeQ     = getBoolean("hide-three");
-	m_showNegativeQ  = getBoolean("negative");
-	m_aboveQ         = getBoolean("above");
-	m_rateQ          = getString("rate");
-	m_hintQ          = getBoolean("hint");
-
-	if (getBoolean("spine-tracks")) {
-		m_spineTracks = getString("spine-tracks");
-	} else if (getBoolean("kern-tracks")) {
-		m_kernTracks = getString("kern-tracks");
+void Tool_extract::processFile(HumdrumFile& infile) {
+	if (countQ) {
+		m_free_text << infile.getMaxTrack() << endl;
+		return;
 	}
-
-	if (m_normalizeQ) {
-		m_compoundQ = true;
-		m_sortQ = true;
+	if (expandQ) {
+		expandSpines(field, subfield, model, infile, expandInterp);
+	} else if (interpQ) {
+		getInterpretationFields(field, subfield, model, infile, interps,
+				interpstate);
+	} else if (reverseQ) {
+		reverseSpines(field, subfield, model, infile, reverseInterp);
+	} else if (removerestQ) {
+		fillFieldDataByNoRest(field, subfield, model, grepString, infile,
+			interpstate);
+	} else if (grepQ) {
+		fillFieldDataByGrep(field, subfield, model, grepString, infile,
+			interpstate);
+	} else if (emptyQ) {
+		fillFieldDataByEmpty(field, subfield, model, infile, interpstate);
+	} else if (noEmptyQ) {
+		fillFieldDataByNoEmpty(field, subfield, model, infile, interpstate);
+	} else if (fieldQ || excludeQ) {
+		fillFieldData(field, subfield, model, fieldstring, infile);
 	}
 
-	if (m_reduceQ) {
-		m_normalizeQ = true;
-		m_compoundQ = true;
-		m_sortQ = true;
+	if (spineListQ) {
+		m_free_text << "-s ";
+		for (int i=0; i<(int)field.size(); i++) {
+			m_free_text << field[i];
+			if (i < (int)field.size() - 1) {
+				m_free_text << ",";
+			}
+		}
+		m_free_text << endl;
+		return;
 	}
 
-	if (m_figuredbassQ) {
-		m_reduceQ = true;
-		m_normalizeQ = true;
-		m_compoundQ = true;
-		m_sortQ = true;
-		m_accidentalsQ = true;
-		m_hideThreeQ = true;
+	if (debugQ && !traceQ) {
+		m_free_text << "!! Field Expansion List:";
+		for (int j=0; j<(int)field.size(); j++) {
+			m_free_text << " " << field[j];
+			if (subfield[j]) {
+				m_free_text << (char)subfield[j];
+			}
+			if (model[j]) {
+				m_free_text << (char)model[j];
+			}
+		}
+		m_free_text << endl;
 	}
 
-	if (m_hintQ) {
-		m_showNegativeQ = true;
-		// m_lowestQ = true;
+	// preserve SEGMENT filename if present (now printed in main())
+	// infile.printNonemptySegmentLabel(m_humdrum_text);
+
+	// analyze the input file according to command-line options
+	if (fieldQ || grepQ || removerestQ) {
+		extractFields(infile, field, subfield, model);
+	} else if (excludeQ) {
+		excludeFields(infile, field, subfield, model);
+	} else if (traceQ) {
+		extractTrace(infile, tracefile);
+	} else {
+		m_humdrum_text << infile;
 	}
 }
 
@@ -82188,1852 +82637,1872 @@ void Tool_fb::initialize(void) {
 
 //////////////////////////////
 //
-// Tool_fb::processFile --
+// Tool_extract::getNullDataTracks --
 //
 
-void Tool_fb::processFile(HumdrumFile& infile) {
+vector<int> Tool_extract::getNullDataTracks(HumdrumFile& infile) {
+	vector<int> output(infile.getMaxTrack() + 1, 1);
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			int track = token->getTrack();
+			if (!output[track]) {
+				continue;
+			}
+			if (!token->isNull()) {
+				output[track] = 0;
+			}
+		}
+		// maybe exit here if all tracks are non-null
+	}
 
-	NoteGrid grid(infile);
+	return output;
+}
 
-	vector<FiguredBassNumber*> numbers;
 
-	vector<HTp> kernspines = infile.getKernSpineStartList();
 
-	int maxTrack = infile.getMaxTrack();
+//////////////////////////////
+//
+// Tool_extract::fillFieldDataByEmpty -- Only keep the spines which contain only
+//    null data tokens.
+//
 
-	// Do nothing if base track not withing kern track range
-	if (m_baseTrackQ < 1 || m_baseTrackQ > maxTrack) {
-		return;
-	}
+void Tool_extract::fillFieldDataByEmpty(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, HumdrumFile& infile, int negate) {
 
-	m_selectedKernSpines.resize(maxTrack + 1); // +1 is needed since track=0 is not used
-	// By default, process all tracks:
-	fill(m_selectedKernSpines.begin(), m_selectedKernSpines.end(), true);
-	// Otherwise, select which **kern track, or spine tracks to process selectively:
+	field.reserve(infile.getMaxTrack()+1);
+	subfield.reserve(infile.getMaxTrack()+1);
+	model.reserve(infile.getMaxTrack()+1);
+	field.resize(0);
+	subfield.resize(0);
+	model.resize(0);
+	vector<int> nullTrack = getNullDataTracks(infile);
 
-	// Calculate which input spines to process based on -s or -k option:
-	if (!m_kernTracks.empty()) {
-		vector<int> ktracks = Convert::extractIntegerList(m_kernTracks, maxTrack);
-		fill(m_selectedKernSpines.begin(), m_selectedKernSpines.end(), false);
-		for (int i=0; i<(int)ktracks.size(); i++) {
-			int index = ktracks[i] - 1;
-			if ((index < 0) || (index >= (int)kernspines.size())) {
-				continue;
+	int zero = 0;
+	for (int i=1; i<(int)nullTrack.size(); i++) {
+		if (negate) {
+			if (!nullTrack[i]) {
+				field.push_back(i);
+				subfield.push_back(zero);
+				model.push_back(zero);
+			}
+		} else {
+			if (nullTrack[i]) {
+				field.push_back(i);
+				subfield.push_back(zero);
+				model.push_back(zero);
 			}
-			int track = kernspines.at(ktracks[i] - 1)->getTrack();
-			m_selectedKernSpines.at(track) = true;
 		}
-	} else if (!m_spineTracks.empty()) {
-		infile.makeBooleanTrackList(m_selectedKernSpines, m_spineTracks);
 	}
 
-	vector<vector<int>> lastNumbers = {};
-	lastNumbers.resize((int)grid.getVoiceCount());
-	vector<vector<int>> currentNumbers = {};
+}
 
-	// Interate through the NoteGrid and fill the numbers vector with
-	// all generated FiguredBassNumbers
-	for (int i=0; i<(int)grid.getSliceCount(); i++) {
-		currentNumbers.clear();
-		currentNumbers.resize((int)grid.getVoiceCount());
 
-		// Reset usedBaseKernTrack
-		int usedBaseKernTrack = m_baseTrackQ;
 
-		// Overwrite usedBaseKernTrack with the lowest voice index of the lowest pitched note
-		if (m_lowestQ) {
-			int lowestNotePitch = 99999;
-			for (int k=0; k<(int)grid.getVoiceCount(); k++) {
-				NoteCell* checkCell = grid.cell(k, i);
-				HTp currentToken = checkCell->getToken();
-				int initialTokenTrack = currentToken->getTrack();
+//////////////////////////////
+//
+// Tool_extract::fillFieldDataByNoEmpty -- Only keep spines which are not all
+//   null data tokens.
+//
 
-				// Handle spine splits
-				do {
-					HTp resolvedToken = currentToken->resolveNull();
-					int lowest = getLowestBase40Pitch(resolvedToken->getBase40Pitches());
+void Tool_extract::fillFieldDataByNoEmpty(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, HumdrumFile& infile, int negate) {
 
-					if (abs(lowest) < lowestNotePitch) {
-						lowestNotePitch = abs(lowest);
-						usedBaseKernTrack = k + 1;
-					}
+	field.reserve(infile.getMaxTrack()+1);
+	subfield.reserve(infile.getMaxTrack()+1);
+	model.reserve(infile.getMaxTrack()+1);
+	field.resize(0);
+	subfield.resize(0);
+	model.resize(0);
+	vector<int> nullTrack = getNullDataTracks(infile);
+	for (int i=1; i<(int)nullTrack.size(); i++) {
+		nullTrack[i] = !nullTrack[i];
+	}
 
-					HTp nextToken = currentToken->getNextField();
-					if (nextToken && (initialTokenTrack == nextToken->getTrack())) {
-						currentToken = nextToken;
-					} else {
-						// Break loop if nextToken is not the same track as initialTokenTrack
-						break;
-					}
-				} while (currentToken);
+	int zero = 0;
+	for (int i=1; i<(int)nullTrack.size(); i++) {
+		if (negate) {
+			if (!nullTrack[i]) {
+				field.push_back(i);
+				subfield.push_back(zero);
+				model.push_back(zero);
+			}
+		} else {
+			if (nullTrack[i]) {
+				field.push_back(i);
+				subfield.push_back(zero);
+				model.push_back(zero);
 			}
 		}
+	}
+}
 
-		NoteCell* baseCell = grid.cell(usedBaseKernTrack - 1, i);
-
-		// Ignore grace notes
-		if (baseCell->getToken()->getOwner()->getDuration() == 0) {
-			continue;
-		}
 
-		string keySignature = getKeySignature(infile, baseCell->getLineIndex());
-
-		// Hide numbers if they do not match rhythmic position of --rate
-		if (!m_rateQ.empty()) {
-			// Get time signatures
-			vector<pair<int, HumNum>> timeSigs;
-			infile.getTimeSigs(timeSigs, baseCell->getToken()->getTrack());
-			// Ignore numbers if they don't fit
-			if (hideNumbersForTokenLine(baseCell->getToken(), timeSigs[baseCell->getLineIndex()])) {
-				continue;
-			}
-		}
 
+//////////////////////////////
+//
+// Tool_extract::fillFieldDataByNoRest --  Find the spines which
+//    contain only rests and remove them.  Also remove cospines (non-kern spines
+//    to the right of the kern spine containing only rests).  If there are
+//    *part# interpretations in the data, then any spine which is all rests
+//    will not be removed if there is another **kern spine with the same
+//    part number if it is also not all rests.
+//
 
-		HTp currentToken = baseCell->getToken();
-		int initialTokenTrack = baseCell->getToken()->getTrack();
-		int lowestBaseNoteBase40Pitch = 9999;
+void Tool_extract::fillFieldDataByNoRest(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, const string& searchstring, HumdrumFile& infile,
+		int state) {
 
-		// Handle spine splits
-		do {
-			HTp resolvedToken = currentToken->resolveNull();
-			int lowest = getLowestBase40Pitch(resolvedToken->getBase40Pitches());
+	field.clear();
+	subfield.clear();
+	model.clear();
 
-			// Ignore if base is a rest or silent note
-			if ((lowest != 0) && (lowest != -1000) && (lowest != -2000)) {
-				if(abs(lowest) < lowestBaseNoteBase40Pitch) {
-					lowestBaseNoteBase40Pitch = abs(lowest);
-				}
-			}
 
-			HTp nextToken = currentToken->getNextField();
-			if (nextToken && (initialTokenTrack == nextToken->getTrack())) {
-				currentToken = nextToken;
-			} else {
-				// Break loop if nextToken is not the same track as initialTokenTrack
-				break;
-			}
-		} while (currentToken);
+	// Check every **kern spine for any notes.  If there is a note
+	// then the tracks variable for that spine will be marked
+	// as non-zero.
+	vector<int> tracks(infile.getMaxTrack() + 1, 0);
+	int track;
+	int partline = 0;
+	bool dataQ = false;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if ((!partline) && (!dataQ) && infile[i].hasSpines()) {
 
-		// Ignore if base is a rest or silent note
-		if ((lowestBaseNoteBase40Pitch == 0) || (lowestBaseNoteBase40Pitch == -1000) || (lowestBaseNoteBase40Pitch == -2000) || (lowestBaseNoteBase40Pitch == 9999)) {
+		}
+		if (!infile[i].isData()) {
 			continue;
 		}
-
-		// Interate through each voice
-		for (int j=0; j<(int)grid.getVoiceCount(); j++) {
-			NoteCell* targetCell = grid.cell(j, i);
-
-			// Ignore voice if track is not active by --kern-tracks or --spine-tracks
-			if (m_selectedKernSpines.at(targetCell->getToken()->getTrack()) == false) {
+		dataQ = true;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
 				continue;
 			}
+			if (token->isNull()) {
+				continue;
+			}
+			if (token->isRest()) {
+				continue;
+			}
+			track = token->getTrack();
+			tracks[track] = 1;
+		}
+	}
 
-			HTp currentToken = targetCell->getToken();
-			int initialTokenTrack = targetCell->getToken()->getTrack();
-			vector<FiguredBassNumber*> chordNumbers = {};
-
-			// Handle spine splits
-			do {
-				HTp resolvedToken = currentToken->resolveNull();
-				for (int subtokenBase40: resolvedToken->getBase40Pitches()) {
-
-					// Ignore if target is a rest or silent note
-					if ((subtokenBase40 == 0) || (subtokenBase40 == -1000) || (subtokenBase40 == -2000)) {
-						continue;
-					}
-
-					// Ignore if same pitch as base voice
-					if ((abs(lowestBaseNoteBase40Pitch) == abs(subtokenBase40)) && (baseCell->getToken()->getTrack() == initialTokenTrack)) {
-						continue;
-					}
-
-					// Create FiguredBassNumber
-					FiguredBassNumber* number = createFiguredBassNumber(abs(lowestBaseNoteBase40Pitch), abs(subtokenBase40), targetCell->getVoiceIndex(), targetCell->getLineIndex(), targetCell->isAttack(), keySignature);
-
-					currentNumbers[j].push_back(number->m_number);
-					chordNumbers.push_back(number);
+	// Go back and mark any empty spines as non-empty if they
+	// are in a part that contains multiple staves. I.e., only
+	// delete a staff if all staves for the part are empty.
+	// There should be a single *part# line at the start of the
+	// score.
+	if (partline > 0) {
+		vector<HTp> kerns;
+		for (int i=0; i<infile[partline].getFieldCount(); i++) {
+			HTp token = infile.token(partline, i);
+			if (!token->isKern()) {
+				continue;
+			}
+			kerns.push_back(token);
+		}
+		for (int i=0; i<(int)kerns.size(); i++) {
+			for (int j=i+1; j<(int)kerns.size(); j++) {
+				if (*kerns[i] != *kerns[j]) {
+					continue;
 				}
-
-				HTp nextToken = currentToken->getNextField();
-				if (nextToken && (initialTokenTrack == nextToken->getTrack())) {
-						currentToken = nextToken;
-				} else {
-					// Break loop if nextToken is not the same track as initialTokenTrack
-					break;
+				if (kerns[i]->find("*part") == string::npos) {
+					continue;
 				}
-			} while (currentToken);
-
-			// Sort chord numbers by size
-			sort(chordNumbers.begin(), chordNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
-				return a->m_number > b->m_number;
-			});
-
-			// Then add to numbers vector
-			for (FiguredBassNumber*  num: chordNumbers) {
-				if (lastNumbers[j].size() != 0) {
-					// If a number belongs to a sustained note but the base note did change
-					// the new numbers need to be displayable
-					num->m_baseOfSustainedNoteDidChange = !num->m_isAttack && std::find(lastNumbers[j].begin(), lastNumbers[j].end(), num->m_number) == lastNumbers[j].end();
+				int track1 = kerns[i]->getTrack();
+				int track2 = kerns[j]->getTrack();
+				int state1 = tracks[track1];
+				int state2 = tracks[track2];
+				if ((state1 && !state2) || (state2 && !state1)) {
+					// Prevent empty staff from being removed
+					// from a multi-staff part:
+					tracks[track1] = 1;
+					tracks[track2] = 1;
 				}
-				numbers.push_back(num);
 			}
 		}
-
-		// Set current numbers as the new last numbers
-		lastNumbers = currentNumbers;
 	}
 
-	string exinterp = m_aboveQ ? "**fba" : "**fb";
 
-	if (m_hintQ) {
-		exinterp = "**hint";
+	// deal with co-spines
+	vector<HTp> sstarts;
+	infile.getSpineStartList(sstarts);
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (!sstarts[i]->isKern()) {
+			track = sstarts[i]->getTrack();
+			tracks[track] = 1;
+		}
 	}
 
-	if (m_intervallsatzQ) {
-		// Create **fb spine for each voice
-		for (int voiceIndex = 0; voiceIndex < grid.getVoiceCount(); voiceIndex++) {
-			vector<string> trackData = getTrackDataForVoice(voiceIndex, numbers, infile.getLineCount());
-			if (voiceIndex + 1 < grid.getVoiceCount()) {
-				int trackIndex = kernspines[voiceIndex + 1]->getTrack();
-				infile.insertDataSpineBefore(trackIndex, trackData, ".", exinterp);
-			} else {
-				infile.appendDataSpine(trackData, ".", exinterp);
+	// remove co-spines attached to removed kern spines
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (!sstarts[i]->isKern()) {
+			continue;
+		}
+		if (tracks[sstarts[i]->getTrack()] != 0) {
+			continue;
+		}
+		for (int j=i+1; j<(int)sstarts.size(); j++) {
+			if (sstarts[j]->isKern()) {
+				break;
 			}
+			track = sstarts[j]->getTrack();
+			tracks[track] = 0;
 		}
-	} else {
-		// Create **fb spine and bind it to the base voice
-		vector<string> trackData = getTrackData(numbers, infile.getLineCount());
-		if (m_baseTrackQ < grid.getVoiceCount()) {
-			int trackIndex = kernspines[m_baseTrackQ]->getTrack();
-			infile.insertDataSpineBefore(trackIndex, trackData, ".", exinterp);
-		} else {
-			infile.appendDataSpine(trackData, ".", exinterp);
+	}
+
+	int zero = 0;
+	for (int i=1; i<(int)tracks.size(); i++) {
+		if (state != 0) {
+			tracks[i] = !tracks[i];
+		}
+		if (tracks[i]) {
+			field.push_back(i);
+			subfield.push_back(zero);
+			model.push_back(zero);
 		}
 	}
 
-	// Enables usage in verovio (`!!!filter: fb`)
-	m_humdrum_text << infile;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::hideNumbersForTokenLine -- Checks if rhythmic position of line should display numbers
+// Tool_extract::fillFieldDataByGrep --
 //
 
-bool Tool_fb::hideNumbersForTokenLine(HTp token, pair<int, HumNum> timeSig) {
-	// Get note duration from --rate option
-	HumNum rateDuration = Convert::recipToDuration(m_rateQ);
-	if (rateDuration.toFloat() != 0) {
-		double timeSigBarDuration = timeSig.first * Convert::recipToDuration(to_string(timeSig.second.getInteger())).toFloat();
-		double durationFromBarline = token->getDurationFromBarline().toFloat();
-		// Handle upbeats
-		if (token->getBarlineDuration().toFloat() < timeSigBarDuration) {
-			// Fix durationFromBarline when current bar duration is shorter than
-			// the bar duration of the time signature
-			durationFromBarline = timeSigBarDuration - token->getDurationToBarline().toFloat();
-		}
-		// Checks if rhythmic position is divisible by rateDuration
-		return fmod(durationFromBarline, rateDuration.toFloat()) != 0;
-	}
-	return false;
-}
-
-
+void Tool_extract::fillFieldDataByGrep(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, const string& searchstring, HumdrumFile& infile,
+		int state) {
 
-//////////////////////////////
-//
-// Tool_fb::getTrackData -- Create **fb spine data with formatted numbers for all voices
-//
+	field.reserve(infile.getMaxTrack()+1);
+	subfield.reserve(infile.getMaxTrack()+1);
+	model.reserve(infile.getMaxTrack()+1);
+	field.resize(0);
+	subfield.resize(0);
+	model.resize(0);
 
-vector<string> Tool_fb::getTrackData(const vector<FiguredBassNumber*>& numbers, int lineCount) {
-	vector<string> trackData;
-	trackData.resize(lineCount);
+	vector<int> tracks;
+	tracks.resize(infile.getMaxTrack()+1);
+	fill(tracks.begin(), tracks.end(), 0);
+	HumRegex hre;
+	int track;
 
-	for (int i = 0; i < lineCount; i++) {
-		vector<FiguredBassNumber*> sliceNumbers = filterFiguredBassNumbersForLine(numbers, i);
-		if (sliceNumbers.size() > 0) {
-			trackData[i] = formatFiguredBassNumbers(sliceNumbers);
+	int i, j;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			continue;
+		}
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			if (hre.search(infile.token(i, j), searchstring, "")) {
+				track = infile[i].token(j)->getTrack();
+				tracks[track] = 1;
+			}
 		}
 	}
 
-	return trackData;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_fb::getTrackDataForVoice -- Create **fb spine data with formatted numbers for passed voiceIndex
-//
-
-vector<string> Tool_fb::getTrackDataForVoice(int voiceIndex, const vector<FiguredBassNumber*>& numbers, int lineCount) {
-	vector<string> trackData;
-	trackData.resize(lineCount);
-
-	for (int i = 0; i < lineCount; i++) {
-		vector<FiguredBassNumber*> sliceNumbers = filterFiguredBassNumbersForLineAndVoice(numbers, i, voiceIndex);
-		if (sliceNumbers.size() > 0) {
-			trackData[i] = formatFiguredBassNumbers(sliceNumbers);
+	int zero = 0;
+	for (i=1; i<(int)tracks.size(); i++) {
+		if (state != 0) {
+			tracks[i] = !tracks[i];
+		}
+		if (tracks[i]) {
+			field.push_back(i);
+			subfield.push_back(zero);
+			model.push_back(zero);
 		}
 	}
-
-	return trackData;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::createFiguredBassNumber -- Create FiguredBassNumber from a NoteCell.
-//    The figured bass number (num) is calculated with a base and target NoteCell
-//    as well as a passed key signature.
+// Tool_extract::getInterpretationFields --
 //
 
-FiguredBassNumber* Tool_fb::createFiguredBassNumber(int basePitchBase40, int targetPitchBase40, int voiceIndex, int lineIndex, bool isAttack, string keySignature) {
-
-	// Calculate figured bass number
-	int baseDiatonicPitch   = Convert::base40ToDiatonic(basePitchBase40);
-	int targetDiatonicPitch = Convert::base40ToDiatonic(targetPitchBase40);
-	int diff        = abs(targetDiatonicPitch) - abs(baseDiatonicPitch);
-	int num;
+void Tool_extract::getInterpretationFields(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, HumdrumFile& infile, string& interps, int state) {
+	vector<string> sstrings; // search strings
+	sstrings.reserve(100);
+	sstrings.resize(0);
 
-	if ((baseDiatonicPitch == 0) || (targetDiatonicPitch == 0)) {
-		num = 0;
-	} else if (diff == 0) {
-		num = 1;
-	} else if (diff > 0) {
-		num = diff + 1;
-	} else {
-		num = diff - 1;
-	}
+	int i, j, k;
+	string buffer;
+	buffer = interps;
 
-	// Transform key signature to lower case
-	transform(keySignature.begin(), keySignature.end(), keySignature.begin(), [](unsigned char c) {
-		return tolower(c);
-	});
+	HumRegex hre;
+	hre.replaceDestructive(buffer, "", "\\s+", "g");
 
-	char targetPitchName = Convert::kernToDiatonicLC(Convert::base40ToKern(targetPitchBase40));
-	int targetAccidNr = Convert::base40ToAccidental(targetPitchBase40);
-	string targetAccid;
-	for (int i=0; i<abs(targetAccidNr); i++) {
-		targetAccid += (targetAccidNr < 0 ? "-" : "#");
+	int start = 0;
+	while (hre.search(buffer, start, "^([^,]+)")) {
+		sstrings.push_back(hre.getMatch(1));
+		start = hre.getMatchEndIndex(1);
 	}
 
-	char basePitchName = Convert::kernToDiatonicLC(Convert::base40ToKern(basePitchBase40));
-	int baseAccidNr = Convert::base40ToAccidental(basePitchBase40);
-	string baseAccid;
-	for (int i=0; i<abs(baseAccidNr); i++) {
-		baseAccid += (baseAccidNr < 0 ? "-" : "#");
+	if (debugQ) {
+		m_humdrum_text << "!! Interpretation strings to search for: " << endl;
+		for (i=0; i<(int)sstrings.size(); i++) {
+			m_humdrum_text << "!!\t" << sstrings[i] << endl;
+		}
 	}
 
-	string accid = targetAccid;
-	bool showAccid = false;
+	vector<int> tracks;
+	tracks.resize(infile.getMaxTrack()+1);
+	fill(tracks.begin(), tracks.end(), 0);
 
-	// Show accidentals when they are not included in the key signature
-	if ((targetAccidNr != 0) && (keySignature.find(targetPitchName + targetAccid) == std::string::npos)) {
-		showAccid = true;
+	// Algorithm below could be made more efficient by
+	// not searching the entire file...
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			for (k=0; k<(int)sstrings.size(); k++) {
+				if (sstrings[k] == *infile.token(i, j)) {
+					tracks[infile[i].token(j)->getTrack()] = 1;
+				}
+			}
+		}
 	}
 
-	// Show natural accidentals when they are alterations of the key signature
-	if ((targetAccidNr == 0) && (keySignature.find(targetPitchName + targetAccid) != std::string::npos)) {
-		accid = "n";
-		showAccid = true;
-	}
+	field.reserve(tracks.size());
+	subfield.reserve(tracks.size());
+	model.reserve(tracks.size());
 
-	// Show accidentlas when pitch class of base and target is equal but alteration is different
-	if (basePitchName == targetPitchName) {
-		if (baseAccidNr == targetAccidNr) {
-			showAccid = false;
-		} else {
-			accid = (targetAccidNr == 0) ? "n" : targetAccid;
-			showAccid = true;
+	field.resize(0);
+	subfield.resize(0);
+	model.resize(0);
+
+	int zero = 0;
+	for (i=1; i<(int)tracks.size(); i++) {
+		if (state == 0) {
+			tracks[i] = !tracks[i];
+		}
+		if (tracks[i]) {
+			field.push_back(i);
+			subfield.push_back(zero);
+			model.push_back(zero);
 		}
 	}
 
-	string intervalQuality = getIntervalQuality(basePitchBase40, targetPitchBase40);
-
-	FiguredBassNumber* number = new FiguredBassNumber(num, accid, showAccid, voiceIndex, lineIndex, isAttack, m_intervallsatzQ, intervalQuality, m_hintQ);
-
-	return number;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::filterNegativeNumbers -- Hide negative numbers if m_showNegativeQ if not true
+// Tool_extract::expandSpines --
 //
 
-vector<FiguredBassNumber*> Tool_fb::filterNegativeNumbers(vector<FiguredBassNumber*> numbers) {
+void Tool_extract::expandSpines(vector<int>& field, vector<int>& subfield, vector<int>& model,
+		HumdrumFile& infile, string& interp) {
 
-	vector<FiguredBassNumber*> filteredNumbers;
+	vector<int> splits;
+	splits.resize(infile.getMaxTrack()+1);
+	fill(splits.begin(), splits.end(), 0);
 
-	bool mQ = m_showNegativeQ;
-	copy_if(numbers.begin(), numbers.end(), back_inserter(filteredNumbers), [mQ](FiguredBassNumber* num) {
-		return mQ ? true : (num->m_number > 0);
-	});
+	int i, j;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isManipulator()) {
+			continue;
+		}
 
-	return filteredNumbers;
-}
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			if (strchr(infile.token(i, j)->getSpineInfo().c_str(), '(') != NULL) {
+				splits[infile[i].token(j)->getTrack()] = 1;
+			}
+		}
+	}
 
+	field.reserve(infile.getMaxTrack()*2);
+	field.resize(0);
 
+	subfield.reserve(infile.getMaxTrack()*2);
+	subfield.resize(0);
 
-//////////////////////////////
-//
-// Tool_fb::filterFiguredBassNumbersForLine -- Find all FiguredBassNumber objects for a slice (line index) of the music.
-//
+	model.reserve(infile.getMaxTrack()*2);
+	model.resize(0);
 
-vector<FiguredBassNumber*> Tool_fb::filterFiguredBassNumbersForLine(vector<FiguredBassNumber*> numbers, int lineIndex) {
+	bool allQ = interp.empty();
 
-	vector<FiguredBassNumber*> filteredNumbers;
+	vector<int> dummyfield;
+	vector<int> dummysubfield;
+	vector<int> dummymodel;
+	getInterpretationFields(dummyfield, dummysubfield, model, infile, interp, 1);
 
-	// filter numbers with passed lineIndex
-	copy_if(numbers.begin(), numbers.end(), back_inserter(filteredNumbers), [lineIndex](FiguredBassNumber* num) {
-		return num->m_lineIndex == lineIndex;
-	});
+	vector<int> interptracks;
 
-	// sort by voiceIndex
-	sort(filteredNumbers.begin(), filteredNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
-		return a->m_voiceIndex > b->m_voiceIndex;
-	});
+	interptracks.resize(infile.getMaxTrack()+1);
+	fill(interptracks.begin(), interptracks.end(), 0);
 
-	return filterNegativeNumbers(filteredNumbers);
+	for (i=0; i<(int)dummyfield.size(); i++) {
+		interptracks[dummyfield[i]] = 1;
+	}
+
+	int aval = 'a';
+	int bval = 'b';
+	int zero = 0;
+	for (i=1; i<(int)splits.size(); i++) {
+		if (splits[i] && (allQ || interptracks[i])) {
+			field.push_back(i);
+			subfield.push_back(aval);
+			model.push_back(zero);
+			field.push_back(i);
+			subfield.push_back(bval);
+			model.push_back(zero);
+		} else {
+			field.push_back(i);
+			subfield.push_back(zero);
+			model.push_back(zero);
+		}
+	}
+
+	if (debugQ) {
+		m_humdrum_text << "!!expand: ";
+		for (i=0; i<(int)field.size(); i++) {
+			m_humdrum_text << field[i];
+			if (subfield[i]) {
+				m_humdrum_text << (char)subfield[i];
+			}
+			if (i < (int)field.size()-1) {
+				m_humdrum_text << ",";
+			}
+		}
+		m_humdrum_text << endl;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::filterFiguredBassNumbersForLineAndVoice --
+// Tool_extract::reverseSpines -- reverse the order of spines, grouped by the
+//   given exclusive interpretation.
 //
 
-vector<FiguredBassNumber*> Tool_fb::filterFiguredBassNumbersForLineAndVoice(vector<FiguredBassNumber*> numbers, int lineIndex, int voiceIndex) {
+void Tool_extract::reverseSpines(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, HumdrumFile& infile, const string& exinterp) {
 
-	vector<FiguredBassNumber*> filteredNumbers;
+	vector<int> target;
+	target.resize(infile.getMaxTrack()+1);
+	fill(target.begin(), target.end(), 0);
 
-	// filter numbers with passed lineIndex and passed voiceIndex
-	copy_if(numbers.begin(), numbers.end(), back_inserter(filteredNumbers), [lineIndex, voiceIndex](FiguredBassNumber* num) {
-		return (num->m_lineIndex == lineIndex) && (num->m_voiceIndex == voiceIndex);
-	});
+	vector<HTp> trackstarts;
+	infile.getSpineStartList(trackstarts);
 
-	// sort by voiceIndex (probably not needed here)
-	sort(filteredNumbers.begin(), filteredNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
-		return a->m_voiceIndex > b->m_voiceIndex;
-	});
+	for (int t=0; t<(int)trackstarts.size(); t++) {
+		if (trackstarts[t]->isDataType(exinterp)) {
+			target.at(t + 1) = 1;
+		}
+	}
 
-	return filterNegativeNumbers(filteredNumbers);
-}
-
-
-
-//////////////////////////////
-//
-// Tool_fb::formatFiguredBassNumbers -- Create a **fb data record string out of the passed FiguredBassNumber objects
-//
-
-string Tool_fb::formatFiguredBassNumbers(const vector<FiguredBassNumber*>& numbers) {
-
-	vector<FiguredBassNumber*> formattedNumbers;
-
-	// Normalize numbers (remove 8 and 1, sort by size, remove duplicate numbers)
-	if (m_normalizeQ) {
-		bool aQ = m_accidentalsQ;
-		// remove 8 and 1 but keep them if they have an accidental
-		copy_if(numbers.begin(), numbers.end(), back_inserter(formattedNumbers), [aQ](FiguredBassNumber* num) {
-			return ((num->getNumberWithinOctave() != 8) && (num->getNumberWithinOctave() != 1)) || (aQ && num->m_showAccidentals);
-		});
-		// sort by size
-		sort(formattedNumbers.begin(), formattedNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
-			return a->getNumberWithinOctave() < b->getNumberWithinOctave();
-		});
-		// remove duplicate numbers
-		formattedNumbers.erase(unique(formattedNumbers.begin(), formattedNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) {
-			return a->getNumberWithinOctave() == b->getNumberWithinOctave();
-		}), formattedNumbers.end());
-	} else {
-		formattedNumbers = numbers;
-	}
+	field.reserve(infile.getMaxTrack()*2);
+	field.resize(0);
 
-	// Hide numbers if they have no attack
-	if (m_intervallsatzQ && m_attackQ) {
-		vector<FiguredBassNumber*> attackNumbers;
-		copy_if(formattedNumbers.begin(), formattedNumbers.end(), back_inserter(attackNumbers), [](FiguredBassNumber* num) {
-			return num->m_isAttack || num->m_baseOfSustainedNoteDidChange;
-		});
-		formattedNumbers = attackNumbers;
+	int lasti = (int)target.size();
+	for (int i=(int)target.size()-1; i>0; i--) {
+		if (target[i]) {
+			lasti = i;
+			field.push_back(i);
+			for (int j=i+1; j<(int)target.size(); j++) {
+				if (!target.at(j)) {
+					field.push_back(j);
+				} else {
+					break;
+				}
+			}
+		}
 	}
 
-	// Analysze before sorting
-	if (m_compoundQ) {
-		formattedNumbers = analyzeChordNumbers(formattedNumbers);
+	// if the grouping spine is not first, then preserve the
+	// locations of the pre-spines.
+	int extras = 0;
+	if (lasti != 1) {
+		extras = lasti - 1;
+		field.resize(field.size()+extras);
+		for (int i=0; i<(int)field.size()-extras; i++) {
+			field[(int)field.size()-1-i] = field[(int)field.size()-1-extras-i];
+		}
+		for (int i=0; i<extras; i++) {
+			field[i] = i+1;
+		}
 	}
 
-	// Sort numbers by size
-	if (m_sortQ) {
-		bool cQ = m_compoundQ;
-		sort(formattedNumbers.begin(), formattedNumbers.end(), [cQ](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
-			// sort by getNumberWithinOctave if compoundQ is true otherwise sort by number
-			return (cQ) ? a->getNumberWithinOctave() > b->getNumberWithinOctave() : a->m_number > b->m_number;
-		});
+	if (debugQ) {
+		m_humdrum_text << "!!reverse: ";
+		for (int i=0; i<(int)field.size(); i++) {
+			m_humdrum_text << field[i] << " ";
+		}
+		m_humdrum_text << endl;
 	}
 
-	if (m_reduceQ) {
-		// Overwrite formattedNumbers with abbreviated numbers
-		formattedNumbers = getAbbreviatedNumbers(formattedNumbers);
-	}
+	subfield.resize(field.size());
+	fill(subfield.begin(), subfield.end(), 0);
 
-	// join numbers
-	string str = "";
-	bool first = true;
-	for (FiguredBassNumber* number: formattedNumbers) {
-		string num = number->toString(m_compoundQ, m_accidentalsQ, m_hideThreeQ);
-		if (num.length() > 0) {
-			if (!first) str += " ";
-			first = false;
-			str += num;
-		}
-	}
-	return str;
+	model.resize(field.size());
+	fill(model.begin(), model.end(), 0);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::getAbbreviatedNumbers -- Get abbreviated figured bass numbers
-//    If no abbreviation is found all numbers will be shown
+// Tool_extract::fillFieldData --
+//
 
-vector<FiguredBassNumber*> Tool_fb::getAbbreviatedNumbers(const vector<FiguredBassNumber*>& numbers) {
+void Tool_extract::fillFieldData(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, string& fieldstring, HumdrumFile& infile) {
 
-	vector<FiguredBassNumber*> abbreviatedNumbers;
+	int maxtrack = infile.getMaxTrack();
 
-	string numberString = getNumberString(numbers);
+	field.reserve(maxtrack);
+	field.resize(0);
 
-	// Check if an abbreviation exists for passed numbers
-	auto it = find_if(FiguredBassAbbreviationMapping::s_mappings.begin(), FiguredBassAbbreviationMapping::s_mappings.end(), [&numberString](const FiguredBassAbbreviationMapping& abbr) {
-		return abbr.m_str == numberString;
-	});
+	subfield.reserve(maxtrack);
+	subfield.resize(0);
 
-	if (it != FiguredBassAbbreviationMapping::s_mappings.end()) {
-		const FiguredBassAbbreviationMapping& abbr = *it;
-		bool aQ = m_accidentalsQ;
-		// Store numbers to display by the abbreviation mapping in abbreviatedNumbers
-		copy_if(numbers.begin(), numbers.end(), back_inserter(abbreviatedNumbers), [&abbr, aQ](FiguredBassNumber* num) {
-			const vector<int>& nums = abbr.m_numbers;
-			// Show numbers if they are part of the abbreviation mapping or if they have an accidental
-			return (find(nums.begin(), nums.end(), num->getNumberWithinOctave()) != nums.end()) || (num->m_showAccidentals && aQ);
-		});
+	model.reserve(maxtrack);
+	model.resize(0);
 
-		return abbreviatedNumbers;
+	HumRegex hre;
+	string buffer = fieldstring;
+	hre.replaceDestructive(buffer, "", "\\s", "gs");
+	int start = 0;
+	string tempstr;
+	vector<int> tempfield;
+	vector<int> tempsubfield;
+	vector<int> tempmodel;
+	while (hre.search(buffer,  start, "^([^,]+,?)")) {
+		tempfield.clear();
+		tempsubfield.clear();
+		tempmodel.clear();
+		processFieldEntry(tempfield, tempsubfield, tempmodel, hre.getMatch(1), infile);
+		start += hre.getMatchEndIndex(1);
+		field.insert(field.end(), tempfield.begin(), tempfield.end());
+		subfield.insert(subfield.end(), tempsubfield.begin(), tempsubfield.end());
+		model.insert(model.end(), tempmodel.begin(), tempmodel.end());
 	}
-
-	return numbers;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::analyzeChordNumbers -- Analyze chord numbers and improve them
-//    Set m_convert2To9 to true when a 3 is included in the chord numbers.
-
-vector<FiguredBassNumber*> Tool_fb::analyzeChordNumbers(const vector<FiguredBassNumber*>& numbers) {
+// Tool_extract::processFieldEntry --
+//   3-6 expands to 3 4 5 6
+//   $   expands to maximum spine track
+//   $-1 expands to maximum spine track minus 1, etc.
+//
 
-	vector<FiguredBassNumber*> analyzedNumbers = numbers;
+void Tool_extract::processFieldEntry(vector<int>& field,
+		vector<int>& subfield, vector<int>& model, const string& astring,
+		HumdrumFile& infile) {
 
-	// Check if compound numbers 3 is withing passed numbers (chord)
-	auto it = find_if(analyzedNumbers.begin(), analyzedNumbers.end(), [](FiguredBassNumber* number) {
-		return number->getNumberWithinOctave() == 3;
-	});
-	if (it != analyzedNumbers.end()) {
-		for (auto &number : analyzedNumbers) {
-			number->m_convert2To9 = true;
-		}
-	}
+	int finitsize = (int)field.size();
+	int maxtrack = infile.getMaxTrack();
 
-	return analyzedNumbers;
-}
+	vector<HTp> ktracks;
+	infile.getKernSpineStartList(ktracks);
+	int maxkerntrack = (int)ktracks.size();
 
+	int modletter;
+	int subletter;
 
+	HumRegex hre;
+	string buffer = astring;
 
-//////////////////////////////
-//
-// Tool_fb::getNumberString -- Get only the numbers (without accidentals) of passed FiguredBassNumbers
-//
+	// remove any comma left at end of input astring (or anywhere else)
+	hre.replaceDestructive(buffer, "", ",", "g");
 
-string Tool_fb::getNumberString(vector<FiguredBassNumber*> numbers) {
-	// Sort numbers by size
-	sort(numbers.begin(), numbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
-		return a->getNumberWithinOctave() > b->getNumberWithinOctave();
-	});
-	// join numbers
-	string str = "";
-	bool first = true;
-	for (FiguredBassNumber* nr: numbers) {
-		int num = nr->getNumberWithinOctave();
-		if (num > 0) {
-			if (!first) str += " ";
-			first = false;
-			str += to_string(num);
-		}
+	// first remove $ symbols and replace with the correct values
+	if (kernQ) {
+		removeDollarsFromString(buffer, maxkerntrack);
+	} else {
+		removeDollarsFromString(buffer, maxtrack);
 	}
-	return str;
-}
-
 
+	int zero = 0;
+	if (hre.search(buffer, "^(\\d+)-(\\d+)$")) {
+		int firstone = hre.getMatchInt(1);
+		int lastone  = hre.getMatchInt(2);
 
-//////////////////////////////
-//
-// Tool_fb::getKeySignature -- Get the key signature for a line index of the input file
-//
+		if ((firstone < 1) && (firstone != 0)) {
+			m_error_text << "Error: range token: \"" << astring << "\""
+			             << " contains too small a number at start: " << firstone << endl;
+			m_error_text << "Minimum number allowed is " << 1 << endl;
+			return;
+		}
+		if ((lastone < 1) && (lastone != 0)) {
+			m_error_text << "Error: range token: \"" << astring << "\""
+			             << " contains too small a number at end: " << lastone << endl;
+			m_error_text << "Minimum number allowed is " << 1 << endl;
+			return;
+		}
+		if (firstone > maxtrack) {
+			m_error_text << "Error: range token: \"" << astring << "\""
+			             << " contains number too large at start: " << firstone << endl;
+			m_error_text << "Maximum number allowed is " << maxtrack << endl;
+			return;
+		}
+		if (lastone > maxtrack) {
+			m_error_text << "Error: range token: \"" << astring << "\""
+			             << " contains number too large at end: " << lastone << endl;
+			m_error_text << "Maximum number allowed is " << maxtrack << endl;
+			return;
+		}
 
-string Tool_fb::getKeySignature(HumdrumFile& infile, int lineIndex) {
-	string keySignature = "";
-	[&] {
-		for (int i = 0; i < infile.getLineCount(); i++) {
-			if (i > lineIndex) {
-				return;
+		if (firstone > lastone) {
+			for (int i=firstone; i>=lastone; i--) {
+				field.push_back(i);
+				subfield.push_back(zero);
+				model.push_back(zero);
 			}
-			HLp line = infile.getLine(i);
-			for (int j = 0; j < line->getFieldCount(); j++) {
-				if (line->token(j)->isKeySignature()) {
-					keySignature = line->getTokenString(j);
-				}
+		} else {
+			for (int i=firstone; i<=lastone; i++) {
+				field.push_back(i);
+				subfield.push_back(zero);
+				model.push_back(zero);
 			}
 		}
-	}();
-	return keySignature;
-}
+	} else if (hre.search(buffer, "^(\\d+)([a-z]*)")) {
+		int value = hre.getMatchInt(1);
+		modletter = 0;
+		subletter = 0;
+		if (hre.getMatch(2) ==  "a") {
+			subletter = 'a';
+		}
+		if (hre.getMatch(2) ==  "b") {
+			subletter = 'b';
+		}
+		if (hre.getMatch(2) ==  "c") {
+			subletter = 'c';
+		}
+		if (hre.getMatch(2) ==  "d") {
+			modletter = 'd';
+		}
+		if (hre.getMatch(2) ==  "n") {
+			modletter = 'n';
+		}
+		if (hre.getMatch(2) ==  "r") {
+			modletter = 'r';
+		}
 
+		if ((value < 1) && (value != 0)) {
+			m_error_text << "Error: range token: \"" << astring << "\""
+			             << " contains too small a number at end: " << value << endl;
+			m_error_text << "Minimum number allowed is " << 1 << endl;
+			return;
+		}
+		if (value > maxtrack) {
+			m_error_text << "Error: range token: \"" << astring << "\""
+			             << " contains number too large at start: " << value << endl;
+			m_error_text << "Maximum number allowed is " << maxtrack << endl;
+			return;
+		}
+		field.push_back(value);
+		if (value == 0) {
+			subfield.push_back(zero);
+			model.push_back(zero);
+		} else {
+			subfield.push_back(subletter);
+			model.push_back(modletter);
+		}
+	}
 
+	if (!kernQ) {
+		return;
+	}
 
-//////////////////////////////
-//
-// Tool_fb::getLowestBase40Pitch -- Get lowest base 40 pitch that is not a rest or silent
-//    TODO: Handle negative values and sustained notes
-//
+	// Insert fields to next **kern spine.
+	vector<int> newfield;
+	vector<int> newsubfield;
+	vector<int> newmodel;
 
-int Tool_fb::getLowestBase40Pitch(vector<int> base40Pitches) {
-	vector<int> filteredBase40Pitches;
-	copy_if(base40Pitches.begin(), base40Pitches.end(), std::back_inserter(filteredBase40Pitches), [](int base40Pitch) {
-		// Ignore if base is a rest or silent note
-		return (base40Pitch != -1000) && (base40Pitch != -2000) && (base40Pitch != 0);
-	});
+	vector<HTp> trackstarts;
+	infile.getTrackStartList(trackstarts);
+	int spine;
 
-	if (filteredBase40Pitches.size() == 0) {
-		return -2000;
+	// convert kern tracks into spine tracks:
+	for (int i=finitsize; i<(int)field.size(); i++) {
+		if (field[i] > 0) {
+			spine = ktracks[field[i]-1]->getTrack();
+		   field[i] = spine;
+		}
 	}
 
-	return *min_element(begin(filteredBase40Pitches), end(filteredBase40Pitches));
+	int startspineindex, stopspineindex;
+	for (int i=0; i<(int)field.size(); i++) {
+		newfield.push_back(field[i]); // copy **kern spine index into new list
+		newsubfield.push_back(subfield[i]);
+		newmodel.push_back(model[i]);
+
+		// search for non **kern spines after specified **kern spine:
+		startspineindex = field[i] + 1 - 1;
+		stopspineindex = maxtrack;
+		for (int j=startspineindex; j<stopspineindex; j++) {
+			if (trackstarts[j]->isKern()) {
+				break;
+			}
+			newfield.push_back(j+1);
+			newsubfield.push_back(zero);
+			newmodel.push_back(zero);
+		}
+	}
+
+	field    = newfield;
+	subfield = newsubfield;
+	model    = newmodel;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fb::getIntervalQuality -- Return interval quality prefix string
+// Tool_extract::removeDollarsFromString -- substitute $ sign for maximum track count.
 //
 
-string Tool_fb::getIntervalQuality(int basePitchBase40, int targetPitchBase40) {
-
-	int diff = (targetPitchBase40 - basePitchBase40) % 40;
-
-	diff = diff < -2 ? abs(diff) : diff;
-
-	// See https://wiki.ccarh.org/wiki/Base_40
-	string quality;
-	switch (diff) {
-		// 1
-		case -2:
-		case 38:
-			quality = "dd"; break;
-		case -1:
-		case 39:
-			quality = "d"; break;
-		case 0: quality = "P"; break;
-		case 1: quality = "A"; break;
-		case 2: quality = "AA"; break;
-
-		// 2
-		case 3: quality = "dd"; break;
-		case 4: quality = "d"; break;
-		case 5: quality = "m"; break;
-		case 6: quality = "M"; break;
-		case 7: quality = "A"; break;
-		case 8: quality = "AA"; break;
-
-		// 3
-		case 9: quality = "dd"; break;
-		case 10: quality = "d"; break;
-		case 11: quality = "m"; break;
-		case 12: quality = "M"; break;
-		case 13: quality = "A"; break;
-		case 14: quality = "AA"; break;
-
-		// 4
-		case 15: quality = "dd"; break;
-		case 16: quality = "d"; break;
-		case 17: quality = "P"; break;
-		case 18: quality = "A"; break;
-		case 19: quality = "AA"; break;
-
-		case 20: quality = "<unused>"; break;
-
-		// 5
-		case 21: quality = "dd"; break;
-		case 22: quality = "d"; break;
-		case 23: quality = "P"; break;
-		case 24: quality = "A"; break;
-		case 25: quality = "AA"; break;
-
-		// 6
-		case 26: quality = "dd"; break;
-		case 27: quality = "d"; break;
-		case 28: quality = "m"; break;
-		case 29: quality = "M"; break;
-		case 30: quality = "A"; break;
-		case 31: quality = "AA"; break;
+void Tool_extract::removeDollarsFromString(string& buffer, int maxtrack) {
+	HumRegex hre;
+	char buf2[128] = {0};
+	int value2;
 
-		// 7
-		case 32: quality = "dd"; break;
-		case 33: quality = "d"; break;
-		case 34: quality = "m"; break;
-		case 35: quality = "M"; break;
-		case 36: quality = "A"; break;
-		case 37: quality = "AA"; break;
+	if (hre.search(buffer, "\\$$")) {
+		snprintf(buf2, 128, "%d", maxtrack);
+		hre.replaceDestructive(buffer, buf2, "\\$$");
+	}
 
-		default: quality = "?"; break;
+	if (hre.search(buffer, "\\$(?![\\d-])")) {
+		// don't know how this case could happen, however...
+		snprintf(buf2, 128, "%d", maxtrack);
+		hre.replaceDestructive(buffer, buf2, "\\$(?![\\d-])", "g");
 	}
 
-	return quality;
+	if (hre.search(buffer, "\\$0")) {
+		// replace $0 with maxtrack (used for reverse orderings)
+		snprintf(buf2, 128, "%d", maxtrack);
+		hre.replaceDestructive(buffer, buf2, "\\$0", "g");
+	}
 
+	while (hre.search(buffer, "\\$(-?\\d+)")) {
+		value2 = maxtrack - abs(hre.getMatchInt(1));
+		snprintf(buf2, 128, "%d", value2);
+		hre.replaceDestructive(buffer, buf2, "\\$-?\\d+");
+	}
 }
 
 
 
 //////////////////////////////
 //
-// FiguredBassNumber::FiguredBassNumber -- Constructor
+// Tool_extract::excludeFields -- print all spines except the ones in the list of fields.
 //
 
-FiguredBassNumber::FiguredBassNumber(int num, string accid, bool showAccid, int voiceIdx, int lineIdx, bool isAtk, bool intervallsatz, string intervalQuality, bool hint) {
-	m_number          = num;
-	m_accidentals     = accid;
-	m_voiceIndex      = voiceIdx;
-	m_lineIndex       = lineIdx;
-	m_showAccidentals = showAccid;
-	m_isAttack        = isAtk;
-	m_intervallsatz   = intervallsatz;
-	m_intervalQuality = intervalQuality;
-	m_hint            = hint;
+void Tool_extract::excludeFields(HumdrumFile& infile, vector<int>& field,
+		vector<int>& subfield, vector<int>& model) {
+	int start = 0;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << '\n';
+			continue;
+		} else {
+			start = 0;
+			for (int j=0; j<infile[i].getFieldCount(); j++) {
+				if (isInList(infile[i].token(j)->getTrack(), field)) {
+					continue;
+				}
+				if (start != 0) {
+					m_humdrum_text << '\t';
+				}
+				start = 1;
+				m_humdrum_text << infile.token(i, j);
+			}
+			if (start != 0) {
+				m_humdrum_text << endl;
+			}
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// FiguredBassNumber::toString -- Convert FiguredBassNumber to a string (accidental + number)
+// Tool_extract::extractFields -- print all spines in the list of fields.
 //
 
-string FiguredBassNumber::toString(bool compoundQ, bool accidentalsQ, bool hideThreeQ) {
-	int num = (compoundQ) ? getNumberWithinOctave() : m_number;
-	if (m_hint) {
-		return m_intervalQuality + to_string(abs(num));
-	}
-	string accid = (accidentalsQ && m_showAccidentals) ? m_accidentals : "";
-	if (((num == 3) || (num == -3)) && accidentalsQ && m_showAccidentals && hideThreeQ) {
-		return accid;
-	}
-	if (num > 0) {
-		return accid + to_string(num);
-	}
-	if (num < 0) {
-		return accid + "~" + to_string(abs(num));
-	}
-	return "";
-}
-
+void Tool_extract::extractFields(HumdrumFile& infile, vector<int>& field,
+		vector<int>& subfield, vector<int>& model) {
 
+	HumRegex hre;
+	int start = 0;
+	int target;
+	int subtarget;
+	int modeltarget;
+	string spat;
+	bool foundBarline = true;
 
-//////////////////////////////
-//
-// FiguredBassNumber::getNumberWithinOctave -- Get a reasonable figured bass number
-//    Replace 0 with 7 and -7
-//    Replace 1 with 8 and -8
-//    Replace 2 with 9 if it is a suspension of the ninth
-//    Allow 1 (unisono) in intervallsatz
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << '\n';
+			continue;
+		}
 
-int FiguredBassNumber::getNumberWithinOctave(void) {
-	int num = m_number % 7;
+		if (infile[i].isManipulator()) {
+			dealWithSpineManipulators(infile, i, field, subfield, model);
+			continue;
+		}
 
-	// Replace 0 with 7 and -7
-	if ((abs(m_number) > 0) && (m_number % 7 == 0)) {
-		return m_number < 0 ? -7 : 7;
-	}
+		if (infile[i].isBarline()) {
+			foundBarline = true;
+		}
 
-	// Replace 1 with 8 and -8
-	if (abs(num) == 1) {
-		// Allow unisono in intervallsatz
-		if (m_intervallsatz || m_hint) {
-			if (abs(m_number) == 1) {
-				return 1;
+		start = 0;
+		for (int t=0; t<(int)field.size(); t++) {
+			target = field[t];
+			subtarget = subfield[t];
+			modeltarget = model[t];
+			if (modeltarget == 0) {
+				switch (subtarget) {
+					case 'a':
+					case 'b':
+						modeltarget = submodel;
+						break;
+					case 'c':
+						modeltarget = comodel;
+				}
+			}
+			if (target == 0) {
+				if (start != 0) {
+					m_humdrum_text << '\t';
+				}
+				start = 1;
+				if (!infile[i].isManipulator()) {
+					if (infile[i].isLocalComment()) {
+						m_humdrum_text << "!";
+					} else if (infile[i].isBarline()) {
+						m_humdrum_text << infile[i].token(0);
+					} else if (infile[i].isData()) {
+						if (foundBarline) {
+							if (addRestsQ) {
+								HumNum dur = infile[i].getDurationToBarline();
+								m_humdrum_text << Convert::durationToRecip(dur);
+							} else {
+								m_humdrum_text << ".";
+							}
+						} else {
+							m_humdrum_text << ".";
+						}
+						// interpretations handled in dealWithSpineManipulators()
+						// [obviously not, so adding a blank one here
+					} else if (infile[i].isInterpretation()) {
+						HTp token = infile.token(i, 0);
+						if (token->isExpansionLabel()) {
+							m_humdrum_text << token;
+						} else if (token->isExpansionList()) {
+							m_humdrum_text << token;
+						} else {
+							if (addRestsQ) {
+								printInterpretationForKernSpine(infile, i);
+							} else {
+								m_humdrum_text << "*";
+							}
+						}
+					}
+				}
+			} else {
+				for (int j=0; j<infile[i].getFieldCount(); j++) {
+					if (infile[i].token(j)->getTrack() != target) {
+						continue;
+					}
+					switch (subtarget) {
+					case 'a':
+						getSearchPat(spat, target, "a");
+						if (hre.search(infile.token(i,j)->getSpineInfo(), spat) ||
+								!hre.search(infile.token(i, j)->getSpineInfo(), "\\(")) {
+							if (start != 0) {
+								m_humdrum_text << '\t';
+							}
+							start = 1;
+							m_humdrum_text << infile.token(i, j);
+						}
+						break;
+					case 'b':
+						getSearchPat(spat, target, "b");
+						if (hre.search(infile.token(i, j)->getSpineInfo(), spat)) {
+							if (start != 0) {
+								m_humdrum_text << '\t';
+							}
+							start = 1;
+							m_humdrum_text << infile.token(i, j);
+						} else if (!hre.search(infile.token(i, j)->getSpineInfo(),
+								"\\(")) {
+							if (start != 0) {
+								m_humdrum_text << '\t';
+							}
+							start = 1;
+							dealWithSecondarySubspine(field, subfield, model, t,
+									infile, i, j, modeltarget);
+						}
+						break;
+					case 'c':
+						if (start != 0) {
+							m_humdrum_text << '\t';
+						}
+						start = 1;
+						dealWithCospine(field, subfield, model, t, infile, i, j,
+								modeltarget, modeltarget, cointerp);
+						break;
+					default:
+						if (start != 0) {
+							m_humdrum_text << '\t';
+						}
+						start = 1;
+						m_humdrum_text << infile.token(i, j);
+					}
+				}
 			}
 		}
-		return m_number < 0 ? -8 : 8;
-	}
 
-	// Replace 2 with 9 if m_convert2To9 is true (e.g. when a 3 is included in the chord numbers)
-	if (m_convert2To9 && (num == 2)) {
-		return 9;
-	}
+		if (infile[i].isData()) {
+			foundBarline = false;
+		}
 
-	return num;
+		if (start != 0) {
+			m_humdrum_text << endl;
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// FiguredBassAbbreviationMapping::FiguredBassAbbreviationMapping -- Constructor
-//    Helper class to store the mappings for abbreviate figured bass numbers
+// Tool_extract::printInterpretationForKernSpine --
 //
 
-FiguredBassAbbreviationMapping::FiguredBassAbbreviationMapping(string s, vector<int> n) {
-	m_str = s;
-	m_numbers = n;
+void Tool_extract::printInterpretationForKernSpine(HumdrumFile& infile, int index) {
+	HTp kerntok = NULL;
+	for (int j=0; j<infile[index].getFieldCount(); j++) {
+		HTp token = infile.token(index, j);
+		if (!token->isKern()) {
+			continue;
+		}
+		kerntok = token;
+		break;
+	}
+
+	if (kerntok == NULL) {
+		m_humdrum_text << "*";
+		return;
+	}
+
+	if (*kerntok == "*") {
+		m_humdrum_text << kerntok;
+		return;
+	}
+
+	if (kerntok->isKeySignature()) {
+		m_humdrum_text << kerntok;
+		return;
+	}
+	if (kerntok->isKeyDesignation()) {
+		m_humdrum_text << kerntok;
+		return;
+	}
+	if (kerntok->isTimeSignature()) {
+		m_humdrum_text << kerntok;
+		return;
+	}
+	if (kerntok->isMensurationSymbol()) {
+		m_humdrum_text << kerntok;
+		return;
+	}
+	if (kerntok->isTempo()) {
+		m_humdrum_text << kerntok;
+		return;
+	}
+	if (kerntok->isInstrumentName()) {
+		m_humdrum_text << "*I\"";
+		return;
+	}
+	if (kerntok->isInstrumentAbbreviation()) {
+		m_humdrum_text << "*I'";
+		return;
+	}
+
+	m_humdrum_text << "*";
 }
 
 
 
 //////////////////////////////
 //
-// FiguredBassAbbreviationMapping::s_mappings -- Mapping to abbreviate figured bass numbers
+// Tool_extract::dealWithCospine -- extract the required token(s) from a co-spine.
 //
 
-const vector<FiguredBassAbbreviationMapping> FiguredBassAbbreviationMapping::s_mappings = {
-	FiguredBassAbbreviationMapping("3", {}),
-	FiguredBassAbbreviationMapping("5", {}),
-	FiguredBassAbbreviationMapping("5 3", {}),
-	FiguredBassAbbreviationMapping("6 3", {6}),
-	FiguredBassAbbreviationMapping("5 4", {4}),
-	FiguredBassAbbreviationMapping("7 5 3", {7}),
-	FiguredBassAbbreviationMapping("7 3", {7}),
-	FiguredBassAbbreviationMapping("7 5", {7}),
-	FiguredBassAbbreviationMapping("6 5 3", {6, 5}),
-	FiguredBassAbbreviationMapping("6 4 3", {4, 3}),
-	FiguredBassAbbreviationMapping("6 4 2", {4, 2}),
-	FiguredBassAbbreviationMapping("9 5 3", {9}),
-	FiguredBassAbbreviationMapping("9 5", {9}),
-	FiguredBassAbbreviationMapping("9 3", {9}),
-};
+void Tool_extract::dealWithCospine(vector<int>& field, vector<int>& subfield, vector<int>& model,
+		int targetindex, HumdrumFile& infile, int line, int cospine,
+		int comodel, int submodel, const string& cointerp) {
 
+	vector<string> cotokens;
+	cotokens.reserve(50);
 
+	string buffer;
+	int i, j, k;
+	int index;
 
-#define RUNTOOL(NAME, INFILE, COMMAND, STATUS)     \
-	Tool_##NAME *tool = new Tool_##NAME;            \
-	tool->process(COMMAND);                         \
-	tool->run(INFILE);                              \
-	if (tool->hasError()) {                         \
-		status = false;                              \
-		tool->getError(cerr);                        \
-		delete tool;                                 \
-		break;                                       \
-	} else if (tool->hasHumdrumText()) {            \
-		INFILE.readString(tool->getHumdrumText());   \
-	}                                               \
-	delete tool;
+	if (infile[line].isInterpretation()) {
+		m_humdrum_text << infile.token(line, cospine);
+		return;
+	}
 
-#define RUNTOOL2(NAME, INFILE1, INFILE2, COMMAND, STATUS) \
-	Tool_##NAME *tool = new Tool_##NAME;            \
-	tool->process(COMMAND);                         \
-	tool->run(INFILE1, INFILE2);                    \
-	if (tool->hasError()) {                         \
-		status = false;                              \
-		tool->getError(cerr);                        \
-		delete tool;                                 \
-		break;                                       \
-	} else if (tool->hasHumdrumText()) {            \
-		INFILE1.readString(tool->getHumdrumText());  \
-	}                                               \
-	delete tool;
+	if (infile[line].isBarline()) {
+		m_humdrum_text << infile.token(line, cospine);
+		return;
+	}
 
-#define RUNTOOLSET(NAME, INFILES, COMMAND, STATUS) \
-	Tool_##NAME *tool = new Tool_##NAME;            \
-	tool->process(COMMAND);                         \
-	tool->run(INFILES);                             \
-	if (tool->hasError()) {                         \
-		status = false;                              \
-		tool->getError(cerr);                        \
-		delete tool;                                 \
-		break;                                       \
-	} else if (tool->hasHumdrumText()) {            \
-		INFILES.readString(tool->getHumdrumText());  \
-	}                                               \
-	delete tool;
+	if (infile[line].isLocalComment()) {
+		m_humdrum_text << infile.token(line, cospine);
+		return;
+	}
 
-#define RUNTOOLSTREAM(NAME, INFILES, COMMAND, STATUS) \
-	Tool_##NAME *tool = new Tool_##NAME;               \
-	tool->process(COMMAND);                            \
-	tool->run(INFILES);                                \
-	if (tool->hasError()) {                            \
-		status = false;                                 \
-		tool->getError(cerr);                           \
-		delete tool;                                    \
-		break;                                          \
-	} else if (tool->hasHumdrumText()) {               \
-		INFILES.readString(tool->getHumdrumText());     \
-	}                                                  \
-	delete tool;
+	int count = infile[line].token(cospine)->getSubtokenCount();
+	for (k=0; k<count; k++) {
+		buffer = infile.token(line, cospine)->getSubtoken(k);
+		cotokens.resize(cotokens.size()+1);
+		index = (int)cotokens.size()-1;
+		cotokens[index] = buffer;
+	}
 
+	vector<int> spineindex;
+	vector<int> subspineindex;
 
+	spineindex.reserve(infile.getMaxTrack()*2);
+	spineindex.resize(0);
 
-////////////////////////////////
-//
-// Tool_filter::Tool_filter -- Set the recognized options for the tool.
-//
+	subspineindex.reserve(infile.getMaxTrack()*2);
+	subspineindex.resize(0);
 
-Tool_filter::Tool_filter(void) {
-	define("debug=b",      "print debug statement");
-	define("v|variant=s:", "Run filters labeled with the given variant");
-}
+	for (j=0; j<infile[line].getFieldCount(); j++) {
+		if (infile.token(line, j)->isDataType(cointerp)) {
+			continue;
+		}
+		if (*infile.token(line, j) == ".") {
+			continue;
+		}
+		count = infile[line].token(j)->getSubtokenCount();
+		for (k=0; k<count; k++) {
+			buffer = infile[line].token(j)->getSubtoken(k);
+			if (comodel == 'r') {
+				if (buffer == "r") {
+					continue;
+				}
+			}
+			spineindex.push_back(j);
+			subspineindex.push_back(k);
+		}
+	}
 
+	if (debugQ) {
+		m_humdrum_text << "\n!!codata:\n";
+		for (i=0; i<(int)cotokens.size(); i++) {
+			m_humdrum_text << "!!\t" << i << "\t" << cotokens[i];
+			if (i < (int)spineindex.size()) {
+				m_humdrum_text << "\tspine=" << spineindex[i];
+				m_humdrum_text << "\tsubspine=" << subspineindex[i];
+			} else {
+				m_humdrum_text << "\tspine=.";
+				m_humdrum_text << "\tsubspine=.";
+			}
+			m_humdrum_text << endl;
+		}
+	}
 
+	string buff;
 
-/////////////////////////////////
-//
-// Tool_filter::run -- Primary interfaces to the tool.
-//
+	int start = 0;
+	for (i=0; i<(int)field.size(); i++) {
+		if (infile.token(line, field[i])->isDataType(cointerp)) {
+			continue;
+		}
 
-bool Tool_filter::run(const string& indata) {
-	HumdrumFileSet infiles(indata);
-	bool status = run(infiles);
-	return status;
+		for (j=0; j<infile[line].getFieldCount(); j++) {
+			if (infile[line].token(j)->getTrack() != field[i]) {
+				continue;
+			}
+			if (subfield[i] == 'a') {
+				getSearchPat(buff, field[i], "a");
+				if ((strchr(infile.token(line, j)->getSpineInfo().c_str(), '(') == NULL) ||
+					(infile.token(line, j)->getSpineInfo().find(buff) != string::npos)) {
+					printCotokenInfo(start, infile, line, j, cotokens, spineindex,
+							subspineindex);
+				}
+			} else if (subfield[i] == 'b') {
+				// this section may need more work...
+				getSearchPat(buff, field[i], "b");
+				if ((strchr(infile.token(line, j)->getSpineInfo().c_str(), '(') == NULL) ||
+					(strstr(infile.token(line, j)->getSpineInfo().c_str(), buff.c_str()) != NULL)) {
+					printCotokenInfo(start, infile, line, j, cotokens, spineindex,
+							subspineindex);
+				}
+			} else {
+				printCotokenInfo(start, infile, line, j, cotokens, spineindex,
+					subspineindex);
+			}
+		}
+	}
 }
 
 
-bool Tool_filter::run(HumdrumFile& infile) {
-	HumdrumFileSet infiles;
-	infiles.appendHumdrumPointer(&infile);
-	bool status = run(infiles);
-	infiles.clearNoFree();
-	return status;
-}
 
-bool Tool_filter::runUniversal(HumdrumFileSet& infiles) {
-	bool status = true;
-	vector<pair<string, string> > commands;
-	getUniversalCommandList(commands, infiles);
+//////////////////////////////
+//
+// Tool_extract::printCotokenInfo --
+//
 
-	for (int i=0; i<(int)commands.size(); i++) {
-		if (commands[i].first == "humdiff") {
-			RUNTOOLSET(humdiff, infiles, commands[i].second, status);
-		} else if (commands[i].first == "chooser") {
-			RUNTOOLSET(chooser, infiles, commands[i].second, status);
-		} else if (commands[i].first == "myank") {
-			RUNTOOL(myank, infiles, commands[i].second, status);
+void Tool_extract::printCotokenInfo(int& start, HumdrumFile& infile, int line, int spine,
+		vector<string>& cotokens, vector<int>& spineindex,
+		vector<int>& subspineindex) {
+	int i;
+	int found = 0;
+	for (i=0; i<(int)spineindex.size(); i++) {
+		if (spineindex[i] == spine) {
+			if (start == 0) {
+				start++;
+			} else {
+				m_humdrum_text << subtokenseparator;
+			}
+			if (i<(int)cotokens.size()) {
+				m_humdrum_text << cotokens[i];
+			} else {
+				m_humdrum_text << ".";
+			}
+		found = 1;
 		}
 	}
-
-	removeUniversalFilterLines(infiles);
-
-	return status;
+	if (!found) {
+		if (start == 0) {
+			start++;
+		} else {
+			m_humdrum_text << subtokenseparator;
+		}
+		m_humdrum_text << ".";
+	}
 }
 
 
+
+//////////////////////////////
 //
-// In-place processing of file:
+// Tool_extract::dealWithSecondarySubspine -- what to print if a secondary spine
+//     does not exist on a line.
 //
 
-bool Tool_filter::run(HumdrumFileSet& infiles) {
-	if (infiles.getCount() == 0) {
-		return false;
-	}
-
-	initialize(infiles[0]);
-
-	HumdrumFile& infile = infiles[0];
+void Tool_extract::dealWithSecondarySubspine(vector<int>& field, vector<int>& subfield,
+		vector<int>& model, int targetindex, HumdrumFile& infile, int line,
+		int spine, int submodel) {
 
-	#ifdef __EMSCRIPTEN__
-	bool optionList = getBoolean("options");
-	if (optionList) {
-		printEmscripten(m_humdrum_text);
-		m_humdrum_text << infile;
-	}
-	#endif
+	int& i = line;
+	int& j = spine;
 
-	bool status = true;
-	vector<pair<string, string> > commands;
-	getCommandList(commands, infile);
-	for (int i=0; i<(int)commands.size(); i++) {
-		if (commands[i].first == "addic") {
-			RUNTOOL(addic, infile, commands[i].second, status);
-		} else if (commands[i].first == "addkey") {
-			RUNTOOL(addkey, infile, commands[i].second, status);
-		} else if (commands[i].first == "addlabels") {
-			RUNTOOL(addlabels, infile, commands[i].second, status);
-		} else if (commands[i].first == "addtempo") {
-			RUNTOOL(addtempo, infile, commands[i].second, status);
-		} else if (commands[i].first == "autoaccid") {
-			RUNTOOL(autoaccid, infile, commands[i].second, status);
-		} else if (commands[i].first == "autobeam") {
-			RUNTOOL(autobeam, infile, commands[i].second, status);
-		} else if (commands[i].first == "autostem") {
-			RUNTOOL(autostem, infile, commands[i].second, status);
-		} else if (commands[i].first == "binroll") {
-			RUNTOOL(binroll, infile, commands[i].second, status);
-		} else if (commands[i].first == "chantize") {
-			RUNTOOL(chantize, infile, commands[i].second, status);
-		} else if (commands[i].first == "chint") {
-			RUNTOOL(chint, infile, commands[i].second, status);
-		} else if (commands[i].first == "chord") {
-			RUNTOOL(chord, infile, commands[i].second, status);
-		} else if (commands[i].first == "cint") {
-			RUNTOOL(cint, infile, commands[i].second, status);
-		} else if (commands[i].first == "cmr") {
-			RUNTOOL(cmr, infile, commands[i].second, status);
-		} else if (commands[i].first == "composite") {
-			RUNTOOL(composite, infile, commands[i].second, status);
-		} else if (commands[i].first == "dissonant") {
-			RUNTOOL(dissonant, infile, commands[i].second, status);
-		} else if (commands[i].first == "double") {
-			RUNTOOL(double, infile, commands[i].second, status);
-		} else if (commands[i].first == "fb") {
-			RUNTOOL(fb, infile, commands[i].second, status);
-		} else if (commands[i].first == "flipper") {
-			RUNTOOL(flipper, infile, commands[i].second, status);
-		} else if (commands[i].first == "filter") {
-			RUNTOOL(filter, infile, commands[i].second, status);
-		} else if (commands[i].first == "gasparize") {
-			RUNTOOL(gasparize, infile, commands[i].second, status);
-		} else if (commands[i].first == "half") {
-			RUNTOOL(half, infile, commands[i].second, status);
-		} else if (commands[i].first == "hands") {
-			RUNTOOL(hands, infile, commands[i].second, status);
-		} else if (commands[i].first == "homorhythm") {
-			RUNTOOL(homorhythm, infile, commands[i].second, status);
-		} else if (commands[i].first == "homorhythm2") {
-			RUNTOOL(homorhythm2, infile, commands[i].second, status);
-		} else if (commands[i].first == "hproof") {
-			RUNTOOL(hproof, infile, commands[i].second, status);
-		} else if (commands[i].first == "humbreak") {
-			RUNTOOL(humbreak, infile, commands[i].second, status);
-		} else if (commands[i].first == "humsheet") {
-			RUNTOOL(humsheet, infile, commands[i].second, status);
-		} else if (commands[i].first == "humtr") {
-			RUNTOOL(humtr, infile, commands[i].second, status);
-		} else if (commands[i].first == "imitation") {
-			RUNTOOL(imitation, infile, commands[i].second, status);
-		} else if (commands[i].first == "instinfo") {
-			RUNTOOL(instinfo, infile, commands[i].second, status);
-		} else if (commands[i].first == "kern2mens") {
-			RUNTOOL(kern2mens, infile, commands[i].second, status);
-		} else if (commands[i].first == "kernify") {
-			RUNTOOL(kernify, infile, commands[i].second, status);
-		} else if (commands[i].first == "kernview") {
-			RUNTOOL(kernview, infile, commands[i].second, status);
-		} else if (commands[i].first == "melisma") {
-			RUNTOOL(melisma, infile, commands[i].second, status);
-		} else if (commands[i].first == "mens2kern") {
-			RUNTOOL(mens2kern, infile, commands[i].second, status);
-		} else if (commands[i].first == "meter") {
-			RUNTOOL(meter, infile, commands[i].second, status);
-		} else if (commands[i].first == "metlev") {
-			RUNTOOL(metlev, infile, commands[i].second, status);
-		} else if (commands[i].first == "modori") {
-			RUNTOOL(modori, infile, commands[i].second, status);
-		} else if (commands[i].first == "msearch") {
-			RUNTOOL(msearch, infile, commands[i].second, status);
-		} else if (commands[i].first == "nproof") {
-			RUNTOOL(nproof, infile, commands[i].second, status);
-		} else if (commands[i].first == "ordergps") {
-			RUNTOOL(ordergps, infile, commands[i].second, status);
-		} else if (commands[i].first == "pbar") {
-			RUNTOOL(pbar, infile, commands[i].second, status);
-		} else if (commands[i].first == "phrase") {
-			RUNTOOL(phrase, infile, commands[i].second, status);
-		} else if (commands[i].first == "pline") {
-			RUNTOOL(pline, infile, commands[i].second, status);
-		} else if (commands[i].first == "prange") {
-			RUNTOOL(prange, infile, commands[i].second, status);
-		} else if (commands[i].first == "recip") {
-			RUNTOOL(recip, infile, commands[i].second, status);
-		} else if (commands[i].first == "restfill") {
-			RUNTOOL(restfill, infile, commands[i].second, status);
-		} else if (commands[i].first == "rphrase") {
-			RUNTOOL(rphrase, infile, commands[i].second, status);
-		} else if (commands[i].first == "sab2gs") {
-			RUNTOOL(sab2gs, infile, commands[i].second, status);
-		} else if (commands[i].first == "scordatura") {
-			RUNTOOL(scordatura, infile, commands[i].second, status);
-		} else if (commands[i].first == "semitones") {
-			RUNTOOL(semitones, infile, commands[i].second, status);
-		} else if (commands[i].first == "shed") {
-			RUNTOOL(shed, infile, commands[i].second, status);
-		} else if (commands[i].first == "sic") {
-			RUNTOOL(sic, infile, commands[i].second, status);
-		} else if (commands[i].first == "simat") {
-			RUNTOOL2(simat, infile, infile, commands[i].second, status);
-		} else if (commands[i].first == "slurcheck") {
-			RUNTOOL(slurcheck, infile, commands[i].second, status);
-		} else if (commands[i].first == "slur") {
-			RUNTOOL(slurcheck, infile, commands[i].second, status);
-		} else if (commands[i].first == "spinetrace") {
-			RUNTOOL(spinetrace, infile, commands[i].second, status);
-		} else if (commands[i].first == "strophe") {
-			RUNTOOL(strophe, infile, commands[i].second, status);
-		} else if (commands[i].first == "synco") {
-			RUNTOOL(synco, infile, commands[i].second, status);
-		} else if (commands[i].first == "tabber") {
-			RUNTOOL(tabber, infile, commands[i].second, status);
-		} else if (commands[i].first == "tassoize") {
-			RUNTOOL(tassoize, infile, commands[i].second, status);
-		} else if (commands[i].first == "tassoise") {
-			RUNTOOL(tassoize, infile, commands[i].second, status);
-		} else if (commands[i].first == "tasso") {
-			RUNTOOL(tassoize, infile, commands[i].second, status);
-		} else if (commands[i].first == "textdur") {
-			RUNTOOL(textdur, infile, commands[i].second, status);
-		} else if (commands[i].first == "tie") {
-			RUNTOOL(tie, infile, commands[i].second, status);
-		} else if (commands[i].first == "tspos") {
-			RUNTOOL(tspos, infile, commands[i].second, status);
-		} else if (commands[i].first == "transpose") {
-			RUNTOOL(transpose, infile, commands[i].second, status);
-		} else if (commands[i].first == "tremolo") {
-			RUNTOOL(tremolo, infile, commands[i].second, status);
-		} else if (commands[i].first == "trillspell") {
-			RUNTOOL(trillspell, infile, commands[i].second, status);
-		} else if (commands[i].first == "vcross") {
-			RUNTOOL(vcross, infile, commands[i].second, status);
-
-		// filters with aliases:
-
-		} else if (commands[i].first == "colortriads") {
-			RUNTOOL(colortriads, infile, commands[i].second, status);
-		} else if (commands[i].first == "colourtriads") {
-			// British spelling
-			RUNTOOL(colortriads, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "colorthirds") {
-			RUNTOOL(tspos, infile, commands[i].second, status);
-		} else if (commands[i].first == "colourthirds") {
-			// British spelling
-			RUNTOOL(tspos, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "colorgroups") {
-			RUNTOOL(colorgroups, infile, commands[i].second, status);
-		} else if (commands[i].first == "colourgroups") { // British spelling
-			RUNTOOL(colorgroups, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "deg") { // humlib version of Humdrum Toolkit deg tool
-			RUNTOOL(deg, infile, commands[i].second, status);
-		} else if (commands[i].first == "degx") { // humlib cli name
-			RUNTOOL(deg, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "extract") { // humlib version of Humdrum Toolkit extract tool
-			RUNTOOL(extract, infile, commands[i].second, status);
-		} else if (commands[i].first == "extractx") { // humlib cli name
-			RUNTOOL(extract, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "grep") {
-			RUNTOOL(grep, infile, commands[i].second, status);
-		} else if (commands[i].first == "humgrep") {
-			RUNTOOL(grep, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "myank") { // humlib version of Humdrum Extras myank tool
-			RUNTOOL(myank, infile, commands[i].second, status);
-		} else if (commands[i].first == "myankx") { // humlib cli name
-			RUNTOOL(myank, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "rid") { // humlib version of Humdrum Toolkit deg tool
-			RUNTOOL(rid, infile, commands[i].second, status);
-		} else if (commands[i].first == "ridx") { // Humdrum Extra cli name
-			RUNTOOL(rid, infile, commands[i].second, status);
-		} else if (commands[i].first == "ridxx") { // humlib cli name
-			RUNTOOL(rid, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "satb2gs") { // humlib version of Humdrum Extras satg2gs tool
-			RUNTOOL(satb2gs, infile, commands[i].second, status);
-		} else if (commands[i].first == "satb2gsx") { // humlib cli name
-			RUNTOOL(satb2gs, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "thru") { // humlib version of Humdrum Toolkit thru tool
-			RUNTOOL(thru, infile, commands[i].second, status);
-		} else if (commands[i].first == "thrux") { // Humdrum Extras cli name
-			RUNTOOL(thru, infile, commands[i].second, status);
-		} else if (commands[i].first == "thruxx") { // humlib cli name
-			RUNTOOL(thru, infile, commands[i].second, status);
-
-		} else if (commands[i].first == "timebase") { // humlib version of Humdrum Toolkit timebase tool
-			RUNTOOL(timebase, infile, commands[i].second, status);
-		} else if (commands[i].first == "timebasex") { // humlib cli name
-			RUNTOOL(timebase, infile, commands[i].second, status);
-		} else {
-			cerr << "UNKNOWN FILTER: " << commands[i].first << " OPTIONS: " << commands[i].second << endl;
-		}
-
-	}
-
-	removeGlobalFilterLines(infile);
-
-	// Re-load the text for each line from their tokens in case any
-	// updates are needed from token changes.
-	infile.createLinesFromTokens();
-	return status;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_filter::removeGlobalFilterLines --
-//
-
-void Tool_filter::removeGlobalFilterLines(HumdrumFile& infile) {
 	HumRegex hre;
-	string text;
-
-	string maintag = "!!!filter:";
-	string mainXtag = "!!!Xfilter:";
-	string maintagQuery = "^!!!filter:";
-
-	string maintagV;
-	string mainXtagV;
-	string maintagQueryV;
-
-	if (m_variant.size() > 0) {
-		maintagV = "!!!filter-" + m_variant + ":";
-		mainXtagV = "!!!Xfilter-" + m_variant + ":";
-		maintagQueryV = "^!!!filter-" + m_variant + ":";
-	}
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReference()) {
-			continue;
+	string buffer;
+	if (infile[line].isLocalComment()) {
+		if ((submodel == 'n') || (submodel == 'r')) {
+			m_humdrum_text << "!";
+		} else {
+			m_humdrum_text << infile.token(i, j);
 		}
-
-		if (m_variant.size() > 0) {
-			if (infile.token(i, 0)->compare(0, maintagV.size(), maintagV) == 0) {
-				text = infile.token(i, 0)->getText();
-				hre.replaceDestructive(text, mainXtagV, maintagQueryV);
-				infile.token(i, 0)->setText(text);
-			}
+	} else if (infile[line].isBarline()) {
+		m_humdrum_text << infile.token(i, j);
+	} else if (infile[line].isInterpretation()) {
+		if ((submodel == 'n') || (submodel == 'r')) {
+			m_humdrum_text << "*";
 		} else {
-			if (infile.token(i, 0)->compare(0, maintag.size(), maintag) == 0) {
-				text = infile.token(i, 0)->getText();
-				hre.replaceDestructive(text, mainXtag, maintagQuery);
-				infile.token(i, 0)->setText(text);
-			}
+			m_humdrum_text << infile.token(i, j);
 		}
-	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_filter::removeUniversalFilterLines --
-//
-
-void Tool_filter::removeUniversalFilterLines(HumdrumFileSet& infiles) {
-	HumRegex hre;
-	string text;
-
-	string maintag = "!!!!filter:";
-	string mainXtag = "!!!!Xfilter:";
-	string maintagQuery = "^!!!!filter:";
-
-	string maintagV;
-	string mainXtagV;
-	string maintagQueryV;
-
-	if (m_variant.size() > 0) {
-		maintagV = "!!!!filter-" + m_variant + ":";
-		mainXtagV = "!!!!Xfilter-" + m_variant + ":";
-		maintagQueryV = "^!!!!filter-" + m_variant + ":";
-	}
-
-	for (int i=0; i<infiles.getCount(); i++) {
-		HumdrumFile& infile = infiles[i];
-		for (int j=0; j<infile.getLineCount(); j++) {
-			if (!infile[i].isUniversalReference()) {
-				continue;
-			}
-			HTp token = infile.token(j, 0);
-			if (m_variant.size() > 0) {
-				if (token->compare(0, maintagV.size(), maintagV) == 0) {
-					text = token->getText();
-					hre.replaceDestructive(text, mainXtagV, maintagQueryV);
-					token->setText(text);
-					infile[j].createLineFromTokens();
-				}
+	} else if (infile[line].isData()) {
+		if (submodel == 'n') {
+			m_humdrum_text << ".";
+		} else if (submodel == 'r') {
+			if (*infile.token(i, j) == ".") {
+				m_humdrum_text << ".";
+			} else if (infile.token(i, j)->find('q') != string::npos) {
+				m_humdrum_text << ".";
+			} else if (infile.token(i, j)->find('Q') != string::npos) {
+				m_humdrum_text << ".";
 			} else {
-				if (token->compare(0, maintag.size(), maintag) == 0) {
-					text = token->getText();
-					hre.replaceDestructive(text, mainXtag, maintagQuery);
-					token->setText(text);
-					infile[j].createLineFromTokens();
+				buffer = *infile.token(i, j);
+				if (hre.search(buffer, "{")) {
+					m_humdrum_text << "{";
+				}
+				// remove secondary chord notes:
+				hre.replaceDestructive(buffer, "", " .*");
+				// remove unnecessary characters (such as stem direction):
+				hre.replaceDestructive(buffer, "",
+						"[^}pPqQA-Ga-g0-9.;%#nr-]", "g");
+				// change pitch to rest:
+				hre.replaceDestructive(buffer, "[A-Ga-g#n-]+", "r");
+				// add editorial marking unless -Y option is given:
+				if (editorialInterpretation != "") {
+					if (hre.search(buffer, "rr")) {
+						 hre.replaceDestructive(buffer, editorialInterpretation, "(?<=rr)");
+						 hre.replaceDestructive(buffer, "r", "rr");
+					} else {
+						 hre.replaceDestructive(buffer, editorialInterpretation, "(?<=r)");
+					}
 				}
+				m_humdrum_text << buffer;
 			}
+		} else {
+			m_humdrum_text << infile.token(i, j);
 		}
+	} else {
+		m_error_text << "Should not get to this line of code" << endl;
+		return;
 	}
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_filter::getCommandList --
+// Tool_extract::getSearchPat --
 //
 
-void Tool_filter::getCommandList(vector<pair<string, string> >& commands,
-		HumdrumFile& infile) {
-
-	vector<HLp> refs = infile.getReferenceRecords();
-	pair<string, string> entry;
-	string tag = "filter";
-	if (m_variant.size() > 0) {
-		tag += "-";
-		tag += m_variant;
-	}
-	vector<string> clist;
-	HumRegex hre;
-	for (int i=0; i<(int)refs.size(); i++) {
-		string refkey = refs[i]->getGlobalReferenceKey();
-		if (refkey != tag) {
-			continue;
-		}
-		string command = refs[i]->getGlobalReferenceValue();
-		splitPipeline(clist, command);
-		for (int j=0; j<(int)clist.size(); j++) {
-			if (hre.search(clist[j], "^\\s*([^\\s]+)")) {
-				entry.first  = hre.getMatch(1);
-				entry.second = clist[j];
-				commands.push_back(entry);
-			}
-		}
+void Tool_extract::getSearchPat(string& spat, int target, const string& modifier) {
+	if (modifier.size() > 20) {
+		m_error_text << "Error in GetSearchPat" << endl;
+		return;
 	}
+	spat.reserve(16);
+	spat = "\\(";
+	spat += to_string(target);
+	spat += "\\)";
+	spat += modifier;
 }
 
 
 
 //////////////////////////////
 //
-//  Tool_filter::splitPipeline --
+// Tool_extract::dealWithSpineManipulators -- check for proper Humdrum syntax of
+//     spine manipulators (**, *-, *x, *v, *^) when creating the output.
 //
 
-void Tool_filter::splitPipeline(vector<string>& clist, const string& command) {
-	clist.clear();
-	clist.resize(1);
-	clist[0] = "";
-	int inDoubleQuotes = -1;
-	int inSingleQuotes = -1;
-	char ch = '\0';
-	char lastch;
-	for (int i=0; i<(int)command.size(); i++) {
-		lastch = ch;
-		ch = command[i];
+void Tool_extract::dealWithSpineManipulators(HumdrumFile& infile, int line,
+		vector<int>& field, vector<int>& subfield, vector<int>& model) {
 
-		if (ch == '"') {
-			if (lastch == '\\') {
-				// escaped double quote, so treat as regular character
-				clist.back() += ch;
-				continue;
-			} else if (inDoubleQuotes >= 0) {
-				// turn off previous double quote sequence
-				clist.back() += ch;
-				inDoubleQuotes = -1;
-				continue;
-			} else if (inSingleQuotes >= 0) {
-				// in an active single quote, so this is not a closing double quote
-				clist.back() += ch;
-				continue;
-			} else {
-				// this is the start of a double quote sequence
-				clist.back() += ch;
-				inDoubleQuotes = i;
-				continue;
-			}
-		}
+	vector<int> vmanip;  // counter for *v records on line
+	vmanip.resize(infile[line].getFieldCount());
+	fill(vmanip.begin(), vmanip.end(), 0);
 
-		if (ch == '\'') {
-			if (lastch == '\\') {
-				// escaped single quote, so treat as regular character
-				clist.back() += ch;
-				continue;
-			} else if (inSingleQuotes >= 0) {
-				// turn off previous single quote sequence
-				clist.back() += ch;
-				inSingleQuotes = -1;
-				continue;
-			} else if (inDoubleQuotes >= 0) {
-				// in an active double quote, so this is not a closing single quote
-				clist.back() += ch;
-				continue;
-			} else {
-				// this is the start of a single quote sequence
-				clist.back() += ch;
-				inSingleQuotes = i;
-				continue;
-			}
+	vector<int> xmanip; // counter for *x record on line
+	xmanip.resize(infile[line].getFieldCount());
+	fill(xmanip.begin(), xmanip.end(), 0);
+
+	int i = 0;
+	int j;
+	for (j=0; j<(int)vmanip.size(); j++) {
+		if (*infile.token(line, j) == "*v") {
+			vmanip[j] = 1;
 		}
+		if (*infile.token(line, j) == "*x") {
+			xmanip[j] = 1;
+		}
+	}
 
-		if (ch == '|') {
-			if ((inSingleQuotes > -1) || (inDoubleQuotes > -1)) {
-				// pipe character
-				clist.back() += ch;
-				continue;
-			} else {
-				// this is a real pipe
-				clist.resize(clist.size() + 1);
-				continue;
-			}
+	int counter = 1;
+	for (i=1; i<(int)xmanip.size(); i++) {
+		if ((xmanip[i] == 1) && (xmanip[i-1] == 1)) {
+			xmanip[i] = counter;
+			xmanip[i-1] = counter;
+			counter++;
 		}
+	}
 
-		if (isspace(ch) && (!(inSingleQuotes > -1)) && (!(inDoubleQuotes > -1))) {
-			if (isspace(lastch)) {
-				// don't repeat spaces outside of quotes.
-				continue;
+	counter = 1;
+	i = 0;
+	while (i < (int)vmanip.size()) {
+		if (vmanip[i] == 1) {
+			while ((i < (int)vmanip.size()) && (vmanip[i] == 1)) {
+				vmanip[i] = counter;
+				i++;
 			}
+			counter++;
 		}
-
-		// regular character
-		clist.back() += ch;
+		i++;
 	}
 
-	// remove leading and trailing spaces
-	HumRegex hre;
-	for (int i=0; i<(int)clist.size(); i++) {
-		hre.replaceDestructive(clist[i], "", "^\\s+");
-		hre.replaceDestructive(clist[i], "", "\\s+$");
+	vector<int> fieldoccur;  // nth occurance of an input spine in the output
+	fieldoccur.resize(field.size());
+	fill(fieldoccur.begin(), fieldoccur.end(), 0);
+
+	vector<int> trackcounter; // counter of input spines occurances in output
+	trackcounter.resize(infile.getMaxTrack()+1);
+	fill(trackcounter.begin(), trackcounter.end(), 0);
+
+	for (i=0; i<(int)field.size(); i++) {
+		if (field[i] != 0) {
+			trackcounter[field[i]]++;
+			fieldoccur[i] = trackcounter[field[i]];
+		}
 	}
 
-}
+	vector<string> tempout;
+	vector<int> vserial;
+	vector<int> xserial;
+	vector<int> fpos;     // input column of output spine
 
+	tempout.reserve(1000);
+	tempout.resize(0);
 
+	vserial.reserve(1000);
+	vserial.resize(0);
 
-//////////////////////////////
-//
-// Tool_filter::getUniversalCommandList --
-//
+	xserial.reserve(1000);
+	xserial.resize(0);
 
-void Tool_filter::getUniversalCommandList(vector<pair<string, string> >& commands,
-		HumdrumFileSet& infiles) {
+	fpos.reserve(1000);
+	fpos.resize(0);
 
-	vector<HLp> refs = infiles.getUniversalReferenceRecords();
-	pair<string, string> entry;
-	string tag = "filter";
-	if (m_variant.size() > 0) {
-		tag += "-";
-		tag += m_variant;
-	}
-	vector<string> clist;
+	string spat;
+	string spinepat;
 	HumRegex hre;
-	for (int i=0; i<(int)refs.size(); i++) {
-		if (refs[i]->getUniversalReferenceKey() != tag) {
-			continue;
-		}
-		string command = refs[i]->getUniversalReferenceValue();
-		hre.split(clist, command, "\\s*\\|\\s*");
-		for (int j=0; j<(int)clist.size(); j++) {
-			if (hre.search(clist[j], "^\\s*([^\\s]+)")) {
-				entry.first  = hre.getMatch(1);
-				entry.second = clist[j];
-				commands.push_back(entry);
+	int subtarget;
+	int modeltarget;
+	int xdebug = 0;
+	int vdebug = 0;
+	int suppress = 0;
+	int target;
+	int tval;
+	for (int t=0; t<(int)field.size(); t++) {
+		target = field[t];
+		subtarget = subfield[t];
+		modeltarget = model[t];
+		if (modeltarget == 0) {
+			switch (subtarget) {
+				case 'a':
+				case 'b':
+					modeltarget = submodel;
+					break;
+				case 'c':
+					modeltarget = comodel;
+			}
+		}
+		suppress = 0;
+		if (target == 0) {
+			if (infile.token(line, 0)->compare(0, 2, "**") == 0) {
+				storeToken(tempout, blankName);
+				tval = 0;
+				vserial.push_back(tval);
+				xserial.push_back(tval);
+				fpos.push_back(tval);
+			} else if (*infile.token(line, 0) == "*-") {
+				storeToken(tempout, "*-");
+				tval = 0;
+				vserial.push_back(tval);
+				xserial.push_back(tval);
+				fpos.push_back(tval);
+			} else {
+				storeToken(tempout, "*");
+				tval = 0;
+				vserial.push_back(tval);
+				xserial.push_back(tval);
+				fpos.push_back(tval);
+			}
+		} else {
+			for (j=0; j<infile[line].getFieldCount(); j++) {
+				if (infile[line].token(j)->getTrack() != target) {
+					continue;
+				}
+		// filter by subfield
+		if (subtarget == 'a') {
+			getSearchPat(spat, target, "b");
+			if (hre.search(infile.token(line, j)->getSpineInfo(), spat)) {
+						continue;
+			}
+		} else if (subtarget == 'b') {
+			getSearchPat(spat, target, "a");
+			if (hre.search(infile.token(line, j)->getSpineInfo(), spat)) {
+				continue;
 			}
 		}
-	}
-}
 
+				switch (subtarget) {
+				case 'a':
 
+					if (!hre.search(infile.token(line, j)->getSpineInfo(), "\\(")) {
+						if (*infile.token(line, j)  == "*^") {
+							 storeToken(tempout, "*");
+						} else {
+							 storeToken(tempout, *infile.token(line, j));
+						}
+					} else {
+						getSearchPat(spat, target, "a");
+						spinepat =  infile.token(line, j)->getSpineInfo();
+						hre.replaceDestructive(spinepat, "\\(", "\\(", "g");
+						hre.replaceDestructive(spinepat, "\\)", "\\)", "g");
 
-//////////////////////////////
-//
-// Tool_filter::initialize -- extract time signature lines for
-//    each **kern spine in file.
-//
+						if ((*infile.token(line, j) == "*v") &&
+							    (spinepat == spat)) {
+							 storeToken(tempout, "*");
+						} else {
+							getSearchPat(spat, target, "b");
+							if ((spinepat == spat) &&
+									(*infile.token(line, j) ==  "*v")) {
+								// do nothing
+								suppress = 1;
+							} else {
+								storeToken(tempout, *infile.token(line, j));
+							}
+						}
+					}
 
-void Tool_filter::initialize(HumdrumFile& infile) {
-	m_debugQ = getBoolean("debug");
-	m_variant.clear();
-	if (getBoolean("variant")) {
-		m_variant = getString("variant");
-	}
-}
+					break;
+				case 'b':
 
+					if (!hre.search(infile.token(line, j)->getSpineInfo(), "\\(")) {
+						if (*infile.token(line, j) == "*^") {
+							storeToken(tempout, "*");
+						} else {
+							storeToken(tempout, *infile.token(line, j));
+						}
+					} else {
+						getSearchPat(spat, target, "b");
+						spinepat = infile.token(line, j)->getSpineInfo();
+						hre.replaceDestructive(spinepat, "\\(", "\\(", "g");
+						hre.replaceDestructive(spinepat, "\\)", "\\)", "g");
 
+						if ((*infile.token(line, j) ==  "*v") &&
+								(spinepat == spat)) {
+							storeToken(tempout, "*");
+						} else {
+							getSearchPat(spat, target, "a");
+							if ((spinepat == spat) &&
+									(*infile.token(line, j) == "*v")) {
+								// do nothing
+								suppress = 1;
+							} else {
+								storeToken(tempout, *infile.token(line, j));
+							}
+						}
+					}
 
+					break;
+				case 'c':
+					// work on later
+					storeToken(tempout, *infile.token(line, j));
+					break;
+				default:
+					storeToken(tempout, *infile.token(line, j));
+				}
 
+				if (suppress) {
+					continue;
+				}
 
-/////////////////////////////////
-//
-// Tool_fixps::Tool_fixps -- Set the recognized options for the tool.
-//
+				if (tempout[(int)tempout.size()-1] == "*x") {
+					tval = fieldoccur[t] * 1000 + xmanip[j];
+					xserial.push_back(tval);
+					xdebug = 1;
+				} else {
+					tval = 0;
+					xserial.push_back(tval);
+				}
 
-Tool_fixps::Tool_fixps(void) {
-	// define ("n|only-remove-empty-transpositions=b", "Only remove empty transpositions");
-}
+				if (tempout[(int)tempout.size()-1] == "*v") {
+					tval = fieldoccur[t] * 1000 + vmanip[j];
+					vserial.push_back(tval);
+					vdebug = 1;
+				} else {
+					tval = 0;
+					vserial.push_back(tval);
+				}
 
+				fpos.push_back(j);
 
+			}
+		}
+	}
 
-/////////////////////////////////
-//
-// Tool_fixps::run -- Primary interfaces to the tool.
-//
+	if (debugQ && xdebug) {
+		m_humdrum_text << "!! *x serials = ";
+		for (int ii=0; ii<(int)xserial.size(); ii++) {
+			m_humdrum_text << xserial[ii] << " ";
+		}
+		m_humdrum_text << "\n";
+	}
 
-bool Tool_fixps::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	if (debugQ && vdebug) {
+		m_humdrum_text << "!!LINE: " << infile[line] << endl;
+		m_humdrum_text << "!! *v serials = ";
+		for (int ii=0; ii<(int)vserial.size(); ii++) {
+			m_humdrum_text << vserial[ii] << " ";
+		}
+		m_humdrum_text << "\n";
 	}
-	return status;
-}
 
+	// check for proper *x syntax /////////////////////////////////
+	for (i=0; i<(int)xserial.size()-1; i++) {
+		if (!xserial[i]) {
+			continue;
+		}
+		if (xserial[i] != xserial[i+1]) {
+			if (tempout[i] == "*x") {
+				xserial[i] = 0;
+				tempout[i] = "*";
+			}
+		} else {
+			i++;
+		}
+	}
 
-bool Tool_fixps::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if ((tempout.size() == 1) || (xserial.size() == 1)) {
+		// get rid of *x if there is only one spine in output
+		if (xserial[0]) {
+			xserial[0] = 0;
+			tempout[0] = "*";
+		}
+	} else if ((int)xserial.size() > 1) {
+		// check the last item in the list
+		int index = (int)xserial.size()-1;
+		if (tempout[index] == "*x") {
+			if (xserial[index] != xserial[index-1]) {
+				xserial[index] = 0;
+				tempout[index] = "*";
+			}
+		}
 	}
-	return status;
-}
 
+	// check for proper *v syntax /////////////////////////////////
+	vector<int> vsplit;
+	vsplit.resize((int)vserial.size());
+	fill(vsplit.begin(), vsplit.end(), 0);
 
-bool Tool_fixps::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	// identify necessary line splits
+	for (i=0; i<(int)vserial.size()-1; i++) {
+		if (!vserial[i]) {
+			continue;
+		}
+		while ((i<(int)vserial.size()-1) && (vserial[i]==vserial[i+1])) {
+			i++;
+		}
+		if ((i<(int)vserial.size()-1) && vserial[i]) {
+			if (vserial.size() > 1) {
+				if (vserial[i+1]) {
+					vsplit[i+1] = 1;
+				}
+			}
+		}
 	}
-	return status;
-}
 
-//
-// In-place processing of file:
-//
+	// remove single *v spines:
 
-bool Tool_fixps::run(HumdrumFile& infile) {
-	processFile(infile);
-	return true;
-}
+	for (i=0; i<(int)vsplit.size()-1; i++) {
+		if (vsplit[i] && vsplit[i+1]) {
+			if (tempout[i] == "*v") {
+				tempout[i] = "*";
+				vsplit[i] = 0;
+			}
+		}
+	}
+
+	if (debugQ) {
+		m_humdrum_text << "!!vsplit array: ";
+		for (i=0; i<(int)vsplit.size(); i++) {
+			m_humdrum_text << " " << vsplit[i];
+		}
+		m_humdrum_text << endl;
+	}
 
+	if (vsplit.size() > 0) {
+		if (vsplit[(int)vsplit.size()-1]) {
+			if (tempout[(int)tempout.size()-1] == "*v") {
+				tempout[(int)tempout.size()-1] = "*";
+				vsplit[(int)vsplit.size()-1] = 0;
+			}
+		}
+	}
 
+	int vcount = 0;
+	for (i=0; i<(int)vsplit.size(); i++) {
+		vcount += vsplit[i];
+	}
 
-//////////////////////////////
-//
-// Tool_fixps::processFile --
-//
+	if (vcount) {
+		printMultiLines(vsplit, vserial, tempout);
+	}
 
-void Tool_fixps::processFile(HumdrumFile& infile) {
-	removeDuplicateDynamics(infile);
-	markEmptyVoices(infile);
-	vector<vector<HTp>> newlist;
-	removeEmpties(newlist, infile);
-	outputNewSpining(newlist, infile);
+	int start = 0;
+	for (i=0; i<(int)tempout.size(); i++) {
+		if (tempout[i] != "") {
+			if (start != 0) {
+				m_humdrum_text << "\t";
+			}
+			m_humdrum_text << tempout[i];
+			start++;
+		}
+	}
+	if (start) {
+		m_humdrum_text << '\n';
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fixps::outputNewSpining --
+// Tool_extract::printMultiLines -- print separate *v lines.
 //
 
-void Tool_fixps::outputNewSpining(vector<vector<HTp>>& newlist, HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
+void Tool_extract::printMultiLines(vector<int>& vsplit, vector<int>& vserial,
+		vector<string>& tempout) {
+	int i;
+
+	int splitpoint = -1;
+	for (i=0; i<(int)vsplit.size(); i++) {
+		if (vsplit[i]) {
+			splitpoint = i;
+			break;
 		}
-		if ((i > 0) && (!newlist[i].empty()) && newlist[i][0]->isCommentLocal()) {
-			if (!newlist[i-1].empty() && newlist[i-1][0]->isCommentLocal()) {
-				if (newlist[i].size() == newlist[i-1].size()) {
-					bool same = true;
-					for (int j=0; j<(int)newlist[i].size(); j++) {
-						if (*(newlist[i][j]) != *(newlist[i-1][j])) {
-cerr << "GOT HERE " << i << " " << j << endl;
-cerr << infile[i-1] << endl;
-cerr << infile[i] << endl;
-cerr << endl;
-							same = false;
-							break;
-						}
-					}
-					if (same) {
-						continue;
-					}
+	}
+
+	if (debugQ) {
+		m_humdrum_text << "!!tempout: ";
+		for (i=0; i<(int)tempout.size(); i++) {
+			m_humdrum_text << tempout[i] << " ";
+		}
+		m_humdrum_text << endl;
+	}
+
+	if (splitpoint == -1) {
+		return;
+	}
+
+	int start = 0;
+	int printv = 0;
+	for (i=0; i<splitpoint; i++) {
+		if (tempout[i] != "") {
+			if (start) {
+				m_humdrum_text << "\t";
+			}
+			m_humdrum_text << tempout[i];
+			start = 1;
+			if (tempout[i] == "*v") {
+				if (printv) {
+					tempout[i] = "";
+				} else {
+					tempout[i] = "*";
+					printv = 1;
 				}
+			} else {
+				tempout[i] = "*";
 			}
 		}
-		if (!infile[i].isManipulator()) {
-			m_humdrum_text << newlist[i].at(0);
-			for (int j=1; j<(int)newlist[i].size(); j++) {
+	}
+
+	for (i=splitpoint; i<(int)vsplit.size(); i++) {
+		if (tempout[i] != "") {
+			if (start) {
 				m_humdrum_text << "\t";
-				m_humdrum_text << newlist[i].at(j);
 			}
-			m_humdrum_text << endl;
-			continue;
-		}
-		if ((i > 0) && !infile[i-1].isManipulator()) {
-			printNewManipulator(infile, newlist, i);
+			m_humdrum_text << "*";
 		}
 	}
-}
 
+	if (start) {
+		m_humdrum_text << "\n";
+	}
 
-//////////////////////////////
-//
-// Tool_fixps::printNewManipulator --
-//
+	vsplit[splitpoint] = 0;
 
-void Tool_fixps::printNewManipulator(HumdrumFile& infile, vector<vector<HTp>>& newlist, int line) {
-	HTp token = infile.token(line, 0);
-	if (*token == "*-") {
-		m_humdrum_text << infile[line] << endl;
-		return;
-	}
-	if (token->compare(0, 2, "**") == 0) {
-		m_humdrum_text << infile[line] << endl;
-		return;
-	}
-	m_humdrum_text << "++++++++++++++++++++" << endl;
+	printMultiLines(vsplit, vserial, tempout);
 }
 
+
+
 //////////////////////////////
 //
-// Tool_fixps::removeDuplicateDynamics --
+// Tool_extract::storeToken --
 //
 
-void Tool_fixps::removeDuplicateDynamics(HumdrumFile& infile) {
-	int scount = infile.getStrandCount();
-	for (int i=0; i<scount; i++) {
-		HTp sstart = infile.getStrandBegin(i);
-		if (!sstart->isDataType("**dynam")) {
-			continue;
-		}
-		HTp send   = infile.getStrandEnd(i);
-		HTp current = sstart;
-		while (current && (current != send)) {
-			vector<string> subtoks = current->getSubtokens();
-			if (subtoks.size() % 2 == 1) {
-				current = current->getNextToken();
-				continue;
-			}
-			bool equal = true;
-			int half = (int)subtoks.size() / 2;
-			for (int j=0; j<half; j++) {
-				if (subtoks[i] != subtoks[i+half]) {
-					equal = false;
-				}
-			}
-			if (equal) {
-				string newtext = subtoks[0];
-				for (int j=1; j<half; j++) {
-					newtext += " ";
-					newtext += subtoks[j];
-				}
-				current->setText(newtext);
-			}
-		}
-	}
+void Tool_extract::storeToken(vector<string>& storage, const string& string) {
+	storage.push_back(string);
+}
+
+void storeToken(vector<string>& storage, int index, const string& string) {
+	storage[index] = string;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fixps::removeEmpties --
+// Tool_extract::isInList -- returns true if first number found in list of numbers.
+//     returns the matching index plus one.
 //
 
-void Tool_fixps::removeEmpties(vector<vector<HTp>>& newlist, HumdrumFile& infile) {
-	newlist.resize(infile.getLineCount());
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			continue;
-		}
-		if (infile[i].isManipulator()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			string value = token->getValue("delete");
-			if (value == "true") {
-				continue;
-			}
-			newlist[i].push_back(token);
+int Tool_extract::isInList(int number, vector<int>& listofnum) {
+	int i;
+	for (i=0; i<(int)listofnum.size(); i++) {
+		if (listofnum[i] == number) {
+			return i+1;
 		}
 	}
+	return 0;
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_fixps::markEmptyVoices --
+// Tool_extract::getTraceData --
 //
 
-void Tool_fixps::markEmptyVoices(HumdrumFile& infile) {
-	HLp barline = NULL;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			continue;
-		}
-		if (infile[i].isManipulator()) {
-			continue;
-		}
-		if (infile[i].isInterpretation()) {
-			if (infile.token(i, 0)->compare(0, 2, "**")) {
-				barline = &infile[i];
-			}
-			continue;
-		}
-		if (infile[i].isBarline()) {
-			barline = &infile[i];
-		}
-		if (!infile[i].isData()) {
-			continue;
-		}
-		if (!barline) {
+void Tool_extract::getTraceData(vector<int>& startline, vector<vector<int> >& fields,
+		const string& tracefile, HumdrumFile& infile) {
+	char buffer[1024] = {0};
+	HumRegex hre;
+	int linenum;
+	startline.reserve(10000);
+	startline.resize(0);
+	fields.reserve(10000);
+	fields.resize(0);
+
+	ifstream input;
+	input.open(tracefile.c_str());
+	if (!input.is_open()) {
+		m_error_text << "Error: cannot open file for reading: " << tracefile << endl;
+		return;
+	}
+
+	string temps;
+	vector<int> field;
+	vector<int> subfield;
+	vector<int> model;
+
+	input.getline(buffer, 1024);
+	while (!input.eof()) {
+		if (hre.search(buffer, "^\\s*$")) {
 			continue;
 		}
-		// check on the data line if:
-		// * it is in the first subspine
-		// * it is an invisible rest
-		// * it takes the full duration of the measure
-		// If so, then mark the tokens for deletion in that layer.
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			// int track = token->getTrack();
-			int subtrack = token->getSubtrack();
-			if (subtrack != 1) {
-				continue;
-			}
-			if (token->find("yy") == string::npos) {
-				continue;
-			}
-			if (!token->isRest()) {
-				continue;
-			}
-			HumNum duration = token->getDuration();
-			HumNum bardur = token->getDurationToBarline();
-			HTp current = token;
-			while (current) {
-			   subtrack = current->getSubtrack();
-				if (subtrack != 1) {
-					break;
-				}
-				current->setValue("delete", "true");
-				if (current->isBarline()) {
-					break;
-				}
-				current = current->getNextToken();
-			}
-			current = token;
-			current = current->getPreviousToken();
-			while (current) {
-				if (current->isManipulator()) {
-					break;
-				}
-				if (current->isBarline()) {
-					break;
-				}
-				subtrack = current->getSubtrack();
-				if (subtrack != 1) {
-					break;
-				}
-				current->setValue("delete", "true");
-				current = current->getPreviousToken();
-			}
+		if (!hre.search(buffer, "(\\d+)")) {
+			continue;
+		}
+		linenum = hre.getMatchInt(1);
+		linenum--;  // adjust so that line 0 is the first line in the file
+		temps = buffer;
+		hre.replaceDestructive(temps, "", "\\d+");
+		hre.replaceDestructive(temps, "", "[^,\\s\\d\\$\\-].*");  // remove any possible comments
+		hre.replaceDestructive(temps, "", "\\s", "g");
+		if (hre.search(temps, "^\\s*$")) {
+			// no field data to process online
+			continue;
 		}
+		startline.push_back(linenum);
+		string ttemp = temps;
+		fillFieldData(field, subfield, model, ttemp, infile);
+		fields.push_back(field);
+		input.getline(buffer, 1024);
 	}
 
 }
 
 
 
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_flipper::Tool_flipper -- Set the recognized options for the tool.
+// Tool_extract::extractTrace --
 //
 
-Tool_flipper::Tool_flipper(void) {
-	define("k|keep=b",                      "keep *flip/*Xflip instructions");
-	define("a|all=b",                       "flip globally, not just inside *flip/*Xflip regions");
-	define("s|strophe=b",                   "flip inside of strophes as well");
-	define("S|strophe-only|only-strophe=b", "flip only inside of strophes as well");
-	define("i|interp=s:kern",               "flip only in this interpretation");
-}
-
-
-
-/////////////////////////////////
-//
-// Tool_flipper::run -- Do the main work of the tool.
-//
+void Tool_extract::extractTrace(HumdrumFile& infile, const string& tracefile) {
+	vector<int> startline;
+	vector<vector<int> > fields;
+	getTraceData(startline, fields, tracefile, infile);
+	int i, j;
 
-bool Tool_flipper::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	if (debugQ) {
+		for (i=0; i<(int)startline.size(); i++) {
+			m_humdrum_text << "!!TRACE " << startline[i]+1 << ":\t";
+			for (j=0; j<(int)fields[i].size(); j++) {
+				m_humdrum_text << fields[i][j] << " ";
+			}
+			m_humdrum_text << "\n";
+		}
 	}
-	return status;
-}
 
 
-bool Tool_flipper::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (startline.size() == 0) {
+		for (i=0; i<infile.getLineCount(); i++) {
+			if (!infile[i].hasSpines()) {
+				m_humdrum_text << infile[i] << '\n';
+			}
+		}
+		return;
 	}
-	return status;
-}
 
-
-bool Tool_flipper::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	for (i=0; i<startline[0]; i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << '\n';
+		}
 	}
-	return status;
-}
-
 
-bool Tool_flipper::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
+	int endline;
+	for (j=0; j<(int)startline.size(); j++) {
+		if (j == (int)startline.size()-1) {
+			endline = infile.getLineCount()-1;
+		} else {
+			endline = startline[j+1]-1;
+		}
+		for (i=startline[j]; i<endline; i++) {
+			if (!infile[i].hasSpines()) {
+				m_humdrum_text << infile[i] << '\n';
+			} else {
+				printTraceLine(infile, i, fields[j]);
+			}
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_flipper::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_extract::printTraceLine --
 //
 
-void Tool_flipper::initialize(void) {
-	m_allQ         = getBoolean("all");
-	m_keepQ        = getBoolean("keep");
-	m_kernQ        = true;
-	m_stropheQ     = getBoolean("strophe");
-	m_interp       = getString("interp");
-	if (m_interp != "kern") {
-		m_kernQ = false;
+void Tool_extract::printTraceLine(HumdrumFile& infile, int line, vector<int>& field) {
+	int j;
+	int t;
+	int start = 0;
+	int target;
+
+	start = 0;
+	for (t=0; t<(int)field.size(); t++) {
+		target = field[t];
+		for (j=0; j<infile[line].getFieldCount(); j++) {
+			if (infile[line].token(j)->getTrack() != target) {
+				continue;
+			}
+			if (start != 0) {
+				m_humdrum_text << '\t';
+			}
+			start = 1;
+			m_humdrum_text << infile.token(line, j);
+		}
+	}
+	if (start != 0) {
+		m_humdrum_text << endl;
 	}
 }
 
@@ -84041,241 +84510,228 @@ void Tool_flipper::initialize(void) {
 
 //////////////////////////////
 //
-// Tool_flipper::processFile --
+// Tool_extract::example -- example usage of the sonority program
 //
 
-void Tool_flipper::processFile(HumdrumFile& infile) {
-
-	m_fliplines.resize(infile.getLineCount());
-	fill(m_fliplines.begin(), m_fliplines.end(), false);
+void Tool_extract::example(void) {
+	m_free_text <<
+	"					                                                          \n"
+	<< endl;
+}
 
-	m_flipState.resize(infile.getMaxTrack()+1);
-	if (m_allQ) {
-		fill(m_flipState.begin(), m_flipState.end(), true);
-	} else {
-		fill(m_flipState.begin(), m_flipState.end(), false);
-	}
 
-	m_strophe.resize(infile.getMaxTrack()+1);
-	fill(m_strophe.begin(), m_strophe.end(), false);
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		processLine(infile, i);
-		if (!m_keepQ) {
-			if (!m_fliplines[i]) {
-				m_humdrum_text << infile[i] << endl;
-			}
-		}
-	}
+//////////////////////////////
+//
+// Tool_extract::usage -- gives the usage statement for the sonority program
+//
 
-	if (m_keepQ) {
-		m_humdrum_text << infile;
-	}
+void Tool_extract::usage(const string& command) {
+	m_free_text <<
+	"					                                                          \n"
+	<< endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_flipper::checkForFlipChanges --
+// Tool_extract::initialize --
 //
 
-void Tool_flipper::checkForFlipChanges(HumdrumFile& infile, int index) {
-	if (!infile[index].isInterpretation()) {
+void Tool_extract::initialize(HumdrumFile& infile) {
+	// handle basic options:
+	if (getBoolean("author")) {
+		m_free_text << "Written by Craig Stuart Sapp, "
+			  << "craig@ccrma.stanford.edu, Feb 2008" << endl;
+		return;
+	} else if (getBoolean("version")) {
+		m_free_text << getArg(0) << ", version: Feb 2008" << endl;
+		m_free_text << "compiled: " << __DATE__ << endl;
+		return;
+	} else if (getBoolean("help")) {
+		usage(getCommand().c_str());
+		return;
+	} else if (getBoolean("example")) {
+		example();
 		return;
 	}
 
-	int track;
+	excludeQ    = getBoolean("x");
+	interpQ     = getBoolean("i");
+	interps     = getString("i");
+	kernQ       = getBoolean("k");
+	rkernQ      = getBoolean("K");
 
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (*token == "*strophe") {
-			track = token->getTrack();
-			m_strophe[track] = true;
-		} else if (*token == "*Xstrophe") {
-			track = token->getTrack();
-			m_strophe[track] = false;
+	interpstate = 1;
+	if (!interpQ) {
+		interpQ = getBoolean("I");
+		interpstate = 0;
+		interps = getString("I");
+	}
+	if (interps.size() > 0) {
+		if (interps[0] != '*') {
+			// Automatically add ** if not given on exclusive interpretation
+			string tstring = "**";
+			interps = tstring + interps;
 		}
 	}
 
+	removerestQ = getBoolean("no-rest");
+	noEmptyQ    = getBoolean("no-empty");
+	emptyQ      = getBoolean("empty");
+	fieldQ      = getBoolean("f");
+	debugQ      = getBoolean("debug");
+	countQ      = getBoolean("count");
+	traceQ      = getBoolean("trace");
+	tracefile   = getString("trace");
+	reverseQ    = getBoolean("reverse");
+	expandQ     = getBoolean("expand") || getBoolean("E");
+	submodel    = getString("model").c_str()[0];
+	cointerp    = getString("cointerp");
+	comodel     = getString("cospine-model").c_str()[0];
 
-	if (m_allQ) {
-		// state always stays on in this case
-		return;
+	if (getBoolean("no-editoral-rests")) {
+		editorialInterpretation = "";
 	}
 
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (*token == "*flip") {
-			track = token->getTrack();
-			m_flipState[track] = true;
-			m_fliplines[i] = true;
-		} else if (*token == "*Xflip") {
-			track = token->getTrack();
-			m_flipState[track] = false;
-			m_fliplines[i] = true;
-		}
+	if (interpQ) {
+		fieldQ = true;
 	}
 
-}
-
-
-
-//////////////////////////////
-//
-// Tool_flipper::processLine --
-//
+	if (emptyQ) {
+		fieldQ = true;
+	}
 
-void Tool_flipper::processLine(HumdrumFile& infile, int index) {
-	if (!infile[index].hasSpines()) {
-		return;
+	if (noEmptyQ) {
+		fieldQ = true;
 	}
-	if (infile[index].isInterpretation()) {
-		checkForFlipChanges(infile, index);
+
+	if (expandQ) {
+		fieldQ = true;
+		expandInterp = getString("expand-interp");
 	}
 
-	vector<vector<HTp>> flipees;
-	extractFlipees(flipees, infile, index);
-	if (!flipees.empty()) {
-		int status = flipSubspines(flipees);
-		if (status) {
-			infile[index].createLineFromTokens();
+	if (!reverseQ) {
+		reverseQ = getBoolean("R");
+		if (reverseQ) {
+			reverseInterp = getString("R");
 		}
 	}
-}
 
+	if (reverseQ) {
+		fieldQ = true;
+	}
 
+	if (excludeQ) {
+		fieldstring = getString("x");
+	} else if (fieldQ) {
+		fieldstring = getString("f");
+	} else if (kernQ) {
+		fieldstring = getString("k");
+		fieldQ = true;
+	} else if (rkernQ) {
+		fieldstring = getString("K");
+		fieldQ = true;
+		fieldstring = reverseFieldString(fieldstring, infile.getMaxTrack());
+	}
 
-//////////////////////////////
-//
-// Tool_flipper::flipSubspines --
-//
+	spineListQ = getBoolean("spine-list");
+	grepQ      = getBoolean("grep");
+	grepString = getString("grep");
 
-bool Tool_flipper::flipSubspines(vector<vector<HTp>>& flipees) {
-	bool regenerateLine = false;
-	for (int i=0; i<(int)flipees.size(); i++) {
-		if (flipees[i].size() > 1) {
-			flipSpineTokens(flipees[i]);
-			regenerateLine = true;
+	if (getBoolean("name")) {
+		blankName = getString("name");
+		if (blankName == "") {
+			blankName = "**blank";
+		} else if (blankName.compare(0, 2, "**") != 0) {
+			if (blankName.compare(0, 1, "*") != 0) {
+				blankName = "**" + blankName;
+			} else {
+				blankName = "*" + blankName;
+			}
+		}
+		if (blankName == "**kern") {
+			addRestsQ = true;
 		}
 	}
-	return regenerateLine;
+
 }
 
 
 //////////////////////////////
 //
-// Tool_flipper::flipSpineTokens --
+// Tool_extract::reverseFieldString --  No dollar expansion for now.
 //
 
-void Tool_flipper::flipSpineTokens(vector<HTp>& subtokens) {
-	if (subtokens.size() < 2) {
-		return;
+string Tool_extract::reverseFieldString(const string& input, int maxval) {
+	string output;
+	string number;
+	for (int i=0; i<(int)input.size(); i++) {
+		if (isdigit(input[i])) {
+			number += input[i];
+			continue;
+		} else {
+			if (!number.empty()) {
+				int value = (int)strtol(number.c_str(), NULL, 10);
+				value = maxval - value + 1;
+				output += to_string(value);
+				output += input[i];
+				number.clear();
+			}
+		}
 	}
-	int count = (int)subtokens.size();
-	count = count / 2;
-	HTp tok1;
-	HTp tok2;
-	string str1;
-	string str2;
-	for (int i=0; i<count; i++) {
-		tok1 = subtokens[i];
-		tok2 = subtokens[subtokens.size() - 1 - i];
-		str1 = *tok1;
-		str2 = *tok2;
-		tok1->setText(str2);
-		tok2->setText(str1);
+	if (!number.empty()) {
+		int value = (int)strtol(number.c_str(), NULL, 10);
+		value = maxval - value + 1;
+		output += to_string(value);
 	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_flipper::extractFlipees --
+// Tool_fb::Tool_fb -- Set the recognized options for the tool.
 //
 
-void Tool_flipper::extractFlipees(vector<vector<HTp>>& flipees,
-		 HumdrumFile& infile, int index) {
-	flipees.clear();
-
-	HLp line = &infile[index];
-	int track;
-	int lastInsertTrack = -1;
-	for (int i=0; i<line->getFieldCount(); i++) {
-		HTp token = line->token(i);
-		track = token->getTrack();
-		if ((!m_stropheQ) && m_strophe[track]) {
-			continue;
-		}
-		if (!m_flipState[track]) {
-			continue;
-		}
-		if (m_kernQ) {
-			if (!token->isKern()) {
-				continue;
-			}
-		} else {
-			if (!token->isDataType(m_interp)) {
-				continue;
-			}
-		}
-		if (lastInsertTrack != track) {
-			flipees.resize(flipees.size() + 1);
-			lastInsertTrack = track;
-		}
-		flipees.back().push_back(token);
-	}
+Tool_fb::Tool_fb(void) {
+	define("c|compound=b",                               "output reasonable figured bass numbers within octave");
+	define("a|accidentals|accid|acc=b",                  "display accidentals in front of the numbers");
+	define("b|base|base-track=i:1",                      "number of the base kern track (compare with -k)");
+	define("i|intervallsatz=b",                          "display numbers under their voice instead of under the base staff");
+	define("o|sort|order=b",                             "sort figured bass numbers by size");
+	define("l|lowest=b",                                 "use lowest note as base note");
+	define("n|normalize=b",                              "remove number 8 and doubled numbers; adds -co");
+	define("r|reduce|abbreviate|abbr=b",                 "use abbreviated figures; adds -nco");
+	define("t|ties=b",                                   "hide numbers without attack or changing base (needs -i)");
+	define("f|figuredbass=b",                            "shortcut for -acorn3");
+	define("3|hide-three=b",                             "hide number 3 if it has an accidental");
+	define("m|negative=b",                               "show negative numbers");
+	define("above=b",                                    "show numbers above the staff (**fba)");
+	define("rate=s:",                                    "rate to display the numbers (use a **recip value, e.g. 4, 4.)");
+	define("k|kern-tracks=s",                            "process only the specified kern spines");
+	define("s|spine-tracks|spine|spines|track|tracks=s", "Process only the specified spines");
+	define("hint=b",                                     "determine harmonic intervals with interval quality");
 }
 
 
 
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_gasparize::Tool_gasparize -- Set the recognized options for the tool.
+// Tool_fb::run -- Do the main work of the tool.
 //
 
-Tool_gasparize::Tool_gasparize(void) {
-	define("R|no-reference-records=b",                "do not add reference records");
-	define("r|only-add-reference-records=b",          "only add reference records");
-
-	define("B|do-not-delete-breaks=b",                "do not delete system/page break markers");
-	define("b|only-delete-breaks=b",                  "only delete breaks");
+bool Tool_fb::run(HumdrumFileSet &infiles) {
+	bool status = true;
+	for (int i = 0; i < infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
 
-	define("A|do-not-fix-instrument-abbreviations=b", "do not fix instrument abbreviations");
-	define("a|only-fix-instrument-abbreviations=b",   "only fix instrument abbreviations");
-
-	define("E|do-not-fix-editorial-accidentals=b",    "do not fix instrument abbreviations");
-	define("e|only-fix-editorial-accidentals=b",      "only fix editorial accidentals");
-
-	define("T|do-not-add-terminal-longs=b",           "do not add terminal long markers");
-	define("t|only-add-terminal-longs=b",             "only add terminal longs");
-
-	define("no-ties=b",                               "do not fix tied notes");
-
-	define("N|do-not-remove-empty-transpositions=b",  "do not remove empty transposition instructions");
-	define ("n|only-remove-empty-transpositions=b",   "only remove empty transpositions");
-}
-
-
-
-/////////////////////////////////
-//
-// Tool_gasparize::run -- Primary interfaces to the tool.
-//
-
-bool Tool_gasparize::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
-
-bool Tool_gasparize::run(const string& indata, ostream& out) {
+bool Tool_fb::run(const string &indata, ostream &out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -84286,8 +84742,7 @@ bool Tool_gasparize::run(const string& indata, ostream& out) {
 	return status;
 }
 
-
-bool Tool_gasparize::run(HumdrumFile& infile, ostream& out) {
+bool Tool_fb::run(HumdrumFile &infile, ostream &out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -84297,19 +84752,9 @@ bool Tool_gasparize::run(HumdrumFile& infile, ostream& out) {
 	return status;
 }
 
-//
-// In-place processing of file:
-//
-
-bool Tool_gasparize::run(HumdrumFile& infile) {
+bool Tool_fb::run(HumdrumFile &infile) {
+	initialize();
 	processFile(infile);
-
-	// Re-load the text for each line from their tokens.
-	infile.createLinesFromTokens();
-
-	// Need to adjust the line numbers for tokens for later
-	// processing.
-	m_humdrum_text << infile;
 	return true;
 }
 
@@ -84317,1482 +84762,1519 @@ bool Tool_gasparize::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_gasparize::processFile --
+// Tool_fb::initialize --
 //
 
-void Tool_gasparize::processFile(HumdrumFile& infile) {
-
-	bool mensurationQ    = true;
-	bool articulationsQ  = true;
-	bool abbreviationsQ  = true;
-	bool accidentalsQ    = true;
-	bool referencesQ     = true;
-	bool terminalsQ      = true;
-	bool breaksQ         = true;
-	bool transpositionsQ = true;
-   bool tieQ            = true;
-   bool teditQ          = true;
-   bool instrumentQ     = true;
-   bool removekeydesigQ = true;
-   bool fixbarlinesQ    = true;
-   bool parenthesesQ    = true;
+void Tool_fb::initialize(void) {
+	m_compoundQ      = getBoolean("compound");
+	m_accidentalsQ   = getBoolean("accidentals");
+	m_baseTrackQ     = getInteger("base");
+	m_intervallsatzQ = getBoolean("intervallsatz");
+	m_sortQ          = getBoolean("sort");
+	m_lowestQ        = getBoolean("lowest");
+	m_normalizeQ     = getBoolean("normalize");
+	m_reduceQ        = getBoolean("reduce");
+	m_attackQ        = getBoolean("ties");
+	m_figuredbassQ   = getBoolean("figuredbass");
+	m_hideThreeQ     = getBoolean("hide-three");
+	m_showNegativeQ  = getBoolean("negative");
+	m_aboveQ         = getBoolean("above");
+	m_rateQ          = getString("rate");
+	m_hintQ          = getBoolean("hint");
 
-	if (getBoolean("no-reference-records")) { referencesQ = false; }
-	if (getBoolean("only-add-reference-records")) {
-		abbreviationsQ  = false;
-		accidentalsQ    = false;
-		referencesQ     = true;
-		terminalsQ      = false;
-		breaksQ         = false;
-		transpositionsQ = false;
+	if (getBoolean("spine-tracks")) {
+		m_spineTracks = getString("spine-tracks");
+	} else if (getBoolean("kern-tracks")) {
+		m_kernTracks = getString("kern-tracks");
 	}
 
-	if (getBoolean("do-not-delete-breaks")) { breaksQ = false; }
-	if (getBoolean("only-delete-breaks")) {
-		abbreviationsQ  = false;
-		accidentalsQ    = false;
-		referencesQ     = false;
-		terminalsQ      = false;
-		breaksQ         = true;
-		transpositionsQ = false;
+	if (m_normalizeQ) {
+		m_compoundQ = true;
+		m_sortQ = true;
 	}
 
-	if (getBoolean("do-not-fix-instrument-abbreviations")) { abbreviationsQ = false; }
-	if (getBoolean("only-fix-instrument-abbreviations")) {
-		abbreviationsQ  = true;
-		accidentalsQ    = false;
-		referencesQ     = false;
-		terminalsQ      = false;
-		breaksQ         = false;
-		transpositionsQ = false;
+	if (m_reduceQ) {
+		m_normalizeQ = true;
+		m_compoundQ = true;
+		m_sortQ = true;
 	}
 
-	if (getBoolean("do-not-fix-editorial-accidentals")) { accidentalsQ = false; }
-	if (getBoolean("only-fix-editorial-accidentals")) {
-		abbreviationsQ  = false;
-		accidentalsQ    = true;
-		referencesQ     = false;
-		terminalsQ      = false;
-		breaksQ         = false;
-		transpositionsQ = false;
+	if (m_figuredbassQ) {
+		m_reduceQ = true;
+		m_normalizeQ = true;
+		m_compoundQ = true;
+		m_sortQ = true;
+		m_accidentalsQ = true;
+		m_hideThreeQ = true;
 	}
 
-	if (getBoolean("do-not-add-terminal-longs")) { terminalsQ = false; }
-	if (getBoolean("only-add-terminal-longs")) {
-		abbreviationsQ  = false;
-		accidentalsQ    = false;
-		referencesQ     = false;
-		terminalsQ      = true;
-		breaksQ         = false;
-		transpositionsQ = false;
+	if (m_hintQ) {
+		m_showNegativeQ = true;
+		// m_lowestQ = true;
 	}
+}
 
-	if (getBoolean("do-not-remove-empty-transpositions")) { transpositionsQ = false; }
 
-	if (getBoolean("no-ties")) { tieQ = false; }
 
-	if (getBoolean("only-remove-empty-transpositions")) {
-		abbreviationsQ  = false;
-		accidentalsQ    = false;
-		referencesQ     = false;
-		terminalsQ      = false;
-		breaksQ         = false;
-		transpositionsQ = true;
-	}
+//////////////////////////////
+//
+// Tool_fb::processFile --
+//
 
-	if (articulationsQ)  { removeArticulations(infile); }
-	if (fixbarlinesQ)    { fixBarlines(infile); }
-	if (tieQ)            { fixTies(infile); }
-	if (abbreviationsQ)  { fixInstrumentAbbreviations(infile); }
-	if (accidentalsQ)    { fixEditorialAccidentals(infile); }
-	if (parenthesesQ)    { createJEditorialAccidentals(infile); }
-	if (referencesQ)     { addBibliographicRecords(infile); }
-	if (breaksQ)         { deleteBreaks(infile); }
-	if (terminalsQ)      { addTerminalLongs(infile); }
-	if (transpositionsQ) { deleteDummyTranspositions(infile); }
-	if (mensurationQ)    { addMensurations(infile); }
-	if (teditQ)          { createEditText(infile); }
-   if (instrumentQ)     { adjustIntrumentNames(infile); }
-   if (removekeydesigQ) { removeKeyDesignations(infile); }
+void Tool_fb::processFile(HumdrumFile& infile) {
 
-	adjustSystemDecoration(infile);
+	NoteGrid grid(infile);
 
-	// Input lyrics may contain "=" signs which are to be converted into
-	// spaces in **text data, and into elisions when displaying with verovio.
-	Tool_shed shed;
-	vector<string> argv;
-	argv.push_back("shed");
-	argv.push_back("-x");     // only apply to **text spines
-	argv.push_back("text");
-	argv.push_back("-e");
-	argv.push_back("s/=/ /g");
-	shed.process(argv);
-	shed.run(infile);
-}
+	vector<FiguredBassNumber*> numbers;
 
+	vector<HTp> kernspines = infile.getKernSpineStartList();
 
+	int maxTrack = infile.getMaxTrack();
 
-//////////////////////////////
-//
-// Tool_gasparize::removeArticulations --
-//
+	// Do nothing if base track not withing kern track range
+	if (m_baseTrackQ < 1 || m_baseTrackQ > maxTrack) {
+		return;
+	}
 
-void Tool_gasparize::removeArticulations(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
+	m_selectedKernSpines.resize(maxTrack + 1); // +1 is needed since track=0 is not used
+	// By default, process all tracks:
+	fill(m_selectedKernSpines.begin(), m_selectedKernSpines.end(), true);
+	// Otherwise, select which **kern track, or spine tracks to process selectively:
+
+	// Calculate which input spines to process based on -s or -k option:
+	if (!m_kernTracks.empty()) {
+		vector<int> ktracks = Convert::extractIntegerList(m_kernTracks, maxTrack);
+		fill(m_selectedKernSpines.begin(), m_selectedKernSpines.end(), false);
+		for (int i=0; i<(int)ktracks.size(); i++) {
+			int index = ktracks[i] - 1;
+			if ((index < 0) || (index >= (int)kernspines.size())) {
 				continue;
 			}
-			bool changed = false;
-			string text = token->getText();
-			if (text.find("'") != string::npos) {
-				// remove staccatos
-				changed = true;
-				hre.replaceDestructive(text, "", "'", "g");
-			}
-			if (text.find("~") != string::npos) {
-				// remove tenutos
-				changed = true;
-				hre.replaceDestructive(text, "", "~", "g");
-			}
-			if (changed) {
-				token->setText(text);
-			}
+			int track = kernspines.at(ktracks[i] - 1)->getTrack();
+			m_selectedKernSpines.at(track) = true;
 		}
+	} else if (!m_spineTracks.empty()) {
+		infile.makeBooleanTrackList(m_selectedKernSpines, m_spineTracks);
 	}
-}
-
 
+	vector<vector<int>> lastNumbers = {};
+	lastNumbers.resize((int)grid.getVoiceCount());
+	vector<vector<int>> currentNumbers = {};
 
-//////////////////////////////
-//
-// Tool_gasparize::adjustSystemDecoration --
-//    !!!system-decoration: [(s1)(s2)(s3)(s4)]
-// to:
-//    !!!system-decoration: [*]
-//
+	// Interate through the NoteGrid and fill the numbers vector with
+	// all generated FiguredBassNumbers
+	for (int i=0; i<(int)grid.getSliceCount(); i++) {
+		currentNumbers.clear();
+		currentNumbers.resize((int)grid.getVoiceCount());
 
-void Tool_gasparize::adjustSystemDecoration(HumdrumFile& infile) {
-	for (int i=infile.getLineCount() - 1; i>=0; i--) {
-		if (!infile[i].isReference()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (token->compare(0, 21, "!!!system-decoration:") == 0) {
-			token->setText("!!!system-decoration: [*]");
-			break;
-		}
-	}
-}
+		// Reset usedBaseKernTrack
+		int usedBaseKernTrack = m_baseTrackQ;
 
+		// Overwrite usedBaseKernTrack with the lowest voice index of the lowest pitched note
+		if (m_lowestQ) {
+			int lowestNotePitch = 99999;
+			for (int k=0; k<(int)grid.getVoiceCount(); k++) {
+				NoteCell* checkCell = grid.cell(k, i);
+				HTp currentToken = checkCell->getToken();
+				int initialTokenTrack = currentToken->getTrack();
 
+				// Handle spine splits
+				do {
+					HTp resolvedToken = currentToken->resolveNull();
+					int lowest = getLowestBase40Pitch(resolvedToken->getBase40Pitches());
 
-//////////////////////////////
-//
-// Tool_gasparize::deleteDummyTranspositions -- Somehow empty
-//    transpositions that go to the same pitch can appear in the
-//    MusicXML data, so remove them here.  Example:
-// 		*Trd0c0
-//
+					if (abs(lowest) < lowestNotePitch) {
+						lowestNotePitch = abs(lowest);
+						usedBaseKernTrack = k + 1;
+					}
 
-void Tool_gasparize::deleteDummyTranspositions(HumdrumFile& infile) {
-	vector<int> ldel;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			continue;
+					HTp nextToken = currentToken->getNextField();
+					if (nextToken && (initialTokenTrack == nextToken->getTrack())) {
+						currentToken = nextToken;
+					} else {
+						// Break loop if nextToken is not the same track as initialTokenTrack
+						break;
+					}
+				} while (currentToken);
+			}
 		}
-		if (!infile[i].isInterpretation()) {
+
+		NoteCell* baseCell = grid.cell(usedBaseKernTrack - 1, i);
+
+		// Ignore grace notes
+		if (baseCell->getToken()->getOwner()->getDuration() == 0) {
 			continue;
 		}
-		bool empty = true;
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (*token == "*") {
+
+		string keySignature = getKeySignature(infile, baseCell->getLineIndex());
+
+		// Hide numbers if they do not match rhythmic position of --rate
+		if (!m_rateQ.empty()) {
+			// Get time signatures
+			vector<pair<int, HumNum>> timeSigs;
+			infile.getTimeSigs(timeSigs, baseCell->getToken()->getTrack());
+			// Ignore numbers if they don't fit
+			if (hideNumbersForTokenLine(baseCell->getToken(), timeSigs[baseCell->getLineIndex()])) {
 				continue;
 			}
-			if (!token->isKern()) {
-				empty = false;
-				continue;
+		}
+
+
+		HTp currentToken = baseCell->getToken();
+		int initialTokenTrack = baseCell->getToken()->getTrack();
+		int lowestBaseNoteBase40Pitch = 9999;
+
+		// Handle spine splits
+		do {
+			HTp resolvedToken = currentToken->resolveNull();
+			int lowest = getLowestBase40Pitch(resolvedToken->getBase40Pitches());
+
+			// Ignore if base is a rest or silent note
+			if ((lowest != 0) && (lowest != -1000) && (lowest != -2000)) {
+				if(abs(lowest) < lowestBaseNoteBase40Pitch) {
+					lowestBaseNoteBase40Pitch = abs(lowest);
+				}
 			}
-			if (*token == "*Trd0c0") {
-				token->setText("*");
+
+			HTp nextToken = currentToken->getNextField();
+			if (nextToken && (initialTokenTrack == nextToken->getTrack())) {
+				currentToken = nextToken;
 			} else {
-				empty = false;
+				// Break loop if nextToken is not the same track as initialTokenTrack
+				break;
 			}
+		} while (currentToken);
+
+		// Ignore if base is a rest or silent note
+		if ((lowestBaseNoteBase40Pitch == 0) || (lowestBaseNoteBase40Pitch == -1000) || (lowestBaseNoteBase40Pitch == -2000) || (lowestBaseNoteBase40Pitch == 9999)) {
+			continue;
 		}
-		if (empty) {
-			ldel.push_back(i);
-		}
-	}
 
-	if (ldel.size() == 1) {
-		infile.deleteLine(ldel[0]);
-	} else if (ldel.size() > 1) {
-		cerr << "Warning: multiple transposition lines, not deleting them" << endl;
-	}
+		// Interate through each voice
+		for (int j=0; j<(int)grid.getVoiceCount(); j++) {
+			NoteCell* targetCell = grid.cell(j, i);
 
-}
+			// Ignore voice if track is not active by --kern-tracks or --spine-tracks
+			if (m_selectedKernSpines.at(targetCell->getToken()->getTrack()) == false) {
+				continue;
+			}
 
+			HTp currentToken = targetCell->getToken();
+			int initialTokenTrack = targetCell->getToken()->getTrack();
+			vector<FiguredBassNumber*> chordNumbers = {};
 
-//////////////////////////////
-//
-// Tool_gasparize::fixEditorialAccidentals -- checkDataLine() does
-//       all of the work for this function, which only manages
-//       key signature and barline processing.
-//    Rules for accidentals in Tasso in Music Project:
-//    (1) Only note accidentals printed in the source editions
-//        are displayed as regular accidentals.  These accidentals
-//        are postfixed with an "X" in the **kern data.
-//    (2) Editorial accidentals are given an "i" marker but not
-//        a "X" marker in the **kern data.  This editorial accidental
-//        is displayed above the note.
-//    This algorithm makes adjustments to the input data because
-//    Sibelius will drop editorial information after the frist
-//    editorial accidental on that pitch in the measure.
-//    (3) If a note is the same pitch as a previous note in the
-//        measure and the previous note has an editorial accidental,
-//        then make the note an editorial note.  However, if the
-//        accidental state of the note matches the key-signature,
-//        then do not add an editorial accidental, and there will be
-//        no accidental displayed on the note.  In that case, add a "y"
-//        after the accidental to indicate that it is interpreted
-//        and not visible in the original score.
-//
+			// Handle spine splits
+			do {
+				HTp resolvedToken = currentToken->resolveNull();
+				for (int subtokenBase40: resolvedToken->getBase40Pitches()) {
 
-void Tool_gasparize::fixEditorialAccidentals(HumdrumFile& infile) {
-	removeDoubledAccidentals(infile);
+					// Ignore if target is a rest or silent note
+					if ((subtokenBase40 == 0) || (subtokenBase40 == -1000) || (subtokenBase40 == -2000)) {
+						continue;
+					}
 
-	m_pstates.resize(infile.getMaxTrack() + 1);
-	m_estates.resize(infile.getMaxTrack() + 1);
-	m_kstates.resize(infile.getMaxTrack() + 1);
+					// Ignore if same pitch as base voice
+					if ((abs(lowestBaseNoteBase40Pitch) == abs(subtokenBase40)) && (baseCell->getToken()->getTrack() == initialTokenTrack)) {
+						continue;
+					}
 
-	for (int i=0; i<(int)m_pstates.size(); i++) {
-		m_pstates[i].resize(70);
-		fill(m_pstates[i].begin(), m_pstates[i].end(), 0);
-		m_kstates[i].resize(70);
-		fill(m_kstates[i].begin(), m_kstates[i].end(), 0);
-		m_estates[i].resize(70);
-		fill(m_estates[i].begin(), m_estates[i].end(), false);
+					// Create FiguredBassNumber
+					FiguredBassNumber* number = createFiguredBassNumber(abs(lowestBaseNoteBase40Pitch), abs(subtokenBase40), targetCell->getVoiceIndex(), targetCell->getLineIndex(), targetCell->isAttack(), keySignature);
+
+					currentNumbers[j].push_back(number->m_number);
+					chordNumbers.push_back(number);
+				}
+
+				HTp nextToken = currentToken->getNextField();
+				if (nextToken && (initialTokenTrack == nextToken->getTrack())) {
+						currentToken = nextToken;
+				} else {
+					// Break loop if nextToken is not the same track as initialTokenTrack
+					break;
+				}
+			} while (currentToken);
+
+			// Sort chord numbers by size
+			sort(chordNumbers.begin(), chordNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
+				return a->m_number > b->m_number;
+			});
+
+			// Then add to numbers vector
+			for (FiguredBassNumber*  num: chordNumbers) {
+				if (lastNumbers[j].size() != 0) {
+					// If a number belongs to a sustained note but the base note did change
+					// the new numbers need to be displayable
+					num->m_baseOfSustainedNoteDidChange = !num->m_isAttack && std::find(lastNumbers[j].begin(), lastNumbers[j].end(), num->m_number) == lastNumbers[j].end();
+				}
+				numbers.push_back(num);
+			}
+		}
+
+		// Set current numbers as the new last numbers
+		lastNumbers = currentNumbers;
 	}
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isInterpretation()) {
-			updateKeySignatures(infile, i);
-			continue;
-		} else if (infile[i].isBarline()) {
-			clearStates();
-			continue;
-		} else if (infile[i].isData()) {
-			checkDataLine(infile, i);
+	string exinterp = m_aboveQ ? "**fba" : "**fb";
+
+	if (m_hintQ) {
+		exinterp = "**hint";
+	}
+
+	if (m_intervallsatzQ) {
+		// Create **fb spine for each voice
+		for (int voiceIndex = 0; voiceIndex < grid.getVoiceCount(); voiceIndex++) {
+			vector<string> trackData = getTrackDataForVoice(voiceIndex, numbers, infile.getLineCount());
+			if (voiceIndex + 1 < grid.getVoiceCount()) {
+				int trackIndex = kernspines[voiceIndex + 1]->getTrack();
+				infile.insertDataSpineBefore(trackIndex, trackData, ".", exinterp);
+			} else {
+				infile.appendDataSpine(trackData, ".", exinterp);
+			}
+		}
+	} else {
+		// Create **fb spine and bind it to the base voice
+		vector<string> trackData = getTrackData(numbers, infile.getLineCount());
+		if (m_baseTrackQ < grid.getVoiceCount()) {
+			int trackIndex = kernspines[m_baseTrackQ]->getTrack();
+			infile.insertDataSpineBefore(trackIndex, trackData, ".", exinterp);
+		} else {
+			infile.appendDataSpine(trackData, ".", exinterp);
 		}
 	}
+
+	// Enables usage in verovio (`!!!filter: fb`)
+	m_humdrum_text << infile;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::removeDoubledAccidentals -- Often caused by transposition
-//    differences between parts in the MusicXML export from Finale.  Also some
-//    strange double sharps appear randomly.
+// Tool_fb::hideNumbersForTokenLine -- Checks if rhythmic position of line should display numbers
 //
 
-void Tool_gasparize::removeDoubledAccidentals(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
-				continue;
-			}
-			if (token->isRest()) {
-				continue;
-			}
-			if (token->find("--") != string::npos) {
-				string text = *token;
-				hre.replaceDestructive(text, "-", "--", "g");
-			} else if (token->find("--") != string::npos) {
-				string text = *token;
-				hre.replaceDestructive(text, "#", "##", "g");
-			}
+bool Tool_fb::hideNumbersForTokenLine(HTp token, pair<int, HumNum> timeSig) {
+	// Get note duration from --rate option
+	HumNum rateDuration = Convert::recipToDuration(m_rateQ);
+	if (rateDuration.toFloat() != 0) {
+		double timeSigBarDuration = timeSig.first * Convert::recipToDuration(to_string(timeSig.second.getInteger())).toFloat();
+		double durationFromBarline = token->getDurationFromBarline().toFloat();
+		// Handle upbeats
+		if (token->getBarlineDuration().toFloat() < timeSigBarDuration) {
+			// Fix durationFromBarline when current bar duration is shorter than
+			// the bar duration of the time signature
+			durationFromBarline = timeSigBarDuration - token->getDurationToBarline().toFloat();
 		}
+		// Checks if rhythmic position is divisible by rateDuration
+		return fmod(durationFromBarline, rateDuration.toFloat()) != 0;
 	}
+	return false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::addTerminalLongs -- Convert all last notes to terminal longs
-//    Also probably add terminal longs before double barlines as in JRP.
+// Tool_fb::getTrackData -- Create **fb spine data with formatted numbers for all voices
 //
 
-void Tool_gasparize::addTerminalLongs(HumdrumFile& infile) {
-	int scount = infile.getStrandCount();
-	for (int i=0; i<scount; i++) {
-		HTp cur = infile.getStrandEnd(i);
-		if (*cur != "*-") {
-			continue;
-		}
-		if (!cur->isKern()) {
-			continue;
+vector<string> Tool_fb::getTrackData(const vector<FiguredBassNumber*>& numbers, int lineCount) {
+	vector<string> trackData;
+	trackData.resize(lineCount);
+
+	for (int i = 0; i < lineCount; i++) {
+		vector<FiguredBassNumber*> sliceNumbers = filterFiguredBassNumbersForLine(numbers, i);
+		if (sliceNumbers.size() > 0) {
+			trackData[i] = formatFiguredBassNumbers(sliceNumbers);
 		}
-		while (cur) {
-			if (!cur->isData()) {
-				cur = cur->getPreviousToken();
-				continue;
-			}
-			if (cur->isNull()) {
-				cur = cur->getPreviousToken();
-				continue;
-			}
-			if (cur->isRest()) {
-				cur = cur->getPreviousToken();
-				continue;
-			}
-			if (cur->isSecondaryTiedNote()) {
-				cur = cur->getPreviousToken();
-				continue;
-			}
-			if (cur->find("l") != string::npos) {
-				// already marked so do not do it again
-				break;
-			}
-			// mark this note with "l"
-			string newtext = *cur;
-			newtext += "l";
-			cur->setText(newtext);
-			break;
+	}
+
+	return trackData;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_fb::getTrackDataForVoice -- Create **fb spine data with formatted numbers for passed voiceIndex
+//
+
+vector<string> Tool_fb::getTrackDataForVoice(int voiceIndex, const vector<FiguredBassNumber*>& numbers, int lineCount) {
+	vector<string> trackData;
+	trackData.resize(lineCount);
+
+	for (int i = 0; i < lineCount; i++) {
+		vector<FiguredBassNumber*> sliceNumbers = filterFiguredBassNumbersForLineAndVoice(numbers, i, voiceIndex);
+		if (sliceNumbers.size() > 0) {
+			trackData[i] = formatFiguredBassNumbers(sliceNumbers);
 		}
 	}
+
+	return trackData;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::fixInstrumentAbbreviations --
+// Tool_fb::createFiguredBassNumber -- Create FiguredBassNumber from a NoteCell.
+//    The figured bass number (num) is calculated with a base and target NoteCell
+//    as well as a passed key signature.
 //
 
-void Tool_gasparize::fixInstrumentAbbreviations(HumdrumFile& infile) {
-	int iline = -1;
-	int aline = -1;
+FiguredBassNumber* Tool_fb::createFiguredBassNumber(int basePitchBase40, int targetPitchBase40, int voiceIndex, int lineIndex, bool isAttack, string keySignature) {
 
-	vector<HTp> kerns = infile.getKernSpineStartList();
-	if (kerns.empty()) {
-		return;
+	// Calculate figured bass number
+	int baseDiatonicPitch   = Convert::base40ToDiatonic(basePitchBase40);
+	int targetDiatonicPitch = Convert::base40ToDiatonic(targetPitchBase40);
+	int diff        = abs(targetDiatonicPitch) - abs(baseDiatonicPitch);
+	int num;
+
+	if ((baseDiatonicPitch == 0) || (targetDiatonicPitch == 0)) {
+		num = 0;
+	} else if (diff == 0) {
+		num = 1;
+	} else if (diff > 0) {
+		num = diff + 1;
+	} else {
+		num = diff - 1;
 	}
 
-	HTp cur = kerns[0];
-	while (cur) {
-		if (cur->isData()) {
-			break;
-		}
-		if (cur->compare(0, 3, "*I\"") == 0) {
-			iline = cur->getLineIndex();
-		} else if (cur->compare(0, 3, "*I'") == 0) {
-			aline = cur->getLineIndex();
-		}
-		cur = cur->getNextToken();
+	// Transform key signature to lower case
+	transform(keySignature.begin(), keySignature.end(), keySignature.begin(), [](unsigned char c) {
+		return tolower(c);
+	});
+
+	char targetPitchName = Convert::kernToDiatonicLC(Convert::base40ToKern(targetPitchBase40));
+	int targetAccidNr = Convert::base40ToAccidental(targetPitchBase40);
+	string targetAccid;
+	for (int i=0; i<abs(targetAccidNr); i++) {
+		targetAccid += (targetAccidNr < 0 ? "-" : "#");
 	}
 
-	if (iline < 0) {
-		// no names to create abbreviations for
-		return;
+	char basePitchName = Convert::kernToDiatonicLC(Convert::base40ToKern(basePitchBase40));
+	int baseAccidNr = Convert::base40ToAccidental(basePitchBase40);
+	string baseAccid;
+	for (int i=0; i<abs(baseAccidNr); i++) {
+		baseAccid += (baseAccidNr < 0 ? "-" : "#");
 	}
-	if (aline < 0) {
-		// not creating a new abbreviation for now
-		// (could add later).
-		return;
+
+	string accid = targetAccid;
+	bool showAccid = false;
+
+	// Show accidentals when they are not included in the key signature
+	if ((targetAccidNr != 0) && (keySignature.find(targetPitchName + targetAccid) == std::string::npos)) {
+		showAccid = true;
 	}
-	if (infile[iline].getFieldCount() != infile[aline].getFieldCount()) {
-		// no spine splitting between the two lines.
-		return;
+
+	// Show natural accidentals when they are alterations of the key signature
+	if ((targetAccidNr == 0) && (keySignature.find(targetPitchName + targetAccid) != std::string::npos)) {
+		accid = "n";
+		showAccid = true;
 	}
-	// Maybe also require them to be adjacent to each other.
-	HumRegex hre;
-	for (int j=0; j<(int)infile[iline].getFieldCount(); j++) {
-		if (!infile.token(iline, j)->isKern()) {
-			continue;
-		}
-		if (!hre.search(*infile.token(iline, j), "([A-Za-z][A-Za-z .0-9]+)")) {
-			continue;
-		}
-		string name = hre.getMatch(1);
-		string abbr = "*I'";
-		if (name == "Basso Continuo") {
-			abbr += "BC";
-		} else if (name == "Basso continuo") {
-			abbr += "BC";
-		} else if (name == "basso continuo") {
-			abbr += "BC";
+
+	// Show accidentlas when pitch class of base and target is equal but alteration is different
+	if (basePitchName == targetPitchName) {
+		if (baseAccidNr == targetAccidNr) {
+			showAccid = false;
 		} else {
-			abbr += toupper(name[0]);
+			accid = (targetAccidNr == 0) ? "n" : targetAccid;
+			showAccid = true;
 		}
-		// check for numbers after the end of the name and add to abbreviation
-		infile.token(aline, j)->setText(abbr);
 	}
+
+	string intervalQuality = getIntervalQuality(basePitchBase40, targetPitchBase40);
+
+	FiguredBassNumber* number = new FiguredBassNumber(num, accid, showAccid, voiceIndex, lineIndex, isAttack, m_intervallsatzQ, intervalQuality, m_hintQ);
+
+	return number;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::convertBreaks --
+// Tool_fb::filterNegativeNumbers -- Hide negative numbers if m_showNegativeQ if not true
 //
 
-void Tool_gasparize::convertBreaks(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=infile.getLineCount()-1; i>= 0; i--) {
-		if (!infile[i].isGlobalComment()) {
-			continue;
-		}
-		if (hre.search(*infile.token(i, 0), "linebreak\\s*:\\s*original")) {
-			string text = "!!LO:LB:g=original";
-			infile[i].setText(text);
-		}
-		else if (hre.search(*infile.token(i, 0), "pagebreak\\s*:\\s*original")) {
-			string text = "!!LO:PB:g=original";
-			infile[i].setText(text);
-		}
-	}
+vector<FiguredBassNumber*> Tool_fb::filterNegativeNumbers(vector<FiguredBassNumber*> numbers) {
+
+	vector<FiguredBassNumber*> filteredNumbers;
+
+	bool mQ = m_showNegativeQ;
+	copy_if(numbers.begin(), numbers.end(), back_inserter(filteredNumbers), [mQ](FiguredBassNumber* num) {
+		return mQ ? true : (num->m_number > 0);
+	});
+
+	return filteredNumbers;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::deleteBreaks --
+// Tool_fb::filterFiguredBassNumbersForLine -- Find all FiguredBassNumber objects for a slice (line index) of the music.
 //
 
-void Tool_gasparize::deleteBreaks(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=infile.getLineCount()-1; i>= 0; i--) {
-		if (!infile[i].isGlobalComment()) {
-			continue;
-		}
-		if (hre.search(*infile.token(i, 0), "linebreak\\s*:\\s*original")) {
-			infile.deleteLine(i);
-		}
-		else if (hre.search(*infile.token(i, 0), "pagebreak\\s*:\\s*original")) {
-			infile.deleteLine(i);
-		}
-	}
+vector<FiguredBassNumber*> Tool_fb::filterFiguredBassNumbersForLine(vector<FiguredBassNumber*> numbers, int lineIndex) {
+
+	vector<FiguredBassNumber*> filteredNumbers;
+
+	// filter numbers with passed lineIndex
+	copy_if(numbers.begin(), numbers.end(), back_inserter(filteredNumbers), [lineIndex](FiguredBassNumber* num) {
+		return num->m_lineIndex == lineIndex;
+	});
+
+	// sort by voiceIndex
+	sort(filteredNumbers.begin(), filteredNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
+		return a->m_voiceIndex > b->m_voiceIndex;
+	});
+
+	return filterNegativeNumbers(filteredNumbers);
 }
 
 
-////////////////////////////////
+
+//////////////////////////////
 //
-// Tool_gasparize::addBibliographicRecords --
+// Tool_fb::filterFiguredBassNumbersForLineAndVoice --
 //
-// !!!COM:
-// !!!CDT:
-// !!!OTL:
-// !!!AGN:
-// !!!SCT:
-// !!!SCA:
-// !!!voices:
+
+vector<FiguredBassNumber*> Tool_fb::filterFiguredBassNumbersForLineAndVoice(vector<FiguredBassNumber*> numbers, int lineIndex, int voiceIndex) {
+
+	vector<FiguredBassNumber*> filteredNumbers;
+
+	// filter numbers with passed lineIndex and passed voiceIndex
+	copy_if(numbers.begin(), numbers.end(), back_inserter(filteredNumbers), [lineIndex, voiceIndex](FiguredBassNumber* num) {
+		return (num->m_lineIndex == lineIndex) && (num->m_voiceIndex == voiceIndex);
+	});
+
+	// sort by voiceIndex (probably not needed here)
+	sort(filteredNumbers.begin(), filteredNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
+		return a->m_voiceIndex > b->m_voiceIndex;
+	});
+
+	return filterNegativeNumbers(filteredNumbers);
+}
+
+
+
+//////////////////////////////
 //
-// At end:
-// !!!RDF**kern: l = terminal long
-// !!!RDF**kern: i = editorial accidental
-// !!!EED:
-// !!!EEV: $DATE
+// Tool_fb::formatFiguredBassNumbers -- Create a **fb data record string out of the passed FiguredBassNumber objects
 //
 
-void Tool_gasparize::addBibliographicRecords(HumdrumFile& infile) {
-	vector<HLp> refinfo = infile.getReferenceRecords();
-	map<string, HLp> refs;
-	for (int i=0; i<(int)refinfo.size(); i++) {
-		string key = refinfo[i]->getReferenceKey();
-		refs[key] = refinfo[i];
-	}
+string Tool_fb::formatFiguredBassNumbers(const vector<FiguredBassNumber*>& numbers) {
 
-	// header records
-	if (refs.find("voices") == refs.end()) {
-		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
-			infile.insertLine(1, "!!!voices:");
-		} else {
-			infile.insertLine(0, "!!!voices:");
-		}
-	}
-	if (refs.find("SCA") == refs.end()) {
-		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
-			infile.insertLine(1, "!!!SCA:");
-		} else {
-			infile.insertLine(0, "!!!SCA:");
-		}
-	}
-	if (refs.find("SCT") == refs.end()) {
-		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
-			infile.insertLine(1, "!!!SCT:");
-		} else {
-			infile.insertLine(0, "!!!SCT:");
-		}
+	vector<FiguredBassNumber*> formattedNumbers;
+
+	// Normalize numbers (remove 8 and 1, sort by size, remove duplicate numbers)
+	if (m_normalizeQ) {
+		bool aQ = m_accidentalsQ;
+		// remove 8 and 1 but keep them if they have an accidental
+		copy_if(numbers.begin(), numbers.end(), back_inserter(formattedNumbers), [aQ](FiguredBassNumber* num) {
+			return ((num->getNumberWithinOctave() != 8) && (num->getNumberWithinOctave() != 1)) || (aQ && num->m_showAccidentals);
+		});
+		// sort by size
+		sort(formattedNumbers.begin(), formattedNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
+			return a->getNumberWithinOctave() < b->getNumberWithinOctave();
+		});
+		// remove duplicate numbers
+		formattedNumbers.erase(unique(formattedNumbers.begin(), formattedNumbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) {
+			return a->getNumberWithinOctave() == b->getNumberWithinOctave();
+		}), formattedNumbers.end());
+	} else {
+		formattedNumbers = numbers;
 	}
-	if (refs.find("AGN") == refs.end()) {
-		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
-			infile.insertLine(1, "!!!AGN:");
-		} else {
-			infile.insertLine(0, "!!!AGN:");
-		}
+
+	// Hide numbers if they have no attack
+	if (m_intervallsatzQ && m_attackQ) {
+		vector<FiguredBassNumber*> attackNumbers;
+		copy_if(formattedNumbers.begin(), formattedNumbers.end(), back_inserter(attackNumbers), [](FiguredBassNumber* num) {
+			return num->m_isAttack || num->m_baseOfSustainedNoteDidChange;
+		});
+		formattedNumbers = attackNumbers;
 	}
 
-	if (refs.find("OTL") == refs.end()) {
-		infile.insertLine(0, "!!!OTL:");
+	// Analysze before sorting
+	if (m_compoundQ) {
+		formattedNumbers = analyzeChordNumbers(formattedNumbers);
 	}
-	if (refs.find("CDT") == refs.end()) {
-		infile.insertLine(0, "!!!CDT: ~1450-~1517");
+
+	// Sort numbers by size
+	if (m_sortQ) {
+		bool cQ = m_compoundQ;
+		sort(formattedNumbers.begin(), formattedNumbers.end(), [cQ](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
+			// sort by getNumberWithinOctave if compoundQ is true otherwise sort by number
+			return (cQ) ? a->getNumberWithinOctave() > b->getNumberWithinOctave() : a->m_number > b->m_number;
+		});
 	}
-	if (refs.find("COM") == refs.end()) {
-		infile.insertLine(0, "!!!COM: Gaspar van Weerbeke");
+
+	if (m_reduceQ) {
+		// Overwrite formattedNumbers with abbreviated numbers
+		formattedNumbers = getAbbreviatedNumbers(formattedNumbers);
 	}
 
-	// trailer records
-	bool foundi = false;
-	bool foundj = false;
-	bool foundl = false;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReference()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (token->find("!!!RDF**kern:") == string::npos) {
-			continue;
-		}
-		if (token->find("terminal breve") != string::npos) {
-			foundl = true;
-		} else if (token->find("editorial accidental") != string::npos) {
-			if (token->find("i =") != string::npos) {
-				foundi = true;
-			} else if (token->find("j =") != string::npos) {
-				foundj = true;
-			}
+	// join numbers
+	string str = "";
+	bool first = true;
+	for (FiguredBassNumber* number: formattedNumbers) {
+		string num = number->toString(m_compoundQ, m_accidentalsQ, m_hideThreeQ);
+		if (num.length() > 0) {
+			if (!first) str += " ";
+			first = false;
+			str += num;
 		}
 	}
-	if (!foundj) {
-		infile.appendLine("!!!RDF**kern: j = editorial accidental, optional, paren up");
-	}
-	if (!foundi) {
-		infile.appendLine("!!!RDF**kern: i = editorial accidental");
-	}
-	if (!foundl) {
-		infile.appendLine("!!!RDF**kern: l = terminal long");
-	}
+	return str;
+}
 
-	if (refs.find("PTL") == refs.end()) {
-		infile.appendLine("!!!PTL: Gaspar van Weerbeke: Collected Works. V. Settings of Liturgical Texts, Songs, and Instrumental Works");
-	}
-	if (refs.find("PPR") == refs.end()) {
-		infile.appendLine("!!!PPR: American Institute of Musicology");
-	}
-	if (refs.find("PC#") == refs.end()) {
-		infile.appendLine("!!!PC#: Corpus Mensurabilis Musicae 106/V");
-	}
-	if (refs.find("PDT") == refs.end()) {
-		infile.appendLine("!!!PDT: {YEAR}");
-	}
-	if (refs.find("PED") == refs.end()) {
-		infile.appendLine("!!!PED: Kolb, Paul");
-		infile.appendLine("!!!PED: Pavanello, Agnese");
-	}
-	if (refs.find("YEC") == refs.end()) {
-		infile.appendLine("!!!YEC: Copyright {YEAR}, Kolb, Paul");
-		infile.appendLine("!!!YEC: Copyright {YEAR}, Pavanello, Agnese");
-	}
-	if (refs.find("YEM") == refs.end()) {
-		infile.appendLine("!!!YEM: CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-nc/4.0/legalcode)");
-	}
-	if (refs.find("EED") == refs.end()) {
-		infile.appendLine("!!!EED: Zybina, Karina");
-		infile.appendLine("!!!EED: Mair-Gruber, Roland");
+
+
+//////////////////////////////
+//
+// Tool_fb::getAbbreviatedNumbers -- Get abbreviated figured bass numbers
+//    If no abbreviation is found all numbers will be shown
+
+vector<FiguredBassNumber*> Tool_fb::getAbbreviatedNumbers(const vector<FiguredBassNumber*>& numbers) {
+
+	vector<FiguredBassNumber*> abbreviatedNumbers;
+
+	string numberString = getNumberString(numbers);
+
+	// Check if an abbreviation exists for passed numbers
+	auto it = find_if(FiguredBassAbbreviationMapping::s_mappings.begin(), FiguredBassAbbreviationMapping::s_mappings.end(), [&numberString](const FiguredBassAbbreviationMapping& abbr) {
+		return abbr.m_str == numberString;
+	});
+
+	if (it != FiguredBassAbbreviationMapping::s_mappings.end()) {
+		const FiguredBassAbbreviationMapping& abbr = *it;
+		bool aQ = m_accidentalsQ;
+		// Store numbers to display by the abbreviation mapping in abbreviatedNumbers
+		copy_if(numbers.begin(), numbers.end(), back_inserter(abbreviatedNumbers), [&abbr, aQ](FiguredBassNumber* num) {
+			const vector<int>& nums = abbr.m_numbers;
+			// Show numbers if they are part of the abbreviation mapping or if they have an accidental
+			return (find(nums.begin(), nums.end(), num->getNumberWithinOctave()) != nums.end()) || (num->m_showAccidentals && aQ);
+		});
+
+		return abbreviatedNumbers;
 	}
-	if (refs.find("EEV") == refs.end()) {
-		string date = getDate();
-		string line = "!!!EEV: " + date;
-		infile.appendLine(line);
+
+	return numbers;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_fb::analyzeChordNumbers -- Analyze chord numbers and improve them
+//    Set m_convert2To9 to true when a 3 is included in the chord numbers.
+
+vector<FiguredBassNumber*> Tool_fb::analyzeChordNumbers(const vector<FiguredBassNumber*>& numbers) {
+
+	vector<FiguredBassNumber*> analyzedNumbers = numbers;
+
+	// Check if compound numbers 3 is withing passed numbers (chord)
+	auto it = find_if(analyzedNumbers.begin(), analyzedNumbers.end(), [](FiguredBassNumber* number) {
+		return number->getNumberWithinOctave() == 3;
+	});
+	if (it != analyzedNumbers.end()) {
+		for (auto &number : analyzedNumbers) {
+			number->m_convert2To9 = true;
+		}
 	}
+
+	return analyzedNumbers;
 }
 
 
 
-////////////////////////////////
+//////////////////////////////
 //
-// Tool_gasparize::checkDataLine --
+// Tool_fb::getNumberString -- Get only the numbers (without accidentals) of passed FiguredBassNumbers
 //
 
-void Tool_gasparize::checkDataLine(HumdrumFile& infile, int lineindex) {
-	HumdrumLine& line = infile[lineindex];
-
-	HumRegex hre;
-	HTp token;
-	bool haseditQ;
-	int base7;
-	int accid;
-	int track;
-	bool removeQ;
-	for (int i=0; i<line.getFieldCount(); i++) {
-		token = line.token(i);
-		track = token->getTrack();
-		if (!token->isKern()) {
-			continue;
-		}
-		if (token->isNull()) {
-			continue;
-		}
-		if (token->isRest()) {
-			continue;
-		}
-		if (token->find('j') != string::npos) {
-			continue;
-		}
-		if (token->isSecondaryTiedNote()) {
-			continue;
-		}
-
-		base7 = Convert::kernToBase7(token);
-		accid = Convert::kernToAccidentalCount(token);
-		haseditQ = false;
-		removeQ = false;
-
-		// Hard-wired to "i" as editorial accidental marker
-		if (token->find("ni") != string::npos) {
-			haseditQ = true;
-		} else if (token->find("-i") != string::npos) {
-			haseditQ = true;
-		} else if (token->find("#i") != string::npos) {
-			haseditQ = true;
-		} else if (token->find("nXi") != string::npos) {
-			haseditQ = true;
-			removeQ = true;
-		} else if (token->find("-Xi") != string::npos) {
-			haseditQ = true;
-			removeQ = true;
-		} else if (token->find("#Xi") != string::npos) {
-			haseditQ = true;
-			removeQ = true;
-		}
-
-		if (removeQ) {
-			string temp = *token;
-			hre.replaceDestructive(temp, "", "X");
-			token->setText(temp);
-		}
-
-		bool explicitQ = false;
-		if (token->find("#X") != string::npos) {
-			explicitQ = true;
-		} else if (token->find("-X") != string::npos) {
-			explicitQ = true;
-		} else if (token->find("nX") != string::npos) {
-			explicitQ = true;
-		} else if (token->find("n") != string::npos) {
-			// add an explicit accidental marker
-			explicitQ = true;
-			string text = *token;
-			hre.replaceDestructive(text, "nX", "n");
-			token->setText(text);
+string Tool_fb::getNumberString(vector<FiguredBassNumber*> numbers) {
+	// Sort numbers by size
+	sort(numbers.begin(), numbers.end(), [](FiguredBassNumber* a, FiguredBassNumber* b) -> bool {
+		return a->getNumberWithinOctave() > b->getNumberWithinOctave();
+	});
+	// join numbers
+	string str = "";
+	bool first = true;
+	for (FiguredBassNumber* nr: numbers) {
+		int num = nr->getNumberWithinOctave();
+		if (num > 0) {
+			if (!first) str += " ";
+			first = false;
+			str += to_string(num);
 		}
+	}
+	return str;
+}
 
-		if (haseditQ) {
-			// Store new editorial pitch state.
-			m_estates.at(track).at(base7) = true;
-			m_pstates.at(track).at(base7) = accid;
-			continue;
-		}
 
-		if (explicitQ) {
-			// No need to make editorial since it is visible.
-			m_estates.at(track).at(base7) = false;
-			m_pstates.at(track).at(base7) = accid;
-			continue;
-		}
 
-		if (accid == m_kstates.at(track).at(base7)) {
-			// 	!m_estates.at(track).at(base7)) {
-			// add !m_estates.at(track).at(base) as a condition if
-			// you want editorial accidentals to be added to return the
-			// note to the accidental in the key.
-			//
-			// The accidental matches the key-signature state,
-			// so it should not be made editorial eventhough
-			// it is not visible.
-			m_pstates.at(track).at(base7) = accid;
+//////////////////////////////
+//
+// Tool_fb::getKeySignature -- Get the key signature for a line index of the input file
+//
 
-			// Add a "y" marker of there is an interpreted accidental
-			// state (flat or sharp) that is part of the key signature.
-			int hasaccid = false;
-			if (token->find("#") != string::npos) {
-				hasaccid = true;
-			} else if (token->find("-") != string::npos) {
-				hasaccid = true;
-			}
-			int hashide = false;
-			if (token->find("-y") != string::npos) {
-				hashide = true;
-			}
-			else if (token->find("#y") != string::npos) {
-				hashide = true;
+string Tool_fb::getKeySignature(HumdrumFile& infile, int lineIndex) {
+	string keySignature = "";
+	[&] {
+		for (int i = 0; i < infile.getLineCount(); i++) {
+			if (i > lineIndex) {
+				return;
 			}
-			if (hasaccid && !hashide) {
-				string text = *token;
-				hre.replaceDestructive(text, "#y", "#");
-				hre.replaceDestructive(text, "-y", "-");
-				token->setText(text);
+			HLp line = infile.getLine(i);
+			for (int j = 0; j < line->getFieldCount(); j++) {
+				if (line->token(j)->isKeySignature()) {
+					keySignature = line->getTokenString(j);
+				}
 			}
-
-			continue;
 		}
+	}();
+	return keySignature;
+}
 
-		// At this point the previous note with this pitch class
-		// had an editorial accidental, and this note also has the
-		// same accidental, or there was a previous visual accidental
-		// outside of the key signature that will cause this note to have
-		// an editorial accidental mark applied (Sibelius will drop
-		// secondary editorial accidentals in a measure when exporting,
-		// MusicXML, which is why this function is needed).
 
-		m_estates[track][base7] = true;
-		m_pstates[track][base7] = accid;
 
-		string text = token->getText();
-		HumRegex hre;
-		hre.replaceDestructive(text, "#", "##+", "g");
-		hre.replaceDestructive(text, "-", "--+", "g");
-		string output = "";
-		bool foundQ = false;
-		for (int j=0; j<(int)text.size(); j++) {
-			if (text[j] == 'n') {
-				output += "ni";
-				foundQ = true;
-			} else if (text[j] == '#') {
-				output += "#i";
-				foundQ = true;
-			} else if (text[j] == '-') {
-				output += "-i";
-				foundQ = true;
-			} else {
-				output += text[j];
-			}
-		}
+//////////////////////////////
+//
+// Tool_fb::getLowestBase40Pitch -- Get lowest base 40 pitch that is not a rest or silent
+//    TODO: Handle negative values and sustained notes
+//
 
-		if (foundQ) {
-			token->setText(output);
-			continue;
-		}
+int Tool_fb::getLowestBase40Pitch(vector<int> base40Pitches) {
+	vector<int> filteredBase40Pitches;
+	copy_if(base40Pitches.begin(), base40Pitches.end(), std::back_inserter(filteredBase40Pitches), [](int base40Pitch) {
+		// Ignore if base is a rest or silent note
+		return (base40Pitch != -1000) && (base40Pitch != -2000) && (base40Pitch != 0);
+	});
 
-		// The note is natural, but has no natural sign.
-		// add the natural sign and editorial mark.
-		for (int j=(int)output.size()-1; j>=0; j--) {
-			if ((tolower(output[j]) >= 'a') && (tolower(output[j]) <= 'g')) {
-				output.insert(j+1, "ni");
-				break;
-			}
-		}
-		token->setText(output);
+	if (filteredBase40Pitches.size() == 0) {
+		return -2000;
 	}
+
+	return *min_element(begin(filteredBase40Pitches), end(filteredBase40Pitches));
 }
 
 
 
-////////////////////////////////
+//////////////////////////////
 //
-// Tool_gasparize::updateKeySignatures -- Fill in the accidental
-//    states for each diatonic pitch.
+// Tool_fb::getIntervalQuality -- Return interval quality prefix string
 //
 
-void Tool_gasparize::updateKeySignatures(HumdrumFile& infile, int lineindex) {
-	HumdrumLine& line = infile[lineindex];
-	int track;
-	for (int i=0; i<line.getFieldCount(); i++) {
-		if (!line.token(i)->isKeySignature()) {
-			continue;
-		}
-		HTp token = line.token(i);
-		track = token->getTrack();
-		string text = token->getText();
-		fill(m_kstates[track].begin(), m_kstates[track].end(), 0);
-		for (int j=3; j<(int)text.size()-1; j++) {
-			if (text[j] == ']') {
-				break;
-			}
-			switch (text[j]) {
-				case 'a': case 'A':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][5] = +1;
-						break;
-						case '-': m_kstates[track][5] = -1;
-						break;
-					}
-					break;
+string Tool_fb::getIntervalQuality(int basePitchBase40, int targetPitchBase40) {
 
-				case 'b': case 'B':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][6] = +1;
-						break;
-						case '-': m_kstates[track][6] = -1;
-						break;
-					}
-					break;
+	int diff = (targetPitchBase40 - basePitchBase40) % 40;
 
-				case 'c': case 'C':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][0] = +1;
-						break;
-						case '-': m_kstates[track][0] = -1;
-						break;
-					}
-					break;
+	diff = diff < -2 ? abs(diff) : diff;
 
-				case 'd': case 'D':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][1] = +1;
-						break;
-						case '-': m_kstates[track][1] = -1;
-						break;
-					}
-					break;
+	// See https://wiki.ccarh.org/wiki/Base_40
+	string quality;
+	switch (diff) {
+		// 1
+		case -2:
+		case 38:
+			quality = "dd"; break;
+		case -1:
+		case 39:
+			quality = "d"; break;
+		case 0: quality = "P"; break;
+		case 1: quality = "A"; break;
+		case 2: quality = "AA"; break;
 
-				case 'e': case 'E':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][2] = +1;
-						break;
-						case '-': m_kstates[track][2] = -1;
-						break;
-					}
-					break;
+		// 2
+		case 3: quality = "dd"; break;
+		case 4: quality = "d"; break;
+		case 5: quality = "m"; break;
+		case 6: quality = "M"; break;
+		case 7: quality = "A"; break;
+		case 8: quality = "AA"; break;
 
-				case 'f': case 'F':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][3] = +1;
-						break;
-						case '-': m_kstates[track][3] = -1;
-						break;
-					}
-					break;
+		// 3
+		case 9: quality = "dd"; break;
+		case 10: quality = "d"; break;
+		case 11: quality = "m"; break;
+		case 12: quality = "M"; break;
+		case 13: quality = "A"; break;
+		case 14: quality = "AA"; break;
 
-				case 'g': case 'G':
-					switch (text[j+1]) {
-						case '#': m_kstates[track][4] = +1;
-						break;
-						case '-': m_kstates[track][4] = -1;
-						break;
-					}
-					break;
-			}
-			for (int j=0; j<7; j++) {
-				if (m_kstates[track][j] == 0) {
-					continue;
-				}
-				for (int k=1; k<10; k++) {
-					m_kstates[track][j+k*7] = m_kstates[track][j];
-				}
-			}
-		}
-	}
+		// 4
+		case 15: quality = "dd"; break;
+		case 16: quality = "d"; break;
+		case 17: quality = "P"; break;
+		case 18: quality = "A"; break;
+		case 19: quality = "AA"; break;
 
-	// initialize m_pstates with contents of m_kstates
-	for (int i=0; i<(int)m_kstates.size(); i++) {
-		for (int j=0; j<(int)m_kstates[i].size(); j++) {
-			m_pstates[i][j] = m_kstates[i][j];
-		}
+		case 20: quality = "<unused>"; break;
+
+		// 5
+		case 21: quality = "dd"; break;
+		case 22: quality = "d"; break;
+		case 23: quality = "P"; break;
+		case 24: quality = "A"; break;
+		case 25: quality = "AA"; break;
+
+		// 6
+		case 26: quality = "dd"; break;
+		case 27: quality = "d"; break;
+		case 28: quality = "m"; break;
+		case 29: quality = "M"; break;
+		case 30: quality = "A"; break;
+		case 31: quality = "AA"; break;
+
+		// 7
+		case 32: quality = "dd"; break;
+		case 33: quality = "d"; break;
+		case 34: quality = "m"; break;
+		case 35: quality = "M"; break;
+		case 36: quality = "A"; break;
+		case 37: quality = "AA"; break;
+
+		default: quality = "?"; break;
 	}
 
+	return quality;
+
 }
 
 
 
-////////////////////////////////
+//////////////////////////////
 //
-// Tool_gasparize::clearStates --
+// FiguredBassNumber::FiguredBassNumber -- Constructor
 //
 
-void Tool_gasparize::clearStates(void) {
-	for (int i=0; i<(int)m_pstates.size(); i++) {
-		fill(m_pstates[i].begin(), m_pstates[i].end(), 0);
-	}
-	for (int i=0; i<(int)m_estates.size(); i++) {
-		fill(m_estates[i].begin(), m_estates[i].end(), false);
-	}
+FiguredBassNumber::FiguredBassNumber(int num, string accid, bool showAccid, int voiceIdx, int lineIdx, bool isAtk, bool intervallsatz, string intervalQuality, bool hint) {
+	m_number          = num;
+	m_accidentals     = accid;
+	m_voiceIndex      = voiceIdx;
+	m_lineIndex       = lineIdx;
+	m_showAccidentals = showAccid;
+	m_isAttack        = isAtk;
+	m_intervallsatz   = intervallsatz;
+	m_intervalQuality = intervalQuality;
+	m_hint            = hint;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_gasparize::getDate --
+// FiguredBassNumber::toString -- Convert FiguredBassNumber to a string (accidental + number)
 //
 
-string Tool_gasparize::getDate(void) {
-	time_t t = time(NULL);
-	tm* timeptr = localtime(&t);
-	stringstream ss;
-	int year = timeptr->tm_year + 1900;
-	int month = timeptr->tm_mon + 1;
-	int day = timeptr->tm_mday;
-	ss << year << "/";
-	if (month < 10) {
-		ss << "0";
+string FiguredBassNumber::toString(bool compoundQ, bool accidentalsQ, bool hideThreeQ) {
+	int num = (compoundQ) ? getNumberWithinOctave() : m_number;
+	if (m_hint) {
+		return m_intervalQuality + to_string(abs(num));
 	}
-	ss << month << "/";
-	if (day < 10) {
-		ss << "0";
+	string accid = (accidentalsQ && m_showAccidentals) ? m_accidentals : "";
+	if (((num == 3) || (num == -3)) && accidentalsQ && m_showAccidentals && hideThreeQ) {
+		return accid;
 	}
-	ss << day;
-	return ss.str();
+	if (num > 0) {
+		return accid + to_string(num);
+	}
+	if (num < 0) {
+		return accid + "~" + to_string(abs(num));
+	}
+	return "";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::fixTies --
-//    If a tie is unclosed or if a note is followed by an invisible rest, then fix.
-//
-
-void Tool_gasparize::fixTies(HumdrumFile& infile) {
-	int strands = infile.getStrandCount();
-	for (int i=0; i<strands; i++) {
-		HTp sstart = infile.getStrandStart(i);
-		if (!sstart) {
-			continue;
-		}
-		if (!sstart->isKern()) {
-			continue;
-		}
-		HTp send   = infile.getStrandEnd(i);
-		fixTiesForStrand(sstart, send);
-	}
-	fixTieStartEnd(infile);
-}
+// FiguredBassNumber::getNumberWithinOctave -- Get a reasonable figured bass number
+//    Replace 0 with 7 and -7
+//    Replace 1 with 8 and -8
+//    Replace 2 with 9 if it is a suspension of the ninth
+//    Allow 1 (unisono) in intervallsatz
 
+int FiguredBassNumber::getNumberWithinOctave(void) {
+	int num = m_number % 7;
 
+	// Replace 0 with 7 and -7
+	if ((abs(m_number) > 0) && (m_number % 7 == 0)) {
+		return m_number < 0 ? -7 : 7;
+	}
 
-void Tool_gasparize::fixTieStartEnd(HumdrumFile& infile) {
-	int strands = infile.getStrandCount();
-	for (int i=0; i<strands; i++) {
-		HTp sstart = infile.getStrandStart(i);
-		if (!sstart) {
-			continue;
-		}
-		if (!sstart->isKern()) {
-			continue;
+	// Replace 1 with 8 and -8
+	if (abs(num) == 1) {
+		// Allow unisono in intervallsatz
+		if (m_intervallsatz || m_hint) {
+			if (abs(m_number) == 1) {
+				return 1;
+			}
 		}
-		HTp send   = infile.getStrandEnd(i);
-		fixTiesStartEnd(sstart, send);
+		return m_number < 0 ? -8 : 8;
+	}
+
+	// Replace 2 with 9 if m_convert2To9 is true (e.g. when a 3 is included in the chord numbers)
+	if (m_convert2To9 && (num == 2)) {
+		return 9;
 	}
+
+	return num;
 }
 
 
 
-void Tool_gasparize::fixTiesStartEnd(HTp starts, HTp ends) {
-	HTp current = starts;
-	HumRegex hre;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if ((current->find('[') != string::npos) &&
-				(current->find(']') != string::npos) &&
-				(current->find(' ') == string::npos)) {
-			string text = *current;
-			hre.replaceDestructive(text, "", "\\[", "g");
-			hre.replaceDestructive(text, "_", "\\]", "g");
-			current->setText(text);
-		}
-		current = current->getNextToken();
-	}
+//////////////////////////////
+//
+// FiguredBassAbbreviationMapping::FiguredBassAbbreviationMapping -- Constructor
+//    Helper class to store the mappings for abbreviate figured bass numbers
+//
+
+FiguredBassAbbreviationMapping::FiguredBassAbbreviationMapping(string s, vector<int> n) {
+	m_str = s;
+	m_numbers = n;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_gasparize::fixTiesForStrand --
+// FiguredBassAbbreviationMapping::s_mappings -- Mapping to abbreviate figured bass numbers
 //
 
-void Tool_gasparize::fixTiesForStrand(HTp sstart, HTp send) {
-	if (!sstart) {
-		return;
-	}
-	HTp current = sstart;
-	HTp last = NULL;
-	current = current->getNextToken();
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (last == NULL) {
-			last = current;
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->find("yy") != string::npos) {
-			fixTieToInvisibleRest(last, current);
-		} else if (((last->find("[") != string::npos) || (last->find("_") != string::npos))
-				&& ((current->find("]") == string::npos) && (current->find("_") == string::npos))) {
-			fixHangingTie(last, current);
-		}
-		last = current;
-		current = current->getNextToken();
-	}
-}
+const vector<FiguredBassAbbreviationMapping> FiguredBassAbbreviationMapping::s_mappings = {
+	FiguredBassAbbreviationMapping("3", {}),
+	FiguredBassAbbreviationMapping("5", {}),
+	FiguredBassAbbreviationMapping("5 3", {}),
+	FiguredBassAbbreviationMapping("6 3", {6}),
+	FiguredBassAbbreviationMapping("5 4", {4}),
+	FiguredBassAbbreviationMapping("7 5 3", {7}),
+	FiguredBassAbbreviationMapping("7 3", {7}),
+	FiguredBassAbbreviationMapping("7 5", {7}),
+	FiguredBassAbbreviationMapping("6 5 3", {6, 5}),
+	FiguredBassAbbreviationMapping("6 4 3", {4, 3}),
+	FiguredBassAbbreviationMapping("6 4 2", {4, 2}),
+	FiguredBassAbbreviationMapping("9 5 3", {9}),
+	FiguredBassAbbreviationMapping("9 5", {9}),
+	FiguredBassAbbreviationMapping("9 3", {9}),
+};
 
 
 
-//////////////////////////////
+#define RUNTOOL(NAME, INFILE, COMMAND, STATUS)     \
+	Tool_##NAME *tool = new Tool_##NAME;            \
+	tool->process(COMMAND);                         \
+	tool->run(INFILE);                              \
+	if (tool->hasError()) {                         \
+		status = false;                              \
+		tool->getError(cerr);                        \
+		delete tool;                                 \
+		break;                                       \
+	} else if (tool->hasHumdrumText()) {            \
+		INFILE.readString(tool->getHumdrumText());   \
+	}                                               \
+	delete tool;
+
+#define RUNTOOL2(NAME, INFILE1, INFILE2, COMMAND, STATUS) \
+	Tool_##NAME *tool = new Tool_##NAME;            \
+	tool->process(COMMAND);                         \
+	tool->run(INFILE1, INFILE2);                    \
+	if (tool->hasError()) {                         \
+		status = false;                              \
+		tool->getError(cerr);                        \
+		delete tool;                                 \
+		break;                                       \
+	} else if (tool->hasHumdrumText()) {            \
+		INFILE1.readString(tool->getHumdrumText());  \
+	}                                               \
+	delete tool;
+
+#define RUNTOOLSET(NAME, INFILES, COMMAND, STATUS) \
+	Tool_##NAME *tool = new Tool_##NAME;            \
+	tool->process(COMMAND);                         \
+	tool->run(INFILES);                             \
+	if (tool->hasError()) {                         \
+		status = false;                              \
+		tool->getError(cerr);                        \
+		delete tool;                                 \
+		break;                                       \
+	} else if (tool->hasHumdrumText()) {            \
+		INFILES.readString(tool->getHumdrumText());  \
+	}                                               \
+	delete tool;
+
+#define RUNTOOLSTREAM(NAME, INFILES, COMMAND, STATUS) \
+	Tool_##NAME *tool = new Tool_##NAME;               \
+	tool->process(COMMAND);                            \
+	tool->run(INFILES);                                \
+	if (tool->hasError()) {                            \
+		status = false;                                 \
+		tool->getError(cerr);                           \
+		delete tool;                                    \
+		break;                                          \
+	} else if (tool->hasHumdrumText()) {               \
+		INFILES.readString(tool->getHumdrumText());     \
+	}                                                  \
+	delete tool;
+
+
+
+////////////////////////////////
 //
-// Tool_gasparize::fixTieToInvisibleRest --
+// Tool_filter::Tool_filter -- Set the recognized options for the tool.
 //
 
-void Tool_gasparize::fixTieToInvisibleRest(HTp first, HTp second) {
-	if (second->find("yy") == string::npos) {
-		return;
-	}
-	if ((first->find("[") == string::npos) && (first->find("_") == string::npos)) {
-		string ftext = *first;
-		ftext = "[" + ftext;
-		first->setText(ftext);
-	}
-	HumRegex hre;
-	if (!hre.search(first, "([A-Ga-g#n-]+)")) {
-		return;
-	}
-	string pitch = hre.getMatch(1);
-	pitch += "]";
-	string text = *second;
-	hre.replaceDestructive(text, pitch, "ryy");
-	second->setText(text);
+Tool_filter::Tool_filter(void) {
+	define("debug=b",      "print debug statement");
+	define("v|variant=s:", "Run filters labeled with the given variant");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_gasparize::fixHangingTie -- Not dealing with chain of missing ties.
+// Tool_filter::run -- Primary interfaces to the tool.
 //
 
-void Tool_gasparize::fixHangingTie(HTp first, HTp second) {
-	string text = *second;
-	text += "]";
-	second->setText(text);
+bool Tool_filter::run(const string& indata) {
+	HumdrumFileSet infiles(indata);
+	bool status = run(infiles);
+	return status;
 }
 
 
+bool Tool_filter::run(HumdrumFile& infile) {
+	HumdrumFileSet infiles;
+	infiles.appendHumdrumPointer(&infile);
+	bool status = run(infiles);
+	infiles.clearNoFree();
+	return status;
+}
 
-//////////////////////////////
-//
-// Tool_gasparize::addMensurations -- Add mensurations.
-//
+bool Tool_filter::runUniversal(HumdrumFileSet& infiles) {
+	bool status = true;
+	vector<pair<string, string> > commands;
+	getUniversalCommandList(commands, infiles);
 
-void Tool_gasparize::addMensurations(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=infile.getLineCount() - 1; i>=0; i--) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, 0);
-			if (hre.search(token, "^\\*M(\\d+)/(\\d+)")) {
-				int value = hre.getMatchInt(1);
-				addMensuration(value, infile, i);
-			}
+	for (int i=0; i<(int)commands.size(); i++) {
+		if (commands[i].first == "humdiff") {
+			RUNTOOLSET(humdiff, infiles, commands[i].second, status);
+		} else if (commands[i].first == "chooser") {
+			RUNTOOLSET(chooser, infiles, commands[i].second, status);
+		} else if (commands[i].first == "myank") {
+			RUNTOOL(myank, infiles, commands[i].second, status);
 		}
 	}
-}
 
+	removeUniversalFilterLines(infiles);
+
+	return status;
+}
 
 
-//////////////////////////////
 //
-// Tool_gasparize::addMensuration --
+// In-place processing of file:
 //
 
-void Tool_gasparize::addMensuration(int top, HumdrumFile& infile, int index) {
-	HTp checktoken = infile[index+1].token(0);
-	if (!checktoken) {
-		return;
+bool Tool_filter::run(HumdrumFileSet& infiles) {
+	if (infiles.getCount() == 0) {
+		return false;
 	}
-	if (checktoken->find("met") != string::npos) {
-		return;
+
+	initialize(infiles[0]);
+
+	HumdrumFile& infile = infiles[0];
+
+	#ifdef __EMSCRIPTEN__
+	bool optionList = getBoolean("options");
+	if (optionList) {
+		printEmscripten(m_humdrum_text);
+		m_humdrum_text << infile;
 	}
-	int fieldcount = infile[index].getFieldCount();
-	string line = "*";
-	HTp token = infile[index].token(0);
-	if (token->isKern()) {
-		if (top == 2) {
-			line += "met(C|)";
+	#endif
+
+	bool status = true;
+	vector<pair<string, string> > commands;
+	getCommandList(commands, infile);
+	for (int i=0; i<(int)commands.size(); i++) {
+		if (commands[i].first == "addic") {
+			RUNTOOL(addic, infile, commands[i].second, status);
+		} else if (commands[i].first == "addkey") {
+			RUNTOOL(addkey, infile, commands[i].second, status);
+		} else if (commands[i].first == "addlabels") {
+			RUNTOOL(addlabels, infile, commands[i].second, status);
+		} else if (commands[i].first == "addtempo") {
+			RUNTOOL(addtempo, infile, commands[i].second, status);
+		} else if (commands[i].first == "autoaccid") {
+			RUNTOOL(autoaccid, infile, commands[i].second, status);
+		} else if (commands[i].first == "autobeam") {
+			RUNTOOL(autobeam, infile, commands[i].second, status);
+		} else if (commands[i].first == "autostem") {
+			RUNTOOL(autostem, infile, commands[i].second, status);
+		} else if (commands[i].first == "binroll") {
+			RUNTOOL(binroll, infile, commands[i].second, status);
+		} else if (commands[i].first == "chantize") {
+			RUNTOOL(chantize, infile, commands[i].second, status);
+		} else if (commands[i].first == "chint") {
+			RUNTOOL(chint, infile, commands[i].second, status);
+		} else if (commands[i].first == "chord") {
+			RUNTOOL(chord, infile, commands[i].second, status);
+		} else if (commands[i].first == "cint") {
+			RUNTOOL(cint, infile, commands[i].second, status);
+		} else if (commands[i].first == "cmr") {
+			RUNTOOL(cmr, infile, commands[i].second, status);
+		} else if (commands[i].first == "composite") {
+			RUNTOOL(composite, infile, commands[i].second, status);
+		} else if (commands[i].first == "dissonant") {
+			RUNTOOL(dissonant, infile, commands[i].second, status);
+		} else if (commands[i].first == "double") {
+			RUNTOOL(double, infile, commands[i].second, status);
+		} else if (commands[i].first == "fb") {
+			RUNTOOL(fb, infile, commands[i].second, status);
+		} else if (commands[i].first == "flipper") {
+			RUNTOOL(flipper, infile, commands[i].second, status);
+		} else if (commands[i].first == "filter") {
+			RUNTOOL(filter, infile, commands[i].second, status);
+		} else if (commands[i].first == "gasparize") {
+			RUNTOOL(gasparize, infile, commands[i].second, status);
+		} else if (commands[i].first == "half") {
+			RUNTOOL(half, infile, commands[i].second, status);
+		} else if (commands[i].first == "hands") {
+			RUNTOOL(hands, infile, commands[i].second, status);
+		} else if (commands[i].first == "homorhythm") {
+			RUNTOOL(homorhythm, infile, commands[i].second, status);
+		} else if (commands[i].first == "homorhythm2") {
+			RUNTOOL(homorhythm2, infile, commands[i].second, status);
+		} else if (commands[i].first == "hproof") {
+			RUNTOOL(hproof, infile, commands[i].second, status);
+		} else if (commands[i].first == "humbreak") {
+			RUNTOOL(humbreak, infile, commands[i].second, status);
+		} else if (commands[i].first == "humsheet") {
+			RUNTOOL(humsheet, infile, commands[i].second, status);
+		} else if (commands[i].first == "humtr") {
+			RUNTOOL(humtr, infile, commands[i].second, status);
+		} else if (commands[i].first == "imitation") {
+			RUNTOOL(imitation, infile, commands[i].second, status);
+		} else if (commands[i].first == "instinfo") {
+			RUNTOOL(instinfo, infile, commands[i].second, status);
+		} else if (commands[i].first == "kern2mens") {
+			RUNTOOL(kern2mens, infile, commands[i].second, status);
+		} else if (commands[i].first == "kernify") {
+			RUNTOOL(kernify, infile, commands[i].second, status);
+		} else if (commands[i].first == "kernview") {
+			RUNTOOL(kernview, infile, commands[i].second, status);
+		} else if (commands[i].first == "melisma") {
+			RUNTOOL(melisma, infile, commands[i].second, status);
+		} else if (commands[i].first == "mens2kern") {
+			RUNTOOL(mens2kern, infile, commands[i].second, status);
+		} else if (commands[i].first == "meter") {
+			RUNTOOL(meter, infile, commands[i].second, status);
+		} else if (commands[i].first == "metlev") {
+			RUNTOOL(metlev, infile, commands[i].second, status);
+		} else if (commands[i].first == "modori") {
+			RUNTOOL(modori, infile, commands[i].second, status);
+		} else if (commands[i].first == "msearch") {
+			RUNTOOL(msearch, infile, commands[i].second, status);
+		} else if (commands[i].first == "nproof") {
+			RUNTOOL(nproof, infile, commands[i].second, status);
+		} else if (commands[i].first == "ordergps") {
+			RUNTOOL(ordergps, infile, commands[i].second, status);
+		} else if (commands[i].first == "pbar") {
+			RUNTOOL(pbar, infile, commands[i].second, status);
+		} else if (commands[i].first == "phrase") {
+			RUNTOOL(phrase, infile, commands[i].second, status);
+		} else if (commands[i].first == "pline") {
+			RUNTOOL(pline, infile, commands[i].second, status);
+		} else if (commands[i].first == "prange") {
+			RUNTOOL(prange, infile, commands[i].second, status);
+		} else if (commands[i].first == "recip") {
+			RUNTOOL(recip, infile, commands[i].second, status);
+		} else if (commands[i].first == "restfill") {
+			RUNTOOL(restfill, infile, commands[i].second, status);
+		} else if (commands[i].first == "rphrase") {
+			RUNTOOL(rphrase, infile, commands[i].second, status);
+		} else if (commands[i].first == "sab2gs") {
+			RUNTOOL(sab2gs, infile, commands[i].second, status);
+		} else if (commands[i].first == "scordatura") {
+			RUNTOOL(scordatura, infile, commands[i].second, status);
+		} else if (commands[i].first == "semitones") {
+			RUNTOOL(semitones, infile, commands[i].second, status);
+		} else if (commands[i].first == "shed") {
+			RUNTOOL(shed, infile, commands[i].second, status);
+		} else if (commands[i].first == "sic") {
+			RUNTOOL(sic, infile, commands[i].second, status);
+		} else if (commands[i].first == "simat") {
+			RUNTOOL2(simat, infile, infile, commands[i].second, status);
+		} else if (commands[i].first == "slurcheck") {
+			RUNTOOL(slurcheck, infile, commands[i].second, status);
+		} else if (commands[i].first == "slur") {
+			RUNTOOL(slurcheck, infile, commands[i].second, status);
+		} else if (commands[i].first == "spinetrace") {
+			RUNTOOL(spinetrace, infile, commands[i].second, status);
+		} else if (commands[i].first == "strophe") {
+			RUNTOOL(strophe, infile, commands[i].second, status);
+		} else if (commands[i].first == "synco") {
+			RUNTOOL(synco, infile, commands[i].second, status);
+		} else if (commands[i].first == "tabber") {
+			RUNTOOL(tabber, infile, commands[i].second, status);
+		} else if (commands[i].first == "tandeminfo") {
+			RUNTOOL(tandeminfo, infile, commands[i].second, status);
+		} else if (commands[i].first == "tassoize") {
+			RUNTOOL(tassoize, infile, commands[i].second, status);
+		} else if (commands[i].first == "tassoise") {
+			RUNTOOL(tassoize, infile, commands[i].second, status);
+		} else if (commands[i].first == "tasso") {
+			RUNTOOL(tassoize, infile, commands[i].second, status);
+		} else if (commands[i].first == "textdur") {
+			RUNTOOL(textdur, infile, commands[i].second, status);
+		} else if (commands[i].first == "tie") {
+			RUNTOOL(tie, infile, commands[i].second, status);
+		} else if (commands[i].first == "tspos") {
+			RUNTOOL(tspos, infile, commands[i].second, status);
+		} else if (commands[i].first == "transpose") {
+			RUNTOOL(transpose, infile, commands[i].second, status);
+		} else if (commands[i].first == "tremolo") {
+			RUNTOOL(tremolo, infile, commands[i].second, status);
+		} else if (commands[i].first == "trillspell") {
+			RUNTOOL(trillspell, infile, commands[i].second, status);
+		} else if (commands[i].first == "vcross") {
+			RUNTOOL(vcross, infile, commands[i].second, status);
+
+		// filters with aliases:
+
+		} else if (commands[i].first == "colortriads") {
+			RUNTOOL(colortriads, infile, commands[i].second, status);
+		} else if (commands[i].first == "colourtriads") {
+			// British spelling
+			RUNTOOL(colortriads, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "colorthirds") {
+			RUNTOOL(tspos, infile, commands[i].second, status);
+		} else if (commands[i].first == "colourthirds") {
+			// British spelling
+			RUNTOOL(tspos, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "colorgroups") {
+			RUNTOOL(colorgroups, infile, commands[i].second, status);
+		} else if (commands[i].first == "colourgroups") { // British spelling
+			RUNTOOL(colorgroups, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "deg") { // humlib version of Humdrum Toolkit deg tool
+			RUNTOOL(deg, infile, commands[i].second, status);
+		} else if (commands[i].first == "degx") { // humlib cli name
+			RUNTOOL(deg, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "extract") { // humlib version of Humdrum Toolkit extract tool
+			RUNTOOL(extract, infile, commands[i].second, status);
+		} else if (commands[i].first == "extractx") { // humlib cli name
+			RUNTOOL(extract, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "grep") {
+			RUNTOOL(grep, infile, commands[i].second, status);
+		} else if (commands[i].first == "humgrep") {
+			RUNTOOL(grep, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "myank") { // humlib version of Humdrum Extras myank tool
+			RUNTOOL(myank, infile, commands[i].second, status);
+		} else if (commands[i].first == "myankx") { // humlib cli name
+			RUNTOOL(myank, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "rid") { // humlib version of Humdrum Toolkit deg tool
+			RUNTOOL(rid, infile, commands[i].second, status);
+		} else if (commands[i].first == "ridx") { // Humdrum Extra cli name
+			RUNTOOL(rid, infile, commands[i].second, status);
+		} else if (commands[i].first == "ridxx") { // humlib cli name
+			RUNTOOL(rid, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "satb2gs") { // humlib version of Humdrum Extras satg2gs tool
+			RUNTOOL(satb2gs, infile, commands[i].second, status);
+		} else if (commands[i].first == "satb2gsx") { // humlib cli name
+			RUNTOOL(satb2gs, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "thru") { // humlib version of Humdrum Toolkit thru tool
+			RUNTOOL(thru, infile, commands[i].second, status);
+		} else if (commands[i].first == "thrux") { // Humdrum Extras cli name
+			RUNTOOL(thru, infile, commands[i].second, status);
+		} else if (commands[i].first == "thruxx") { // humlib cli name
+			RUNTOOL(thru, infile, commands[i].second, status);
+
+		} else if (commands[i].first == "timebase") { // humlib version of Humdrum Toolkit timebase tool
+			RUNTOOL(timebase, infile, commands[i].second, status);
+		} else if (commands[i].first == "timebasex") { // humlib cli name
+			RUNTOOL(timebase, infile, commands[i].second, status);
 		} else {
-			line += "met(O)";
-		}
-	}
-	for (int i=1; i<fieldcount; i++) {
-		line += "\t*";
-		HTp token = infile[index].token(i);
-		if (token->isKern()) {
-			if (top == 2) {
-				line += "met(C|)";
-			} else {
-				line += "met(O)";
-			}
+			cerr << "UNKNOWN FILTER: " << commands[i].first << " OPTIONS: " << commands[i].second << endl;
 		}
+
 	}
-	infile.insertLine(index+1, line);
+
+	removeGlobalFilterLines(infile);
+
+	// Re-load the text for each line from their tokens in case any
+	// updates are needed from token changes.
+	infile.createLinesFromTokens();
+	return status;
 }
 
 
-///////////////////////////////
+
+//////////////////////////////
 //
-// Tool_gasparize::createEditText -- Convert <i> markers into *edit interps.
+// Tool_filter::removeGlobalFilterLines --
 //
 
-void Tool_gasparize::createEditText(HumdrumFile& infile) {
-	// previous process manipulated the structure so reanalyze here for now:
-	infile.analyzeBaseFromTokens();
-	infile.analyzeStructureNoRhythm();
+void Tool_filter::removeGlobalFilterLines(HumdrumFile& infile) {
+	HumRegex hre;
+	string text;
 
-	int strands = infile.getStrandCount();
-	for (int i=0; i<strands; i++) {
-		HTp sstart = infile.getStrandStart(i);
-		if (!sstart) {
-			continue;
-		}
-		if (!sstart->isDataType("**text")) {
+	string maintag = "!!!filter:";
+	string mainXtag = "!!!Xfilter:";
+	string maintagQuery = "^!!!filter:";
+
+	string maintagV;
+	string mainXtagV;
+	string maintagQueryV;
+
+	if (m_variant.size() > 0) {
+		maintagV = "!!!filter-" + m_variant + ":";
+		mainXtagV = "!!!Xfilter-" + m_variant + ":";
+		maintagQueryV = "^!!!filter-" + m_variant + ":";
+	}
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReference()) {
 			continue;
 		}
-		HTp send   = infile.getStrandEnd(i);
-		bool status = addEditStylingForText(infile, sstart, send);
-		if (status) {
-			infile.analyzeBaseFromTokens();
-			infile.analyzeStructureNoRhythm();
+
+		if (m_variant.size() > 0) {
+			if (infile.token(i, 0)->compare(0, maintagV.size(), maintagV) == 0) {
+				text = infile.token(i, 0)->getText();
+				hre.replaceDestructive(text, mainXtagV, maintagQueryV);
+				infile.token(i, 0)->setText(text);
+			}
+		} else {
+			if (infile.token(i, 0)->compare(0, maintag.size(), maintag) == 0) {
+				text = infile.token(i, 0)->getText();
+				hre.replaceDestructive(text, mainXtag, maintagQuery);
+				infile.token(i, 0)->setText(text);
+			}
 		}
 	}
 }
 
 
+
 //////////////////////////////
 //
-// Tool_gasparize::addEditStylingForText --
+// Tool_filter::removeUniversalFilterLines --
 //
 
-bool Tool_gasparize::addEditStylingForText(HumdrumFile& infile, HTp sstart, HTp send) {
-	HTp current = send->getPreviousToken();
-	bool output = false;
-	string state = "";
-	string laststate = "";
+void Tool_filter::removeUniversalFilterLines(HumdrumFileSet& infiles) {
 	HumRegex hre;
-	HTp lastdata = NULL;
-	bool italicQ = false;
-	while (current && (current != sstart)) {
-		if (!current->isData()) {
-			current = current->getPreviousToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getPreviousToken();
-			continue;
-		}
-		italicQ = false;
-		string text = current->getText();
-		if (text.find("<i>") != string::npos) {
-			italicQ = true;
-			hre.replaceDestructive(text, "", "<i>", "g");
-			hre.replaceDestructive(text, "", "</i>", "g");
-			current->setText(text);
-		} else {
-}
-		if (laststate == "") {
-			if (italicQ) {
-				laststate = "italic";
-			} else {
-				laststate = "regular";
-			}
-			current = current->getPreviousToken();
-			continue;
-		} else {
-			if (italicQ) {
-				state = "italic";
-			} else {
-				state = "regular";
+	string text;
+
+	string maintag = "!!!!filter:";
+	string mainXtag = "!!!!Xfilter:";
+	string maintagQuery = "^!!!!filter:";
+
+	string maintagV;
+	string mainXtagV;
+	string maintagQueryV;
+
+	if (m_variant.size() > 0) {
+		maintagV = "!!!!filter-" + m_variant + ":";
+		mainXtagV = "!!!!Xfilter-" + m_variant + ":";
+		maintagQueryV = "^!!!!filter-" + m_variant + ":";
+	}
+
+	for (int i=0; i<infiles.getCount(); i++) {
+		HumdrumFile& infile = infiles[i];
+		for (int j=0; j<infile.getLineCount(); j++) {
+			if (!infile[i].isUniversalReference()) {
+				continue;
 			}
-		}
-		if (state != laststate) {
-			if (lastdata && (laststate == "italic")) {
-				output = true;
-				if (!insertEditText("*edit", infile, lastdata->getLineIndex() - 1, lastdata->getFieldIndex())) {
-					string line = getEditLine("*edit", lastdata->getFieldIndex(), lastdata->getOwner());
-					infile.insertLine(lastdata->getLineIndex(), line);
+			HTp token = infile.token(j, 0);
+			if (m_variant.size() > 0) {
+				if (token->compare(0, maintagV.size(), maintagV) == 0) {
+					text = token->getText();
+					hre.replaceDestructive(text, mainXtagV, maintagQueryV);
+					token->setText(text);
+					infile[j].createLineFromTokens();
 				}
-			} else if (lastdata && (laststate == "regular")) {
-				output = true;
-				if (!insertEditText("*Xedit", infile, lastdata->getLineIndex() - 1, lastdata->getFieldIndex())) {
-					string line = getEditLine("*Xedit", lastdata->getFieldIndex(), lastdata->getOwner());
-					infile.insertLine(lastdata->getLineIndex(), line);
+			} else {
+				if (token->compare(0, maintag.size(), maintag) == 0) {
+					text = token->getText();
+					hre.replaceDestructive(text, mainXtag, maintagQuery);
+					token->setText(text);
+					infile[j].createLineFromTokens();
 				}
 			}
 		}
-		laststate = state;
-		lastdata = current;
-		current = current->getPreviousToken();
-	}
-
-	if (lastdata && italicQ) {
-		// add *edit before first syllable in **text.
-		output = true;
-		if (!insertEditText("*edit", infile, lastdata->getLineIndex() - 1, lastdata->getFieldIndex())) {
-			string line = getEditLine("*edit", lastdata->getFieldIndex(), lastdata->getOwner());
-			infile.insertLine(lastdata->getLineIndex(), line);
-		}
 	}
-
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::insertEditText --
+// Tool_filter::getCommandList --
 //
 
-bool Tool_gasparize::insertEditText(const string& text, HumdrumFile& infile, int line, int field) {
-	if (!infile[line].isInterpretation()) {
-		return false;
+void Tool_filter::getCommandList(vector<pair<string, string> >& commands,
+		HumdrumFile& infile) {
+
+	vector<HLp> refs = infile.getReferenceRecords();
+	pair<string, string> entry;
+	string tag = "filter";
+	if (m_variant.size() > 0) {
+		tag += "-";
+		tag += m_variant;
 	}
-	HTp token;
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		token = infile.token(line, i);
-		if (token->isNull()) {
+	vector<string> clist;
+	HumRegex hre;
+	for (int i=0; i<(int)refs.size(); i++) {
+		string refkey = refs[i]->getGlobalReferenceKey();
+		if (refkey != tag) {
 			continue;
 		}
-		if (token->find("edit") != string::npos) {
-			break;
-		}
-		return false;
-	}
-	token = infile.token(line, field);
-	token->setText(text);
-
-	return true;
-}
-
-
-
-/////////////////////
-//
-// Tool_gasparize::getEditLine --
-//
-
-string Tool_gasparize::getEditLine(const string& text, int fieldindex, HLp line) {
-	string output;
-	for (int i=0; i<fieldindex; i++) {
-		output += "*";
-		if (i < line->getFieldCount()) {
-			output += "\t";
-		}
-	}
-	output += text;
-	if (fieldindex < line->getFieldCount()) {
-		output += "\t";
-	}
-	for (int i=fieldindex+1; i<line->getFieldCount(); i++) {
-		output += "*";
-		if (i < line->getFieldCount()) {
-			output += "\t";
+		string command = refs[i]->getGlobalReferenceValue();
+		splitPipeline(clist, command);
+		for (int j=0; j<(int)clist.size(); j++) {
+			if (hre.search(clist[j], "^\\s*([^\\s]+)")) {
+				entry.first  = hre.getMatch(1);
+				entry.second = clist[j];
+				commands.push_back(entry);
+			}
 		}
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// adjustIntrumentNames --
+//  Tool_filter::splitPipeline --
 //
 
-void Tool_gasparize::adjustIntrumentNames(HumdrumFile& infile) {
-	int instrumentLine = -1;
-	int abbrLine = -1;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			break;
-		}
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->compare(0, 3, "*I\"") == 0) {
-				instrumentLine = i;
-			}
-			if (token->compare(0, 3, "*I'") == 0) {
-				abbrLine = i;
+void Tool_filter::splitPipeline(vector<string>& clist, const string& command) {
+	clist.clear();
+	clist.resize(1);
+	clist[0] = "";
+	int inDoubleQuotes = -1;
+	int inSingleQuotes = -1;
+	char ch = '\0';
+	char lastch;
+	for (int i=0; i<(int)command.size(); i++) {
+		lastch = ch;
+		ch = command[i];
+
+		if (ch == '"') {
+			if (lastch == '\\') {
+				// escaped double quote, so treat as regular character
+				clist.back() += ch;
+				continue;
+			} else if (inDoubleQuotes >= 0) {
+				// turn off previous double quote sequence
+				clist.back() += ch;
+				inDoubleQuotes = -1;
+				continue;
+			} else if (inSingleQuotes >= 0) {
+				// in an active single quote, so this is not a closing double quote
+				clist.back() += ch;
+				continue;
+			} else {
+				// this is the start of a double quote sequence
+				clist.back() += ch;
+				inDoubleQuotes = i;
+				continue;
 			}
 		}
-	}
-	if (instrumentLine < 0) {
-		return;
-	}
-	for (int i=0; i<infile[instrumentLine].getFieldCount(); i++) {
-		HTp token = infile.token(instrumentLine, i);
-		if (*token == "*I\"CT I") {
-			token->setText("*I\"Contratenor 1");
-		} else if (*token == "*I\"CTI") {
-			token->setText("*I\"Contratenor 1");
-		} else if (*token == "*I\"CTII") {
-			token->setText("*I\"Contratenor 2");
-		} else if (*token == "*I\"CT II") {
-			token->setText("*I\"Contratenor 2");
-		} else if (*token == "*I\"CT") {
-			token->setText("*I\"Contratenor");
-		} else if (*token == "*I\"S") {
-			token->setText("*I\"Superius");
-		} else if (*token == "*I\"A") {
-			token->setText("*I\"Altus");
-		} else if (*token == "*I\"T") {
-			token->setText("*I\"Tenor");
-		} else if (*token == "*I\"B") {
-			token->setText("*I\"Bassus");
-		} else if (*token == "*I\"V") {
-			token->setText("*I\"Quintus");
-		} else if (*token == "*I\"VI") {
-			token->setText("*I\"Sextus");
-		}
-	}
-	if (abbrLine >= 0) {
-		return;
-	}
-	string abbr;
-	HumRegex hre;
-	for (int i=0; i<infile[instrumentLine].getFieldCount(); i++) {
-		HTp token = infile.token(instrumentLine, i);
-		string text = *token;
-		if (text == "*I\"Quintus") {
-			abbr += "*I'V";
-		} else if (text == "*I\"Contratenor") {
-			abbr += "*I'Ct";
-		} else if (text == "*I\"Sextus") {
-			abbr += "*I'VI";
-		} else if (text == "*I\"Contratenor 1") {
-			abbr += "*I'Ct1";
-		} else if (text == "*I\"Contratenor 2") {
-			abbr += "*I'Ct2";
-		} else if (hre.search(text, "^\\*I\"([A-Z])")) {
-			abbr += "*I'";
-			abbr += hre.getMatch(1);
-		} else {
-			abbr += "*";
-		}
-		if (i < infile[instrumentLine].getFieldCount() - 1) {
-			abbr += "\t";
-		}
-	}
-	infile.insertLine(instrumentLine+1, abbr);
-	infile.analyzeBaseFromTokens();
-	infile.analyzeStructureNoRhythm();
-}
-
-
-//////////////////////////////
-//
-// Tool_gaspar::removeKeyDesignations --
-//
 
-void Tool_gasparize::removeKeyDesignations(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=infile.getLineCount() - 1; i>=0; i--) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (*token == "*") {
+		if (ch == '\'') {
+			if (lastch == '\\') {
+				// escaped single quote, so treat as regular character
+				clist.back() += ch;
 				continue;
-			}
-			if (!token->isKern()) {
+			} else if (inSingleQuotes >= 0) {
+				// turn off previous single quote sequence
+				clist.back() += ch;
+				inSingleQuotes = -1;
+				continue;
+			} else if (inDoubleQuotes >= 0) {
+				// in an active double quote, so this is not a closing single quote
+				clist.back() += ch;
+				continue;
+			} else {
+				// this is the start of a single quote sequence
+				clist.back() += ch;
+				inSingleQuotes = i;
 				continue;
-			}
-			if (hre.search(token, "^\\*[A-Ga-g][#n-]*:$")) {
-				// suppress the key desingation
-				infile.deleteLine(i);
-				break;
 			}
 		}
-	}
-
-}
-
-
-//////////////////////////////
-//
-// Tool_gasparize::fixBarlines -- Add final double barline and convert
-//    any intermediate final barlines to double barlines.
-//
-
-void Tool_gasparize::fixBarlines(HumdrumFile& infile) {
-	fixFinalBarline(infile);
-	HumRegex hre;
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isBarline()) {
-			continue;
-		}
-		if (infile[i].getDurationToEnd() == 0) {
-			break;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->find("==") == string::npos) {
+		if (ch == '|') {
+			if ((inSingleQuotes > -1) || (inDoubleQuotes > -1)) {
+				// pipe character
+				clist.back() += ch;
+				continue;
+			} else {
+				// this is a real pipe
+				clist.resize(clist.size() + 1);
 				continue;
 			}
-			if (hre.search(token, "^==(\\d*)")) {
-				string text = "=";
-				text += hre.getMatch(1);
-				text += "||";
-				token->setText(text);
+		}
+
+		if (isspace(ch) && (!(inSingleQuotes > -1)) && (!(inDoubleQuotes > -1))) {
+			if (isspace(lastch)) {
+				// don't repeat spaces outside of quotes.
+				continue;
 			}
 		}
+
+		// regular character
+		clist.back() += ch;
 	}
+
+	// remove leading and trailing spaces
+	HumRegex hre;
+	for (int i=0; i<(int)clist.size(); i++) {
+		hre.replaceDestructive(clist[i], "", "^\\s+");
+		hre.replaceDestructive(clist[i], "", "\\s+$");
+	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_gasparize::fixFinalBarline --
+// Tool_filter::getUniversalCommandList --
 //
 
-void Tool_gasparize::fixFinalBarline(HumdrumFile& infile) {
-	for (int i=infile.getLineCount() - 1; i>=0; i--) {
-		if (infile[i].isData()) {
-			break;
-		}
-		if (!infile[i].isBarline()) {
+void Tool_filter::getUniversalCommandList(vector<pair<string, string> >& commands,
+		HumdrumFileSet& infiles) {
+
+	vector<HLp> refs = infiles.getUniversalReferenceRecords();
+	pair<string, string> entry;
+	string tag = "filter";
+	if (m_variant.size() > 0) {
+		tag += "-";
+		tag += m_variant;
+	}
+	vector<string> clist;
+	HumRegex hre;
+	for (int i=0; i<(int)refs.size(); i++) {
+		if (refs[i]->getUniversalReferenceKey() != tag) {
 			continue;
 		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (*token != "==") {
-				token->setText("==");
+		string command = refs[i]->getUniversalReferenceValue();
+		hre.split(clist, command, "\\s*\\|\\s*");
+		for (int j=0; j<(int)clist.size(); j++) {
+			if (hre.search(clist[j], "^\\s*([^\\s]+)")) {
+				entry.first  = hre.getMatch(1);
+				entry.second = clist[j];
+				commands.push_back(entry);
 			}
 		}
 	}
@@ -85802,76 +86284,16 @@ void Tool_gasparize::fixFinalBarline(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_gasparize::createJEditorialAccidentals --
-// convert
-// 	!LO:TX:a:t=(    )
-// 	4F#
+// Tool_filter::initialize -- extract time signature lines for
+//    each **kern spine in file.
 //
 
-void Tool_gasparize::createJEditorialAccidentals(HumdrumFile& infile) {
-	int strands = infile.getStrandCount();
-	for (int i=0; i<strands; i++) {
-		HTp sstart = infile.getStrandStart(i);
-		if (!sstart) {
-			continue;
-		}
-		if (!sstart->isKern()) {
-			continue;
-		}
-		HTp send   = infile.getStrandEnd(i);
-		createJEditorialAccidentals(sstart, send);
-	}
-}
-
-void Tool_gasparize::createJEditorialAccidentals(HTp sstart, HTp send) {
-	HTp current = sstart->getNextToken();
-	HumRegex hre;
-	while (current && (current != send)) {
-		if (!current->isCommentLocal()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (hre.search(current, "^!LO:TX:a:t=\\(\\s*\\)$")) {
-			current->setText("!");
-			convertNextNoteToJAccidental(current);
-		}
-		current = current->getNextToken();
-	}
-}
-
-void Tool_gasparize::convertNextNoteToJAccidental(HTp current) {
-	current = current->getNextToken();
-	HumRegex hre;
-	while (current) {
-		if (!current->isData()) {
-			// Does not handle LO for non-data.
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			break;
-		}
-		if (current->isRest()) {
-			break;
-		}
-		string text = *current;
-		if (hre.search(text, "i")) {
-			hre.replaceDestructive(text, "j", "i");
-			current->setText(text);
-			break;
-		} else if (hre.search(text, "[-#n]")) {
-			hre.replaceDestructive(text, "$1j", "(.*[-#n]+)");
-			current->setText(text);
-			break;
-		} else {
-			// Need to add a natural sign as well.
-			hre.replaceDestructive(text, "$1nj", "(.*[A-Ga-g]+)");
-			current->setText(text);
-			break;
-		}
-		break;
+void Tool_filter::initialize(HumdrumFile& infile) {
+	m_debugQ = getBoolean("debug");
+	m_variant.clear();
+	if (getBoolean("variant")) {
+		m_variant = getString("variant");
 	}
-	current = current->getNextToken();
 }
 
 
@@ -85880,21 +86302,21 @@ void Tool_gasparize::convertNextNoteToJAccidental(HTp current) {
 
 /////////////////////////////////
 //
-// Tool_grep::Tool_grep -- Set the recognized options for the tool.
+// Tool_fixps::Tool_fixps -- Set the recognized options for the tool.
 //
 
-Tool_grep::Tool_grep(void) {
-	define("v|remove-matching-lines=b",    "remove lines that match regex");
-	define("e|regex|regular-expression=s", "regular expression to search with");
+Tool_fixps::Tool_fixps(void) {
+	// define ("n|only-remove-empty-transpositions=b", "Only remove empty transpositions");
 }
 
 
+
 /////////////////////////////////
 //
-// Tool_grep::run -- Do the main work of the tool.
+// Tool_fixps::run -- Primary interfaces to the tool.
 //
 
-bool Tool_grep::run(HumdrumFileSet& infiles) {
+bool Tool_fixps::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -85903,7 +86325,7 @@ bool Tool_grep::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_grep::run(const string& indata, ostream& out) {
+bool Tool_fixps::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -85915,7 +86337,7 @@ bool Tool_grep::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_grep::run(HumdrumFile& infile, ostream& out) {
+bool Tool_fixps::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -85925,9 +86347,11 @@ bool Tool_grep::run(HumdrumFile& infile, ostream& out) {
 	return status;
 }
 
+//
+// In-place processing of file:
+//
 
-bool Tool_grep::run(HumdrumFile& infile) {
-	initialize();
+bool Tool_fixps::run(HumdrumFile& infile) {
 	processFile(infile);
 	return true;
 }
@@ -85936,211 +86360,146 @@ bool Tool_grep::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_grep::initialize --  Initializations that only have to be
-//    done one for all HumdrumFile segments.
+// Tool_fixps::processFile --
 //
 
-void Tool_grep::initialize(void) {
-	m_negateQ = getBoolean("remove-matching-lines");
-	m_regex = getString("regular-expression");
+void Tool_fixps::processFile(HumdrumFile& infile) {
+	removeDuplicateDynamics(infile);
+	markEmptyVoices(infile);
+	vector<vector<HTp>> newlist;
+	removeEmpties(newlist, infile);
+	outputNewSpining(newlist, infile);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_grep::processFile --
+// Tool_fixps::outputNewSpining --
 //
 
-void Tool_grep::processFile(HumdrumFile& infile) {
-	HumRegex hre;
-	bool match;
+void Tool_fixps::outputNewSpining(vector<vector<HTp>>& newlist, HumdrumFile& infile) {
 	for (int i=0; i<infile.getLineCount(); i++) {
-		match = hre.search(infile[i], m_regex);
-		if (m_negateQ) {
-			if (match) {
-				continue;
-			}
-		} else {
-			if (!match) {
-				continue;
-			}
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << endl;
+			continue;
+		}
+		if ((i > 0) && (!newlist[i].empty()) && newlist[i][0]->isCommentLocal()) {
+			if (!newlist[i-1].empty() && newlist[i-1][0]->isCommentLocal()) {
+				if (newlist[i].size() == newlist[i-1].size()) {
+					bool same = true;
+					for (int j=0; j<(int)newlist[i].size(); j++) {
+						if (*(newlist[i][j]) != *(newlist[i-1][j])) {
+cerr << "GOT HERE " << i << " " << j << endl;
+cerr << infile[i-1] << endl;
+cerr << infile[i] << endl;
+cerr << endl;
+							same = false;
+							break;
+						}
+					}
+					if (same) {
+						continue;
+					}
+				}
+			}
+		}
+		if (!infile[i].isManipulator()) {
+			m_humdrum_text << newlist[i].at(0);
+			for (int j=1; j<(int)newlist[i].size(); j++) {
+				m_humdrum_text << "\t";
+				m_humdrum_text << newlist[i].at(j);
+			}
+			m_humdrum_text << endl;
+			continue;
+		}
+		if ((i > 0) && !infile[i-1].isManipulator()) {
+			printNewManipulator(infile, newlist, i);
 		}
-		m_humdrum_text << infile[i] << "\n";
-	}
-}
-
-
-
-
-/////////////////////////////////
-//
-// Tool_half::Tool_half -- Set the recognized options for the tool.
-//
-
-Tool_half::Tool_half(void) {
-	define("l|lyric-beam-break=b", "Break beams at syllable starts");
-}
-
-
-
-/////////////////////////////////
-//
-// Tool_half::run -- Primary interfaces to the tool.
-//
-
-bool Tool_half::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
-
-bool Tool_half::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
-
-
-bool Tool_half::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
 	}
-	return status;
-}
-
-//
-// In-place processing of file:
-//
-
-bool Tool_half::run(HumdrumFile& infile) {
-	processFile(infile);
-
-	// Re-load the text for each line from their tokens.
-	infile.createLinesFromTokens();
-
-	// Need to adjust the line numbers for tokens for later
-	// processing.
-	m_humdrum_text << infile;
-	return true;
 }
 
 
-
 //////////////////////////////
 //
-// Tool_half::processFile --
+// Tool_fixps::printNewManipulator --
 //
 
-void Tool_half::processFile(HumdrumFile& infile) {
-	m_lyricBreakQ = getBoolean("lyric-beam-break");
-	terminalLongToTerminalBreve(infile);
-	halfRhythms(infile);
-	adjustBeams(infile);
+void Tool_fixps::printNewManipulator(HumdrumFile& infile, vector<vector<HTp>>& newlist, int line) {
+	HTp token = infile.token(line, 0);
+	if (*token == "*-") {
+		m_humdrum_text << infile[line] << endl;
+		return;
+	}
+	if (token->compare(0, 2, "**") == 0) {
+		m_humdrum_text << infile[line] << endl;
+		return;
+	}
+	m_humdrum_text << "++++++++++++++++++++" << endl;
 }
 
-
-
 //////////////////////////////
 //
-// Tool_half::adjustBeams --
+// Tool_fixps::removeDuplicateDynamics --
 //
 
-void Tool_half::adjustBeams(HumdrumFile& infile) {
-	Tool_autobeam autobeam;
-	vector<string> argv;
-	argv.push_back("autobeam");
-	if (m_lyricBreakQ) {
-		argv.push_back("-l");
+void Tool_fixps::removeDuplicateDynamics(HumdrumFile& infile) {
+	int scount = infile.getStrandCount();
+	for (int i=0; i<scount; i++) {
+		HTp sstart = infile.getStrandBegin(i);
+		if (!sstart->isDataType("**dynam")) {
+			continue;
+		}
+		HTp send   = infile.getStrandEnd(i);
+		HTp current = sstart;
+		while (current && (current != send)) {
+			vector<string> subtoks = current->getSubtokens();
+			if (subtoks.size() % 2 == 1) {
+				current = current->getNextToken();
+				continue;
+			}
+			bool equal = true;
+			int half = (int)subtoks.size() / 2;
+			for (int j=0; j<half; j++) {
+				if (subtoks[i] != subtoks[i+half]) {
+					equal = false;
+				}
+			}
+			if (equal) {
+				string newtext = subtoks[0];
+				for (int j=1; j<half; j++) {
+					newtext += " ";
+					newtext += subtoks[j];
+				}
+				current->setText(newtext);
+			}
+		}
 	}
-	autobeam.process(argv);
-	autobeam.run(infile);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_half::halfRhythms --
+// Tool_fixps::removeEmpties --
 //
 
-void Tool_half::halfRhythms(HumdrumFile& infile) {
-	HumRegex hre;
+void Tool_fixps::removeEmpties(vector<vector<HTp>>& newlist, HumdrumFile& infile) {
+	newlist.resize(infile.getLineCount());
 	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			for (int j=0; j<infile[i].getFieldCount(); j++) {
-				HTp token = infile.token(i, j);
-				if (!token->isKern()) {
-					continue;
-				}
-				if (token->isNull()) {
-					continue;
-				}
-
-				string text = *token;
-				// extract duration without dot
-				HumNum durnodot = Convert::recipToDurationNoDots(text);
-				durnodot /= 2;
-				string newrhythm = Convert::durationToRecip(durnodot);
-				hre.replaceDestructive(text, newrhythm, "\\d+%?\\d*");
-				token->setText(text);
-			}
-		} else if (infile[i].isInterpretation()) {
-			// half time signatures
-			for (int j=0; j<infile[i].getFieldCount(); j++) {
-				HTp token = infile.token(i, j);
-				if (hre.search(token, "^\\*M(\\d+)/(\\d+)%(\\d+)")) {
-					int bot1 = hre.getMatchInt(2);
-					int bot2 = hre.getMatchInt(3);
-					if (bot2 % 2) {
-						cerr << "Cannot handle conversion of time signature " << token << endl;
-						continue;
-					}
-					bot2 /= 2;
-					if (bot2 == 1) {
-						string text = *token;
-						string replacement = "/" + to_string(bot1);
-						hre.replaceDestructive(text, replacement, "/\\d+%\\d+");
-						token->setText(text);
-					} else {
-						string text = *token;
-						string replacement = "/" + to_string(bot1);
-						replacement += "%" + to_string(bot2);
-						hre.replaceDestructive(text, replacement, "/\\d+");
-						token->setText(text);
-					}
-				} else if (hre.search(token, "^\\*M(\\d+)/(\\d+)")) {
-					int bot = hre.getMatchInt(2);
-					if (bot == 4) {
-						bot = 8;
-					} else if (bot == 2) {
-						bot = 4;
-					} else if (bot == 3) {
-						bot = 6;
-					} else if (bot == 1) {
-						bot = 2;
-					} else if (bot == 0) {
-						bot = 1;
-					} else {
-						cerr << "Warning: ignored time signature: " << token << endl;
-					}
-					string text = *token;
-					string replacement = "/" + to_string(bot);
-					hre.replaceDestructive(text, replacement, "/\\d+");
-					token->setText(text);
-				}
+		if (!infile[i].hasSpines()) {
+			continue;
+		}
+		if (infile[i].isManipulator()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			string value = token->getValue("delete");
+			if (value == "true") {
+				continue;
 			}
+			newlist[i].push_back(token);
 		}
 	}
 }
@@ -86149,69 +86508,111 @@ void Tool_half::halfRhythms(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_half::terminalLongToTerminalBreve --
+// Tool_fixps::markEmptyVoices --
 //
 
-void Tool_half::terminalLongToTerminalBreve(HumdrumFile& infile) {
-	HumRegex hre;
+void Tool_fixps::markEmptyVoices(HumdrumFile& infile) {
+	HLp barline = NULL;
 	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReference()) {
+		if (!infile[i].hasSpines()) {
 			continue;
 		}
-		HTp token = infile.token(i, 0);
-		if (token->find("terminal long") == string::npos) {
+		if (infile[i].isManipulator()) {
 			continue;
 		}
-		string text = *token;
-		hre.replaceDestructive(text, "terminal breve", "terminal long", "g");
-		token->setText(text);
+		if (infile[i].isInterpretation()) {
+			if (infile.token(i, 0)->compare(0, 2, "**")) {
+				barline = &infile[i];
+			}
+			continue;
+		}
+		if (infile[i].isBarline()) {
+			barline = &infile[i];
+		}
+		if (!infile[i].isData()) {
+			continue;
+		}
+		if (!barline) {
+			continue;
+		}
+		// check on the data line if:
+		// * it is in the first subspine
+		// * it is an invisible rest
+		// * it takes the full duration of the measure
+		// If so, then mark the tokens for deletion in that layer.
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			// int track = token->getTrack();
+			int subtrack = token->getSubtrack();
+			if (subtrack != 1) {
+				continue;
+			}
+			if (token->find("yy") == string::npos) {
+				continue;
+			}
+			if (!token->isRest()) {
+				continue;
+			}
+			HumNum duration = token->getDuration();
+			HumNum bardur = token->getDurationToBarline();
+			HTp current = token;
+			while (current) {
+			   subtrack = current->getSubtrack();
+				if (subtrack != 1) {
+					break;
+				}
+				current->setValue("delete", "true");
+				if (current->isBarline()) {
+					break;
+				}
+				current = current->getNextToken();
+			}
+			current = token;
+			current = current->getPreviousToken();
+			while (current) {
+				if (current->isManipulator()) {
+					break;
+				}
+				if (current->isBarline()) {
+					break;
+				}
+				subtrack = current->getSubtrack();
+				if (subtrack != 1) {
+					break;
+				}
+				current->setValue("delete", "true");
+				current = current->getPreviousToken();
+			}
+		}
 	}
-}
-
-
 
+}
 
-/////////////////////////////////
-//
-// Tool_hands::Tool_hands -- Set the recognized options for the tool.
-//
 
-Tool_hands::Tool_hands(void) {
-	define("c|color=b", "color right-hand notes red and left-hand notes blue");
-	define("lcolor|left-color=s:dodgerblue", "color of left-hand notes");
-	define("rcolor|right-color=s:crimson",   "color of right-hand notes");
-	define("l|left-only=b",                  "remove right-hand notes");
-	define("r|right-only=b",                 "remove left-hand notes");
-	define("m|mark=b",                       "mark left and right-hand notes");
-	define("a|attacks-only=b",               "only mark note attacks and not note sustains");
-}
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_hands::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_flipper::Tool_flipper -- Set the recognized options for the tool.
 //
 
-void Tool_hands::initialize(void) {
-	m_colorQ       = getBoolean("color");
-	m_leftColor    = getString("left-color");
-	m_rightColor   = getString("right-color");
-	m_leftOnlyQ    = getBoolean("left-only");
-	m_rightOnlyQ   = getBoolean("right-only");
-	m_markQ        = getBoolean("mark");
-	m_attacksOnlyQ = getBoolean("attacks-only");
+Tool_flipper::Tool_flipper(void) {
+	define("k|keep=b",                      "keep *flip/*Xflip instructions");
+	define("a|all=b",                       "flip globally, not just inside *flip/*Xflip regions");
+	define("s|strophe=b",                   "flip inside of strophes as well");
+	define("S|strophe-only|only-strophe=b", "flip only inside of strophes as well");
+	define("i|interp=s:kern",               "flip only in this interpretation");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_hands::run -- Do the main work of the tool.
+// Tool_flipper::run -- Do the main work of the tool.
 //
 
-bool Tool_hands::run(HumdrumFileSet& infiles) {
+bool Tool_flipper::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -86220,8 +86621,7 @@ bool Tool_hands::run(HumdrumFileSet& infiles) {
 }
 
 
-
-bool Tool_hands::run(const string& indata, ostream& out) {
+bool Tool_flipper::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -86233,7 +86633,7 @@ bool Tool_hands::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_hands::run(HumdrumFile& infile, ostream& out) {
+bool Tool_flipper::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -86244,7 +86644,7 @@ bool Tool_hands::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_hands::run(HumdrumFile& infile) {
+bool Tool_flipper::run(HumdrumFile& infile) {
 	initialize();
 	processFile(infile);
 	return true;
@@ -86254,75 +86654,54 @@ bool Tool_hands::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_hands::processFile --
+// Tool_flipper::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_hands::processFile(HumdrumFile& infile) {
-	if (m_markQ || m_leftOnlyQ || m_rightOnlyQ) {
-		infile.doHandAnalysis(m_attacksOnlyQ);
-	}
-	if (m_leftOnlyQ) {
-		removeNotes(infile, "RH");
-	} else if (m_rightOnlyQ) {
-		removeNotes(infile, "LH");
-	}
-	if (m_colorQ) {
-		colorHands(infile);
-	} else if (m_markQ) {
-		markNotes(infile);
+void Tool_flipper::initialize(void) {
+	m_allQ         = getBoolean("all");
+	m_keepQ        = getBoolean("keep");
+	m_kernQ        = true;
+	m_stropheQ     = getBoolean("strophe");
+	m_interp       = getString("interp");
+	if (m_interp != "kern") {
+		m_kernQ = false;
 	}
-	m_humdrum_text << infile;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_hands::removeNotes --
+// Tool_flipper::processFile --
 //
 
-void Tool_hands::removeNotes(HumdrumFile& infile, const string& htype) {
-	int counter = 0;
-	int scount = infile.getStrandCount();
-	for (int i=0; i<scount; i++) {
-		HTp sstart = infile.getStrandStart(i);
-		HTp xtok = sstart->getExclusiveInterpretation();
-		int hasHandMarkup = xtok->getValueInt("auto", "hand");
-		if (!hasHandMarkup) {
-			continue;
-		}
-		HTp send = infile.getStrandEnd(i);
-		removeNotes(sstart, send, htype);
-		counter++;
-	}
+void Tool_flipper::processFile(HumdrumFile& infile) {
 
-	
-	if (counter) {
-		infile.createLinesFromTokens();
+	m_fliplines.resize(infile.getLineCount());
+	fill(m_fliplines.begin(), m_fliplines.end(), false);
+
+	m_flipState.resize(infile.getMaxTrack()+1);
+	if (m_allQ) {
+		fill(m_flipState.begin(), m_flipState.end(), true);
+	} else {
+		fill(m_flipState.begin(), m_flipState.end(), false);
 	}
-}
 
+	m_strophe.resize(infile.getMaxTrack()+1);
+	fill(m_strophe.begin(), m_strophe.end(), false);
 
-void Tool_hands::removeNotes(HTp sstart, HTp send, const string& htype) {
-	HTp current = sstart;
-	while (current && (current != send)) {
-		if (!current->isData() || current->isNull()) {
-			current = current->getNextToken();
-			continue;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		processLine(infile, i);
+		if (!m_keepQ) {
+			if (!m_fliplines[i]) {
+				m_humdrum_text << infile[i] << endl;
+			}
 		}
+	}
 
-		HumRegex hre;
-		string ttype = current->getValue("auto", "hand");
-		if (ttype != htype) {
-			current = current->getNextToken();
-			continue;
-		}
-		string text = *current;
-		hre.replaceDestructive(text, "", "[^0-9.%q ]", "g");
-		hre.replaceDestructive(text, "ryy ", " ", "g");
-		text += "ryy";
-		current->setText(text);
-		current = current->getNextToken();
+	if (m_keepQ) {
+		m_humdrum_text << infile;
 	}
 }
 
@@ -86330,124 +86709,196 @@ void Tool_hands::removeNotes(HTp sstart, HTp send, const string& htype) {
 
 //////////////////////////////
 //
-// Tool_hands::markNotes --
+// Tool_flipper::checkForFlipChanges --
 //
 
-void Tool_hands::markNotes(HumdrumFile& infile) {
-	HumRegex hre;
-
-	int counter = 0;
-	int scount = infile.getStrandCount();
-	for (int i=0; i<scount; i++) {
-		HTp sstart = infile.getStrandStart(i);
-		HTp xtok = sstart->getExclusiveInterpretation();
-		int hasHandMarkup = xtok->getValueInt("auto", "hand");
-		if (!hasHandMarkup) {
-			continue;
-		}
-		HTp send   = infile.getStrandEnd(i);
-		markNotes(sstart, send);
-		counter++;
+void Tool_flipper::checkForFlipChanges(HumdrumFile& infile, int index) {
+	if (!infile[index].isInterpretation()) {
+		return;
 	}
 
-	if (counter) {
-		infile.appendLine("!!!RDF**kern: " + m_leftMarker + " = marked note, color=\"" + m_leftColor + "\", left-hand note");
-		infile.appendLine("!!!RDF**kern: " + m_rightMarker + " = marked note, color=\"" + m_rightColor + "\", right-hand note");
-		infile.createLinesFromTokens();
+	int track;
+
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (*token == "*strophe") {
+			track = token->getTrack();
+			m_strophe[track] = true;
+		} else if (*token == "*Xstrophe") {
+			track = token->getTrack();
+			m_strophe[track] = false;
+		}
 	}
-}
 
 
-void Tool_hands::markNotes(HTp sstart, HTp send) {
-	HTp current = sstart;
-	while (current && (current != send)) {
-		if (!current->isData() || current->isNull() || current->isRest()) {
-			current = current->getNextToken();
-			continue;
-		}
+	if (m_allQ) {
+		// state always stays on in this case
+		return;
+	}
 
-		HumRegex hre;
-		string text = *current;
-		string htype = current->getValue("auto", "hand");
-		if (htype == "LH") {
-			hre.replaceDestructive(text, " " + m_leftMarker, " +", "g");
-			text = m_leftMarker + text;
-		} else if (htype == "RH") {
-			hre.replaceDestructive(text, " " + m_rightMarker, " +", "g");
-			text = m_rightMarker + text;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (*token == "*flip") {
+			track = token->getTrack();
+			m_flipState[track] = true;
+			m_fliplines[i] = true;
+		} else if (*token == "*Xflip") {
+			track = token->getTrack();
+			m_flipState[track] = false;
+			m_fliplines[i] = true;
 		}
-		current->setText(text);
-		current = current->getNextToken();
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_hands::colorHands -- Convert for example *LH into *color:dodgerblue.
+// Tool_flipper::processLine --
 //
 
-void Tool_hands::colorHands(HumdrumFile& infile) {
-	string left = "*color:" + m_leftColor;
-	string right = "*color:" + m_rightColor;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
+void Tool_flipper::processLine(HumdrumFile& infile, int index) {
+	if (!infile[index].hasSpines()) {
+		return;
+	}
+	if (infile[index].isInterpretation()) {
+		checkForFlipChanges(infile, index);
+	}
+
+	vector<vector<HTp>> flipees;
+	extractFlipees(flipees, infile, index);
+	if (!flipees.empty()) {
+		int status = flipSubspines(flipees);
+		if (status) {
+			infile[index].createLineFromTokens();
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_flipper::flipSubspines --
+//
+
+bool Tool_flipper::flipSubspines(vector<vector<HTp>>& flipees) {
+	bool regenerateLine = false;
+	for (int i=0; i<(int)flipees.size(); i++) {
+		if (flipees[i].size() > 1) {
+			flipSpineTokens(flipees[i]);
+			regenerateLine = true;
+		}
+	}
+	return regenerateLine;
+}
+
+
+//////////////////////////////
+//
+// Tool_flipper::flipSpineTokens --
+//
+
+void Tool_flipper::flipSpineTokens(vector<HTp>& subtokens) {
+	if (subtokens.size() < 2) {
+		return;
+	}
+	int count = (int)subtokens.size();
+	count = count / 2;
+	HTp tok1;
+	HTp tok2;
+	string str1;
+	string str2;
+	for (int i=0; i<count; i++) {
+		tok1 = subtokens[i];
+		tok2 = subtokens[subtokens.size() - 1 - i];
+		str1 = *tok1;
+		str2 = *tok2;
+		tok1->setText(str2);
+		tok2->setText(str1);
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_flipper::extractFlipees --
+//
+
+void Tool_flipper::extractFlipees(vector<vector<HTp>>& flipees,
+		 HumdrumFile& infile, int index) {
+	flipees.clear();
+
+	HLp line = &infile[index];
+	int track;
+	int lastInsertTrack = -1;
+	for (int i=0; i<line->getFieldCount(); i++) {
+		HTp token = line->token(i);
+		track = token->getTrack();
+		if ((!m_stropheQ) && m_strophe[track]) {
 			continue;
 		}
-		bool changed = false;
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
+		if (!m_flipState[track]) {
+			continue;
+		}
+		if (m_kernQ) {
 			if (!token->isKern()) {
 				continue;
 			}
-			if (*token == "*LH") {
-				token->setText(left);
-				changed = true;
-			}
-			if (*token == "*RH") {
-				token->setText(right);
-				changed = true;
+		} else {
+			if (!token->isDataType(m_interp)) {
+				continue;
 			}
 		}
-		if (changed) {
-			infile[i].createLineFromTokens();
+		if (lastInsertTrack != track) {
+			flipees.resize(flipees.size() + 1);
+			lastInsertTrack = track;
 		}
+		flipees.back().push_back(token);
 	}
 }
 
 
 
 
+
 /////////////////////////////////
 //
-// Tool_homorhythm::Tool_homorhythm -- Set the recognized options for the tool.
+// Tool_gasparize::Tool_gasparize -- Set the recognized options for the tool.
 //
 
-Tool_homorhythm::Tool_homorhythm(void) {
-	define("a|append=b",                 "append analysis to end of input data");
-	define("attacks=b",                  "append attack counts for each sonority");
-	define("p|prepend=b",                "prepend analysis to end of input data");
-	define("r|raw-sonority=b",           "display individual sonority scores only");
-	define("raw-score=b",                "display accumulated scores");
-	define("M|no-marks=b",               "do not mark homorhythm section notes");
-	define("f|fraction=b",               "calculate fraction of music that is homorhythm");
-	define("v|voice=b",                  "display voice information or fraction results");
-	define("F|filename=b",               "show filename for f option");
-	define("n|t|threshold=d:4.0",        "threshold score sum required for homorhythm texture detection");
-	define("s|score=d:1.0",              "score assigned to a sonority with three or more attacks");
-	define("m|intermediate-score=d:0.5", "score to give sonority between two adjacent attack sonoroties");
-	define("l|letter=b",                 "display letter scoress before calculations");
+Tool_gasparize::Tool_gasparize(void) {
+	define("R|no-reference-records=b",                "do not add reference records");
+	define("r|only-add-reference-records=b",          "only add reference records");
+
+	define("B|do-not-delete-breaks=b",                "do not delete system/page break markers");
+	define("b|only-delete-breaks=b",                  "only delete breaks");
+
+	define("A|do-not-fix-instrument-abbreviations=b", "do not fix instrument abbreviations");
+	define("a|only-fix-instrument-abbreviations=b",   "only fix instrument abbreviations");
+
+	define("E|do-not-fix-editorial-accidentals=b",    "do not fix instrument abbreviations");
+	define("e|only-fix-editorial-accidentals=b",      "only fix editorial accidentals");
+
+	define("T|do-not-add-terminal-longs=b",           "do not add terminal long markers");
+	define("t|only-add-terminal-longs=b",             "only add terminal longs");
+
+	define("no-ties=b",                               "do not fix tied notes");
+
+	define("N|do-not-remove-empty-transpositions=b",  "do not remove empty transposition instructions");
+	define ("n|only-remove-empty-transpositions=b",   "only remove empty transpositions");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_homorhythm::run -- Do the main work of the tool.
+// Tool_gasparize::run -- Primary interfaces to the tool.
 //
 
-bool Tool_homorhythm::run(HumdrumFileSet& infiles) {
+bool Tool_gasparize::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -86456,9 +86907,8 @@ bool Tool_homorhythm::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_homorhythm::run(const string& indata, ostream& out) {
-	HumdrumFile infile;
-	infile.readStringNoRhythm(indata);
+bool Tool_gasparize::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -86469,7 +86919,7 @@ bool Tool_homorhythm::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_homorhythm::run(HumdrumFile& infile, ostream& out) {
+bool Tool_gasparize::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -86479,14 +86929,19 @@ bool Tool_homorhythm::run(HumdrumFile& infile, ostream& out) {
 	return status;
 }
 
+//
+// In-place processing of file:
+//
 
-bool Tool_homorhythm::run(HumdrumFile& infile) {
-	initialize();
-	infile.analyzeStructure();
-	m_voice_count = getExtantVoiceCount(infile);
-	m_letterQ = getBoolean("letter");
+bool Tool_gasparize::run(HumdrumFile& infile) {
 	processFile(infile);
+
+	// Re-load the text for each line from their tokens.
 	infile.createLinesFromTokens();
+
+	// Need to adjust the line numbers for tokens for later
+	// processing.
+	m_humdrum_text << infile;
 	return true;
 }
 
@@ -86494,880 +86949,1042 @@ bool Tool_homorhythm::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_homorhythm::markHomophonicNotes --
+// Tool_gasparize::processFile --
 //
 
-void Tool_homorhythm::markHomophonicNotes(void) {
-	// currently done with a **color spine
-}
-
+void Tool_gasparize::processFile(HumdrumFile& infile) {
 
+	bool mensurationQ    = true;
+	bool articulationsQ  = true;
+	bool abbreviationsQ  = true;
+	bool accidentalsQ    = true;
+	bool referencesQ     = true;
+	bool terminalsQ      = true;
+	bool breaksQ         = true;
+	bool transpositionsQ = true;
+   bool tieQ            = true;
+   bool teditQ          = true;
+   bool instrumentQ     = true;
+   bool removekeydesigQ = true;
+   bool fixbarlinesQ    = true;
+   bool parenthesesQ    = true;
 
-//////////////////////////////
-//
-// Tool_homorhythm::initialize --
-//
+	if (getBoolean("no-reference-records")) { referencesQ = false; }
+	if (getBoolean("only-add-reference-records")) {
+		abbreviationsQ  = false;
+		accidentalsQ    = false;
+		referencesQ     = true;
+		terminalsQ      = false;
+		breaksQ         = false;
+		transpositionsQ = false;
+	}
 
-void Tool_homorhythm::initialize(void) {
-	m_threshold = getInteger("threshold");
-	if (m_threshold < 1.0) {
-		m_threshold = 1.0;
+	if (getBoolean("do-not-delete-breaks")) { breaksQ = false; }
+	if (getBoolean("only-delete-breaks")) {
+		abbreviationsQ  = false;
+		accidentalsQ    = false;
+		referencesQ     = false;
+		terminalsQ      = false;
+		breaksQ         = true;
+		transpositionsQ = false;
 	}
 
-	m_score = getDouble("score");
-	if (m_score < 1.0) {
-		m_score = 1.0;
+	if (getBoolean("do-not-fix-instrument-abbreviations")) { abbreviationsQ = false; }
+	if (getBoolean("only-fix-instrument-abbreviations")) {
+		abbreviationsQ  = true;
+		accidentalsQ    = false;
+		referencesQ     = false;
+		terminalsQ      = false;
+		breaksQ         = false;
+		transpositionsQ = false;
 	}
 
-	m_intermediate_score = getDouble("intermediate-score");
-	if (m_intermediate_score < 0.0) {
-		m_intermediate_score = 0.0;
+	if (getBoolean("do-not-fix-editorial-accidentals")) { accidentalsQ = false; }
+	if (getBoolean("only-fix-editorial-accidentals")) {
+		abbreviationsQ  = false;
+		accidentalsQ    = true;
+		referencesQ     = false;
+		terminalsQ      = false;
+		breaksQ         = false;
+		transpositionsQ = false;
 	}
 
-	if (m_intermediate_score > m_score) {
-		m_intermediate_score = m_score;
+	if (getBoolean("do-not-add-terminal-longs")) { terminalsQ = false; }
+	if (getBoolean("only-add-terminal-longs")) {
+		abbreviationsQ  = false;
+		accidentalsQ    = false;
+		referencesQ     = false;
+		terminalsQ      = true;
+		breaksQ         = false;
+		transpositionsQ = false;
 	}
 
-}
+	if (getBoolean("do-not-remove-empty-transpositions")) { transpositionsQ = false; }
 
+	if (getBoolean("no-ties")) { tieQ = false; }
 
+	if (getBoolean("only-remove-empty-transpositions")) {
+		abbreviationsQ  = false;
+		accidentalsQ    = false;
+		referencesQ     = false;
+		terminalsQ      = false;
+		breaksQ         = false;
+		transpositionsQ = true;
+	}
 
-//////////////////////////////
-//
-// Tool_homorhythm::processFile --
-//
+	if (articulationsQ)  { removeArticulations(infile); }
+	if (fixbarlinesQ)    { fixBarlines(infile); }
+	if (tieQ)            { fixTies(infile); }
+	if (abbreviationsQ)  { fixInstrumentAbbreviations(infile); }
+	if (accidentalsQ)    { fixEditorialAccidentals(infile); }
+	if (parenthesesQ)    { createJEditorialAccidentals(infile); }
+	if (referencesQ)     { addBibliographicRecords(infile); }
+	if (breaksQ)         { deleteBreaks(infile); }
+	if (terminalsQ)      { addTerminalLongs(infile); }
+	if (transpositionsQ) { deleteDummyTranspositions(infile); }
+	if (mensurationQ)    { addMensurations(infile); }
+	if (teditQ)          { createEditText(infile); }
+   if (instrumentQ)     { adjustIntrumentNames(infile); }
+   if (removekeydesigQ) { removeKeyDesignations(infile); }
 
-void Tool_homorhythm::processFile(HumdrumFile& infile) {
-	vector<int> data;
-	data.reserve(infile.getLineCount());
+	adjustSystemDecoration(infile);
 
-	m_homorhythm.clear();
-	m_homorhythm.resize(infile.getLineCount());
+	// Input lyrics may contain "=" signs which are to be converted into
+	// spaces in **text data, and into elisions when displaying with verovio.
+	Tool_shed shed;
+	vector<string> argv;
+	argv.push_back("shed");
+	argv.push_back("-x");     // only apply to **text spines
+	argv.push_back("text");
+	argv.push_back("-e");
+	argv.push_back("s/=/ /g");
+	shed.process(argv);
+	shed.run(infile);
+}
 
-	m_notecount.clear();
-	m_notecount.resize(infile.getLineCount());
-	fill(m_notecount.begin(), m_notecount.end(), 0);
 
-	m_attacks.clear();
-	m_attacks.resize(infile.getLineCount());
-	fill(m_attacks.begin(), m_attacks.end(), 0);
 
-	m_notes.clear();
-	m_notes.resize(infile.getLineCount());
+//////////////////////////////
+//
+// Tool_gasparize::removeArticulations --
+//
 
+void Tool_gasparize::removeArticulations(HumdrumFile& infile) {
+	HumRegex hre;
 	for (int i=0; i<infile.getLineCount(); i++) {
 		if (!infile[i].isData()) {
 			continue;
 		}
-		data.push_back(i);
-		analyzeLine(infile, i);
-	}
-
-	// change Y N Y patterns to Y Y Y
-	for (int i=1; i<(int)data.size() - 1; i++) {
-		if (m_homorhythm[data[i]] == "Y") {
-			continue;
-		}
-		if (m_homorhythm[data[i+1]] == "N") {
-			continue;
-		}
-		if (m_homorhythm[data[i-1]] == "N") {
-			continue;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			bool changed = false;
+			string text = token->getText();
+			if (text.find("'") != string::npos) {
+				// remove staccatos
+				changed = true;
+				hre.replaceDestructive(text, "", "'", "g");
+			}
+			if (text.find("~") != string::npos) {
+				// remove tenutos
+				changed = true;
+				hre.replaceDestructive(text, "", "~", "g");
+			}
+			if (changed) {
+				token->setText(text);
+			}
 		}
-	  	m_homorhythm[data[i]] = "NY";  // not homphonic by will get intermediate score.
 	}
+}
 
-	vector<double> score(infile.getLineCount(), 0);
-	vector<double> raw(infile.getLineCount(), 0);
 
-	double sum = 0.0;
-	for (int i=0; i<(int)data.size(); i++) {
-		if (m_homorhythm[data[i]].find("Y") != string::npos) {
-			if (m_homorhythm[data[i]].find("N") != string::npos) {
-				// sonority between two homorhythm-like sonorities.
-				// maybe also differentiate based on metric position.
-				sum += m_intermediate_score;
-				raw[data[i]] = m_intermediate_score;
-			} else {
-				sum += m_score;
-				raw[data[i]] = m_score;
-			}
-		} else {
-			sum = 0.0;
-		}
-		score[data[i]] = sum;
-	}
 
-	for (int i=(int)data.size()-2; i>=0; i--) {
-		if (score[data[i]] == 0) {
+//////////////////////////////
+//
+// Tool_gasparize::adjustSystemDecoration --
+//    !!!system-decoration: [(s1)(s2)(s3)(s4)]
+// to:
+//    !!!system-decoration: [*]
+//
+
+void Tool_gasparize::adjustSystemDecoration(HumdrumFile& infile) {
+	for (int i=infile.getLineCount() - 1; i>=0; i--) {
+		if (!infile[i].isReference()) {
 			continue;
 		}
-		if (score[data[i+1]] > score[data[i]]) {
-			score[data[i]] = score[data[i+1]];
+		HTp token = infile.token(i, 0);
+		if (token->compare(0, 21, "!!!system-decoration:") == 0) {
+			token->setText("!!!system-decoration: [*]");
+			break;
 		}
 	}
+}
 
-	if (getBoolean("raw-score")) {
-		addAccumulatedScores(infile, score);
-	}
-
-	if (getBoolean("raw-sonority")) {
-		addRawAnalysis(infile, raw);
-	}
-	if (getBoolean("raw-score")) {
-		addAccumulatedScores(infile, score);
-	}
 
-	if (getBoolean("fraction")) {
-		addFractionAnalysis(infile, score);
-	}
 
-	if (getBoolean("attacks")) {
-		addAttacks(infile, m_attacks);
-	}
+//////////////////////////////
+//
+// Tool_gasparize::deleteDummyTranspositions -- Somehow empty
+//    transpositions that go to the same pitch can appear in the
+//    MusicXML data, so remove them here.  Example:
+// 		*Trd0c0
+//
 
-	if (!getBoolean("fraction")) {
-		// Color the notes within homorhythm textures.
-		// mark homorhythm regions in red,
-		// non-homorhythm sonorities within these regions in green
-		// and non-homorhythm regions in black.
-		if (m_letterQ) {
-			infile.appendDataSpine(m_homorhythm, "", "**hp");
+void Tool_gasparize::deleteDummyTranspositions(HumdrumFile& infile) {
+	vector<int> ldel;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			continue;
 		}
-		for (int i=0; i<(int)data.size(); i++) {
-			if (score[data[i]] >= m_threshold) {
-				if (m_attacks[data[i]] < (int)m_notes[data[i]].size() - 1) {
-					m_homorhythm[data[i]] = "dodgerblue";
-				} else {
-					m_homorhythm[data[i]] = "red";
-				}
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		bool empty = true;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (*token == "*") {
+				continue;
+			}
+			if (!token->isKern()) {
+				empty = false;
+				continue;
+			}
+			if (*token == "*Trd0c0") {
+				token->setText("*");
 			} else {
-				m_homorhythm[data[i]] = "black";
+				empty = false;
 			}
 		}
-		infile.appendDataSpine(m_homorhythm, "", "**color");
+		if (empty) {
+			ldel.push_back(i);
+		}
+	}
 
-		// problem with **color spine in javascript, so output via humdrum text
-		m_humdrum_text << infile;
+	if (ldel.size() == 1) {
+		infile.deleteLine(ldel[0]);
+	} else if (ldel.size() > 1) {
+		cerr << "Warning: multiple transposition lines, not deleting them" << endl;
 	}
 
 }
 
 
-
 //////////////////////////////
 //
-// Tool_homorhythm::addAccumulatedScores --
+// Tool_gasparize::fixEditorialAccidentals -- checkDataLine() does
+//       all of the work for this function, which only manages
+//       key signature and barline processing.
+//    Rules for accidentals in Tasso in Music Project:
+//    (1) Only note accidentals printed in the source editions
+//        are displayed as regular accidentals.  These accidentals
+//        are postfixed with an "X" in the **kern data.
+//    (2) Editorial accidentals are given an "i" marker but not
+//        a "X" marker in the **kern data.  This editorial accidental
+//        is displayed above the note.
+//    This algorithm makes adjustments to the input data because
+//    Sibelius will drop editorial information after the frist
+//    editorial accidental on that pitch in the measure.
+//    (3) If a note is the same pitch as a previous note in the
+//        measure and the previous note has an editorial accidental,
+//        then make the note an editorial note.  However, if the
+//        accidental state of the note matches the key-signature,
+//        then do not add an editorial accidental, and there will be
+//        no accidental displayed on the note.  In that case, add a "y"
+//        after the accidental to indicate that it is interpreted
+//        and not visible in the original score.
 //
 
-void Tool_homorhythm::addAccumulatedScores(HumdrumFile& infile, vector<double>& score) {
-	infile.appendDataSpine(score, "", "**score", false);
+void Tool_gasparize::fixEditorialAccidentals(HumdrumFile& infile) {
+	removeDoubledAccidentals(infile);
+
+	m_pstates.resize(infile.getMaxTrack() + 1);
+	m_estates.resize(infile.getMaxTrack() + 1);
+	m_kstates.resize(infile.getMaxTrack() + 1);
+
+	for (int i=0; i<(int)m_pstates.size(); i++) {
+		m_pstates[i].resize(70);
+		fill(m_pstates[i].begin(), m_pstates[i].end(), 0);
+		m_kstates[i].resize(70);
+		fill(m_kstates[i].begin(), m_kstates[i].end(), 0);
+		m_estates[i].resize(70);
+		fill(m_estates[i].begin(), m_estates[i].end(), false);
+	}
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isInterpretation()) {
+			updateKeySignatures(infile, i);
+			continue;
+		} else if (infile[i].isBarline()) {
+			clearStates();
+			continue;
+		} else if (infile[i].isData()) {
+			checkDataLine(infile, i);
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_homorhythm::addRawAnalysis --
-//
-
-void Tool_homorhythm::addRawAnalysis(HumdrumFile& infile, vector<double>& raw) {
-	infile.appendDataSpine(raw, "", "**raw", false);
-}
-
-
-
-//////////////////////////////
-//
-// Tool_homorhythm::addAttacks --
-//
-
-void Tool_homorhythm::addAttacks(HumdrumFile& infile, vector<int>& attacks) {
-	infile.appendDataSpine(attacks, "", "**atks");
-}
-
-
-
-//////////////////////////////
-//
-// Tool_homorhythm::addFractionAnalysis --
+// Tool_gasparize::removeDoubledAccidentals -- Often caused by transposition
+//    differences between parts in the MusicXML export from Finale.  Also some
+//    strange double sharps appear randomly.
 //
 
-void Tool_homorhythm::addFractionAnalysis(HumdrumFile& infile, vector<double>& score) {
-	double sum = 0.0;
+void Tool_gasparize::removeDoubledAccidentals(HumdrumFile& infile) {
+	HumRegex hre;
 	for (int i=0; i<infile.getLineCount(); i++) {
 		if (!infile[i].isData()) {
 			continue;
 		}
-		if (score[i] > m_threshold) {
-			sum += infile[i].getDuration().getFloat();
-		}
-	}
-	double total = infile.getScoreDuration().getFloat();
-	int ocount = getOriginalVoiceCount(infile);
-	double fraction = sum / total;
-	double percent = int(fraction * 1000.0 + 0.5)/10.0;
-	if (getBoolean("filename")) {
-		m_free_text << infile.getFilename() << "\t";
-	}
-	if (getBoolean("voice")) {
-		m_free_text << ocount;
-		m_free_text << "\t";
-		m_free_text << m_voice_count;
-		m_free_text << "\t";
-		if (ocount == m_voice_count) {
-			m_free_text << "complete" << "\t";
-		} else {
-			m_free_text << "incomplete" << "\t";
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			if (token->isRest()) {
+				continue;
+			}
+			if (token->find("--") != string::npos) {
+				string text = *token;
+				hre.replaceDestructive(text, "-", "--", "g");
+			} else if (token->find("--") != string::npos) {
+				string text = *token;
+				hre.replaceDestructive(text, "#", "##", "g");
+			}
 		}
 	}
-	if (m_voice_count < 2) {
-		m_free_text << -1;
-	} else {
-		m_free_text << percent;
-	}
-	m_free_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_homorhythm::getOriginalVoiceCount --
+// Tool_gasparize::addTerminalLongs -- Convert all last notes to terminal longs
+//    Also probably add terminal longs before double barlines as in JRP.
 //
 
-int Tool_homorhythm::getOriginalVoiceCount(HumdrumFile& infile) {
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReference()) {
+void Tool_gasparize::addTerminalLongs(HumdrumFile& infile) {
+	int scount = infile.getStrandCount();
+	for (int i=0; i<scount; i++) {
+		HTp cur = infile.getStrandEnd(i);
+		if (*cur != "*-") {
 			continue;
 		}
-		HTp token = infile.token(i, 0);
-		if (hre.search(token, "^\\!\\!\\!voices\\s*:\\s*(\\d+)")) {
-			int count = hre.getMatchInt(1);
-			if (hre.search(token, "bc", "i")) {
-				// add one for basso-continuo
-				count++;
+		if (!cur->isKern()) {
+			continue;
+		}
+		while (cur) {
+			if (!cur->isData()) {
+				cur = cur->getPreviousToken();
+				continue;
 			}
-			return count;
+			if (cur->isNull()) {
+				cur = cur->getPreviousToken();
+				continue;
+			}
+			if (cur->isRest()) {
+				cur = cur->getPreviousToken();
+				continue;
+			}
+			if (cur->isSecondaryTiedNote()) {
+				cur = cur->getPreviousToken();
+				continue;
+			}
+			if (cur->find("l") != string::npos) {
+				// already marked so do not do it again
+				break;
+			}
+			// mark this note with "l"
+			string newtext = *cur;
+			newtext += "l";
+			cur->setText(newtext);
+			break;
 		}
 	}
-	return 0;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_homorhythm::getExtantVoiceCount --
+// Tool_gasparize::fixInstrumentAbbreviations --
 //
 
-int Tool_homorhythm::getExtantVoiceCount(HumdrumFile& infile) {
-	vector<HTp> spines = infile.getKernSpineStartList();
-	return (int)spines.size();
-}
-
+void Tool_gasparize::fixInstrumentAbbreviations(HumdrumFile& infile) {
+	int iline = -1;
+	int aline = -1;
 
+	vector<HTp> kerns = infile.getKernSpineStartList();
+	if (kerns.empty()) {
+		return;
+	}
 
-//////////////////////////////
-//
-// Tool_homorhythm::analyzeLine --
-//
+	HTp cur = kerns[0];
+	while (cur) {
+		if (cur->isData()) {
+			break;
+		}
+		if (cur->compare(0, 3, "*I\"") == 0) {
+			iline = cur->getLineIndex();
+		} else if (cur->compare(0, 3, "*I'") == 0) {
+			aline = cur->getLineIndex();
+		}
+		cur = cur->getNextToken();
+	}
 
-void Tool_homorhythm::analyzeLine(HumdrumFile& infile, int line) {
-	m_notes[line].reserve(10);
-	HPNote note;
-	if (!infile[line].isData()) {
+	if (iline < 0) {
+		// no names to create abbreviations for
 		return;
 	}
-	int nullQ = 0;
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		HTp token = infile.token(line, i);
-		if (!token->isKern()) {
+	if (aline < 0) {
+		// not creating a new abbreviation for now
+		// (could add later).
+		return;
+	}
+	if (infile[iline].getFieldCount() != infile[aline].getFieldCount()) {
+		// no spine splitting between the two lines.
+		return;
+	}
+	// Maybe also require them to be adjacent to each other.
+	HumRegex hre;
+	for (int j=0; j<(int)infile[iline].getFieldCount(); j++) {
+		if (!infile.token(iline, j)->isKern()) {
 			continue;
 		}
-		if (token->isRest()) {
+		if (!hre.search(*infile.token(iline, j), "([A-Za-z][A-Za-z .0-9]+)")) {
 			continue;
 		}
-		if (token->isNull()) {
-			nullQ = 1;
-			token = token->resolveNull();
-			if (!token) {
-				continue;
-			}
-			if (token->isRest()) {
-				continue;
-			}
-		} else {
-			nullQ = 0;
-		}
-		int track = token->getTrack();
-		vector<string> subtokens = token->getSubtokens();
-		for (int j=0; j<(int)subtokens.size(); j++) {
-			note.track = track;
-			note.line = token->getLineIndex();
-			note.field = token->getFieldIndex();
-			note.subfield = j;
-			note.token = token;
-			note.text = subtokens[j];
-			note.duration = Convert::recipToDuration(note.text);
-			if (nullQ) {
-				note.attack = false;
-				note.nullQ = true;
-			} else {
-				note.nullQ = false;
-				if ((note.text.find("_") != string::npos) ||
-				    (note.text.find("]") != string::npos)) {
-					note.attack = false;
-				} else {
-					note.attack = true;
-				}
-			}
-			m_notes[line].push_back(note);
-		}
-	}
-
-	// There must be at least three attacks to be considered homorhythm
-	// maybe adjust to N-1 or three voices, or a similar rule.
-	vector<HumNum> adurs;
-	for (int i=0; i<(int)m_notes[line].size(); i++) {
-		if (m_notes[line][i].attack) {
-			adurs.push_back(m_notes[line][i].duration);
-			m_attacks[line]++;
-		}
-	}
-	// if ((int)m_attacks[line] >= (int)m_notes[line].size() - 1) {
-	if ((int)m_attacks[line] >= 3) {
-		string value = "Y";
-		// value += to_string(m_attacks[line]);
-		m_homorhythm[line] = value;
-	} else if ((m_voice_count == 3) && (m_attacks[line] == 2)) {
-		if ((adurs.size() >= 2) && (adurs[0] == adurs[1])) {
-			m_homorhythm[line] = "Y";
+		string name = hre.getMatch(1);
+		string abbr = "*I'";
+		if (name == "Basso Continuo") {
+			abbr += "BC";
+		} else if (name == "Basso continuo") {
+			abbr += "BC";
+		} else if (name == "basso continuo") {
+			abbr += "BC";
 		} else {
-			m_homorhythm[line] = "N";
+			abbr += toupper(name[0]);
 		}
-	} else {
-		string value = "N";
-		// value += to_string(m_attacks[line]);
-		m_homorhythm[line] = value;
-	}
-	// redundant or three-or-more case:
-	if (m_notes[line].size() <= 2) {
-		m_homorhythm[line] = "N";
+		// check for numbers after the end of the name and add to abbreviation
+		infile.token(aline, j)->setText(abbr);
 	}
 }
 
 
 
-
-/////////////////////////////////
-//
-// Tool_homorhythm2::Tool_homorhythm -- Set the recognized options for the tool.
-//
-
-Tool_homorhythm2::Tool_homorhythm2(void) {
-	define("t|threshold=d:1.6",  "threshold score sum required for homorhythm texture detection");
-	define("u|threshold2=d:1.3", "threshold score sum required for semi-homorhythm texture detection");
-	define("s|score=b",          "show numeric scores");
-	define("n|length=i:4",       "sonority length to calculate");
-	define("f|fraction=b",       "report fraction of music that is homorhythm");
-}
-
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_homorhythm2::run -- Do the main work of the tool.
+// Tool_gasparize::convertBreaks --
 //
 
-bool Tool_homorhythm2::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
-
-bool Tool_homorhythm2::run(const string& indata, ostream& out) {
-	HumdrumFile infile;
-	infile.read(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
-
-
-bool Tool_homorhythm2::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+void Tool_gasparize::convertBreaks(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=infile.getLineCount()-1; i>= 0; i--) {
+		if (!infile[i].isGlobalComment()) {
+			continue;
+		}
+		if (hre.search(*infile.token(i, 0), "linebreak\\s*:\\s*original")) {
+			string text = "!!LO:LB:g=original";
+			infile[i].setText(text);
+		}
+		else if (hre.search(*infile.token(i, 0), "pagebreak\\s*:\\s*original")) {
+			string text = "!!LO:PB:g=original";
+			infile[i].setText(text);
+		}
 	}
-	return status;
-}
-
-
-bool Tool_homorhythm2::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_homorhythm2::initialize --
+// Tool_gasparize::deleteBreaks --
 //
 
-void Tool_homorhythm2::initialize(void) {
-	m_threshold = getDouble("threshold");
-	if (m_threshold < 0.0) {
-		m_threshold = 0.0;
-	}
-	m_threshold2 = getDouble("threshold2");
-	if (m_threshold2 < 0.0) {
-		m_threshold2 = 0.0;
-	}
-	if (m_threshold < m_threshold2) {
-		double temp = m_threshold;
-		m_threshold = m_threshold2;
-		m_threshold2 = temp;
+void Tool_gasparize::deleteBreaks(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=infile.getLineCount()-1; i>= 0; i--) {
+		if (!infile[i].isGlobalComment()) {
+			continue;
+		}
+		if (hre.search(*infile.token(i, 0), "linebreak\\s*:\\s*original")) {
+			infile.deleteLine(i);
+		}
+		else if (hre.search(*infile.token(i, 0), "pagebreak\\s*:\\s*original")) {
+			infile.deleteLine(i);
+		}
 	}
-
 }
 
 
-
-//////////////////////////////
+////////////////////////////////
 //
-// Tool_homorhythm2::processFile --
+// Tool_gasparize::addBibliographicRecords --
+//
+// !!!COM:
+// !!!CDT:
+// !!!OTL:
+// !!!AGN:
+// !!!SCT:
+// !!!SCA:
+// !!!voices:
+//
+// At end:
+// !!!RDF**kern: l = terminal long
+// !!!RDF**kern: i = editorial accidental
+// !!!EED:
+// !!!EEV: $DATE
 //
 
-void Tool_homorhythm2::processFile(HumdrumFile& infile) {
-	infile.analyzeStructure();
-	NoteGrid grid(infile);
-	m_score.resize(infile.getLineCount());
-	fill(m_score.begin(), m_score.end(), 0.0);
-
-	double score;
-	int count;
-	int wsize = getInteger("length");
+void Tool_gasparize::addBibliographicRecords(HumdrumFile& infile) {
+	vector<HLp> refinfo = infile.getReferenceRecords();
+	map<string, HLp> refs;
+	for (int i=0; i<(int)refinfo.size(); i++) {
+		string key = refinfo[i]->getReferenceKey();
+		refs[key] = refinfo[i];
+	}
 
-	for (int i=0; i<grid.getSliceCount()-wsize; i++) {
-		score = 0;
-		count = 0;
-		for (int j=0; j<grid.getVoiceCount(); j++) {
-			for (int k=j+1; k<grid.getVoiceCount(); k++) {
-				for (int m=0; m<wsize; m++) {
-					NoteCell* cell1 = grid.cell(j, i+m);
-					if (cell1->isRest()) {
-						continue;
-					}
-					NoteCell* cell2 = grid.cell(k, i+m);
-					if (cell2->isRest()) {
-						continue;
-					}
-					count++;
-					if (cell1->isAttack() && cell2->isAttack()) {
-						score += 1.0;
-					}
-				}
-			}
+	// header records
+	if (refs.find("voices") == refs.end()) {
+		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
+			infile.insertLine(1, "!!!voices:");
+		} else {
+			infile.insertLine(0, "!!!voices:");
 		}
-		int index = grid.getLineIndex(i);
-		m_score[index] = score / count;
 	}
-
-	for (int i=grid.getSliceCount()-1; i>=wsize; i--) {
-		score = 0;
-		count = 0;
-		for (int j=0; j<grid.getVoiceCount(); j++) {
-			for (int k=j+1; k<grid.getVoiceCount(); k++) {
-				for (int m=0; m<wsize; m++) {
-					NoteCell* cell1 = grid.cell(j, i-m);
-					if (cell1->isRest()) {
-						continue;
-					}
-					NoteCell* cell2 = grid.cell(k, i-m);
-					if (cell2->isRest()) {
-						continue;
-					}
-					count++;
-					if (cell1->isAttack() && cell2->isAttack()) {
-						score += 1.0;
-					}
-				}
-			}
+	if (refs.find("SCA") == refs.end()) {
+		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
+			infile.insertLine(1, "!!!SCA:");
+		} else {
+			infile.insertLine(0, "!!!SCA:");
 		}
-		int index = grid.getLineIndex(i);
-		m_score[index] += score / count;
 	}
-
-
-	for (int i=0; i<(int)m_score.size(); i++) {
-		m_score[i] = int(m_score[i] * 100.0 + 0.5) / 100.0;
+	if (refs.find("SCT") == refs.end()) {
+		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
+			infile.insertLine(1, "!!!SCT:");
+		} else {
+			infile.insertLine(0, "!!!SCT:");
+		}
+	}
+	if (refs.find("AGN") == refs.end()) {
+		if (infile.token(0, 0)->find("!!!OTL") != string::npos) {
+			infile.insertLine(1, "!!!AGN:");
+		} else {
+			infile.insertLine(0, "!!!AGN:");
+		}
 	}
 
+	if (refs.find("OTL") == refs.end()) {
+		infile.insertLine(0, "!!!OTL:");
+	}
+	if (refs.find("CDT") == refs.end()) {
+		infile.insertLine(0, "!!!CDT: ~1450-~1517");
+	}
+	if (refs.find("COM") == refs.end()) {
+		infile.insertLine(0, "!!!COM: Gaspar van Weerbeke");
+	}
 
-	vector<string> color(infile.getLineCount());;
+	// trailer records
+	bool foundi = false;
+	bool foundj = false;
+	bool foundl = false;
 	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
+		if (!infile[i].isReference()) {
 			continue;
 		}
-		if (m_score[i] >= m_threshold) {
-			color[i] = "red";
-		} else if (m_score[i] >= m_threshold2) {
-			color[i] = "orange";
-		} else {
-			color[i] = "black";
+		HTp token = infile.token(i, 0);
+		if (token->find("!!!RDF**kern:") == string::npos) {
+			continue;
 		}
-	}
-
-	if (getBoolean("fraction")) {
-		HumNum sum = 0;
-		HumNum total = infile.getScoreDuration();
-		for (int i=0; i<(int)m_score.size(); i++) {
-			if (m_score[i] >= m_threshold2) {
-				sum += infile[i].getDuration();
+		if (token->find("terminal breve") != string::npos) {
+			foundl = true;
+		} else if (token->find("editorial accidental") != string::npos) {
+			if (token->find("i =") != string::npos) {
+				foundi = true;
+			} else if (token->find("j =") != string::npos) {
+				foundj = true;
 			}
 		}
-		HumNum fraction = sum / total;
-		m_free_text << int(fraction.getFloat() * 1000.0 + 0.5) / 10.0 << endl;
-	} else {
-		if (getBoolean("score")) {
-			infile.appendDataSpine(m_score, ".", "**cdata", false);
-		}
-		infile.appendDataSpine(color, ".", "**color", true);
-		infile.createLinesFromTokens();
-
-		// problem within emscripten-compiled version, so force to output as string:
-		m_humdrum_text << infile;
+	}
+	if (!foundj) {
+		infile.appendLine("!!!RDF**kern: j = editorial accidental, optional, paren up");
+	}
+	if (!foundi) {
+		infile.appendLine("!!!RDF**kern: i = editorial accidental");
+	}
+	if (!foundl) {
+		infile.appendLine("!!!RDF**kern: l = terminal long");
 	}
 
+	if (refs.find("PTL") == refs.end()) {
+		infile.appendLine("!!!PTL: Gaspar van Weerbeke: Collected Works. V. Settings of Liturgical Texts, Songs, and Instrumental Works");
+	}
+	if (refs.find("PPR") == refs.end()) {
+		infile.appendLine("!!!PPR: American Institute of Musicology");
+	}
+	if (refs.find("PC#") == refs.end()) {
+		infile.appendLine("!!!PC#: Corpus Mensurabilis Musicae 106/V");
+	}
+	if (refs.find("PDT") == refs.end()) {
+		infile.appendLine("!!!PDT: {YEAR}");
+	}
+	if (refs.find("PED") == refs.end()) {
+		infile.appendLine("!!!PED: Kolb, Paul");
+		infile.appendLine("!!!PED: Pavanello, Agnese");
+	}
+	if (refs.find("YEC") == refs.end()) {
+		infile.appendLine("!!!YEC: Copyright {YEAR}, Kolb, Paul");
+		infile.appendLine("!!!YEC: Copyright {YEAR}, Pavanello, Agnese");
+	}
+	if (refs.find("YEM") == refs.end()) {
+		infile.appendLine("!!!YEM: CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-nc/4.0/legalcode)");
+	}
+	if (refs.find("EED") == refs.end()) {
+		infile.appendLine("!!!EED: Zybina, Karina");
+		infile.appendLine("!!!EED: Mair-Gruber, Roland");
+	}
+	if (refs.find("EEV") == refs.end()) {
+		string date = getDate();
+		string line = "!!!EEV: " + date;
+		infile.appendLine(line);
+	}
 }
 
 
 
-
-
-
-/////////////////////////////////
-//
-// Tool_gridtest::Tool_hproof -- Set the recognized options for the tool.
-//
-
-Tool_hproof::Tool_hproof(void) {
-	// put option definitions here
-}
-
-
-
-///////////////////////////////
+////////////////////////////////
 //
-// Tool_hproof::run -- Primary interfaces to the tool.
+// Tool_gasparize::checkDataLine --
 //
 
-bool Tool_hproof::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
+void Tool_gasparize::checkDataLine(HumdrumFile& infile, int lineindex) {
+	HumdrumLine& line = infile[lineindex];
 
-bool Tool_hproof::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	return run(infile, out);
-}
+	HumRegex hre;
+	HTp token;
+	bool haseditQ;
+	int base7;
+	int accid;
+	int track;
+	bool removeQ;
+	for (int i=0; i<line.getFieldCount(); i++) {
+		token = line.token(i);
+		track = token->getTrack();
+		if (!token->isKern()) {
+			continue;
+		}
+		if (token->isNull()) {
+			continue;
+		}
+		if (token->isRest()) {
+			continue;
+		}
+		if (token->find('j') != string::npos) {
+			continue;
+		}
+		if (token->isSecondaryTiedNote()) {
+			continue;
+		}
 
+		base7 = Convert::kernToBase7(token);
+		accid = Convert::kernToAccidentalCount(token);
+		haseditQ = false;
+		removeQ = false;
 
-bool Tool_hproof::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	out << infile;
-	return status;
-}
+		// Hard-wired to "i" as editorial accidental marker
+		if (token->find("ni") != string::npos) {
+			haseditQ = true;
+		} else if (token->find("-i") != string::npos) {
+			haseditQ = true;
+		} else if (token->find("#i") != string::npos) {
+			haseditQ = true;
+		} else if (token->find("nXi") != string::npos) {
+			haseditQ = true;
+			removeQ = true;
+		} else if (token->find("-Xi") != string::npos) {
+			haseditQ = true;
+			removeQ = true;
+		} else if (token->find("#Xi") != string::npos) {
+			haseditQ = true;
+			removeQ = true;
+		}
 
+		if (removeQ) {
+			string temp = *token;
+			hre.replaceDestructive(temp, "", "X");
+			token->setText(temp);
+		}
 
-bool Tool_hproof::run(HumdrumFile& infile) {
-	markNonChordTones(infile);
-	infile.appendLine("!!!RDF**kern: N = marked note, color=chocolate (non-chord tone)");
-	infile.appendLine("!!!RDF**kern: Z = marked note, color=black (chord tone)");
-	infile.createLinesFromTokens();
-	return true;
-}
+		bool explicitQ = false;
+		if (token->find("#X") != string::npos) {
+			explicitQ = true;
+		} else if (token->find("-X") != string::npos) {
+			explicitQ = true;
+		} else if (token->find("nX") != string::npos) {
+			explicitQ = true;
+		} else if (token->find("n") != string::npos) {
+			// add an explicit accidental marker
+			explicitQ = true;
+			string text = *token;
+			hre.replaceDestructive(text, "nX", "n");
+			token->setText(text);
+		}
 
+		if (haseditQ) {
+			// Store new editorial pitch state.
+			m_estates.at(track).at(base7) = true;
+			m_pstates.at(track).at(base7) = accid;
+			continue;
+		}
 
+		if (explicitQ) {
+			// No need to make editorial since it is visible.
+			m_estates.at(track).at(base7) = false;
+			m_pstates.at(track).at(base7) = accid;
+			continue;
+		}
 
-//////////////////////////////
-//
-// Tool_hproof::markNonChordTones -- Mark
-//
+		if (accid == m_kstates.at(track).at(base7)) {
+			// 	!m_estates.at(track).at(base7)) {
+			// add !m_estates.at(track).at(base) as a condition if
+			// you want editorial accidentals to be added to return the
+			// note to the accidental in the key.
+			//
+			// The accidental matches the key-signature state,
+			// so it should not be made editorial eventhough
+			// it is not visible.
+			m_pstates.at(track).at(base7) = accid;
 
-void Tool_hproof::markNonChordTones(HumdrumFile& infile) {
-	vector<HTp> list;
-	infile.getSpineStartList(list);
-	vector<HTp> hlist;
-	for (auto it : list) {
-		if (*it == "**harm") {
-			hlist.push_back(it);
-		}
-		if (*it == "**rhrm") {
-			hlist.push_back(it);
-		}
-	}
-	if (hlist.empty()) {
-		cerr << "Warning: No **harm or **rhrm spines in data" << endl;
-		return;
-	}
+			// Add a "y" marker of there is an interpreted accidental
+			// state (flat or sharp) that is part of the key signature.
+			int hasaccid = false;
+			if (token->find("#") != string::npos) {
+				hasaccid = true;
+			} else if (token->find("-") != string::npos) {
+				hasaccid = true;
+			}
+			int hashide = false;
+			if (token->find("-y") != string::npos) {
+				hashide = true;
+			}
+			else if (token->find("#y") != string::npos) {
+				hashide = true;
+			}
+			if (hasaccid && !hashide) {
+				string text = *token;
+				hre.replaceDestructive(text, "#y", "#");
+				hre.replaceDestructive(text, "-y", "-");
+				token->setText(text);
+			}
 
-	processHarmSpine(infile, hlist[0]);
-}
+			continue;
+		}
 
+		// At this point the previous note with this pitch class
+		// had an editorial accidental, and this note also has the
+		// same accidental, or there was a previous visual accidental
+		// outside of the key signature that will cause this note to have
+		// an editorial accidental mark applied (Sibelius will drop
+		// secondary editorial accidentals in a measure when exporting,
+		// MusicXML, which is why this function is needed).
 
+		m_estates[track][base7] = true;
+		m_pstates[track][base7] = accid;
 
-//////////////////////////////
-//
-// processHarmSpine --
-//
+		string text = token->getText();
+		HumRegex hre;
+		hre.replaceDestructive(text, "#", "##+", "g");
+		hre.replaceDestructive(text, "-", "--+", "g");
+		string output = "";
+		bool foundQ = false;
+		for (int j=0; j<(int)text.size(); j++) {
+			if (text[j] == 'n') {
+				output += "ni";
+				foundQ = true;
+			} else if (text[j] == '#') {
+				output += "#i";
+				foundQ = true;
+			} else if (text[j] == '-') {
+				output += "-i";
+				foundQ = true;
+			} else {
+				output += text[j];
+			}
+		}
 
-void Tool_hproof::processHarmSpine(HumdrumFile& infile, HTp hstart) {
-	string key = "*C:";  // assume C major if no key designation
-	HTp token = hstart;
-	HTp ntoken = token->getNextNNDT();
-	while (token) {
-		markNotesInRange(infile, token, ntoken, key);
-		if (!ntoken) {
-			break;
+		if (foundQ) {
+			token->setText(output);
+			continue;
 		}
-		if (ntoken && token) {
-			getNewKey(token, ntoken, key);
+
+		// The note is natural, but has no natural sign.
+		// add the natural sign and editorial mark.
+		for (int j=(int)output.size()-1; j>=0; j--) {
+			if ((tolower(output[j]) >= 'a') && (tolower(output[j]) <= 'g')) {
+				output.insert(j+1, "ni");
+				break;
+			}
 		}
-		token = ntoken;
-		ntoken = ntoken->getNextNNDT();
+		token->setText(output);
 	}
 }
 
 
 
-//////////////////////////////
+////////////////////////////////
 //
-// Tool_hproof::getNewKey --
+// Tool_gasparize::updateKeySignatures -- Fill in the accidental
+//    states for each diatonic pitch.
 //
 
-void Tool_hproof::getNewKey(HTp token, HTp ntoken, string& key) {
-	token = token->getNextToken();
-	while (token && (token != ntoken)) {
-		if (token->isKeyDesignation()) {
-			key = *token;
+void Tool_gasparize::updateKeySignatures(HumdrumFile& infile, int lineindex) {
+	HumdrumLine& line = infile[lineindex];
+	int track;
+	for (int i=0; i<line.getFieldCount(); i++) {
+		if (!line.token(i)->isKeySignature()) {
+			continue;
 		}
-		token = token->getNextToken();
-	}
-}
+		HTp token = line.token(i);
+		track = token->getTrack();
+		string text = token->getText();
+		fill(m_kstates[track].begin(), m_kstates[track].end(), 0);
+		for (int j=3; j<(int)text.size()-1; j++) {
+			if (text[j] == ']') {
+				break;
+			}
+			switch (text[j]) {
+				case 'a': case 'A':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][5] = +1;
+						break;
+						case '-': m_kstates[track][5] = -1;
+						break;
+					}
+					break;
 
+				case 'b': case 'B':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][6] = +1;
+						break;
+						case '-': m_kstates[track][6] = -1;
+						break;
+					}
+					break;
 
+				case 'c': case 'C':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][0] = +1;
+						break;
+						case '-': m_kstates[track][0] = -1;
+						break;
+					}
+					break;
 
-//////////////////////////////
-//
-// Tool_hproof::markNotesInRange --
-//
+				case 'd': case 'D':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][1] = +1;
+						break;
+						case '-': m_kstates[track][1] = -1;
+						break;
+					}
+					break;
 
-void Tool_hproof::markNotesInRange(HumdrumFile& infile, HTp ctoken, HTp ntoken, const string& key) {
-	if (!ctoken) {
-		return;
-	}
-	int startline = ctoken->getLineIndex();
-	int stopline = infile.getLineCount();
-	if (ntoken) {
-		stopline = ntoken->getLineIndex();
-	}
-	vector<int> cts;
-	cts = Convert::harmToBase40(ctoken, key);
-	for (int i=startline; i<stopline; i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			if (!infile.token(i, j)->isKern()) {
-				continue;
-			}
-			HTp tok = infile.token(i, j);
-			if (tok->isNull()) {
-				continue;
+				case 'e': case 'E':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][2] = +1;
+						break;
+						case '-': m_kstates[track][2] = -1;
+						break;
+					}
+					break;
+
+				case 'f': case 'F':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][3] = +1;
+						break;
+						case '-': m_kstates[track][3] = -1;
+						break;
+					}
+					break;
+
+				case 'g': case 'G':
+					switch (text[j+1]) {
+						case '#': m_kstates[track][4] = +1;
+						break;
+						case '-': m_kstates[track][4] = -1;
+						break;
+					}
+					break;
 			}
-			if (tok->isRest()) {
-				continue;
+			for (int j=0; j<7; j++) {
+				if (m_kstates[track][j] == 0) {
+					continue;
+				}
+				for (int k=1; k<10; k++) {
+					m_kstates[track][j+k*7] = m_kstates[track][j];
+				}
 			}
-			markHarmonicTones(tok, cts);
 		}
 	}
 
-// cerr << "TOK\t" << ctoken << "\tLINES\t" << startline << "\t" << stopline << "\t";
-// for (int i=0; i<cts.size(); i++) {
-// cerr << " " << Convert::base40ToKern(cts[i]);
-// }
-// cerr << endl;
+	// initialize m_pstates with contents of m_kstates
+	for (int i=0; i<(int)m_kstates.size(); i++) {
+		for (int j=0; j<(int)m_kstates[i].size(); j++) {
+			m_pstates[i][j] = m_kstates[i][j];
+		}
+	}
 
 }
 
 
 
-//////////////////////////////
+////////////////////////////////
 //
-// Tool_hproof::markHarmonicTones --
+// Tool_gasparize::clearStates --
 //
 
-void Tool_hproof::markHarmonicTones(HTp tok, vector<int>& cts) {
-	int count = tok->getSubtokenCount();
-	vector<int> notes = cts;
-	string output;
-	for (int i=0; i<count; i++) {
-		string subtok = tok->getSubtoken(i);
-		int pitch = Convert::kernToBase40(subtok);
-		if (i > 0) {
-			output += " ";
-		}
-		bool found = false;
-		for (int j=0; j<(int)cts.size(); j++) {
-			if (pitch % 40 == cts[j] % 40) {
-				output += subtok;
-				output += "Z";
-				found = true;
-				break;
-			}
-		}
-		if (!found) {
-			output += subtok;
-			output += "N";
-		}
+void Tool_gasparize::clearStates(void) {
+	for (int i=0; i<(int)m_pstates.size(); i++) {
+		fill(m_pstates[i].begin(), m_pstates[i].end(), 0);
+	}
+	for (int i=0; i<(int)m_estates.size(); i++) {
+		fill(m_estates[i].begin(), m_estates[i].end(), false);
 	}
-	tok->setText(output);
 }
 
 
-
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_humbreak::Tool_humbreak -- Set the recognized options for the tool.
+// Tool_gasparize::getDate --
 //
 
-Tool_humbreak::Tool_humbreak(void) {
-	define("m|measures=s",             "measures numbers to place linebreaks before");
-	define("p|page-breaks=s",          "measure numbers to place page breaks before");
-	define("g|group=s:original",       "line/page break group");
-	define("r|remove|remove-breaks=b", "remove line/page breaks");
-	define("l|page-to-line-breaks=b",  "convert page breaks to line breaks");
+string Tool_gasparize::getDate(void) {
+	time_t t = time(NULL);
+	tm* timeptr = localtime(&t);
+	stringstream ss;
+	int year = timeptr->tm_year + 1900;
+	int month = timeptr->tm_mon + 1;
+	int day = timeptr->tm_mday;
+	ss << year << "/";
+	if (month < 10) {
+		ss << "0";
+	}
+	ss << month << "/";
+	if (day < 10) {
+		ss << "0";
+	}
+	ss << day;
+	return ss.str();
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_humbreak::run -- Do the main work of the tool.
+// Tool_gasparize::fixTies --
+//    If a tie is unclosed or if a note is followed by an invisible rest, then fix.
 //
 
-bool Tool_humbreak::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+void Tool_gasparize::fixTies(HumdrumFile& infile) {
+	int strands = infile.getStrandCount();
+	for (int i=0; i<strands; i++) {
+		HTp sstart = infile.getStrandStart(i);
+		if (!sstart) {
+			continue;
+		}
+		if (!sstart->isKern()) {
+			continue;
+		}
+		HTp send   = infile.getStrandEnd(i);
+		fixTiesForStrand(sstart, send);
 	}
-	return status;
+	fixTieStartEnd(infile);
 }
 
 
-bool Tool_humbreak::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+
+void Tool_gasparize::fixTieStartEnd(HumdrumFile& infile) {
+	int strands = infile.getStrandCount();
+	for (int i=0; i<strands; i++) {
+		HTp sstart = infile.getStrandStart(i);
+		if (!sstart) {
+			continue;
+		}
+		if (!sstart->isKern()) {
+			continue;
+		}
+		HTp send   = infile.getStrandEnd(i);
+		fixTiesStartEnd(sstart, send);
 	}
-	return status;
 }
 
 
-bool Tool_humbreak::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
-
 
-bool Tool_humbreak::run(HumdrumFile& infile) {
-	processFile(infile);
-	return true;
+void Tool_gasparize::fixTiesStartEnd(HTp starts, HTp ends) {
+	HTp current = starts;
+	HumRegex hre;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if ((current->find('[') != string::npos) &&
+				(current->find(']') != string::npos) &&
+				(current->find(' ') == string::npos)) {
+			string text = *current;
+			hre.replaceDestructive(text, "", "\\[", "g");
+			hre.replaceDestructive(text, "_", "\\]", "g");
+			current->setText(text);
+		}
+		current = current->getNextToken();
+	}
 }
 
 
-
 //////////////////////////////
 //
-// Tool_humbreak::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_gasparize::fixTiesForStrand --
 //
 
-void Tool_humbreak::initialize(void) {
-	string systemMeasures = getString("measures");
-	string pageMeasures = getString("page-breaks");
-	m_group = getString("group");
-	m_removeQ = getBoolean("remove-breaks");
-	m_page2lineQ = getBoolean("page-to-line-breaks");
-
-	vector<string> lbs;
-	vector<string> pbs;
-	HumRegex hre;
-	hre.split(lbs, systemMeasures, "[^\\da-z]+");
-	hre.split(pbs, pageMeasures, "[^\\da-z]+");
-
-	for (int i=0; i<(int)lbs.size(); i++) {
-		if (hre.search(lbs[i], "^(p?)(\\d+)([a-z]?)")) {
-			int number = hre.getMatchInt(2);
-			if (!hre.getMatch(1).empty()) {
-				m_pageMeasures[number] = 1;
-				int offset = 0;
-				string letter;
-				if (!hre.getMatch(3).empty()) {
-					letter = hre.getMatch(3);
-					offset = letter.at(0) - 'a';
-				}
-				m_pageOffset[number] = offset;
-			} else {
-				m_lineMeasures[number] = 1;
-				int offset = 0;
-				if (!hre.getMatch(3).empty()) {
-					string letter = hre.getMatch(3);
-					offset = letter.at(0) - 'a';
-				}
-				m_lineOffset[number] = offset;
-			}
-		}
+void Tool_gasparize::fixTiesForStrand(HTp sstart, HTp send) {
+	if (!sstart) {
+		return;
 	}
-
-	for (int i=0; i<(int)pbs.size(); i++) {
-		if (hre.search(pbs[i], "^(\\d+)([a-z]?)")) {
-			int number = hre.getMatchInt(1);
-			m_pageMeasures[number] = 1;
-			int offset = 0;
-			if (!hre.getMatch(2).empty()) {
-				string letter = hre.getMatch(2);
-				offset = letter.at(0) - 'a';
-			}
-			m_pageOffset[number] = offset;
+	HTp current = sstart;
+	HTp last = NULL;
+	current = current->getNextToken();
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (last == NULL) {
+			last = current;
+			current = current->getNextToken();
+			continue;
 		}
+		if (current->find("yy") != string::npos) {
+			fixTieToInvisibleRest(last, current);
+		} else if (((last->find("[") != string::npos) || (last->find("_") != string::npos))
+				&& ((current->find("]") == string::npos) && (current->find("_") == string::npos))) {
+			fixHangingTie(last, current);
+		}
+		last = current;
+		current = current->getNextToken();
 	}
 }
 
@@ -87375,189 +87992,62 @@ void Tool_humbreak::initialize(void) {
 
 //////////////////////////////
 //
-// Tool_humbreak::markLineBreakMeasures --
+// Tool_gasparize::fixTieToInvisibleRest --
 //
 
-void Tool_humbreak::markLineBreakMeasures(HumdrumFile& infile) {
-	vector<HLp> pbreak;
-	vector<HLp> lbreak;
+void Tool_gasparize::fixTieToInvisibleRest(HTp first, HTp second) {
+	if (second->find("yy") == string::npos) {
+		return;
+	}
+	if ((first->find("[") == string::npos) && (first->find("_") == string::npos)) {
+		string ftext = *first;
+		ftext = "[" + ftext;
+		first->setText(ftext);
+	}
 	HumRegex hre;
-	map<int, int> used;
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isCommentGlobal()) {
-			HTp token = infile[i].token(0);
-			if (hre.search(token, "^!!LO:LB:")) {
-				lbreak.push_back(&infile[i]);
-			} else if (hre.search(token, "^!!LO:PB:")) {
-				pbreak.push_back(&infile[i]);
-			}
-		}
+	if (!hre.search(first, "([A-Ga-g#n-]+)")) {
+		return;
+	}
+	string pitch = hre.getMatch(1);
+	pitch += "]";
+	string text = *second;
+	hre.replaceDestructive(text, pitch, "ryy");
+	second->setText(text);
+}
 
-		if (!infile[i].isBarline()) {
-			continue;
-		}
 
-		int barnum = infile[i].getBarNumber();
-		if (barnum < 0) {
-			lbreak.clear();
-			pbreak.clear();
-			continue;
-		}
 
-		int status = m_lineMeasures[barnum];
-		if (status) {
-			HLp line = &infile[i];
-			int offset = m_lineOffset[barnum];
-			if (offset && (used[barnum] == 0)) {
-				used[barnum] = offset;
-				int ocounter = 0;
-				lbreak.clear();
-				pbreak.clear();
-				for (int j=i+1; j<infile.getLineCount(); j++) {
-					if (infile[i].isCommentGlobal()) {
-						HTp token = infile.token(i, 0);
-						if (hre.search(token, "^!!LO:LB:")) {
-							lbreak.push_back(&infile[i]);
-						}
-						if (hre.search(token, "^!!LO:PB:")) {
-							pbreak.push_back(&infile[i]);
-						}
-					}
-					if (!infile[j].isBarline()) {
-						continue;
-					}
-					ocounter++;
-					if (ocounter == offset) {
-						line = &infile[j];
-					}
-				}
-				if (!lbreak.empty()) {
-					lbreak.back()->setValue("auto", "barnum", barnum + 1);
-				} else {
-					line->setValue("auto", "barnum", barnum + 1);
-				}
-			} else {
-				line->setValue("auto", "barnum", barnum + 1);
-			}
-		}
+//////////////////////////////
+//
+// Tool_gasparize::fixHangingTie -- Not dealing with chain of missing ties.
+//
 
-		status = m_pageMeasures[barnum];
-		if (status) {
-			HLp line = &infile[i];
-			int offset = m_pageOffset[barnum];
-			if (offset) {
-				int ocounter = 0;
-				lbreak.clear();
-				pbreak.clear();
-				for (int j=i+1; j<infile.getLineCount(); j++) {
-					if (infile[i].isCommentGlobal()) {
-						HTp token = infile.token(i, 0);
-						if (hre.search(token, "^!!LO:LB:")) {
-							lbreak.push_back(&infile[i]);
-						}
-						if (hre.search(token, "^!!LO:PB:")) {
-							pbreak.push_back(&infile[i]);
-						}
-					}
-					if (!infile[j].isBarline()) {
-						continue;
-					}
-					ocounter++;
-					if (ocounter == offset) {
-						line = &infile[j];
-					}
-				}
-				if (!pbreak.empty()) {
-					pbreak.back()->setValue("auto", "barnum", barnum + 1);
-					pbreak.back()->setValue("auto", "page", 1);
-				}
-			} else {
-				line->setValue("auto", "barnum", barnum + 1);
-				line->setValue("auto", "page", 1);
-			}
-		}
-	}
+void Tool_gasparize::fixHangingTie(HTp first, HTp second) {
+	string text = *second;
+	text += "]";
+	second->setText(text);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humbreak::addBreaks --
+// Tool_gasparize::addMensurations -- Add mensurations.
 //
 
-void Tool_humbreak::addBreaks(HumdrumFile& infile) {
-	markLineBreakMeasures(infile);
-
+void Tool_gasparize::addMensurations(HumdrumFile& infile) {
 	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!(infile[i].isBarline() || infile[i].isComment())) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
-		}
-
-		int barnum = infile[i].getValueInt("auto", "barnum");
-		if (barnum < 1) {
-			m_humdrum_text << infile[i] << endl;
+	for (int i=infile.getLineCount() - 1; i>=0; i--) {
+		if (!infile[i].isInterpretation()) {
 			continue;
 		}
-		barnum--;
-		int pageQ = infile[i].getValueInt("auto", "page");
-
-		if (pageQ && infile[i].isComment()) {
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
 			HTp token = infile.token(i, 0);
-			if (hre.search(token, "^!!LO:PB:")) {
-				// Add group to existing LO:PB:
-				HTp token = infile.token(i, 0);
-				HTp barToken = infile.token(i+1, 0);
-				if (barToken->isBarline()) {
-					int measure = infile[i+1].getBarNumber();
-					int pbStatus = m_pageMeasures[measure];
-					if (pbStatus) {
-						string query = "\\b" + m_group + "\\b";
-						if (!hre.match(token, query)) {
-							m_humdrum_text << token << ", " << m_group << endl;
-						} else {
-							m_humdrum_text << token << endl;
-						}
-					} else {
-						m_humdrum_text << token << endl;
-					}
-					m_humdrum_text << infile[i+1] << endl;
-					i++;
-					continue;
-				}
-			} else if (hre.search(token, "^!!LO:LB:")) {
-				// Add group to existing LO:LB:
-				HTp token = infile.token(i, 0);
-				HTp barToken = infile.token(i+1, 0);
-				if (barToken->isBarline()) {
-					int measure = infile[i+1].getBarNumber();
-					int lbStatus = m_lineMeasures[measure];
-					if (lbStatus) {
-						string query = "\\b" + m_group + "\\b";
-						if (!hre.match(token, query)) {
-							m_humdrum_text << token << ", " << m_group << endl;
-						} else {
-							m_humdrum_text << token << endl;
-						}
-					} else {
-						m_humdrum_text << token << endl;
-					}
-					m_humdrum_text << infile[i+1] << endl;
-					i++;
-					continue;
-				}
+			if (hre.search(token, "^\\*M(\\d+)/(\\d+)")) {
+				int value = hre.getMatchInt(1);
+				addMensuration(value, infile, i);
 			}
 		}
-
-		if (pageQ) {
-			m_humdrum_text << "!!LO:PB:g=" << m_group << endl;
-		} else {
-			m_humdrum_text << "!!LO:LB:g=" << m_group << endl;
-		}
-		m_humdrum_text << infile[i] << endl;
 	}
 }
 
@@ -87565,364 +88055,352 @@ void Tool_humbreak::addBreaks(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_humbreak::processFile --
+// Tool_gasparize::addMensuration --
 //
 
-void Tool_humbreak::processFile(HumdrumFile& infile) {
-	initialize();
-	if (m_removeQ) {
-		removeBreaks(infile);
-	} else if (m_page2lineQ) {
-		convertPageToLine(infile);
-	} else {
-		addBreaks(infile);
+void Tool_gasparize::addMensuration(int top, HumdrumFile& infile, int index) {
+	HTp checktoken = infile[index+1].token(0);
+	if (!checktoken) {
+		return;
+	}
+	if (checktoken->find("met") != string::npos) {
+		return;
+	}
+	int fieldcount = infile[index].getFieldCount();
+	string line = "*";
+	HTp token = infile[index].token(0);
+	if (token->isKern()) {
+		if (top == 2) {
+			line += "met(C|)";
+		} else {
+			line += "met(O)";
+		}
+	}
+	for (int i=1; i<fieldcount; i++) {
+		line += "\t*";
+		HTp token = infile[index].token(i);
+		if (token->isKern()) {
+			if (top == 2) {
+				line += "met(C|)";
+			} else {
+				line += "met(O)";
+			}
+		}
 	}
+	infile.insertLine(index+1, line);
 }
 
 
-
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_humbreak::removeBreaks --
+// Tool_gasparize::createEditText -- Convert <i> markers into *edit interps.
 //
 
-void Tool_humbreak::removeBreaks(HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].token(0)->compare(0, 7, "!!LO:LB") == 0) {
+void Tool_gasparize::createEditText(HumdrumFile& infile) {
+	// previous process manipulated the structure so reanalyze here for now:
+	infile.analyzeBaseFromTokens();
+	infile.analyzeStructureNoRhythm();
+
+	int strands = infile.getStrandCount();
+	for (int i=0; i<strands; i++) {
+		HTp sstart = infile.getStrandStart(i);
+		if (!sstart) {
 			continue;
 		}
-		if (infile[i].token(0)->compare(0, 7, "!!LO:PB") == 0) {
+		if (!sstart->isDataType("**text")) {
 			continue;
 		}
-		m_humdrum_text << infile[i] << endl;
+		HTp send   = infile.getStrandEnd(i);
+		bool status = addEditStylingForText(infile, sstart, send);
+		if (status) {
+			infile.analyzeBaseFromTokens();
+			infile.analyzeStructureNoRhythm();
+		}
 	}
 }
 
 
-
 //////////////////////////////
 //
-// Tool_humbreak::convertPageToLine --
+// Tool_gasparize::addEditStylingForText --
 //
 
-void Tool_humbreak::convertPageToLine(HumdrumFile& infile) {
+bool Tool_gasparize::addEditStylingForText(HumdrumFile& infile, HTp sstart, HTp send) {
+	HTp current = send->getPreviousToken();
+	bool output = false;
+	string state = "";
+	string laststate = "";
 	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].token(0)->compare(0, 7, "!!LO:PB") == 0) {
-			string text = *infile[i].token(0);
-			hre.replaceDestructive(text, "!!LO:LB", "!!LO:PB");
-			m_humdrum_text << text << endl;
+	HTp lastdata = NULL;
+	bool italicQ = false;
+	while (current && (current != sstart)) {
+		if (!current->isData()) {
+			current = current->getPreviousToken();
 			continue;
 		}
-		m_humdrum_text << infile[i] << endl;
-	}
+		if (current->isNull()) {
+			current = current->getPreviousToken();
+			continue;
+		}
+		italicQ = false;
+		string text = current->getText();
+		if (text.find("<i>") != string::npos) {
+			italicQ = true;
+			hre.replaceDestructive(text, "", "<i>", "g");
+			hre.replaceDestructive(text, "", "</i>", "g");
+			current->setText(text);
+		} else {
 }
+		if (laststate == "") {
+			if (italicQ) {
+				laststate = "italic";
+			} else {
+				laststate = "regular";
+			}
+			current = current->getPreviousToken();
+			continue;
+		} else {
+			if (italicQ) {
+				state = "italic";
+			} else {
+				state = "regular";
+			}
+		}
+		if (state != laststate) {
+			if (lastdata && (laststate == "italic")) {
+				output = true;
+				if (!insertEditText("*edit", infile, lastdata->getLineIndex() - 1, lastdata->getFieldIndex())) {
+					string line = getEditLine("*edit", lastdata->getFieldIndex(), lastdata->getOwner());
+					infile.insertLine(lastdata->getLineIndex(), line);
+				}
+			} else if (lastdata && (laststate == "regular")) {
+				output = true;
+				if (!insertEditText("*Xedit", infile, lastdata->getLineIndex() - 1, lastdata->getFieldIndex())) {
+					string line = getEditLine("*Xedit", lastdata->getFieldIndex(), lastdata->getOwner());
+					infile.insertLine(lastdata->getLineIndex(), line);
+				}
+			}
+		}
+		laststate = state;
+		lastdata = current;
+		current = current->getPreviousToken();
+	}
 
+	if (lastdata && italicQ) {
+		// add *edit before first syllable in **text.
+		output = true;
+		if (!insertEditText("*edit", infile, lastdata->getLineIndex() - 1, lastdata->getFieldIndex())) {
+			string line = getEditLine("*edit", lastdata->getFieldIndex(), lastdata->getOwner());
+			infile.insertLine(lastdata->getLineIndex(), line);
+		}
+	}
 
-
-
-/////////////////////////////////
-//
-// Tool_humdiff::Tool_humdiff -- Set the recognized options for the tool.
-//
-
-Tool_humdiff::Tool_humdiff(void) {
-	define("r|reference=i:1",     "sequence number of reference score");
-	define("report=b",            "display report of differences");
-	define("time-points|times=b", "display timepoint lists for each file");
-	define("note-points|notes=b", "display notepoint lists for each file");
-	define("c|color=s:red",       "color for difference markers");
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humdiff::run --
+// Tool_gasparize::insertEditText --
 //
 
-bool Tool_humdiff::run(HumdrumFileSet& infiles) {
-	int reference = getInteger("reference") - 1;
-	if (reference < 0) {
-		cerr << "Error: reference has to be 1 or higher" << endl;
-		return false;
-	}
-	if (reference > infiles.getCount()) {
-		cerr << "Error: reference number is too large: " << reference << endl;
-		cerr << "Maximum is " << infiles.getCount() << endl;
+bool Tool_gasparize::insertEditText(const string& text, HumdrumFile& infile, int line, int field) {
+	if (!infile[line].isInterpretation()) {
 		return false;
 	}
-
-	if (infiles.getSize() == 0) {
-		cerr << "Usage: " << getCommand() << " files" << endl;
-		return false;
-	} else if (infiles.getSize() < 2) {
-		cerr << "Error: requires two or more files" << endl;
-		cerr << "Usage: " << getCommand() << " files" << endl;
-		return false;
-	} else {
-		HumNum targetdur = infiles[0].getScoreDuration();
-		for (int i=1; i<infiles.getSize(); i++) {
-			HumNum dur = infiles[i].getScoreDuration();
-			if (dur != targetdur) {
-				cerr << "Error: all files must have the same duration" << endl;
-				return false;
-			}
-		}
-
-		for (int i=0; i<infiles.getCount(); i++) {
-			if (i == reference) {
-				continue;
-			}
-			compareFiles(infiles[reference], infiles[i]);
+	HTp token;
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		token = infile.token(line, i);
+		if (token->isNull()) {
+			continue;
 		}
-
-		if (!getBoolean("report")) {
-			infiles[reference].createLinesFromTokens();
-			m_humdrum_text << infiles[reference];
-			if (m_marked) {
-				m_humdrum_text << "!!!RDF**kern: @ = marked note";
-				if (getBoolean("color")) {
-					m_humdrum_text << "color=\"" << getString("color") << "\"";
-				}
-				m_humdrum_text << endl;
-			}
+		if (token->find("edit") != string::npos) {
+			break;
 		}
+		return false;
 	}
+	token = infile.token(line, field);
+	token->setText(text);
 
 	return true;
 }
 
 
 
-//////////////////////////////
+/////////////////////
 //
-// Tool_humdiff::compareFiles --
+// Tool_gasparize::getEditLine --
 //
 
-void Tool_humdiff::compareFiles(HumdrumFile& reference, HumdrumFile& alternate) {
-	vector<vector<TimePoint>> timepoints(2);
-	extractTimePoints(timepoints.at(0), reference);
-	extractTimePoints(timepoints.at(1), alternate);
-
-	if (getBoolean("time-points")) {
-		printTimePoints(timepoints[0]);
-		printTimePoints(timepoints[1]);
+string Tool_gasparize::getEditLine(const string& text, int fieldindex, HLp line) {
+	string output;
+	for (int i=0; i<fieldindex; i++) {
+		output += "*";
+		if (i < line->getFieldCount()) {
+			output += "\t";
+		}
 	}
-
-	compareTimePoints(timepoints, reference, alternate);
-}
-
-
-
-//////////////////////////////
-//
-// Tool_humdiff::printTimePoints --
-//
-
-void Tool_humdiff::printTimePoints(vector<TimePoint>& timepoints) {
-	for (int i=0; i<(int)timepoints.size(); i++) {
-		m_free_text << "TIMEPOINT " << i << ":" << endl;
-		m_free_text << timepoints[i] << endl;
+	output += text;
+	if (fieldindex < line->getFieldCount()) {
+		output += "\t";
+	}
+	for (int i=fieldindex+1; i<line->getFieldCount(); i++) {
+		output += "*";
+		if (i < line->getFieldCount()) {
+			output += "\t";
+		}
 	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humdiff::compareTimePoints --
+// adjustIntrumentNames --
 //
 
-void Tool_humdiff::compareTimePoints(vector<vector<TimePoint>>& timepoints,
-		HumdrumFile& reference, HumdrumFile& alternate) {
-	vector<int> indexes(timepoints.size(), 0);
-	HumNum minval;
-	HumNum value;
-	int found;
-
-	vector<HumdrumFile*> infiles(2, NULL);
-	infiles[0] = &reference;
-	infiles[1] = &alternate;
-
-	vector<int> increment(timepoints.size(), 0);
-
-	while ((1)) {
-		if (indexes.at(0) >= (int)timepoints.at(0).size()) {
-			// at the end of the list of notes for the first file.
-			// break from the comparison for now and figure out how
-			// to report differences of added notes in the other file(s)
-			// later.
+void Tool_gasparize::adjustIntrumentNames(HumdrumFile& infile) {
+	int instrumentLine = -1;
+	int abbrLine = -1;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
 			break;
 		}
-		timepoints.at(0).at(indexes.at(0)).index.resize(timepoints.size());
-		for (int i=1; i<(int)timepoints.size(); i++) {
-			timepoints.at(0).at(indexes.at(0)).index.at(i) = -1;
+		if (!infile[i].isInterpretation()) {
+			continue;
 		}
-		minval = timepoints.at(0).at(indexes.at(0)).timestamp;
-		for (int i=1; i<(int)timepoints.size(); i++) {
-			if (indexes.at(i) >= (int)timepoints.at(i).size()) {
-				continue;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->compare(0, 3, "*I\"") == 0) {
+				instrumentLine = i;
 			}
-			value = timepoints.at(i).at(indexes.at(i)).timestamp;
-			if (value < minval) {
-				minval = value;
+			if (token->compare(0, 3, "*I'") == 0) {
+				abbrLine = i;
 			}
 		}
-		found = 0;
-		fill(increment.begin(), increment.end(), 0);
-
-		for (int i=0; i<(int)timepoints.size(); i++) {
-			if (indexes.at(i) >= (int)timepoints.at(i).size()) {
-				// index is too large for file, so skip checking it.
-				continue;
-			}
-			found = 1;
-			value = timepoints.at(i).at(indexes.at(i)).timestamp;
-
-			if (value == minval) {
-				timepoints.at(0).at(indexes.at(0)).index.at(i) = timepoints.at(i).at(indexes.at(i)).index.at(0);
-				increment.at(i)++;
-			}
+	}
+	if (instrumentLine < 0) {
+		return;
+	}
+	for (int i=0; i<infile[instrumentLine].getFieldCount(); i++) {
+		HTp token = infile.token(instrumentLine, i);
+		if (*token == "*I\"CT I") {
+			token->setText("*I\"Contratenor 1");
+		} else if (*token == "*I\"CTI") {
+			token->setText("*I\"Contratenor 1");
+		} else if (*token == "*I\"CTII") {
+			token->setText("*I\"Contratenor 2");
+		} else if (*token == "*I\"CT II") {
+			token->setText("*I\"Contratenor 2");
+		} else if (*token == "*I\"CT") {
+			token->setText("*I\"Contratenor");
+		} else if (*token == "*I\"S") {
+			token->setText("*I\"Superius");
+		} else if (*token == "*I\"A") {
+			token->setText("*I\"Altus");
+		} else if (*token == "*I\"T") {
+			token->setText("*I\"Tenor");
+		} else if (*token == "*I\"B") {
+			token->setText("*I\"Bassus");
+		} else if (*token == "*I\"V") {
+			token->setText("*I\"Quintus");
+		} else if (*token == "*I\"VI") {
+			token->setText("*I\"Sextus");
 		}
-		if (!found) {
-			break;
+	}
+	if (abbrLine >= 0) {
+		return;
+	}
+	string abbr;
+	HumRegex hre;
+	for (int i=0; i<infile[instrumentLine].getFieldCount(); i++) {
+		HTp token = infile.token(instrumentLine, i);
+		string text = *token;
+		if (text == "*I\"Quintus") {
+			abbr += "*I'V";
+		} else if (text == "*I\"Contratenor") {
+			abbr += "*I'Ct";
+		} else if (text == "*I\"Sextus") {
+			abbr += "*I'VI";
+		} else if (text == "*I\"Contratenor 1") {
+			abbr += "*I'Ct1";
+		} else if (text == "*I\"Contratenor 2") {
+			abbr += "*I'Ct2";
+		} else if (hre.search(text, "^\\*I\"([A-Z])")) {
+			abbr += "*I'";
+			abbr += hre.getMatch(1);
 		} else {
-			compareLines(minval, indexes, timepoints, infiles);
+			abbr += "*";
 		}
-		for (int i=0; i<(int)increment.size(); i++) {
-			indexes.at(i) += increment.at(i);
+		if (i < infile[instrumentLine].getFieldCount() - 1) {
+			abbr += "\t";
 		}
 	}
+	infile.insertLine(instrumentLine+1, abbr);
+	infile.analyzeBaseFromTokens();
+	infile.analyzeStructureNoRhythm();
 }
 
 
-
 //////////////////////////////
 //
-// Tool_humdiff::printNotePoints --
+// Tool_gaspar::removeKeyDesignations --
 //
 
-void Tool_humdiff::printNotePoints(vector<NotePoint>& notelist) {
-	m_free_text << "vvvvvvvvvvvvvvvvvvvvvvvvv" << endl;
-	for (int i=0; i<(int)notelist.size(); i++) {
-		m_free_text << "NOTE " << i << endl;
-		m_free_text << notelist.at(i) << endl;
+void Tool_gasparize::removeKeyDesignations(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=infile.getLineCount() - 1; i>=0; i--) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (*token == "*") {
+				continue;
+			}
+			if (!token->isKern()) {
+				continue;
+			}
+			if (hre.search(token, "^\\*[A-Ga-g][#n-]*:$")) {
+				// suppress the key desingation
+				infile.deleteLine(i);
+				break;
+			}
+		}
 	}
-	m_free_text << "^^^^^^^^^^^^^^^^^^^^^^^^^" << endl;
-	m_free_text << endl;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_humdiff::markNote -- mark the note (since it does not have a match in other edition(s).
-//
 
-void Tool_humdiff::markNote(NotePoint& np) {
-	m_marked = 1;
-	HTp token = np.token;
-	if (!token) {
-		return;
-	}
-	if (!token->isChord()) {
-		string contents = *token;
-		contents += "@";
-		token->setText(contents);
-		return;
-	}
-	vector<string> tokens = token->getSubtokens();
-	tokens[np.subindex] += "@";
-	string output = tokens[0];
-	for (int i=1; i<(int)tokens.size(); i++) {
-		output += " ";
-		output += tokens[i];
-	}
-	token->setText(output);
 }
 
 
-
 //////////////////////////////
 //
-// Tool_humdiff::compareLines --
+// Tool_gasparize::fixBarlines -- Add final double barline and convert
+//    any intermediate final barlines to double barlines.
 //
 
-void Tool_humdiff::compareLines(HumNum minval, vector<int>& indexes,
-		vector<vector<TimePoint>>& timepoints, vector<HumdrumFile*> infiles) {
-
-	bool reportQ = getBoolean("report");
-
-	// cerr << "COMPARING LINES ====================================" << endl;
-	vector<vector<NotePoint>> notelist(indexes.size());
+void Tool_gasparize::fixBarlines(HumdrumFile& infile) {
+	fixFinalBarline(infile);
+	HumRegex hre;
 
-	// Note: timepoints size must be 2
-	// and infiles size must be 2
-	for (int i=0; i<(int)timepoints.size(); i++) {
-		if (indexes.at(i) >= (int)timepoints.at(i).size()) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isBarline()) {
 			continue;
 		}
-		if (timepoints.at(i).at(indexes.at(i)).timestamp != minval) {
-			// not at the same time
-			continue;
+		if (infile[i].getDurationToEnd() == 0) {
+			break;
 		}
-
-		getNoteList(notelist.at(i), *infiles[i],
-			timepoints.at(i).at(indexes.at(i)).index[0],
-			timepoints.at(i).at(indexes.at(i)).measure, i, indexes.at(i));
-
-
-	}
-	for (int i=0; i<(int)notelist.at(0).size(); i++) {
-		notelist.at(0).at(i).matched.resize(notelist.size());
-		fill(notelist.at(0).at(i).matched.begin(), notelist.at(0).at(i).matched.end(), -1);
-		notelist.at(0).at(i).matched.at(0) = i;
-		for (int j=1; j<(int)notelist.size(); j++) {
-			int status = findNoteInList(notelist.at(0).at(i), notelist.at(j));
-			notelist.at(0).at(i).matched.at(j) = status;
-			if ((status < 0) && !reportQ) {
-				markNote(notelist.at(0).at(i));
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->find("==") == string::npos) {
+				continue;
 			}
-		}
-	}
-
-	if (getBoolean("notes")) {
-		for (int i=0; i<(int)notelist.size(); i++) {
-			cerr << "========== NOTES FOR I=" << i << endl;
-			printNotePoints(notelist.at(i));
-			cerr << endl;
-		}
-	}
-
-	if (!reportQ) {
-		return;
-	}
-
-	// report
-	for (int i=0; i<(int)notelist.at(0).size(); i++) {
-		for (int j=1; j<(int)notelist.at(0).at(i).matched.size(); j++) {
-			if (notelist.at(0).at(i).matched.at(j) < 0) {
-				cout << "NOTE " << notelist.at(0).at(i).subtoken
-				     << " DOES NOT HAVE EXACT MATCH IN SOURCE " << j << endl;
-				int humindex = notelist.at(0).at(i).token->getLineIndex();
-				cout << "\tREFERENCE MEASURE\t: " << notelist.at(0).at(i).measure << endl;
-				cout << "\tREFERENCE LINE NO.\t: " << humindex+1 << endl;
-				cout << "\tREFERENCE LINE TEXT\t: " << (*infiles[0])[humindex] << endl;
-
-				cout << "\tTARGET  " << j << " LINE NO. ";
-				if (j < 10) {
-					cout << " ";
-				}
-				cout << ":\t" << "X" << endl;
-
-				cout << "\tTARGET  " << j << " LINE TEXT";
-				if (j < 10) {
-					cout << " ";
-				}
-				cout << ":\t" << "X" << endl;
-
-				cout << endl;
+			if (hre.search(token, "^==(\\d*)")) {
+				string text = "=";
+				text += hre.getMatch(1);
+				text += "||";
+				token->setText(text);
 			}
 		}
 	}
@@ -87932,195 +88410,132 @@ void Tool_humdiff::compareLines(HumNum minval, vector<int>& indexes,
 
 //////////////////////////////
 //
-// Tool_humdiff::findNoteInList --
+// Tool_gasparize::fixFinalBarline --
 //
 
-int Tool_humdiff::findNoteInList(NotePoint& np, vector<NotePoint>& nps) {
-	for (int i=0; i<(int)nps.size(); i++) {
-		// cerr << "COMPARING " << np.token << " (" << np.b40 << ") TO " << nps.at(i).token << " (" << nps.at(i).b40 << ") " << endl;
-		if (nps.at(i).processed) {
-			continue;
+void Tool_gasparize::fixFinalBarline(HumdrumFile& infile) {
+	for (int i=infile.getLineCount() - 1; i>=0; i--) {
+		if (infile[i].isData()) {
+			break;
 		}
-		if (nps.at(i).b40 != np.b40) {
+		if (!infile[i].isBarline()) {
 			continue;
 		}
-		if (nps.at(i).duration != np.duration) {
-			continue;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (*token != "==") {
+				token->setText("==");
+			}
 		}
-		return i;
 	}
-	// cerr << "\tCannot find note " << np.token << " on line " << np.token->getLineIndex() << " in other work" << endl;
-	return -1;
 }
 
 
 
-
 //////////////////////////////
 //
-// Tool_humdiff::getNoteList --
+// Tool_gasparize::createJEditorialAccidentals --
+// convert
+// 	!LO:TX:a:t=(    )
+// 	4F#
 //
 
-void Tool_humdiff::getNoteList(vector<NotePoint>& notelist, HumdrumFile& infile, int line, int measure, int sourceindex, int tpindex) {
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		HTp token = infile.token(line, i);
-		if (!token->isKern()) {
+void Tool_gasparize::createJEditorialAccidentals(HumdrumFile& infile) {
+	int strands = infile.getStrandCount();
+	for (int i=0; i<strands; i++) {
+		HTp sstart = infile.getStrandStart(i);
+		if (!sstart) {
 			continue;
 		}
-		if (token->isNull()) {
+		if (!sstart->isKern()) {
 			continue;
 		}
-		if (token->isRest()) {
+		HTp send   = infile.getStrandEnd(i);
+		createJEditorialAccidentals(sstart, send);
+	}
+}
+
+void Tool_gasparize::createJEditorialAccidentals(HTp sstart, HTp send) {
+	HTp current = sstart->getNextToken();
+	HumRegex hre;
+	while (current && (current != send)) {
+		if (!current->isCommentLocal()) {
+			current = current->getNextToken();
 			continue;
 		}
-		int scount = token->getSubtokenCount();
-		int track = token->getTrack();
-		int layer = token->getSubtrack();
-		for (int j=0; j<scount; j++) {
-			string subtok = token->getSubtoken(j);
-			if (subtok.find("]") != string::npos) {
-				continue;
-			}
-			if (subtok.find("_") != string::npos) {
-				continue;
-			}
-			// found a note to store;
-			notelist.resize(notelist.size() + 1);
-			notelist.back().token = token;
-			notelist.back().subtoken = subtok;
-			notelist.back().subindex = j;
-			notelist.back().measurequarter = token->getDurationFromBarline();
-			notelist.back().measure =
-			notelist.back().track = track;
-			notelist.back().layer = layer;
-			notelist.back().sourceindex = sourceindex;
-			notelist.back().tpindex = tpindex;
-			notelist.back().duration = token->getTiedDuration();
-			notelist.back().b40 = Convert::kernToBase40(subtok);
+		if (hre.search(current, "^!LO:TX:a:t=\\(\\s*\\)$")) {
+			current->setText("!");
+			convertNextNoteToJAccidental(current);
 		}
+		current = current->getNextToken();
 	}
 }
 
-
-
-//////////////////////////////
-//
-// Tool_humdiff::extractTimePoints -- Extract a list of the timestamps in a file.
-//
-
-void Tool_humdiff::extractTimePoints(vector<TimePoint>& points, HumdrumFile& infile) {
-	TimePoint tp;
-	points.clear();
+void Tool_gasparize::convertNextNoteToJAccidental(HTp current) {
+	current = current->getNextToken();
 	HumRegex hre;
-	points.reserve(infile.getLineCount());
-	int measure = -1;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isBarline()) {
-			if (hre.search(infile.token(i, 0), "(\\d+)")) {
-				measure = hre.getMatchInt(1);
-			}
-		}
-		if (!infile[i].isData()) {
+	while (current) {
+		if (!current->isData()) {
+			// Does not handle LO for non-data.
+			current = current->getNextToken();
 			continue;
 		}
-		if (infile[i].getDuration() == 0) {
-			// ignore grace notes for now
-			continue;
+		if (current->isNull()) {
+			break;
 		}
-		tp.clear();
-		tp.file.push_back(&infile);
-		tp.index.push_back(i);
-		tp.timestamp = infile[i].getDurationFromStart();
-		tp.measure = measure;
-		points.push_back(tp);
+		if (current->isRest()) {
+			break;
+		}
+		string text = *current;
+		if (hre.search(text, "i")) {
+			hre.replaceDestructive(text, "j", "i");
+			current->setText(text);
+			break;
+		} else if (hre.search(text, "[-#n]")) {
+			hre.replaceDestructive(text, "$1j", "(.*[-#n]+)");
+			current->setText(text);
+			break;
+		} else {
+			// Need to add a natural sign as well.
+			hre.replaceDestructive(text, "$1nj", "(.*[A-Ga-g]+)");
+			current->setText(text);
+			break;
+		}
+		break;
 	}
+	current = current->getNextToken();
 }
 
 
 
-//////////////////////////////
-//
-// operator<< == print a TimePoint
-//
-
-ostream& operator<<(ostream& out, TimePoint& tp) {
-	out << "\ttimestamp:\t" << tp.timestamp.getFloat() << endl;
-	out << "\tmeasure:\t" << tp.measure << endl;
-	out << "\tindexes:\t" << endl;
-	for (int i=0; i<(int)tp.index.size(); i++) {
-		out << "\t\tindex " << i << " is:\t" << tp.index[i] << "\t" << (*tp.file[i])[tp.index[i]] << endl;
-	}
-	return out;
-}
-
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// operator<< == print a NotePoint
+// Tool_grep::Tool_grep -- Set the recognized options for the tool.
 //
 
-ostream& operator<<(ostream& out, NotePoint& np) {
-	if (np.token) {
-		out << "\ttoken:\t\t" << np.token << endl;
-	}
-	out << "\ttoken index:\t" << np.subindex << endl;
-	if (!np.subtoken.empty()) {
-		out << "\tsubtoken:\t" << np.subtoken << endl;
-	}
-	out << "\tmeasure:\t" << np.measure << endl;
-	out << "\tmquarter:\t" << np.measurequarter << endl;
-	out << "\ttrack:\t\t" << np.track << endl;
-	out << "\tlayer:\t\t" << np.layer << endl;
-	out << "\tduration:\t" << np.duration << endl;
-	out << "\tb40:\t\t" << np.b40 << endl;
-	out << "\tprocessed:\t" << np.processed << endl;
-	out << "\tsourceindex:\t" << np.sourceindex << endl;
-	out << "\ttpindex:\t" << np.tpindex << endl;
-	out << "\tmatched:\t" << endl;
-	for (int i=0; i<(int)np.matched.size(); i++) {
-		out << "\t\tindex " << i << " is:\t" << np.matched[i] << endl;
-	}
-	return out;
+Tool_grep::Tool_grep(void) {
+	define("v|remove-matching-lines=b",    "remove lines that match regex");
+	define("e|regex|regular-expression=s", "regular expression to search with");
 }
 
 
-
-
-
 /////////////////////////////////
 //
-// Tool_humsheet::Tool_humsheet -- Set the recognized options for the tool.
+// Tool_grep::run -- Do the main work of the tool.
 //
 
-Tool_humsheet::Tool_humsheet(void) {
-	define("h|H|html|HTML=b",       "output table in HTML wrapper");
-	define("i|id|ID=b",             "include ID for each cell");
-	define("z|zebra=b",             "add zebra striping by spine to style");
-	define("y|z2|zebra2|zebra-2=b", "zebra striping by data type");
-	define("t|tab-index=b",         "vertical tab indexing");
-	define("X|no-exinterp=b",       "do not embed exclusive interp data");
-	define("J|no-javascript=b",     "do not embed javascript code");
-	define("S|no-style=b",          "do not embed CSS style element");
+bool Tool_grep::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
 }
 
 
-
-/////////////////////////////////
-//
-// Tool_humsheet::run -- Do the main work of the tool.
-//
-
-bool Tool_humsheet::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
-
-bool Tool_humsheet::run(const string& indata, ostream& out) {
+bool Tool_grep::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -88132,7 +88547,7 @@ bool Tool_humsheet::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_humsheet::run(HumdrumFile& infile, ostream& out) {
+bool Tool_grep::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -88143,7 +88558,7 @@ bool Tool_humsheet::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_humsheet::run(HumdrumFile& infile) {
+bool Tool_grep::run(HumdrumFile& infile) {
 	initialize();
 	processFile(infile);
 	return true;
@@ -88153,384 +88568,2601 @@ bool Tool_humsheet::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_humsheet::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_grep::initialize --  Initializations that only have to be
+//    done one for all HumdrumFile segments.
 //
 
-void Tool_humsheet::initialize(void) {
-	m_idQ         = getBoolean("id");
-	m_htmlQ       = getBoolean("html");
-	m_zebraQ      = getBoolean("zebra");
-	m_zebra2Q     = getBoolean("zebra2");
-	m_exinterpQ   = !getBoolean("no-exinterp");
-	m_javascriptQ = !getBoolean("no-javascript");
-	m_tabindexQ   = getBoolean("tab-index");
+void Tool_grep::initialize(void) {
+	m_negateQ = getBoolean("remove-matching-lines");
+	m_regex = getString("regular-expression");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humsheet::processFile --
+// Tool_grep::processFile --
 //
 
-void Tool_humsheet::processFile(HumdrumFile& infile) {
-	analyzeTracks(infile);
-	if (m_htmlQ) {
-		printHtmlHeader();
-		printStyle(infile);
-	}
-	if (m_tabindexQ) {
-		analyzeTabIndex(infile);
-	}
-	m_free_text << "<table class=\"humdrum\"";
-	m_free_text << " data-spine-count=\"" << infile.getMaxTrack() << "\"";
-	m_free_text << ">\n";
+void Tool_grep::processFile(HumdrumFile& infile) {
+	HumRegex hre;
+	bool match;
 	for (int i=0; i<infile.getLineCount(); i++) {
-		m_free_text << "<tr";
-		printRowClasses(infile, i);
-		printRowData(infile, i);
-		printTitle(infile, i);
-		m_free_text << ">";
-		printRowContents(infile, i);
-		m_free_text << "</tr>\n";
-	}
-	m_free_text << "</table>";
-	if (m_htmlQ) {
-		if (m_javascriptQ) {
-			printJavascript();
+		match = hre.search(infile[i], m_regex);
+		if (m_negateQ) {
+			if (match) {
+				continue;
+			}
+		} else {
+			if (!match) {
+				continue;
+			}
 		}
-		printHtmlFooter();
+		m_humdrum_text << infile[i] << "\n";
 	}
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_humsheet::printTitle --
+// Tool_half::Tool_half -- Set the recognized options for the tool.
 //
 
-void Tool_humsheet::printTitle(HumdrumFile& infile, int line) {
-	if (!infile[line].isReference()) {
-		return;
-	}
-	string meaning = Convert::getReferenceKeyMeaning(infile[line].token(0));
-	if (!meaning.empty()) {
-		m_free_text << " title=\"" << meaning << "\"";
-	}
+Tool_half::Tool_half(void) {
+	define("l|lyric-beam-break=b", "Break beams at syllable starts");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_humsheet::printRowData --
+// Tool_half::run -- Primary interfaces to the tool.
 //
 
-void Tool_humsheet::printRowData(HumdrumFile& infile, int line) {
-	m_free_text << " data-line=\"" << line << "\"";
+bool Tool_half::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_half::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
 }
 
 
+bool Tool_half::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
 
-///////////////////////////////
 //
-// printHtmlHeader --
+// In-place processing of file:
 //
 
-void Tool_humsheet::printHtmlHeader(void) {
-	m_free_text << "<html>\n";
-	m_free_text << "<head>\n";
-	m_free_text << "<title>\n";
-	m_free_text << "UNTITLED\n";
-	m_free_text << "</title>\n";
-	m_free_text << "</head>\n";
-	m_free_text << "<body>\n";
+bool Tool_half::run(HumdrumFile& infile) {
+	processFile(infile);
+
+	// Re-load the text for each line from their tokens.
+	infile.createLinesFromTokens();
+
+	// Need to adjust the line numbers for tokens for later
+	// processing.
+	m_humdrum_text << infile;
+	return true;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// printHtmlFooter --
+// Tool_half::processFile --
 //
 
-void Tool_humsheet::printHtmlFooter(void) {
-	m_free_text << "</body>\n";
-	m_free_text << "</html>\n";
+void Tool_half::processFile(HumdrumFile& infile) {
+	m_lyricBreakQ = getBoolean("lyric-beam-break");
+	terminalLongToTerminalBreve(infile);
+	halfRhythms(infile);
+	adjustBeams(infile);
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// printRowClasses --
+// Tool_half::adjustBeams --
 //
 
-void Tool_humsheet::printRowClasses(HumdrumFile& infile, int row) {
-	string classes;
-	HLp hl = &infile[row];
-	if (hl->hasSpines()) {
-		classes += "spined ";
-	}
-	if (hl->isEmpty()) {
-		classes += "empty ";
-	}
-	if (hl->isData()) {
-		classes += "data ";
-	}
-	if (hl->isInterpretation()) {
-		classes += "interp ";
-		HTp token = hl->token(0);
-		if (token->compare(0, 2, "*>") == 0) {
-			classes += "label ";
-		}
-	}
-	if (hl->isLocalComment()) {
-		classes += "lcomment ";
-		if (isLayout(hl)) {
-			classes += "layout ";
-		}
-	}
-	HTp token = hl->token(0);
-	if (token->compare(0, 2, "!!") == 0) {
-		if ((token->size() == 2) || (token->at(3) != '!')) {
-			classes += "gcommet ";
-		}
-	}
-
-	if (hl->isUniversalReference()) {
-		if (token->compare(0, 11, "!!!!filter:") == 0) {
-			classes += "ufilter ";
-		} else if (token->compare(0, 12, "!!!!Xfilter:") == 0) {
-			classes += "usedufilter ";
-		} else {
-			classes += "ureference ";
-			if (token->compare(0, 12, "!!!!SEGMENT:") == 0) {
-				classes += "segment ";
-			}
-		}
-	} else if (hl->isCommentUniversal()) {
-		classes += "ucomment ";
-	} else if (hl->isReference()) {
-		classes += "reference ";
-	} else if (hl->isGlobalComment()) {
-		HTp token = hl->token(0);
-		if (token->compare(0, 10, "!!!filter:") == 0) {
-			classes += "filter ";
-		} else if (token->compare(0, 11, "!!!Xfilter:") == 0) {
-			classes += "usedfilter ";
-		} else {
-			classes += "gcomment ";
-			if (isLayout(hl)) {
-				classes += "layout ";
-			}
-		}
-	}
-
-	if (hl->isBarline()) {
-		classes += "barline ";
-	}
-	if (hl->isManipulator()) {
-		HTp token = hl->token(0);
-		if (token->compare(0, 2, "**") == 0) {
-			classes += "exinterp ";
-		} else {
-			classes += "manip ";
-		}
-	}
-	if (!classes.empty()) {
-      // remove space.
-		classes.resize((int)classes.size() - 1);
-		m_free_text << " class=\"" << classes << "\"";
+void Tool_half::adjustBeams(HumdrumFile& infile) {
+	Tool_autobeam autobeam;
+	vector<string> argv;
+	argv.push_back("autobeam");
+	if (m_lyricBreakQ) {
+		argv.push_back("-l");
 	}
+	autobeam.process(argv);
+	autobeam.run(infile);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humsheet::isLayout -- check to see if any cell
-//    starts with "!LO:".
+// Tool_half::halfRhythms --
 //
 
-bool Tool_humsheet::isLayout(HLp line) {
-	if (line->hasSpines()) {
-		if (!line->isCommentLocal()) {
-			return false;
-		}
-		for (int i=0; i<line->getFieldCount(); i++) {
-			HTp token = line->token(i);
-			if (token->compare(0, 4, "!LO:") == 0) {
-				return true;
+void Tool_half::halfRhythms(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			for (int j=0; j<infile[i].getFieldCount(); j++) {
+				HTp token = infile.token(i, j);
+				if (!token->isKern()) {
+					continue;
+				}
+				if (token->isNull()) {
+					continue;
+				}
+
+				string text = *token;
+				// extract duration without dot
+				HumNum durnodot = Convert::recipToDurationNoDots(text);
+				durnodot /= 2;
+				string newrhythm = Convert::durationToRecip(durnodot);
+				hre.replaceDestructive(text, newrhythm, "\\d+%?\\d*");
+				token->setText(text);
+			}
+		} else if (infile[i].isInterpretation()) {
+			// half time signatures
+			for (int j=0; j<infile[i].getFieldCount(); j++) {
+				HTp token = infile.token(i, j);
+				if (hre.search(token, "^\\*M(\\d+)/(\\d+)%(\\d+)")) {
+					int bot1 = hre.getMatchInt(2);
+					int bot2 = hre.getMatchInt(3);
+					if (bot2 % 2) {
+						cerr << "Cannot handle conversion of time signature " << token << endl;
+						continue;
+					}
+					bot2 /= 2;
+					if (bot2 == 1) {
+						string text = *token;
+						string replacement = "/" + to_string(bot1);
+						hre.replaceDestructive(text, replacement, "/\\d+%\\d+");
+						token->setText(text);
+					} else {
+						string text = *token;
+						string replacement = "/" + to_string(bot1);
+						replacement += "%" + to_string(bot2);
+						hre.replaceDestructive(text, replacement, "/\\d+");
+						token->setText(text);
+					}
+				} else if (hre.search(token, "^\\*M(\\d+)/(\\d+)")) {
+					int bot = hre.getMatchInt(2);
+					if (bot == 4) {
+						bot = 8;
+					} else if (bot == 2) {
+						bot = 4;
+					} else if (bot == 3) {
+						bot = 6;
+					} else if (bot == 1) {
+						bot = 2;
+					} else if (bot == 0) {
+						bot = 1;
+					} else {
+						cerr << "Warning: ignored time signature: " << token << endl;
+					}
+					string text = *token;
+					string replacement = "/" + to_string(bot);
+					hre.replaceDestructive(text, replacement, "/\\d+");
+					token->setText(text);
+				}
 			}
-		}
-	} else {
-		HTp token = line->token(0);
-		if (token->compare(0, 5, "!!LO:") == 0) {
-			return true;
 		}
 	}
-	return false;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_humsheet::printRowContents --
+// Tool_half::terminalLongToTerminalBreve --
 //
 
-void Tool_humsheet::printRowContents(HumdrumFile& infile, int row) {
-	int fieldcount = infile[row].getFieldCount();
-	for (int i=0; i<fieldcount; i++) {
-		HTp token = infile.token(row, i);
-		m_free_text << "<td";
-		if (m_idQ) {
-			printId(token);
-		}
-		printCellClasses(token);
-		printCellData(token);
-		if (m_tabindexQ) {
-			printTabIndex(token);
+void Tool_half::terminalLongToTerminalBreve(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReference()) {
+			continue;
 		}
-		printColSpan(token);
-		if (!infile[row].isManipulator()) {
-			// do not allow manipulators to be edited
-			m_free_text << " contenteditable=\"true\"";
-		} else if (infile[row].isExclusive()) {
-			// but allow exclusive interpretation to be edited
-			m_free_text << " contenteditable=\"true\"";
+		HTp token = infile.token(i, 0);
+		if (token->find("terminal long") == string::npos) {
+			continue;
 		}
-		m_free_text << ">";
-		printToken(token);
-		m_free_text << "</td>";
+		string text = *token;
+		hre.replaceDestructive(text, "terminal breve", "terminal long", "g");
+		token->setText(text);
 	}
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_humsheet::printCellData --
+// Tool_hands::Tool_hands -- Set the recognized options for the tool.
 //
 
-void Tool_humsheet::printCellData(HTp token) {
-	int field = token->getFieldIndex();
-	m_free_text << " data-field=\"" << field << "\"";
-
-
-	if (token->getOwner()->hasSpines()) {
-		int spine = token->getTrack() - 1;
-		m_free_text << " data-spine=\"" << spine << "\"";
-
-		int subspine = token->getSubtrack();
-		if (subspine > 0) {
-			m_free_text << " data-subspine=\"" << subspine << "\"";
-		}
-
-		string exinterp = token->getDataType().substr(2);
-		if (m_exinterpQ && !exinterp.empty()) {
-			m_free_text << " data-x=\"" << exinterp << "\"";
-		}
-	}
+Tool_hands::Tool_hands(void) {
+	define("c|color=b", "color right-hand notes red and left-hand notes blue");
+	define("lcolor|left-color=s:dodgerblue", "color of left-hand notes");
+	define("rcolor|right-color=s:crimson",   "color of right-hand notes");
+	define("l|left-only=b",                  "remove right-hand notes");
+	define("r|right-only=b",                 "remove left-hand notes");
+	define("m|mark=b",                       "mark left and right-hand notes");
+	define("a|attacks-only=b",               "only mark note attacks and not note sustains");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humsheet::printToken --
+// Tool_hands::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_humsheet::printToken(HTp token) {
-	for (int i=0; i<(int)token->size(); i++) {
-		switch (token->at(i)) {
-			case '>':
-				m_free_text << "&gt;";
-				break;
-			case '<':
-				m_free_text << "&lt;";
-				break;
-			default:
-				m_free_text << token->at(i);
-		}
-	}
+void Tool_hands::initialize(void) {
+	m_colorQ       = getBoolean("color");
+	m_leftColor    = getString("left-color");
+	m_rightColor   = getString("right-color");
+	m_leftOnlyQ    = getBoolean("left-only");
+	m_rightOnlyQ   = getBoolean("right-only");
+	m_markQ        = getBoolean("mark");
+	m_attacksOnlyQ = getBoolean("attacks-only");
 }
 
 
 
-///////////////////////////////
+/////////////////////////////////
 //
-// Tool_humsheet::printId --
+// Tool_hands::run -- Do the main work of the tool.
 //
 
-void Tool_humsheet::printId(HTp token) {
-	int line = token->getLineNumber();
-	int field = token->getFieldNumber();
-	string id = "tok-L";
-	id += to_string(line);
-	id += "F";
-	id += to_string(field);
-	m_free_text << " id=\"" << id << "\"";
+bool Tool_hands::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
 }
 
 
 
-///////////////////////////////
-//
-// Tool_humsheet::printTabIndex --
-//
+bool Tool_hands::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
 
-void Tool_humsheet::printTabIndex(HTp token) {
-	string number = token->getValue("auto", "tabindex");
-	if (number.empty()) {
-		return;
+
+bool Tool_hands::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	m_free_text << " tabindex=\"" << number << "\"";
+	return status;
+}
+
+
+bool Tool_hands::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_humsheet::printColspan -- print any necessary colspan values for
-//    token (to align by primary spines)
+// Tool_hands::processFile --
 //
 
-void Tool_humsheet::printColSpan(HTp token) {
-	if (!token->getOwner()->hasSpines()) {
-		m_free_text << " colspan=\"" << m_max_field << "\"";
-		return;
-	}
-	int track = token->getTrack() - 1;
-	int scount = m_max_subtrack.at(track);
-	int subtrack = token->getSubtrack();
-	if (subtrack > 1) {
-		subtrack--;
+void Tool_hands::processFile(HumdrumFile& infile) {
+	if (m_markQ || m_leftOnlyQ || m_rightOnlyQ) {
+		infile.doHandAnalysis(m_attacksOnlyQ);
 	}
-	HTp nexttok = token->getNextFieldToken();
-	int ntrack = -1;
-	if (nexttok) {
-		ntrack = nexttok->getTrack() - 1;
+	if (m_leftOnlyQ) {
+		removeNotes(infile, "RH");
+	} else if (m_rightOnlyQ) {
+		removeNotes(infile, "LH");
 	}
-	if ((ntrack < 0) || (ntrack != track)) {
-		// at the end of a primary spine, so do a colspan with the remaining subtracks
-		if (subtrack < scount-1) {
-			int colspan = scount - subtrack;
-			m_free_text << " colspan=\"" << colspan << "\"";
-		}
-	} else {
-		// do nothing
+	if (m_colorQ) {
+		colorHands(infile);
+	} else if (m_markQ) {
+		markNotes(infile);
 	}
+	m_humdrum_text << infile;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// printCellClasses --
+// Tool_hands::removeNotes --
+//
+
+void Tool_hands::removeNotes(HumdrumFile& infile, const string& htype) {
+	int counter = 0;
+	int scount = infile.getStrandCount();
+	for (int i=0; i<scount; i++) {
+		HTp sstart = infile.getStrandStart(i);
+		HTp xtok = sstart->getExclusiveInterpretation();
+		int hasHandMarkup = xtok->getValueInt("auto", "hand");
+		if (!hasHandMarkup) {
+			continue;
+		}
+		HTp send = infile.getStrandEnd(i);
+		removeNotes(sstart, send, htype);
+		counter++;
+	}
+
+	
+	if (counter) {
+		infile.createLinesFromTokens();
+	}
+}
+
+
+void Tool_hands::removeNotes(HTp sstart, HTp send, const string& htype) {
+	HTp current = sstart;
+	while (current && (current != send)) {
+		if (!current->isData() || current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+
+		HumRegex hre;
+		string ttype = current->getValue("auto", "hand");
+		if (ttype != htype) {
+			current = current->getNextToken();
+			continue;
+		}
+		string text = *current;
+		hre.replaceDestructive(text, "", "[^0-9.%q ]", "g");
+		hre.replaceDestructive(text, "ryy ", " ", "g");
+		text += "ryy";
+		current->setText(text);
+		current = current->getNextToken();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_hands::markNotes --
+//
+
+void Tool_hands::markNotes(HumdrumFile& infile) {
+	HumRegex hre;
+
+	int counter = 0;
+	int scount = infile.getStrandCount();
+	for (int i=0; i<scount; i++) {
+		HTp sstart = infile.getStrandStart(i);
+		HTp xtok = sstart->getExclusiveInterpretation();
+		int hasHandMarkup = xtok->getValueInt("auto", "hand");
+		if (!hasHandMarkup) {
+			continue;
+		}
+		HTp send   = infile.getStrandEnd(i);
+		markNotes(sstart, send);
+		counter++;
+	}
+
+	if (counter) {
+		infile.appendLine("!!!RDF**kern: " + m_leftMarker + " = marked note, color=\"" + m_leftColor + "\", left-hand note");
+		infile.appendLine("!!!RDF**kern: " + m_rightMarker + " = marked note, color=\"" + m_rightColor + "\", right-hand note");
+		infile.createLinesFromTokens();
+	}
+}
+
+
+void Tool_hands::markNotes(HTp sstart, HTp send) {
+	HTp current = sstart;
+	while (current && (current != send)) {
+		if (!current->isData() || current->isNull() || current->isRest()) {
+			current = current->getNextToken();
+			continue;
+		}
+
+		HumRegex hre;
+		string text = *current;
+		string htype = current->getValue("auto", "hand");
+		if (htype == "LH") {
+			hre.replaceDestructive(text, " " + m_leftMarker, " +", "g");
+			text = m_leftMarker + text;
+		} else if (htype == "RH") {
+			hre.replaceDestructive(text, " " + m_rightMarker, " +", "g");
+			text = m_rightMarker + text;
+		}
+		current->setText(text);
+		current = current->getNextToken();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_hands::colorHands -- Convert for example *LH into *color:dodgerblue.
+//
+
+void Tool_hands::colorHands(HumdrumFile& infile) {
+	string left = "*color:" + m_leftColor;
+	string right = "*color:" + m_rightColor;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		bool changed = false;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (*token == "*LH") {
+				token->setText(left);
+				changed = true;
+			}
+			if (*token == "*RH") {
+				token->setText(right);
+				changed = true;
+			}
+		}
+		if (changed) {
+			infile[i].createLineFromTokens();
+		}
+	}
+}
+
+
+
+
+/////////////////////////////////
+//
+// Tool_homorhythm::Tool_homorhythm -- Set the recognized options for the tool.
+//
+
+Tool_homorhythm::Tool_homorhythm(void) {
+	define("a|append=b",                 "append analysis to end of input data");
+	define("attacks=b",                  "append attack counts for each sonority");
+	define("p|prepend=b",                "prepend analysis to end of input data");
+	define("r|raw-sonority=b",           "display individual sonority scores only");
+	define("raw-score=b",                "display accumulated scores");
+	define("M|no-marks=b",               "do not mark homorhythm section notes");
+	define("f|fraction=b",               "calculate fraction of music that is homorhythm");
+	define("v|voice=b",                  "display voice information or fraction results");
+	define("F|filename=b",               "show filename for f option");
+	define("n|t|threshold=d:4.0",        "threshold score sum required for homorhythm texture detection");
+	define("s|score=d:1.0",              "score assigned to a sonority with three or more attacks");
+	define("m|intermediate-score=d:0.5", "score to give sonority between two adjacent attack sonoroties");
+	define("l|letter=b",                 "display letter scoress before calculations");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_homorhythm::run -- Do the main work of the tool.
+//
+
+bool Tool_homorhythm::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_homorhythm::run(const string& indata, ostream& out) {
+	HumdrumFile infile;
+	infile.readStringNoRhythm(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_homorhythm::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_homorhythm::run(HumdrumFile& infile) {
+	initialize();
+	infile.analyzeStructure();
+	m_voice_count = getExtantVoiceCount(infile);
+	m_letterQ = getBoolean("letter");
+	processFile(infile);
+	infile.createLinesFromTokens();
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::markHomophonicNotes --
+//
+
+void Tool_homorhythm::markHomophonicNotes(void) {
+	// currently done with a **color spine
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::initialize --
+//
+
+void Tool_homorhythm::initialize(void) {
+	m_threshold = getInteger("threshold");
+	if (m_threshold < 1.0) {
+		m_threshold = 1.0;
+	}
+
+	m_score = getDouble("score");
+	if (m_score < 1.0) {
+		m_score = 1.0;
+	}
+
+	m_intermediate_score = getDouble("intermediate-score");
+	if (m_intermediate_score < 0.0) {
+		m_intermediate_score = 0.0;
+	}
+
+	if (m_intermediate_score > m_score) {
+		m_intermediate_score = m_score;
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::processFile --
+//
+
+void Tool_homorhythm::processFile(HumdrumFile& infile) {
+	vector<int> data;
+	data.reserve(infile.getLineCount());
+
+	m_homorhythm.clear();
+	m_homorhythm.resize(infile.getLineCount());
+
+	m_notecount.clear();
+	m_notecount.resize(infile.getLineCount());
+	fill(m_notecount.begin(), m_notecount.end(), 0);
+
+	m_attacks.clear();
+	m_attacks.resize(infile.getLineCount());
+	fill(m_attacks.begin(), m_attacks.end(), 0);
+
+	m_notes.clear();
+	m_notes.resize(infile.getLineCount());
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		data.push_back(i);
+		analyzeLine(infile, i);
+	}
+
+	// change Y N Y patterns to Y Y Y
+	for (int i=1; i<(int)data.size() - 1; i++) {
+		if (m_homorhythm[data[i]] == "Y") {
+			continue;
+		}
+		if (m_homorhythm[data[i+1]] == "N") {
+			continue;
+		}
+		if (m_homorhythm[data[i-1]] == "N") {
+			continue;
+		}
+	  	m_homorhythm[data[i]] = "NY";  // not homphonic by will get intermediate score.
+	}
+
+	vector<double> score(infile.getLineCount(), 0);
+	vector<double> raw(infile.getLineCount(), 0);
+
+	double sum = 0.0;
+	for (int i=0; i<(int)data.size(); i++) {
+		if (m_homorhythm[data[i]].find("Y") != string::npos) {
+			if (m_homorhythm[data[i]].find("N") != string::npos) {
+				// sonority between two homorhythm-like sonorities.
+				// maybe also differentiate based on metric position.
+				sum += m_intermediate_score;
+				raw[data[i]] = m_intermediate_score;
+			} else {
+				sum += m_score;
+				raw[data[i]] = m_score;
+			}
+		} else {
+			sum = 0.0;
+		}
+		score[data[i]] = sum;
+	}
+
+	for (int i=(int)data.size()-2; i>=0; i--) {
+		if (score[data[i]] == 0) {
+			continue;
+		}
+		if (score[data[i+1]] > score[data[i]]) {
+			score[data[i]] = score[data[i+1]];
+		}
+	}
+
+	if (getBoolean("raw-score")) {
+		addAccumulatedScores(infile, score);
+	}
+
+	if (getBoolean("raw-sonority")) {
+		addRawAnalysis(infile, raw);
+	}
+	if (getBoolean("raw-score")) {
+		addAccumulatedScores(infile, score);
+	}
+
+	if (getBoolean("fraction")) {
+		addFractionAnalysis(infile, score);
+	}
+
+	if (getBoolean("attacks")) {
+		addAttacks(infile, m_attacks);
+	}
+
+	if (!getBoolean("fraction")) {
+		// Color the notes within homorhythm textures.
+		// mark homorhythm regions in red,
+		// non-homorhythm sonorities within these regions in green
+		// and non-homorhythm regions in black.
+		if (m_letterQ) {
+			infile.appendDataSpine(m_homorhythm, "", "**hp");
+		}
+		for (int i=0; i<(int)data.size(); i++) {
+			if (score[data[i]] >= m_threshold) {
+				if (m_attacks[data[i]] < (int)m_notes[data[i]].size() - 1) {
+					m_homorhythm[data[i]] = "dodgerblue";
+				} else {
+					m_homorhythm[data[i]] = "red";
+				}
+			} else {
+				m_homorhythm[data[i]] = "black";
+			}
+		}
+		infile.appendDataSpine(m_homorhythm, "", "**color");
+
+		// problem with **color spine in javascript, so output via humdrum text
+		m_humdrum_text << infile;
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::addAccumulatedScores --
+//
+
+void Tool_homorhythm::addAccumulatedScores(HumdrumFile& infile, vector<double>& score) {
+	infile.appendDataSpine(score, "", "**score", false);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::addRawAnalysis --
+//
+
+void Tool_homorhythm::addRawAnalysis(HumdrumFile& infile, vector<double>& raw) {
+	infile.appendDataSpine(raw, "", "**raw", false);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::addAttacks --
+//
+
+void Tool_homorhythm::addAttacks(HumdrumFile& infile, vector<int>& attacks) {
+	infile.appendDataSpine(attacks, "", "**atks");
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::addFractionAnalysis --
+//
+
+void Tool_homorhythm::addFractionAnalysis(HumdrumFile& infile, vector<double>& score) {
+	double sum = 0.0;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		if (score[i] > m_threshold) {
+			sum += infile[i].getDuration().getFloat();
+		}
+	}
+	double total = infile.getScoreDuration().getFloat();
+	int ocount = getOriginalVoiceCount(infile);
+	double fraction = sum / total;
+	double percent = int(fraction * 1000.0 + 0.5)/10.0;
+	if (getBoolean("filename")) {
+		m_free_text << infile.getFilename() << "\t";
+	}
+	if (getBoolean("voice")) {
+		m_free_text << ocount;
+		m_free_text << "\t";
+		m_free_text << m_voice_count;
+		m_free_text << "\t";
+		if (ocount == m_voice_count) {
+			m_free_text << "complete" << "\t";
+		} else {
+			m_free_text << "incomplete" << "\t";
+		}
+	}
+	if (m_voice_count < 2) {
+		m_free_text << -1;
+	} else {
+		m_free_text << percent;
+	}
+	m_free_text << endl;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::getOriginalVoiceCount --
+//
+
+int Tool_homorhythm::getOriginalVoiceCount(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReference()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		if (hre.search(token, "^\\!\\!\\!voices\\s*:\\s*(\\d+)")) {
+			int count = hre.getMatchInt(1);
+			if (hre.search(token, "bc", "i")) {
+				// add one for basso-continuo
+				count++;
+			}
+			return count;
+		}
+	}
+	return 0;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::getExtantVoiceCount --
+//
+
+int Tool_homorhythm::getExtantVoiceCount(HumdrumFile& infile) {
+	vector<HTp> spines = infile.getKernSpineStartList();
+	return (int)spines.size();
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm::analyzeLine --
+//
+
+void Tool_homorhythm::analyzeLine(HumdrumFile& infile, int line) {
+	m_notes[line].reserve(10);
+	HPNote note;
+	if (!infile[line].isData()) {
+		return;
+	}
+	int nullQ = 0;
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		HTp token = infile.token(line, i);
+		if (!token->isKern()) {
+			continue;
+		}
+		if (token->isRest()) {
+			continue;
+		}
+		if (token->isNull()) {
+			nullQ = 1;
+			token = token->resolveNull();
+			if (!token) {
+				continue;
+			}
+			if (token->isRest()) {
+				continue;
+			}
+		} else {
+			nullQ = 0;
+		}
+		int track = token->getTrack();
+		vector<string> subtokens = token->getSubtokens();
+		for (int j=0; j<(int)subtokens.size(); j++) {
+			note.track = track;
+			note.line = token->getLineIndex();
+			note.field = token->getFieldIndex();
+			note.subfield = j;
+			note.token = token;
+			note.text = subtokens[j];
+			note.duration = Convert::recipToDuration(note.text);
+			if (nullQ) {
+				note.attack = false;
+				note.nullQ = true;
+			} else {
+				note.nullQ = false;
+				if ((note.text.find("_") != string::npos) ||
+				    (note.text.find("]") != string::npos)) {
+					note.attack = false;
+				} else {
+					note.attack = true;
+				}
+			}
+			m_notes[line].push_back(note);
+		}
+	}
+
+	// There must be at least three attacks to be considered homorhythm
+	// maybe adjust to N-1 or three voices, or a similar rule.
+	vector<HumNum> adurs;
+	for (int i=0; i<(int)m_notes[line].size(); i++) {
+		if (m_notes[line][i].attack) {
+			adurs.push_back(m_notes[line][i].duration);
+			m_attacks[line]++;
+		}
+	}
+	// if ((int)m_attacks[line] >= (int)m_notes[line].size() - 1) {
+	if ((int)m_attacks[line] >= 3) {
+		string value = "Y";
+		// value += to_string(m_attacks[line]);
+		m_homorhythm[line] = value;
+	} else if ((m_voice_count == 3) && (m_attacks[line] == 2)) {
+		if ((adurs.size() >= 2) && (adurs[0] == adurs[1])) {
+			m_homorhythm[line] = "Y";
+		} else {
+			m_homorhythm[line] = "N";
+		}
+	} else {
+		string value = "N";
+		// value += to_string(m_attacks[line]);
+		m_homorhythm[line] = value;
+	}
+	// redundant or three-or-more case:
+	if (m_notes[line].size() <= 2) {
+		m_homorhythm[line] = "N";
+	}
+}
+
+
+
+
+/////////////////////////////////
+//
+// Tool_homorhythm2::Tool_homorhythm -- Set the recognized options for the tool.
+//
+
+Tool_homorhythm2::Tool_homorhythm2(void) {
+	define("t|threshold=d:1.6",  "threshold score sum required for homorhythm texture detection");
+	define("u|threshold2=d:1.3", "threshold score sum required for semi-homorhythm texture detection");
+	define("s|score=b",          "show numeric scores");
+	define("n|length=i:4",       "sonority length to calculate");
+	define("f|fraction=b",       "report fraction of music that is homorhythm");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_homorhythm2::run -- Do the main work of the tool.
+//
+
+bool Tool_homorhythm2::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_homorhythm2::run(const string& indata, ostream& out) {
+	HumdrumFile infile;
+	infile.read(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_homorhythm2::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_homorhythm2::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm2::initialize --
+//
+
+void Tool_homorhythm2::initialize(void) {
+	m_threshold = getDouble("threshold");
+	if (m_threshold < 0.0) {
+		m_threshold = 0.0;
+	}
+	m_threshold2 = getDouble("threshold2");
+	if (m_threshold2 < 0.0) {
+		m_threshold2 = 0.0;
+	}
+	if (m_threshold < m_threshold2) {
+		double temp = m_threshold;
+		m_threshold = m_threshold2;
+		m_threshold2 = temp;
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_homorhythm2::processFile --
+//
+
+void Tool_homorhythm2::processFile(HumdrumFile& infile) {
+	infile.analyzeStructure();
+	NoteGrid grid(infile);
+	m_score.resize(infile.getLineCount());
+	fill(m_score.begin(), m_score.end(), 0.0);
+
+	double score;
+	int count;
+	int wsize = getInteger("length");
+
+	for (int i=0; i<grid.getSliceCount()-wsize; i++) {
+		score = 0;
+		count = 0;
+		for (int j=0; j<grid.getVoiceCount(); j++) {
+			for (int k=j+1; k<grid.getVoiceCount(); k++) {
+				for (int m=0; m<wsize; m++) {
+					NoteCell* cell1 = grid.cell(j, i+m);
+					if (cell1->isRest()) {
+						continue;
+					}
+					NoteCell* cell2 = grid.cell(k, i+m);
+					if (cell2->isRest()) {
+						continue;
+					}
+					count++;
+					if (cell1->isAttack() && cell2->isAttack()) {
+						score += 1.0;
+					}
+				}
+			}
+		}
+		int index = grid.getLineIndex(i);
+		m_score[index] = score / count;
+	}
+
+	for (int i=grid.getSliceCount()-1; i>=wsize; i--) {
+		score = 0;
+		count = 0;
+		for (int j=0; j<grid.getVoiceCount(); j++) {
+			for (int k=j+1; k<grid.getVoiceCount(); k++) {
+				for (int m=0; m<wsize; m++) {
+					NoteCell* cell1 = grid.cell(j, i-m);
+					if (cell1->isRest()) {
+						continue;
+					}
+					NoteCell* cell2 = grid.cell(k, i-m);
+					if (cell2->isRest()) {
+						continue;
+					}
+					count++;
+					if (cell1->isAttack() && cell2->isAttack()) {
+						score += 1.0;
+					}
+				}
+			}
+		}
+		int index = grid.getLineIndex(i);
+		m_score[index] += score / count;
+	}
+
+
+	for (int i=0; i<(int)m_score.size(); i++) {
+		m_score[i] = int(m_score[i] * 100.0 + 0.5) / 100.0;
+	}
+
+
+	vector<string> color(infile.getLineCount());;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		if (m_score[i] >= m_threshold) {
+			color[i] = "red";
+		} else if (m_score[i] >= m_threshold2) {
+			color[i] = "orange";
+		} else {
+			color[i] = "black";
+		}
+	}
+
+	if (getBoolean("fraction")) {
+		HumNum sum = 0;
+		HumNum total = infile.getScoreDuration();
+		for (int i=0; i<(int)m_score.size(); i++) {
+			if (m_score[i] >= m_threshold2) {
+				sum += infile[i].getDuration();
+			}
+		}
+		HumNum fraction = sum / total;
+		m_free_text << int(fraction.getFloat() * 1000.0 + 0.5) / 10.0 << endl;
+	} else {
+		if (getBoolean("score")) {
+			infile.appendDataSpine(m_score, ".", "**cdata", false);
+		}
+		infile.appendDataSpine(color, ".", "**color", true);
+		infile.createLinesFromTokens();
+
+		// problem within emscripten-compiled version, so force to output as string:
+		m_humdrum_text << infile;
+	}
+
+}
+
+
+
+
+
+
+/////////////////////////////////
+//
+// Tool_gridtest::Tool_hproof -- Set the recognized options for the tool.
+//
+
+Tool_hproof::Tool_hproof(void) {
+	// put option definitions here
+}
+
+
+
+///////////////////////////////
+//
+// Tool_hproof::run -- Primary interfaces to the tool.
+//
+
+bool Tool_hproof::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+bool Tool_hproof::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
+}
+
+
+bool Tool_hproof::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	out << infile;
+	return status;
+}
+
+
+bool Tool_hproof::run(HumdrumFile& infile) {
+	markNonChordTones(infile);
+	infile.appendLine("!!!RDF**kern: N = marked note, color=chocolate (non-chord tone)");
+	infile.appendLine("!!!RDF**kern: Z = marked note, color=black (chord tone)");
+	infile.createLinesFromTokens();
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_hproof::markNonChordTones -- Mark
+//
+
+void Tool_hproof::markNonChordTones(HumdrumFile& infile) {
+	vector<HTp> list;
+	infile.getSpineStartList(list);
+	vector<HTp> hlist;
+	for (auto it : list) {
+		if (*it == "**harm") {
+			hlist.push_back(it);
+		}
+		if (*it == "**rhrm") {
+			hlist.push_back(it);
+		}
+	}
+	if (hlist.empty()) {
+		cerr << "Warning: No **harm or **rhrm spines in data" << endl;
+		return;
+	}
+
+	processHarmSpine(infile, hlist[0]);
+}
+
+
+
+//////////////////////////////
+//
+// processHarmSpine --
+//
+
+void Tool_hproof::processHarmSpine(HumdrumFile& infile, HTp hstart) {
+	string key = "*C:";  // assume C major if no key designation
+	HTp token = hstart;
+	HTp ntoken = token->getNextNNDT();
+	while (token) {
+		markNotesInRange(infile, token, ntoken, key);
+		if (!ntoken) {
+			break;
+		}
+		if (ntoken && token) {
+			getNewKey(token, ntoken, key);
+		}
+		token = ntoken;
+		ntoken = ntoken->getNextNNDT();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_hproof::getNewKey --
+//
+
+void Tool_hproof::getNewKey(HTp token, HTp ntoken, string& key) {
+	token = token->getNextToken();
+	while (token && (token != ntoken)) {
+		if (token->isKeyDesignation()) {
+			key = *token;
+		}
+		token = token->getNextToken();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_hproof::markNotesInRange --
+//
+
+void Tool_hproof::markNotesInRange(HumdrumFile& infile, HTp ctoken, HTp ntoken, const string& key) {
+	if (!ctoken) {
+		return;
+	}
+	int startline = ctoken->getLineIndex();
+	int stopline = infile.getLineCount();
+	if (ntoken) {
+		stopline = ntoken->getLineIndex();
+	}
+	vector<int> cts;
+	cts = Convert::harmToBase40(ctoken, key);
+	for (int i=startline; i<stopline; i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			if (!infile.token(i, j)->isKern()) {
+				continue;
+			}
+			HTp tok = infile.token(i, j);
+			if (tok->isNull()) {
+				continue;
+			}
+			if (tok->isRest()) {
+				continue;
+			}
+			markHarmonicTones(tok, cts);
+		}
+	}
+
+// cerr << "TOK\t" << ctoken << "\tLINES\t" << startline << "\t" << stopline << "\t";
+// for (int i=0; i<cts.size(); i++) {
+// cerr << " " << Convert::base40ToKern(cts[i]);
+// }
+// cerr << endl;
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_hproof::markHarmonicTones --
+//
+
+void Tool_hproof::markHarmonicTones(HTp tok, vector<int>& cts) {
+	int count = tok->getSubtokenCount();
+	vector<int> notes = cts;
+	string output;
+	for (int i=0; i<count; i++) {
+		string subtok = tok->getSubtoken(i);
+		int pitch = Convert::kernToBase40(subtok);
+		if (i > 0) {
+			output += " ";
+		}
+		bool found = false;
+		for (int j=0; j<(int)cts.size(); j++) {
+			if (pitch % 40 == cts[j] % 40) {
+				output += subtok;
+				output += "Z";
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			output += subtok;
+			output += "N";
+		}
+	}
+	tok->setText(output);
+}
+
+
+
+
+
+/////////////////////////////////
+//
+// Tool_humbreak::Tool_humbreak -- Set the recognized options for the tool.
+//
+
+Tool_humbreak::Tool_humbreak(void) {
+	define("m|measures=s",             "measures numbers to place linebreaks before");
+	define("p|page-breaks=s",          "measure numbers to place page breaks before");
+	define("g|group=s:original",       "line/page break group");
+	define("r|remove|remove-breaks=b", "remove line/page breaks");
+	define("l|page-to-line-breaks=b",  "convert page breaks to line breaks");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_humbreak::run -- Do the main work of the tool.
+//
+
+bool Tool_humbreak::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_humbreak::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_humbreak::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_humbreak::run(HumdrumFile& infile) {
+	processFile(infile);
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humbreak::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
+//
+
+void Tool_humbreak::initialize(void) {
+	string systemMeasures = getString("measures");
+	string pageMeasures = getString("page-breaks");
+	m_group = getString("group");
+	m_removeQ = getBoolean("remove-breaks");
+	m_page2lineQ = getBoolean("page-to-line-breaks");
+
+	vector<string> lbs;
+	vector<string> pbs;
+	HumRegex hre;
+	hre.split(lbs, systemMeasures, "[^\\da-z]+");
+	hre.split(pbs, pageMeasures, "[^\\da-z]+");
+
+	for (int i=0; i<(int)lbs.size(); i++) {
+		if (hre.search(lbs[i], "^(p?)(\\d+)([a-z]?)")) {
+			int number = hre.getMatchInt(2);
+			if (!hre.getMatch(1).empty()) {
+				m_pageMeasures[number] = 1;
+				int offset = 0;
+				string letter;
+				if (!hre.getMatch(3).empty()) {
+					letter = hre.getMatch(3);
+					offset = letter.at(0) - 'a';
+				}
+				m_pageOffset[number] = offset;
+			} else {
+				m_lineMeasures[number] = 1;
+				int offset = 0;
+				if (!hre.getMatch(3).empty()) {
+					string letter = hre.getMatch(3);
+					offset = letter.at(0) - 'a';
+				}
+				m_lineOffset[number] = offset;
+			}
+		}
+	}
+
+	for (int i=0; i<(int)pbs.size(); i++) {
+		if (hre.search(pbs[i], "^(\\d+)([a-z]?)")) {
+			int number = hre.getMatchInt(1);
+			m_pageMeasures[number] = 1;
+			int offset = 0;
+			if (!hre.getMatch(2).empty()) {
+				string letter = hre.getMatch(2);
+				offset = letter.at(0) - 'a';
+			}
+			m_pageOffset[number] = offset;
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humbreak::markLineBreakMeasures --
+//
+
+void Tool_humbreak::markLineBreakMeasures(HumdrumFile& infile) {
+	vector<HLp> pbreak;
+	vector<HLp> lbreak;
+	HumRegex hre;
+	map<int, int> used;
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isCommentGlobal()) {
+			HTp token = infile[i].token(0);
+			if (hre.search(token, "^!!LO:LB:")) {
+				lbreak.push_back(&infile[i]);
+			} else if (hre.search(token, "^!!LO:PB:")) {
+				pbreak.push_back(&infile[i]);
+			}
+		}
+
+		if (!infile[i].isBarline()) {
+			continue;
+		}
+
+		int barnum = infile[i].getBarNumber();
+		if (barnum < 0) {
+			lbreak.clear();
+			pbreak.clear();
+			continue;
+		}
+
+		int status = m_lineMeasures[barnum];
+		if (status) {
+			HLp line = &infile[i];
+			int offset = m_lineOffset[barnum];
+			if (offset && (used[barnum] == 0)) {
+				used[barnum] = offset;
+				int ocounter = 0;
+				lbreak.clear();
+				pbreak.clear();
+				for (int j=i+1; j<infile.getLineCount(); j++) {
+					if (infile[i].isCommentGlobal()) {
+						HTp token = infile.token(i, 0);
+						if (hre.search(token, "^!!LO:LB:")) {
+							lbreak.push_back(&infile[i]);
+						}
+						if (hre.search(token, "^!!LO:PB:")) {
+							pbreak.push_back(&infile[i]);
+						}
+					}
+					if (!infile[j].isBarline()) {
+						continue;
+					}
+					ocounter++;
+					if (ocounter == offset) {
+						line = &infile[j];
+					}
+				}
+				if (!lbreak.empty()) {
+					lbreak.back()->setValue("auto", "barnum", barnum + 1);
+				} else {
+					line->setValue("auto", "barnum", barnum + 1);
+				}
+			} else {
+				line->setValue("auto", "barnum", barnum + 1);
+			}
+		}
+
+		status = m_pageMeasures[barnum];
+		if (status) {
+			HLp line = &infile[i];
+			int offset = m_pageOffset[barnum];
+			if (offset) {
+				int ocounter = 0;
+				lbreak.clear();
+				pbreak.clear();
+				for (int j=i+1; j<infile.getLineCount(); j++) {
+					if (infile[i].isCommentGlobal()) {
+						HTp token = infile.token(i, 0);
+						if (hre.search(token, "^!!LO:LB:")) {
+							lbreak.push_back(&infile[i]);
+						}
+						if (hre.search(token, "^!!LO:PB:")) {
+							pbreak.push_back(&infile[i]);
+						}
+					}
+					if (!infile[j].isBarline()) {
+						continue;
+					}
+					ocounter++;
+					if (ocounter == offset) {
+						line = &infile[j];
+					}
+				}
+				if (!pbreak.empty()) {
+					pbreak.back()->setValue("auto", "barnum", barnum + 1);
+					pbreak.back()->setValue("auto", "page", 1);
+				}
+			} else {
+				line->setValue("auto", "barnum", barnum + 1);
+				line->setValue("auto", "page", 1);
+			}
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humbreak::addBreaks --
+//
+
+void Tool_humbreak::addBreaks(HumdrumFile& infile) {
+	markLineBreakMeasures(infile);
+
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!(infile[i].isBarline() || infile[i].isComment())) {
+			m_humdrum_text << infile[i] << endl;
+			continue;
+		}
+
+		int barnum = infile[i].getValueInt("auto", "barnum");
+		if (barnum < 1) {
+			m_humdrum_text << infile[i] << endl;
+			continue;
+		}
+		barnum--;
+		int pageQ = infile[i].getValueInt("auto", "page");
+
+		if (pageQ && infile[i].isComment()) {
+			HTp token = infile.token(i, 0);
+			if (hre.search(token, "^!!LO:PB:")) {
+				// Add group to existing LO:PB:
+				HTp token = infile.token(i, 0);
+				HTp barToken = infile.token(i+1, 0);
+				if (barToken->isBarline()) {
+					int measure = infile[i+1].getBarNumber();
+					int pbStatus = m_pageMeasures[measure];
+					if (pbStatus) {
+						string query = "\\b" + m_group + "\\b";
+						if (!hre.match(token, query)) {
+							m_humdrum_text << token << ", " << m_group << endl;
+						} else {
+							m_humdrum_text << token << endl;
+						}
+					} else {
+						m_humdrum_text << token << endl;
+					}
+					m_humdrum_text << infile[i+1] << endl;
+					i++;
+					continue;
+				}
+			} else if (hre.search(token, "^!!LO:LB:")) {
+				// Add group to existing LO:LB:
+				HTp token = infile.token(i, 0);
+				HTp barToken = infile.token(i+1, 0);
+				if (barToken->isBarline()) {
+					int measure = infile[i+1].getBarNumber();
+					int lbStatus = m_lineMeasures[measure];
+					if (lbStatus) {
+						string query = "\\b" + m_group + "\\b";
+						if (!hre.match(token, query)) {
+							m_humdrum_text << token << ", " << m_group << endl;
+						} else {
+							m_humdrum_text << token << endl;
+						}
+					} else {
+						m_humdrum_text << token << endl;
+					}
+					m_humdrum_text << infile[i+1] << endl;
+					i++;
+					continue;
+				}
+			}
+		}
+
+		if (pageQ) {
+			m_humdrum_text << "!!LO:PB:g=" << m_group << endl;
+		} else {
+			m_humdrum_text << "!!LO:LB:g=" << m_group << endl;
+		}
+		m_humdrum_text << infile[i] << endl;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humbreak::processFile --
+//
+
+void Tool_humbreak::processFile(HumdrumFile& infile) {
+	initialize();
+	if (m_removeQ) {
+		removeBreaks(infile);
+	} else if (m_page2lineQ) {
+		convertPageToLine(infile);
+	} else {
+		addBreaks(infile);
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humbreak::removeBreaks --
+//
+
+void Tool_humbreak::removeBreaks(HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].token(0)->compare(0, 7, "!!LO:LB") == 0) {
+			continue;
+		}
+		if (infile[i].token(0)->compare(0, 7, "!!LO:PB") == 0) {
+			continue;
+		}
+		m_humdrum_text << infile[i] << endl;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humbreak::convertPageToLine --
+//
+
+void Tool_humbreak::convertPageToLine(HumdrumFile& infile) {
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].token(0)->compare(0, 7, "!!LO:PB") == 0) {
+			string text = *infile[i].token(0);
+			hre.replaceDestructive(text, "!!LO:LB", "!!LO:PB");
+			m_humdrum_text << text << endl;
+			continue;
+		}
+		m_humdrum_text << infile[i] << endl;
+	}
+}
+
+
+
+
+/////////////////////////////////
+//
+// Tool_humdiff::Tool_humdiff -- Set the recognized options for the tool.
+//
+
+Tool_humdiff::Tool_humdiff(void) {
+	define("r|reference=i:1",     "sequence number of reference score");
+	define("report=b",            "display report of differences");
+	define("time-points|times=b", "display timepoint lists for each file");
+	define("note-points|notes=b", "display notepoint lists for each file");
+	define("c|color=s:red",       "color for difference markers");
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::run --
+//
+
+bool Tool_humdiff::run(HumdrumFileSet& infiles) {
+	int reference = getInteger("reference") - 1;
+	if (reference < 0) {
+		cerr << "Error: reference has to be 1 or higher" << endl;
+		return false;
+	}
+	if (reference > infiles.getCount()) {
+		cerr << "Error: reference number is too large: " << reference << endl;
+		cerr << "Maximum is " << infiles.getCount() << endl;
+		return false;
+	}
+
+	if (infiles.getSize() == 0) {
+		cerr << "Usage: " << getCommand() << " files" << endl;
+		return false;
+	} else if (infiles.getSize() < 2) {
+		cerr << "Error: requires two or more files" << endl;
+		cerr << "Usage: " << getCommand() << " files" << endl;
+		return false;
+	} else {
+		HumNum targetdur = infiles[0].getScoreDuration();
+		for (int i=1; i<infiles.getSize(); i++) {
+			HumNum dur = infiles[i].getScoreDuration();
+			if (dur != targetdur) {
+				cerr << "Error: all files must have the same duration" << endl;
+				return false;
+			}
+		}
+
+		for (int i=0; i<infiles.getCount(); i++) {
+			if (i == reference) {
+				continue;
+			}
+			compareFiles(infiles[reference], infiles[i]);
+		}
+
+		if (!getBoolean("report")) {
+			infiles[reference].createLinesFromTokens();
+			m_humdrum_text << infiles[reference];
+			if (m_marked) {
+				m_humdrum_text << "!!!RDF**kern: @ = marked note";
+				if (getBoolean("color")) {
+					m_humdrum_text << "color=\"" << getString("color") << "\"";
+				}
+				m_humdrum_text << endl;
+			}
+		}
+	}
+
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::compareFiles --
+//
+
+void Tool_humdiff::compareFiles(HumdrumFile& reference, HumdrumFile& alternate) {
+	vector<vector<TimePoint>> timepoints(2);
+	extractTimePoints(timepoints.at(0), reference);
+	extractTimePoints(timepoints.at(1), alternate);
+
+	if (getBoolean("time-points")) {
+		printTimePoints(timepoints[0]);
+		printTimePoints(timepoints[1]);
+	}
+
+	compareTimePoints(timepoints, reference, alternate);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::printTimePoints --
+//
+
+void Tool_humdiff::printTimePoints(vector<TimePoint>& timepoints) {
+	for (int i=0; i<(int)timepoints.size(); i++) {
+		m_free_text << "TIMEPOINT " << i << ":" << endl;
+		m_free_text << timepoints[i] << endl;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::compareTimePoints --
+//
+
+void Tool_humdiff::compareTimePoints(vector<vector<TimePoint>>& timepoints,
+		HumdrumFile& reference, HumdrumFile& alternate) {
+	vector<int> indexes(timepoints.size(), 0);
+	HumNum minval;
+	HumNum value;
+	int found;
+
+	vector<HumdrumFile*> infiles(2, NULL);
+	infiles[0] = &reference;
+	infiles[1] = &alternate;
+
+	vector<int> increment(timepoints.size(), 0);
+
+	while ((1)) {
+		if (indexes.at(0) >= (int)timepoints.at(0).size()) {
+			// at the end of the list of notes for the first file.
+			// break from the comparison for now and figure out how
+			// to report differences of added notes in the other file(s)
+			// later.
+			break;
+		}
+		timepoints.at(0).at(indexes.at(0)).index.resize(timepoints.size());
+		for (int i=1; i<(int)timepoints.size(); i++) {
+			timepoints.at(0).at(indexes.at(0)).index.at(i) = -1;
+		}
+		minval = timepoints.at(0).at(indexes.at(0)).timestamp;
+		for (int i=1; i<(int)timepoints.size(); i++) {
+			if (indexes.at(i) >= (int)timepoints.at(i).size()) {
+				continue;
+			}
+			value = timepoints.at(i).at(indexes.at(i)).timestamp;
+			if (value < minval) {
+				minval = value;
+			}
+		}
+		found = 0;
+		fill(increment.begin(), increment.end(), 0);
+
+		for (int i=0; i<(int)timepoints.size(); i++) {
+			if (indexes.at(i) >= (int)timepoints.at(i).size()) {
+				// index is too large for file, so skip checking it.
+				continue;
+			}
+			found = 1;
+			value = timepoints.at(i).at(indexes.at(i)).timestamp;
+
+			if (value == minval) {
+				timepoints.at(0).at(indexes.at(0)).index.at(i) = timepoints.at(i).at(indexes.at(i)).index.at(0);
+				increment.at(i)++;
+			}
+		}
+		if (!found) {
+			break;
+		} else {
+			compareLines(minval, indexes, timepoints, infiles);
+		}
+		for (int i=0; i<(int)increment.size(); i++) {
+			indexes.at(i) += increment.at(i);
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::printNotePoints --
+//
+
+void Tool_humdiff::printNotePoints(vector<NotePoint>& notelist) {
+	m_free_text << "vvvvvvvvvvvvvvvvvvvvvvvvv" << endl;
+	for (int i=0; i<(int)notelist.size(); i++) {
+		m_free_text << "NOTE " << i << endl;
+		m_free_text << notelist.at(i) << endl;
+	}
+	m_free_text << "^^^^^^^^^^^^^^^^^^^^^^^^^" << endl;
+	m_free_text << endl;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::markNote -- mark the note (since it does not have a match in other edition(s).
+//
+
+void Tool_humdiff::markNote(NotePoint& np) {
+	m_marked = 1;
+	HTp token = np.token;
+	if (!token) {
+		return;
+	}
+	if (!token->isChord()) {
+		string contents = *token;
+		contents += "@";
+		token->setText(contents);
+		return;
+	}
+	vector<string> tokens = token->getSubtokens();
+	tokens[np.subindex] += "@";
+	string output = tokens[0];
+	for (int i=1; i<(int)tokens.size(); i++) {
+		output += " ";
+		output += tokens[i];
+	}
+	token->setText(output);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::compareLines --
+//
+
+void Tool_humdiff::compareLines(HumNum minval, vector<int>& indexes,
+		vector<vector<TimePoint>>& timepoints, vector<HumdrumFile*> infiles) {
+
+	bool reportQ = getBoolean("report");
+
+	// cerr << "COMPARING LINES ====================================" << endl;
+	vector<vector<NotePoint>> notelist(indexes.size());
+
+	// Note: timepoints size must be 2
+	// and infiles size must be 2
+	for (int i=0; i<(int)timepoints.size(); i++) {
+		if (indexes.at(i) >= (int)timepoints.at(i).size()) {
+			continue;
+		}
+		if (timepoints.at(i).at(indexes.at(i)).timestamp != minval) {
+			// not at the same time
+			continue;
+		}
+
+		getNoteList(notelist.at(i), *infiles[i],
+			timepoints.at(i).at(indexes.at(i)).index[0],
+			timepoints.at(i).at(indexes.at(i)).measure, i, indexes.at(i));
+
+
+	}
+	for (int i=0; i<(int)notelist.at(0).size(); i++) {
+		notelist.at(0).at(i).matched.resize(notelist.size());
+		fill(notelist.at(0).at(i).matched.begin(), notelist.at(0).at(i).matched.end(), -1);
+		notelist.at(0).at(i).matched.at(0) = i;
+		for (int j=1; j<(int)notelist.size(); j++) {
+			int status = findNoteInList(notelist.at(0).at(i), notelist.at(j));
+			notelist.at(0).at(i).matched.at(j) = status;
+			if ((status < 0) && !reportQ) {
+				markNote(notelist.at(0).at(i));
+			}
+		}
+	}
+
+	if (getBoolean("notes")) {
+		for (int i=0; i<(int)notelist.size(); i++) {
+			cerr << "========== NOTES FOR I=" << i << endl;
+			printNotePoints(notelist.at(i));
+			cerr << endl;
+		}
+	}
+
+	if (!reportQ) {
+		return;
+	}
+
+	// report
+	for (int i=0; i<(int)notelist.at(0).size(); i++) {
+		for (int j=1; j<(int)notelist.at(0).at(i).matched.size(); j++) {
+			if (notelist.at(0).at(i).matched.at(j) < 0) {
+				cout << "NOTE " << notelist.at(0).at(i).subtoken
+				     << " DOES NOT HAVE EXACT MATCH IN SOURCE " << j << endl;
+				int humindex = notelist.at(0).at(i).token->getLineIndex();
+				cout << "\tREFERENCE MEASURE\t: " << notelist.at(0).at(i).measure << endl;
+				cout << "\tREFERENCE LINE NO.\t: " << humindex+1 << endl;
+				cout << "\tREFERENCE LINE TEXT\t: " << (*infiles[0])[humindex] << endl;
+
+				cout << "\tTARGET  " << j << " LINE NO. ";
+				if (j < 10) {
+					cout << " ";
+				}
+				cout << ":\t" << "X" << endl;
+
+				cout << "\tTARGET  " << j << " LINE TEXT";
+				if (j < 10) {
+					cout << " ";
+				}
+				cout << ":\t" << "X" << endl;
+
+				cout << endl;
+			}
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::findNoteInList --
+//
+
+int Tool_humdiff::findNoteInList(NotePoint& np, vector<NotePoint>& nps) {
+	for (int i=0; i<(int)nps.size(); i++) {
+		// cerr << "COMPARING " << np.token << " (" << np.b40 << ") TO " << nps.at(i).token << " (" << nps.at(i).b40 << ") " << endl;
+		if (nps.at(i).processed) {
+			continue;
+		}
+		if (nps.at(i).b40 != np.b40) {
+			continue;
+		}
+		if (nps.at(i).duration != np.duration) {
+			continue;
+		}
+		return i;
+	}
+	// cerr << "\tCannot find note " << np.token << " on line " << np.token->getLineIndex() << " in other work" << endl;
+	return -1;
+}
+
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::getNoteList --
+//
+
+void Tool_humdiff::getNoteList(vector<NotePoint>& notelist, HumdrumFile& infile, int line, int measure, int sourceindex, int tpindex) {
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		HTp token = infile.token(line, i);
+		if (!token->isKern()) {
+			continue;
+		}
+		if (token->isNull()) {
+			continue;
+		}
+		if (token->isRest()) {
+			continue;
+		}
+		int scount = token->getSubtokenCount();
+		int track = token->getTrack();
+		int layer = token->getSubtrack();
+		for (int j=0; j<scount; j++) {
+			string subtok = token->getSubtoken(j);
+			if (subtok.find("]") != string::npos) {
+				continue;
+			}
+			if (subtok.find("_") != string::npos) {
+				continue;
+			}
+			// found a note to store;
+			notelist.resize(notelist.size() + 1);
+			notelist.back().token = token;
+			notelist.back().subtoken = subtok;
+			notelist.back().subindex = j;
+			notelist.back().measurequarter = token->getDurationFromBarline();
+			notelist.back().measure =
+			notelist.back().track = track;
+			notelist.back().layer = layer;
+			notelist.back().sourceindex = sourceindex;
+			notelist.back().tpindex = tpindex;
+			notelist.back().duration = token->getTiedDuration();
+			notelist.back().b40 = Convert::kernToBase40(subtok);
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humdiff::extractTimePoints -- Extract a list of the timestamps in a file.
+//
+
+void Tool_humdiff::extractTimePoints(vector<TimePoint>& points, HumdrumFile& infile) {
+	TimePoint tp;
+	points.clear();
+	HumRegex hre;
+	points.reserve(infile.getLineCount());
+	int measure = -1;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isBarline()) {
+			if (hre.search(infile.token(i, 0), "(\\d+)")) {
+				measure = hre.getMatchInt(1);
+			}
+		}
+		if (!infile[i].isData()) {
+			continue;
+		}
+		if (infile[i].getDuration() == 0) {
+			// ignore grace notes for now
+			continue;
+		}
+		tp.clear();
+		tp.file.push_back(&infile);
+		tp.index.push_back(i);
+		tp.timestamp = infile[i].getDurationFromStart();
+		tp.measure = measure;
+		points.push_back(tp);
+	}
+}
+
+
+
+//////////////////////////////
+//
+// operator<< == print a TimePoint
+//
+
+ostream& operator<<(ostream& out, TimePoint& tp) {
+	out << "\ttimestamp:\t" << tp.timestamp.getFloat() << endl;
+	out << "\tmeasure:\t" << tp.measure << endl;
+	out << "\tindexes:\t" << endl;
+	for (int i=0; i<(int)tp.index.size(); i++) {
+		out << "\t\tindex " << i << " is:\t" << tp.index[i] << "\t" << (*tp.file[i])[tp.index[i]] << endl;
+	}
+	return out;
+}
+
+
+
+//////////////////////////////
+//
+// operator<< == print a NotePoint
+//
+
+ostream& operator<<(ostream& out, NotePoint& np) {
+	if (np.token) {
+		out << "\ttoken:\t\t" << np.token << endl;
+	}
+	out << "\ttoken index:\t" << np.subindex << endl;
+	if (!np.subtoken.empty()) {
+		out << "\tsubtoken:\t" << np.subtoken << endl;
+	}
+	out << "\tmeasure:\t" << np.measure << endl;
+	out << "\tmquarter:\t" << np.measurequarter << endl;
+	out << "\ttrack:\t\t" << np.track << endl;
+	out << "\tlayer:\t\t" << np.layer << endl;
+	out << "\tduration:\t" << np.duration << endl;
+	out << "\tb40:\t\t" << np.b40 << endl;
+	out << "\tprocessed:\t" << np.processed << endl;
+	out << "\tsourceindex:\t" << np.sourceindex << endl;
+	out << "\ttpindex:\t" << np.tpindex << endl;
+	out << "\tmatched:\t" << endl;
+	for (int i=0; i<(int)np.matched.size(); i++) {
+		out << "\t\tindex " << i << " is:\t" << np.matched[i] << endl;
+	}
+	return out;
+}
+
+
+
+
+
+/////////////////////////////////
+//
+// Tool_humsheet::Tool_humsheet -- Set the recognized options for the tool.
+//
+
+Tool_humsheet::Tool_humsheet(void) {
+	define("h|H|html|HTML=b",       "output table in HTML wrapper");
+	define("i|id|ID=b",             "include ID for each cell");
+	define("z|zebra=b",             "add zebra striping by spine to style");
+	define("y|z2|zebra2|zebra-2=b", "zebra striping by data type");
+	define("t|tab-index=b",         "vertical tab indexing");
+	define("X|no-exinterp=b",       "do not embed exclusive interp data");
+	define("J|no-javascript=b",     "do not embed javascript code");
+	define("S|no-style=b",          "do not embed CSS style element");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_humsheet::run -- Do the main work of the tool.
+//
+
+bool Tool_humsheet::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_humsheet::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_humsheet::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_humsheet::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
+//
+
+void Tool_humsheet::initialize(void) {
+	m_idQ         = getBoolean("id");
+	m_htmlQ       = getBoolean("html");
+	m_zebraQ      = getBoolean("zebra");
+	m_zebra2Q     = getBoolean("zebra2");
+	m_exinterpQ   = !getBoolean("no-exinterp");
+	m_javascriptQ = !getBoolean("no-javascript");
+	m_tabindexQ   = getBoolean("tab-index");
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::processFile --
+//
+
+void Tool_humsheet::processFile(HumdrumFile& infile) {
+	analyzeTracks(infile);
+	if (m_htmlQ) {
+		printHtmlHeader();
+		printStyle(infile);
+	}
+	if (m_tabindexQ) {
+		analyzeTabIndex(infile);
+	}
+	m_free_text << "<table class=\"humdrum\"";
+	m_free_text << " data-spine-count=\"" << infile.getMaxTrack() << "\"";
+	m_free_text << ">\n";
+	for (int i=0; i<infile.getLineCount(); i++) {
+		m_free_text << "<tr";
+		printRowClasses(infile, i);
+		printRowData(infile, i);
+		printTitle(infile, i);
+		m_free_text << ">";
+		printRowContents(infile, i);
+		m_free_text << "</tr>\n";
+	}
+	m_free_text << "</table>";
+	if (m_htmlQ) {
+		if (m_javascriptQ) {
+			printJavascript();
+		}
+		printHtmlFooter();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::printTitle --
+//
+
+void Tool_humsheet::printTitle(HumdrumFile& infile, int line) {
+	if (!infile[line].isReference()) {
+		return;
+	}
+	string meaning = Convert::getReferenceKeyMeaning(infile[line].token(0));
+	if (!meaning.empty()) {
+		m_free_text << " title=\"" << meaning << "\"";
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::printRowData --
+//
+
+void Tool_humsheet::printRowData(HumdrumFile& infile, int line) {
+	m_free_text << " data-line=\"" << line << "\"";
+}
+
+
+
+///////////////////////////////
+//
+// printHtmlHeader --
+//
+
+void Tool_humsheet::printHtmlHeader(void) {
+	m_free_text << "<html>\n";
+	m_free_text << "<head>\n";
+	m_free_text << "<title>\n";
+	m_free_text << "UNTITLED\n";
+	m_free_text << "</title>\n";
+	m_free_text << "</head>\n";
+	m_free_text << "<body>\n";
+}
+
+
+
+///////////////////////////////
+//
+// printHtmlFooter --
+//
+
+void Tool_humsheet::printHtmlFooter(void) {
+	m_free_text << "</body>\n";
+	m_free_text << "</html>\n";
+}
+
+
+
+///////////////////////////////
+//
+// printRowClasses --
+//
+
+void Tool_humsheet::printRowClasses(HumdrumFile& infile, int row) {
+	string classes;
+	HLp hl = &infile[row];
+	if (hl->hasSpines()) {
+		classes += "spined ";
+	}
+	if (hl->isEmpty()) {
+		classes += "empty ";
+	}
+	if (hl->isData()) {
+		classes += "data ";
+	}
+	if (hl->isInterpretation()) {
+		classes += "interp ";
+		HTp token = hl->token(0);
+		if (token->compare(0, 2, "*>") == 0) {
+			classes += "label ";
+		}
+	}
+	if (hl->isLocalComment()) {
+		classes += "lcomment ";
+		if (isLayout(hl)) {
+			classes += "layout ";
+		}
+	}
+	HTp token = hl->token(0);
+	if (token->compare(0, 2, "!!") == 0) {
+		if ((token->size() == 2) || (token->at(3) != '!')) {
+			classes += "gcommet ";
+		}
+	}
+
+	if (hl->isUniversalReference()) {
+		if (token->compare(0, 11, "!!!!filter:") == 0) {
+			classes += "ufilter ";
+		} else if (token->compare(0, 12, "!!!!Xfilter:") == 0) {
+			classes += "usedufilter ";
+		} else {
+			classes += "ureference ";
+			if (token->compare(0, 12, "!!!!SEGMENT:") == 0) {
+				classes += "segment ";
+			}
+		}
+	} else if (hl->isCommentUniversal()) {
+		classes += "ucomment ";
+	} else if (hl->isReference()) {
+		classes += "reference ";
+	} else if (hl->isGlobalComment()) {
+		HTp token = hl->token(0);
+		if (token->compare(0, 10, "!!!filter:") == 0) {
+			classes += "filter ";
+		} else if (token->compare(0, 11, "!!!Xfilter:") == 0) {
+			classes += "usedfilter ";
+		} else {
+			classes += "gcomment ";
+			if (isLayout(hl)) {
+				classes += "layout ";
+			}
+		}
+	}
+
+	if (hl->isBarline()) {
+		classes += "barline ";
+	}
+	if (hl->isManipulator()) {
+		HTp token = hl->token(0);
+		if (token->compare(0, 2, "**") == 0) {
+			classes += "exinterp ";
+		} else {
+			classes += "manip ";
+		}
+	}
+	if (!classes.empty()) {
+      // remove space.
+		classes.resize((int)classes.size() - 1);
+		m_free_text << " class=\"" << classes << "\"";
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::isLayout -- check to see if any cell
+//    starts with "!LO:".
+//
+
+bool Tool_humsheet::isLayout(HLp line) {
+	if (line->hasSpines()) {
+		if (!line->isCommentLocal()) {
+			return false;
+		}
+		for (int i=0; i<line->getFieldCount(); i++) {
+			HTp token = line->token(i);
+			if (token->compare(0, 4, "!LO:") == 0) {
+				return true;
+			}
+		}
+	} else {
+		HTp token = line->token(0);
+		if (token->compare(0, 5, "!!LO:") == 0) {
+			return true;
+		}
+	}
+	return false;
+}
+
+
+
+///////////////////////////////
+//
+// Tool_humsheet::printRowContents --
+//
+
+void Tool_humsheet::printRowContents(HumdrumFile& infile, int row) {
+	int fieldcount = infile[row].getFieldCount();
+	for (int i=0; i<fieldcount; i++) {
+		HTp token = infile.token(row, i);
+		m_free_text << "<td";
+		if (m_idQ) {
+			printId(token);
+		}
+		printCellClasses(token);
+		printCellData(token);
+		if (m_tabindexQ) {
+			printTabIndex(token);
+		}
+		printColSpan(token);
+		if (!infile[row].isManipulator()) {
+			// do not allow manipulators to be edited
+			m_free_text << " contenteditable=\"true\"";
+		} else if (infile[row].isExclusive()) {
+			// but allow exclusive interpretation to be edited
+			m_free_text << " contenteditable=\"true\"";
+		}
+		m_free_text << ">";
+		printToken(token);
+		m_free_text << "</td>";
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::printCellData --
+//
+
+void Tool_humsheet::printCellData(HTp token) {
+	int field = token->getFieldIndex();
+	m_free_text << " data-field=\"" << field << "\"";
+
+
+	if (token->getOwner()->hasSpines()) {
+		int spine = token->getTrack() - 1;
+		m_free_text << " data-spine=\"" << spine << "\"";
+
+		int subspine = token->getSubtrack();
+		if (subspine > 0) {
+			m_free_text << " data-subspine=\"" << subspine << "\"";
+		}
+
+		string exinterp = token->getDataType().substr(2);
+		if (m_exinterpQ && !exinterp.empty()) {
+			m_free_text << " data-x=\"" << exinterp << "\"";
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::printToken --
+//
+
+void Tool_humsheet::printToken(HTp token) {
+	for (int i=0; i<(int)token->size(); i++) {
+		switch (token->at(i)) {
+			case '>':
+				m_free_text << "&gt;";
+				break;
+			case '<':
+				m_free_text << "&lt;";
+				break;
+			default:
+				m_free_text << token->at(i);
+		}
+	}
+}
+
+
+
+///////////////////////////////
+//
+// Tool_humsheet::printId --
+//
+
+void Tool_humsheet::printId(HTp token) {
+	int line = token->getLineNumber();
+	int field = token->getFieldNumber();
+	string id = "tok-L";
+	id += to_string(line);
+	id += "F";
+	id += to_string(field);
+	m_free_text << " id=\"" << id << "\"";
+}
+
+
+
+///////////////////////////////
+//
+// Tool_humsheet::printTabIndex --
+//
+
+void Tool_humsheet::printTabIndex(HTp token) {
+	string number = token->getValue("auto", "tabindex");
+	if (number.empty()) {
+		return;
+	}
+	m_free_text << " tabindex=\"" << number << "\"";
+}
+
+
+
+//////////////////////////////
+//
+// Tool_humsheet::printColspan -- print any necessary colspan values for
+//    token (to align by primary spines)
+//
+
+void Tool_humsheet::printColSpan(HTp token) {
+	if (!token->getOwner()->hasSpines()) {
+		m_free_text << " colspan=\"" << m_max_field << "\"";
+		return;
+	}
+	int track = token->getTrack() - 1;
+	int scount = m_max_subtrack.at(track);
+	int subtrack = token->getSubtrack();
+	if (subtrack > 1) {
+		subtrack--;
+	}
+	HTp nexttok = token->getNextFieldToken();
+	int ntrack = -1;
+	if (nexttok) {
+		ntrack = nexttok->getTrack() - 1;
+	}
+	if ((ntrack < 0) || (ntrack != track)) {
+		// at the end of a primary spine, so do a colspan with the remaining subtracks
+		if (subtrack < scount-1) {
+			int colspan = scount - subtrack;
+			m_free_text << " colspan=\"" << colspan << "\"";
+		}
+	} else {
+		// do nothing
+	}
+}
+
+
+
+///////////////////////////////
+//
+// printCellClasses --
 //
 
 void Tool_humsheet::printCellClasses(HTp token) {
@@ -95404,843 +98036,1597 @@ void Tool_mei2hum::processNodeStartLinks(string& output, xml_node node,
 //     duration of the note/rest/chord is calculated.
 //
 
-void Tool_mei2hum::processNodeStartLinks2(xml_node node,
-		vector<xml_node>& nodelist) {
-	for (int i=0; i<(int)nodelist.size(); i++) {
-		string nodename = nodelist[i].name();
-		if (nodename == "tupletSpan") {
-			parseTupletSpanStart(node, nodelist[i]);
+void Tool_mei2hum::processNodeStartLinks2(xml_node node,
+		vector<xml_node>& nodelist) {
+	for (int i=0; i<(int)nodelist.size(); i++) {
+		string nodename = nodelist[i].name();
+		if (nodename == "tupletSpan") {
+			parseTupletSpanStart(node, nodelist[i]);
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseTupletSpanStart --
+//     Such as:
+//          <tupletSpan staff="10" num="3" numbase="2" num.visible="true"
+//                num.place="below" num.format="count" startid="#4235235"
+//                endid="#532532"/>
+//
+
+void Tool_mei2hum::parseTupletSpanStart(xml_node node,
+		xml_node tupletSpan) {
+	NODE_VERIFY(tupletSpan, )
+
+	if (strcmp(tupletSpan.attribute("endid").value(), "") == 0) {
+		cerr << "Warning: <tupletSpan> requires endid attribute (at least ";
+		cerr << "for this parser)" << endl;
+		return;
+	}
+
+	if (strcmp(tupletSpan.attribute("startid").value(), "") == 0) {
+		cerr << "Warning: <tupletSpan> requires startid attribute (at least ";
+		cerr << "for this parser)" << endl;
+		return;
+	}
+
+	string num = tupletSpan.attribute("num").value();
+	string numbase = tupletSpan.attribute("numbase").value();
+
+	HumNum newfactor = 1;
+
+	if (numbase == "") {
+		cerr << "Warning: tuplet@numbase is empty" << endl;
+	} else {
+		newfactor = stoi(numbase);
+	}
+
+	if (num == "") {
+		cerr << "Warning: tuplet@num is empty" << endl;
+	} else {
+		newfactor /= stoi(num);
+	}
+
+	m_tupletfactor *= newfactor;
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseTupletSpanStop --
+//     Such as:
+//          <tupletSpan staff="10" num="3" numbase="2" num.visible="true"
+//                num.place="below" num.format="count" startid="#4235235"
+//                endid="#532532"/>
+//
+
+void Tool_mei2hum::parseTupletSpanStop(string& output, xml_node node,
+		xml_node tupletSpan) {
+	NODE_VERIFY(tupletSpan, )
+
+	if (strcmp(tupletSpan.attribute("endid").value(), "") == 0) {
+		return;
+	}
+	if (strcmp(tupletSpan.attribute("startid").value(), "") == 0) {
+		return;
+	}
+
+	string num = tupletSpan.attribute("num").value();
+	string numbase = tupletSpan.attribute("numbase").value();
+
+	HumNum newfactor = 1;
+
+	if (numbase == "") {
+		cerr << "Warning: tuplet@numbase is empty" << endl;
+	} else {
+		newfactor = stoi(numbase);
+	}
+
+	if (num == "") {
+		cerr << "Warning: tuplet@num is empty" << endl;
+	} else {
+		newfactor /= stoi(num);
+	}
+
+	// undo the tuplet factor:
+	m_tupletfactor /= newfactor;
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseArpeg -- Only handles single chord arpeggiation for now
+//    (ignores @endid).
+//
+
+void Tool_mei2hum::parseArpeg(string& output, xml_node node, xml_node arpeg) {
+	NODE_VERIFY(arpeg, )
+
+	if (strcmp(arpeg.attribute("endid").value(), "") != 0) {
+		cerr << "Warning: multi-note arpeggios are not yet handled in the converter." << endl;
+	}
+
+	string nodename = node.name();
+	if (nodename == "note") {
+		output += ':';
+	} else if (nodename == "chord") {
+		string temp = output;
+		output.clear();
+		for (int i=0; i<(int)temp.size(); i++) {
+			if (temp[i] == ' ') {
+				output += ": ";
+			} else {
+				output += temp[i];
+			}
+		}
+		output += ':';
+	} else {
+		cerr << DKHTP << "an arpeggio attached to a "
+		     << nodename << " element" << endl;
+		return;
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::processNodeStopLinks --
+//
+
+void Tool_mei2hum::processNodeStopLinks(string& output, xml_node node,
+		vector<xml_node>& nodelist) {
+	for (int i=0; i<(int)nodelist.size(); i++) {
+		string nodename = nodelist[i].name();
+		if (nodename == "slur") {
+			parseSlurStop(output, node, nodelist[i]);
+		} else if (nodename == "tie") {
+			parseTieStop(output, node, nodelist[i]);
+		} else if (nodename == "tupletSpan") {
+			parseTupletSpanStop(output, node, nodelist[i]);
+		} else {
+			cerr << DKHTP << nodename
+			     << " element in processNodeStopLinks()" << endl;
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseSlurStart --
+//
+
+void Tool_mei2hum::parseSlurStart(string& output, xml_node node, xml_node slur) {
+	NODE_VERIFY(slur, )
+	string nodename = node.name();
+	if (nodename == "note") {
+		output = "(" + setPlacement(slur.attribute("curvedir").value()) + output;
+	} else if (nodename == "chord") {
+		output = "(" + setPlacement(slur.attribute("curvedir").value()) + output;
+	} else {
+		cerr << DKHTP << "a slur start attached to a "
+		     << nodename << " element" << endl;
+		return;
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseSlurStop --
+//
+
+void Tool_mei2hum::parseSlurStop(string& output, xml_node node, xml_node slur) {
+	NODE_VERIFY(slur, )
+	string nodename = node.name();
+	if (nodename == "note") {
+		output += ")";
+	} else if (nodename == "chord") {
+		output += ")";
+	} else {
+		cerr << DKHTP << "a tie end attached to a "
+		     << nodename << " element" << endl;
+		return;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseTieStart -- Need to deal with chords later.
+//
+
+void Tool_mei2hum::parseTieStart(string& output, xml_node node, xml_node tie) {
+	NODE_VERIFY(tie, )
+
+	string id = node.attribute("xml:id").value();
+	if (!id.empty()) {
+		auto found = m_stoplinks.find(id);
+		if (found != m_stoplinks.end()) {
+			for (auto item : (*found).second) {
+				if (strcmp(tie.attribute("startid").value(), item.attribute("endid").value()) == 0) {
+					// deal with tie middles in parseTieStop().
+					return;
+				}
+			}
+		}
+	}
+
+	string nodename = node.name();
+	if (nodename == "note") {
+		output = "[" + output;
+	} else {
+		cerr << DKHTP << "a tie start attached to a "
+		     << nodename << " element" << endl;
+		return;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseTrill --
+//
+
+void Tool_mei2hum::parseTrill(string& output, xml_node node, xml_node trill) {
+	NODE_VERIFY(trill, )
+
+	auto loc = output.find(";");
+	if (loc != string::npos) {
+		output.insert(loc, "T");
+		return;
+	}
+
+	loc = output.find(")");
+	if (loc != string::npos) {
+		output.insert(loc, "T");
+		return;
+	}
+
+	output += "T";
+
+	// Deal with endid attribute on trills later.
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseTieStop -- Need to deal with chords later.
+//
+
+void Tool_mei2hum::parseTieStop(string& output, xml_node node, xml_node tie) {
+	NODE_VERIFY(tie, )
+
+	string id = node.attribute("xml:id").value();
+	if (!id.empty()) {
+		auto found = m_startlinks.find(id);
+		if (found != m_startlinks.end()) {
+			for (auto item : (*found).second) {
+				if (strcmp(tie.attribute("endid").value(), item.attribute("startid").value()) == 0) {
+					output += "_";
+					return;
+				}
+			}
+		}
+	}
+
+	string nodename = node.name();
+	if (nodename == "note") {
+		output += "]";
+	} else {
+		cerr << DKHTP << "a tie end attached to a "
+		     << nodename << " element" << endl;
+		return;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::parseFermata -- deal with a fermata attached to something.
+///     output is a Humdrum token string (maybe have it as a HumdrumToken object).
+//
+
+void Tool_mei2hum::parseFermata(string& output, xml_node node, xml_node fermata) {
+	NODE_VERIFY(fermata, )
+
+	string nodename = node.name();
+	if (nodename == "note") {
+		output += ';';
+	} else if (nodename == "chord") {
+		output += ';';
+	} else if (nodename == "rest") {
+		output += ';';
+	} else {
+		cerr << DKHTP << "a fermata attached to a "
+		     << nodename << " element" << endl;
+		return;
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::getHumdrumRecip --
+//
+
+string Tool_mei2hum::getHumdrumRecip(HumNum duration, int dotcount) {
+	string output;
+
+	if (dotcount > 0) {
+		// remove dots from duration
+		int top = (1 << (dotcount+1)) - 1;
+		int bot = 1 << dotcount;
+		HumNum dotfactor(bot, top);
+		duration *= dotfactor;
+	}
+
+	if (duration.getNumerator() == 1) {
+		output = to_string(duration.getDenominator());
+	} else if ((duration.getNumerator() == 2) && (duration.getDenominator() == 1)) {
+		// breve symbol:
+		output = "0";
+	} else if ((duration.getNumerator() == 4) && (duration.getDenominator() == 1)) {
+		// long symbol:
+		output = "00";
+	} else if ((duration.getNumerator() == 8) && (duration.getDenominator() == 1)) {
+		// maxima symbol:
+		output = "000";
+	} else {
+		output = to_string(duration.getDenominator());
+		output += "%";
+		output += to_string(duration.getNumerator());
+	}
+
+	for (int i=0; i<dotcount; i++) {
+		output += '.';
+	}
+
+	return output;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::getChildAccidVis -- Return accid@accid from any element
+//   in the list, if it is not editorial or cautionary.
+//
+
+string Tool_mei2hum::getChildAccidVis(vector<xml_node>& children) {
+	for (int i=0; i<(int)children.size(); i++) {
+		string nodename = children[i].name();
+		if (nodename != "accid") {
+			continue;
+		}
+		string func = children[i].attribute("func").value();
+		if (func == "caution") {
+			// cautionary accidental handled elsewhere
+			return "";
+		} else if (func == "edit") {
+			// editorial accidental handled elsewhere
+			return "";
+		}
+		string accid = children[i].attribute("accid").value();
+		return accid;
+	}
+	return "";
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::getChildAccidGes -- Return the accid@accid.ges value
+//    of any element in the input list, but not if the accidental is
+//    part of an cautionary or editorial accidental.
+//
+
+string Tool_mei2hum::getChildAccidGes(vector<xml_node>& children) {
+	for (int i=0; i<(int)children.size(); i++) {
+		string nodename = children[i].name();
+		if (nodename != "accid") {
+			continue;
+		}
+		string func = children[i].attribute("func").value();
+		if (func == "caution") {
+			// cautionary accidental handled elsewhere
+			return "";
+		} else if (func == "edit") {
+			// editorial accidental handled elsewhere
+			return "";
+		}
+		string accidges = children[i].attribute("accid.ges").value();
+		return accidges;
+	}
+	return "";
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::getHumdrumPitch --
+//
+
+string Tool_mei2hum::getHumdrumPitch(xml_node note, vector<xml_node>& children) {
+	string pname = note.attribute("pname").value();
+	string accidvis = note.attribute("accid").value();
+	string accidges = note.attribute("accid.ges").value();
+
+	string accidvischild = getChildAccidVis(children);
+	string accidgeschild = getChildAccidGes(children);
+
+	int octnum = 4;
+	string oct = note.attribute("oct").value();
+	if (oct == "") {
+		cerr << "Empty octave" << endl;
+	} else if (isdigit(oct[0])) {
+		octnum = stoi(oct);
+	} else {
+		cerr << "Unknown octave value: " << oct << endl;
+	}
+
+	if (pname == "") {
+		cerr << "Empty pname" << endl;
+		return "x";
+	}
+
+	string output;
+	if (octnum < 4) {
+		char val = toupper(pname[0]);
+		int count = 4 - octnum;
+		for (int i=0; i<count; i++) {
+			output += val;
+		}
+	} else {
+		char val = pname[0];
+		int count = octnum - 3;
+		for (int i=0; i<count; i++) {
+			output += val;
+		}
+	}
+
+	if (accidges != "") {
+		string acc = accidToKern(accidges);
+		if (acc != "n") {
+			output += acc;
+			// accidental is not visible
+			output += "y";
+		}
+	} else if (accidvis != "") {
+		string acc = accidToKern(accidvis);
+		output += acc;
+	} else if (accidvischild != "") {
+		string acc = accidToKern(accidvischild);
+		output += acc;
+	} else if (accidgeschild != "") {
+		string acc = accidToKern(accidgeschild);
+		if (acc != "n") {
+			output += acc;
+			// accidental is not visible
+			output += "y";
+		}
+	}
+
+	// Transpose to C score if part is transposing:
+	if (m_currentStaff) {
+		if (m_scoreDef.staves[m_currentStaff-1].base40 != 0) {
+			int base40 = Convert::kernToBase40(output);
+			base40 += m_scoreDef.staves[m_currentStaff-1].base40;
+			output = Convert::base40ToKern(base40);
 		}
 	}
+
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseTupletSpanStart --
-//     Such as:
-//          <tupletSpan staff="10" num="3" numbase="2" num.visible="true"
-//                num.place="below" num.format="count" startid="#4235235"
-//                endid="#532532"/>
+// Tool_mei2hum::getDuration -- Get duration from note or chord.  If chord does not
+//    have @dur then use @dur of first note in children elements.
 //
 
-void Tool_mei2hum::parseTupletSpanStart(xml_node node,
-		xml_node tupletSpan) {
-	NODE_VERIFY(tupletSpan, )
-
-	if (strcmp(tupletSpan.attribute("endid").value(), "") == 0) {
-		cerr << "Warning: <tupletSpan> requires endid attribute (at least ";
-		cerr << "for this parser)" << endl;
-		return;
+HumNum Tool_mei2hum::getDuration(xml_node element) {
+	xml_attribute dur_attr = element.attribute("dur");
+	string name = element.name();
+	if ((!dur_attr) && (name == "note")) {
+		// real notes must have durations, but this one
+		// does not, so assign zero duration
+		return 0;
+	}
+	if ((!dur_attr) && (name == "chord")) {
+		// if there is no dur attribute on a chord, then look for it
+		// on the first note subelement of the chord.
+		auto newelement = element.select_node(".//note").node();
+		if (newelement) {
+			element = newelement;
+			dur_attr = element.attribute("dur");
+			name = element.name();
+		} else {
+			return 0;
+		}
 	}
 
-	if (strcmp(tupletSpan.attribute("startid").value(), "") == 0) {
-		cerr << "Warning: <tupletSpan> requires startid attribute (at least ";
-		cerr << "for this parser)" << endl;
-		return;
+	string dur = dur_attr.value();
+	if (dur == "") {
+		return 0;
 	}
 
-	string num = tupletSpan.attribute("num").value();
-	string numbase = tupletSpan.attribute("numbase").value();
+	HumNum output;
+	if (dur == "breve") {
+		output = 2;
+	} else if (dur == "long") {
+		output = 4;
+	} else if (dur == "maxima") {
+		output = 8;
+	} else if (isdigit(dur[0])) {
+		output = 1;
+		output /= stoi(dur);
+	} else {
+		cerr << "Unknown " << element.name() << "@dur: " << dur << endl;
+		return 0;
+	}
 
-	HumNum newfactor = 1;
+	if (output == 0) {
+		cerr << "Error: zero duration for note" << endl;
+	}
 
-	if (numbase == "") {
-		cerr << "Warning: tuplet@numbase is empty" << endl;
+	int dotcount;
+	string dots = element.attribute("dots").value();
+	if (dots == "") {
+		dotcount = 0;
+	} else if (isdigit(dots[0])) {
+		dotcount = stoi(dots);
 	} else {
-		newfactor = stoi(numbase);
+		cerr << "Unknown " << element.name() << "@dotcount: " << dur << endl;
+		return 0;
 	}
 
-	if (num == "") {
-		cerr << "Warning: tuplet@num is empty" << endl;
-	} else {
-		newfactor /= stoi(num);
+	if (dotcount > 0) {
+		int top = (1 << (dotcount+1)) - 1;
+		int bot = 1 << dotcount;
+		HumNum dotfactor(top, bot);
+		output *= dotfactor;
 	}
 
-	m_tupletfactor *= newfactor;
+	// add a correction for the tuplet factor which is currently active.
+	if (m_tupletfactor != 1) {
+		output *= m_tupletfactor;
+	}
 
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseTupletSpanStop --
-//     Such as:
-//          <tupletSpan staff="10" num="3" numbase="2" num.visible="true"
-//                num.place="below" num.format="count" startid="#4235235"
-//                endid="#532532"/>
+// Tool_mei2hum::getDuration_mensural -- Get duration from note or chord.  If chord does not
+//    have @dur then use @dur of first note in children elements.
+//
+// @dur: https://music-encoding.org/guidelines/v4/data-types/data.duration.mensural.html
+//          X = maxima
+//          L = longa
+//          S = brevis
+//          s = semibrevis
+//          M = minima
+//          m = semiminima
+//          U = fusa
+//          u = semifusa
+// @dur.quality:
+//          i = imperfecta  :: remove augmentation dot
+//          p = perfecta    :: add augmentation dot
+//          altera = altera :: duration is double the rhythmic value of notes
 //
 
-void Tool_mei2hum::parseTupletSpanStop(string& output, xml_node node,
-		xml_node tupletSpan) {
-	NODE_VERIFY(tupletSpan, )
+HumNum Tool_mei2hum::getDuration_mensural(xml_node element, int& dotcount) {
+	dotcount = 0;
 
-	if (strcmp(tupletSpan.attribute("endid").value(), "") == 0) {
-		return;
+	xml_attribute dur_qual = element.attribute("dur.quality");
+	xml_attribute dur_attr = element.attribute("dur");
+	string name = element.name();
+
+	if ((!dur_attr) && (name == "note")) {
+		// real notes must have durations, but this one
+		// does not, so assign zero duration
+		return 0;
 	}
-	if (strcmp(tupletSpan.attribute("startid").value(), "") == 0) {
-		return;
+	if ((!dur_attr) && (name == "chord")) {
+		// if there is no dur attribute on a chord, then look for it
+		// on the first note subelement of the chord.
+		auto newelement = element.select_node(".//note").node();
+		if (newelement) {
+			element = newelement;
+			dur_attr = element.attribute("dur");
+			name = element.name();
+			dur_qual = element.attribute("dur.quality");
+		} else {
+			return 0;
+		}
 	}
 
-	string num = tupletSpan.attribute("num").value();
-	string numbase = tupletSpan.attribute("numbase").value();
-
-	HumNum newfactor = 1;
-
-	if (numbase == "") {
-		cerr << "Warning: tuplet@numbase is empty" << endl;
-	} else {
-		newfactor = stoi(numbase);
+	string dur = dur_attr.value();
+	if (dur == "") {
+		return 0;
 	}
+	string durquality = dur_qual.value();
 
-	if (num == "") {
-		cerr << "Warning: tuplet@num is empty" << endl;
+	char rhythm = '\0';
+	if (dur == "maxima") {
+		rhythm = 'X';
+	} else if (dur == "longa") {
+		rhythm = 'L';
+	} else if (dur == "brevis") {
+		rhythm = 'S';
+	} else if (dur == "semibrevis") {
+		rhythm = 's';
+	} else if (dur == "minima") {
+		rhythm = 'M';
+	} else if (dur == "semiminima") {
+		rhythm = 'm';
+	} else if (dur == "fusa") {
+		rhythm = 'U';
+	} else if (dur == "semifusa") {
+		rhythm = 'u';
 	} else {
-		newfactor /= stoi(num);
+		cerr << "Error: unknown rhythm" << element.name() << "@dur: " << dur << endl;
+		return 0;
 	}
 
-	// undo the tuplet factor:
-	m_tupletfactor /= newfactor;
+	mei_staffDef& ss = m_scoreDef.staves.at(m_currentStaff - 1);
+	int maximodus = ss.maximodus == 3 ? 3 : 2;
+	int modus     = ss.modus     == 3 ? 3 : 2;
+	int tempus    = ss.tempus    == 3 ? 3 : 2;
+	int prolatio  = ss.prolatio  == 3 ? 3 : 2;
+
+	bool altera     = false;
+	bool perfecta   = false;
+	bool imperfecta = false;
+
+	if (durquality == "imperfecta") {
+		imperfecta = true;
+	} else if (durquality == "perfecta") {
+		perfecta = true;
+	} else if (durquality == "altera") {
+		altera = true;
+	}
 
+	HumNum output = Convert::mensToDuration(rhythm, altera, perfecta, imperfecta, maximodus, modus, tempus, prolatio);
+	return output;
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_mei2hum::parseArpeg -- Only handles single chord arpeggiation for now
-//    (ignores @endid).
+// Tool_mei2hum::parseVerse --
 //
 
-void Tool_mei2hum::parseArpeg(string& output, xml_node node, xml_node arpeg) {
-	NODE_VERIFY(arpeg, )
+void Tool_mei2hum::parseVerse(xml_node verse, GridStaff* staff) {
+	NODE_VERIFY(verse, )
+	MAKE_CHILD_LIST(children, verse);
 
-	if (strcmp(arpeg.attribute("endid").value(), "") != 0) {
-		cerr << "Warning: multi-note arpeggios are not yet handled in the converter." << endl;
+	string n = verse.attribute("n").value();
+	int nnum = 1;
+	if (n.empty()) {
+		cerr << "Warning: no layer number on layer element" << endl;
+	} else {
+		nnum = stoi(n);
+	}
+	if (nnum < 1) {
+		cerr << "Warning: invalid layer number: " << nnum << endl;
+		cerr << "Setting it to 1." << endl;
+		nnum = 1;
 	}
 
-	string nodename = node.name();
-	if (nodename == "note") {
-		output += ':';
-	} else if (nodename == "chord") {
-		string temp = output;
-		output.clear();
-		for (int i=0; i<(int)temp.size(); i++) {
-			if (temp[i] == ' ') {
-				output += ": ";
-			} else {
-				output += temp[i];
+	string versetext;
+	int sylcount = 0;
+
+	for (int i=0; i<(int)children.size(); i++) {
+		string nodename = children[i].name();
+		if (nodename == "syl") {
+			if (sylcount > 0) {
+				versetext += " ";
 			}
+			sylcount++;
+			versetext += parseSyl(children[i]);
+		} else {
+			cerr << DKHTP << verse.name() << "/" << nodename << CURRLOC << endl;
 		}
-		output += ':';
-	} else {
-		cerr << DKHTP << "an arpeggio attached to a "
-		     << nodename << " element" << endl;
+	}
+
+	if (versetext == "") {
+		// nothing to store
 		return;
 	}
 
+	staff->setVerse(nnum-1, versetext);
+	reportVerseNumber(nnum, m_currentStaff-1);
+
+	return;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::processNodeStopLinks --
+// Tool_mei2hum::parseBareSyl -- Only one syl allows as a bar child of note element.
+//     This function is used to process syl elements that are not wrapped in a verse element.
 //
 
-void Tool_mei2hum::processNodeStopLinks(string& output, xml_node node,
-		vector<xml_node>& nodelist) {
-	for (int i=0; i<(int)nodelist.size(); i++) {
-		string nodename = nodelist[i].name();
-		if (nodename == "slur") {
-			parseSlurStop(output, node, nodelist[i]);
-		} else if (nodename == "tie") {
-			parseTieStop(output, node, nodelist[i]);
-		} else if (nodename == "tupletSpan") {
-			parseTupletSpanStop(output, node, nodelist[i]);
-		} else {
-			cerr << DKHTP << nodename
-			     << " element in processNodeStopLinks()" << endl;
-		}
-	}
-}
+void Tool_mei2hum::parseBareSyl(xml_node syl, GridStaff* staff) {
+	NODE_VERIFY(syl, )
 
+	int nnum = 1;
+	xml_attribute n_attr = syl.attribute("n");
+	if (n_attr) {
+		nnum = n_attr.as_int();
+	}
 
+	if (nnum < 1) {
+		cerr << "Warning: invalid layer number: " << nnum << endl;
+		cerr << "Setting it to 1." << endl;
+		nnum = 1;
+	}
 
-//////////////////////////////
-//
-// Tool_mei2hum::parseSlurStart --
-//
+	string versetext = parseSyl(syl);
 
-void Tool_mei2hum::parseSlurStart(string& output, xml_node node, xml_node slur) {
-	NODE_VERIFY(slur, )
-	string nodename = node.name();
-	if (nodename == "note") {
-		output = "(" + setPlacement(slur.attribute("curvedir").value()) + output;
-	} else if (nodename == "chord") {
-		output = "(" + setPlacement(slur.attribute("curvedir").value()) + output;
-	} else {
-		cerr << DKHTP << "a slur start attached to a "
-		     << nodename << " element" << endl;
+	if (versetext == "") {
+		// nothing to store
 		return;
 	}
 
+	staff->setVerse(nnum-1, versetext);
+	reportVerseNumber(nnum, m_currentStaff-1);
+
+	return;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseSlurStop --
+// Tool_mei2hum::reportVerseNumber --
 //
 
-void Tool_mei2hum::parseSlurStop(string& output, xml_node node, xml_node slur) {
-	NODE_VERIFY(slur, )
-	string nodename = node.name();
-	if (nodename == "note") {
-		output += ")";
-	} else if (nodename == "chord") {
-		output += ")";
-	} else {
-		cerr << DKHTP << "a tie end attached to a "
-		     << nodename << " element" << endl;
+void Tool_mei2hum::reportVerseNumber(int pmax, int staffindex) {
+	if (staffindex < 0) {
+		return;
+	}
+	if (staffindex >= (int)m_maxverse.size()) {
 		return;
 	}
+	if (m_maxverse.at(staffindex) < pmax) {
+		m_maxverse[staffindex] = pmax;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseTieStart -- Need to deal with chords later.
+// Tool_mei2hum::parseSyl --
 //
 
-void Tool_mei2hum::parseTieStart(string& output, xml_node node, xml_node tie) {
-	NODE_VERIFY(tie, )
+string Tool_mei2hum::parseSyl(xml_node syl) {
+	NODE_VERIFY(syl, "")
+	MAKE_CHILD_LIST(children, syl);
 
-	string id = node.attribute("xml:id").value();
-	if (!id.empty()) {
-		auto found = m_stoplinks.find(id);
-		if (found != m_stoplinks.end()) {
-			for (auto item : (*found).second) {
-				if (strcmp(tie.attribute("startid").value(), item.attribute("endid").value()) == 0) {
-					// deal with tie middles in parseTieStop().
-					return;
-				}
-			}
+	string text = syl.child_value();
+	for (int i=0; i<(int)text.size(); i++) {
+		if (text[i] == '_') {
+			text[i] = ' ';
 		}
 	}
 
-	string nodename = node.name();
-	if (nodename == "note") {
-		output = "[" + output;
-	} else {
-		cerr << DKHTP << "a tie start attached to a "
-		     << nodename << " element" << endl;
-		return;
+	string wordpos = syl.attribute("wordpos").value();
+	if (wordpos == "i") {
+		text = text + "-";
+	} else if (wordpos == "m") {
+		text = "-" + text + "-";
+	} else if (wordpos == "t") {
+		text = "-" + text;
 	}
+
+	return text;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseTrill --
+// Tool_mei2hum::parseClef --
+//
 //
 
-void Tool_mei2hum::parseTrill(string& output, xml_node node, xml_node trill) {
-	NODE_VERIFY(trill, )
+void Tool_mei2hum::parseClef(xml_node clef, HumNum starttime) {
+	NODE_VERIFY(clef, )
 
-	auto loc = output.find(";");
-	if (loc != string::npos) {
-		output.insert(loc, "T");
-		return;
-	}
+	string shape = clef.attribute("shape").value();
+	string line = clef.attribute("line").value();
+	string clefdis = clef.attribute("clef.dis").value();
+	string clefdisplace = clef.attribute("clef.dis.place").value();
 
-	loc = output.find(")");
-	if (loc != string::npos) {
-		output.insert(loc, "T");
-		return;
-	}
+	string tok = makeHumdrumClef(shape, line, clefdis, clefdisplace);
 
-	output += "T";
+	m_outdata.back()->addClefToken(tok, starttime QUARTER_CONVERT,
+			m_currentStaff-1, 0, 0, m_staffcount);
 
-	// Deal with endid attribute on trills later.
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseTieStop -- Need to deal with chords later.
+// Tool_mei2hum::makeHumdrumClef --
+//
+// Example:
+//     <clef shape="G" line="2" clef.dis="8" clef.dis.place="below" />
 //
 
-void Tool_mei2hum::parseTieStop(string& output, xml_node node, xml_node tie) {
-	NODE_VERIFY(tie, )
-
-	string id = node.attribute("xml:id").value();
-	if (!id.empty()) {
-		auto found = m_startlinks.find(id);
-		if (found != m_startlinks.end()) {
-			for (auto item : (*found).second) {
-				if (strcmp(tie.attribute("endid").value(), item.attribute("startid").value()) == 0) {
-					output += "_";
-					return;
-				}
-			}
+string Tool_mei2hum::makeHumdrumClef(const string& shape,
+		const string& line, const string& clefdis, const string& clefdisplace) {
+	string output = "*clef" + shape;
+	if (!clefdis.empty()) {
+		int number = stoi(clefdis);
+		int count = 0;
+		if (number == 8) {
+			count = 1;
+		} else if (number == 15) {
+			count = 2;
+		}
+		if (clefdisplace != "above") {
+			count = -count;
+		}
+		switch (count) {
+			case 1: output += "^"; break;
+			case 2: output += "^^"; break;
+			case -1: output += "v"; break;
+			case -2: output += "vv"; break;
 		}
 	}
-
-	string nodename = node.name();
-	if (nodename == "note") {
-		output += "]";
-	} else {
-		cerr << DKHTP << "a tie end attached to a "
-		     << nodename << " element" << endl;
-		return;
-	}
+	output += line;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseFermata -- deal with a fermata attached to something.
-///     output is a Humdrum token string (maybe have it as a HumdrumToken object).
+// Tool_mei2hum::parseChord --
 //
 
-void Tool_mei2hum::parseFermata(string& output, xml_node node, xml_node fermata) {
-	NODE_VERIFY(fermata, )
-
-	string nodename = node.name();
-	if (nodename == "note") {
-		output += ';';
-	} else if (nodename == "chord") {
-		output += ';';
-	} else if (nodename == "rest") {
-		output += ';';
-	} else {
-		cerr << DKHTP << "a fermata attached to a "
-		     << nodename << " element" << endl;
-		return;
-	}
-
-}
-
-
-
-//////////////////////////////
-//
-// Tool_mei2hum::getHumdrumRecip --
-//
+HumNum Tool_mei2hum::parseChord(xml_node chord, HumNum starttime, int gracenumber) {
+	NODE_VERIFY(chord, starttime)
+	MAKE_CHILD_LIST(children, chord);
 
-string Tool_mei2hum::getHumdrumRecip(HumNum duration, int dotcount) {
-	string output;
+	processPreliminaryLinkedNodes(chord);
 
-	if (dotcount > 0) {
-		// remove dots from duration
-		int top = (1 << (dotcount+1)) - 1;
-		int bot = 1 << dotcount;
-		HumNum dotfactor(bot, top);
-		duration *= dotfactor;
-	}
+	HumNum duration = getDuration(chord);
 
-	if (duration.getNumerator() == 1) {
-		output = to_string(duration.getDenominator());
-	} else if ((duration.getNumerator() == 2) && (duration.getDenominator() == 1)) {
-		// breve symbol:
-		output = "0";
-	} else if ((duration.getNumerator() == 4) && (duration.getDenominator() == 1)) {
-		// long symbol:
-		output = "00";
-	} else if ((duration.getNumerator() == 8) && (duration.getDenominator() == 1)) {
-		// maxima symbol:
-		output = "000";
-	} else {
-		output = to_string(duration.getDenominator());
-		output += "%";
-		output += to_string(duration.getNumerator());
+	string tok;
+	int counter = 0;
+	for (int i=0; i<(int)children.size(); i++) {
+		string nodename = children[i].name();
+		if (nodename == "note") {
+			counter++;
+			if (counter > 1) {
+				tok += " ";
+			}
+			parseNote(children[i], chord, tok, starttime, gracenumber);
+		} else if (nodename == "artic") {
+			// This is handled within parseNote();
+		} else {
+			cerr << DKHTP << chord.name() << "/" << nodename << CURRLOC << endl;
+		}
 	}
 
-	for (int i=0; i<dotcount; i++) {
-		output += '.';
+	m_fermata = false;
+	processLinkedNodes(tok, chord);
+	if (!m_fermata) {
+		processFermataAttribute(tok, chord);
 	}
 
-	return output;
+	m_outdata.back()->addDataToken(tok, starttime QUARTER_CONVERT, m_currentStaff-1,
+		0, m_currentLayer-1, m_staffcount);
+
+	return starttime + duration;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::getChildAccidVis -- Return accid@accid from any element
-//   in the list, if it is not editorial or cautionary.
+// Tool_mei2hum::getChildrenVector -- Return a list of all children elements
+//   of a given element.  Pugixml does not allow random access, but storing
+//   them in a vector allows that possibility.
 //
 
-string Tool_mei2hum::getChildAccidVis(vector<xml_node>& children) {
-	for (int i=0; i<(int)children.size(); i++) {
-		string nodename = children[i].name();
-		if (nodename != "accid") {
-			continue;
-		}
-		string func = children[i].attribute("func").value();
-		if (func == "caution") {
-			// cautionary accidental handled elsewhere
-			return "";
-		} else if (func == "edit") {
-			// editorial accidental handled elsewhere
-			return "";
-		}
-		string accid = children[i].attribute("accid").value();
-		return accid;
+void Tool_mei2hum::getChildrenVector(vector<xml_node>& children,
+		xml_node parent) {
+	children.clear();
+	for (xml_node child : parent.children()) {
+		children.push_back(child);
 	}
-	return "";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::getChildAccidGes -- Return the accid@accid.ges value
-//    of any element in the input list, but not if the accidental is
-//    part of an cautionary or editorial accidental.
+// Tool_mei2hum::initialize -- Setup for the tool, mostly parsing command-line
+//   (input) options.
 //
 
-string Tool_mei2hum::getChildAccidGes(vector<xml_node>& children) {
-	for (int i=0; i<(int)children.size(); i++) {
-		string nodename = children[i].name();
-		if (nodename != "accid") {
-			continue;
-		}
-		string func = children[i].attribute("func").value();
-		if (func == "caution") {
-			// cautionary accidental handled elsewhere
-			return "";
-		} else if (func == "edit") {
-			// editorial accidental handled elsewhere
-			return "";
-		}
-		string accidges = children[i].attribute("accid.ges").value();
-		return accidges;
-	}
-	return "";
+void Tool_mei2hum::initialize(void) {
+	m_recipQ   =  getBoolean("recip");
+	m_stemsQ   =  getBoolean("stems");
+	m_xmlidQ   =  getBoolean("xmlids");
+	m_xmlidQ   = 1;  // for testing
+	m_appLabel =  getString("app-label");
+	m_placeQ   = !getBoolean("no-place");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::getHumdrumPitch --
+// Tool_mei2hum::buildIdLinkMap -- Build table of startid and endid links between elements.
+//
+// Reference: https://pugixml.org/docs/samples/traverse_walker.cpp
 //
 
-string Tool_mei2hum::getHumdrumPitch(xml_node note, vector<xml_node>& children) {
-	string pname = note.attribute("pname").value();
-	string accidvis = note.attribute("accid").value();
-	string accidges = note.attribute("accid.ges").value();
-
-	string accidvischild = getChildAccidVis(children);
-	string accidgeschild = getChildAccidGes(children);
+void Tool_mei2hum::buildIdLinkMap(xml_document& doc) {
+	class linkmap_walker : public pugi::xml_tree_walker {
+		public:
+			virtual bool for_each(pugi::xml_node& node) {
+				xml_attribute startid = node.attribute("startid");
+				xml_attribute endid = node.attribute("endid");
+				if (startid) {
 
-	int octnum = 4;
-	string oct = note.attribute("oct").value();
-	if (oct == "") {
-		cerr << "Empty octave" << endl;
-	} else if (isdigit(oct[0])) {
-		octnum = stoi(oct);
-	} else {
-		cerr << "Unknown octave value: " << oct << endl;
-	}
+					string value = startid.value();
+					if (!value.empty()) {
+						if (value[0] == '#') {
+							value = value.substr(1, string::npos);
+						}
+					}
+					if (!value.empty()) {
+						(*startlinks)[value].push_back(node);
+					}
 
-	if (pname == "") {
-		cerr << "Empty pname" << endl;
-		return "x";
-	}
+				}
+				if (endid) {
 
-	string output;
-	if (octnum < 4) {
-		char val = toupper(pname[0]);
-		int count = 4 - octnum;
-		for (int i=0; i<count; i++) {
-			output += val;
-		}
-	} else {
-		char val = pname[0];
-		int count = octnum - 3;
-		for (int i=0; i<count; i++) {
-			output += val;
-		}
-	}
+					string value = endid.value();
+					if (!value.empty()) {
+						if (value[0] == '#') {
+							value = value.substr(1, string::npos);
+						}
+					}
+					if (!value.empty()) {
+						(*stoplinks)[value].push_back(node);
+					}
 
-	if (accidges != "") {
-		string acc = accidToKern(accidges);
-		if (acc != "n") {
-			output += acc;
-			// accidental is not visible
-			output += "y";
-		}
-	} else if (accidvis != "") {
-		string acc = accidToKern(accidvis);
-		output += acc;
-	} else if (accidvischild != "") {
-		string acc = accidToKern(accidvischild);
-		output += acc;
-	} else if (accidgeschild != "") {
-		string acc = accidToKern(accidgeschild);
-		if (acc != "n") {
-			output += acc;
-			// accidental is not visible
-			output += "y";
-		}
-	}
+				}
+				return true; // continue traversal
+			}
 
-	// Transpose to C score if part is transposing:
-	if (m_currentStaff) {
-		if (m_scoreDef.staves[m_currentStaff-1].base40 != 0) {
-			int base40 = Convert::kernToBase40(output);
-			base40 += m_scoreDef.staves[m_currentStaff-1].base40;
-			output = Convert::base40ToKern(base40);
-		}
-	}
+			map<string, vector<xml_node>>* startlinks = NULL;
+			map<string, vector<xml_node>>* stoplinks = NULL;
+	};
 
-	return output;
+	m_startlinks.clear();
+	m_stoplinks.clear();
+	linkmap_walker walker;
+	walker.startlinks = &m_startlinks;
+	walker.stoplinks = &m_stoplinks;
+	doc.traverse(walker);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::getDuration -- Get duration from note or chord.  If chord does not
-//    have @dur then use @dur of first note in children elements.
+// Tool_mei2hum::parseDir -- Meter cannot change in middle of measure.
+//     Need to implement @startid version.
+//
+// Example:
+//    <dir xml:id="dir-L408F3" place="below" staff="1" tstamp="2.0">con espressione</dir>
+//
+// or with normal font specified:
+//    <dir xml:id="dir-L7F1" staff="1" tstamp="2.000000">
+//       <rend xml:id="rend-0000001696821523" fontstyle="normal">test</rend>
+//    </dir>
+//
+// bold font:
+//   <dir xml:id="dir-L25F3" place="above" staff="1" tstamp="3.000000">
+//      <rend xml:id="rend-0000001714819172" fontstyle="normal" fontweight="bold">comment</rend>
+//   </dir>
 //
 
-HumNum Tool_mei2hum::getDuration(xml_node element) {
-	xml_attribute dur_attr = element.attribute("dur");
-	string name = element.name();
-	if ((!dur_attr) && (name == "note")) {
-		// real notes must have durations, but this one
-		// does not, so assign zero duration
-		return 0;
+void Tool_mei2hum::parseDir(xml_node dir, HumNum starttime) {
+	NODE_VERIFY(dir, )
+	MAKE_CHILD_LIST(children, dir);
+
+	string font = "i";  // italic by default in verovio
+
+	string placement = ""; // a = above, b = below
+
+	string place = dir.attribute("place").value();
+	if (place == "above") {
+		placement = "a:";
 	}
-	if ((!dur_attr) && (name == "chord")) {
-		// if there is no dur attribute on a chord, then look for it
-		// on the first note subelement of the chord.
-		auto newelement = element.select_node(".//note").node();
-		if (newelement) {
-			element = newelement;
-			dur_attr = element.attribute("dur");
-			name = element.name();
-		} else {
-			return 0;
+	// Below is the default in Humdrum layout commands.
+
+	string text;
+
+	if (!children.empty()) { // also includes the above text node, but only looking at <rend>.
+		int count = 0;
+		for (int i=0; i<(int)children.size(); i++) {
+			string nodename = children[i].name();
+			if (nodename == "rend") {
+				if (count) {
+					text += " ";
+				}
+				count++;
+				text += children[i].child_value();
+				if (strcmp(children[i].attribute("fontstyle").value(), "normal") == 0) {
+					font = "";  // normal is default in Humdrum layout
+				}
+				if (strcmp(children[i].attribute("fontweight").value(), "bold") == 0) {
+					font += "B";  // normal is default in Humdrum layout
+				}
+			} else if (nodename == "") {
+				// text node
+				if (count) {
+					text += " ";
+				}
+				count++;
+				text += children[i].value();
+			} else {
+				cerr << DKHTP << dir.name() << "/" << nodename << CURRLOC << endl;
+			}
 		}
 	}
 
-	string dur = dur_attr.value();
-	if (dur == "") {
-		return 0;
+	if (text.empty()) {
+		return;
 	}
 
-	HumNum output;
-	if (dur == "breve") {
-		output = 2;
-	} else if (dur == "long") {
-		output = 4;
-	} else if (dur == "maxima") {
-		output = 8;
-	} else if (isdigit(dur[0])) {
-		output = 1;
-		output /= stoi(dur);
-	} else {
-		cerr << "Unknown " << element.name() << "@dur: " << dur << endl;
-		return 0;
+	string message = "!LO:TX:";
+	message += placement;
+	if (!font.empty()) {
+		message += font + ":";
 	}
+	message += "t=" + cleanDirText(text);
 
-	if (output == 0) {
-		cerr << "Error: zero duration for note" << endl;
+	string ts = dir.attribute("tstamp").value();
+	if (ts.empty()) {
+		cerr << "Error: no timestamp on dir element and can't currently processes with @startid." << endl;
+		return;
 	}
 
-	int dotcount;
-	string dots = element.attribute("dots").value();
-	if (dots == "") {
-		dotcount = 0;
-	} else if (isdigit(dots[0])) {
-		dotcount = stoi(dots);
-	} else {
-		cerr << "Unknown " << element.name() << "@dotcount: " << dur << endl;
-		return 0;
+	xml_attribute atstaffnum = dir.attribute("staff");
+	if (!atstaffnum) {
+		cerr << "Error: staff number required on dir element in measure "
+		     << m_currentMeasure  << " (ignoring text: " << cleanWhiteSpace(text) << ")" << endl;
+		return;
 	}
-
-	if (dotcount > 0) {
-		int top = (1 << (dotcount+1)) - 1;
-		int bot = 1 << dotcount;
-		HumNum dotfactor(top, bot);
-		output *= dotfactor;
+	int staffnum = dir.attribute("staff").as_int();
+	if (staffnum <= 0) {
+		cerr << "Error: staff number on dir element in measure should be positive.\n";
+		cerr << "Instead the staff number is: " << m_currentMeasure  << " (ignoring text: " <<  cleanWhiteSpace(text) << ")" << endl;
+		return;
 	}
 
-	// add a correction for the tuplet factor which is currently active.
-	if (m_tupletfactor != 1) {
-		output *= m_tupletfactor;
-	}
+	double meterunit = m_currentMeterUnit[staffnum - 1];
+	double tsd = (stof(ts)-1) * 4.0 / meterunit;
 
-	return output;
+	GridMeasure* gm = m_outdata.back();
+	double tsm = gm->getTimestamp().getFloat();
+	bool foundslice = false;
+	GridSlice* gs;
+	for (auto gsit = gm->begin(); gsit != gm->end(); gsit++) {
+		gs = *gsit;
+		if (!gs->isDataSlice()) {
+			continue;
+		}
+		double gsts = gs->getTimestamp().getFloat();
+		double difference = (gsts-tsm) - tsd;
+		if (!(fabs(difference) < 0.0001)) {
+			continue;
+		}
+		// GridVoice* voice = gs->at(staffnum-1)->at(0)->at(0);
+		// HTp token = voice->getToken();
+		// if (token != NULL) {
+		// 	token->setValue("LO", "TX", "t", text);
+		// } else {
+		// 	cerr << "Strange null-token error while inserting dir element." << endl;
+		// }
+		foundslice = true;
+
+		// Found data line which should prefixed with a layout line
+		// should be done with HumHash post-processing, but do it manually for now.
+
+		auto previousit = gsit;
+		previousit--;
+		if (previousit == gm->end()) {
+			previousit = gsit;
+		}
+		auto previous = *previousit;
+		if (previous->isLayoutSlice()) {
+			GridVoice* voice = previous->at(staffnum-1)->at(0)->at(0);
+			HTp tok = voice->getToken();
+			if (tok == NULL) {
+				HTp newtok = new HumdrumToken(message);
+				voice->setToken(newtok);
+				tok = voice->getToken();
+				break;
+			} else if (tok->isNull()) {
+				tok->setText(message);
+				break;
+			}
+		}
+
+		// Insert a layout slice in front of current data slice.
+		GridSlice* ngs = new GridSlice(gm, gs->getTimestamp(), SliceType::Layouts, m_maxStaffInFile);
+		int parti = staffnum - 1;
+		int staffi = 0;
+		int voicei = 0;
+		ngs->addToken(message, parti, staffi, voicei);
+		gm->insert(gsit, ngs);
+
+		break;
+	}
+	if (!foundslice) {
+		cerr << "Warning: dir elements not occuring at note/rest times are not yet supported" << endl;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::getDuration_mensural -- Get duration from note or chord.  If chord does not
-//    have @dur then use @dur of first note in children elements.
-//
-// @dur: https://music-encoding.org/guidelines/v4/data-types/data.duration.mensural.html
-//          X = maxima
-//          L = longa
-//          S = brevis
-//          s = semibrevis
-//          M = minima
-//          m = semiminima
-//          U = fusa
-//          u = semifusa
-// @dur.quality:
-//          i = imperfecta  :: remove augmentation dot
-//          p = perfecta    :: add augmentation dot
-//          altera = altera :: duration is double the rhythmic value of notes
+// Tool_mei2hum::cleanWhiteSpace -- Convert newlines to "\n", and trim spaces.
+//    Also remove more than one space in a row.
 //
 
-HumNum Tool_mei2hum::getDuration_mensural(xml_node element, int& dotcount) {
-	dotcount = 0;
-
-	xml_attribute dur_qual = element.attribute("dur.quality");
-	xml_attribute dur_attr = element.attribute("dur");
-	string name = element.name();
-
-	if ((!dur_attr) && (name == "note")) {
-		// real notes must have durations, but this one
-		// does not, so assign zero duration
-		return 0;
-	}
-	if ((!dur_attr) && (name == "chord")) {
-		// if there is no dur attribute on a chord, then look for it
-		// on the first note subelement of the chord.
-		auto newelement = element.select_node(".//note").node();
-		if (newelement) {
-			element = newelement;
-			dur_attr = element.attribute("dur");
-			name = element.name();
-			dur_qual = element.attribute("dur.quality");
+string Tool_mei2hum::cleanWhiteSpace(const string& input) {
+	string output;
+	output.reserve(input.size() + 8);
+	bool foundstart = false;
+	for (int i=0; i<(int)input.size(); i++) {
+		if ((!foundstart) && std::isspace(input[i])) {
+			continue;
+		}
+		foundstart = true;
+		if (input[i] == '\t') {
+			if ((!output.empty()) && (output.back() != ' ')) {
+				output += ' ';
+			}
+		} else if (input[i] == '\n') {
+			if ((!output.empty()) && (output.back() != ' ')) {
+				output += ' ';
+			}
+		} else if (input[i] == ' ') {
+			if ((!output.empty()) && (output.back() != ' ')) {
+				output += ' ';
+			}
 		} else {
-			return 0;
+			output += input[i];
 		}
 	}
-
-	string dur = dur_attr.value();
-	if (dur == "") {
-		return 0;
+	while ((!output.empty()) && (output.back() == ' ')) {
+		output.pop_back();
 	}
-	string durquality = dur_qual.value();
 
-	char rhythm = '\0';
-	if (dur == "maxima") {
-		rhythm = 'X';
-	} else if (dur == "longa") {
-		rhythm = 'L';
-	} else if (dur == "brevis") {
-		rhythm = 'S';
-	} else if (dur == "semibrevis") {
-		rhythm = 's';
-	} else if (dur == "minima") {
-		rhythm = 'M';
-	} else if (dur == "semiminima") {
-		rhythm = 'm';
-	} else if (dur == "fusa") {
-		rhythm = 'U';
-	} else if (dur == "semifusa") {
-		rhythm = 'u';
-	} else {
-		cerr << "Error: unknown rhythm" << element.name() << "@dur: " << dur << endl;
-		return 0;
-	}
+	return output;
+}
 
-	mei_staffDef& ss = m_scoreDef.staves.at(m_currentStaff - 1);
-	int maximodus = ss.maximodus == 3 ? 3 : 2;
-	int modus     = ss.modus     == 3 ? 3 : 2;
-	int tempus    = ss.tempus    == 3 ? 3 : 2;
-	int prolatio  = ss.prolatio  == 3 ? 3 : 2;
 
-	bool altera     = false;
-	bool perfecta   = false;
-	bool imperfecta = false;
 
-	if (durquality == "imperfecta") {
-		imperfecta = true;
-	} else if (durquality == "perfecta") {
-		perfecta = true;
-	} else if (durquality == "altera") {
-		altera = true;
+//////////////////////////////
+//
+// Tool_mei2hum::cleanDirText -- convert ":" to "&colon;".
+//     Remove tabs and newlines, and trim spaces.  Maybe allow
+//     newlines using "\n" and allow font changes in the future.
+//     Remove redundant whitespace. Do accents later perhaps or
+//     monitor for UTF-8.
+//
+
+string Tool_mei2hum::cleanDirText(const string& input) {
+	string output;
+	output.reserve(input.size() + 8);
+	bool foundstart = false;
+	for (int i=0; i<(int)input.size(); i++) {
+		if ((!foundstart) && std::isspace(input[i])) {
+			continue;
+		}
+		foundstart = true;
+		if (input[i] == ':') {
+			output += "&colon;";
+		} else if (input[i] == '\t') {
+			if ((!output.empty()) && (output.back() != ' ')) {
+				output += ' ';
+			}
+		} else if (input[i] == '\n') {
+			if ((!output.empty()) && (output.back() != ' ')) {
+				output += ' ';
+			}
+		} else if (input[i] == ' ') {
+			if ((!output.empty()) && (output.back() != ' ')) {
+				output += ' ';
+			}
+		} else {
+			output += input[i];
+		}
+	}
+	while ((!output.empty()) && (output.back() == ' ')) {
+		output.pop_back();
 	}
 
-	HumNum output = Convert::mensToDuration(rhythm, altera, perfecta, imperfecta, maximodus, modus, tempus, prolatio);
 	return output;
 }
 
 
 
-
 //////////////////////////////
 //
-// Tool_mei2hum::parseVerse --
+// Tool_mei2hum::cleanVerseText --
+//     Remove tabs and newlines, and trim spaces.
+//     Do accents later perhaps or monitor for UTF-8.
 //
 
-void Tool_mei2hum::parseVerse(xml_node verse, GridStaff* staff) {
-	NODE_VERIFY(verse, )
-	MAKE_CHILD_LIST(children, verse);
-
-	string n = verse.attribute("n").value();
-	int nnum = 1;
-	if (n.empty()) {
-		cerr << "Warning: no layer number on layer element" << endl;
-	} else {
-		nnum = stoi(n);
+string Tool_mei2hum::cleanVerseText(const string& input) {
+	string output;
+	output.reserve(input.size() + 8);
+	bool foundstart = false;
+	for (int i=0; i<(int)input.size(); i++) {
+		if ((!foundstart) && std::isspace(input[i])) {
+			continue;
+		}
+		foundstart = true;
+		if (input[i] == '\t') {
+			output += ' ';
+		} else if (input[i] == '\n') {
+			output += ' ';
+		} else {
+			output += input[i];
+		}
 	}
-	if (nnum < 1) {
-		cerr << "Warning: invalid layer number: " << nnum << endl;
-		cerr << "Setting it to 1." << endl;
-		nnum = 1;
+	while ((!output.empty()) && (output.back() == ' ')) {
+		output.pop_back();
 	}
 
-	string versetext;
-	int sylcount = 0;
+	return output;
+}
 
-	for (int i=0; i<(int)children.size(); i++) {
-		string nodename = children[i].name();
-		if (nodename == "syl") {
-			if (sylcount > 0) {
-				versetext += " ";
+
+
+//////////////////////////////
+//
+// Tool_mei2hum::cleanReferenceRecordText -- convert ":" to "&colon;".
+//     Remove tabs and newlines, and trim spaces.  Maybe allow
+//     newlines using "\n" and allow font changes in the future.
+//     Do accents later perhaps or monitor for UTF-8.
+//
+
+string Tool_mei2hum::cleanReferenceRecordText(const string& input) {
+	string output;
+	output.reserve(input.size() + 8);
+	bool foundstart = false;
+	char lastchar = '\0';
+	for (int i=0; i<(int)input.size(); i++) {
+		if ((!foundstart) && std::isspace(input[i])) {
+			continue;
+		}
+		foundstart = true;
+		if (input[i] == '\n') {
+			if (lastchar != ' ') {
+				output += ' ';
 			}
-			sylcount++;
-			versetext += parseSyl(children[i]);
+			lastchar = ' ';
+		} else if (input[i] == '\t') {
+			if (lastchar != ' ') {
+				output += ' ';
+			}
+			lastchar = ' ';
 		} else {
-			cerr << DKHTP << verse.name() << "/" << nodename << CURRLOC << endl;
+			output += input[i];
+			lastchar = input[i];
 		}
 	}
-
-	if (versetext == "") {
-		// nothing to store
-		return;
+	while ((!output.empty()) && (output.back() == ' ')) {
+		output.pop_back();
 	}
 
-	staff->setVerse(nnum-1, versetext);
-	reportVerseNumber(nnum, m_currentStaff-1);
-
-	return;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseBareSyl -- Only one syl allows as a bar child of note element.
-//     This function is used to process syl elements that are not wrapped in a verse element.
+// Tool_mei2hum::parseTempo --
+//
+// Example:
+//   <tempo tstamp="1" place="above" staff="1">
+//      1 - Allegro con spirito <rend fontname="VerovioText">&#xE1D5;</rend> = 132
+//   </tempo>
+//
+//
+// Ways of indicating tempo:
 //
+// tempo@midi.bpm == tempo per quarter note (Same as Humdrum *MM value)
+//
+// tempo@midi.mspb == microseconds per quarter note ( bpm = mspb * 60 / 1000000)
+//
+// tempo@mm == tempo per beat (bpm = mm / unit(dots))
+// tempo@mm.unit == beat unit for tempo@mm
+// tempo@mm.dots == dots for tempo@unit
+//
+// Free-form text:
+//
+// &#xE1D5; == quarter note
+//
+// #define SMUFL_QUARTER_NOTE "\ue1d5"
 
-void Tool_mei2hum::parseBareSyl(xml_node syl, GridStaff* staff) {
-	NODE_VERIFY(syl, )
+void Tool_mei2hum::parseTempo(xml_node tempo, HumNum starttime) {
+	NODE_VERIFY(tempo, )
 
-	int nnum = 1;
-	xml_attribute n_attr = syl.attribute("n");
-	if (n_attr) {
-		nnum = n_attr.as_int();
-	}
+	bool found = false;
+	double value = 0.0;
 
-	if (nnum < 1) {
-		cerr << "Warning: invalid layer number: " << nnum << endl;
-		cerr << "Setting it to 1." << endl;
-		nnum = 1;
+	xml_attribute bpm = tempo.attribute("bpm");
+	if (bpm) {
+		value = bpm.as_double();
+		if (value > 0.0) {
+			found = true;
+		}
 	}
 
-	string versetext = parseSyl(syl);
-
-	if (versetext == "") {
-		// nothing to store
-		return;
+	if (!found) {
+		xml_attribute mspb   = tempo.attribute("mspb");
+		value = mspb.as_double() * 60.0 / 1000000.0;
+		if (value > 0.0) {
+			found = true;
+		}
 	}
 
-	staff->setVerse(nnum-1, versetext);
-	reportVerseNumber(nnum, m_currentStaff-1);
+	if (!found) {
+		xml_attribute mm     = tempo.attribute("mm");
+		xml_attribute mmunit = tempo.attribute("mm.unit");
+		xml_attribute mmdots = tempo.attribute("mm.dots");
+		value = mm.as_double();
+		string recip = mmunit.value();
+		int dcount = mmdots.as_int();
+		for (int i=0; i<dcount; i++) {
+			recip += '.';
+		}
+		HumNum duration = Convert::recipToDuration(recip);
+		value *= duration.getFloat();
+		if (value > 0.0) {
+			found = true;
+		}
+	}
 
-	return;
-}
+	if (!found) {
+		// search for free-form tempo marking.  Something like:
+		//   <tempo tstamp="1" place="above" staff="1">
+		//      1 - Allegro con spirito <rend fontname="VerovioText">&#xE1D5;</rend> = 132
+		//   </tempo>
+		//
+		// UTF-8 version in string "\ue1d5";
+		string text;
 
+		MAKE_CHILD_LIST(children, tempo);
+		for (int i=0; i<(int)children.size(); i++) {
+			if (children[i].type() == pugi::node_pcdata) {
+				text += children[i].value();
+			} else {
+				text += children[i].child_value();
+			}
+			text += " ";
 
+		}
+		HumRegex hre;
+		// #define SMUFL_QUARTER_NOTE "\ue1d5"
+		// if (hre.search(text, SMUFL_QUARTER_NOTE "\\s*=\\s*(\\d+\\.?\\d*)")) {
+		if (hre.search(text, "\\s*=\\s*(\\d+\\.?\\d*)")) {
+			// assuming quarter note for now.
+			value = hre.getMatchDouble(1);
+			found = true;
+		}
+		// further rhythmic values for tempo should go here.
+	}
 
-//////////////////////////////
-//
-// Tool_mei2hum::reportVerseNumber --
-//
+	// also deal with tempo designiations such as "Allegro"...
 
-void Tool_mei2hum::reportVerseNumber(int pmax, int staffindex) {
-	if (staffindex < 0) {
+	if (!found) {
+		// no tempo to set
 		return;
 	}
-	if (staffindex >= (int)m_maxverse.size()) {
-		return;
+
+	// insert tempo
+	GridMeasure* gm = m_outdata.back();
+	GridSlice* gs = new GridSlice(gm, starttime, SliceType::Tempos, m_maxStaffInFile);
+	stringstream stok;
+	stok << "*MM" << value;
+	string token = stok.str();
+
+	for (int i=0; i<m_maxStaffInFile; i++) {
+		gs->at(i)->at(0)->at(0)->setToken(token);
 	}
-	if (m_maxverse.at(staffindex) < pmax) {
-		m_maxverse[staffindex] = pmax;
+
+	// insert after time signature at same timestamp if possible
+	bool inserted = false;
+	for (auto it = gm->begin(); it != gm->end(); it++) {
+		if ((*it)->getTimestamp() > starttime) {
+			gm->insert(it, gs);
+			inserted = true;
+			break;
+		} else if ((*it)->isTimeSigSlice()) {
+			it++;
+			gm->insert(it, gs);
+			inserted = true;
+			break;
+		} else if (((*it)->getTimestamp() == starttime) && ((*it)->isNoteSlice()
+				|| (*it)->isGraceSlice())) {
+			gm->insert(it, gs);
+			inserted = true;
+			break;
+		}
+	}
+
+	if (!inserted) {
+		gm->push_back(gs);
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseSyl --
+// Tool_mei2hum::parseHarm -- Not yet ready to convert <harm> data.
+//    There will be different types of harm (such as figured bass), which
+//    will need to be subcategorized into different datatypes, such as
+//    *fb for figured bass.  Also free-text can be present in <harm>
+//    data, so the current datatype for that is **cdata  (meaning chord-like
+//    data that will be mapped back into <harm> which converting back to
+//    MEI data.
+//
+// Example:
+//     <harm staff="1" tstamp="1.000000">C major</harm>
 //
 
-string Tool_mei2hum::parseSyl(xml_node syl) {
-	NODE_VERIFY(syl, "")
-	MAKE_CHILD_LIST(children, syl);
+void Tool_mei2hum::parseHarm(xml_node harm, HumNum starttime) {
+	NODE_VERIFY(harm, )
+	MAKE_CHILD_LIST(children, harm);
 
-	string text = syl.child_value();
-	for (int i=0; i<(int)text.size(); i++) {
-		if (text[i] == '_') {
-			text[i] = ' ';
+	string text = harm.child_value();
+
+	if (text.empty()) { // looking at <rend> sub-elements
+		int count = 0;
+		for (int i=0; i<(int)children.size(); i++) {
+			string nodename = children[i].name();
+			if (nodename == "rend") {
+				if (count) {
+					text += " ";
+				}
+				count++;
+				text += children[i].child_value();
+				//if (strcmp(children[i].attribute("fontstyle").value(), "normal") == 0) {
+				//	font = "";  // normal is default in Humdrum layout
+				//}
+				//if (strcmp(children[i].attribute("fontweight").value(), "bold") == 0) {
+				//	font += "B";  // normal is default in Humdrum layout
+				//}
+			} else if (nodename == "") {
+				// text node
+				if (count) {
+					text += " ";
+				}
+				count++;
+				text += children[i].value();
+			} else {
+				cerr << DKHTP << harm.name() << "/" << nodename << CURRLOC << endl;
+			}
 		}
 	}
 
-	string wordpos = syl.attribute("wordpos").value();
-	if (wordpos == "i") {
-		text = text + "-";
-	} else if (wordpos == "m") {
-		text = "-" + text + "-";
-	} else if (wordpos == "t") {
-		text = "-" + text;
+	if (text.empty()) {
+		return;
 	}
 
-	return text;
-}
-
-
+   // cerr << "FOUND HARM DATA " << text << endl;
 
-//////////////////////////////
-//
-// Tool_mei2hum::parseClef --
-//
-//
+/*
 
-void Tool_mei2hum::parseClef(xml_node clef, HumNum starttime) {
-	NODE_VERIFY(clef, )
+	string startid = harm.attribute("startid").value();
 
-	string shape = clef.attribute("shape").value();
-	string line = clef.attribute("line").value();
-	string clefdis = clef.attribute("clef.dis").value();
-	string clefdisplace = clef.attribute("clef.dis.place").value();
+	int staffnum = harm.attribute("staff").as_int();
+	if (staffnum == 0) {
+		cerr << "Error: staff number required on harm element" << endl;
+		return;
+	}
+	double meterunit = m_currentMeterUnit[staffnum - 1];
 
-	string tok = makeHumdrumClef(shape, line, clefdis, clefdisplace);
+	if (!startid.empty()) {
+		// Harmony is (or at least should) be attached directly
+		// do a note, so it is handled elsewhere.
+		cerr << "Warning DYNAMIC " << text << " is not yet processed." << endl;
+		return;
+	}
 
-	m_outdata.back()->addClefToken(tok, starttime QUARTER_CONVERT,
-			m_currentStaff-1, 0, 0, m_staffcount);
+	string ts = harm.attribute("tstamp").value();
+	if (ts.empty()) {
+		cerr << "Error: no timestamp on harm element" << endl;
+		return;
+	}
+	double tsd = (stof(ts)-1) * 4.0 / meterunit;
+	double tolerance = 0.001;
+	GridMeasure* gm = m_outdata.back();
+	double tsm = gm->getTimestamp().getFloat();
+	bool foundslice = false;
+	GridSlice *nextgs = NULL;
+	for (auto gs : *gm) {
+		if (!gs->isDataSlice()) {
+			continue;
+		}
+		double gsts = gs->getTimestamp().getFloat();
+		double difference = (gsts-tsm) - tsd;
+		if (difference < tolerance) {
+			// did not find data line at exact timestamp, so move
+			// the harm to the next event. Need to think about adding
+			// a new timeslice for the harm when it is not attached to
+			// a note.
+			nextgs = gs;
+			break;
+		}
+		if (!(fabs(difference) < tolerance)) {
+			continue;
+		}
+		GridPart* part = gs->at(staffnum-1);
+		part->setHarmony(text);
+		m_outdata.setHarmonyPresent(staffnum-1);
+		foundslice = true;
+		break;
+	}
+	if (!foundslice) {
+		if (nextgs == NULL) {
+			cerr << "Warning: harmony not attched to system events "
+					<< "are not yet supported in measure " << m_currentMeasure << endl;
+		} else {
+			GridPart* part = nextgs->at(staffnum-1);
+			part->setHarmony(text);
+			m_outdata.setHarmonyPresent(staffnum-1);
+			// Give a time offset for displaying the harmmony here.
+		}
+	}
+*/
 
 }
 
@@ -96248,446 +99634,623 @@ void Tool_mei2hum::parseClef(xml_node clef, HumNum starttime) {
 
 //////////////////////////////
 //
-// Tool_mei2hum::makeHumdrumClef --
+// Tool_mei2hum::parseDynam --
 //
 // Example:
-//     <clef shape="G" line="2" clef.dis="8" clef.dis.place="below" />
+//     <dynam staff="1" tstamp="1.000000">p</dynam>
 //
 
-string Tool_mei2hum::makeHumdrumClef(const string& shape,
-		const string& line, const string& clefdis, const string& clefdisplace) {
-	string output = "*clef" + shape;
-	if (!clefdis.empty()) {
-		int number = stoi(clefdis);
+void Tool_mei2hum::parseDynam(xml_node dynam, HumNum starttime) {
+	NODE_VERIFY(dynam, )
+	MAKE_CHILD_LIST(children, dynam);
+
+	string text = dynam.child_value();
+
+	if (text.empty()) { // looking at <rend> sub-elements
 		int count = 0;
-		if (number == 8) {
-			count = 1;
-		} else if (number == 15) {
-			count = 2;
+		for (int i=0; i<(int)children.size(); i++) {
+			string nodename = children[i].name();
+			if (nodename == "rend") {
+				if (count) {
+					text += " ";
+				}
+				count++;
+				text += children[i].child_value();
+				//if (strcmp(children[i].attribute("fontstyle").value(), "normal") == 0) {
+				//	font = "";  // normal is default in Humdrum layout
+				//}
+				//if (strcmp(children[i].attribute("fontweight").value(), "bold") == 0) {
+				//	font += "B";  // normal is default in Humdrum layout
+				//}
+			} else if (nodename == "") {
+				// text node
+				if (count) {
+					text += " ";
+				}
+				count++;
+				text += children[i].value();
+			} else {
+				cerr << DKHTP << dynam.name() << "/" << nodename << CURRLOC << endl;
+			}
 		}
-		if (clefdisplace != "above") {
-			count = -count;
+	}
+
+	if (text.empty()) {
+		return;
+	}
+
+	string startid = dynam.attribute("startid").value();
+
+	int staffnum = dynam.attribute("staff").as_int();
+	if (staffnum == 0) {
+		cerr << "Error: staff number required on dynam element" << endl;
+		return;
+	}
+	double meterunit = m_currentMeterUnit[staffnum - 1];
+
+	if (!startid.empty()) {
+		// Dynamic is (or at least should) be attached directly
+		// do a note, so it is handled elsewhere.
+		cerr << "Warning DYNAMIC " << text << " is not yet processed." << endl;
+		return;
+	}
+
+	string ts = dynam.attribute("tstamp").value();
+	if (ts.empty()) {
+		cerr << "Error: no timestamp on dynam element" << endl;
+		return;
+	}
+	double tsd = (stof(ts)-1) * 4.0 / meterunit;
+	double tolerance = 0.001;
+	GridMeasure* gm = m_outdata.back();
+	double tsm = gm->getTimestamp().getFloat();
+	bool foundslice = false;
+	GridSlice *nextgs = NULL;
+	for (auto gs : *gm) {
+		if (!gs->isDataSlice()) {
+			continue;
 		}
-		switch (count) {
-			case 1: output += "^"; break;
-			case 2: output += "^^"; break;
-			case -1: output += "v"; break;
-			case -2: output += "vv"; break;
+		double gsts = gs->getTimestamp().getFloat();
+		double difference = (gsts-tsm) - tsd;
+		if (difference < tolerance) {
+			// did not find data line at exact timestamp, so move
+			// the dynamic to the next event. Maybe think about adding
+			// a new timeslice for the dynamic.
+			nextgs = gs;
+			break;
+		}
+		if (!(fabs(difference) < tolerance)) {
+			continue;
 		}
+		GridPart* part = gs->at(staffnum-1);
+		part->setDynamics(text);
+		m_outdata.setDynamicsPresent(staffnum-1);
+		foundslice = true;
+		break;
 	}
-	output += line;
-	return output;
+	if (!foundslice) {
+		if (nextgs == NULL) {
+			cerr << "Warning: dynamics not attched to system events "
+					<< "are not yet supported in measure " << m_currentMeasure << endl;
+		} else {
+			GridPart* part = nextgs->at(staffnum-1);
+			part->setDynamics(text);
+			m_outdata.setDynamicsPresent(staffnum-1);
+			// Give a time offset for displaying the dynamic here.
+		}
+	}
+}
+
+
+
+
+
+/////////////////////////////////
+//
+// Tool_gridtest::Tool_melisma -- Set the recognized options for the tool.
+//
+
+Tool_melisma::Tool_melisma(void) {
+	define("m|min=i:2",        "minimum length to identify as a melisma");
+	define("r|replace=b",      "replace lyrics with note counts");
+	define("a|average|avg=b",  "calculate note-to-syllable ratio");
+	define("w|words=b",        "list words that contain a melisma");
+	define("p|part=b",         "also calculate note-to-syllable ratios by part");
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_mei2hum::parseChord --
+// Tool_melisma::run -- Primary interfaces to the tool.
 //
 
-HumNum Tool_mei2hum::parseChord(xml_node chord, HumNum starttime, int gracenumber) {
-	NODE_VERIFY(chord, starttime)
-	MAKE_CHILD_LIST(children, chord);
+bool Tool_melisma::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
 
-	processPreliminaryLinkedNodes(chord);
 
-	HumNum duration = getDuration(chord);
+bool Tool_melisma::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
+}
 
-	string tok;
-	int counter = 0;
-	for (int i=0; i<(int)children.size(); i++) {
-		string nodename = children[i].name();
-		if (nodename == "note") {
-			counter++;
-			if (counter > 1) {
-				tok += " ";
-			}
-			parseNote(children[i], chord, tok, starttime, gracenumber);
-		} else if (nodename == "artic") {
-			// This is handled within parseNote();
-		} else {
-			cerr << DKHTP << chord.name() << "/" << nodename << CURRLOC << endl;
-		}
-	}
 
-	m_fermata = false;
-	processLinkedNodes(tok, chord);
-	if (!m_fermata) {
-		processFermataAttribute(tok, chord);
-	}
+bool Tool_melisma::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	return status;
+}
 
-	m_outdata.back()->addDataToken(tok, starttime QUARTER_CONVERT, m_currentStaff-1,
-		0, m_currentLayer-1, m_staffcount);
 
-	return starttime + duration;
+bool Tool_melisma::run(HumdrumFile& infile) {
+   initialize(infile);
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::getChildrenVector -- Return a list of all children elements
-//   of a given element.  Pugixml does not allow random access, but storing
-//   them in a vector allows that possibility.
+// Tool_melisma::initialize --
 //
 
-void Tool_mei2hum::getChildrenVector(vector<xml_node>& children,
-		xml_node parent) {
-	children.clear();
-	for (xml_node child : parent.children()) {
-		children.push_back(child);
-	}
+void Tool_melisma::initialize(HumdrumFile& infile) {
+	// do nothing for now
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::initialize -- Setup for the tool, mostly parsing command-line
-//   (input) options.
+// Tool_melisma::processFile --
 //
 
-void Tool_mei2hum::initialize(void) {
-	m_recipQ   =  getBoolean("recip");
-	m_stemsQ   =  getBoolean("stems");
-	m_xmlidQ   =  getBoolean("xmlids");
-	m_xmlidQ   = 1;  // for testing
-	m_appLabel =  getString("app-label");
-	m_placeQ   = !getBoolean("no-place");
+void Tool_melisma::processFile(HumdrumFile& infile) {
+	vector<vector<int>> notecount;
+	getNoteCounts(infile, notecount);
+	vector<WordInfo> wordinfo;
+	wordinfo.reserve(1000);
+	map<string, int> wordlist;
+	initializePartInfo(infile);
+
+	if (getBoolean("replace")) {
+		replaceLyrics(infile, notecount);
+	} else if (getBoolean("words")) {
+		markMelismas(infile, notecount);
+		extractWordlist(wordinfo, wordlist, infile, notecount);
+		printWordlist(infile, wordinfo, wordlist);
+	} else {
+		markMelismas(infile, notecount);
+	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::buildIdLinkMap -- Build table of startid and endid links between elements.
-//
-// Reference: https://pugixml.org/docs/samples/traverse_walker.cpp
+// Tool_melisma::initializePartInfo --
 //
 
-void Tool_mei2hum::buildIdLinkMap(xml_document& doc) {
-	class linkmap_walker : public pugi::xml_tree_walker {
-		public:
-			virtual bool for_each(pugi::xml_node& node) {
-				xml_attribute startid = node.attribute("startid");
-				xml_attribute endid = node.attribute("endid");
-				if (startid) {
+void Tool_melisma::initializePartInfo(HumdrumFile& infile) {
+	m_names.clear();
+	m_abbreviations.clear();
+	m_partnums.clear();
 
-					string value = startid.value();
-					if (!value.empty()) {
-						if (value[0] == '#') {
-							value = value.substr(1, string::npos);
-						}
-					}
-					if (!value.empty()) {
-						(*startlinks)[value].push_back(node);
-					}
+	m_names.resize(infile.getTrackCount() + 1);
+	m_abbreviations.resize(infile.getTrackCount() + 1);
+	m_partnums.resize(infile.getTrackCount() + 1);
+	fill(m_partnums.begin(), m_partnums.end(), -1);
 
+	vector<HTp> starts;
+	infile.getSpineStartList(starts);
+	int ktrack = 0;
+	int track = 0;
+	int part = 0;
+	for (int i=0; i<(int)starts.size(); i++) {
+		track = starts[i]->getTrack();
+		if (starts[i]->isKern()) {
+			ktrack = track;
+			part++;
+			m_partnums[ktrack] = part;
+			HTp current = starts[i];
+			while (current) {
+				if (current->isData()) {
+					break;
 				}
-				if (endid) {
-
-					string value = endid.value();
-					if (!value.empty()) {
-						if (value[0] == '#') {
-							value = value.substr(1, string::npos);
-						}
-					}
-					if (!value.empty()) {
-						(*stoplinks)[value].push_back(node);
-					}
-
+				if (current->compare(0, 3, "*I\"") == 0) {
+					m_names[ktrack] = current->substr(3);
+				} else if (current->compare(0, 3, "*I\'") == 0) {
+					m_abbreviations[ktrack] = current->substr(3);
 				}
-				return true; // continue traversal
+				current = current->getNextToken();
 			}
+		} else if (ktrack) {
+			m_names[track] = m_names[ktrack];
+			m_abbreviations[track] = m_abbreviations[ktrack];
+			m_partnums[track] = m_partnums[ktrack];
+		}
+	}
 
-			map<string, vector<xml_node>>* startlinks = NULL;
-			map<string, vector<xml_node>>* stoplinks = NULL;
-	};
-
-	m_startlinks.clear();
-	m_stoplinks.clear();
-	linkmap_walker walker;
-	walker.startlinks = &m_startlinks;
-	walker.stoplinks = &m_stoplinks;
-	doc.traverse(walker);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseDir -- Meter cannot change in middle of measure.
-//     Need to implement @startid version.
-//
-// Example:
-//    <dir xml:id="dir-L408F3" place="below" staff="1" tstamp="2.0">con espressione</dir>
-//
-// or with normal font specified:
-//    <dir xml:id="dir-L7F1" staff="1" tstamp="2.000000">
-//       <rend xml:id="rend-0000001696821523" fontstyle="normal">test</rend>
-//    </dir>
-//
-// bold font:
-//   <dir xml:id="dir-L25F3" place="above" staff="1" tstamp="3.000000">
-//      <rend xml:id="rend-0000001714819172" fontstyle="normal" fontweight="bold">comment</rend>
-//   </dir>
+// printWordlist --
 //
 
-void Tool_mei2hum::parseDir(xml_node dir, HumNum starttime) {
-	NODE_VERIFY(dir, )
-	MAKE_CHILD_LIST(children, dir);
-
-	string font = "i";  // italic by default in verovio
+void Tool_melisma::printWordlist(HumdrumFile& infile, vector<WordInfo>& wordinfo,
+		map<string, int> words) {
 
-	string placement = ""; // a = above, b = below
+	// for (auto& item : words) {
+	// 	m_free_text << item.first;
+	// 	if (item.second > 1) {
+	// 		m_free_text << " (" << item.second << ")";
+	// 	}
+	// 	m_free_text << endl;
+	// }
 
-	string place = dir.attribute("place").value();
-	if (place == "above") {
-		placement = "a:";
-	}
-	// Below is the default in Humdrum layout commands.
+	vector<int> ncounts;
+	vector<int> mcounts;
+	getMelismaNoteCounts(ncounts, mcounts, infile);
 
-	string text;
+	// m_free_text << "===========================" << endl;
 
-	if (!children.empty()) { // also includes the above text node, but only looking at <rend>.
-		int count = 0;
-		for (int i=0; i<(int)children.size(); i++) {
-			string nodename = children[i].name();
-			if (nodename == "rend") {
-				if (count) {
-					text += " ";
-				}
-				count++;
-				text += children[i].child_value();
-				if (strcmp(children[i].attribute("fontstyle").value(), "normal") == 0) {
-					font = "";  // normal is default in Humdrum layout
-				}
-				if (strcmp(children[i].attribute("fontweight").value(), "bold") == 0) {
-					font += "B";  // normal is default in Humdrum layout
-				}
-			} else if (nodename == "") {
-				// text node
-				if (count) {
-					text += " ";
-				}
-				count++;
-				text += children[i].value();
-			} else {
-				cerr << DKHTP << dir.name() << "/" << nodename << CURRLOC << endl;
-			}
-		}
-	}
+	std::vector<HTp> kspines = infile.getKernSpineStartList();
 
-	if (text.empty()) {
-		return;
-	}
+	m_free_text << "@@BEGIN:\tMELISMAS\n";
 
-	string message = "!LO:TX:";
-	message += placement;
-	if (!font.empty()) {
-		message += font + ":";
+	string filename = infile.getFilename();
+	auto pos = filename.rfind("/");
+	if (pos != string::npos) {
+		filename = filename.substr(pos+1);
 	}
-	message += "t=" + cleanDirText(text);
+	m_free_text << "@FILENAME:\t" << filename << endl;
+	m_free_text << "@PARTCOUNT:\t" << kspines.size() << endl;
+	m_free_text << "@WORDCOUNT:\t" << wordinfo.size() << endl;
+	m_free_text << "@SCOREDURATION:\t" << getScoreDuration(infile) << endl;
+	m_free_text << "@NOTES:\t\t" << ncounts[0] << endl;
+	m_free_text << "@MELISMANOTES:\t" << mcounts[0] << endl;
 
-	string ts = dir.attribute("tstamp").value();
-	if (ts.empty()) {
-		cerr << "Error: no timestamp on dir element and can't currently processes with @startid." << endl;
-		return;
+	m_free_text << "@MELISMASCORE:\t" << int((double)mcounts[0] / (double)ncounts[0] * 1000.0 + 0.5)/10.0 << "%" << endl;
+	for (int i=1; i<(int)m_partnums.size(); i++) {
+		if (m_partnums[i] == 0) {
+			continue;
+		}
+		if (m_partnums[i] == m_partnums[i-1]) {
+			continue;
+		}
+		m_free_text << "@PARTSCORE-" << m_partnums[i] << ":\t" << int((double)mcounts[i] / (double)ncounts[i] * 1000.0 + 0.5)/10.0 << "%" << endl;
 	}
 
-	xml_attribute atstaffnum = dir.attribute("staff");
-	if (!atstaffnum) {
-		cerr << "Error: staff number required on dir element in measure "
-		     << m_currentMeasure  << " (ignoring text: " << cleanWhiteSpace(text) << ")" << endl;
-		return;
-	}
-	int staffnum = dir.attribute("staff").as_int();
-	if (staffnum <= 0) {
-		cerr << "Error: staff number on dir element in measure should be positive.\n";
-		cerr << "Instead the staff number is: " << m_currentMeasure  << " (ignoring text: " <<  cleanWhiteSpace(text) << ")" << endl;
-		return;
+	for (int i=1; i<(int)m_partnums.size(); i++) {
+		if (m_partnums[i] == 0) {
+			continue;
+		}
+		if (m_partnums[i] == m_partnums[i-1]) {
+			continue;
+		}
+		m_free_text << "@PARTNAME-" << m_partnums[i] << ":\t" << m_names[i] << endl;
 	}
 
-	double meterunit = m_currentMeterUnit[staffnum - 1];
-	double tsd = (stof(ts)-1) * 4.0 / meterunit;
-
-	GridMeasure* gm = m_outdata.back();
-	double tsm = gm->getTimestamp().getFloat();
-	bool foundslice = false;
-	GridSlice* gs;
-	for (auto gsit = gm->begin(); gsit != gm->end(); gsit++) {
-		gs = *gsit;
-		if (!gs->isDataSlice()) {
+	for (int i=1; i<(int)m_partnums.size(); i++) {
+		if (m_partnums[i] == 0) {
 			continue;
 		}
-		double gsts = gs->getTimestamp().getFloat();
-		double difference = (gsts-tsm) - tsd;
-		if (!(fabs(difference) < 0.0001)) {
+		if (m_partnums[i] == m_partnums[i-1]) {
 			continue;
 		}
-		// GridVoice* voice = gs->at(staffnum-1)->at(0)->at(0);
-		// HTp token = voice->getToken();
-		// if (token != NULL) {
-		// 	token->setValue("LO", "TX", "t", text);
-		// } else {
-		// 	cerr << "Strange null-token error while inserting dir element." << endl;
-		// }
-		foundslice = true;
+		m_free_text << "@PARTABBR-" << m_partnums[i] << ":\t" << m_abbreviations[i] << endl;
+	}
 
-		// Found data line which should prefixed with a layout line
-		// should be done with HumHash post-processing, but do it manually for now.
+	m_free_text << endl;
 
-		auto previousit = gsit;
-		previousit--;
-		if (previousit == gm->end()) {
-			previousit = gsit;
+	for (int i=0; i<(int)wordinfo.size(); i++) {
+		m_free_text << "@@BEGIN:\tWORD\n";
+		m_free_text << "@PARTNUM:\t" << wordinfo[i].partnum << endl;
+		// m_free_text << "@NAME:\t\t" << wordinfo[i].name << endl;
+		// m_free_text << "@ABBR:\t\t" << wordinfo[i].abbreviation << endl;
+		m_free_text << "@WORD:\t\t" << wordinfo[i].word << endl;
+		m_free_text << "@STARTTIME:\t" << wordinfo[i].starttime.getFloat() << endl;
+		m_free_text << "@ENDTIME:\t" << wordinfo[i].endtime.getFloat() << endl;
+		m_free_text << "@STARTBAR:\t" << wordinfo[i].bar << endl;
+
+		m_free_text << "@SYLLABLES:\t";
+		for (int j=0; j<(int)wordinfo[i].syllables.size(); j++) {
+			m_free_text << wordinfo[i].syllables[j];
+			if (j < (int)wordinfo[i].syllables.size() - 1) {
+				m_free_text << " ";
+			}
 		}
-		auto previous = *previousit;
-		if (previous->isLayoutSlice()) {
-			GridVoice* voice = previous->at(staffnum-1)->at(0)->at(0);
-			HTp tok = voice->getToken();
-			if (tok == NULL) {
-				HTp newtok = new HumdrumToken(message);
-				voice->setToken(newtok);
-				tok = voice->getToken();
-				break;
-			} else if (tok->isNull()) {
-				tok->setText(message);
-				break;
+		m_free_text << endl;
+
+		m_free_text << "@NOTECOUNTS:\t";
+		for (int j=0; j<(int)wordinfo[i].notecounts.size(); j++) {
+			m_free_text << wordinfo[i].notecounts[j];
+			if (j < (int)wordinfo[i].notecounts.size() - 1) {
+				m_free_text << " ";
 			}
 		}
+		m_free_text << endl;
 
-		// Insert a layout slice in front of current data slice.
-		GridSlice* ngs = new GridSlice(gm, gs->getTimestamp(), SliceType::Layouts, m_maxStaffInFile);
-		int parti = staffnum - 1;
-		int staffi = 0;
-		int voicei = 0;
-		ngs->addToken(message, parti, staffi, voicei);
-		gm->insert(gsit, ngs);
+		m_free_text << "@BARLINES:\t";
+		for (int j=0; j<(int)wordinfo[i].bars.size(); j++) {
+			m_free_text << wordinfo[i].bars[j];
+			if (j < (int)wordinfo[i].bars.size() - 1) {
+				m_free_text << " ";
+			}
+		}
+		m_free_text << endl;
 
-		break;
-	}
-	if (!foundslice) {
-		cerr << "Warning: dir elements not occuring at note/rest times are not yet supported" << endl;
+		m_free_text << "@STARTTIMES:\t";
+		for (int j=0; j<(int)wordinfo[i].starttimes.size(); j++) {
+			m_free_text << wordinfo[i].starttimes[j].getFloat();
+			if (j < (int)wordinfo[i].starttimes.size() - 1) {
+				m_free_text << " ";
+			}
+		}
+		m_free_text << endl;
+
+		m_free_text << "@ENDTIMES:\t";
+		for (int j=0; j<(int)wordinfo[i].endtimes.size(); j++) {
+			m_free_text << wordinfo[i].endtimes[j].getFloat();
+			if (j < (int)wordinfo[i].endtimes.size() - 1) {
+				m_free_text << " ";
+			}
+		}
+		m_free_text << endl;
+
+		m_free_text << "@@END:\tWORD\n";
+		m_free_text << endl;
 	}
+
+	m_free_text << "@@END:\tMELISMAS\n";
+	m_free_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::cleanWhiteSpace -- Convert newlines to "\n", and trim spaces.
-//    Also remove more than one space in a row.
+// Tool_melisma::getScoreDuration --
 //
 
-string Tool_mei2hum::cleanWhiteSpace(const string& input) {
-	string output;
-	output.reserve(input.size() + 8);
-	bool foundstart = false;
-	for (int i=0; i<(int)input.size(); i++) {
-		if ((!foundstart) && std::isspace(input[i])) {
+double Tool_melisma::getScoreDuration(HumdrumFile& infile) {
+	double output = 0.0;
+	for (int i=infile.getLineCount() - 1; i>=0; i--) {
+		if (!infile[i].isData()) {
 			continue;
 		}
-		foundstart = true;
-		if (input[i] == '\t') {
-			if ((!output.empty()) && (output.back() != ' ')) {
-				output += ' ';
+		output = (infile[i].getDurationFromStart() + infile[i].getDuration()).getFloat();
+		break;
+	}
+	return output;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_melisma::getMelismaNoteCounts --
+//
+
+void Tool_melisma::getMelismaNoteCounts(vector<int>& ncounts, vector<int>& mcounts, HumdrumFile& infile) {
+	ncounts.resize(infile.getTrackCount() + 1);
+	mcounts.resize(infile.getTrackCount() + 1);
+	fill(ncounts.begin(), ncounts.end(), 0);
+	fill(mcounts.begin(), mcounts.end(), 0);
+	vector<HTp> starts = infile.getKernSpineStartList();
+	for (int i=0; i<(int)starts.size(); i++) {
+		HTp current = starts[i];
+		int track = current->getTrack();
+		while (current) {
+			if (!current->isData()) {
+				current = current->getNextToken();
+				continue;
 			}
-		} else if (input[i] == '\n') {
-			if ((!output.empty()) && (output.back() != ' ')) {
-				output += ' ';
+			if (current->isNull()) {
+				current = current->getNextToken();
+				continue;
 			}
-		} else if (input[i] == ' ') {
-			if ((!output.empty()) && (output.back() != ' ')) {
-				output += ' ';
+			if (current->isRest()) {
+				current = current->getNextToken();
+				continue;
 			}
-		} else {
-			output += input[i];
+			if (!current->isNoteAttack()) {
+				current = current->getNextToken();
+				continue;
+			}
+			ncounts[track]++;
+			if (current->find("@") != string::npos) {
+				mcounts[track]++;
+			}
+			current = current->getNextToken();
+		}
+	}
+
+	for (int i=1; i<(int)mcounts.size(); i++) {
+		mcounts[0] += mcounts[i];
+		ncounts[0] += ncounts[i];
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_melisma::extractWordlist --
+//
+
+void Tool_melisma::extractWordlist(vector<WordInfo>& wordinfo, map<string, int>& wordlist,
+		HumdrumFile& infile, vector<vector<int>>& notecount) {
+	int mincount = getInteger("min");
+	if (mincount < 2) {
+		mincount = 2;
+	}
+	string word;
+	WordInfo winfo;
+	for (int i=0; i<(int)notecount.size(); i++) {
+		for (int j=0; j<(int)notecount[i].size(); j++) {
+			if (notecount[i][j] < mincount) {
+				continue;
+			}
+			HTp token = infile.token(i, j);
+			word = extractWord(winfo, token, notecount);
+			wordlist[word]++;
+			int track = token->getTrack();
+			winfo.name = m_names[track];
+			winfo.abbreviation = m_abbreviations[track];
+			winfo.partnum = m_partnums[track];
+			wordinfo.push_back(winfo);
 		}
 	}
-	while ((!output.empty()) && (output.back() == ' ')) {
-		output.pop_back();
-	}
-
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::cleanDirText -- convert ":" to "&colon;".
-//     Remove tabs and newlines, and trim spaces.  Maybe allow
-//     newlines using "\n" and allow font changes in the future.
-//     Remove redundant whitespace. Do accents later perhaps or
-//     monitor for UTF-8.
+// Tool_melisma::extractWord --
 //
 
-string Tool_mei2hum::cleanDirText(const string& input) {
-	string output;
-	output.reserve(input.size() + 8);
-	bool foundstart = false;
-	for (int i=0; i<(int)input.size(); i++) {
-		if ((!foundstart) && std::isspace(input[i])) {
+string Tool_melisma::extractWord(WordInfo& winfo, HTp token, vector<vector<int>>& counts) {
+	winfo.clear();
+	string output = *token;
+	string syllable;
+	HTp current = token;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getPreviousToken();
 			continue;
 		}
-		foundstart = true;
-		if (input[i] == ':') {
-			output += "&colon;";
-		} else if (input[i] == '\t') {
-			if ((!output.empty()) && (output.back() != ' ')) {
-				output += ' ';
-			}
-		} else if (input[i] == '\n') {
-			if ((!output.empty()) && (output.back() != ' ')) {
-				output += ' ';
-			}
-		} else if (input[i] == ' ') {
-			if ((!output.empty()) && (output.back() != ' ')) {
-				output += ' ';
+		if (current->isNull()) {
+			current = current->getPreviousToken();
+			continue;
+		}
+		syllable = *current;
+		auto pos = syllable.rfind(" ");
+		if (pos != string::npos) {
+			syllable = syllable.substr(pos + 1);
+		}
+		if (syllable.size() > 0) {
+			if (syllable.at(0) == '-') {
+				current = current->getPreviousToken();
+				continue;
+			} else {
+				// found start of word
+				break;
 			}
 		} else {
-			output += input[i];
+			// some strange problem
+			break;
 		}
 	}
-	while ((!output.empty()) && (output.back() == ' ')) {
-		output.pop_back();
+	if (!current) {
+		// strange problem (no start of word)
+		return "";
+	}
+	if (syllable.size() == 0) {
+		return "";
 	}
 
-	return output;
-}
+	winfo.starttime = current->getDurationFromStart();
+	int line = current->getLineIndex();
+	int field = current->getFieldIndex();
+	winfo.endtime = m_endtimes[line][field];
+	winfo.bar = m_measures[line];
 
+	transform(syllable.begin(), syllable.end(), syllable.begin(), ::tolower);
+	if (syllable.back() == '-') {
+		syllable.resize(syllable.size() - 1);
+		winfo.syllables.push_back(syllable);
+		winfo.starttimes.push_back(current->getDurationFromStart());
+		winfo.endtimes.push_back(m_endtimes[line][field]);
+		winfo.notecounts.push_back(counts[line][field]);
+		winfo.bars.push_back(m_measures[line]);
+	} else {
+		// single-syllable word
+		winfo.endtime = getEndtime(current);
+		transform(syllable.begin(), syllable.end(), syllable.begin(), ::tolower);
+		winfo.word = syllable;
+		winfo.syllables.push_back(syllable);
+		winfo.starttimes.push_back(current->getDurationFromStart());
+		winfo.endtimes.push_back(m_endtimes[line][field]);
+		winfo.notecounts.push_back(counts[line][field]);
+		winfo.bars.push_back(m_measures[line]);
+		return syllable;
+	}
+	output = syllable;
+	HumRegex hre;
 
+	current = current->getNextToken();
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+		syllable = *current;
 
-//////////////////////////////
-//
-// Tool_mei2hum::cleanVerseText --
-//     Remove tabs and newlines, and trim spaces.
-//     Do accents later perhaps or monitor for UTF-8.
-//
+		auto pos = syllable.find(" ");
+		if (pos != string::npos) {
+			syllable = syllable.substr(0, pos);
+		}
 
-string Tool_mei2hum::cleanVerseText(const string& input) {
-	string output;
-	output.reserve(input.size() + 8);
-	bool foundstart = false;
-	for (int i=0; i<(int)input.size(); i++) {
-		if ((!foundstart) && std::isspace(input[i])) {
-			continue;
+		// if there is an elision of words and the second word is more
+		// than one syllable, then end the word at the apostrophe.
+		pos = syllable.find("'");
+		if (pos != string::npos) {
+			if (syllable.back() == '-') {
+				syllable = syllable.substr(0, pos+1);
+			}
 		}
-		foundstart = true;
-		if (input[i] == '\t') {
-			output += ' ';
-		} else if (input[i] == '\n') {
-			output += ' ';
+
+		if (syllable.size() == 0) {
+			// strange problem
+			return "";
+		}
+		if (syllable.at(0) != '-') {
+			// word was not terminated properly?
+			cerr << "Syllable error at syllable : " << syllable;
+			cerr << ", line: " << current->getLineNumber();
+			cerr << ", field: " << current->getFieldNumber();
+			cerr << endl;
 		} else {
-			output += input[i];
+			syllable = syllable.substr(1);
+		}
+		transform(syllable.begin(), syllable.end(), syllable.begin(), ::tolower);
+		winfo.endtime = getEndtime(current);
+		hre.replaceDestructive(syllable, "", "[<>.:?!;,\"]", "g");
+		winfo.syllables.push_back(syllable);
+		winfo.starttimes.push_back(current->getDurationFromStart());
+		int cline = current->getLineIndex();
+		int cfield = current->getFieldIndex();
+		winfo.endtimes.push_back(m_endtimes[cline][cfield]);
+		winfo.notecounts.push_back(counts[cline][cfield]);
+		winfo.bars.push_back(m_measures[cline]);
+		output += syllable;
+		if (output.back() == '-') {
+			output.resize(output.size() - 1);
+			current = current->getNextToken();
+			winfo.syllables.back().resize((int)winfo.syllables.back().size() - 1);
+			continue;
+		} else {
+			// last syllable in word
+			break;
 		}
-	}
-	while ((!output.empty()) && (output.back() == ' ')) {
-		output.pop_back();
 	}
 
+	winfo.word = output;
 	return output;
 }
 
@@ -96695,418 +100258,248 @@ string Tool_mei2hum::cleanVerseText(const string& input) {
 
 //////////////////////////////
 //
-// Tool_mei2hum::cleanReferenceRecordText -- convert ":" to "&colon;".
-//     Remove tabs and newlines, and trim spaces.  Maybe allow
-//     newlines using "\n" and allow font changes in the future.
-//     Do accents later perhaps or monitor for UTF-8.
+// Tool_melisma::getEndtime --
 //
 
-string Tool_mei2hum::cleanReferenceRecordText(const string& input) {
-	string output;
-	output.reserve(input.size() + 8);
-	bool foundstart = false;
-	char lastchar = '\0';
-	for (int i=0; i<(int)input.size(); i++) {
-		if ((!foundstart) && std::isspace(input[i])) {
-			continue;
-		}
-		foundstart = true;
-		if (input[i] == '\n') {
-			if (lastchar != ' ') {
-				output += ' ';
-			}
-			lastchar = ' ';
-		} else if (input[i] == '\t') {
-			if (lastchar != ' ') {
-				output += ' ';
+HumNum Tool_melisma::getEndtime(HTp text) {
+	int line = text->getLineIndex();
+	int field = text->getFieldIndex();
+	return m_endtimes[line][field];
+}
+
+
+
+/////////////////////////////
+//
+// Tool_melisma::markMelismas --
+//
+
+void Tool_melisma::markMelismas(HumdrumFile& infile, vector<vector<int>>& counts) {
+	int mincount = getInteger("min");
+	if (mincount < 2) {
+		mincount = 2;
+	}
+	for (int i=0; i<(int)counts.size(); i++) {
+		for (int j=0; j<(int)counts[i].size(); j++) {
+			if (counts[i][j] >= mincount) {
+				HTp token = infile.token(i, j);
+				markMelismaNotes(token, counts[i][j]);
 			}
-			lastchar = ' ';
-		} else {
-			output += input[i];
-			lastchar = input[i];
 		}
 	}
-	while ((!output.empty()) && (output.back() == ' ')) {
-		output.pop_back();
-	}
-
-	return output;
+	infile.appendLine("!!!RDF**kern: @ = marked note (melisma)");
+	infile.createLinesFromTokens();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseTempo --
-//
-// Example:
-//   <tempo tstamp="1" place="above" staff="1">
-//      1 - Allegro con spirito <rend fontname="VerovioText">&#xE1D5;</rend> = 132
-//   </tempo>
-//
-//
-// Ways of indicating tempo:
-//
-// tempo@midi.bpm == tempo per quarter note (Same as Humdrum *MM value)
-//
-// tempo@midi.mspb == microseconds per quarter note ( bpm = mspb * 60 / 1000000)
-//
-// tempo@mm == tempo per beat (bpm = mm / unit(dots))
-// tempo@mm.unit == beat unit for tempo@mm
-// tempo@mm.dots == dots for tempo@unit
-//
-// Free-form text:
-//
-// &#xE1D5; == quarter note
+// Tool_melisma::markMelismaNotes --
 //
-// #define SMUFL_QUARTER_NOTE "\ue1d5"
-
-void Tool_mei2hum::parseTempo(xml_node tempo, HumNum starttime) {
-	NODE_VERIFY(tempo, )
 
-	bool found = false;
-	double value = 0.0;
+void Tool_melisma::markMelismaNotes(HTp text, int count) {
+	int counter = 0;
 
-	xml_attribute bpm = tempo.attribute("bpm");
-	if (bpm) {
-		value = bpm.as_double();
-		if (value > 0.0) {
-			found = true;
+	HTp current = text->getPreviousFieldToken();
+	while (current) {
+		if (current->isKern()) {
+			break;
 		}
+		current = current->getPreviousFieldToken();
 	}
-
-	if (!found) {
-		xml_attribute mspb   = tempo.attribute("mspb");
-		value = mspb.as_double() * 60.0 / 1000000.0;
-		if (value > 0.0) {
-			found = true;
-		}
+	if (!current) {
+		return;
 	}
-
-	if (!found) {
-		xml_attribute mm     = tempo.attribute("mm");
-		xml_attribute mmunit = tempo.attribute("mm.unit");
-		xml_attribute mmdots = tempo.attribute("mm.dots");
-		value = mm.as_double();
-		string recip = mmunit.value();
-		int dcount = mmdots.as_int();
-		for (int i=0; i<dcount; i++) {
-			recip += '.';
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		HumNum duration = Convert::recipToDuration(recip);
-		value *= duration.getFloat();
-		if (value > 0.0) {
-			found = true;
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
 		}
-	}
-
-	if (!found) {
-		// search for free-form tempo marking.  Something like:
-		//   <tempo tstamp="1" place="above" staff="1">
-		//      1 - Allegro con spirito <rend fontname="VerovioText">&#xE1D5;</rend> = 132
-		//   </tempo>
-		//
-		// UTF-8 version in string "\ue1d5";
-		string text;
-
-		MAKE_CHILD_LIST(children, tempo);
-		for (int i=0; i<(int)children.size(); i++) {
-			if (children[i].type() == pugi::node_pcdata) {
-				text += children[i].value();
-			} else {
-				text += children[i].child_value();
-			}
-			text += " ";
-
+		if (current->isRest()) {
+			current = current->getNextToken();
+			continue;
 		}
-		HumRegex hre;
-		// #define SMUFL_QUARTER_NOTE "\ue1d5"
-		// if (hre.search(text, SMUFL_QUARTER_NOTE "\\s*=\\s*(\\d+\\.?\\d*)")) {
-		if (hre.search(text, "\\s*=\\s*(\\d+\\.?\\d*)")) {
-			// assuming quarter note for now.
-			value = hre.getMatchDouble(1);
-			found = true;
+		if (current->isNoteAttack()) {
+			counter++;
 		}
-		// further rhythmic values for tempo should go here.
-	}
-
-	// also deal with tempo designiations such as "Allegro"...
-
-	if (!found) {
-		// no tempo to set
-		return;
-	}
-
-	// insert tempo
-	GridMeasure* gm = m_outdata.back();
-	GridSlice* gs = new GridSlice(gm, starttime, SliceType::Tempos, m_maxStaffInFile);
-	stringstream stok;
-	stok << "*MM" << value;
-	string token = stok.str();
-
-	for (int i=0; i<m_maxStaffInFile; i++) {
-		gs->at(i)->at(0)->at(0)->setToken(token);
-	}
-
-	// insert after time signature at same timestamp if possible
-	bool inserted = false;
-	for (auto it = gm->begin(); it != gm->end(); it++) {
-		if ((*it)->getTimestamp() > starttime) {
-			gm->insert(it, gs);
-			inserted = true;
-			break;
-		} else if ((*it)->isTimeSigSlice()) {
-			it++;
-			gm->insert(it, gs);
-			inserted = true;
-			break;
-		} else if (((*it)->getTimestamp() == starttime) && ((*it)->isNoteSlice()
-				|| (*it)->isGraceSlice())) {
-			gm->insert(it, gs);
-			inserted = true;
+		string text = *current;
+		text += "@";
+		current->setText(text);
+		if (counter >= count) {
 			break;
 		}
+		current = current->getNextToken();
 	}
-
-	if (!inserted) {
-		gm->push_back(gs);
-	}
-
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mei2hum::parseHarm -- Not yet ready to convert <harm> data.
-//    There will be different types of harm (such as figured bass), which
-//    will need to be subcategorized into different datatypes, such as
-//    *fb for figured bass.  Also free-text can be present in <harm>
-//    data, so the current datatype for that is **cdata  (meaning chord-like
-//    data that will be mapped back into <harm> which converting back to
-//    MEI data.
-//
-// Example:
-//     <harm staff="1" tstamp="1.000000">C major</harm>
+// Tool_melisma::replaceLyrics --
 //
 
-void Tool_mei2hum::parseHarm(xml_node harm, HumNum starttime) {
-	NODE_VERIFY(harm, )
-	MAKE_CHILD_LIST(children, harm);
-
-	string text = harm.child_value();
-
-	if (text.empty()) { // looking at <rend> sub-elements
-		int count = 0;
-		for (int i=0; i<(int)children.size(); i++) {
-			string nodename = children[i].name();
-			if (nodename == "rend") {
-				if (count) {
-					text += " ";
-				}
-				count++;
-				text += children[i].child_value();
-				//if (strcmp(children[i].attribute("fontstyle").value(), "normal") == 0) {
-				//	font = "";  // normal is default in Humdrum layout
-				//}
-				//if (strcmp(children[i].attribute("fontweight").value(), "bold") == 0) {
-				//	font += "B";  // normal is default in Humdrum layout
-				//}
-			} else if (nodename == "") {
-				// text node
-				if (count) {
-					text += " ";
-				}
-				count++;
-				text += children[i].value();
-			} else {
-				cerr << DKHTP << harm.name() << "/" << nodename << CURRLOC << endl;
+void Tool_melisma::replaceLyrics(HumdrumFile& infile, vector<vector<int>>& counts) {
+	for (int i=0; i<(int)counts.size(); i++) {
+		for (int j=0; j<(int)counts[i].size(); j++) {
+			if (counts[i][j] == -1) {
+				continue;
 			}
+			string text = to_string(counts[i][j]);
+			HTp token = infile.token(i, j);
+			token->setText(text);
 		}
 	}
+	infile.createLinesFromTokens();
+}
 
-	if (text.empty()) {
-		return;
-	}
-
-   // cerr << "FOUND HARM DATA " << text << endl;
 
-/*
 
-	string startid = harm.attribute("startid").value();
+//////////////////////////////
+//
+// Tool_melisma::getNoteCounts --
+//
 
-	int staffnum = harm.attribute("staff").as_int();
-	if (staffnum == 0) {
-		cerr << "Error: staff number required on harm element" << endl;
-		return;
+void Tool_melisma::getNoteCounts(HumdrumFile& infile, vector<vector<int>>& counts) {
+	infile.initializeArray(counts, -1);
+	initBarlines(infile);
+	HumNum negativeOne = -1;
+	infile.initializeArray(m_endtimes, negativeOne);
+	vector<HTp> lyrics;
+	infile.getSpineStartList(lyrics, "**text");
+	for (int i=0; i<(int)lyrics.size(); i++) {
+		getNoteCountsForLyric(counts, lyrics[i]);
 	}
-	double meterunit = m_currentMeterUnit[staffnum - 1];
+}
 
-	if (!startid.empty()) {
-		// Harmony is (or at least should) be attached directly
-		// do a note, so it is handled elsewhere.
-		cerr << "Warning DYNAMIC " << text << " is not yet processed." << endl;
-		return;
-	}
 
-	string ts = harm.attribute("tstamp").value();
-	if (ts.empty()) {
-		cerr << "Error: no timestamp on harm element" << endl;
-		return;
-	}
-	double tsd = (stof(ts)-1) * 4.0 / meterunit;
-	double tolerance = 0.001;
-	GridMeasure* gm = m_outdata.back();
-	double tsm = gm->getTimestamp().getFloat();
-	bool foundslice = false;
-	GridSlice *nextgs = NULL;
-	for (auto gs : *gm) {
-		if (!gs->isDataSlice()) {
-			continue;
-		}
-		double gsts = gs->getTimestamp().getFloat();
-		double difference = (gsts-tsm) - tsd;
-		if (difference < tolerance) {
-			// did not find data line at exact timestamp, so move
-			// the harm to the next event. Need to think about adding
-			// a new timeslice for the harm when it is not attached to
-			// a note.
-			nextgs = gs;
-			break;
-		}
-		if (!(fabs(difference) < tolerance)) {
-			continue;
-		}
-		GridPart* part = gs->at(staffnum-1);
-		part->setHarmony(text);
-		m_outdata.setHarmonyPresent(staffnum-1);
-		foundslice = true;
-		break;
-	}
-	if (!foundslice) {
-		if (nextgs == NULL) {
-			cerr << "Warning: harmony not attched to system events "
-					<< "are not yet supported in measure " << m_currentMeasure << endl;
-		} else {
-			GridPart* part = nextgs->at(staffnum-1);
-			part->setHarmony(text);
-			m_outdata.setHarmonyPresent(staffnum-1);
-			// Give a time offset for displaying the harmmony here.
+
+//////////////////////////////
+//
+// Tool_melisma::initBarlines --
+//
+
+void Tool_melisma::initBarlines(HumdrumFile& infile) {
+	m_measures.resize(infile.getLineCount());
+	fill(m_measures.begin(), m_measures.end(), 0);
+	HumRegex hre;
+	for (int i=1; i<infile.getLineCount(); i++) {
+		if (!infile[i].isBarline()) {
+			m_measures[i] = m_measures[i-1];
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		if (hre.search(token, "(\\d+)")) {
+			m_measures[i] = hre.getMatchInt(1);
 		}
 	}
-*/
-
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_mei2hum::parseDynam --
-//
-// Example:
-//     <dynam staff="1" tstamp="1.000000">p</dynam>
+// Tool_melisma::getNoteCountsForLyric --
 //
 
-void Tool_mei2hum::parseDynam(xml_node dynam, HumNum starttime) {
-	NODE_VERIFY(dynam, )
-	MAKE_CHILD_LIST(children, dynam);
-
-	string text = dynam.child_value();
-
-	if (text.empty()) { // looking at <rend> sub-elements
-		int count = 0;
-		for (int i=0; i<(int)children.size(); i++) {
-			string nodename = children[i].name();
-			if (nodename == "rend") {
-				if (count) {
-					text += " ";
-				}
-				count++;
-				text += children[i].child_value();
-				//if (strcmp(children[i].attribute("fontstyle").value(), "normal") == 0) {
-				//	font = "";  // normal is default in Humdrum layout
-				//}
-				//if (strcmp(children[i].attribute("fontweight").value(), "bold") == 0) {
-				//	font += "B";  // normal is default in Humdrum layout
-				//}
-			} else if (nodename == "") {
-				// text node
-				if (count) {
-					text += " ";
-				}
-				count++;
-				text += children[i].value();
-			} else {
-				cerr << DKHTP << dynam.name() << "/" << nodename << CURRLOC << endl;
-			}
+void Tool_melisma::getNoteCountsForLyric(vector<vector<int>>& counts, HTp lyricStart) {
+	HTp current = lyricStart;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
 		}
+		int line = current->getLineIndex();
+		int field = current->getFieldIndex();
+		counts[line][field] = getCountForSyllable(current);
+		current = current->getNextToken();
 	}
+}
 
-	if (text.empty()) {
-		return;
-	}
 
-	string startid = dynam.attribute("startid").value();
 
-	int staffnum = dynam.attribute("staff").as_int();
-	if (staffnum == 0) {
-		cerr << "Error: staff number required on dynam element" << endl;
-		return;
-	}
-	double meterunit = m_currentMeterUnit[staffnum - 1];
+//////////////////////////////
+//
+// Tool_melisma::getCountForSyllable --
+//
 
-	if (!startid.empty()) {
-		// Dynamic is (or at least should) be attached directly
-		// do a note, so it is handled elsewhere.
-		cerr << "Warning DYNAMIC " << text << " is not yet processed." << endl;
-		return;
+int Tool_melisma::getCountForSyllable(HTp token) {
+	if (token->back() == '&') {
+		return 1;
+	}
+	HTp nexttok = token->getNextToken();
+	int eline   = token->getLineIndex();
+	int efield  = token->getFieldIndex();
+	m_endtimes[eline][efield] = token->getDurationFromStart() + token->getDuration();
+	while (nexttok) {
+		if (!nexttok->isData()) {
+			nexttok = nexttok->getNextToken();
+			continue;
+		}
+		if (nexttok->isNull()) {
+			nexttok = nexttok->getNextToken();
+			continue;
+		}
+		// found non-null data token
+		break;
 	}
 
-	string ts = dynam.attribute("tstamp").value();
-	if (ts.empty()) {
-		cerr << "Error: no timestamp on dynam element" << endl;
-		return;
+	HumdrumFile& infile = *token->getOwner()->getOwner();
+	int endline = infile.getLineCount() - 1;
+	if (nexttok) {
+		endline = nexttok->getLineIndex();
 	}
-	double tsd = (stof(ts)-1) * 4.0 / meterunit;
-	double tolerance = 0.001;
-	GridMeasure* gm = m_outdata.back();
-	double tsm = gm->getTimestamp().getFloat();
-	bool foundslice = false;
-	GridSlice *nextgs = NULL;
-	for (auto gs : *gm) {
-		if (!gs->isDataSlice()) {
+	int output = 0;
+	HTp current = token->getPreviousFieldToken();
+	while (current) {
+		if (current->isKern()) {
+			break;
+		}
+		current = current->getPreviousFieldToken();
+	}
+	if (!current) {
+		return 0;
+	}
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
 			continue;
 		}
-		double gsts = gs->getTimestamp().getFloat();
-		double difference = (gsts-tsm) - tsd;
-		if (difference < tolerance) {
-			// did not find data line at exact timestamp, so move
-			// the dynamic to the next event. Maybe think about adding
-			// a new timeslice for the dynamic.
-			nextgs = gs;
-			break;
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
 		}
-		if (!(fabs(difference) < tolerance)) {
+		if (current->isRest()) {
+			current = current->getNextToken();
 			continue;
 		}
-		GridPart* part = gs->at(staffnum-1);
-		part->setDynamics(text);
-		m_outdata.setDynamicsPresent(staffnum-1);
-		foundslice = true;
-		break;
-	}
-	if (!foundslice) {
-		if (nextgs == NULL) {
-			cerr << "Warning: dynamics not attched to system events "
-					<< "are not yet supported in measure " << m_currentMeasure << endl;
+		if (!current->isNoteAttack()) {
+			// ignore tied notes
+			m_endtimes[eline][efield] = current->getDurationFromStart() + current->getDuration();
+			current = current->getNextToken();
+			continue;
+		}
+		int line = current->getLineIndex();
+		if (line < endline) {
+			m_endtimes[eline][efield] = current->getDurationFromStart() + current->getDuration();
+			output++;
 		} else {
-			GridPart* part = nextgs->at(staffnum-1);
-			part->setDynamics(text);
-			m_outdata.setDynamicsPresent(staffnum-1);
-			// Give a time offset for displaying the dynamic here.
+			break;
 		}
+		current = current->getNextToken();
 	}
+
+	return output;
 }
 
 
@@ -97115,25 +100508,21 @@ void Tool_mei2hum::parseDynam(xml_node dynam, HumNum starttime) {
 
 /////////////////////////////////
 //
-// Tool_gridtest::Tool_melisma -- Set the recognized options for the tool.
+// Tool_mens2kern::Tool_mens2kern -- Set the recognized options for the tool.
 //
 
-Tool_melisma::Tool_melisma(void) {
-	define("m|min=i:2",        "minimum length to identify as a melisma");
-	define("r|replace=b",      "replace lyrics with note counts");
-	define("a|average|avg=b",  "calculate note-to-syllable ratio");
-	define("w|words=b",        "list words that contain a melisma");
-	define("p|part=b",         "also calculate note-to-syllable ratios by part");
+Tool_mens2kern::Tool_mens2kern(void) {
+	define("debug=b",    "print debugging statements");
 }
 
 
 
-///////////////////////////////
+/////////////////////////////////
 //
-// Tool_melisma::run -- Primary interfaces to the tool.
+// Tool_mens2kern::run -- Do the main work of the tool.
 //
 
-bool Tool_melisma::run(HumdrumFileSet& infiles) {
+bool Tool_mens2kern::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -97142,20 +100531,31 @@ bool Tool_melisma::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_melisma::run(const string& indata, ostream& out) {
+bool Tool_mens2kern::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
-	return run(infile, out);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
 }
 
 
-bool Tool_melisma::run(HumdrumFile& infile, ostream& out) {
+bool Tool_mens2kern::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
 	return status;
 }
 
 
-bool Tool_melisma::run(HumdrumFile& infile) {
-   initialize(infile);
+bool Tool_mens2kern::run(HumdrumFile& infile) {
+	initialize();
 	processFile(infile);
 	return true;
 }
@@ -97164,544 +100564,691 @@ bool Tool_melisma::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_melisma::initialize --
+// Tool_mens2kern::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_melisma::initialize(HumdrumFile& infile) {
-	// do nothing for now
+void Tool_mens2kern::initialize(void) {
+	m_debugQ = getBoolean("debug");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::processFile --
+// Tool_mens2kern::processFile --
 //
 
-void Tool_melisma::processFile(HumdrumFile& infile) {
-	vector<vector<int>> notecount;
-	getNoteCounts(infile, notecount);
-	vector<WordInfo> wordinfo;
-	wordinfo.reserve(1000);
-	map<string, int> wordlist;
-	initializePartInfo(infile);
-
-	if (getBoolean("replace")) {
-		replaceLyrics(infile, notecount);
-	} else if (getBoolean("words")) {
-		markMelismas(infile, notecount);
-		extractWordlist(wordinfo, wordlist, infile, notecount);
-		printWordlist(infile, wordinfo, wordlist);
-	} else {
-		markMelismas(infile, notecount);
+void Tool_mens2kern::processFile(HumdrumFile& infile) {
+	vector<HTp> melody;
+	int scount = infile.getStrandCount();
+	for (int i=0; i<scount; i++) {
+		HTp sstart = infile.getStrandBegin(i);
+		if (!sstart->isDataType("**mens")) {
+			continue;
+		}
+		HTp sstop = infile.getStrandEnd(i);
+		HTp current = sstart;
+		while (current && (current != sstop)) {
+			if (current->isNull()) {
+				// ignore null data tokens
+				current = current->getNextToken();
+				continue;
+			}
+			melody.push_back(current);
+			current = current->getNextToken();
+		}
+		processMelody(melody);
+		melody.clear();
 	}
 
+	infile.createLinesFromTokens();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::initializePartInfo --
+// Tool_mens2kern::processMelody --
 //
 
-void Tool_melisma::initializePartInfo(HumdrumFile& infile) {
-	m_names.clear();
-	m_abbreviations.clear();
-	m_partnums.clear();
+void Tool_mens2kern::processMelody(vector<HTp>& melody) {
+	int maximodus = 0;
+	int modus = 0;
+	int tempus = 0;
+	int prolatio = 0;
+	int semibrevis_def = 0;
+	int brevis_def     = 0;
+	int longa_def      = 0;
+	int maxima_def     = 0;
+	string regexopts;
+	HumRegex hre;
+	string rhythm;
+	bool imperfecta;
+	bool perfecta;
+	bool altera;
 
-	m_names.resize(infile.getTrackCount() + 1);
-	m_abbreviations.resize(infile.getTrackCount() + 1);
-	m_partnums.resize(infile.getTrackCount() + 1);
-	fill(m_partnums.begin(), m_partnums.end(), -1);
+	for (int i=0; i<(int)melody.size(); i++) {
+		if (*melody[i] == "**mens") {
+			// convert spine to **kern data:
+			melody[i]->setText("**kern");
+		}
 
-	vector<HTp> starts;
-	infile.getSpineStartList(starts);
-	int ktrack = 0;
-	int track = 0;
-	int part = 0;
-	for (int i=0; i<(int)starts.size(); i++) {
-		track = starts[i]->getTrack();
-		if (starts[i]->isKern()) {
-			ktrack = track;
-			part++;
-			m_partnums[ktrack] = part;
-			HTp current = starts[i];
-			while (current) {
-				if (current->isData()) {
-					break;
-				}
-				if (current->compare(0, 3, "*I\"") == 0) {
-					m_names[ktrack] = current->substr(3);
-				} else if (current->compare(0, 3, "*I\'") == 0) {
-					m_abbreviations[ktrack] = current->substr(3);
-				}
-				current = current->getNextToken();
+		if (melody[i]->isMensuration()) {
+			getMensuralInfo(melody[i], maximodus, modus, tempus, prolatio);
+
+			// Default value of notes from maxima to semibrevis in minims:
+			semibrevis_def = prolatio;
+			brevis_def     = tempus    * semibrevis_def;
+			longa_def      = modus     * brevis_def;
+			maxima_def     = maximodus * longa_def;
+			if (m_debugQ) {
+				cerr << "LEVELS X_def = "    << maxima_def
+					  << " | L_def = " << longa_def
+					  << " | S_def = " << brevis_def
+					  << " | s_def = " << semibrevis_def << endl;
 			}
-		} else if (ktrack) {
-			m_names[track] = m_names[ktrack];
-			m_abbreviations[track] = m_abbreviations[ktrack];
-			m_partnums[track] = m_partnums[ktrack];
 		}
+
+		if (!melody[i]->isData()) {
+			continue;
+		}
+		string text = melody[i]->getText();
+		imperfecta = hre.search(text, "i") ? true : false;
+		perfecta = hre.search(text, "p") ? true : false;
+		altera = hre.search(text, "\\+") ? true : false;
+		if (hre.search(text, "([XLSsMmUu])")) {
+			rhythm = hre.getMatch(1);
+		} else {
+			cerr << "Error: token " << melody[i] << " has no rhythm" << endl;
+			cerr << "   ON LINE: "  << melody[i]->getLineNumber()    << endl;
+			continue;
+		}
+
+		string kernRhythm = mens2kernRhythm(rhythm, altera, perfecta, imperfecta, maxima_def, longa_def, brevis_def, semibrevis_def);
+
+		hre.replaceDestructive(text, kernRhythm, rhythm);
+		// Remove any dot of division/augmentation
+		hre.replaceDestructive(text, "", ":");
+		// remove perfection/imperfection/alteration markers
+		hre.replaceDestructive(text, "", "[pi\\+]");
+		if (text.empty()) {
+			text = ".";
+		}
+		melody[i]->setText(text);
+	}
+}
+
+
+//////////////////////////////
+//
+// Tool_mens2kern::getMensuralInfo --
+//
+
+void Tool_mens2kern::getMensuralInfo(HTp token, int& maximodus, int& modus,
+		int& tempus, int& prolatio) {
+	HumRegex hre;
+	if (!hre.search(token, "^\\*met\\(.*?\\)_(\\d+)")) {
+		// need to interpret symbols without underscores.
+		if (token->getText() == "*met(C)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 2;
+			prolatio = 2;
+		} else if (token->getText() == "*met(O)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 3;
+			prolatio = 2;
+		} else if (token->getText() == "*met(C.)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 2;
+			prolatio = 3;
+		} else if (token->getText() == "*met(O.)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 3;
+			prolatio = 3;
+		} else if (token->getText() == "*met(C|)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 2;
+			prolatio = 2;
+		} else if (token->getText() == "*met(O|)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 3;
+			prolatio = 2;
+		} else if (token->getText() == "*met(C.|)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 2;
+			prolatio = 3;
+		} else if (token->getText() == "*met(O.|)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 3;
+			prolatio = 3;
+		} else if (token->getText() == "*met(C2)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 2;
+			prolatio = 2;
+		} else if (token->getText() == "*met(C3)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 3;
+			prolatio = 2;
+		} else if (token->getText() == "*met(O2)") {
+			maximodus = 2;
+			modus = 3;
+			tempus = 2;
+			prolatio = 2;
+		} else if (token->getText() == "*met(O3)") {
+			maximodus = 3;
+			modus = 3;
+			tempus = 3;
+			prolatio = 2;
+		} else if (token->getText() == "*met(C3/2)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 2;
+			prolatio = 3;
+		} else if (token->getText() == "*met(C|3/2)") {
+			maximodus = 2;
+			modus = 2;
+			tempus = 3;
+			prolatio = 2;
+		}
+	} else {
+		string levels = hre.getMatch(1);
+		if (levels.size() >= 1) {
+			maximodus = levels[0] - '0';
+		}
+		if (levels.size() >= 2) {
+			modus = levels[1] - '0';
+		}
+		if (levels.size() >= 3) {
+			tempus = levels[2] - '0';
+		}
+		if (levels.size() >= 4) {
+			prolatio = levels[3] - '0';
+		}
+	}
+
+	if (m_debugQ) {
+		cerr << "MENSURAL INFO: maximodus = "   << maximodus
+			  << " | modus = "    << modus
+			  << " | tempus = "   << tempus
+			  << " | prolatio = " << prolatio << endl;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_mens2kern::mens2kernRhythm --
+//
+
+string Tool_mens2kern::mens2kernRhythm(const string& rhythm, bool altera, bool perfecta, bool imperfecta, int maxima_def, int longa_def, int brevis_def, int semibrevis_def) {
+	double val_note;
+	double minima_def = 1;
+	double semiminima_def = 0.5;
+	double fusa_def = 0.25;
+	double semifusa_def = 0.125;
+
+	if (rhythm == "X") {
+		if (perfecta) { val_note = 3 * longa_def; }
+		else if (imperfecta) { val_note = 2 * longa_def; }
+		else { val_note = maxima_def; }
+	}
+	else if (rhythm == "L") {
+		if (perfecta) { val_note = 3 * brevis_def; }
+		else if (imperfecta) { val_note = 2 * brevis_def; }
+		else if (altera) { val_note = 2 * longa_def; }
+		else { val_note = longa_def; }
+	}
+	else if (rhythm == "S") {
+		if (perfecta) { val_note = 3 * semibrevis_def; }
+		else if (imperfecta) { val_note = 2 * semibrevis_def; }
+		else if (altera) { val_note = 2 * brevis_def; }
+		else { val_note = brevis_def; }
+	}
+	else if (rhythm == "s") {
+		if (perfecta) { val_note = 3 * minima_def; }
+		else if (imperfecta) { val_note = 2 * minima_def; }
+		else if (altera) { val_note = 2 * semibrevis_def; }
+		else { val_note = semibrevis_def; }
+	}
+	else if (rhythm == "M") {
+		if (perfecta) { val_note = 1.5 * minima_def; }
+		else if (altera) { val_note = 2 * minima_def; }
+		else { val_note = minima_def; }
+	}
+	else if (rhythm == "m") {
+		if (perfecta) { val_note = 1.5 * semiminima_def; }
+		else { val_note = semiminima_def; }
+	}
+	else if (rhythm == "U") {
+		if (perfecta) { val_note = 1.5 * fusa_def; }
+		else { val_note = fusa_def; }
+	}
+	else if (rhythm == "u") {
+		if (perfecta) { val_note = 1.5 * semifusa_def; }
+		else { val_note = semifusa_def; }
+	}
+	else { cerr << "UNKNOWN RHYTHM: " << rhythm << endl; return ""; }
+
+	switch ((int)(val_note * 10000)) {
+		case 1250:     return "16";   break;   // sixteenth note
+		case 1875:     return "16.";  break;   // dotted sixteenth note
+		case 2500:     return "8";    break;   // eighth note
+		case 3750:     return "8.";   break;   // dotted eighth note
+		case 5000:     return "4";    break;   // quarter note
+		case 7500:     return "4.";   break;   // dotted quarter note
+		case 10000:    return "2";    break;   // half note
+		case 15000:    return "2.";   break;   // dotted half note
+		case 20000:    return "1";    break;   // whole note
+		case 30000:    return "1.";   break;   // dotted whole note
+		case 40000:    return "0";    break;   // breve note
+		case 60000:    return "0.";   break;   // dotted breve note
+		case 90000:    return "2%9";  break;   // or ["0.", "1."];
+		case 80000:    return "00";   break;   // long note
+		case 120000:   return "00.";  break;   // dotted long note
+		case 180000:   return "1%9";  break;   // or ["00.", "0."];
+		case 270000:   return "2%27"; break;   // or ["0.", "1.", "0.", "1.", "0.", "1."];
+		case 160000:   return "000";  break;   // maxima note
+		case 240000:   return "000."; break;   // dotted maxima note
+		case 360000:   return "1%18"; break;   // or ["000.", "00."];
+		case 540000:   return "1%27"; break;   // or ["00.", "0.", "00.", "0.", "00.", "0."];
+		case 810000:   return "2%81"; break;   // or ["00.", "0.", "00.", "0.", "00.", "0.", "0.", "1.", "0.", "1.", "0.", "1."];
+		default:
+			cerr << "Error: unknown val_note: " << val_note << endl;
 	}
 
+	return "";
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// printWordlist --
+// Tool_meter::Tool_meter -- Set the recognized options for the tool.
 //
 
-void Tool_melisma::printWordlist(HumdrumFile& infile, vector<WordInfo>& wordinfo,
-		map<string, int> words) {
-
-	// for (auto& item : words) {
-	// 	m_free_text << item.first;
-	// 	if (item.second > 1) {
-	// 		m_free_text << " (" << item.second << ")";
-	// 	}
-	// 	m_free_text << endl;
-	// }
-
-	vector<int> ncounts;
-	vector<int> mcounts;
-	getMelismaNoteCounts(ncounts, mcounts, infile);
+Tool_meter::Tool_meter(void) {
+	define("c|comma=b",                       "display decimal points as commas");
+	define("d|denominator=b",                 "display denominator spine");
+	define("e|eighth=b",                      "metric positions in eighth notes rather than beats");
+	define("f|float=b",                       "floating-point beat values instead of rational numbers");
+	define("h|half=b",                        "metric positions in half notes rather than beats");
+	define("j|join=b",                        "join time signature information and metric positions into a single token");
+	define("n|numerator=b",                   "display numerator spine");
+	define("q|quarter=b",                     "metric positions in quarter notes rather than beats");
+	define("r|rest=b",                        "add meteric positions of rests");
+	define("s|sixteenth=b",                   "metric positions in sixteenth notes rather than beats");
+	define("t|time-signature|tsig|m|meter=b", "display active time signature for each note");
+	define("w|whole=b",                       "metric positions in whole notes rather than beats");
+	define("z|zero=b",                        "start of measure is beat 0 rather than beat 1");
 
-	// m_free_text << "===========================" << endl;
+	define("B|no-beat=b",                     "Do not display metric positions (beats)");
+	define("D|digits=i:0",                    "number of digits after decimal point");
+	define("L|no-label=b",                    "do not add labels to analysis spines");
+}
 
-	std::vector<HTp> kspines = infile.getKernSpineStartList();
 
-	m_free_text << "@@BEGIN:\tMELISMAS\n";
 
-	string filename = infile.getFilename();
-	auto pos = filename.rfind("/");
-	if (pos != string::npos) {
-		filename = filename.substr(pos+1);
-	}
-	m_free_text << "@FILENAME:\t" << filename << endl;
-	m_free_text << "@PARTCOUNT:\t" << kspines.size() << endl;
-	m_free_text << "@WORDCOUNT:\t" << wordinfo.size() << endl;
-	m_free_text << "@SCOREDURATION:\t" << getScoreDuration(infile) << endl;
-	m_free_text << "@NOTES:\t\t" << ncounts[0] << endl;
-	m_free_text << "@MELISMANOTES:\t" << mcounts[0] << endl;
+/////////////////////////////////
+//
+// Tool_meter::run -- Do the main work of the tool.
+//
 
-	m_free_text << "@MELISMASCORE:\t" << int((double)mcounts[0] / (double)ncounts[0] * 1000.0 + 0.5)/10.0 << "%" << endl;
-	for (int i=1; i<(int)m_partnums.size(); i++) {
-		if (m_partnums[i] == 0) {
-			continue;
-		}
-		if (m_partnums[i] == m_partnums[i-1]) {
-			continue;
-		}
-		m_free_text << "@PARTSCORE-" << m_partnums[i] << ":\t" << int((double)mcounts[i] / (double)ncounts[i] * 1000.0 + 0.5)/10.0 << "%" << endl;
+bool Tool_meter::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
+}
 
-	for (int i=1; i<(int)m_partnums.size(); i++) {
-		if (m_partnums[i] == 0) {
-			continue;
-		}
-		if (m_partnums[i] == m_partnums[i-1]) {
-			continue;
-		}
-		m_free_text << "@PARTNAME-" << m_partnums[i] << ":\t" << m_names[i] << endl;
-	}
 
-	for (int i=1; i<(int)m_partnums.size(); i++) {
-		if (m_partnums[i] == 0) {
-			continue;
-		}
-		if (m_partnums[i] == m_partnums[i-1]) {
-			continue;
-		}
-		m_free_text << "@PARTABBR-" << m_partnums[i] << ":\t" << m_abbreviations[i] << endl;
+bool Tool_meter::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
+}
 
-	m_free_text << endl;
-
-	for (int i=0; i<(int)wordinfo.size(); i++) {
-		m_free_text << "@@BEGIN:\tWORD\n";
-		m_free_text << "@PARTNUM:\t" << wordinfo[i].partnum << endl;
-		// m_free_text << "@NAME:\t\t" << wordinfo[i].name << endl;
-		// m_free_text << "@ABBR:\t\t" << wordinfo[i].abbreviation << endl;
-		m_free_text << "@WORD:\t\t" << wordinfo[i].word << endl;
-		m_free_text << "@STARTTIME:\t" << wordinfo[i].starttime.getFloat() << endl;
-		m_free_text << "@ENDTIME:\t" << wordinfo[i].endtime.getFloat() << endl;
-		m_free_text << "@STARTBAR:\t" << wordinfo[i].bar << endl;
-
-		m_free_text << "@SYLLABLES:\t";
-		for (int j=0; j<(int)wordinfo[i].syllables.size(); j++) {
-			m_free_text << wordinfo[i].syllables[j];
-			if (j < (int)wordinfo[i].syllables.size() - 1) {
-				m_free_text << " ";
-			}
-		}
-		m_free_text << endl;
-
-		m_free_text << "@NOTECOUNTS:\t";
-		for (int j=0; j<(int)wordinfo[i].notecounts.size(); j++) {
-			m_free_text << wordinfo[i].notecounts[j];
-			if (j < (int)wordinfo[i].notecounts.size() - 1) {
-				m_free_text << " ";
-			}
-		}
-		m_free_text << endl;
-
-		m_free_text << "@BARLINES:\t";
-		for (int j=0; j<(int)wordinfo[i].bars.size(); j++) {
-			m_free_text << wordinfo[i].bars[j];
-			if (j < (int)wordinfo[i].bars.size() - 1) {
-				m_free_text << " ";
-			}
-		}
-		m_free_text << endl;
-
-		m_free_text << "@STARTTIMES:\t";
-		for (int j=0; j<(int)wordinfo[i].starttimes.size(); j++) {
-			m_free_text << wordinfo[i].starttimes[j].getFloat();
-			if (j < (int)wordinfo[i].starttimes.size() - 1) {
-				m_free_text << " ";
-			}
-		}
-		m_free_text << endl;
-
-		m_free_text << "@ENDTIMES:\t";
-		for (int j=0; j<(int)wordinfo[i].endtimes.size(); j++) {
-			m_free_text << wordinfo[i].endtimes[j].getFloat();
-			if (j < (int)wordinfo[i].endtimes.size() - 1) {
-				m_free_text << " ";
-			}
-		}
-		m_free_text << endl;
 
-		m_free_text << "@@END:\tWORD\n";
-		m_free_text << endl;
+bool Tool_meter::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
+}
 
-	m_free_text << "@@END:\tMELISMAS\n";
-	m_free_text << endl;
+
+bool Tool_meter::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::getScoreDuration --
+// Tool_meter::initialize --
 //
 
-double Tool_melisma::getScoreDuration(HumdrumFile& infile) {
-	double output = 0.0;
-	for (int i=infile.getLineCount() - 1; i>=0; i--) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		output = (infile[i].getDurationFromStart() + infile[i].getDuration()).getFloat();
-		break;
-	}
-	return output;
-}
+void Tool_meter::initialize(void) {
 
+	m_commaQ       = getBoolean("comma");
+	m_denominatorQ = getBoolean("denominator");
+	m_digits       = getInteger("digits");
+	m_floatQ       = getBoolean("float");
+	m_halfQ        = getBoolean("half");
+	m_joinQ        = getBoolean("join");
+	m_nobeatQ      = getBoolean("no-beat");
+	m_nolabelQ     = getBoolean("no-label");
+	m_numeratorQ   = getBoolean("numerator");
+	m_quarterQ     = getBoolean("quarter");
+	m_halfQ        = getBoolean("half");
+	m_eighthQ      = getBoolean("eighth");
+	m_sixteenthQ   = getBoolean("sixteenth");
+	m_restQ        = getBoolean("rest");
+	m_tsigQ        = getBoolean("meter");
+	m_wholeQ       = getBoolean("whole");
+	m_zeroQ        = getBoolean("zero");
 
+	if (m_digits < 0) {
+		m_digits = 0;
+	}
+	if (m_digits > 15) {
+		m_digits = 15;
+	}
 
-//////////////////////////////
-//
-// Tool_melisma::getMelismaNoteCounts --
-//
+	if (m_joinQ && !(m_tsigQ || m_numeratorQ || m_denominatorQ)) {
+		m_tsigQ = true;
+	}
+	if (m_joinQ) {
+		m_nobeatQ = false;
+	}
+	if (m_joinQ && m_numeratorQ && m_denominatorQ) {
+		m_tsigQ = true;
+	}
 
-void Tool_melisma::getMelismaNoteCounts(vector<int>& ncounts, vector<int>& mcounts, HumdrumFile& infile) {
-	ncounts.resize(infile.getTrackCount() + 1);
-	mcounts.resize(infile.getTrackCount() + 1);
-	fill(ncounts.begin(), ncounts.end(), 0);
-	fill(mcounts.begin(), mcounts.end(), 0);
-	vector<HTp> starts = infile.getKernSpineStartList();
-	for (int i=0; i<(int)starts.size(); i++) {
-		HTp current = starts[i];
-		int track = current->getTrack();
-		while (current) {
-			if (!current->isData()) {
-				current = current->getNextToken();
-				continue;
-			}
-			if (current->isNull()) {
-				current = current->getNextToken();
-				continue;
-			}
-			if (current->isRest()) {
-				current = current->getNextToken();
-				continue;
-			}
-			if (!current->isNoteAttack()) {
-				current = current->getNextToken();
-				continue;
-			}
-			ncounts[track]++;
-			if (current->find("@") != string::npos) {
-				mcounts[track]++;
-			}
-			current = current->getNextToken();
-		}
+	if (m_tsigQ) {
+		m_numeratorQ = true;
+		m_denominatorQ = true;
 	}
 
-	for (int i=1; i<(int)mcounts.size(); i++) {
-		mcounts[0] += mcounts[i];
-		ncounts[0] += ncounts[i];
+	// Only one fix-width metric position allowed, prioritize
+	// largest given duration:
+	if (m_wholeQ) {
+		m_halfQ      = false;
+		m_quarterQ   = false;
+		m_eighthQ    = false;
+		m_sixteenthQ = false;
+	} else if (m_halfQ) {
+		m_wholeQ     = false;
+		m_quarterQ   = false;
+		m_eighthQ    = false;
+		m_sixteenthQ = false;
+	} else if (m_quarterQ) {
+		m_wholeQ     = false;
+		m_halfQ      = false;
+		m_eighthQ    = false;
+		m_sixteenthQ = false;
+	} else if (m_eighthQ) {
+		m_wholeQ     = false;
+		m_halfQ      = false;
+		m_quarterQ   = false;
+		m_sixteenthQ = false;
+	} else if (m_sixteenthQ) {
+		m_wholeQ     = false;
+		m_halfQ      = false;
+		m_quarterQ   = false;
+		m_eighthQ    = false;
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::extractWordlist --
+// Tool_meter::processFile --
 //
 
-void Tool_melisma::extractWordlist(vector<WordInfo>& wordinfo, map<string, int>& wordlist,
-		HumdrumFile& infile, vector<vector<int>>& notecount) {
-	int mincount = getInteger("min");
-	if (mincount < 2) {
-		mincount = 2;
-	}
-	string word;
-	WordInfo winfo;
-	for (int i=0; i<(int)notecount.size(); i++) {
-		for (int j=0; j<(int)notecount[i].size(); j++) {
-			if (notecount[i][j] < mincount) {
-				continue;
-			}
-			HTp token = infile.token(i, j);
-			word = extractWord(winfo, token, notecount);
-			wordlist[word]++;
-			int track = token->getTrack();
-			winfo.name = m_names[track];
-			winfo.abbreviation = m_abbreviations[track];
-			winfo.partnum = m_partnums[track];
-			wordinfo.push_back(winfo);
-		}
-	}
+void Tool_meter::processFile(HumdrumFile& infile) {
+	analyzePickupMeasures(infile);
+	getMeterData(infile);
+	printMeterData(infile);
 }
 
 
-
 //////////////////////////////
 //
-// Tool_melisma::extractWord --
+// Tool_meter::analyzePickupMeasures --
 //
 
-string Tool_melisma::extractWord(WordInfo& winfo, HTp token, vector<vector<int>>& counts) {
-	winfo.clear();
-	string output = *token;
-	string syllable;
-	HTp current = token;
+void Tool_meter::analyzePickupMeasures(HumdrumFile& infile) {
+	vector<HTp> sstarts;
+	infile.getKernSpineStartList(sstarts);
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		analyzePickupMeasures(sstarts[i]);
+	}
+}
+
+
+void Tool_meter::analyzePickupMeasures(HTp sstart) {
+	// First dimension are visible barlines.
+	// Second dimension are time signature(s) within the barlines.
+	vector<vector<HTp>> barandtime;
+	barandtime.reserve(1000);
+	barandtime.resize(1);
+	barandtime[0].push_back(sstart);
+	HTp current = sstart->getNextToken();
 	while (current) {
-		if (!current->isData()) {
-			current = current->getPreviousToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getPreviousToken();
-			continue;
-		}
-		syllable = *current;
-		auto pos = syllable.rfind(" ");
-		if (pos != string::npos) {
-			syllable = syllable.substr(pos + 1);
-		}
-		if (syllable.size() > 0) {
-			if (syllable.at(0) == '-') {
-				current = current->getPreviousToken();
+		if (current->isTimeSignature()) {
+			barandtime.back().push_back(current);
+		} else if (current->isBarline()) {
+			if (current->find("-") != std::string::npos) {
+				current = current->getNextToken();
 				continue;
-			} else {
-				// found start of word
-				break;
 			}
-		} else {
-			// some strange problem
+			barandtime.resize(barandtime.size() + 1);
+			barandtime.back().push_back(current);
+		} else if (*current == "*-") {
+			barandtime.resize(barandtime.size() + 1);
+			barandtime.back().push_back(current);
 			break;
 		}
+		current = current->getNextToken();
 	}
-	if (!current) {
-		// strange problem (no start of word)
-		return "";
-	}
-	if (syllable.size() == 0) {
-		return "";
-	}
-
-	winfo.starttime = current->getDurationFromStart();
-	int line = current->getLineIndex();
-	int field = current->getFieldIndex();
-	winfo.endtime = m_endtimes[line][field];
-	winfo.bar = m_measures[line];
 
-	transform(syllable.begin(), syllable.end(), syllable.begin(), ::tolower);
-	if (syllable.back() == '-') {
-		syllable.resize(syllable.size() - 1);
-		winfo.syllables.push_back(syllable);
-		winfo.starttimes.push_back(current->getDurationFromStart());
-		winfo.endtimes.push_back(m_endtimes[line][field]);
-		winfo.notecounts.push_back(counts[line][field]);
-		winfo.bars.push_back(m_measures[line]);
-	} else {
-		// single-syllable word
-		winfo.endtime = getEndtime(current);
-		transform(syllable.begin(), syllable.end(), syllable.begin(), ::tolower);
-		winfo.word = syllable;
-		winfo.syllables.push_back(syllable);
-		winfo.starttimes.push_back(current->getDurationFromStart());
-		winfo.endtimes.push_back(m_endtimes[line][field]);
-		winfo.notecounts.push_back(counts[line][field]);
-		winfo.bars.push_back(m_measures[line]);
-		return syllable;
+	// Extract the actual duration of measures:
+	vector<HumNum> bardur(barandtime.size(), 0);
+	for (int i=0; i<(int)barandtime.size() - 1; i++) {
+		HumNum starttime = barandtime[i][0]->getDurationFromStart();
+		HumNum endtime = barandtime.at(i+1)[0]->getDurationFromStart();
+		HumNum duration = endtime - starttime;
+		bardur.at(i) = duration;
 	}
-	output = syllable;
-	HumRegex hre;
 
-	current = current->getNextToken();
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
+	// Extract the expected duration of measures:
+	vector<HumNum> tsigdur(barandtime.size(), 0);
+	int firstmeasure = -1;
+	HumNum active = 0;
+	for (int i=0; i<(int)barandtime.size() - 1; i++) {
+		if (firstmeasure < 0) {
+			if (bardur.at(i) > 0) {
+				firstmeasure = i;
+			}
 		}
-		if (current->isNull()) {
-			current = current->getNextToken();
+		if (barandtime[i].size() < 2) {
+			tsigdur.at(i) = active;
 			continue;
 		}
-		syllable = *current;
+		active = getTimeSigDuration(barandtime.at(i).at(1));
+		tsigdur.at(i) = active;
+	}
 
-		auto pos = syllable.find(" ");
-		if (pos != string::npos) {
-			syllable = syllable.substr(0, pos);
+	vector<bool> pickup(barandtime.size(), false);
+	for (int i=0; i<(int)barandtime.size() - 1; i++) {
+		if (tsigdur.at(i) == bardur.at(i)) {
+			// actual and expected are the same
+			continue;
 		}
-
-		// if there is an elision of words and the second word is more
-		// than one syllable, then end the word at the apostrophe.
-		pos = syllable.find("'");
-		if (pos != string::npos) {
-			if (syllable.back() == '-') {
-				syllable = syllable.substr(0, pos+1);
+		if (tsigdur.at(i) == tsigdur.at(i+1)) {
+			if (bardur.at(i) + bardur.at(i+1) == tsigdur.at(i)) {
+				pickup.at(i+1) = true;
+				i++;
+				continue;
 			}
 		}
+	}
 
-		if (syllable.size() == 0) {
-			// strange problem
-			return "";
+	// check for first-measure pickup
+	if (firstmeasure >= 0) {
+		if (bardur.at(firstmeasure) < tsigdur.at(firstmeasure)) {
+			pickup.at(firstmeasure) = true;
 		}
-		if (syllable.at(0) != '-') {
-			// word was not terminated properly?
-			cerr << "Syllable error at syllable : " << syllable;
-			cerr << ", line: " << current->getLineNumber();
-			cerr << ", field: " << current->getFieldNumber();
+	}
+
+	if (m_debugQ) {
+		cerr << "============================" << endl;
+		for (int i=0; i<(int)barandtime.size(); i++) {
+			cerr << pickup.at(i);
+			cerr << "\t";
+			cerr << bardur.at(i);
+			cerr << "\t";
+			cerr << tsigdur.at(i);
+			cerr << "\t";
+			for (int j=0; j<(int)barandtime[i].size(); j++) {
+				cerr << barandtime.at(i).at(j) << "\t";
+			}
 			cerr << endl;
-		} else {
-			syllable = syllable.substr(1);
 		}
-		transform(syllable.begin(), syllable.end(), syllable.begin(), ::tolower);
-		winfo.endtime = getEndtime(current);
-		hre.replaceDestructive(syllable, "", "[<>.:?!;,\"]", "g");
-		winfo.syllables.push_back(syllable);
-		winfo.starttimes.push_back(current->getDurationFromStart());
-		int cline = current->getLineIndex();
-		int cfield = current->getFieldIndex();
-		winfo.endtimes.push_back(m_endtimes[cline][cfield]);
-		winfo.notecounts.push_back(counts[cline][cfield]);
-		winfo.bars.push_back(m_measures[cline]);
-		output += syllable;
-		if (output.back() == '-') {
-			output.resize(output.size() - 1);
-			current = current->getNextToken();
-			winfo.syllables.back().resize((int)winfo.syllables.back().size() - 1);
+		cerr << endl;
+	}
+
+	// Markup pickup measure notes/rests
+	for (int i=0; i<(int)pickup.size() - 1; i++) {
+		if (!pickup[i]) {
 			continue;
-		} else {
-			// last syllable in word
-			break;
 		}
+		markPickupContent(barandtime.at(i).at(0), barandtime.at(i+1).at(0));
 	}
 
-	winfo.word = output;
-	return output;
+	// Pickup/incomplete measures covering three or more barlines are not considered
+	// (these could be used with dashed barlines or similar).
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::getEndtime --
+// Tool_meter::markPickupContent --
 //
 
-HumNum Tool_melisma::getEndtime(HTp text) {
-	int line = text->getLineIndex();
-	int field = text->getFieldIndex();
-	return m_endtimes[line][field];
+void Tool_meter::markPickupContent(HTp stok, HTp etok) {
+	int endline = etok->getLineIndex();
+	HTp current = stok;
+	while (current) {
+		int line = current->getLineIndex();
+		if (line > endline) {
+			break;
+		}
+		if (current->isData()) {
+			HTp field = current;
+			int track = field->getTrack();
+			while (field) {
+				int ttrack = field->getTrack();
+				if (ttrack != track) {
+					break;
+				}
+				if (field->isNull()) {
+					field = field->getNextFieldToken();
+					continue;
+				}
+				field->setValue("auto", "pickup", 1);
+				HumNum nbt = etok->getDurationFromStart() - field->getDurationFromStart();
+				stringstream ntime;
+				ntime.str("");
+				ntime << nbt.getNumerator() << "/" << nbt.getDenominator();
+				field->setValue("auto", "nextBarTime", ntime.str());
+				field = field->getNextFieldToken();
+			}
+		}
+		if (current == etok) {
+			break;
+		}
+		current = current->getNextToken();
+	}
 }
 
 
 
-/////////////////////////////
+//////////////////////////////
 //
-// Tool_melisma::markMelismas --
+// Tool_meter::getTimeSigDuration --
 //
 
-void Tool_melisma::markMelismas(HumdrumFile& infile, vector<vector<int>>& counts) {
-	int mincount = getInteger("min");
-	if (mincount < 2) {
-		mincount = 2;
-	}
-	for (int i=0; i<(int)counts.size(); i++) {
-		for (int j=0; j<(int)counts[i].size(); j++) {
-			if (counts[i][j] >= mincount) {
-				HTp token = infile.token(i, j);
-				markMelismaNotes(token, counts[i][j]);
-			}
-		}
+HumNum Tool_meter::getTimeSigDuration(HTp tsig) {
+	HumNum output = 0;
+	HumRegex hre;
+	if (hre.search(tsig, "^\\*M(\\d+)/(\\d+%?\\d*)")) {
+		int top = hre.getMatchInt(1);
+		string bot = hre.getMatch(2);
+		HumNum botdur = Convert::recipToDuration(bot);
+		output = botdur * top;
 	}
-	infile.appendLine("!!!RDF**kern: @ = marked note (melisma)");
-	infile.createLinesFromTokens();
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::markMelismaNotes --
+// Tool_meter::printMeterData --
 //
 
-void Tool_melisma::markMelismaNotes(HTp text, int count) {
-	int counter = 0;
+void Tool_meter::printMeterData(HumdrumFile& infile) {
+	bool foundLabel = false;
+	bool foundData  = false;
 
-	HTp current = text->getPreviousFieldToken();
-	while (current) {
-		if (current->isKern()) {
-			break;
-		}
-		current = current->getPreviousFieldToken();
-	}
-	if (!current) {
-		return;
-	}
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isRest()) {
-			current = current->getNextToken();
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << endl;
 			continue;
 		}
-		if (current->isNoteAttack()) {
-			counter++;
+
+		if ((!foundData) && (!foundLabel) && (!m_nolabelQ) && infile[i].isData()) {
+			printLabelLine(infile[i]);
+			foundData = true;
 		}
-		string text = *current;
-		text += "@";
-		current->setText(text);
-		if (counter >= count) {
-			break;
+
+		bool hasLabel = false;
+		if ((!m_nolabelQ) && (!foundLabel) && (!foundData)) {
+			if (searchForLabels(infile[i])) {
+				hasLabel = true;
+				foundLabel = true;
+			}
 		}
-		current = current->getNextToken();
+		printHumdrumLine(infile[i], hasLabel);
 	}
 }
 
@@ -97709,534 +101256,667 @@ void Tool_melisma::markMelismaNotes(HTp text, int count) {
 
 //////////////////////////////
 //
-// Tool_melisma::replaceLyrics --
+// printLabelLine --
 //
 
-void Tool_melisma::replaceLyrics(HumdrumFile& infile, vector<vector<int>>& counts) {
-	for (int i=0; i<(int)counts.size(); i++) {
-		for (int j=0; j<(int)counts[i].size(); j++) {
-			if (counts[i][j] == -1) {
-				continue;
-			}
-			string text = to_string(counts[i][j]);
-			HTp token = infile.token(i, j);
-			token->setText(text);
+void Tool_meter::printLabelLine(HumdrumLine& line) {
+	bool forceInterpretation = true;
+	bool printLabels = true;
+	for (int i=0; i<line.getFieldCount(); i++) {
+		HTp token = line.token(i);
+		if (token->isKern()) {
+			i = printKernAndAnalysisSpine(line, i, printLabels, forceInterpretation);
+		} else {
+			m_humdrum_text << "*";
+		}
+		if (i < line.getFieldCount() - 1) {
+			m_humdrum_text << "\t";
 		}
 	}
-	infile.createLinesFromTokens();
+	m_humdrum_text << "\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::getNoteCounts --
+// Tool_meter::printHumdrumLine --
 //
 
-void Tool_melisma::getNoteCounts(HumdrumFile& infile, vector<vector<int>>& counts) {
-	infile.initializeArray(counts, -1);
-	initBarlines(infile);
-	HumNum negativeOne = -1;
-	infile.initializeArray(m_endtimes, negativeOne);
-	vector<HTp> lyrics;
-	infile.getSpineStartList(lyrics, "**text");
-	for (int i=0; i<(int)lyrics.size(); i++) {
-		getNoteCountsForLyric(counts, lyrics[i]);
+void Tool_meter::printHumdrumLine(HumdrumLine& line, bool printLabels) {
+
+	for (int i=0; i<line.getFieldCount(); i++) {
+		HTp token = line.token(i);
+		if (token->isKern()) {
+			i = printKernAndAnalysisSpine(line, i, printLabels);
+		} else {
+			m_humdrum_text << token;
+		}
+		if (i < line.getFieldCount() - 1) {
+			m_humdrum_text << "\t";
+		}
 	}
+	m_humdrum_text << "\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::initBarlines --
+// Tool_meter::searchForLabels --
 //
 
-void Tool_melisma::initBarlines(HumdrumFile& infile) {
-	m_measures.resize(infile.getLineCount());
-	fill(m_measures.begin(), m_measures.end(), 0);
+bool Tool_meter::searchForLabels(HumdrumLine& line) {
+	if (!line.isInterpretation()) {
+		return false;
+	}
 	HumRegex hre;
-	for (int i=1; i<infile.getLineCount(); i++) {
-		if (!infile[i].isBarline()) {
-			m_measures[i] = m_measures[i-1];
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (hre.search(token, "(\\d+)")) {
-			m_measures[i] = hre.getMatchInt(1);
+	for (int i=0; i<line.getFieldCount(); i++) {
+		HTp token = line.token(i);
+		if (hre.search(token, "^\\*v[ibB]*:")) {
+			return true;
 		}
 	}
+	return false;
 }
 
 
 
-
 //////////////////////////////
 //
-// Tool_melisma::getNoteCountsForLyric --
+// Tool_meter::getHumNum --
 //
 
-void Tool_melisma::getNoteCountsForLyric(vector<vector<int>>& counts, HTp lyricStart) {
-	HTp current = lyricStart;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		int line = current->getLineIndex();
-		int field = current->getFieldIndex();
-		counts[line][field] = getCountForSyllable(current);
-		current = current->getNextToken();
+HumNum Tool_meter::getHumNum(HTp token, const string& parameter) {
+	HumRegex hre;
+	HumNum output;
+	string value = token->getValue("auto", parameter);
+	if (hre.search(value, "(\\d+)/(\\d+)")) {
+		output = hre.getMatchInt(1);
+		output /=  hre.getMatchInt(2);
+	} else if (hre.search(value, "(\\d+)")) {
+		output = hre.getMatchInt(1);
 	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_melisma::getCountForSyllable --
+// Tool_meter::getHumNumString --
 //
 
-int Tool_melisma::getCountForSyllable(HTp token) {
-	if (token->back() == '&') {
-		return 1;
-	}
-	HTp nexttok = token->getNextToken();
-	int eline   = token->getLineIndex();
-	int efield  = token->getFieldIndex();
-	m_endtimes[eline][efield] = token->getDurationFromStart() + token->getDuration();
-	while (nexttok) {
-		if (!nexttok->isData()) {
-			nexttok = nexttok->getNextToken();
-			continue;
+string Tool_meter::getHumNumString(HumNum input) {
+	stringstream tem;
+	input.printTwoPart(tem);
+	return tem.str();
+}
+
+
+
+//////////////////////////////
+//
+// Tool_meter::printKernAndAnalysisSpine --
+//
+
+int Tool_meter::printKernAndAnalysisSpine(HumdrumLine& line, int index, bool printLabels, bool forceInterpretation) {
+	HTp starttok = line.token(index);
+	int track = starttok->getTrack();
+	int counter = 0;
+
+	string analysis = ".";
+	string numerator = ".";
+	string denominator = ".";
+	string meter = ".";
+	bool hasNote = false;
+	bool hasRest = false;
+
+	for (int i=index; i<line.getFieldCount(); i++) {
+		HTp token = line.token(i);
+		int ttrack = token->getTrack();
+		if (ttrack != track) {
+			break;
 		}
-		if (nexttok->isNull()) {
-			nexttok = nexttok->getNextToken();
-			continue;
+		if (counter > 0) {
+			m_humdrum_text << "\t";
+		}
+		counter++;
+		if (forceInterpretation) {
+			m_humdrum_text << "*";
+		} else {
+			m_humdrum_text << token;
 		}
-		// found non-null data token
-		break;
-	}
 
-	HumdrumFile& infile = *token->getOwner()->getOwner();
-	int endline = infile.getLineCount() - 1;
-	if (nexttok) {
-		endline = nexttok->getLineIndex();
-	}
-	int output = 0;
-	HTp current = token->getPreviousFieldToken();
-	while (current) {
-		if (current->isKern()) {
-			break;
+		if (line.isData() && !forceInterpretation) {
+			if (token->isNull()) {
+				// analysis = ".";
+			} else if (token->isRest() && !m_restQ) {
+				// analysis = ".";
+			} else if ((!token->isNoteAttack()) && !(m_restQ && token->isRest())) {
+				// analysis = ".";
+			} else if ((analysis == ".") && (token->getValueBool("auto", "hasData"))) {
+				string data = token->getValue("auto", "zeroBeat");
+				if (m_restQ) {
+					if (token->isRest()) {
+						hasRest = true;
+					} else {
+						hasNote = true;
+					}
+				}
+				HumNum value;
+				HumNum nvalue;
+				HumNum dvalue;
+				if (!data.empty()) {
+					value = getHumNum(token, "zeroBeat");
+					if (m_numeratorQ) {
+						nvalue = getHumNum(token, "numerator");
+						numerator = getHumNumString(nvalue);
+					}
+					if (m_denominatorQ) {
+						dvalue = getHumNum(token, "denominator");
+						denominator = getHumNumString(dvalue);
+					}
+					if (m_tsigQ) {
+						meter = numerator;
+						meter += "/";
+						meter += denominator;
+					}
+				}
+				if (!m_zeroQ) {
+					value += 1;
+				}
+				if (m_floatQ) {
+					stringstream tem;
+					if (m_digits) {
+						tem << std::setprecision(m_digits + 1) << value.getFloat();
+					} else {
+						tem << value.getFloat();
+					}
+					analysis = tem.str();
+					if (m_commaQ) {
+						HumRegex hre;
+						hre.replaceDestructive(analysis, ",", "\\.");
+					}
+				} else {
+					analysis = getHumNumString(value);
+				}
+			}
+		} else if (line.isInterpretation() || forceInterpretation) {
+			if (token->compare(0, 2, "**") == 0) {
+				analysis = "**cdata-beat";
+				if (m_tsigQ) {
+					meter = "**cdata-tsig";
+				}
+				if (m_numeratorQ) {
+					numerator = "**cdata-num";
+				}
+				if (m_denominatorQ) {
+					denominator = "**cdata-den";
+				}
+			} else if (*token == "*-") {
+				analysis = "*-";
+				numerator = "*-";
+				denominator = "*-";
+				meter = "*-";
+			} else if (token->isTimeSignature()) {
+				analysis = *token;
+			} else {
+				analysis = "*";
+				numerator = "*";
+				denominator = "*";
+				meter = "*";
+				if (printLabels) {
+					if (m_quarterQ) {
+						analysis = "*vi:4ths:";
+					} else if (m_eighthQ) {
+						analysis = "*vi:8ths:";
+					} else if (m_halfQ) {
+						analysis = "*vi:half:";
+					} else if (m_wholeQ) {
+						analysis = "*vi:whole:";
+					} else if (m_sixteenthQ) {
+						analysis = "*vi:16ths:";
+					} else {
+						analysis = "*vi:beat:";
+					}
+					numerator = "*vi:top:";
+					denominator = "*vi:bot:";
+					meter = "*vi:tsig:";
+					if (m_joinQ) {
+						numerator = "";
+						denominator = "";
+						meter = "";
+					}
+				}
+			}
+		} else if (line.isBarline()) {
+			analysis = *token;
+			numerator = *token;
+			denominator = *token;
+			meter = *token;
+		} else if (line.isCommentLocal()) {
+			analysis = "!";
+			numerator = "!";
+			denominator = "!";
+			meter = "!";
+		} else {
+			cerr << "STRANGE LINE: " << line << endl;
 		}
-		current = current->getPreviousFieldToken();
-	}
-	if (!current) {
-		return 0;
 	}
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
+
+	if (m_joinQ) {
+		if (line.isData() && !forceInterpretation) {
+			if (m_tsigQ) {
+					m_humdrum_text << "\t" << meter;
+			} else {
+				if (m_numeratorQ) {
+					m_humdrum_text << "\t" << numerator;
+				}
+				if (m_denominatorQ) {
+					m_humdrum_text << "\t" << denominator;
+				}
+			}
 		}
-		if (current->isRest()) {
-			current = current->getNextToken();
-			continue;
+		if (!m_nobeatQ) {
+			if (line.isData() && !forceInterpretation) {
+				m_humdrum_text << ":";
+			} else {
+				m_humdrum_text << "\t";
+			}
+			m_humdrum_text << analysis;
+			if (line.isData() && hasRest && !hasNote) {
+				m_humdrum_text << "r";
+			}
 		}
-		if (!current->isNoteAttack()) {
-			// ignore tied notes
-			m_endtimes[eline][efield] = current->getDurationFromStart() + current->getDuration();
-			current = current->getNextToken();
-			continue;
+	} else {
+		if (!m_nobeatQ) {
+			m_humdrum_text << "\t" << analysis;
+			if (line.isData() && hasRest && !hasNote) {
+				m_humdrum_text << "r";
+			}
 		}
-		int line = current->getLineIndex();
-		if (line < endline) {
-			m_endtimes[eline][efield] = current->getDurationFromStart() + current->getDuration();
-			output++;
+		if (m_tsigQ) {
+				m_humdrum_text << "\t" << meter;
 		} else {
-			break;
+			if (m_numeratorQ) {
+				m_humdrum_text << "\t" << numerator;
+			}
+			if (m_denominatorQ) {
+				m_humdrum_text << "\t" << denominator;
+			}
 		}
-		current = current->getNextToken();
-	}
-
-	return output;
-}
-
 
+	}
 
-
-
-/////////////////////////////////
-//
-// Tool_mens2kern::Tool_mens2kern -- Set the recognized options for the tool.
-//
-
-Tool_mens2kern::Tool_mens2kern(void) {
-	define("debug=b",    "print debugging statements");
+	return index + counter - 1;
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_mens2kern::run -- Do the main work of the tool.
+// Tool_meter::getMeterData --
 //
 
-bool Tool_mens2kern::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
-
-bool Tool_mens2kern::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
+void Tool_meter::getMeterData(HumdrumFile& infile) {
 
+	int maxtrack = infile.getMaxTrack();
+	vector<HumNum> curNum(maxtrack + 1, 0);
+	vector<HumNum> curDen(maxtrack + 1, 0);
+	vector<HumNum> curBeat(maxtrack + 1, 0);
+	vector<HumNum> curBarTime(maxtrack + 1, 0);
 
-bool Tool_mens2kern::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		processLine(infile[i], curNum, curDen, curBeat, curBarTime);
 	}
-	return status;
-}
-
-
-bool Tool_mens2kern::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_mens2kern::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_meter::processLine --
 //
 
-void Tool_mens2kern::initialize(void) {
-	m_debugQ = getBoolean("debug");
-}
-
+void Tool_meter::processLine(HumdrumLine& line, vector<HumNum>& curNum,
+		vector<HumNum>& curDen, vector<HumNum>& curBeat,
+		vector<HumNum>& curBarTime) {
 
+	int fieldCount = line.getFieldCount();
 
-//////////////////////////////
-//
-// Tool_mens2kern::processFile --
-//
+	if (!line.hasSpines()) {
+		return;
+	}
 
-void Tool_mens2kern::processFile(HumdrumFile& infile) {
-	vector<HTp> melody;
-	int scount = infile.getStrandCount();
-	for (int i=0; i<scount; i++) {
-		HTp sstart = infile.getStrandBegin(i);
-		if (!sstart->isDataType("**mens")) {
-			continue;
-		}
-		HTp sstop = infile.getStrandEnd(i);
-		HTp current = sstart;
-		while (current && (current != sstop)) {
-			if (current->isNull()) {
-				// ignore null data tokens
-				current = current->getNextToken();
+	HumRegex hre;
+	if (line.isBarline()) {
+		for (int i=0; i<fieldCount; i++) {
+			HTp token = line.token(i);
+			if (!token->isKern()) {
 				continue;
 			}
-			melody.push_back(current);
-			current = current->getNextToken();
+			if (hre.search(token, "-")) {
+				// invisible barline: ignore
+				continue;
+			}
+			int track = token->getTrack();
+			HumNum curTime = token->getDurationFromStart();
+			curBarTime.at(track) = curTime;
 		}
-		processMelody(melody);
-		melody.clear();
+		return;
 	}
 
-	infile.createLinesFromTokens();
-}
-
+	if (line.isInterpretation()) {
+		// check for time signatures
+		for (int i=0; i<fieldCount; i++) {
+			HTp token = line.token(i);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (hre.search(token, "^\\*M(\\d+)/(\\d+)")) {
+				int top = hre.getMatchInt(1);
+				int bot = hre.getMatchInt(2);
+				int track = token->getTrack();
+				curNum.at(track) = top;
+				curDen.at(track) = bot;
+				curBeat.at(track) = 0;
+			} else if (hre.search(token, "^\\*beat:\\s*([\\d.%]+)\\s*$")) {
+				int track = token->getTrack();
+				string recip = hre.getMatch(1);
+				curBeat.at(track) = Convert::recipToDuration(recip);
+			}
+		}
+		return;
+	}
 
+	if (line.isData()) {
+		// check for time signatures
+		for (int i=0; i<fieldCount; i++) {
+			HTp token = line.token(i);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			if ((!m_restQ) && token->isRest()) {
+				continue;
+			}
+			if (!token->isNoteAttack() && !(m_restQ && token->isRest())) {
+				continue;
+			}
+			int pickup = token->getValueInt("auto", "pickup");
+			int track = token->getTrack();
+			stringstream value;
+			value.str("");
+			value << curNum.at(track);
+			token->setValue("auto", "numerator", value.str());
+			value.str("");
+			value << curDen.at(track);
+			token->setValue("auto", "denominator", value.str());
+			HumNum curTime = token->getDurationFromStart();
+			HumNum q;
+			if (pickup) {
+				HumNum meterDur = curNum.at(track);
+				meterDur /= curDen.at(track);
+				meterDur *= 4;
+				HumNum nbt = getHumNum(token, "nextBarTime");
+				q = meterDur - nbt;
+			} else {
+				q = curTime - curBarTime.at(track);
+			}
+			value.str("");
+			value << q;
+			token->setValue("auto", "q", value.str());
+			bool compound = false;
+			int multiple = curNum.at(track).getNumerator() / 3;
+			int remainder = curNum.at(track).getNumerator() % 3;
+			int bottom = curDen.at(track).getNumerator();
+			if ((curBeat.at(track) == 0) && (bottom >= 8) && (multiple > 1) && (remainder == 0)) {
+				compound = true;
+			}
 
-//////////////////////////////
-//
-// Tool_mens2kern::processMelody --
-//
+			HumNum qq = q;
+			if (m_quarterQ) {
+				// do nothing (prior calculations are done in quarter notes)
+			} else if (m_halfQ) {
+				qq /= 2;
+			} else if (m_wholeQ) {
+				qq /= 4;
+			} else if (m_eighthQ) {
+				qq *= 2;
+			} else if (m_sixteenthQ) {
+				qq *= 4;
+			} else {
+				// convert quarter note metric positions into beat positions
+				if (compound) {
+					qq *= curDen.at(track);
+					qq /= 4;
+					qq /= 3;
+				} else if (curBeat.at(track) > 0) {
+					qq /= curBeat.at(track);
+				} else {
+					qq *= curDen.at(track);
+					qq /= 4;
+				}
+			}
 
-void Tool_mens2kern::processMelody(vector<HTp>& melody) {
-	int maximodus = 0;
-	int modus = 0;
-	int tempus = 0;
-	int prolatio = 0;
-	int semibrevis_def = 0;
-	int brevis_def     = 0;
-	int longa_def      = 0;
-	int maxima_def     = 0;
-	string regexopts;
-	HumRegex hre;
-	string rhythm;
-	bool imperfecta;
-	bool perfecta;
-	bool altera;
+			value.str("");
+			value << qq;
+			token->setValue("auto", "zeroBeat", value.str());
+			token->setValue("auto", "hasData", 1);
 
-	for (int i=0; i<(int)melody.size(); i++) {
-		if (*melody[i] == "**mens") {
-			// convert spine to **kern data:
-			melody[i]->setText("**kern");
 		}
+		return;
+	}
+}
 
-		if (melody[i]->isMensuration()) {
-			getMensuralInfo(melody[i], maximodus, modus, tempus, prolatio);
 
-			// Default value of notes from maxima to semibrevis in minims:
-			semibrevis_def = prolatio;
-			brevis_def     = tempus    * semibrevis_def;
-			longa_def      = modus     * brevis_def;
-			maxima_def     = maximodus * longa_def;
-			if (m_debugQ) {
-				cerr << "LEVELS X_def = "    << maxima_def
-					  << " | L_def = " << longa_def
-					  << " | S_def = " << brevis_def
-					  << " | s_def = " << semibrevis_def << endl;
-			}
-		}
 
-		if (!melody[i]->isData()) {
-			continue;
-		}
-		string text = melody[i]->getText();
-		imperfecta = hre.search(text, "i") ? true : false;
-		perfecta = hre.search(text, "p") ? true : false;
-		altera = hre.search(text, "\\+") ? true : false;
-		if (hre.search(text, "([XLSsMmUu])")) {
-			rhythm = hre.getMatch(1);
-		} else {
-			cerr << "Error: token " << melody[i] << " has no rhythm" << endl;
-			cerr << "   ON LINE: "  << melody[i]->getLineNumber()    << endl;
-			continue;
-		}
 
-		string kernRhythm = mens2kernRhythm(rhythm, altera, perfecta, imperfecta, maxima_def, longa_def, brevis_def, semibrevis_def);
+/////////////////////////////////
+//
+// Tool_gridtest::Tool_metlev -- Set the recognized options for the tool.
+//
 
-		hre.replaceDestructive(text, kernRhythm, rhythm);
-		// Remove any dot of division/augmentation
-		hre.replaceDestructive(text, "", ":");
-		// remove perfection/imperfection/alteration markers
-		hre.replaceDestructive(text, "", "[pi\\+]");
-		if (text.empty()) {
-			text = ".";
-		}
-		melody[i]->setText(text);
-	}
+Tool_metlev::Tool_metlev(void) {
+	define("a|append=b",         "append data analysis to input file");
+	define("p|prepend=b",        "prepend data analysis to input file");
+	define("c|composite=b",      "generate composite rhythm");
+	define("i|integer=b",        "quantize metric levels to int values");
+	define("x|attacks-only=b",   "only mark lines with note attacks");
+	define("G|no-grace-notes=b", "do not mark grace note lines");
+	define("k|kern-spine=i:1",   "analyze only given kern spine");
+	define("e|exinterp=s:blev",  "exclusive interpretation type for output");
 }
 
 
-//////////////////////////////
+
+///////////////////////////////
 //
-// Tool_mens2kern::getMensuralInfo --
+// Tool_metlev::run -- Primary interfaces to the tool.
 //
 
-void Tool_mens2kern::getMensuralInfo(HTp token, int& maximodus, int& modus,
-		int& tempus, int& prolatio) {
-	HumRegex hre;
-	if (!hre.search(token, "^\\*met\\(.*?\\)_(\\d+)")) {
-		// need to interpret symbols without underscores.
-		if (token->getText() == "*met(C)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 2;
-			prolatio = 2;
-		} else if (token->getText() == "*met(O)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 3;
-			prolatio = 2;
-		} else if (token->getText() == "*met(C.)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 2;
-			prolatio = 3;
-		} else if (token->getText() == "*met(O.)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 3;
-			prolatio = 3;
-		} else if (token->getText() == "*met(C|)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 2;
-			prolatio = 2;
-		} else if (token->getText() == "*met(O|)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 3;
-			prolatio = 2;
-		} else if (token->getText() == "*met(C.|)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 2;
-			prolatio = 3;
-		} else if (token->getText() == "*met(O.|)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 3;
-			prolatio = 3;
-		} else if (token->getText() == "*met(C2)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 2;
-			prolatio = 2;
-		} else if (token->getText() == "*met(C3)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 3;
-			prolatio = 2;
-		} else if (token->getText() == "*met(O2)") {
-			maximodus = 2;
-			modus = 3;
-			tempus = 2;
-			prolatio = 2;
-		} else if (token->getText() == "*met(O3)") {
-			maximodus = 3;
-			modus = 3;
-			tempus = 3;
-			prolatio = 2;
-		} else if (token->getText() == "*met(C3/2)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 2;
-			prolatio = 3;
-		} else if (token->getText() == "*met(C|3/2)") {
-			maximodus = 2;
-			modus = 2;
-			tempus = 3;
-			prolatio = 2;
-		}
-	} else {
-		string levels = hre.getMatch(1);
-		if (levels.size() >= 1) {
-			maximodus = levels[0] - '0';
-		}
-		if (levels.size() >= 2) {
-			modus = levels[1] - '0';
-		}
-		if (levels.size() >= 3) {
-			tempus = levels[2] - '0';
-		}
-		if (levels.size() >= 4) {
-			prolatio = levels[3] - '0';
-		}
-	}
-
-	if (m_debugQ) {
-		cerr << "MENSURAL INFO: maximodus = "   << maximodus
-			  << " | modus = "    << modus
-			  << " | tempus = "   << tempus
-			  << " | prolatio = " << prolatio << endl;
+bool Tool_metlev::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
 }
 
 
+bool Tool_metlev::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
+}
+
 
-//////////////////////////////
-//
-// Tool_mens2kern::mens2kernRhythm --
-//
+bool Tool_metlev::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	out << infile;
+	return status;
+}
 
-string Tool_mens2kern::mens2kernRhythm(const string& rhythm, bool altera, bool perfecta, bool imperfecta, int maxima_def, int longa_def, int brevis_def, int semibrevis_def) {
-	double val_note;
-	double minima_def = 1;
-	double semiminima_def = 0.5;
-	double fusa_def = 0.25;
-	double semifusa_def = 0.125;
 
-	if (rhythm == "X") {
-		if (perfecta) { val_note = 3 * longa_def; }
-		else if (imperfecta) { val_note = 2 * longa_def; }
-		else { val_note = maxima_def; }
+bool Tool_metlev::run(HumdrumFile& infile) {
+	int lineCount = infile.getLineCount();
+	if (lineCount == 0) {
+		m_error_text << "No input data";
+		return false;
 	}
-	else if (rhythm == "L") {
-		if (perfecta) { val_note = 3 * brevis_def; }
-		else if (imperfecta) { val_note = 2 * brevis_def; }
-		else if (altera) { val_note = 2 * longa_def; }
-		else { val_note = longa_def; }
+
+	string exinterp = getString("exinterp");
+	if (exinterp.empty()) {
+		exinterp = "**blev";
+	} else if (exinterp[0] != '*') {
+		exinterp.insert(0, "*");
 	}
-	else if (rhythm == "S") {
-		if (perfecta) { val_note = 3 * semibrevis_def; }
-		else if (imperfecta) { val_note = 2 * semibrevis_def; }
-		else if (altera) { val_note = 2 * brevis_def; }
-		else { val_note = brevis_def; }
+	if (exinterp[1] != '*') {
+		exinterp.insert(0, "*");
 	}
-	else if (rhythm == "s") {
-		if (perfecta) { val_note = 3 * minima_def; }
-		else if (imperfecta) { val_note = 2 * minima_def; }
-		else if (altera) { val_note = 2 * semibrevis_def; }
-		else { val_note = semibrevis_def; }
+
+	m_kernspines = infile.getKernSpineStartList();
+
+	vector<double> beatlev(lineCount, NAN);
+	int track = 0;
+	if (m_kernspines.size() > 0) {
+		track = m_kernspines[0]->getTrack();
+	} else {
+		m_error_text << "No **kern spines in input file" << endl;
+		return false;
 	}
-	else if (rhythm == "M") {
-		if (perfecta) { val_note = 1.5 * minima_def; }
-		else if (altera) { val_note = 2 * minima_def; }
-		else { val_note = minima_def; }
+	infile.getMetricLevels(beatlev, track, NAN);
+
+	for (int i=0; i<lineCount; i++) {
+		if (!infile[i].isData()) {
+				continue;
+		}
+		if (getBoolean("no-grace-notes") && (infile[i].getDuration() == 0)) {
+			beatlev[i] = NAN;
+			continue;
+		}
+		if (getBoolean("attacks-only")) {
+			if (!infile[i].getKernNoteAttacks()) {
+				beatlev[i] = NAN;
+				continue;
+			}
+		}
+		if (beatlev[i] - (int)beatlev[i] != 0.0) {
+			if (getBoolean("integer")) {
+					beatlev[i] = floor(beatlev[i]);
+			} else {
+				beatlev[i] = Convert::significantDigits(beatlev[i], 2);
+			}
+		}
 	}
-	else if (rhythm == "m") {
-		if (perfecta) { val_note = 1.5 * semiminima_def; }
-		else { val_note = semiminima_def; }
+
+	if (getBoolean("kern-spine")) {
+		int kspine = getInteger("kern-spine") - 1;
+		if ((kspine >= 0) && (kspine < (int)m_kernspines.size())) {
+			vector<vector<double> > results;
+			fillVoiceResults(results, infile, beatlev);
+			if (kspine == (int)m_kernspines.size() - 1) {
+				infile.appendDataSpine(results.back(), "nan", exinterp);
+			} else {
+				int track = m_kernspines[kspine+1]->getTrack();
+				infile.insertDataSpineBefore(track, results[kspine],
+						"nan", exinterp);
+			}
+			infile.createLinesFromTokens();
+			return true;
+		}
+	} else if (getBoolean("append")) {
+		infile.appendDataSpine(beatlev, "nan", exinterp);
+		infile.createLinesFromTokens();
+		return true;
+	} else if (getBoolean("prepend")) {
+		infile.prependDataSpine(beatlev, "nan", exinterp);
+		infile.createLinesFromTokens();
+		return true;
+	} else if (getBoolean("composite")) {
+		infile.prependDataSpine(beatlev, "nan", exinterp);
+		infile.printFieldIndex(0, m_humdrum_text);
+		infile.clear();
+		infile.readString(m_humdrum_text.str());
+	} else {
+		vector<vector<double> > results;
+		fillVoiceResults(results, infile, beatlev);
+		infile.appendDataSpine(results.back(), "nan", exinterp);
+		for (int i = (int)results.size()-1; i>0; i--) {
+			int track = m_kernspines[i]->getTrack();
+			infile.insertDataSpineBefore(track, results[i-1], "nan", exinterp);
+		}
+		infile.createLinesFromTokens();
+		return true;
 	}
-	else if (rhythm == "U") {
-		if (perfecta) { val_note = 1.5 * fusa_def; }
-		else { val_note = fusa_def; }
+
+	return false;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_metlev::fillVoiceResults -- Split the metric level analysis into values
+//     for each voice.
+//
+
+void Tool_metlev::fillVoiceResults(vector<vector<double> >& results,
+		HumdrumFile& infile, vector<double>& beatlev) {
+
+	results.resize(m_kernspines.size());
+	for (int i=0; i<(int)results.size(); i++) {
+		results[i].resize(beatlev.size());
+		fill(results[i].begin(), results[i].end(), NAN);
 	}
-	else if (rhythm == "u") {
-		if (perfecta) { val_note = 1.5 * semifusa_def; }
-		else { val_note = semifusa_def; }
+	int track;
+	vector<int> rtracks(infile.getTrackCount() + 1, -1);
+	for (int i=0; i<(int)m_kernspines.size(); i++) {
+		int track = m_kernspines[i]->getTrack();
+		rtracks[track] = i;
 	}
-	else { cerr << "UNKNOWN RHYTHM: " << rhythm << endl; return ""; }
 
-	switch ((int)(val_note * 10000)) {
-		case 1250:     return "16";   break;   // sixteenth note
-		case 1875:     return "16.";  break;   // dotted sixteenth note
-		case 2500:     return "8";    break;   // eighth note
-		case 3750:     return "8.";   break;   // dotted eighth note
-		case 5000:     return "4";    break;   // quarter note
-		case 7500:     return "4.";   break;   // dotted quarter note
-		case 10000:    return "2";    break;   // half note
-		case 15000:    return "2.";   break;   // dotted half note
-		case 20000:    return "1";    break;   // whole note
-		case 30000:    return "1.";   break;   // dotted whole note
-		case 40000:    return "0";    break;   // breve note
-		case 60000:    return "0.";   break;   // dotted breve note
-		case 90000:    return "2%9";  break;   // or ["0.", "1."];
-		case 80000:    return "00";   break;   // long note
-		case 120000:   return "00.";  break;   // dotted long note
-		case 180000:   return "1%9";  break;   // or ["00.", "0."];
-		case 270000:   return "2%27"; break;   // or ["0.", "1.", "0.", "1.", "0.", "1."];
-		case 160000:   return "000";  break;   // maxima note
-		case 240000:   return "000."; break;   // dotted maxima note
-		case 360000:   return "1%18"; break;   // or ["000.", "00."];
-		case 540000:   return "1%27"; break;   // or ["00.", "0.", "00.", "0.", "00.", "0."];
-		case 810000:   return "2%81"; break;   // or ["00.", "0.", "00.", "0.", "00.", "0.", "0.", "1.", "0.", "1.", "0.", "1."];
-		default:
-			cerr << "Error: unknown val_note: " << val_note << endl;
+	bool attacksQ = getBoolean("attacks-only");
+	vector<int> nonnullcount(m_kernspines.size(), 0);
+	vector<int> attackcount(m_kernspines.size(), 0);
+	HTp token;
+	int voice;
+	int i, j;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			track = token->getTrack();
+			voice = rtracks[track];
+			nonnullcount[voice]++;
+			if (token->isNoteAttack()) {
+				attackcount[voice]++;
+			}
+		}
+		for (int v=0; v<(int)m_kernspines.size(); v++) {
+			if (attacksQ) {
+				if (attackcount[v]) {
+					results[v][i] = beatlev[i];
+					attackcount[v] = 0;
+				}
+			} else {
+				if (nonnullcount[v]) {
+					results[v][i] = beatlev[i];
+				}
+				nonnullcount[v] = 0;
+			}
+		}
 	}
-
-	return "";
 }
 
 
@@ -98244,37 +101924,31 @@ string Tool_mens2kern::mens2kernRhythm(const string& rhythm, bool altera, bool p
 
 /////////////////////////////////
 //
-// Tool_meter::Tool_meter -- Set the recognized options for the tool.
+// Tool_modori::Tool_modori -- Set the recognized options for the tool.
 //
 
-Tool_meter::Tool_meter(void) {
-	define("c|comma=b",                       "display decimal points as commas");
-	define("d|denominator=b",                 "display denominator spine");
-	define("e|eighth=b",                      "metric positions in eighth notes rather than beats");
-	define("f|float=b",                       "floating-point beat values instead of rational numbers");
-	define("h|half=b",                        "metric positions in half notes rather than beats");
-	define("j|join=b",                        "join time signature information and metric positions into a single token");
-	define("n|numerator=b",                   "display numerator spine");
-	define("q|quarter=b",                     "metric positions in quarter notes rather than beats");
-	define("r|rest=b",                        "add meteric positions of rests");
-	define("s|sixteenth=b",                   "metric positions in sixteenth notes rather than beats");
-	define("t|time-signature|tsig|m|meter=b", "display active time signature for each note");
-	define("w|whole=b",                       "metric positions in whole notes rather than beats");
-	define("z|zero=b",                        "start of measure is beat 0 rather than beat 1");
-
-	define("B|no-beat=b",                     "Do not display metric positions (beats)");
-	define("D|digits=i:0",                    "number of digits after decimal point");
-	define("L|no-label=b",                    "do not add labels to analysis spines");
+Tool_modori::Tool_modori(void) {
+	define("m|modern=b",                                                 "prepare score for modern style");
+	define("o|original=b",                                               "prepare score for original style");
+	define("d|info=b",                                                   "display key/clef/mensuration information");
+	define("I|no-instrument-name|no-instrument-names=b",                 "do not change part labels");
+	define("A|no-instrument-abbreviation|no-instrument-abbreviations=b", "do not change part label abbreviations");
+	define("C|no-clef|no-clefs=b",                                       "do not change clefs");
+	define("K|no-key|no-keys=b",                                         "do not change key signatures");
+	define("L|no-lyrics=b",                                              "do not change **text exclusive interpretations");
+	define("M|no-mensuration|no-mensurations=b",                         "do not change mensurations");
+	define("R|no-references=b",                                          "do not change reference records keys");
+	define("T|no-text=b",                                                "do not change !LO:(TX|DY) layout parameters");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_meter::run -- Do the main work of the tool.
+// Tool_modori::run -- Do the main work of the tool.
 //
 
-bool Tool_meter::run(HumdrumFileSet& infiles) {
+bool Tool_modori::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -98283,7 +101957,7 @@ bool Tool_meter::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_meter::run(const string& indata, ostream& out) {
+bool Tool_modori::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -98295,7 +101969,7 @@ bool Tool_meter::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_meter::run(HumdrumFile& infile, ostream& out) {
+bool Tool_modori::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -98306,7 +101980,7 @@ bool Tool_meter::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_meter::run(HumdrumFile& infile) {
+bool Tool_modori::run(HumdrumFile& infile) {
 	initialize();
 	processFile(infile);
 	return true;
@@ -98316,627 +101990,715 @@ bool Tool_meter::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_meter::initialize --
+// Tool_modori::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_meter::initialize(void) {
-
-	m_commaQ       = getBoolean("comma");
-	m_denominatorQ = getBoolean("denominator");
-	m_digits       = getInteger("digits");
-	m_floatQ       = getBoolean("float");
-	m_halfQ        = getBoolean("half");
-	m_joinQ        = getBoolean("join");
-	m_nobeatQ      = getBoolean("no-beat");
-	m_nolabelQ     = getBoolean("no-label");
-	m_numeratorQ   = getBoolean("numerator");
-	m_quarterQ     = getBoolean("quarter");
-	m_halfQ        = getBoolean("half");
-	m_eighthQ      = getBoolean("eighth");
-	m_sixteenthQ   = getBoolean("sixteenth");
-	m_restQ        = getBoolean("rest");
-	m_tsigQ        = getBoolean("meter");
-	m_wholeQ       = getBoolean("whole");
-	m_zeroQ        = getBoolean("zero");
-
-	if (m_digits < 0) {
-		m_digits = 0;
-	}
-	if (m_digits > 15) {
-		m_digits = 15;
-	}
-
-	if (m_joinQ && !(m_tsigQ || m_numeratorQ || m_denominatorQ)) {
-		m_tsigQ = true;
-	}
-	if (m_joinQ) {
-		m_nobeatQ = false;
-	}
-	if (m_joinQ && m_numeratorQ && m_denominatorQ) {
-		m_tsigQ = true;
-	}
-
-	if (m_tsigQ) {
-		m_numeratorQ = true;
-		m_denominatorQ = true;
-	}
-
-	// Only one fix-width metric position allowed, prioritize
-	// largest given duration:
-	if (m_wholeQ) {
-		m_halfQ      = false;
-		m_quarterQ   = false;
-		m_eighthQ    = false;
-		m_sixteenthQ = false;
-	} else if (m_halfQ) {
-		m_wholeQ     = false;
-		m_quarterQ   = false;
-		m_eighthQ    = false;
-		m_sixteenthQ = false;
-	} else if (m_quarterQ) {
-		m_wholeQ     = false;
-		m_halfQ      = false;
-		m_eighthQ    = false;
-		m_sixteenthQ = false;
-	} else if (m_eighthQ) {
-		m_wholeQ     = false;
-		m_halfQ      = false;
-		m_quarterQ   = false;
-		m_sixteenthQ = false;
-	} else if (m_sixteenthQ) {
-		m_wholeQ     = false;
-		m_halfQ      = false;
-		m_quarterQ   = false;
-		m_eighthQ    = false;
+void Tool_modori::initialize(void) {
+	m_infoQ = getBoolean("info");
+	m_modernQ = getBoolean("modern");
+	m_originalQ = getBoolean("original");
+	if (m_modernQ && m_originalQ) {
+		// if both options are used, ignore -m:
+		m_modernQ = false;
 	}
-
+	m_nokeyQ         = getBoolean("no-key");
+	m_noclefQ        = getBoolean("no-clef");
+	m_nolotextQ      = getBoolean("no-text");
+	m_nolyricsQ      = getBoolean("no-lyrics");
+	m_norefsQ        = getBoolean("no-references");
+	m_nomensurationQ = getBoolean("no-mensuration");
+	m_nolabelsQ      = getBoolean("no-instrument-names");
+	m_nolabelAbbrsQ  = getBoolean("no-instrument-abbreviations");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_meter::processFile --
-//
-
-void Tool_meter::processFile(HumdrumFile& infile) {
-	analyzePickupMeasures(infile);
-	getMeterData(infile);
-	printMeterData(infile);
-}
-
-
-//////////////////////////////
-//
-// Tool_meter::analyzePickupMeasures --
+// Tool_modori::processFile --
 //
 
-void Tool_meter::analyzePickupMeasures(HumdrumFile& infile) {
-	vector<HTp> sstarts;
-	infile.getKernSpineStartList(sstarts);
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		analyzePickupMeasures(sstarts[i]);
-	}
-}
-
+void Tool_modori::processFile(HumdrumFile& infile) {
+	m_keys.clear();
+	m_labels.clear();
+	m_labelAbbrs.clear();
+	m_clefs.clear();
+	m_mensurations.clear();
+	m_references.clear();
+	m_lyrics.clear();
+	m_lotext.clear();
 
-void Tool_meter::analyzePickupMeasures(HTp sstart) {
-	// First dimension are visible barlines.
-	// Second dimension are time signature(s) within the barlines.
-	vector<vector<HTp>> barandtime;
-	barandtime.reserve(1000);
-	barandtime.resize(1);
-	barandtime[0].push_back(sstart);
-	HTp current = sstart->getNextToken();
-	while (current) {
-		if (current->isTimeSignature()) {
-			barandtime.back().push_back(current);
-		} else if (current->isBarline()) {
-			if (current->find("-") != std::string::npos) {
-				current = current->getNextToken();
-				continue;
-			}
-			barandtime.resize(barandtime.size() + 1);
-			barandtime.back().push_back(current);
-		} else if (*current == "*-") {
-			barandtime.resize(barandtime.size() + 1);
-			barandtime.back().push_back(current);
-			break;
-		}
-		current = current->getNextToken();
-	}
+	int maxtrack = infile.getMaxTrack();
+	m_keys.resize(maxtrack+1);
+	m_labels.resize(maxtrack+1);
+	m_labelAbbrs.resize(maxtrack+1);
+	m_clefs.resize(maxtrack+1);
+	m_mensurations.resize(maxtrack+1);
+	m_references.reserve(1000);
+	m_lyrics.reserve(1000);
+	m_lotext.reserve(1000);
 
-	// Extract the actual duration of measures:
-	vector<HumNum> bardur(barandtime.size(), 0);
-	for (int i=0; i<(int)barandtime.size() - 1; i++) {
-		HumNum starttime = barandtime[i][0]->getDurationFromStart();
-		HumNum endtime = barandtime.at(i+1)[0]->getDurationFromStart();
-		HumNum duration = endtime - starttime;
-		bardur.at(i) = duration;
-	}
+	HumRegex hre;
+	int exinterpLine = -1;
 
-	// Extract the expected duration of measures:
-	vector<HumNum> tsigdur(barandtime.size(), 0);
-	int firstmeasure = -1;
-	HumNum active = 0;
-	for (int i=0; i<(int)barandtime.size() - 1; i++) {
-		if (firstmeasure < 0) {
-			if (bardur.at(i) > 0) {
-				firstmeasure = i;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isCommentLocal() || infile[i].isCommentGlobal()) {
+			for (int j=0; j<infile[i].getFieldCount(); j++) {
+				HTp token = infile.token(i, j);
+				if (*token == "!") {
+					continue;
+				}
+				if (hre.search(token, "^!!?LO:(TX|DY).*:mod=")) {
+					m_lotext.push_back(token);
+				} else if (hre.search(token, "^!!?LO:(TX|DY).*:ori=")) {
+					m_lotext.push_back(token);
+				}
+				if (hre.search(token, "^!LO:MO:.*")) {
+					m_lomo.push_back(token);
+				}
 			}
 		}
-		if (barandtime[i].size() < 2) {
-			tsigdur.at(i) = active;
-			continue;
-		}
-		active = getTimeSigDuration(barandtime.at(i).at(1));
-		tsigdur.at(i) = active;
-	}
-
-	vector<bool> pickup(barandtime.size(), false);
-	for (int i=0; i<(int)barandtime.size() - 1; i++) {
-		if (tsigdur.at(i) == bardur.at(i)) {
-			// actual and expected are the same
+		if (!infile[i].isInterpretation()) {
 			continue;
 		}
-		if (tsigdur.at(i) == tsigdur.at(i+1)) {
-			if (bardur.at(i) + bardur.at(i+1) == tsigdur.at(i)) {
-				pickup.at(i+1) = true;
-				i++;
+		HumNum timeval = infile[i].getDurationFromStart();
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isExclusiveInterpretation()) {
+				exinterpLine = i;
 				continue;
 			}
-		}
-	}
+			if (!token->isKern()) {
+				continue;
+			}
+			if (*token == "*") {
+				continue;
+			}
+			int track = token->getTrack();
+			if (token->isKeySignature()) {
+				m_keys[track][timeval].push_back(token);
+			} else if (token->isOriginalKeySignature()) {
+				m_keys[track][timeval].push_back(token);
+			} else if (token->isModernKeySignature()) {
+				m_keys[track][timeval].push_back(token);
 
-	// check for first-measure pickup
-	if (firstmeasure >= 0) {
-		if (bardur.at(firstmeasure) < tsigdur.at(firstmeasure)) {
-			pickup.at(firstmeasure) = true;
-		}
-	}
+			} else if (token->isInstrumentName()) {
+				m_labels[track][timeval].push_back(token);
+			} else if (token->isOriginalInstrumentName()) {
+				m_labels[track][timeval].push_back(token);
+			} else if (token->isModernInstrumentName()) {
+				m_labels[track][timeval].push_back(token);
 
-	if (m_debugQ) {
-		cerr << "============================" << endl;
-		for (int i=0; i<(int)barandtime.size(); i++) {
-			cerr << pickup.at(i);
-			cerr << "\t";
-			cerr << bardur.at(i);
-			cerr << "\t";
-			cerr << tsigdur.at(i);
-			cerr << "\t";
-			for (int j=0; j<(int)barandtime[i].size(); j++) {
-				cerr << barandtime.at(i).at(j) << "\t";
+			} else if (token->isInstrumentAbbreviation()) {
+				m_labelAbbrs[track][timeval].push_back(token);
+			} else if (token->isOriginalInstrumentAbbreviation()) {
+				m_labelAbbrs[track][timeval].push_back(token);
+			} else if (token->isModernInstrumentAbbreviation()) {
+				m_labelAbbrs[track][timeval].push_back(token);
+
+			} else if (token->isClef()) {
+				m_clefs[track][timeval].push_back(token);
+			} else if (token->isOriginalClef()) {
+				m_clefs[track][timeval].push_back(token);
+			} else if (token->isModernClef()) {
+				m_clefs[track][timeval].push_back(token);
+
+			} else if (token->isMensuration()) {
+				m_mensurations[track][timeval].push_back(token);
+			} else if (token->isOriginalMensuration()) {
+				m_mensurations[track][timeval].push_back(token);
+			} else if (token->isModernMensuration()) {
+				m_mensurations[track][timeval].push_back(token);
 			}
-			cerr << endl;
 		}
-		cerr << endl;
 	}
 
-	// Markup pickup measure notes/rests
-	for (int i=0; i<(int)pickup.size() - 1; i++) {
-		if (!pickup[i]) {
-			continue;
+	if (exinterpLine >= 0) {
+		processExclusiveInterpretationLine(infile, exinterpLine);
+	}
+
+	storeModOriReferenceRecords(infile);
+
+	if (m_infoQ) {
+		if (m_modernQ || m_originalQ) {
+			m_humdrum_text << infile;
 		}
-		markPickupContent(barandtime.at(i).at(0), barandtime.at(i+1).at(0));
+		printInfo();
 	}
 
-	// Pickup/incomplete measures covering three or more barlines are not considered
-	// (these could be used with dashed barlines or similar).
+	if (!(m_modernQ || m_originalQ)) {
+		// nothing to do
+		return;
+	}
 
+	switchModernOriginal(infile);
+	printModoriOutput(infile);
 }
 
 
-
 //////////////////////////////
 //
-// Tool_meter::markPickupContent --
+// Tool_modori::processExclusiveInterpretationLine --
 //
 
-void Tool_meter::markPickupContent(HTp stok, HTp etok) {
-	int endline = etok->getLineIndex();
-	HTp current = stok;
-	while (current) {
-		int line = current->getLineIndex();
-		if (line > endline) {
-			break;
+void Tool_modori::processExclusiveInterpretationLine(HumdrumFile& infile, int line) {
+	vector<HTp> staffish;
+	vector<HTp> staff;
+	vector<vector<HTp>> nonstaff;
+	bool init = false;
+	bool changed = false;
+
+	if (!infile[line].isExclusive()) {
+		return;
+	}
+
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		HTp token = infile.token(line, i);
+		if (!token->isExclusiveInterpretation()) {
+			continue;
 		}
-		if (current->isData()) {
-			HTp field = current;
-			int track = field->getTrack();
-			while (field) {
-				int ttrack = field->getTrack();
-				if (ttrack != track) {
-					break;
-				}
-				if (field->isNull()) {
-					field = field->getNextFieldToken();
-					continue;
-				}
-				field->setValue("auto", "pickup", 1);
-				HumNum nbt = etok->getDurationFromStart() - field->getDurationFromStart();
-				stringstream ntime;
-				ntime.str("");
-				ntime << nbt.getNumerator() << "/" << nbt.getDenominator();
-				field->setValue("auto", "nextBarTime", ntime.str());
-				field = field->getNextFieldToken();
+		if (token->isStaff()) {
+			staff.push_back(token);
+			nonstaff.resize(nonstaff.size() + 1);
+			init = 1;
+		} else {
+			if (init) {
+				nonstaff.back().push_back(token);
 			}
 		}
-		if (current == etok) {
-			break;
+		if (token->isStaff()) {
+			staffish.push_back(token);
+		} else if (*token == "**mod-kern") {
+			staffish.push_back(token);
+		} else if (*token == "**mod-mens") {
+			staffish.push_back(token);
+		} else if (*token == "**ori-kern") {
+			staffish.push_back(token);
+		} else if (*token == "**ori-mens") {
+			staffish.push_back(token);
 		}
-		current = current->getNextToken();
 	}
-}
-
-
 
-//////////////////////////////
-//
-// Tool_meter::getTimeSigDuration --
-//
-
-HumNum Tool_meter::getTimeSigDuration(HTp tsig) {
-	HumNum output = 0;
-	HumRegex hre;
-	if (hre.search(tsig, "^\\*M(\\d+)/(\\d+%?\\d*)")) {
-		int top = hre.getMatchInt(1);
-		string bot = hre.getMatch(2);
-		HumNum botdur = Convert::recipToDuration(bot);
-		output = botdur * top;
+	for (int i=0; i<(int)staff.size(); i++) {
+		changed |= processStaffCompanionSpines(nonstaff[i]);
+	}
+
+	changed |= processStaffSpines(staffish);
+
+	if (changed) {
+		infile[line].createLineFromTokens();
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_meter::printMeterData --
+// Tool_modori::processStaffSpines --
 //
 
-void Tool_meter::printMeterData(HumdrumFile& infile) {
-	bool foundLabel = false;
-	bool foundData  = false;
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
-		}
-
-		if ((!foundData) && (!foundLabel) && (!m_nolabelQ) && infile[i].isData()) {
-			printLabelLine(infile[i]);
-			foundData = true;
-		}
+bool Tool_modori::processStaffSpines(vector<HTp>& tokens) {
 
-		bool hasLabel = false;
-		if ((!m_nolabelQ) && (!foundLabel) && (!foundData)) {
-			if (searchForLabels(infile[i])) {
-				hasLabel = true;
-				foundLabel = true;
-			}
+	HumRegex hre;
+	bool changed = false;
+	for (int i=0; i<(int)tokens.size(); i++) {
+		if (hre.search(tokens[i], "^\\*\\*(ori|mod)-(.*)")) {
+			string newexinterp = "**" + hre.getMatch(2) + "-" + hre.getMatch(1);
+			tokens[i]->setText(newexinterp);
+			changed = true;
+		} else if (hre.search(tokens[i], "^\\*\\*(.*?)-(ori|mod)$")) {
+			string newexinterp = "**" + hre.getMatch(2) + "-" + hre.getMatch(1);
+			tokens[i]->setText(newexinterp);
+			changed = true;
 		}
-		printHumdrumLine(infile[i], hasLabel);
 	}
+
+	return changed;
 }
 
 
 
 //////////////////////////////
 //
-// printLabelLine --
+// Tool_modori::processStaffCompanionSpines --
 //
 
-void Tool_meter::printLabelLine(HumdrumLine& line) {
-	bool forceInterpretation = true;
-	bool printLabels = true;
-	for (int i=0; i<line.getFieldCount(); i++) {
-		HTp token = line.token(i);
-		if (token->isKern()) {
-			i = printKernAndAnalysisSpine(line, i, printLabels, forceInterpretation);
+bool Tool_modori::processStaffCompanionSpines(vector<HTp> tokens) {
+
+	vector<HTp> mods;
+	vector<HTp> oris;
+	vector<HTp> other;
+
+	for (int i=0; i<(int)tokens.size(); i++) {
+		if (tokens[i]->find("**mod-") != string::npos) {
+			mods.push_back(tokens[i]);
+		} else if (tokens[i]->find("**ori-") != string::npos) {
+			oris.push_back(tokens[i]);
 		} else {
-			m_humdrum_text << "*";
-		}
-		if (i < line.getFieldCount() - 1) {
-			m_humdrum_text << "\t";
+			other.push_back(tokens[i]);
 		}
 	}
-	m_humdrum_text << "\n";
-}
 
+	bool gchanged = false;
 
+	if (mods.empty() && oris.empty()) {
+		// Nothing to do.
+		return false;
+	}
 
-//////////////////////////////
-//
-// Tool_meter::printHumdrumLine --
-//
+	// mods and oris should not be mixed, so if there are no
+	// other spines, then also give up:
+	if (other.empty()) {
+		return false;
+	}
 
-void Tool_meter::printHumdrumLine(HumdrumLine& line, bool printLabels) {
 
-	for (int i=0; i<line.getFieldCount(); i++) {
-		HTp token = line.token(i);
-		if (token->isKern()) {
-			i = printKernAndAnalysisSpine(line, i, printLabels);
-		} else {
-			m_humdrum_text << token;
+	if (m_modernQ) {
+		bool changed = false;
+		// Swap (**mod-XXX and **XXX) to (**XXX and **ori-XXX)
+
+		for (int i=0; i<(int)other.size(); i++) {
+			if (other[i] == NULL) {
+				continue;
+			}
+			string target = "**mod-" + other[i]->substr(2);
+			for (int j=0; j<(int)mods.size(); j++) {
+				if (mods[j] == NULL) {
+					continue;
+				}
+				if (*mods[j] != target) {
+					continue;
+				}
+				mods[j]->setText(*other[i]);
+				mods[j] = NULL;
+				changed = true;
+				gchanged = true;
+			}
+			if (changed) {
+				string replacement = "**ori-" + other[i]->substr(2);
+				other[i]->setText(replacement);
+				other[i] = NULL;
+			}
 		}
-		if (i < line.getFieldCount() - 1) {
-			m_humdrum_text << "\t";
+
+	} else if (m_originalQ) {
+		bool changed = false;
+		// Swap (**ori-XXX and **XXX) to (**XXX and **mod-XXX)
+
+		for (int i=0; i<(int)other.size(); i++) {
+			if (other[i] == NULL) {
+				continue;
+			}
+			string target = "**ori-" + other[i]->substr(2);
+			for (int j=0; j<(int)oris.size(); j++) {
+				if (oris[j] == NULL) {
+					continue;
+				}
+				if (*oris[j] != target) {
+					continue;
+				}
+				oris[j]->setText(*other[i]);
+				oris[j] = NULL;
+				changed = true;
+				gchanged = true;
+			}
+			if (changed) {
+				string replacement = "**mod-" + other[i]->substr(2);
+				other[i]->setText(replacement);
+				other[i] = NULL;
+			}
 		}
 	}
-	m_humdrum_text << "\n";
+
+	return gchanged;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_meter::searchForLabels --
+// Tool_modori::storeModOriReferenceRecors --
 //
 
-bool Tool_meter::searchForLabels(HumdrumLine& line) {
-	if (!line.isInterpretation()) {
-		return false;
+void Tool_modori::storeModOriReferenceRecords(HumdrumFile& infile) {
+	m_references.clear();
+
+	vector<HLp> refs = infile.getGlobalReferenceRecords();
+	vector<string> keys(refs.size());
+	for (int i=0; i<(int)refs.size(); i++) {
+		string key = refs.at(i)->getReferenceKey();
+		keys.at(i) = key;
 	}
+
+	vector<int> modernIndex;
+	vector<int> originalIndex;
+
 	HumRegex hre;
-	for (int i=0; i<line.getFieldCount(); i++) {
-		HTp token = line.token(i);
-		if (hre.search(token, "^\\*v[ibB]*:")) {
-			return true;
+	for (int i=0; i<(int)keys.size(); i++) {
+		if (m_modernQ || m_infoQ) {
+			if (hre.search(keys[i], "-mod$")) {
+				modernIndex.push_back(i);
+			}
+		} else if (m_originalQ || m_infoQ) {
+			if (hre.search(keys[i], "-ori$")) {
+				originalIndex.push_back(i);
+			}
 		}
 	}
-	return false;
-}
-
 
+	if (m_modernQ || m_infoQ) {
+		// Store *-mod reference records if there is a pairing:
+		int pairing = -1;
+		for (int i=0; i<(int)modernIndex.size(); i++) {
+			int index = modernIndex[i];
+			pairing = getPairedReference(index, keys);
+			if (pairing >= 0) {
+				m_references.push_back(make_pair(refs[index]->token(0), refs[pairing]->token(0)));
+			}
+		}
+	}
 
-//////////////////////////////
-//
-// Tool_meter::getHumNum --
-//
-
-HumNum Tool_meter::getHumNum(HTp token, const string& parameter) {
-	HumRegex hre;
-	HumNum output;
-	string value = token->getValue("auto", parameter);
-	if (hre.search(value, "(\\d+)/(\\d+)")) {
-		output = hre.getMatchInt(1);
-		output /=  hre.getMatchInt(2);
-	} else if (hre.search(value, "(\\d+)")) {
-		output = hre.getMatchInt(1);
+	if (m_originalQ || m_infoQ) {
+		// Store *-ori reference records if there is a pairing:
+		int pairing = -1;
+		string target;
+		for (int i=0; i<(int)originalIndex.size(); i++) {
+			int index = originalIndex[i];
+			pairing = getPairedReference(index, keys);
+			if (pairing >= 0) {
+				target = keys[index];
+				m_references.push_back(make_pair(refs[index]->token(0), refs[pairing]->token(0)));
+			}
+		}
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_meter::getHumNumString --
+// Tool_modori::getPairedReference --
 //
 
-string Tool_meter::getHumNumString(HumNum input) {
-	stringstream tem;
-	input.printTwoPart(tem);
-	return tem.str();
+int Tool_modori::getPairedReference(int index, vector<string>& keys) {
+	string key = keys.at(index);
+	string tkey = key;
+	if (tkey.size() > 4) {
+		tkey.resize(tkey.size() - 4);
+	} else {
+		return -1;
+	}
+
+	for (int i=0; i<(int)keys.size(); i++) {
+		int ii = index + i;
+		if (ii < (int)keys.size()) {
+			if (tkey == keys.at(ii)) {
+				return ii;
+			}
+		}
+		ii = index - i;
+		if (ii >= 0) {
+			if (tkey == keys.at(ii)) {
+				return ii;
+			}
+		}
+	}
+	return -1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_meter::printKernAndAnalysisSpine --
+// Tool_modori::switchModernOriginal --
 //
 
-int Tool_meter::printKernAndAnalysisSpine(HumdrumLine& line, int index, bool printLabels, bool forceInterpretation) {
-	HTp starttok = line.token(index);
-	int track = starttok->getTrack();
-	int counter = 0;
+void Tool_modori::switchModernOriginal(HumdrumFile& infile) {
 
-	string analysis = ".";
-	string numerator = ".";
-	string denominator = ".";
-	string meter = ".";
-	bool hasNote = false;
-	bool hasRest = false;
+	set<int> changed;
 
-	for (int i=index; i<line.getFieldCount(); i++) {
-		HTp token = line.token(i);
-		int ttrack = token->getTrack();
-		if (ttrack != track) {
-			break;
-		}
-		if (counter > 0) {
-			m_humdrum_text << "\t";
-		}
-		counter++;
-		if (forceInterpretation) {
-			m_humdrum_text << "*";
-		} else {
-			m_humdrum_text << token;
+	if (!m_nokeyQ) {
+		for (int t=1; t<(int)m_keys.size(); ++t) {
+			for (auto it = m_keys.at(t).begin(); it != m_keys.at(t).end(); ++it) {
+				if (it->second.size() != 2) {
+					continue;
+				}
+				bool status = swapKeyStyle(it->second.at(0), it->second.at(1));
+				if (status) {
+					int line = it->second.at(0)->getLineIndex();
+					changed.insert(line);
+					line = it->second.at(1)->getLineIndex();
+					changed.insert(line);
+				}
+			}
 		}
+	}
 
-		if (line.isData() && !forceInterpretation) {
-			if (token->isNull()) {
-				// analysis = ".";
-			} else if (token->isRest() && !m_restQ) {
-				// analysis = ".";
-			} else if ((!token->isNoteAttack()) && !(m_restQ && token->isRest())) {
-				// analysis = ".";
-			} else if ((analysis == ".") && (token->getValueBool("auto", "hasData"))) {
-				string data = token->getValue("auto", "zeroBeat");
-				if (m_restQ) {
-					if (token->isRest()) {
-						hasRest = true;
-					} else {
-						hasNote = true;
-					}
+	if (!m_nolabelsQ) {
+		for (int t=1; t<(int)m_labels.size(); ++t) {
+			for (auto it = m_labels.at(t).begin(); it != m_labels.at(t).end(); ++it) {
+				if (it->second.size() != 2) {
+					continue;
 				}
-				HumNum value;
-				HumNum nvalue;
-				HumNum dvalue;
-				if (!data.empty()) {
-					value = getHumNum(token, "zeroBeat");
-					if (m_numeratorQ) {
-						nvalue = getHumNum(token, "numerator");
-						numerator = getHumNumString(nvalue);
-					}
-					if (m_denominatorQ) {
-						dvalue = getHumNum(token, "denominator");
-						denominator = getHumNumString(dvalue);
-					}
-					if (m_tsigQ) {
-						meter = numerator;
-						meter += "/";
-						meter += denominator;
-					}
+				bool status = swapInstrumentNameStyle(it->second.at(0), it->second.at(1));
+				if (status) {
+					int line = it->second.at(0)->getLineIndex();
+					changed.insert(line);
+					line = it->second.at(1)->getLineIndex();
+					changed.insert(line);
 				}
-				if (!m_zeroQ) {
-					value += 1;
+			}
+		}
+	}
+
+	if (!m_nolabelAbbrsQ) {
+		for (int t=1; t<(int)m_labelAbbrs.size(); ++t) {
+			for (auto it = m_labelAbbrs.at(t).begin(); it != m_labelAbbrs.at(t).end(); ++it) {
+				if (it->second.size() != 2) {
+					continue;
 				}
-				if (m_floatQ) {
-					stringstream tem;
-					if (m_digits) {
-						tem << std::setprecision(m_digits + 1) << value.getFloat();
-					} else {
-						tem << value.getFloat();
-					}
-					analysis = tem.str();
-					if (m_commaQ) {
-						HumRegex hre;
-						hre.replaceDestructive(analysis, ",", "\\.");
-					}
-				} else {
-					analysis = getHumNumString(value);
+				bool status = swapInstrumentAbbreviationStyle(it->second.at(0), it->second.at(1));
+				if (status) {
+					int line = it->second.at(0)->getLineIndex();
+					changed.insert(line);
+					line = it->second.at(1)->getLineIndex();
+					changed.insert(line);
 				}
 			}
-		} else if (line.isInterpretation() || forceInterpretation) {
-			if (token->compare(0, 2, "**") == 0) {
-				analysis = "**cdata-beat";
-				if (m_tsigQ) {
-					meter = "**cdata-tsig";
+		}
+	}
+
+	if (!m_nolyricsQ) {
+		bool adjust = false;
+		int line = -1;
+		for (int i=0; i<(int)m_lyrics.size(); i++) {
+			HTp token = m_lyrics[i];
+			line = token->getLineIndex();
+			if (m_modernQ) {
+				if (*token == "**text") {
+					adjust = true;
+					token->setText("**ori-text");
+				} else if (*token == "**mod-text") {
+					adjust = true;
+					token->setText("**text");
 				}
-				if (m_numeratorQ) {
-					numerator = "**cdata-num";
+			} else {
+				if (*token == "**text") {
+					adjust = true;
+					token->setText("**mod-text");
+				} else if (*token == "**ori-text") {
+					adjust = true;
+					token->setText("**text");
 				}
-				if (m_denominatorQ) {
-					denominator = "**cdata-den";
+			}
+		}
+		if (adjust && (line >= 0)) {
+			infile[line].createLineFromTokens();
+		}
+	}
+
+	if (!m_nolotextQ) {
+		HumRegex hre;
+		for (int i=0; i<(int)m_lotext.size(); i++) {
+			HTp token = m_lotext[i];
+			int line = token->getLineIndex();
+			if (hre.search(token, "^!!?LO:(TX|DY).*:mod=")) {
+				string text = *token;
+				hre.replaceDestructive(text, ":ori=", ":t=");
+				hre.replaceDestructive(text, ":t=", ":mod=");
+				token->setText(text);
+				changed.insert(line);
+			} else if (hre.search(token, "^!!?LO:(TX|DY).*:ori=")) {
+				string text = *token;
+				hre.replaceDestructive(text, ":mod=", ":t=");
+				hre.replaceDestructive(text, ":t=", ":ori=");
+				token->setText(text);
+				changed.insert(line);
+			}
+		}
+	}
+
+	if (!m_norefsQ) {
+		HumRegex hre;
+		for (int i=0; i<(int)m_references.size(); i++) {
+			HTp first = m_references[i].first;
+			HTp second = m_references[i].second;
+
+			if (m_modernQ) {
+				if (hre.search(first, "^!!![^:]*?-mod:")) {
+					string text = *first;
+					hre.replaceDestructive(text, ":", "-...:");
+					first->setText(text);
+					infile[first->getLineIndex()].createLineFromTokens();
+
+					text = *second;
+					hre.replaceDestructive(text, "-ori:", ":");
+					second->setText(text);
+					infile[second->getLineIndex()].createLineFromTokens();
 				}
-			} else if (*token == "*-") {
-				analysis = "*-";
-				numerator = "*-";
-				denominator = "*-";
-				meter = "*-";
-			} else if (token->isTimeSignature()) {
-				analysis = *token;
-			} else {
-				analysis = "*";
-				numerator = "*";
-				denominator = "*";
-				meter = "*";
-				if (printLabels) {
-					if (m_quarterQ) {
-						analysis = "*vi:4ths:";
-					} else if (m_eighthQ) {
-						analysis = "*vi:8ths:";
-					} else if (m_halfQ) {
-						analysis = "*vi:half:";
-					} else if (m_wholeQ) {
-						analysis = "*vi:whole:";
-					} else if (m_sixteenthQ) {
-						analysis = "*vi:16ths:";
-					} else {
-						analysis = "*vi:beat:";
+			} else if (m_originalQ) {
+				if (hre.search(first, "^!!![^:]*?-ori:")) {
+					string text = *first;
+					hre.replaceDestructive(text, ":", "-...:");
+					first->setText(text);
+					infile[first->getLineIndex()].createLineFromTokens();
+
+					text = *second;
+					hre.replaceDestructive(text, "-mod:", ":");
+					second->setText(text);
+					infile[second->getLineIndex()].createLineFromTokens();
+				}
+			}
+
+		}
+	}
+
+	// Mensurations are only used for "original" display.  It is possible
+	// to use a modern metric signature (common time or cut time) but these
+	// are not currently allowed.  Only one *met at a given time position
+	// is allowed.
+
+	if (!m_nomensurationQ) {
+		for (int t=1; t<(int)m_mensurations.size(); ++t) {
+			for (auto it = m_mensurations.at(t).begin(); it != m_mensurations.at(t).end(); ++it) {
+				if (it->second.size() == 1) {
+					// swap omet to met, or met to omet:
+					bool status = flipMensurationStyle(it->second.at(0));
+					if (status) {
+						int line = it->second.at(0)->getLineIndex();
+						changed.insert(line);
 					}
-					numerator = "*vi:top:";
-					denominator = "*vi:bot:";
-					meter = "*vi:tsig:";
-					if (m_joinQ) {
-						numerator = "";
-						denominator = "";
-						meter = "";
+				} else if (it->second.size() == 2) {
+					// swap omet/met or mmet/met:
+					bool status = swapMensurationStyle(it->second.at(0), it->second.at(1));
+					if (status) {
+						int line = it->second.at(0)->getLineIndex();
+						changed.insert(line);
+						line = it->second.at(1)->getLineIndex();
+						changed.insert(line);
 					}
 				}
 			}
-		} else if (line.isBarline()) {
-			analysis = *token;
-			numerator = *token;
-			denominator = *token;
-			meter = *token;
-		} else if (line.isCommentLocal()) {
-			analysis = "!";
-			numerator = "!";
-			denominator = "!";
-			meter = "!";
-		} else {
-			cerr << "STRANGE LINE: " << line << endl;
 		}
 	}
 
-	if (m_joinQ) {
-		if (line.isData() && !forceInterpretation) {
-			if (m_tsigQ) {
-					m_humdrum_text << "\t" << meter;
-			} else {
-				if (m_numeratorQ) {
-					m_humdrum_text << "\t" << numerator;
+	if (!m_noclefQ) {
+		for (int t=1; t<(int)m_clefs.size(); ++t) {
+			for (auto it = m_clefs.at(t).begin(); it != m_clefs.at(t).end(); ++it) {
+				if (it->second.size() != 2) {
+					continue;
 				}
-				if (m_denominatorQ) {
-					m_humdrum_text << "\t" << denominator;
+				bool status = swapClefStyle(it->second.at(0), it->second.at(1));
+				if (status) {
+					int line = it->second.at(0)->getLineIndex();
+					changed.insert(line);
+					line = it->second.at(1)->getLineIndex();
+					changed.insert(line);
 				}
 			}
 		}
-		if (!m_nobeatQ) {
-			if (line.isData() && !forceInterpretation) {
-				m_humdrum_text << ":";
-			} else {
-				m_humdrum_text << "\t";
-			}
-			m_humdrum_text << analysis;
-			if (line.isData() && hasRest && !hasNote) {
-				m_humdrum_text << "r";
+	}
+
+	for (auto it = changed.begin(); it != changed.end(); ++it) {
+		int line = *it;
+		infile[line].createLineFromTokens();
+	}
+
+	updateLoMo(infile);
+}
+
+
+//////////////////////////////
+//
+// Tool_modori::printModoriOutput --
+//
+
+void Tool_modori::printModoriOutput(HumdrumFile& infile) {
+	string state;
+	if (m_modernQ) {
+
+		// convert to modern
+		for (int i=0; i<infile.getLineCount(); i++) {
+			if (infile[i].isCommentGlobal()) {
+				HTp token = infile.token(i, 0);
+				if (*token == "!!LO:MO:mod") {
+				   state = "mod";
+					m_humdrum_text << token << endl;
+					continue;
+				} else if (*token == "!!LO:MO:ori") {
+				   state = "ori";
+					m_humdrum_text << token << endl;
+					continue;
+				} else if (*token == "!!LO:MO:end") {
+					state = "";
+					m_humdrum_text << token << endl;
+					continue;
+				}
 			}
-		}
-	} else {
-		if (!m_nobeatQ) {
-			m_humdrum_text << "\t" << analysis;
-			if (line.isData() && hasRest && !hasNote) {
-				m_humdrum_text << "r";
+			if (state == "mod") {
+				// Remove global comment prefix "!! ".  Complain if not there.
+				if (infile[i].compare(0, 3, "!! ") != 0) {
+					cerr << "Error: line does not start with \"!! \":\t" << infile[i] << endl;
+				} else {
+					m_humdrum_text << infile[i].substr(3) << endl;
+				}
+			} else if (state == "ori") {
+				// Add global comment prefix "!! ".
+				m_humdrum_text << "!! " << infile[i] << endl;
+			} else {
+				m_humdrum_text << infile[i] << endl;
 			}
 		}
-		if (m_tsigQ) {
-				m_humdrum_text << "\t" << meter;
-		} else {
-			if (m_numeratorQ) {
-				m_humdrum_text << "\t" << numerator;
+
+	} else if (m_originalQ) {
+
+		// convert to original
+		for (int i=0; i<infile.getLineCount(); i++) {
+			if (infile[i].isCommentGlobal()) {
+				HTp token = infile.token(i, 0);
+				if (*token == "!!LO:MO:mod") {
+				   state = "mod";
+					m_humdrum_text << token << endl;
+					continue;
+				} else if (*token == "!!LO:MO:ori") {
+				   state = "ori";
+					m_humdrum_text << token << endl;
+					continue;
+				} else if (*token == "!!LO:MO:end") {
+					state = "";
+					m_humdrum_text << token << endl;
+					continue;
+				}
 			}
-			if (m_denominatorQ) {
-				m_humdrum_text << "\t" << denominator;
+			if (state == "ori") {
+				// Remove global comment prefix "!! ".  Complain if not there.
+				if (infile[i].compare(0, 3, "!! ") != 0) {
+					cerr << "Error: line does not start with \"!! \":\t" << infile[i] << endl;
+				} else {
+					m_humdrum_text << infile[i].substr(3) << endl;
+				}
+			} else if (state == "mod") {
+				// Add global comment prefix "!! ".
+				m_humdrum_text << "!! " << infile[i] << endl;
+			} else {
+				m_humdrum_text << infile[i] << endl;
 			}
 		}
 
 	}
-
-	return index + counter - 1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_meter::getMeterData --
+// Tool_modori::updateLoMo --
 //
 
-void Tool_meter::getMeterData(HumdrumFile& infile) {
-
-	int maxtrack = infile.getMaxTrack();
-	vector<HumNum> curNum(maxtrack + 1, 0);
-	vector<HumNum> curDen(maxtrack + 1, 0);
-	vector<HumNum> curBeat(maxtrack + 1, 0);
-	vector<HumNum> curBarTime(maxtrack + 1, 0);
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		processLine(infile[i], curNum, curDen, curBeat, curBarTime);
+void Tool_modori::updateLoMo(HumdrumFile& infile) {
+	for (int i=0; i<(int)m_lomo.size(); i++) {
+		processLoMo(m_lomo[i]);
 	}
 }
 
@@ -98944,618 +102706,491 @@ void Tool_meter::getMeterData(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_meter::processLine --
+// Tool_modori::processLoMo --
 //
 
-void Tool_meter::processLine(HumdrumLine& line, vector<HumNum>& curNum,
-		vector<HumNum>& curDen, vector<HumNum>& curBeat,
-		vector<HumNum>& curBarTime) {
-
-	int fieldCount = line.getFieldCount();
-
-	if (!line.hasSpines()) {
-		return;
-	}
-
+void Tool_modori::processLoMo(HTp lomo) {
 	HumRegex hre;
-	if (line.isBarline()) {
-		for (int i=0; i<fieldCount; i++) {
-			HTp token = line.token(i);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (hre.search(token, "-")) {
-				// invisible barline: ignore
-				continue;
-			}
-			int track = token->getTrack();
-			HumNum curTime = token->getDurationFromStart();
-			curBarTime.at(track) = curTime;
-		}
-		return;
-	}
 
-	if (line.isInterpretation()) {
-		// check for time signatures
-		for (int i=0; i<fieldCount; i++) {
-			HTp token = line.token(i);
-			if (!token->isKern()) {
-				continue;
+	if (m_modernQ) {
+		string text = lomo->getText();
+		string modtext;
+		string oritext;
+		string base;
+		string rest;
+		if (hre.search(text, "(.*):mod=([^:]*)(.*)")) {
+			base = hre.getMatch(1);
+			modtext = hre.getMatch(2);
+			rest = hre.getMatch(3);
+			hre.replaceDestructive(modtext, ":", "&colon;", "g");
+			HTp current = lomo->getNextToken();
+			// null parameter allows next following null token
+			// to be swapped out
+			bool nullQ = hre.search(text, ":null:");
+			if (!nullQ) {
+				while (current) {
+					if (current->isNull()) {
+						current = current->getNextToken();
+						continue;
+					}
+					break;
+				}
 			}
-			if (hre.search(token, "^\\*M(\\d+)/(\\d+)")) {
-				int top = hre.getMatchInt(1);
-				int bot = hre.getMatchInt(2);
-				int track = token->getTrack();
-				curNum.at(track) = top;
-				curDen.at(track) = bot;
-				curBeat.at(track) = 0;
-			} else if (hre.search(token, "^\\*beat:\\s*([\\d.%]+)\\s*$")) {
-				int track = token->getTrack();
-				string recip = hre.getMatch(1);
-				curBeat.at(track) = Convert::recipToDuration(recip);
+			if (current) {
+				string oritext = current->getText();
+				hre.replaceDestructive(oritext, "&colon;", ":", "g");
+				current->setText(modtext);
+				string newtext = base;
+				newtext += ":ori=";
+				newtext += oritext;
+				newtext += rest;
+				lomo->setText(newtext);
+				lomo->getLine()->createLineFromTokens();
+				current->getLine()->createLineFromTokens();
 			}
 		}
-		return;
-	}
-
-	if (line.isData()) {
-		// check for time signatures
-		for (int i=0; i<fieldCount; i++) {
-			HTp token = line.token(i);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
-				continue;
-			}
-			if ((!m_restQ) && token->isRest()) {
-				continue;
-			}
-			if (!token->isNoteAttack() && !(m_restQ && token->isRest())) {
-				continue;
-			}
-			int pickup = token->getValueInt("auto", "pickup");
-			int track = token->getTrack();
-			stringstream value;
-			value.str("");
-			value << curNum.at(track);
-			token->setValue("auto", "numerator", value.str());
-			value.str("");
-			value << curDen.at(track);
-			token->setValue("auto", "denominator", value.str());
-			HumNum curTime = token->getDurationFromStart();
-			HumNum q;
-			if (pickup) {
-				HumNum meterDur = curNum.at(track);
-				meterDur /= curDen.at(track);
-				meterDur *= 4;
-				HumNum nbt = getHumNum(token, "nextBarTime");
-				q = meterDur - nbt;
-			} else {
-				q = curTime - curBarTime.at(track);
-			}
-			value.str("");
-			value << q;
-			token->setValue("auto", "q", value.str());
-			bool compound = false;
-			int multiple = curNum.at(track).getNumerator() / 3;
-			int remainder = curNum.at(track).getNumerator() % 3;
-			int bottom = curDen.at(track).getNumerator();
-			if ((curBeat.at(track) == 0) && (bottom >= 8) && (multiple > 1) && (remainder == 0)) {
-				compound = true;
-			}
 
-			HumNum qq = q;
-			if (m_quarterQ) {
-				// do nothing (prior calculations are done in quarter notes)
-			} else if (m_halfQ) {
-				qq /= 2;
-			} else if (m_wholeQ) {
-				qq /= 4;
-			} else if (m_eighthQ) {
-				qq *= 2;
-			} else if (m_sixteenthQ) {
-				qq *= 4;
-			} else {
-				// convert quarter note metric positions into beat positions
-				if (compound) {
-					qq *= curDen.at(track);
-					qq /= 4;
-					qq /= 3;
-				} else if (curBeat.at(track) > 0) {
-					qq /= curBeat.at(track);
-				} else {
-					qq *= curDen.at(track);
-					qq /= 4;
+	} else if (m_originalQ) {
+		string text = lomo->getText();
+		string modtext;
+		string oritext;
+		string base;
+		string rest;
+		if (hre.search(text, "(.*):ori=([^:]*)(.*)")) {
+			base = hre.getMatch(1);
+			oritext = hre.getMatch(2);
+			rest = hre.getMatch(3);
+			hre.replaceDestructive(oritext, ":", "&colon;", "g");
+			HTp current = lomo->getNextToken();
+			// null parameter allows next following null token
+			// to be swapped out
+			bool nullQ = hre.search(text, ":null:");
+			if (nullQ) {
+				while (current) {
+					if (current->isNull()) {
+						current = current->getNextToken();
+						continue;
+					}
+					break;
 				}
 			}
-
-			value.str("");
-			value << qq;
-			token->setValue("auto", "zeroBeat", value.str());
-			token->setValue("auto", "hasData", 1);
-
+			if (current) {
+				string modtext = current->getText();
+				hre.replaceDestructive(modtext, "&colon;", ":", "g");
+				current->setText(oritext);
+				string newtext = base;
+				newtext += ":mod=";
+				newtext += modtext;
+				newtext += rest;
+				lomo->setText(newtext);
+				lomo->getLine()->createLineFromTokens();
+				current->getLine()->createLineFromTokens();
+			}
 		}
-		return;
 	}
 }
 
 
 
-
-/////////////////////////////////
-//
-// Tool_gridtest::Tool_metlev -- Set the recognized options for the tool.
-//
-
-Tool_metlev::Tool_metlev(void) {
-	define("a|append=b",         "append data analysis to input file");
-	define("p|prepend=b",        "prepend data analysis to input file");
-	define("c|composite=b",      "generate composite rhythm");
-	define("i|integer=b",        "quantize metric levels to int values");
-	define("x|attacks-only=b",   "only mark lines with note attacks");
-	define("G|no-grace-notes=b", "do not mark grace note lines");
-	define("k|kern-spine=i:1",   "analyze only given kern spine");
-	define("e|exinterp=s:blev",  "exclusive interpretation type for output");
-}
-
-
-
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_metlev::run -- Primary interfaces to the tool.
+// Tool_modori::flipMensurationStyle -- Returns true if swapped.
 //
 
-bool Tool_metlev::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+bool Tool_modori::flipMensurationStyle(HTp token) {
+	bool output = false;
+	HumRegex hre;
+	string text;
+	if (token->isMensuration()) {
+		// switch to invisible mensuration
+		text = "*omet";
+		text += token->substr(4);
+		token->setText(text);
+		output = true;
+	} else if (token->isOriginalMensuration()) {
+		// switch to visible mensuration
+		text = "*met";
+		text += token->substr(5);
+		token->setText(text);
+		output = true;
 	}
-	return status;
-}
-
-
-bool Tool_metlev::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	return run(infile, out);
-}
-
 
-bool Tool_metlev::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	out << infile;
-	return status;
+	return output;
 }
 
 
-bool Tool_metlev::run(HumdrumFile& infile) {
-	int lineCount = infile.getLineCount();
-	if (lineCount == 0) {
-		m_error_text << "No input data";
-		return false;
-	}
 
-	string exinterp = getString("exinterp");
-	if (exinterp.empty()) {
-		exinterp = "**blev";
-	} else if (exinterp[0] != '*') {
-		exinterp.insert(0, "*");
-	}
-	if (exinterp[1] != '*') {
-		exinterp.insert(0, "*");
-	}
+//////////////////////////////
+//
+// Tool_modori::swapKeyStyle -- Returns true if swapped.
+//
 
-	m_kernspines = infile.getKernSpineStartList();
+bool Tool_modori::swapKeyStyle(HTp one, HTp two) {
+	bool mtype1 = false;
+	bool mtype2 = false;
+	bool otype1 = false;
+	bool otype2 = false;
+	bool ktype1 = false;
+	bool ktype2 = false;
+	bool output = false;
 
-	vector<double> beatlev(lineCount, NAN);
-	int track = 0;
-	if (m_kernspines.size() > 0) {
-		track = m_kernspines[0]->getTrack();
-	} else {
-		m_error_text << "No **kern spines in input file" << endl;
-		return false;
+	if (one->isKeySignature()) {
+		ktype1 = true;
+	} else if (one->isModernKeySignature()) {
+		mtype1 = true;
+	} else if (one->isOriginalKeySignature()) {
+		otype1 = true;
 	}
-	infile.getMetricLevels(beatlev, track, NAN);
 
-	for (int i=0; i<lineCount; i++) {
-		if (!infile[i].isData()) {
-				continue;
-		}
-		if (getBoolean("no-grace-notes") && (infile[i].getDuration() == 0)) {
-			beatlev[i] = NAN;
-			continue;
-		}
-		if (getBoolean("attacks-only")) {
-			if (!infile[i].getKernNoteAttacks()) {
-				beatlev[i] = NAN;
-				continue;
-			}
-		}
-		if (beatlev[i] - (int)beatlev[i] != 0.0) {
-			if (getBoolean("integer")) {
-					beatlev[i] = floor(beatlev[i]);
-			} else {
-				beatlev[i] = Convert::significantDigits(beatlev[i], 2);
-			}
-		}
+	if (two->isKeySignature()) {
+		ktype2 = true;
+	} else if (two->isModernKeySignature()) {
+		mtype2 = true;
+	} else if (two->isOriginalKeySignature()) {
+		otype2 = true;
 	}
 
-	if (getBoolean("kern-spine")) {
-		int kspine = getInteger("kern-spine") - 1;
-		if ((kspine >= 0) && (kspine < (int)m_kernspines.size())) {
-			vector<vector<double> > results;
-			fillVoiceResults(results, infile, beatlev);
-			if (kspine == (int)m_kernspines.size() - 1) {
-				infile.appendDataSpine(results.back(), "nan", exinterp);
-			} else {
-				int track = m_kernspines[kspine+1]->getTrack();
-				infile.insertDataSpineBefore(track, results[kspine],
-						"nan", exinterp);
-			}
-			infile.createLinesFromTokens();
-			return true;
-		}
-	} else if (getBoolean("append")) {
-		infile.appendDataSpine(beatlev, "nan", exinterp);
-		infile.createLinesFromTokens();
-		return true;
-	} else if (getBoolean("prepend")) {
-		infile.prependDataSpine(beatlev, "nan", exinterp);
-		infile.createLinesFromTokens();
-		return true;
-	} else if (getBoolean("composite")) {
-		infile.prependDataSpine(beatlev, "nan", exinterp);
-		infile.printFieldIndex(0, m_humdrum_text);
-		infile.clear();
-		infile.readString(m_humdrum_text.str());
-	} else {
-		vector<vector<double> > results;
-		fillVoiceResults(results, infile, beatlev);
-		infile.appendDataSpine(results.back(), "nan", exinterp);
-		for (int i = (int)results.size()-1; i>0; i--) {
-			int track = m_kernspines[i]->getTrack();
-			infile.insertDataSpineBefore(track, results[i-1], "nan", exinterp);
+	if (m_modernQ) {
+		// Show the modern key signature.  If one key is *mk and the
+		// other is *k then change *mk to *k and *k to *ok respectively.
+		if (ktype1 && mtype2) {
+			convertKeySignatureToOriginal(one);
+			convertKeySignatureToRegular(two);
+			output = true;
+		} else if (mtype1 && ktype2) {
+			convertKeySignatureToRegular(one);
+			convertKeySignatureToOriginal(two);
+			output = true;
+		}
+	} else if (m_originalQ) {
+		// Show the original key.  If one key is *ok and the
+		// other is *k then change *ok to *k and *k to *mk respectively.
+		if (ktype1 && otype2) {
+			convertKeySignatureToModern(one);
+			convertKeySignatureToRegular(two);
+			output = true;
+		} else if (otype1 && ktype2) {
+			convertKeySignatureToRegular(one);
+			convertKeySignatureToModern(two);
+			output = true;
 		}
-		infile.createLinesFromTokens();
-		return true;
 	}
-
-	return false;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_metlev::fillVoiceResults -- Split the metric level analysis into values
-//     for each voice.
+// Tool_modori::swapInstrumentNameStyle -- Returns true if swapped.
 //
 
-void Tool_metlev::fillVoiceResults(vector<vector<double> >& results,
-		HumdrumFile& infile, vector<double>& beatlev) {
+bool Tool_modori::swapInstrumentNameStyle(HTp one, HTp two) {
+	bool mtype1 = false;
+	bool mtype2 = false;
+	bool otype1 = false;
+	bool otype2 = false;
+	bool ktype1 = false;
+	bool ktype2 = false;
+	bool output = false;
 
-	results.resize(m_kernspines.size());
-	for (int i=0; i<(int)results.size(); i++) {
-		results[i].resize(beatlev.size());
-		fill(results[i].begin(), results[i].end(), NAN);
+	if (one->isInstrumentName()) {
+		ktype1 = true;
+	} else if (one->isModernInstrumentName()) {
+		mtype1 = true;
+	} else if (one->isOriginalInstrumentName()) {
+		otype1 = true;
 	}
-	int track;
-	vector<int> rtracks(infile.getTrackCount() + 1, -1);
-	for (int i=0; i<(int)m_kernspines.size(); i++) {
-		int track = m_kernspines[i]->getTrack();
-		rtracks[track] = i;
+
+	if (two->isInstrumentName()) {
+		ktype2 = true;
+	} else if (two->isModernInstrumentName()) {
+		mtype2 = true;
+	} else if (two->isOriginalInstrumentName()) {
+		otype2 = true;
 	}
 
-	bool attacksQ = getBoolean("attacks-only");
-	vector<int> nonnullcount(m_kernspines.size(), 0);
-	vector<int> attackcount(m_kernspines.size(), 0);
-	HTp token;
-	int voice;
-	int i, j;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
-				continue;
-			}
-			track = token->getTrack();
-			voice = rtracks[track];
-			nonnullcount[voice]++;
-			if (token->isNoteAttack()) {
-				attackcount[voice]++;
-			}
+	if (m_modernQ) {
+		// Show the modern instrument name.  If one name is *mI" and the
+		// other is *I" then change *mI" to *I" and *I" to *oI" respectively.
+		if (ktype1 && mtype2) {
+			convertInstrumentNameToOriginal(one);
+			convertInstrumentNameToRegular(two);
+			output = true;
+		} else if (mtype1 && ktype2) {
+			convertInstrumentNameToRegular(one);
+			convertInstrumentNameToOriginal(two);
+			output = true;
 		}
-		for (int v=0; v<(int)m_kernspines.size(); v++) {
-			if (attacksQ) {
-				if (attackcount[v]) {
-					results[v][i] = beatlev[i];
-					attackcount[v] = 0;
-				}
-			} else {
-				if (nonnullcount[v]) {
-					results[v][i] = beatlev[i];
-				}
-				nonnullcount[v] = 0;
-			}
+	} else if (m_originalQ) {
+		// Show the original key.  If one key is *ok and the
+		// other is *k then change *ok to *k and *k to *mk respectively.
+		if (ktype1 && otype2) {
+			convertInstrumentNameToModern(one);
+			convertInstrumentNameToRegular(two);
+			output = true;
+		} else if (otype1 && ktype2) {
+			convertInstrumentNameToRegular(one);
+			convertInstrumentNameToModern(two);
+			output = true;
 		}
 	}
+	return output;
 }
 
 
 
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_modori::Tool_modori -- Set the recognized options for the tool.
+// Tool_modori::swapInstrumentAbbreviationStyle -- Returns true if swapped.
 //
 
-Tool_modori::Tool_modori(void) {
-	define("m|modern=b",                                                 "prepare score for modern style");
-	define("o|original=b",                                               "prepare score for original style");
-	define("d|info=b",                                                   "display key/clef/mensuration information");
-	define("I|no-instrument-name|no-instrument-names=b",                 "do not change part labels");
-	define("A|no-instrument-abbreviation|no-instrument-abbreviations=b", "do not change part label abbreviations");
-	define("C|no-clef|no-clefs=b",                                       "do not change clefs");
-	define("K|no-key|no-keys=b",                                         "do not change key signatures");
-	define("L|no-lyrics=b",                                              "do not change **text exclusive interpretations");
-	define("M|no-mensuration|no-mensurations=b",                         "do not change mensurations");
-	define("R|no-references=b",                                          "do not change reference records keys");
-	define("T|no-text=b",                                                "do not change !LO:(TX|DY) layout parameters");
-}
-
-
-
-/////////////////////////////////
-//
-// Tool_modori::run -- Do the main work of the tool.
-//
+bool Tool_modori::swapInstrumentAbbreviationStyle(HTp one, HTp two) {
+	bool mtype1 = false;
+	bool mtype2 = false;
+	bool otype1 = false;
+	bool otype2 = false;
+	bool ktype1 = false;
+	bool ktype2 = false;
+	bool output = false;
 
-bool Tool_modori::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	if (one->isInstrumentAbbreviation()) {
+		ktype1 = true;
+	} else if (one->isModernInstrumentAbbreviation()) {
+		mtype1 = true;
+	} else if (one->isOriginalInstrumentAbbreviation()) {
+		otype1 = true;
 	}
-	return status;
-}
 
-
-bool Tool_modori::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (two->isInstrumentAbbreviation()) {
+		ktype2 = true;
+	} else if (two->isModernInstrumentAbbreviation()) {
+		mtype2 = true;
+	} else if (two->isOriginalInstrumentAbbreviation()) {
+		otype2 = true;
 	}
-	return status;
-}
 
-
-bool Tool_modori::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (m_modernQ) {
+		// Show the modern instrument name.  If one name is *mI" and the
+		// other is *I" then change *mI" to *I" and *I" to *oI" respectively.
+		if (ktype1 && mtype2) {
+			convertInstrumentAbbreviationToOriginal(one);
+			convertInstrumentAbbreviationToRegular(two);
+			output = true;
+		} else if (mtype1 && ktype2) {
+			convertInstrumentAbbreviationToRegular(one);
+			convertInstrumentAbbreviationToOriginal(two);
+			output = true;
+		}
+	} else if (m_originalQ) {
+		// Show the original key.  If one key is *ok and the
+		// other is *k then change *ok to *k and *k to *mk respectively.
+		if (ktype1 && otype2) {
+			convertInstrumentAbbreviationToModern(one);
+			convertInstrumentAbbreviationToRegular(two);
+			output = true;
+		} else if (otype1 && ktype2) {
+			convertInstrumentAbbreviationToRegular(one);
+			convertInstrumentAbbreviationToModern(two);
+			output = true;
+		}
 	}
-	return status;
-}
-
-
-bool Tool_modori::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_modori::swapMensurationStyle -- Returns true if swapped.
 //
 
-void Tool_modori::initialize(void) {
-	m_infoQ = getBoolean("info");
-	m_modernQ = getBoolean("modern");
-	m_originalQ = getBoolean("original");
-	if (m_modernQ && m_originalQ) {
-		// if both options are used, ignore -m:
-		m_modernQ = false;
+bool Tool_modori::swapMensurationStyle(HTp one, HTp two) {
+	bool mtype1 = false;
+	bool mtype2 = false;
+	bool otype1 = false;
+	bool otype2 = false;
+	bool ktype1 = false;
+	bool ktype2 = false;
+	bool output = false;
+
+	if (one->isMensuration()) {
+		ktype1 = true;
+	} else if (one->isModernMensuration()) {
+		mtype1 = true;
+	} else if (one->isOriginalMensuration()) {
+		otype1 = true;
 	}
-	m_nokeyQ         = getBoolean("no-key");
-	m_noclefQ        = getBoolean("no-clef");
-	m_nolotextQ      = getBoolean("no-text");
-	m_nolyricsQ      = getBoolean("no-lyrics");
-	m_norefsQ        = getBoolean("no-references");
-	m_nomensurationQ = getBoolean("no-mensuration");
-	m_nolabelsQ      = getBoolean("no-instrument-names");
-	m_nolabelAbbrsQ  = getBoolean("no-instrument-abbreviations");
+
+	if (two->isMensuration()) {
+		ktype2 = true;
+	} else if (two->isModernMensuration()) {
+		mtype2 = true;
+	} else if (two->isOriginalMensuration()) {
+		otype2 = true;
+	}
+
+	if (m_modernQ) {
+		if (ktype1 && mtype2) {
+			convertMensurationToOriginal(one);
+			convertMensurationToRegular(two);
+			output = true;
+		} else if (mtype1 && ktype2) {
+			convertMensurationToRegular(one);
+			convertMensurationToOriginal(two);
+			output = true;
+		}
+	} else if (m_originalQ) {
+		if (ktype1 && otype2) {
+			convertMensurationToModern(one);
+			convertMensurationToRegular(two);
+			output = true;
+		} else if (otype1 && ktype2) {
+			convertMensurationToRegular(one);
+			convertMensurationToModern(two);
+			output = true;
+		}
+	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::processFile --
+// Tool_modori::swapClefStyle -- Returns true if swapped.
 //
 
-void Tool_modori::processFile(HumdrumFile& infile) {
-	m_keys.clear();
-	m_labels.clear();
-	m_labelAbbrs.clear();
-	m_clefs.clear();
-	m_mensurations.clear();
-	m_references.clear();
-	m_lyrics.clear();
-	m_lotext.clear();
+bool Tool_modori::swapClefStyle(HTp one, HTp two) {
+	bool mtype1 = false;
+	bool mtype2 = false;
+	bool otype1 = false;
+	bool otype2 = false;
+	bool ktype1 = false;
+	bool ktype2 = false;
+	bool output = false;
 
-	int maxtrack = infile.getMaxTrack();
-	m_keys.resize(maxtrack+1);
-	m_labels.resize(maxtrack+1);
-	m_labelAbbrs.resize(maxtrack+1);
-	m_clefs.resize(maxtrack+1);
-	m_mensurations.resize(maxtrack+1);
-	m_references.reserve(1000);
-	m_lyrics.reserve(1000);
-	m_lotext.reserve(1000);
+	if (one->isClef()) {
+		ktype1 = true;
+	} else if (one->isModernClef()) {
+		mtype1 = true;
+	} else if (one->isOriginalClef()) {
+		otype1 = true;
+	}
 
-	HumRegex hre;
-	int exinterpLine = -1;
+	if (two->isClef()) {
+		ktype2 = true;
+	} else if (two->isModernClef()) {
+		mtype2 = true;
+	} else if (two->isOriginalClef()) {
+		otype2 = true;
+	}
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isCommentLocal() || infile[i].isCommentGlobal()) {
-			for (int j=0; j<infile[i].getFieldCount(); j++) {
-				HTp token = infile.token(i, j);
-				if (*token == "!") {
-					continue;
-				}
-				if (hre.search(token, "^!!?LO:(TX|DY).*:mod=")) {
-					m_lotext.push_back(token);
-				} else if (hre.search(token, "^!!?LO:(TX|DY).*:ori=")) {
-					m_lotext.push_back(token);
-				}
-				if (hre.search(token, "^!LO:MO:.*")) {
-					m_lomo.push_back(token);
-				}
-			}
+	if (m_modernQ) {
+		// Show the modern key signature.  If one key is *mk and the
+		// other is *k then change *mk to *k and *k to *ok respectively.
+		if (ktype1 && mtype2) {
+			convertClefToOriginal(one);
+			convertClefToRegular(two);
+			output = true;
+		} else if (mtype1 && ktype2) {
+			convertClefToRegular(one);
+			convertClefToOriginal(two);
+			output = true;
 		}
-		if (!infile[i].isInterpretation()) {
-			continue;
+	} else if (m_originalQ) {
+		// Show the original key.  If one key is *ok and the
+		// other is *k then change *ok to *k and *k to *mk respectively.
+		if (ktype1 && otype2) {
+			convertClefToModern(one);
+			convertClefToRegular(two);
+			output = true;
+		} else if (otype1 && ktype2) {
+			convertClefToRegular(one);
+			convertClefToModern(two);
+			output = true;
 		}
-		HumNum timeval = infile[i].getDurationFromStart();
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isExclusiveInterpretation()) {
-				exinterpLine = i;
-				continue;
-			}
-			if (!token->isKern()) {
-				continue;
-			}
-			if (*token == "*") {
-				continue;
-			}
-			int track = token->getTrack();
-			if (token->isKeySignature()) {
-				m_keys[track][timeval].push_back(token);
-			} else if (token->isOriginalKeySignature()) {
-				m_keys[track][timeval].push_back(token);
-			} else if (token->isModernKeySignature()) {
-				m_keys[track][timeval].push_back(token);
+	}
+	return output;
+}
 
-			} else if (token->isInstrumentName()) {
-				m_labels[track][timeval].push_back(token);
-			} else if (token->isOriginalInstrumentName()) {
-				m_labels[track][timeval].push_back(token);
-			} else if (token->isModernInstrumentName()) {
-				m_labels[track][timeval].push_back(token);
 
-			} else if (token->isInstrumentAbbreviation()) {
-				m_labelAbbrs[track][timeval].push_back(token);
-			} else if (token->isOriginalInstrumentAbbreviation()) {
-				m_labelAbbrs[track][timeval].push_back(token);
-			} else if (token->isModernInstrumentAbbreviation()) {
-				m_labelAbbrs[track][timeval].push_back(token);
 
-			} else if (token->isClef()) {
-				m_clefs[track][timeval].push_back(token);
-			} else if (token->isOriginalClef()) {
-				m_clefs[track][timeval].push_back(token);
-			} else if (token->isModernClef()) {
-				m_clefs[track][timeval].push_back(token);
+//////////////////////////////
+//
+// Tool_modori::convertKeySignatureToModern --
+//
 
-			} else if (token->isMensuration()) {
-				m_mensurations[track][timeval].push_back(token);
-			} else if (token->isOriginalMensuration()) {
-				m_mensurations[track][timeval].push_back(token);
-			} else if (token->isModernMensuration()) {
-				m_mensurations[track][timeval].push_back(token);
-			}
-		}
+void Tool_modori::convertKeySignatureToModern(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?k(.*)")) {
+		string text = "*mk";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
 
-	if (exinterpLine >= 0) {
-		processExclusiveInterpretationLine(infile, exinterpLine);
-	}
 
-	storeModOriReferenceRecords(infile);
 
-	if (m_infoQ) {
-		if (m_modernQ || m_originalQ) {
-			m_humdrum_text << infile;
-		}
-		printInfo();
-	}
+//////////////////////////////
+//
+// Tool_modori::convertInstrumentNameToModern --
+//
 
-	if (!(m_modernQ || m_originalQ)) {
-		// nothing to do
-		return;
+void Tool_modori::convertInstrumentNameToModern(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?I\"(.*)")) {
+		string text = "*mI\"";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
-
-	switchModernOriginal(infile);
-	printModoriOutput(infile);
 }
 
 
+
 //////////////////////////////
 //
-// Tool_modori::processExclusiveInterpretationLine --
+// Tool_modori::convertInstrumentAbbreviationToModern --
 //
 
-void Tool_modori::processExclusiveInterpretationLine(HumdrumFile& infile, int line) {
-	vector<HTp> staffish;
-	vector<HTp> staff;
-	vector<vector<HTp>> nonstaff;
-	bool init = false;
-	bool changed = false;
-
-	if (!infile[line].isExclusive()) {
-		return;
+void Tool_modori::convertInstrumentAbbreviationToModern(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?I'(.*)")) {
+		string text = "*mI'";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
 
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		HTp token = infile.token(line, i);
-		if (!token->isExclusiveInterpretation()) {
-			continue;
-		}
-		if (token->isStaff()) {
-			staff.push_back(token);
-			nonstaff.resize(nonstaff.size() + 1);
-			init = 1;
-		} else {
-			if (init) {
-				nonstaff.back().push_back(token);
-			}
-		}
-		if (token->isStaff()) {
-			staffish.push_back(token);
-		} else if (*token == "**mod-kern") {
-			staffish.push_back(token);
-		} else if (*token == "**mod-mens") {
-			staffish.push_back(token);
-		} else if (*token == "**ori-kern") {
-			staffish.push_back(token);
-		} else if (*token == "**ori-mens") {
-			staffish.push_back(token);
-		}
-	}
 
-	for (int i=0; i<(int)staff.size(); i++) {
-		changed |= processStaffCompanionSpines(nonstaff[i]);
+
+//////////////////////////////
+//
+// Tool_modori::convertKeySignatureToOriginal --
+//
+
+void Tool_modori::convertKeySignatureToOriginal(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?k(.*)")) {
+		string text = "*ok";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
 
-	changed |= processStaffSpines(staffish);
 
-	if (changed) {
-		infile[line].createLineFromTokens();
+
+//////////////////////////////
+//
+// Tool_modori::convertInstrumentNameToOriginal --
+//
+
+void Tool_modori::convertInstrumentNameToOriginal(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?I\"(.*)")) {
+		string text = "*oI\"";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
 }
 
@@ -99563,182 +103198,111 @@ void Tool_modori::processExclusiveInterpretationLine(HumdrumFile& infile, int li
 
 //////////////////////////////
 //
-// Tool_modori::processStaffSpines --
+// Tool_modori::convertInstrumentAbbreviationToOriginal --
 //
 
-bool Tool_modori::processStaffSpines(vector<HTp>& tokens) {
-
+void Tool_modori::convertInstrumentAbbreviationToOriginal(HTp token) {
 	HumRegex hre;
-	bool changed = false;
-	for (int i=0; i<(int)tokens.size(); i++) {
-		if (hre.search(tokens[i], "^\\*\\*(ori|mod)-(.*)")) {
-			string newexinterp = "**" + hre.getMatch(2) + "-" + hre.getMatch(1);
-			tokens[i]->setText(newexinterp);
-			changed = true;
-		} else if (hre.search(tokens[i], "^\\*\\*(.*?)-(ori|mod)$")) {
-			string newexinterp = "**" + hre.getMatch(2) + "-" + hre.getMatch(1);
-			tokens[i]->setText(newexinterp);
-			changed = true;
-		}
+	if (hre.search(token, "^\\*[mo]?I'(.*)")) {
+		string text = "*oI'";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
-
-	return changed;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::processStaffCompanionSpines --
+// Tool_modori::convertKeySignatureToRegular --
 //
 
-bool Tool_modori::processStaffCompanionSpines(vector<HTp> tokens) {
-
-	vector<HTp> mods;
-	vector<HTp> oris;
-	vector<HTp> other;
-
-	for (int i=0; i<(int)tokens.size(); i++) {
-		if (tokens[i]->find("**mod-") != string::npos) {
-			mods.push_back(tokens[i]);
-		} else if (tokens[i]->find("**ori-") != string::npos) {
-			oris.push_back(tokens[i]);
-		} else {
-			other.push_back(tokens[i]);
-		}
+void Tool_modori::convertKeySignatureToRegular(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?k(.*)")) {
+		string text = "*k";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
 
-	bool gchanged = false;
 
-	if (mods.empty() && oris.empty()) {
-		// Nothing to do.
-		return false;
-	}
 
-	// mods and oris should not be mixed, so if there are no
-	// other spines, then also give up:
-	if (other.empty()) {
-		return false;
-	}
+//////////////////////////////
+//
+// Tool_modori::convertInstrumentNameToRegular --
+//
 
+void Tool_modori::convertInstrumentNameToRegular(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?I\"(.*)")) {
+		string text = "*I\"";
+		text += hre.getMatch(1);
+		token->setText(text);
+	}
+}
 
-	if (m_modernQ) {
-		bool changed = false;
-		// Swap (**mod-XXX and **XXX) to (**XXX and **ori-XXX)
 
-		for (int i=0; i<(int)other.size(); i++) {
-			if (other[i] == NULL) {
-				continue;
-			}
-			string target = "**mod-" + other[i]->substr(2);
-			for (int j=0; j<(int)mods.size(); j++) {
-				if (mods[j] == NULL) {
-					continue;
-				}
-				if (*mods[j] != target) {
-					continue;
-				}
-				mods[j]->setText(*other[i]);
-				mods[j] = NULL;
-				changed = true;
-				gchanged = true;
-			}
-			if (changed) {
-				string replacement = "**ori-" + other[i]->substr(2);
-				other[i]->setText(replacement);
-				other[i] = NULL;
-			}
-		}
 
-	} else if (m_originalQ) {
-		bool changed = false;
-		// Swap (**ori-XXX and **XXX) to (**XXX and **mod-XXX)
+//////////////////////////////
+//
+// Tool_modori::convertInstrumentAbbreviationToRegular --
+//
 
-		for (int i=0; i<(int)other.size(); i++) {
-			if (other[i] == NULL) {
-				continue;
-			}
-			string target = "**ori-" + other[i]->substr(2);
-			for (int j=0; j<(int)oris.size(); j++) {
-				if (oris[j] == NULL) {
-					continue;
-				}
-				if (*oris[j] != target) {
-					continue;
-				}
-				oris[j]->setText(*other[i]);
-				oris[j] = NULL;
-				changed = true;
-				gchanged = true;
-			}
-			if (changed) {
-				string replacement = "**mod-" + other[i]->substr(2);
-				other[i]->setText(replacement);
-				other[i] = NULL;
-			}
-		}
+void Tool_modori::convertInstrumentAbbreviationToRegular(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?I'(.*)")) {
+		string text = "*I'";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
-
-	return gchanged;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::storeModOriReferenceRecors --
+// Tool_modori::convertClefToModern --
 //
 
-void Tool_modori::storeModOriReferenceRecords(HumdrumFile& infile) {
-	m_references.clear();
-
-	vector<HLp> refs = infile.getGlobalReferenceRecords();
-	vector<string> keys(refs.size());
-	for (int i=0; i<(int)refs.size(); i++) {
-		string key = refs.at(i)->getReferenceKey();
-		keys.at(i) = key;
+void Tool_modori::convertClefToModern(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?clef(.*)")) {
+		string text = "*mclef";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
+
 
-	vector<int> modernIndex;
-	vector<int> originalIndex;
 
+//////////////////////////////
+//
+// Tool_modori::convertClefToOriginal --
+//
+
+void Tool_modori::convertClefToOriginal(HTp token) {
 	HumRegex hre;
-	for (int i=0; i<(int)keys.size(); i++) {
-		if (m_modernQ || m_infoQ) {
-			if (hre.search(keys[i], "-mod$")) {
-				modernIndex.push_back(i);
-			}
-		} else if (m_originalQ || m_infoQ) {
-			if (hre.search(keys[i], "-ori$")) {
-				originalIndex.push_back(i);
-			}
-		}
+	if (hre.search(token, "^\\*[mo]?clef(.*)")) {
+		string text = "*oclef";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
 
-	if (m_modernQ || m_infoQ) {
-		// Store *-mod reference records if there is a pairing:
-		int pairing = -1;
-		for (int i=0; i<(int)modernIndex.size(); i++) {
-			int index = modernIndex[i];
-			pairing = getPairedReference(index, keys);
-			if (pairing >= 0) {
-				m_references.push_back(make_pair(refs[index]->token(0), refs[pairing]->token(0)));
-			}
-		}
-	}
 
-	if (m_originalQ || m_infoQ) {
-		// Store *-ori reference records if there is a pairing:
-		int pairing = -1;
-		string target;
-		for (int i=0; i<(int)originalIndex.size(); i++) {
-			int index = originalIndex[i];
-			pairing = getPairedReference(index, keys);
-			if (pairing >= 0) {
-				target = keys[index];
-				m_references.push_back(make_pair(refs[index]->token(0), refs[pairing]->token(0)));
-			}
-		}
+
+//////////////////////////////
+//
+// Tool_modori::convertClefToRegular --
+//
+
+void Tool_modori::convertClefToRegular(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?clef(.*)")) {
+		string text = "*clef";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
 }
 
@@ -99746,755 +103310,678 @@ void Tool_modori::storeModOriReferenceRecords(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_modori::getPairedReference --
+// Tool_modori::convertMensurationToModern --
 //
 
-int Tool_modori::getPairedReference(int index, vector<string>& keys) {
-	string key = keys.at(index);
-	string tkey = key;
-	if (tkey.size() > 4) {
-		tkey.resize(tkey.size() - 4);
-	} else {
-		return -1;
+void Tool_modori::convertMensurationToModern(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?met\\((.*)")) {
+		string text = "*mmet(";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
+}
 
-	for (int i=0; i<(int)keys.size(); i++) {
-		int ii = index + i;
-		if (ii < (int)keys.size()) {
-			if (tkey == keys.at(ii)) {
-				return ii;
-			}
-		}
-		ii = index - i;
-		if (ii >= 0) {
-			if (tkey == keys.at(ii)) {
-				return ii;
-			}
-		}
+
+
+//////////////////////////////
+//
+// Tool_modori::convertMensurationToOriginal --
+//
+
+void Tool_modori::convertMensurationToOriginal(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?met\\((.*)")) {
+		string text = "*omet(";
+		text += hre.getMatch(1);
+		token->setText(text);
 	}
-	return -1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::switchModernOriginal --
+// Tool_modori::convertMensurationToRegular --
 //
 
-void Tool_modori::switchModernOriginal(HumdrumFile& infile) {
+void Tool_modori::convertMensurationToRegular(HTp token) {
+	HumRegex hre;
+	if (hre.search(token, "^\\*[mo]?met\\((.*)")) {
+		string text = "*met(";
+		text += hre.getMatch(1);
+		token->setText(text);
+	}
+}
 
-	set<int> changed;
 
-	if (!m_nokeyQ) {
-		for (int t=1; t<(int)m_keys.size(); ++t) {
-			for (auto it = m_keys.at(t).begin(); it != m_keys.at(t).end(); ++it) {
-				if (it->second.size() != 2) {
-					continue;
-				}
-				bool status = swapKeyStyle(it->second.at(0), it->second.at(1));
-				if (status) {
-					int line = it->second.at(0)->getLineIndex();
-					changed.insert(line);
-					line = it->second.at(1)->getLineIndex();
-					changed.insert(line);
-				}
-			}
+
+////////////////////
+//
+// Tool_modori::printInfo --
+//
+
+void Tool_modori::printInfo(void) {
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	m_humdrum_text << "!! KEYS:" << endl;
+
+	for (int t=1; t<(int)m_keys.size(); ++t) {
+		for (auto it = m_keys.at(t).begin(); it != m_keys.at(t).end(); ++it) {
+			m_humdrum_text << "!!\t" << it->first;
+			for (int j=0; j<(int)it->second.size(); ++j) {
+				m_humdrum_text << '\t' << it->second.at(j);
+		}
+			m_humdrum_text << endl;
 		}
 	}
 
-	if (!m_nolabelsQ) {
-		for (int t=1; t<(int)m_labels.size(); ++t) {
-			for (auto it = m_labels.at(t).begin(); it != m_labels.at(t).end(); ++it) {
-				if (it->second.size() != 2) {
-					continue;
-				}
-				bool status = swapInstrumentNameStyle(it->second.at(0), it->second.at(1));
-				if (status) {
-					int line = it->second.at(0)->getLineIndex();
-					changed.insert(line);
-					line = it->second.at(1)->getLineIndex();
-					changed.insert(line);
-				}
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	m_humdrum_text << "!! CLEFS:" << endl;
+
+	for (int t=1; t<(int)m_keys.size(); ++t) {
+		for (auto it = m_clefs.at(t).begin(); it != m_clefs.at(t).end(); ++it) {
+			m_humdrum_text << "!!\t" << it->first;
+			for (int j=0; j<(int)it->second.size(); ++j) {
+				m_humdrum_text << '\t' << it->second.at(j);
 			}
+			m_humdrum_text << endl;
 		}
 	}
 
-	if (!m_nolabelAbbrsQ) {
-		for (int t=1; t<(int)m_labelAbbrs.size(); ++t) {
-			for (auto it = m_labelAbbrs.at(t).begin(); it != m_labelAbbrs.at(t).end(); ++it) {
-				if (it->second.size() != 2) {
-					continue;
-				}
-				bool status = swapInstrumentAbbreviationStyle(it->second.at(0), it->second.at(1));
-				if (status) {
-					int line = it->second.at(0)->getLineIndex();
-					changed.insert(line);
-					line = it->second.at(1)->getLineIndex();
-					changed.insert(line);
-				}
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	m_humdrum_text << "!! MENSURATIONS:" << endl;
+
+	for (int t=1; t<(int)m_mensurations.size(); ++t) {
+		for (auto it = m_mensurations.at(t).begin(); it != m_mensurations.at(t).end(); ++it) {
+			m_humdrum_text << "!!\t" << it->first;
+			for (int j=0; j<(int)it->second.size(); j++) {
+				m_humdrum_text << '\t' << it->second.at(j);
 			}
+			m_humdrum_text << endl;
 		}
 	}
 
-	if (!m_nolyricsQ) {
-		bool adjust = false;
-		int line = -1;
-		for (int i=0; i<(int)m_lyrics.size(); i++) {
-			HTp token = m_lyrics[i];
-			line = token->getLineIndex();
-			if (m_modernQ) {
-				if (*token == "**text") {
-					adjust = true;
-					token->setText("**ori-text");
-				} else if (*token == "**mod-text") {
-					adjust = true;
-					token->setText("**text");
-				}
-			} else {
-				if (*token == "**text") {
-					adjust = true;
-					token->setText("**mod-text");
-				} else if (*token == "**ori-text") {
-					adjust = true;
-					token->setText("**text");
-				}
-			}
-		}
-		if (adjust && (line >= 0)) {
-			infile[line].createLineFromTokens();
-		}
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	m_humdrum_text << "!! LYRICS:" << endl;
+
+	for (int i=0; i<(int)m_lyrics.size(); i++) {
+		HTp token = m_lyrics[i];
+		m_humdrum_text << "!!\t";
+		m_humdrum_text << token;
+		m_humdrum_text << endl;
 	}
 
-	if (!m_nolotextQ) {
-		HumRegex hre;
-		for (int i=0; i<(int)m_lotext.size(); i++) {
-			HTp token = m_lotext[i];
-			int line = token->getLineIndex();
-			if (hre.search(token, "^!!?LO:(TX|DY).*:mod=")) {
-				string text = *token;
-				hre.replaceDestructive(text, ":ori=", ":t=");
-				hre.replaceDestructive(text, ":t=", ":mod=");
-				token->setText(text);
-				changed.insert(line);
-			} else if (hre.search(token, "^!!?LO:(TX|DY).*:ori=")) {
-				string text = *token;
-				hre.replaceDestructive(text, ":mod=", ":t=");
-				hre.replaceDestructive(text, ":t=", ":ori=");
-				token->setText(text);
-				changed.insert(line);
-			}
-		}
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	m_humdrum_text << "!! TEXT:" << endl;
+
+	for (int i=0; i<(int)m_lotext.size(); i++) {
+		m_humdrum_text << "!!\t" << m_lotext[i] << endl;
 	}
 
-	if (!m_norefsQ) {
-		HumRegex hre;
-		for (int i=0; i<(int)m_references.size(); i++) {
-			HTp first = m_references[i].first;
-			HTp second = m_references[i].second;
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	m_humdrum_text << "!! REFERENCES:" << endl;
 
-			if (m_modernQ) {
-				if (hre.search(first, "^!!![^:]*?-mod:")) {
-					string text = *first;
-					hre.replaceDestructive(text, ":", "-...:");
-					first->setText(text);
-					infile[first->getLineIndex()].createLineFromTokens();
+	for (int i=0; i<(int)m_references.size(); i++) {
+		m_humdrum_text << "!!\t" << m_references[i].first << endl;
+		m_humdrum_text << "!!\t" << m_references[i].second << endl;
+		m_humdrum_text << "!!\n";
+	}
 
-					text = *second;
-					hre.replaceDestructive(text, "-ori:", ":");
-					second->setText(text);
-					infile[second->getLineIndex()].createLineFromTokens();
-				}
-			} else if (m_originalQ) {
-				if (hre.search(first, "^!!![^:]*?-ori:")) {
-					string text = *first;
-					hre.replaceDestructive(text, ":", "-...:");
-					first->setText(text);
-					infile[first->getLineIndex()].createLineFromTokens();
+	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+}
 
-					text = *second;
-					hre.replaceDestructive(text, "-mod:", ":");
-					second->setText(text);
-					infile[second->getLineIndex()].createLineFromTokens();
-				}
-			}
 
-		}
-	}
 
-	// Mensurations are only used for "original" display.  It is possible
-	// to use a modern metric signature (common time or cut time) but these
-	// are not currently allowed.  Only one *met at a given time position
-	// is allowed.
 
-	if (!m_nomensurationQ) {
-		for (int t=1; t<(int)m_mensurations.size(); ++t) {
-			for (auto it = m_mensurations.at(t).begin(); it != m_mensurations.at(t).end(); ++it) {
-				if (it->second.size() == 1) {
-					// swap omet to met, or met to omet:
-					bool status = flipMensurationStyle(it->second.at(0));
-					if (status) {
-						int line = it->second.at(0)->getLineIndex();
-						changed.insert(line);
-					}
-				} else if (it->second.size() == 2) {
-					// swap omet/met or mmet/met:
-					bool status = swapMensurationStyle(it->second.at(0), it->second.at(1));
-					if (status) {
-						int line = it->second.at(0)->getLineIndex();
-						changed.insert(line);
-						line = it->second.at(1)->getLineIndex();
-						changed.insert(line);
-					}
-				}
-			}
-		}
+//////////////////////////////
+//
+// SonorityDatabase::buildDatabase --
+//
+
+void SonorityDatabase::buildDatabase(HLp line) {
+	clear();
+	if (line == NULL) {
+		return;
+	}
+	m_line = line;
+	bool nullQ = false;
+	if (!line->isData()) {
+		return;
 	}
+	int lowesti = 0;
+	int lowest12 = 1000;
 
-	if (!m_noclefQ) {
-		for (int t=1; t<(int)m_clefs.size(); ++t) {
-			for (auto it = m_clefs.at(t).begin(); it != m_clefs.at(t).end(); ++it) {
-				if (it->second.size() != 2) {
-					continue;
-				}
-				bool status = swapClefStyle(it->second.at(0), it->second.at(1));
-				if (status) {
-					int line = it->second.at(0)->getLineIndex();
-					changed.insert(line);
-					line = it->second.at(1)->getLineIndex();
-					changed.insert(line);
-				}
+	for (int i=0; i<line->getFieldCount(); i++) {
+		HTp token = m_line->token(i);
+		if (!token->isKern()) {
+			continue;
+		}
+		if (token->isRest()) {
+			// ignoring rests, at least for now
+			continue;
+		}
+		if (token->isNull()) {
+			nullQ = true;
+			token = token->resolveNull();
+		}
+		if (token->isNull()) {
+			continue;
+		}
+		int scount = token->getSubtokenCount();
+		for (int j=0; j<scount; j++) {
+			expandList();
+			m_notes.back().setToken(token, nullQ, j);
+			if (m_notes.back().getBase12() < lowest12) {
+				lowesti = (int)m_notes.size() - 1;
+				lowest12 = m_notes.back().getBase12();
 			}
 		}
 	}
-
-	for (auto it = changed.begin(); it != changed.end(); ++it) {
-		int line = *it;
-		infile[line].createLineFromTokens();
+	if (!m_notes.empty()) {
+		m_lowest = m_notes[lowesti];
 	}
-
-	updateLoMo(infile);
 }
 
 
+
 //////////////////////////////
 //
-// Tool_modori::printModoriOutput --
+// SonorityDatabase::addNote --
 //
 
-void Tool_modori::printModoriOutput(HumdrumFile& infile) {
-	string state;
-	if (m_modernQ) {
+void SonorityDatabase::addNote(const std::string& text) {
+	expandList();
+	m_notes.back().setString(text);
+	// not dealing with lowest note
+}
 
-		// convert to modern
-		for (int i=0; i<infile.getLineCount(); i++) {
-			if (infile[i].isCommentGlobal()) {
-				HTp token = infile.token(i, 0);
-				if (*token == "!!LO:MO:mod") {
-				   state = "mod";
-					m_humdrum_text << token << endl;
-					continue;
-				} else if (*token == "!!LO:MO:ori") {
-				   state = "ori";
-					m_humdrum_text << token << endl;
-					continue;
-				} else if (*token == "!!LO:MO:end") {
-					state = "";
-					m_humdrum_text << token << endl;
-					continue;
-				}
-			}
-			if (state == "mod") {
-				// Remove global comment prefix "!! ".  Complain if not there.
-				if (infile[i].compare(0, 3, "!! ") != 0) {
-					cerr << "Error: line does not start with \"!! \":\t" << infile[i] << endl;
-				} else {
-					m_humdrum_text << infile[i].substr(3) << endl;
-				}
-			} else if (state == "ori") {
-				// Add global comment prefix "!! ".
-				m_humdrum_text << "!! " << infile[i] << endl;
-			} else {
-				m_humdrum_text << infile[i] << endl;
-			}
-		}
 
-	} else if (m_originalQ) {
 
-		// convert to original
-		for (int i=0; i<infile.getLineCount(); i++) {
-			if (infile[i].isCommentGlobal()) {
-				HTp token = infile.token(i, 0);
-				if (*token == "!!LO:MO:mod") {
-				   state = "mod";
-					m_humdrum_text << token << endl;
-					continue;
-				} else if (*token == "!!LO:MO:ori") {
-				   state = "ori";
-					m_humdrum_text << token << endl;
-					continue;
-				} else if (*token == "!!LO:MO:end") {
-					state = "";
-					m_humdrum_text << token << endl;
-					continue;
-				}
-			}
-			if (state == "ori") {
-				// Remove global comment prefix "!! ".  Complain if not there.
-				if (infile[i].compare(0, 3, "!! ") != 0) {
-					cerr << "Error: line does not start with \"!! \":\t" << infile[i] << endl;
-				} else {
-					m_humdrum_text << infile[i].substr(3) << endl;
-				}
-			} else if (state == "mod") {
-				// Add global comment prefix "!! ".
-				m_humdrum_text << "!! " << infile[i] << endl;
-			} else {
-				m_humdrum_text << infile[i] << endl;
-			}
+//////////////////////////////
+//
+// MSearchQueryToken::parseHarmonicQuery --
+//
+
+void MSearchQueryToken::parseHarmonicQuery(void) {
+	if (!hpieces.empty()) {
+		// do not reparse
+		return;
+	}
+	for (int i=0; i<(int)harmonic.size(); i++) {
+		char ch = tolower(harmonic[i]);
+		if (ch >= 'a' && ch <= 'g') {
+			hpieces.resize(hpieces.size() + 1);
+			hpieces.back() += harmonic[i];
+		} else if (ch == '-') {
+			hpieces.back() += ch;
+		} else if (ch == 'n') {
+			hpieces.back() += ch;
+		} else if (ch == '#') {
+			hpieces.back() += ch;
 		}
+	}
 
+	hquery.resize(hpieces.size());
+	for (int i=0; i<(int)hpieces.size(); i++) {
+		hquery[i].setString(hpieces[i]);
 	}
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_modori::updateLoMo --
+// Tool_msearch::Tool_msearch -- Set the recognized options for the tool.
 //
 
-void Tool_modori::updateLoMo(HumdrumFile& infile) {
-	for (int i=0; i<(int)m_lomo.size(); i++) {
-		processLoMo(m_lomo[i]);
-	}
+Tool_msearch::Tool_msearch(void) {
+	define("debug=b",                     "diatonic search");
+	define("q|query=s:4c4d4e4f4g",        "combined rhythm/pitch query string");
+	define("p|pitch=s:cdefg",             "pitch query string");
+	define("i|interval=s:2222",           "interval query string");
+	define("r|d|rhythm|duration=s:44444", "rhythm query string");
+	define("t|text=s:",                   "lyrical text query string");
+	define("O|no-overlap=b",              "do not allow matches to overlap");
+	define("x|cross=b",                   "search across parts");
+	define("c|color=s",                   "highlight color");
+	define("m|mark|marker=s:@",           "marking character");
+	define("M|no-mark|no-marker=b",       "do not mark matches");
+	define("Q|quiet=b",                   "quiet mode: do not summarize matches");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_modori::processLoMo --
+// Tool_msearch::run -- Do the main work of the tool.
 //
 
-void Tool_modori::processLoMo(HTp lomo) {
-	HumRegex hre;
+bool Tool_msearch::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
 
-	if (m_modernQ) {
-		string text = lomo->getText();
-		string modtext;
-		string oritext;
-		string base;
-		string rest;
-		if (hre.search(text, "(.*):mod=([^:]*)(.*)")) {
-			base = hre.getMatch(1);
-			modtext = hre.getMatch(2);
-			rest = hre.getMatch(3);
-			hre.replaceDestructive(modtext, ":", "&colon;", "g");
-			HTp current = lomo->getNextToken();
-			// null parameter allows next following null token
-			// to be swapped out
-			bool nullQ = hre.search(text, ":null:");
-			if (!nullQ) {
-				while (current) {
-					if (current->isNull()) {
-						current = current->getNextToken();
-						continue;
-					}
-					break;
-				}
-			}
-			if (current) {
-				string oritext = current->getText();
-				hre.replaceDestructive(oritext, "&colon;", ":", "g");
-				current->setText(modtext);
-				string newtext = base;
-				newtext += ":ori=";
-				newtext += oritext;
-				newtext += rest;
-				lomo->setText(newtext);
-				lomo->getLine()->createLineFromTokens();
-				current->getLine()->createLineFromTokens();
-			}
-		}
 
-	} else if (m_originalQ) {
-		string text = lomo->getText();
-		string modtext;
-		string oritext;
-		string base;
-		string rest;
-		if (hre.search(text, "(.*):ori=([^:]*)(.*)")) {
-			base = hre.getMatch(1);
-			oritext = hre.getMatch(2);
-			rest = hre.getMatch(3);
-			hre.replaceDestructive(oritext, ":", "&colon;", "g");
-			HTp current = lomo->getNextToken();
-			// null parameter allows next following null token
-			// to be swapped out
-			bool nullQ = hre.search(text, ":null:");
-			if (nullQ) {
-				while (current) {
-					if (current->isNull()) {
-						current = current->getNextToken();
-						continue;
-					}
-					break;
-				}
-			}
-			if (current) {
-				string modtext = current->getText();
-				hre.replaceDestructive(modtext, "&colon;", ":", "g");
-				current->setText(oritext);
-				string newtext = base;
-				newtext += ":mod=";
-				newtext += modtext;
-				newtext += rest;
-				lomo->setText(newtext);
-				lomo->getLine()->createLineFromTokens();
-				current->getLine()->createLineFromTokens();
-			}
+bool Tool_msearch::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_msearch::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_msearch::run(HumdrumFile& infile) {
+	m_sonorities.resize(infile.getLineCount());
+	m_sonoritiesChecked.resize(infile.getLineCount());
+	fill(m_sonoritiesChecked.begin(), m_sonoritiesChecked.end(), false);
+	m_debugQ = getBoolean("debug");
+	m_quietQ = getBoolean("quiet");
+	m_nooverlapQ = getBoolean("no-overlap");
+	NoteGrid grid(infile);
+	if (m_debugQ) {
+		grid.printGridInfo(cerr);
+		// return 1;
+	}
+	initialize();
+
+	if (getBoolean("text")) {
+		m_text = getString("text");
+	}
+
+	if (m_text.empty()) {
+		vector<MSearchQueryToken> query;
+		fillMusicQuery(query);
+		if (!query.empty()) {
+			doMusicSearch(infile, grid, query);
 		}
+	} else {
+		vector<MSearchTextQuery> query;
+		fillTextQuery(query, getString("text"));
+		doTextSearch(infile, grid, query);
 	}
+
+	infile.createLinesFromTokens();
+	m_humdrum_text << infile;
+
+	return 1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::flipMensurationStyle -- Returns true if swapped.
+// Tool_msearch::initialize --
 //
 
-bool Tool_modori::flipMensurationStyle(HTp token) {
-	bool output = false;
-	HumRegex hre;
-	string text;
-	if (token->isMensuration()) {
-		// switch to invisible mensuration
-		text = "*omet";
-		text += token->substr(4);
-		token->setText(text);
-		output = true;
-	} else if (token->isOriginalMensuration()) {
-		// switch to visible mensuration
-		text = "*met";
-		text += token->substr(5);
-		token->setText(text);
-		output = true;
+void Tool_msearch::initialize(void) {
+	m_marker = getString("marker");
+	// only allowing a single character for now:
+	m_markQ = !getBoolean("no-marker");
+	if (!m_markQ) {
+		m_marker.clear();
+	} else if (!m_marker.empty()) {
+		m_marker = m_marker[0];
 	}
-
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::swapKeyStyle -- Returns true if swapped.
+// Tool_msearch::fillWords --
 //
 
-bool Tool_modori::swapKeyStyle(HTp one, HTp two) {
-	bool mtype1 = false;
-	bool mtype2 = false;
-	bool otype1 = false;
-	bool otype2 = false;
-	bool ktype1 = false;
-	bool ktype2 = false;
-	bool output = false;
-
-	if (one->isKeySignature()) {
-		ktype1 = true;
-	} else if (one->isModernKeySignature()) {
-		mtype1 = true;
-	} else if (one->isOriginalKeySignature()) {
-		otype1 = true;
+void Tool_msearch::fillWords(HumdrumFile& infile, vector<TextInfo*>& words) {
+	vector<HTp> textspines;
+	infile.getSpineStartList(textspines, "**silbe");
+	if (textspines.empty()) {
+		infile.getSpineStartList(textspines, "**text");
 	}
-
-	if (two->isKeySignature()) {
-		ktype2 = true;
-	} else if (two->isModernKeySignature()) {
-		mtype2 = true;
-	} else if (two->isOriginalKeySignature()) {
-		otype2 = true;
+	for (int i=0; i<(int)textspines.size(); i++) {
+		fillWordsForTrack(words, textspines[i]);
 	}
+}
 
-	if (m_modernQ) {
-		// Show the modern key signature.  If one key is *mk and the
-		// other is *k then change *mk to *k and *k to *ok respectively.
-		if (ktype1 && mtype2) {
-			convertKeySignatureToOriginal(one);
-			convertKeySignatureToRegular(two);
-			output = true;
-		} else if (mtype1 && ktype2) {
-			convertKeySignatureToRegular(one);
-			convertKeySignatureToOriginal(two);
-			output = true;
+
+
+//////////////////////////////
+//
+// Tool_msearch::fillWordsForTrack --
+//
+
+void Tool_msearch::fillWordsForTrack(vector<TextInfo*>& words,
+		HTp starttoken) {
+	HTp tok = starttoken->getNextToken();
+	while (tok != NULL) {
+		if (tok->empty()) {
+			tok = tok->getNextToken();
+			continue;
 		}
-	} else if (m_originalQ) {
-		// Show the original key.  If one key is *ok and the
-		// other is *k then change *ok to *k and *k to *mk respectively.
-		if (ktype1 && otype2) {
-			convertKeySignatureToModern(one);
-			convertKeySignatureToRegular(two);
-			output = true;
-		} else if (otype1 && ktype2) {
-			convertKeySignatureToRegular(one);
-			convertKeySignatureToModern(two);
-			output = true;
+		if (tok->isNull()) {
+			tok = tok->getNextToken();
+			continue;
+		}
+		if (!tok->isData()) {
+			tok = tok->getNextToken();
+			continue;
+		}
+		if (tok->at(0) == '-') {
+			// append a syllable to the end of previous word
+			if (!words.empty()) {
+				words.back()->fullword += tok->substr(1, string::npos);
+				if (words.back()->fullword.back() == '-') {
+					words.back()->fullword.pop_back();
+				}
+			}
+			tok = tok->getNextToken();
+			continue;
+		} else {
+			// start a new word
+			TextInfo* temp = new TextInfo();
+			temp->nexttoken = NULL;
+			if (!words.empty()) {
+				words.back()->nexttoken = tok;
+			}
+			temp->fullword = *tok;
+			if (!temp->fullword.empty()) {
+				if (temp->fullword.back() == '-') {
+					temp->fullword.pop_back();
+				}
+			}
+			temp->starttoken = tok;
+			words.push_back(temp);
+			tok = tok->getNextToken();
+			continue;
 		}
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::swapInstrumentNameStyle -- Returns true if swapped.
+// Tool_msearch::doTextSearch -- do a basic text search of all parts.
 //
 
-bool Tool_modori::swapInstrumentNameStyle(HTp one, HTp two) {
-	bool mtype1 = false;
-	bool mtype2 = false;
-	bool otype1 = false;
-	bool otype2 = false;
-	bool ktype1 = false;
-	bool ktype2 = false;
-	bool output = false;
+void Tool_msearch::doTextSearch(HumdrumFile& infile, NoteGrid& grid,
+		vector<MSearchTextQuery>& query) {
 
-	if (one->isInstrumentName()) {
-		ktype1 = true;
-	} else if (one->isModernInstrumentName()) {
-		mtype1 = true;
-	} else if (one->isOriginalInstrumentName()) {
-		otype1 = true;
-	}
+	vector<TextInfo*> words;
+	words.reserve(10000);
+	fillWords(infile, words);
+	int tcount = 0;
 
-	if (two->isInstrumentName()) {
-		ktype2 = true;
-	} else if (two->isModernInstrumentName()) {
-		mtype2 = true;
-	} else if (two->isOriginalInstrumentName()) {
-		otype2 = true;
+	HumRegex hre;
+	for (int i=0; i<(int)query.size(); i++) {
+		for (int j=0; j<(int)words.size(); j++) {
+			if (hre.search(words.at(j)->fullword, query.at(i).word, "i")) {
+				tcount++;
+				markTextMatch(infile, *words[j]);
+			}
+		}
 	}
 
-	if (m_modernQ) {
-		// Show the modern instrument name.  If one name is *mI" and the
-		// other is *I" then change *mI" to *I" and *I" to *oI" respectively.
-		if (ktype1 && mtype2) {
-			convertInstrumentNameToOriginal(one);
-			convertInstrumentNameToRegular(two);
-			output = true;
-		} else if (mtype1 && ktype2) {
-			convertInstrumentNameToRegular(one);
-			convertInstrumentNameToOriginal(two);
-			output = true;
+	string textinterp = "**text";
+	vector<HTp> interps;
+	infile.getSpineStartList(interps);
+	//int textcount = 0;
+	int silbecount = 0;
+	for (int i=0; i<(int)interps.size(); i++) {
+		//if (interps[i]->getText() == "**text") {
+		//	textcount++;
+		//}
+		if (interps[i]->getText() == "**silbe") {
+			silbecount++;
 		}
-	} else if (m_originalQ) {
-		// Show the original key.  If one key is *ok and the
-		// other is *k then change *ok to *k and *k to *mk respectively.
-		if (ktype1 && otype2) {
-			convertInstrumentNameToModern(one);
-			convertInstrumentNameToRegular(two);
-			output = true;
-		} else if (otype1 && ktype2) {
-			convertInstrumentNameToRegular(one);
-			convertInstrumentNameToModern(two);
-			output = true;
+	}
+	if (silbecount > 0) {
+		// giving priority to **silbe content
+		textinterp = "**silbe";
+	}
+
+	if (tcount && m_markQ) {
+		string content = "!!!RDF";
+		content += textinterp;
+		content += ": ";
+		content += m_marker;
+		content += " = marked text";
+		if (getBoolean("color")) {
+			content += ", color=\"" + getString("color") + "\"";
 		}
+		infile.appendLine(content);
+		infile.createLinesFromTokens();
+	}
+
+	for (int i=0; i<(int)words.size(); i++) {
+		delete words[i];
+		words[i] = NULL;
+	}
+
+	if (!m_quietQ) {
+		addTextSearchSummary(infile, tcount, m_marker);
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::swapInstrumentAbbreviationStyle -- Returns true if swapped.
+// Tool_msearch::printQuery --
 //
 
-bool Tool_modori::swapInstrumentAbbreviationStyle(HTp one, HTp two) {
-	bool mtype1 = false;
-	bool mtype2 = false;
-	bool otype1 = false;
-	bool otype2 = false;
-	bool ktype1 = false;
-	bool ktype2 = false;
-	bool output = false;
+void Tool_msearch::printQuery(vector<MSearchQueryToken>& query) {
+	for (int i=0; i<(int)query.size(); i++) {
+		cout << query[i];
+	}
+}
 
-	if (one->isInstrumentAbbreviation()) {
-		ktype1 = true;
-	} else if (one->isModernInstrumentAbbreviation()) {
-		mtype1 = true;
-	} else if (one->isOriginalInstrumentAbbreviation()) {
-		otype1 = true;
+
+
+//////////////////////////////
+//
+// Tool_msearch::doMusicSearch -- do a basic melodic search of all parts.
+//
+
+void Tool_msearch::doMusicSearch(HumdrumFile& infile, NoteGrid& grid,
+		vector<MSearchQueryToken>& query) {
+
+	m_matches.clear();
+
+	if (m_debugQ) {
+		printQuery(query);
 	}
 
-	if (two->isInstrumentAbbreviation()) {
-		ktype2 = true;
-	} else if (two->isModernInstrumentAbbreviation()) {
-		mtype2 = true;
-	} else if (two->isOriginalInstrumentAbbreviation()) {
-		otype2 = true;
+	vector<vector<NoteCell*>> attacks;
+	attacks.resize(grid.getVoiceCount());
+	for (int i=0; i<grid.getVoiceCount(); i++) {
+		grid.getNoteAndRestAttacks(attacks[i], i);
 	}
 
-	if (m_modernQ) {
-		// Show the modern instrument name.  If one name is *mI" and the
-		// other is *I" then change *mI" to *I" and *I" to *oI" respectively.
-		if (ktype1 && mtype2) {
-			convertInstrumentAbbreviationToOriginal(one);
-			convertInstrumentAbbreviationToRegular(two);
-			output = true;
-		} else if (mtype1 && ktype2) {
-			convertInstrumentAbbreviationToRegular(one);
-			convertInstrumentAbbreviationToOriginal(two);
-			output = true;
+	vector<NoteCell*> match;
+	int mcount = 0;
+	for (int i=0; i<(int)attacks.size(); i++) {
+		for (int j=0; j<(int)attacks[i].size(); j++) {
+			m_tomark.clear();
+			bool status = checkForMusicMatch(attacks[i], j, query, match);
+			if (!status) {
+				m_tomark.clear();
+			}
+			if (status && !match.empty()) {
+				mcount++;
+				markMatch(infile, match);
+				storeMatch(match);
+				// cerr << "FOUND MATCH AT " << i << ", " << j << endl;
+				// markNotes(attacks[i], j, (int)query.size());
+			}
 		}
-	} else if (m_originalQ) {
-		// Show the original key.  If one key is *ok and the
-		// other is *k then change *ok to *k and *k to *mk respectively.
-		if (ktype1 && otype2) {
-			convertInstrumentAbbreviationToModern(one);
-			convertInstrumentAbbreviationToRegular(two);
-			output = true;
-		} else if (otype1 && ktype2) {
-			convertInstrumentAbbreviationToRegular(one);
-			convertInstrumentAbbreviationToModern(two);
-			output = true;
+	}
+
+	if (mcount && m_markQ) {
+		string content = "!!!RDF**kern: " + m_marker + " = marked note";
+		if (getBoolean("color")) {
+			content += ", color=\"" + getString("color") + "\"";
 		}
+		infile.appendLine(content);
+		infile.createLinesFromTokens();
+	}
+	if (!m_quietQ) {
+		addMusicSearchSummary(infile, mcount, m_marker);
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::swapMensurationStyle -- Returns true if swapped.
+// Tool_msearch::addMusicSearchSummary --
 //
 
-bool Tool_modori::swapMensurationStyle(HTp one, HTp two) {
-	bool mtype1 = false;
-	bool mtype2 = false;
-	bool otype1 = false;
-	bool otype2 = false;
-	bool ktype1 = false;
-	bool ktype2 = false;
-	bool output = false;
+void Tool_msearch::addMusicSearchSummary(HumdrumFile& infile, int mcount, const string& marker) {
 
-	if (one->isMensuration()) {
-		ktype1 = true;
-	} else if (one->isModernMensuration()) {
-		mtype1 = true;
-	} else if (one->isOriginalMensuration()) {
-		otype1 = true;
+	m_barnums = infile.getMeasureNumbers();
+
+	infile.appendLine("!!@@BEGIN: MUSIC_SEARCH_RESULT");
+	string line;
+
+	line = "!!@QUERY:\t";
+
+	if (getBoolean("query")) {
+		line += " -q ";
+		string qstring = getString("query");
+		makeLowerCase(qstring);
+		if ((qstring.find(' ') != string::npos) || (qstring.find('(') != string::npos)) {
+			line += '"';
+			line += qstring;
+			line += '"';
+		} else {
+			line += qstring;
+		}
 	}
 
-	if (two->isMensuration()) {
-		ktype2 = true;
-	} else if (two->isModernMensuration()) {
-		mtype2 = true;
-	} else if (two->isOriginalMensuration()) {
-		otype2 = true;
+	if (getBoolean("pitch")) {
+		line += " -p ";
+		string pstring = getString("pitch");
+		makeLowerCase(pstring);
+		if ((pstring.find(' ') != string::npos) || (pstring.find('(') != string::npos)) {
+			line += '"';
+			line += pstring;
+			line += '"';
+		} else {
+			line += pstring;
+		}
 	}
 
-	if (m_modernQ) {
-		if (ktype1 && mtype2) {
-			convertMensurationToOriginal(one);
-			convertMensurationToRegular(two);
-			output = true;
-		} else if (mtype1 && ktype2) {
-			convertMensurationToRegular(one);
-			convertMensurationToOriginal(two);
-			output = true;
+	if (getBoolean("rhythm")) {
+		line += " -r ";
+		string rstring = getString("rhythm");
+		makeLowerCase(rstring);
+		if ((rstring.find(' ') != string::npos) || (rstring.find('(') != string::npos)) {
+			line += '"';
+			line += rstring;
+			line += '"';
+		} else {
+			line += rstring;
 		}
-	} else if (m_originalQ) {
-		if (ktype1 && otype2) {
-			convertMensurationToModern(one);
-			convertMensurationToRegular(two);
-			output = true;
-		} else if (otype1 && ktype2) {
-			convertMensurationToRegular(one);
-			convertMensurationToModern(two);
-			output = true;
+	}
+
+	if (getBoolean("interval")) {
+		line += " -i ";
+		string istring = getString("interval");
+		makeLowerCase(istring);
+		if ((istring.find(' ') != string::npos) || (istring.find('(') != string::npos)) {
+			line += '"';
+			line += istring;
+			line += '"';
+		} else {
+			line += istring;
 		}
 	}
-	return output;
+
+	infile.appendLine(line);
+
+	line = "!!@MATCHES:\t";
+	line += to_string(mcount);
+	infile.appendLine(line);
+
+	if (m_markQ) {
+		line = "!!@MARKER:\t";
+		line += marker;
+		infile.appendLine(line);
+	}
+
+	// Print music match location here.
+	for (int i=0; i<(int)m_matches.size(); i++) {
+		addMatch(infile, m_matches[i]);
+	}
+
+	infile.appendLine("!!@@END: MUSIC_SEARCH_RESULT");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::swapClefStyle -- Returns true if swapped.
+// Tool_msearch::addMatch --
+//
+// Todo:
+//		* add duration of match
 //
 
-bool Tool_modori::swapClefStyle(HTp one, HTp two) {
-	bool mtype1 = false;
-	bool mtype2 = false;
-	bool otype1 = false;
-	bool otype2 = false;
-	bool ktype1 = false;
-	bool ktype2 = false;
-	bool output = false;
-
-	if (one->isClef()) {
-		ktype1 = true;
-	} else if (one->isModernClef()) {
-		mtype1 = true;
-	} else if (one->isOriginalClef()) {
-		otype1 = true;
+void Tool_msearch::addMatch(HumdrumFile& infile, vector<NoteCell*>& match) {
+	if (match.empty()) {
+		return;
 	}
-
-	if (two->isClef()) {
-		ktype2 = true;
-	} else if (two->isModernClef()) {
-		mtype2 = true;
-	} else if (two->isOriginalClef()) {
-		otype2 = true;
+	if (match.back() == NULL) {
+		// strange problem
+		return;
 	}
+	int startIndex   = match.at(0)->getLineIndex();
+	int endIndex     = match.back()->getLineIndex();
+	int startMeasure = m_barnums.at(startIndex);
+	int endMeasure   = m_barnums.at(endIndex);
 
-	if (m_modernQ) {
-		// Show the modern key signature.  If one key is *mk and the
-		// other is *k then change *mk to *k and *k to *ok respectively.
-		if (ktype1 && mtype2) {
-			convertClefToOriginal(one);
-			convertClefToRegular(two);
-			output = true;
-		} else if (mtype1 && ktype2) {
-			convertClefToRegular(one);
-			convertClefToOriginal(two);
-			output = true;
-		}
-	} else if (m_originalQ) {
-		// Show the original key.  If one key is *ok and the
-		// other is *k then change *ok to *k and *k to *mk respectively.
-		if (ktype1 && otype2) {
-			convertClefToModern(one);
-			convertClefToRegular(two);
-			output = true;
-		} else if (otype1 && ktype2) {
-			convertClefToRegular(one);
-			convertClefToModern(two);
-			output = true;
-		}
+	infile.appendLine("!!@@BEGIN:\tMATCH");
+
+	string measure = "!!@MEASURE: ";
+
+	measure += to_string(startMeasure);
+	if (startMeasure != endMeasure) {
+		measure += " ";
+		measure += to_string(endMeasure);
 	}
-	return output;
+	infile.appendLine(measure);
+
+	infile.appendLine("!!@@END:\tMATCH");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::convertKeySignatureToModern --
+// Tool_msearch::makeLowerCase --
 //
 
-void Tool_modori::convertKeySignatureToModern(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?k(.*)")) {
-		string text = "*mk";
-		text += hre.getMatch(1);
-		token->setText(text);
+void Tool_msearch::makeLowerCase(string& inout) {
+	for (int i=0; i<(int)inout.size(); i++) {
+		inout[i] = tolower(inout[i]);
 	}
 }
 
@@ -100502,31 +103989,74 @@ void Tool_modori::convertKeySignatureToModern(HTp token) {
 
 //////////////////////////////
 //
-// Tool_modori::convertInstrumentNameToModern --
+// Tool_msearch::addTextSearchSummary --
 //
 
-void Tool_modori::convertInstrumentNameToModern(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?I\"(.*)")) {
-		string text = "*mI\"";
-		text += hre.getMatch(1);
-		token->setText(text);
+void Tool_msearch::addTextSearchSummary(HumdrumFile& infile, int mcount, const string& marker) {
+	infile.appendLine("!!@@BEGIN: TEXT_SEARCH_RESULT");
+	string line;
+
+	line = "!!@QUERY:\t";
+
+	if (getBoolean("text")) {
+		line += " -t ";
+		string tstring = getString("text");
+		if (tstring.find(' ') != string::npos) {
+			line += '"';
+			line += tstring;
+			line += '"';
+		} else {
+			line += tstring;
+		}
+	}
+
+	infile.appendLine(line);
+
+	line = "!!@MATCHES:\t";
+	line += to_string(mcount);
+	infile.appendLine(line);
+
+	if (m_markQ) {
+		line = "!!@MARKER:\t";
+		line += marker;
+		infile.appendLine(line);
 	}
+
+	// Print match location here.
+	infile.appendLine("!!@@END: TEXT_SEARCH_RESULT");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::convertInstrumentAbbreviationToModern --
+// Tool_msearch::markNote --
 //
 
-void Tool_modori::convertInstrumentAbbreviationToModern(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?I'(.*)")) {
-		string text = "*mI'";
-		text += hre.getMatch(1);
-		token->setText(text);
+void Tool_msearch::markNote(HTp token, int index) {
+	if (index < 0) {
+		return;
+	}
+	if (!token->isChord()) {
+		if (token->find(m_marker) == string::npos) {
+			string text = *token;
+			text += m_marker;
+			token->setText(text);
+		}
+		return;
+	}
+	vector<std::string> subtoks = token->getSubtokens();
+	if (index >= (int)subtoks.size()) {
+		return;
+	}
+	if (subtoks[index].find(m_marker) == string::npos) {
+		subtoks[index] += m_marker;
+		string output = subtoks[0];
+		for (int i=1; i<(int)subtoks.size(); i++) {
+			output += " ";
+			output += subtoks[i];
+		}
+		token->setText(output);
 	}
 }
 
@@ -100534,15 +104064,46 @@ void Tool_modori::convertInstrumentAbbreviationToModern(HTp token) {
 
 //////////////////////////////
 //
-// Tool_modori::convertKeySignatureToOriginal --
+// Tool_msearch::markMatch -- assumes monophonic music.
 //
 
-void Tool_modori::convertKeySignatureToOriginal(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?k(.*)")) {
-		string text = "*ok";
-		text += hre.getMatch(1);
-		token->setText(text);
+void Tool_msearch::markMatch(HumdrumFile& infile, vector<NoteCell*>& match) {
+	for (int i=0; i<(int)m_tomark.size(); i++) {
+		markNote(m_tomark[i].first, m_tomark[i].second);
+	}
+	if (match.empty()) {
+		return;
+	}
+	HTp mstart = match[0]->getToken();
+	HTp mend = NULL;
+	if (match.back() != NULL) {
+		mend = match.back()->getToken();
+	} else {
+		// there is an extra NULL token at the end of the music to allow
+		// marking tied notes.
+	}
+	HTp tok = mstart;
+	string text;
+	while (tok && (tok != mend)) {
+		if (!tok->isData()) {
+			tok = tok->getNextToken();
+			continue;
+		}
+		if (tok->isNull()) {
+			tok = tok->getNextToken();
+			continue;
+		}
+		if (tok->empty()) {
+			// skip marking null tokens
+			tok = tok->getNextToken();
+			continue;
+		}
+		markNote(tok, 0);
+		tok = tok->getNextToken();
+		if (tok && !tok->isKern()) {
+			cerr << "STRANGE LINKING WITH TEXT SPINE" << endl;
+			break;
+		}
 	}
 }
 
@@ -100550,15 +104111,57 @@ void Tool_modori::convertKeySignatureToOriginal(HTp token) {
 
 //////////////////////////////
 //
-// Tool_modori::convertInstrumentNameToOriginal --
+// Tool_msearch::markTextMatch -- assumes monophonic voices.
 //
 
-void Tool_modori::convertInstrumentNameToOriginal(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?I\"(.*)")) {
-		string text = "*oI\"";
-		text += hre.getMatch(1);
-		token->setText(text);
+void Tool_msearch::markTextMatch(HumdrumFile& infile, TextInfo& word) {
+	HTp mstart = word.starttoken;
+	HTp mnext = word.nexttoken;
+	// while (mstart && !mstart->isKern()) {
+	// 	mstart = mstart->getPreviousFieldToken();
+	// }
+	// HTp mend = word.nexttoken;
+	// while (mend && !mend->isKern()) {
+	// 	mend = mend->getPreviousFieldToken();
+	// }
+
+	if (mstart) {
+		if (!mstart->isData()) {
+			return;
+		} else if (mstart->isNull()) {
+			return;
+		}
+	}
+
+	//if (mend) {
+	//	if (!mend->isData()) {
+	//		mend = NULL;
+	//	} else if (mend->isNull()) {
+	//		mend = NULL;
+	//	}
+	//}
+
+	HTp tok = mstart;
+	string text;
+	while (tok && (tok != mnext)) {
+		if (!tok->isData()) {
+			tok = tok->getNextToken();
+			continue;
+		}
+		if (tok->isNull()) {
+			tok = tok->getNextToken();
+			continue;
+		}
+		text = tok->getText();
+		if ((!text.empty()) && (text.back() == '-')) {
+			text.pop_back();
+			text += m_marker;
+			text += '-';
+		} else {
+			text += m_marker;
+		}
+		tok->setText(text);
+		tok = tok->getNextToken();
 	}
 }
 
@@ -100566,2163 +104169,2181 @@ void Tool_modori::convertInstrumentNameToOriginal(HTp token) {
 
 //////////////////////////////
 //
-// Tool_modori::convertInstrumentAbbreviationToOriginal --
+// Tool_msearch::checkForMusicMatch -- See if the given position
+//    in the music matches the query.
 //
 
-void Tool_modori::convertInstrumentAbbreviationToOriginal(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?I'(.*)")) {
-		string text = "*oI'";
-		text += hre.getMatch(1);
-		token->setText(text);
+bool Tool_msearch::checkForMusicMatch(vector<NoteCell*>& notes, int index,
+		vector<MSearchQueryToken>& query, vector<NoteCell*>& match) {
+
+	match.clear();
+	int maxi = (int)notes.size() - index;
+	if ((int)query.size() > maxi) {
+		// Search would extend off of the end of the music, so cannot be a match.
+		match.clear();
+		return false;
 	}
-}
 
+	int c = 0;
+
+	for (int i=0; i<(int)query.size(); i++) {
+		int currindex = index + i - c;
+		int lastindex = index + i -c - 1;
+		int nextindex = index + i -c + 1;
+		if (nextindex >= (int)notes.size()) {
+			nextindex = -1;
+		}
+
+		if (currindex < 0) {
+			cerr << "STRANGE NEGATIVE INDEX " << currindex << endl;
+			break;
+		}
+
+		// If the query item can be anything, it automatically matches:
+		if (query[i].anything) {
+			match.push_back(notes[currindex]);
+			continue;
+		}
+
+		//////////////////////////////
+		//
+		// RHYTHM
+		//
+
+		if (!query[i].anyrhythm) {
+			if (notes[currindex]->getDuration() != query[i].duration) {
+				match.clear();
+				return false;
+			}
+		}
 
+		//////////////////////////////
+		//
+		// INTERVALS
+		//
 
-//////////////////////////////
-//
-// Tool_modori::convertKeySignatureToRegular --
-//
+		if (query[i].dinterval > -1000) {
+			// match to a specific diatonic interval to the next note
 
-void Tool_modori::convertKeySignatureToRegular(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?k(.*)")) {
-		string text = "*k";
-		text += hre.getMatch(1);
-		token->setText(text);
-	}
-}
+			double currpitch;
+			double nextpitch;
 
+			currpitch = notes[currindex]->getAbsDiatonicPitch();
 
+			if (nextindex >= 0) {
+				nextpitch = notes[nextindex]->getAbsDiatonicPitch();
+			} else {
+				nextpitch = -123456789.0;
+			}
 
-//////////////////////////////
-//
-// Tool_modori::convertInstrumentNameToRegular --
-//
+			// maybe be careful of rests getting into this calculation:
+			int interval = (int)(nextpitch - currpitch);
 
-void Tool_modori::convertInstrumentNameToRegular(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?I\"(.*)")) {
-		string text = "*I\"";
-		text += hre.getMatch(1);
-		token->setText(text);
-	}
-}
+			if (interval != query[i].dinterval) {
+				match.clear();
+				return false;
+			}
+		} else if (query[i].cinterval > -1000) {
+			// match to a specific chromatic interval to the next note
 
+			double currpitch;
+			double nextpitch;
 
+			currpitch = notes[currindex]->getAbsBase40Pitch();
 
-//////////////////////////////
-//
-// Tool_modori::convertInstrumentAbbreviationToRegular --
-//
+			if (nextindex >= 0) {
+				nextpitch = notes[nextindex]->getAbsBase40Pitch();
+			} else {
+				nextpitch = -123456789.0;
+			}
 
-void Tool_modori::convertInstrumentAbbreviationToRegular(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?I'(.*)")) {
-		string text = "*I'";
-		text += hre.getMatch(1);
-		token->setText(text);
-	}
-}
+			// maybe be careful of rests getting into this calculation:
+			int interval = (int)(nextpitch - currpitch);
 
+			if (interval != query[i].cinterval) {
+				match.clear();
+				return false;
+			}
 
+		} else if (!query[i].anyinterval) {
 
-//////////////////////////////
-//
-// Tool_modori::convertClefToModern --
-//
+			double currpitch;
+			double nextpitch;
+			double lastpitch;
 
-void Tool_modori::convertClefToModern(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?clef(.*)")) {
-		string text = "*mclef";
-		text += hre.getMatch(1);
-		token->setText(text);
-	}
-}
+			currpitch = notes[currindex]->getAbsDiatonicPitchClass();
 
+			if (nextindex >= 0) {
+				nextpitch = notes[nextindex]->getAbsDiatonicPitchClass();
+			} else {
+				nextpitch = -123456789.0;
+			}
 
+			if (lastindex >= 0) {
+				lastpitch = notes[nextindex]->getAbsDiatonicPitchClass();
+			} else {
+				lastpitch = -987654321.0;
+			}
 
-//////////////////////////////
-//
-// Tool_modori::convertClefToOriginal --
-//
+			if (query[i].anypitch) {
+				// search forward interval
+				if (nextindex < 0) {
+					// Match can not go off the edge of the music.
+					match.clear();
+					return false;
+				} else {
+					// check here if either note is a rest
+					if (notes[currindex]->isRest() || notes[nextindex]->isRest()) {
+						match.clear();
+						return false;
+					}
 
-void Tool_modori::convertClefToOriginal(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?clef(.*)")) {
-		string text = "*oclef";
-		text += hre.getMatch(1);
-		token->setText(text);
-	}
-}
+					if (query[i].direction > 0) {
+						if (nextpitch - currpitch <= 0.0) {
+							match.clear();
+							return false;
+						}
+					} if (query[i].direction < 0) {
+						if (nextpitch - currpitch >= 0.0) {
+							match.clear();
+							return false;
+						}
+					} else if (query[i].direction == 0.0) {
+						if (nextpitch - currpitch != 0) {
+							match.clear();
+							return false;
+						}
+					}
+				}
+			} else {
+				// search backward interval
+				if (lastindex < 0) {
+					// Match can not go off the edge of the music.
+					match.clear();
+					return false;
+				} else {
+					// check here if either note is a rest.
+					if (notes[currindex]->isRest() || notes[nextindex]->isRest()) {
+						match.clear();
+						return false;
+					}
 
+					if (query[i].direction > 0) {
+						if (lastpitch - currpitch <= 0.0) {
+							match.clear();
+							return false;
+						}
+					} if (query[i].direction < 0) {
+						if (lastpitch - currpitch >= 0.0) {
+							match.clear();
+							return false;
+						}
+					} else if (query[i].direction == 0.0) {
+						if (lastpitch - currpitch != 0) {
+							match.clear();
+							return false;
+						}
+					}
+				}
+			}
+		}
 
+		//////////////////////////////
+		//
+		// PITCH
+		//
 
-//////////////////////////////
-//
-// Tool_modori::convertClefToRegular --
-//
+		if (!query[i].anypitch) {
+			double qpitch = query[i].pc;
+			double npitch = 0;
+			if (notes[currindex]->isRest()) {
+				if (Convert::isNaN(qpitch)) {
+					// both notes are rests, so they match
+					match.push_back(notes[currindex]);
+					continue;
+				} else {
+					// query is not a rest but test note is
+					match.clear();
+					return false;
+				}
+			} else if (Convert::isNaN(qpitch)) {
+				// query is a rest but test note is not
+				match.clear();
+				return false;
+			}
 
-void Tool_modori::convertClefToRegular(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?clef(.*)")) {
-		string text = "*clef";
-		text += hre.getMatch(1);
-		token->setText(text);
-	}
-}
+			if (query[i].base == 40) {
+				npitch = notes[currindex]->getAbsBase40PitchClass();
+			} else if (query[i].base == 12) {
+				npitch = ((int)notes[currindex]->getAbsMidiPitch()) % 12;
+			} else if (query[i].base == 7) {
+				npitch = ((int)notes[currindex]->getAbsDiatonicPitch()) % 7;
+			} else {
+				npitch = notes[currindex]->getAbsBase40PitchClass();
+			}
 
+			if (qpitch != npitch) {
+				match.clear();
+				return false;
+			}
+		}
 
+		if (!query[i].harmonic.empty()) {
+			query[i].parseHarmonicQuery();
+			bool status = doHarmonicPitchSearch(query[i], notes[currindex]->getToken());
+			if (!status) {
+				return false;
+			}
+		}
 
-//////////////////////////////
-//
-// Tool_modori::convertMensurationToModern --
-//
+		// All requirements for the note were matched, so store note
+		// and continue to next note if needed.
+		match.push_back(notes[currindex]);
+	}
 
-void Tool_modori::convertMensurationToModern(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?met\\((.*)")) {
-		string text = "*mmet(";
-		text += hre.getMatch(1);
-		token->setText(text);
+	// Add extra token for marking tied notes at end of match
+	if (index + (int)query.size() < (int)notes.size()) {
+		match.push_back(notes[index + (int)query.size() - c]);
+	} else {
+		match.push_back(NULL);
 	}
+
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_modori::convertMensurationToOriginal --
+// Tool_msearch::doHarmonicPitchSearch --
 //
 
-void Tool_modori::convertMensurationToOriginal(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?met\\((.*)")) {
-		string text = "*omet(";
-		text += hre.getMatch(1);
-		token->setText(text);
+bool Tool_msearch::doHarmonicPitchSearch(MSearchQueryToken& query, HTp token) {
+	if (query.harmonic.empty()) {
+		return true;
 	}
-}
-
 
+	int lindex = token->getLineIndex();
+	if (m_verticalOnlyQ && m_sonoritiesChecked[lindex]) {
+		// Only count once if searching only for vertical sonoroties
+		// Later make this more efficient perhaps by not searching every
+		// note for vertical-only searches, but rather search
+		// the sonorities in one pass (but maybe this will not actually
+		// be more efficient).
+		return false;
+	}
+	m_sonoritiesChecked[lindex] = true;
+	SonorityDatabase& sonorities = m_sonorities[lindex];
+	if (sonorities.isEmpty()) {
+		sonorities.buildDatabase(token->getLine());
+	}
 
-//////////////////////////////
-//
-// Tool_modori::convertMensurationToRegular --
-//
+	bool exactQ = false;
+	bool onlyQ = false;
 
-void Tool_modori::convertMensurationToRegular(HTp token) {
-	HumRegex hre;
-	if (hre.search(token, "^\\*[mo]?met\\((.*)")) {
-		string text = "*met(";
-		text += hre.getMatch(1);
-		token->setText(text);
+	if (query.harmonic.find("==") != string::npos) {
+		exactQ = true;
+	} else if (query.harmonic.find("=") != string::npos) {
+		onlyQ = true;
 	}
-}
 
+	vector<int> diatonicCountsQuery(7, 0);
+	vector<int> diatonicCountsMatch(7, 0);
+	vector<int> diatonicCountsData(7, 0);
+	vector<int> chromaticCountsQuery(40, 0);
+	vector<int> chromaticCountsMatch(40, 0);
+	vector<int> chromaticCountsData(40, 0);
 
+	for (int i=0; i<sonorities.getNoteCount(); i++) {
+		diatonicCountsData.at(sonorities[i].getBase7Pc())++;
+		chromaticCountsData.at(sonorities[i].getBase40Pc())++;
+	}
 
-////////////////////
-//
-// Tool_modori::printInfo --
-//
+	int sum = 0;
+	for(int i=0; i<(int)query.hquery.size(); i++) {
+		if (query.hquery[i].hasAccidental()) {
+			diatonicCountsQuery.at(query.hquery[i].getBase7Pc())++;
+			if (query.hquery[i].hasUpperCase()) {
+				if (query.hquery[i].getBase7Pc() != sonorities.getLowest().getBase7Pc()) {
+					return false;
+				}
+			}
 
-void Tool_modori::printInfo(void) {
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
-	m_humdrum_text << "!! KEYS:" << endl;
+			// Don't check for same pitch-class twice:
+			if (chromaticCountsMatch.at(query.hquery[i].getBase40Pc())) {
+				continue;
+			}
+		} else {
+			diatonicCountsQuery.at(query.hquery[i].getBase7Pc())++;
+			if (query.hquery[i].hasUpperCase()) {
+				if (query.hquery[i].getBase7Pc() != sonorities.getLowest().getBase7Pc()) {
+					return false;
+				}
+			}
 
-	for (int t=1; t<(int)m_keys.size(); ++t) {
-		for (auto it = m_keys.at(t).begin(); it != m_keys.at(t).end(); ++it) {
-			m_humdrum_text << "!!\t" << it->first;
-			for (int j=0; j<(int)it->second.size(); ++j) {
-				m_humdrum_text << '\t' << it->second.at(j);
-		}
-			m_humdrum_text << endl;
+			// Don't check for same pitch-class twice:
+			if (diatonicCountsMatch.at(query.hquery[i].getBase7Pc())) {
+				continue;
+			}
 		}
-	}
 
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
-	m_humdrum_text << "!! CLEFS:" << endl;
+		int status = checkHarmonicPitchMatch(query.hquery[i], sonorities, false);
 
-	for (int t=1; t<(int)m_keys.size(); ++t) {
-		for (auto it = m_clefs.at(t).begin(); it != m_clefs.at(t).end(); ++it) {
-			m_humdrum_text << "!!\t" << it->first;
-			for (int j=0; j<(int)it->second.size(); ++j) {
-				m_humdrum_text << '\t' << it->second.at(j);
-			}
-			m_humdrum_text << endl;
+		if (!status) {
+			return false;
 		}
-	}
-
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
-	m_humdrum_text << "!! MENSURATIONS:" << endl;
 
-	for (int t=1; t<(int)m_mensurations.size(); ++t) {
-		for (auto it = m_mensurations.at(t).begin(); it != m_mensurations.at(t).end(); ++it) {
-			m_humdrum_text << "!!\t" << it->first;
-			for (int j=0; j<(int)it->second.size(); j++) {
-				m_humdrum_text << '\t' << it->second.at(j);
-			}
-			m_humdrum_text << endl;
+		if (query.hquery[i].hasAccidental()) {
+			chromaticCountsMatch.at(query.hquery[i].getBase40Pc()) += status;
+		} else {
+			diatonicCountsMatch.at(query.hquery[i].getBase7Pc()) += status;
 		}
-	}
+		sum += status;
 
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
-	m_humdrum_text << "!! LYRICS:" << endl;
+	}
 
-	for (int i=0; i<(int)m_lyrics.size(); i++) {
-		HTp token = m_lyrics[i];
-		m_humdrum_text << "!!\t";
-		m_humdrum_text << token;
-		m_humdrum_text << endl;
+	if ((!exactQ) && (!onlyQ)) {
+		return true;
 	}
 
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
-	m_humdrum_text << "!! TEXT:" << endl;
 
-	for (int i=0; i<(int)m_lotext.size(); i++) {
-		m_humdrum_text << "!!\t" << m_lotext[i] << endl;
+	if (exactQ && (sum != sonorities.getNoteCount())) {
+		return false;
 	}
 
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
-	m_humdrum_text << "!! REFERENCES:" << endl;
+	if (exactQ) {
+		for (int i=0; i<(int)diatonicCountsMatch.size(); i++) {
+			if (diatonicCountsMatch[i] != diatonicCountsQuery[i]) {
+				return false;
+			}
+		}
+		for (int i=0; i<(int)chromaticCountsMatch.size(); i++) {
+			if (chromaticCountsMatch[i] != chromaticCountsQuery[i]) {
+				return false;
+			}
+		}
+	} else if (onlyQ) {
+		SonorityDatabase son2;
+		for (int i=0; i<(int)query.hpieces.size(); i++) {
+			son2.addNote(query.hpieces[i]);
+		}
 
-	for (int i=0; i<(int)m_references.size(); i++) {
-		m_humdrum_text << "!!\t" << m_references[i].first << endl;
-		m_humdrum_text << "!!\t" << m_references[i].second << endl;
-		m_humdrum_text << "!!\n";
+		for (int k=0; k<sonorities.getNoteCount(); k++) {
+			int status2 = checkHarmonicPitchMatch(sonorities[k], son2, true);
+			if (!status2) {
+				return false;
+			}
+		}
 	}
 
-	m_humdrum_text << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
+	return true;
 }
 
 
 
-
 //////////////////////////////
 //
-// SonorityDatabase::buildDatabase --
+// Tool_msearch::checkHarmonicPitchMatch -- Returns the number of matched notes.
 //
 
-void SonorityDatabase::buildDatabase(HLp line) {
-	clear();
-	if (line == NULL) {
-		return;
-	}
-	m_line = line;
-	bool nullQ = false;
-	if (!line->isData()) {
-		return;
-	}
-	int lowesti = 0;
-	int lowest12 = 1000;
+int Tool_msearch::checkHarmonicPitchMatch(SonorityNoteData& query,
+		SonorityDatabase& sonorities, bool suppressQ) {
+	bool isChromatic = query.hasAccidental();
+	bool isLowest = query.hasUpperCase();
 
-	for (int i=0; i<line->getFieldCount(); i++) {
-		HTp token = m_line->token(i);
-		if (!token->isKern()) {
-			continue;
-		}
-		if (token->isRest()) {
-			// ignoring rests, at least for now
-			continue;
-		}
-		if (token->isNull()) {
-			nullQ = true;
-			token = token->resolveNull();
-		}
-		if (token->isNull()) {
-			continue;
-		}
-		int scount = token->getSubtokenCount();
-		for (int j=0; j<scount; j++) {
-			expandList();
-			m_notes.back().setToken(token, nullQ, j);
-			if (m_notes.back().getBase12() < lowest12) {
-				lowesti = (int)m_notes.size() - 1;
-				lowest12 = m_notes.back().getBase12();
+	if (isLowest) {
+		if (isChromatic) {
+			int cpc = query.getBase40Pc();
+			if (cpc != sonorities.getLowest().getBase40Pc()) {
+				return 0;
+			}
+		} else {
+			int dpc = query.getBase7Pc();
+			if (dpc != sonorities.getLowest().getBase7Pc()) {
+				return 0;
 			}
 		}
 	}
-	if (!m_notes.empty()) {
-		m_lowest = m_notes[lowesti];
-	}
-}
-
 
+	pair<HTp, int> tomark;
 
-//////////////////////////////
-//
-// SonorityDatabase::addNote --
-//
+	// this algorithm highlights all vertical sonorities of given pitch class.
+	int output = 0;
+	if (isChromatic) {
+		int cpitch = query.getBase40Pc();
+		int cpc = cpitch % 40;
+		for (int i=0; i<sonorities.getCount(); i++) {
+			if (cpc == sonorities[i].getBase40Pc()) {
+				if (!suppressQ) {
+					tomark.first = sonorities[i].getToken();
+					tomark.second = sonorities[i].getIndex();
+					m_tomark.push_back(tomark);
+				}
+				output += 1;
+			}
+		}
+	} else {
+		int dpitch = query.getBase7Pc();
+		int dpc = dpitch % 7;
+		for (int i=0; i<sonorities.getCount(); i++) {
+			if (dpc == sonorities[i].getBase7Pc()) {
+				if (!suppressQ) {
+					tomark.first = sonorities[i].getToken();
+					tomark.second = sonorities[i].getIndex();
+					m_tomark.push_back(tomark);
+				}
+				output += 1;
+			}
+		}
+	}
 
-void SonorityDatabase::addNote(const std::string& text) {
-	expandList();
-	m_notes.back().setString(text);
-	// not dealing with lowest note
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// MSearchQueryToken::parseHarmonicQuery --
+// Tool_msearch::fillTextQuery --
 //
 
-void MSearchQueryToken::parseHarmonicQuery(void) {
-	if (!hpieces.empty()) {
-		// do not reparse
-		return;
-	}
-	for (int i=0; i<(int)harmonic.size(); i++) {
-		char ch = tolower(harmonic[i]);
-		if (ch >= 'a' && ch <= 'g') {
-			hpieces.resize(hpieces.size() + 1);
-			hpieces.back() += harmonic[i];
-		} else if (ch == '-') {
-			hpieces.back() += ch;
-		} else if (ch == 'n') {
-			hpieces.back() += ch;
-		} else if (ch == '#') {
-			hpieces.back() += ch;
-		}
-	}
+void Tool_msearch::fillTextQuery(vector<MSearchTextQuery>& query,
+		const string& input) {
+	query.clear();
+	bool inquote = false;
 
-	hquery.resize(hpieces.size());
-	for (int i=0; i<(int)hpieces.size(); i++) {
-		hquery[i].setString(hpieces[i]);
+	query.resize(1);
+
+	for (int i=0; i<(int)input.size(); i++) {
+		if (input[i] == '"') {
+			inquote = !inquote;
+			query.resize(query.size() + 1);
+			continue;
+		}
+		if (isspace(input[i])) {
+			query.resize(query.size() + 1);
+		}
+		query.back().word.push_back(input[i]);
+		if (inquote) {
+			query.back().link = true;
+		}
 	}
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_msearch::Tool_msearch -- Set the recognized options for the tool.
+// Tool_msearch::fillMusicQuery --
 //
 
-Tool_msearch::Tool_msearch(void) {
-	define("debug=b",                     "diatonic search");
-	define("q|query=s:4c4d4e4f4g",        "combined rhythm/pitch query string");
-	define("p|pitch=s:cdefg",             "pitch query string");
-	define("i|interval=s:2222",           "interval query string");
-	define("r|d|rhythm|duration=s:44444", "rhythm query string");
-	define("t|text=s:",                   "lyrical text query string");
-	define("O|no-overlap=b",              "do not allow matches to overlap");
-	define("x|cross=b",                   "search across parts");
-	define("c|color=s",                   "highlight color");
-	define("m|mark|marker=s:@",           "marking character");
-	define("M|no-mark|no-marker=b",       "do not mark matches");
-	define("Q|quiet=b",                   "quiet mode: do not summarize matches");
-}
-
-
+void Tool_msearch::fillMusicQuery(vector<MSearchQueryToken>& query) {
+	query.clear();
 
-/////////////////////////////////
-//
-// Tool_msearch::run -- Do the main work of the tool.
-//
+	string qinput;
+	string pinput;
+	string iinput;
+	string rinput;
 
-bool Tool_msearch::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	if (getBoolean("query")) {
+		qinput = getString("query");
 	}
-	return status;
-}
 
+	if (getBoolean("pitch")) {
+		pinput = getString("pitch");
+		m_verticalOnlyQ = checkVerticalOnly(pinput);
+	}
 
-bool Tool_msearch::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (getBoolean("interval")) {
+		iinput = getString("interval");
 	}
-	return status;
-}
 
+	if (getBoolean("rhythm")) {
+		rinput = getString("rhythm");
+	}
 
-bool Tool_msearch::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (!rinput.empty()) {
+		fillMusicQueryRhythm(query, rinput);
 	}
-	return status;
-}
 
+	if (!qinput.empty()) {
+		fillMusicQueryInterleaved(query, qinput);
+	}
 
-bool Tool_msearch::run(HumdrumFile& infile) {
-	m_sonorities.resize(infile.getLineCount());
-	m_sonoritiesChecked.resize(infile.getLineCount());
-	fill(m_sonoritiesChecked.begin(), m_sonoritiesChecked.end(), false);
-	m_debugQ = getBoolean("debug");
-	m_quietQ = getBoolean("quiet");
-	m_nooverlapQ = getBoolean("no-overlap");
-	NoteGrid grid(infile);
-	if (m_debugQ) {
-		grid.printGridInfo(cerr);
-		// return 1;
+	if (!pinput.empty()) {
+		fillMusicQueryPitch(query, pinput);
 	}
-	initialize();
 
-	if (getBoolean("text")) {
-		m_text = getString("text");
+	if (!iinput.empty()) {
+		fillMusicQueryInterval(query, iinput);
 	}
 
-	if (m_text.empty()) {
-		vector<MSearchQueryToken> query;
-		fillMusicQuery(query);
-		if (!query.empty()) {
-			doMusicSearch(infile, grid, query);
+	if (query.size() == 1) {
+		if (query[0].anything) {
+			query.clear();
 		}
-	} else {
-		vector<MSearchTextQuery> query;
-		fillTextQuery(query, getString("text"));
-		doTextSearch(infile, grid, query);
 	}
 
-	infile.createLinesFromTokens();
-	m_humdrum_text << infile;
-
-	return 1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::initialize --
+// Tool_msearch::fillMusicQueryPitch --
 //
 
-void Tool_msearch::initialize(void) {
-	m_marker = getString("marker");
-	// only allowing a single character for now:
-	m_markQ = !getBoolean("no-marker");
-	if (!m_markQ) {
-		m_marker.clear();
-	} else if (!m_marker.empty()) {
-		m_marker = m_marker[0];
-	}
+void Tool_msearch::fillMusicQueryPitch(vector<MSearchQueryToken>& query,
+		const string& input) {
+	fillMusicQueryInterleaved(query, input);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::fillWords --
+// Tool_msearch::fillMusicQueryRhythm --
 //
 
-void Tool_msearch::fillWords(HumdrumFile& infile, vector<TextInfo*>& words) {
-	vector<HTp> textspines;
-	infile.getSpineStartList(textspines, "**silbe");
-	if (textspines.empty()) {
-		infile.getSpineStartList(textspines, "**text");
+void Tool_msearch::fillMusicQueryRhythm(vector<MSearchQueryToken>& query,
+		const string& input) {
+	string output;
+	output.reserve(input.size() * 4);
+
+	for (int i=0; i<(int)input.size(); i++) {
+		output += input[i];
+		output += ' ';
 	}
-	for (int i=0; i<(int)textspines.size(); i++) {
-		fillWordsForTrack(words, textspines[i]);
+
+	// remove spaces to allow rhythms:
+	// 64 => 64
+   // 32 => 32
+	// 16 => 16
+	for (int i=0; i<(int)output.size(); i++) {
+		if ((i > 1) && (output[i] == '6') && (output[i-1] == ' ') && (output[i-2] == '1')) {
+			output.erase(i-1, 1);
+			i--;
+		}
+		if ((i > 1) && (output[i] == '2') && (output[i-1] == ' ') && (output[i-2] == '3')) {
+			output.erase(i-1, 1);
+			i--;
+		}
+		if ((i > 1) && (output[i] == '4') && (output[i-1] == ' ') && (output[i-2] == '6')) {
+			output.erase(i-1, 1);
+			i--;
+		}
+      if ((i > 0) && (output[i] == '.')) {
+			output.erase(i-1, 1);
+			i--;
+		}
 	}
+
+	fillMusicQueryInterleaved(query, output, true);
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::fillWordsForTrack --
+// Tool_msearch::convertPitchesToIntervals --
 //
 
-void Tool_msearch::fillWordsForTrack(vector<TextInfo*>& words,
-		HTp starttoken) {
-	HTp tok = starttoken->getNextToken();
-	while (tok != NULL) {
-		if (tok->empty()) {
-			tok = tok->getNextToken();
-			continue;
-		}
-		if (tok->isNull()) {
-			tok = tok->getNextToken();
-			continue;
+string Tool_msearch::convertPitchesToIntervals(const string& input) {
+	if (input.empty()) {
+		return "";
+	}
+	for (int i=0; i<(int)input.size(); i++) {
+		if (isdigit(input[i])) {
+			return input;
 		}
-		if (!tok->isData()) {
-			tok = tok->getNextToken();
-			continue;
+		if (tolower(input[i] == 'r')) {
+			// not allowing rests for now
+			return input;
 		}
-		if (tok->at(0) == '-') {
-			// append a syllable to the end of previous word
-			if (!words.empty()) {
-				words.back()->fullword += tok->substr(1, string::npos);
-				if (words.back()->fullword.back() == '-') {
-					words.back()->fullword.pop_back();
+	}
+	vector<string> pitches;
+
+	for (int i=0; i<(int)input.size(); i++) {
+		char ch = tolower(input[i]);
+		if (ch >= 'a' && ch <= 'g') {
+			string val;
+			val += ch;
+			pitches.push_back(val);
+			if (i > 0) {
+				if (input[i-1] == '^') {
+					pitches.back().insert(0, "^");
 				}
-			}
-			tok = tok->getNextToken();
-			continue;
-		} else {
-			// start a new word
-			TextInfo* temp = new TextInfo();
-			temp->nexttoken = NULL;
-			if (!words.empty()) {
-				words.back()->nexttoken = tok;
-			}
-			temp->fullword = *tok;
-			if (!temp->fullword.empty()) {
-				if (temp->fullword.back() == '-') {
-					temp->fullword.pop_back();
+				if (input[i-1] == 'v') {
+					pitches.back().insert(0, "v");
 				}
 			}
-			temp->starttoken = tok;
-			words.push_back(temp);
-			tok = tok->getNextToken();
 			continue;
 		}
-	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_msearch::doTextSearch -- do a basic text search of all parts.
-//
-
-void Tool_msearch::doTextSearch(HumdrumFile& infile, NoteGrid& grid,
-		vector<MSearchTextQuery>& query) {
-
-	vector<TextInfo*> words;
-	words.reserve(10000);
-	fillWords(infile, words);
-	int tcount = 0;
-
-	HumRegex hre;
-	for (int i=0; i<(int)query.size(); i++) {
-		for (int j=0; j<(int)words.size(); j++) {
-			if (hre.search(words.at(j)->fullword, query.at(i).word, "i")) {
-				tcount++;
-				markTextMatch(infile, *words[j]);
+		if (!pitches.empty()) {
+			if (ch == 'n') {
+				pitches.back() += 'n';
+			} else if (ch == '-') {
+				pitches.back() += '-';
+			} else if (ch == '#') {
+				pitches.back() += '#';
 			}
 		}
 	}
 
-	string textinterp = "**text";
-	vector<HTp> interps;
-	infile.getSpineStartList(interps);
-	//int textcount = 0;
-	int silbecount = 0;
-	for (int i=0; i<(int)interps.size(); i++) {
-		//if (interps[i]->getText() == "**text") {
-		//	textcount++;
-		//}
-		if (interps[i]->getText() == "**silbe") {
-			silbecount++;
-		}
-	}
-	if (silbecount > 0) {
-		// giving priority to **silbe content
-		textinterp = "**silbe";
+	if (pitches.size() <= 1) {
+		return "";
 	}
 
-	if (tcount && m_markQ) {
-		string content = "!!!RDF";
-		content += textinterp;
-		content += ": ";
-		content += m_marker;
-		content += " = marked text";
-		if (getBoolean("color")) {
-			content += ", color=\"" + getString("color") + "\"";
+	vector<bool> chromatic(pitches.size(), false);
+	for (int i=0; i<(int)pitches.size(); i++) {
+		for (int j=(int)pitches[i].size()-1; j>0; j--) {
+			int ch = pitches[i][j];
+			if ((ch == 'n') || (ch == '-') || (ch == '#')) {
+				chromatic[i] = true;
+				break;
+			}
 		}
-		infile.appendLine(content);
-		infile.createLinesFromTokens();
 	}
 
-	for (int i=0; i<(int)words.size(); i++) {
-		delete words[i];
-		words[i] = NULL;
+	string output;
+	int p1;
+	int p2;
+	int base40;
+	int base7;
+	int sign;
+	for (int i=0; i<(int)pitches.size() - 1; i++) {
+		if (chromatic[i] && chromatic[i+1]) {
+			p1 = Convert::kernToBase40(pitches[i]);
+			p2 = Convert::kernToBase40(pitches[i+1]);
+			base40 = p2 - p1;
+			sign = base40 < 0 ? -1 : +1;
+			if (sign < 0) {
+				base40 = -base40;
+			}
+			string value = "";
+			if (sign < 0) {
+				value += "-";
+			}
+			value += Convert::base40ToIntervalAbbr(base40);
+			output += value;
+			output += " ";
+		} else {
+			p1 = Convert::kernToBase7(pitches[i]);
+			p2 = Convert::kernToBase7(pitches[i+1]);
+			base7 = p2 - p1;
+			sign = base7 < 0 ? -1 : +1;
+			if (sign < 0) {
+				base7 = -base7;
+			}
+			string value = "";
+			if (sign < 0) {
+				value += "-";
+			}
+			value += to_string(base7 + 1);
+			output += value;
+			output += " ";
+		}
 	}
 
-	if (!m_quietQ) {
-		addTextSearchSummary(infile, tcount, m_marker);
+	if (output.size() > 0) {
+		if (output.back() == ' ') {
+			output.resize((int)output.size() - 1);
+		}
 	}
-}
 
-
-
-//////////////////////////////
-//
-// Tool_msearch::printQuery --
-//
-
-void Tool_msearch::printQuery(vector<MSearchQueryToken>& query) {
-	for (int i=0; i<(int)query.size(); i++) {
-		cout << query[i];
-	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::doMusicSearch -- do a basic melodic search of all parts.
+// Tool_msearch::fillMusicQueryInterval --
 //
 
-void Tool_msearch::doMusicSearch(HumdrumFile& infile, NoteGrid& grid,
-		vector<MSearchQueryToken>& query) {
+void Tool_msearch::fillMusicQueryInterval(vector<MSearchQueryToken>& query,
+		const string& input) {
 
-	m_matches.clear();
+	string newinput = convertPitchesToIntervals(input);
 
-	if (m_debugQ) {
-		printQuery(query);
-	}
+	char ch;
+	int counter = 0;
+	MSearchQueryToken temp;
+	MSearchQueryToken *active = &temp;
 
-	vector<vector<NoteCell*>> attacks;
-	attacks.resize(grid.getVoiceCount());
-	for (int i=0; i<grid.getVoiceCount(); i++) {
-		grid.getNoteAndRestAttacks(attacks[i], i);
+	if (query.size() > 0) {
+		active = &query.at(counter);
+	} else {
+		// what is this for?
 	}
 
-	vector<NoteCell*> match;
-	int mcount = 0;
-	for (int i=0; i<(int)attacks.size(); i++) {
-		for (int j=0; j<(int)attacks[i].size(); j++) {
-			m_tomark.clear();
-			bool status = checkForMusicMatch(attacks[i], j, query, match);
-			if (!status) {
-				m_tomark.clear();
+	int sign = 1;
+	string alteration;
+	for (int i=0; i<(int)newinput.size(); i++) {
+		ch = newinput[i];
+		if (ch == ' ') {
+			// skip over spaces
+			continue;
+		}
+		if ((ch == 'P') ||  (ch == 'p')) {
+			alteration = "P";
+			continue;
+		}
+		if ((ch == 'd') ||  (ch == 'D')) {
+			if ((!alteration.empty()) && (alteration[0] == 'd')) {
+				alteration += "d";
+			} else {
+				alteration = "d";
 			}
-			if (status && !match.empty()) {
-				mcount++;
-				markMatch(infile, match);
-				storeMatch(match);
-				// cerr << "FOUND MATCH AT " << i << ", " << j << endl;
-				// markNotes(attacks[i], j, (int)query.size());
+			continue;
+		}
+		if ((ch == 'A') ||  (ch == 'a')) {
+			if ((!alteration.empty()) && (alteration[0] == 'A')) {
+				alteration += "A";
+			} else {
+				alteration = "A";
 			}
+			continue;
 		}
-	}
-
-	if (mcount && m_markQ) {
-		string content = "!!!RDF**kern: " + m_marker + " = marked note";
-		if (getBoolean("color")) {
-			content += ", color=\"" + getString("color") + "\"";
+		if ((ch == 'M') ||  (ch == 'm')) {
+			alteration = ch;
+			continue;
 		}
-		infile.appendLine(content);
-		infile.createLinesFromTokens();
-	}
-	if (!m_quietQ) {
-		addMusicSearchSummary(infile, mcount, m_marker);
-	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_msearch::addMusicSearchSummary --
-//
-
-void Tool_msearch::addMusicSearchSummary(HumdrumFile& infile, int mcount, const string& marker) {
-
-	m_barnums = infile.getMeasureNumbers();
-
-	infile.appendLine("!!@@BEGIN: MUSIC_SEARCH_RESULT");
-	string line;
-
-	line = "!!@QUERY:\t";
+		if (ch == '-') {
+			sign = -1;
+			continue;
+		}
+		if (ch == '+') {
+			sign = +1;
+			continue;
+		}
+		ch = tolower(ch);
 
-	if (getBoolean("query")) {
-		line += " -q ";
-		string qstring = getString("query");
-		makeLowerCase(qstring);
-		if ((qstring.find(' ') != string::npos) || (qstring.find('(') != string::npos)) {
-			line += '"';
-			line += qstring;
-			line += '"';
-		} else {
-			line += qstring;
+		if (!isdigit(ch)) {
+			// skip over non-digits (sign of interval
+			// will be read retroactively).
+			continue;
 		}
-	}
 
-	if (getBoolean("pitch")) {
-		line += " -p ";
-		string pstring = getString("pitch");
-		makeLowerCase(pstring);
-		if ((pstring.find(' ') != string::npos) || (pstring.find('(') != string::npos)) {
-			line += '"';
-			line += pstring;
-			line += '"';
-		} else {
-			line += pstring;
-		}
-	}
+		// check for intervals.  Intervals will trigger a
+		// new element in the query list
 
-	if (getBoolean("rhythm")) {
-		line += " -r ";
-		string rstring = getString("rhythm");
-		makeLowerCase(rstring);
-		if ((rstring.find(' ') != string::npos) || (rstring.find('(') != string::npos)) {
-			line += '"';
-			line += rstring;
-			line += '"';
+		active->anything = false;
+		active->anyinterval = false;
+		// active->direction = 1;
+
+		if (alteration.empty()) {
+			// store a diatonic interval
+			active->dinterval = (ch - '0') - 1; // zero-indexed interval
+			active->dinterval *= sign;
 		} else {
-			line += rstring;
+			active->cinterval = makeBase40Interval((ch - '0') - 1, alteration);
+			active->cinterval *= sign;
 		}
-	}
+		sign = 1;
+		alteration.clear();
 
-	if (getBoolean("interval")) {
-		line += " -i ";
-		string istring = getString("interval");
-		makeLowerCase(istring);
-		if ((istring.find(' ') != string::npos) || (istring.find('(') != string::npos)) {
-			line += '"';
-			line += istring;
-			line += '"';
+		if (active == &temp) {
+			query.push_back(temp);
+			temp.clear();
+		}
+		counter++;
+		if ((int)query.size() > counter) {
+			active = &query.at(counter);
 		} else {
-			line += istring;
+			active = &temp;
 		}
 	}
 
-	infile.appendLine(line);
-
-	line = "!!@MATCHES:\t";
-	line += to_string(mcount);
-	infile.appendLine(line);
-
-	if (m_markQ) {
-		line = "!!@MARKER:\t";
-		line += marker;
-		infile.appendLine(line);
-	}
-
-	// Print music match location here.
-	for (int i=0; i<(int)m_matches.size(); i++) {
-		addMatch(infile, m_matches[i]);
+	// The last element in the interval search is set to
+	// any pitch, because the interval was already checked
+	// to the next note, and this value is needed to highlight
+	// the next note of the interval.
+	active->anything = true;
+	active->anyinterval = true;
+	if (active == &temp) {
+		query.push_back(temp);
+		temp.clear();
 	}
 
-	infile.appendLine("!!@@END: MUSIC_SEARCH_RESULT");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::addMatch --
-//
-// Todo:
-//		* add duration of match
+// Tool_msearch::makeBase40Interval --
 //
 
-void Tool_msearch::addMatch(HumdrumFile& infile, vector<NoteCell*>& match) {
-	if (match.empty()) {
-		return;
+int Tool_msearch::makeBase40Interval(int diatonic, const string& alteration) {
+	int sign = 1;
+	if (diatonic < 0) {
+		sign = -1;
+		diatonic = -diatonic;
 	}
-	if (match.back() == NULL) {
-		// strange problem
-		return;
+	bool perfectQ = false;
+	int base40 = 0;
+	switch (diatonic) {
+		case 0:  // unison
+			base40 = 0;
+			perfectQ = true;
+			break;
+		case 1:  // second
+			base40 = 6;
+			perfectQ = false;
+			break;
+		case 2:  // third
+			base40 = 12;
+			perfectQ = false;
+			break;
+		case 3:  // fourth
+			base40 = 17;
+			perfectQ = true;
+			break;
+		case 4:  // fifth
+			base40 = 23;
+			perfectQ = true;
+			break;
+		case 5:  // sixth
+			base40 = 29;
+			perfectQ = false;
+			break;
+		case 6:  // seventh
+			base40 = 35;
+			perfectQ = false;
+			break;
+		case 7:  // octave
+			base40 = 40;
+			perfectQ = true;
+			break;
+		case 8:  // ninth
+			base40 = 46;
+			perfectQ = false;
+			break;
+		case 9:  // tenth
+			base40 = 52;
+			perfectQ = false;
+			break;
+		default:
+			cerr << "cannot handle this interval yet.  Setting to unison" << endl;
+			base40 = 0;
+			perfectQ = 1;
 	}
-	int startIndex   = match.at(0)->getLineIndex();
-	int endIndex     = match.back()->getLineIndex();
-	int startMeasure = m_barnums.at(startIndex);
-	int endMeasure   = m_barnums.at(endIndex);
-
-	infile.appendLine("!!@@BEGIN:\tMATCH");
-
-	string measure = "!!@MEASURE: ";
 
-	measure += to_string(startMeasure);
-	if (startMeasure != endMeasure) {
-		measure += " ";
-		measure += to_string(endMeasure);
+	if (perfectQ) {
+		if (alteration == "P") {
+			// do nothing since the interval is already perfect
+		} else if ((!alteration.empty()) && (alteration[0] == 'd')) {
+			if (alteration.size() <= 2) {
+				base40 -= (int)alteration.size();
+			} else {
+				cerr << "TOO MUCH DIMINISHED, IGNORING" << endl;
+			}
+		} else if ((!alteration.empty()) && (alteration[0] == 'A')) {
+			if (alteration.size() <= 2) {
+				base40 += (int)alteration.size();
+			} else {
+				cerr << "TOO MUCH AUGMENTED, IGNORING" << endl;
+			}
+		}
+	} else {
+		if (alteration == "M") {
+			// do nothing since the interval is already major
+		} else if (alteration == "m") {
+			base40--;
+		} else if ((!alteration.empty()) && (alteration[0] == 'd')) {
+			if (alteration.size() <= 2) {
+				base40 -= (int)alteration.size() + 1;
+			} else {
+				cerr << "TOO MUCH DIMINISHED, IGNORING" << endl;
+			}
+		} else if ((!alteration.empty()) && (alteration[0] == 'A')) {
+			if (alteration.size() <= 2) {
+				base40 += (int)alteration.size();
+			} else {
+				cerr << "TOO MUCH AUGMENTED, IGNORING" << endl;
+			}
+		}
 	}
-	infile.appendLine(measure);
-
-	infile.appendLine("!!@@END:\tMATCH");
+	base40 *= sign;
+	return base40;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::makeLowerCase --
+// Tool_msearch::fillMusicQueryInterleaved --
 //
 
-void Tool_msearch::makeLowerCase(string& inout) {
-	for (int i=0; i<(int)inout.size(); i++) {
-		inout[i] = tolower(inout[i]);
-	}
-}
+void Tool_msearch::fillMusicQueryInterleaved(vector<MSearchQueryToken>& query,
+		const string& input, bool rhythmQ) {
 
+	string newinput = input;
+	char ch;
+	int counter = 0;
+	MSearchQueryToken temp;
+	MSearchQueryToken *active = &temp;
+	string paren;
 
+	if (query.size() > 0) {
+		active = &query.at(counter);
+	} else {
+		// what is this for?
+	}
 
-//////////////////////////////
-//
-// Tool_msearch::addTextSearchSummary --
-//
+	for (int i=0; i<(int)newinput.size(); i++) {
+		paren.clear();
+		ch = tolower(newinput[i]);
+		if (ch == '(') {
+			paren += ch;
+			newinput[i] = ' ';
+			// A harmonic search initiated
+			int j = i;
+			bool keepQ = true;
+			bool diatonicQ = false;
+			for (j=i+1; j<(int)newinput.size(); j++) {
+				char ch2 = tolower(newinput[j]);
+				if (ch2 == ')') {
+					paren += ch2;
+					newinput[j] = ' ';
+					break;
+				}
+				if (ch2 >= 'a' && ch2 <= 'g') {
+					if (diatonicQ) {
+						keepQ = false;
+					} else {
+						diatonicQ = true;
+					}
+				}
+				if (keepQ) {
+					paren += newinput[j];
+					continue;
+				} else {
+					paren += newinput[j];
+					newinput[j] = ' ';
+				}
+			}
+			if (!paren.empty()) {
+				active->harmonic = paren;
+				paren.clear();
+			}
+			continue;
+		}
 
-void Tool_msearch::addTextSearchSummary(HumdrumFile& infile, int mcount, const string& marker) {
-	infile.appendLine("!!@@BEGIN: TEXT_SEARCH_RESULT");
-	string line;
+		if (ch == '=') {
+			continue;
+		}
+		if (ch == ' ') {
+			// skip over multiple spaces
+			if (i > 0) {
+            if (newinput[i-1] == ' ') {
+					continue;
+				}
+			}
+		}
 
-	line = "!!@QUERY:\t";
+		if (ch == '^') {
+			active->anything = false;
+			active->anyinterval = false;
+			active->direction = -1;
+			continue;
+		}
+		if (ch == 'v') {
+			active->anything = false;
+			active->anyinterval = false;
+			active->direction = 1;
+			continue;
+		}
 
-	if (getBoolean("text")) {
-		line += " -t ";
-		string tstring = getString("text");
-		if (tstring.find(' ') != string::npos) {
-			line += '"';
-			line += tstring;
-			line += '"';
-		} else {
-			line += tstring;
+		// process rhythm.  This must go first then intervals then pitches
+		if (isdigit(ch) || (ch == '.')) {
+			active->anything = false;
+			active->anyrhythm = false;
+			active->rhythm += ch;
+			if (i < (int)newinput.size() - 1) {
+				if (newinput[i+1] == ' ') {
+					if (active == &temp) {
+						query.push_back(temp);
+						temp.clear();
+					}
+					counter++;
+					if ((int)query.size() > counter) {
+						active = &query.at(counter);
+					} else {
+						active = &temp;
+					}
+					continue;
+				}
+			} else {
+				// this is the last charcter in the input string
+				if (active == &temp) {
+						query.push_back(temp);
+						temp.clear();
+				}
+				counter++;
+				if ((int)query.size() > counter) {
+					active = &query.at(counter);
+				} else {
+					active = &temp;
+				}
+			}
 		}
-	}
 
-	infile.appendLine(line);
+		// check for intervals.  Intervals will trigger a
+		// new element in the query list
+		// A new type ^ or v will not increment the query list
+		// (and they will expect a pitch after them).
+		if (ch == '/') {
+			active->anything = false;
+			active->anyinterval = false;
+			active->direction = 1;
+			if (active == &temp) {
+				query.push_back(temp);
+				temp.clear();
+			}
+			counter++;
+			if ((int)query.size() > counter) {
+				active = &query.at(counter);
+			} else {
+				active = &temp;
+			}
+			continue;
+		} else if (ch == '\\') {
+			active->anything = false;
+			active->anyinterval = false;
+			active->direction = -1;
+			if (active == &temp) {
+				query.push_back(temp);
+				temp.clear();
+			}
+			counter++;
+			if ((int)query.size() > counter) {
+				active = &query.at(counter);
+			} else {
+				active = &temp;
+			}
+			continue;
+		} else if (ch == '=') {
+			active->anything = false;
+			active->anyinterval = false;
+			active->direction = 0;
+			if (active == &temp) {
+				query.push_back(temp);
+				temp.clear();
+			}
+			counter++;
+			if ((int)query.size() > counter) {
+				active = &query.at(counter);
+			} else {
+				active = &temp;
+			}
+			continue;
+		}
 
-	line = "!!@MATCHES:\t";
-	line += to_string(mcount);
-	infile.appendLine(line);
+		// check for actual pitches
+		if ((ch >= 'a' && ch <= 'g')) {
+			active->anything = false;
+			active->anypitch = false;
+			active->base = 7;
+			active->pc = (ch - 'a' + 5) % 7;
+			if (active == &temp) {
+				query.push_back(temp);
+				temp.clear();
+			}
+			counter++;
+			if ((int)query.size() > counter) {
+				active = &query.at(counter);
+			} else {
+				active = &temp;
+			}
+			continue;
+		} else if (ch == 'r') {
+			active->anything = false;
+			active->anypitch = false;
+			active->base = 7;
+			active->pc = GRIDREST;
+			if (active == &temp) {
+				query.push_back(temp);
+				temp.clear();
+			}
+			counter++;
+			if ((int)query.size() > counter) {
+				active = &query.at(counter);
+			} else {
+				active = &temp;
+			}
+			continue;
+		}
 
-	if (m_markQ) {
-		line = "!!@MARKER:\t";
-		line += marker;
-		infile.appendLine(line);
+		// accidentals:
+		if ((!query.empty()) && (ch == 'n') && (!Convert::isNaN(query.back().pc))) {
+			query.back().base = 40;
+			query.back().pc = Convert::base7ToBase40((int)query.back().pc + 70) % 40;
+		} else if ((!query.empty()) && (ch == '#') && (!Convert::isNaN(query.back().pc))) {
+			query.back().base = 40;
+			query.back().pc = (Convert::base7ToBase40((int)query.back().pc + 70) + 1) % 40;
+		} else if ((!query.empty()) && (ch == '-') && (!Convert::isNaN(query.back().pc))) {
+			query.back().base = 40;
+			query.back().pc = (Convert::base7ToBase40((int)query.back().pc + 70) - 1) % 40;
+		}
+		// deal with double sharps and double flats here
 	}
 
-	// Print match location here.
-	infile.appendLine("!!@@END: TEXT_SEARCH_RESULT");
-}
-
-
-
-//////////////////////////////
-//
-// Tool_msearch::markNote --
-//
-
-void Tool_msearch::markNote(HTp token, int index) {
-	if (index < 0) {
-		return;
-	}
-	if (!token->isChord()) {
-		if (token->find(m_marker) == string::npos) {
-			string text = *token;
-			text += m_marker;
-			token->setText(text);
+	// Convert rhythms to durations
+	for (int i=0; i<(int)query.size(); i++) {
+		if (query[i].anyrhythm) {
+			continue;
 		}
-		return;
-	}
-	vector<std::string> subtoks = token->getSubtokens();
-	if (index >= (int)subtoks.size()) {
-		return;
-	}
-	if (subtoks[index].find(m_marker) == string::npos) {
-		subtoks[index] += m_marker;
-		string output = subtoks[0];
-		for (int i=1; i<(int)subtoks.size(); i++) {
-			output += " ";
-			output += subtoks[i];
+		if (query[i].rhythm.empty()) {
+			continue;
 		}
-		token->setText(output);
+		query[i].duration = Convert::recipToDuration(query[i].rhythm);
 	}
+
+	// what is this for (end condition)?
+	//if ((!query.empty()) && (query[0].base <= 0)) {
+	//	temp.clear();
+	//	temp.anything = true;
+	//	query.insert(query.begin(), temp);
+	//}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::markMatch -- assumes monophonic music.
+// checkVerticalOnly --
 //
 
-void Tool_msearch::markMatch(HumdrumFile& infile, vector<NoteCell*>& match) {
-	for (int i=0; i<(int)m_tomark.size(); i++) {
-		markNote(m_tomark[i].first, m_tomark[i].second);
+bool Tool_msearch::checkVerticalOnly(const string& input) {
+	if (input.empty()) {
+		return false;
 	}
-	if (match.empty()) {
-		return;
+	if (input.size() < 2) {
+		return false;
 	}
-	HTp mstart = match[0]->getToken();
-	HTp mend = NULL;
-	if (match.back() != NULL) {
-		mend = match.back()->getToken();
-	} else {
-		// there is an extra NULL token at the end of the music to allow
-		// marking tied notes.
+	if (input[0] != '(') {
+		return false;
 	}
-	HTp tok = mstart;
-	string text;
-	while (tok && (tok != mend)) {
-		if (!tok->isData()) {
-			tok = tok->getNextToken();
-			continue;
-		}
-		if (tok->isNull()) {
-			tok = tok->getNextToken();
-			continue;
-		}
-		if (tok->empty()) {
-			// skip marking null tokens
-			tok = tok->getNextToken();
-			continue;
+	if (input.back() != ')') {
+		return false;
+	}
+	for (int i=1; i<(int)input.size()-1; i++) {
+		// Maybe allow internal () if there is nothing outside of them.
+		if (input[i] == '(') {
+			return false;
 		}
-		markNote(tok, 0);
-		tok = tok->getNextToken();
-		if (tok && !tok->isKern()) {
-			cerr << "STRANGE LINKING WITH TEXT SPINE" << endl;
-			break;
+		if (input[i] == ')') {
+			return false;
 		}
 	}
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::markTextMatch -- assumes monophonic voices.
+// Tool_msearch::storeMatch -- Store a search result for later printing
+//    in the input file footer.
 //
 
-void Tool_msearch::markTextMatch(HumdrumFile& infile, TextInfo& word) {
-	HTp mstart = word.starttoken;
-	HTp mnext = word.nexttoken;
-	// while (mstart && !mstart->isKern()) {
-	// 	mstart = mstart->getPreviousFieldToken();
-	// }
-	// HTp mend = word.nexttoken;
-	// while (mend && !mend->isKern()) {
-	// 	mend = mend->getPreviousFieldToken();
-	// }
-
-	if (mstart) {
-		if (!mstart->isData()) {
-			return;
-		} else if (mstart->isNull()) {
-			return;
-		}
+void Tool_msearch::storeMatch(vector<NoteCell*>& match) {
+	m_matches.resize(m_matches.size() + 1);
+	m_matches.back().resize(match.size());
+	for (int i=0; i<(int)match.size(); i++) {
+		m_matches.back().at(i) = match.at(i);
 	}
+}
 
-	//if (mend) {
-	//	if (!mend->isData()) {
-	//		mend = NULL;
-	//	} else if (mend->isNull()) {
-	//		mend = NULL;
-	//	}
-	//}
 
-	HTp tok = mstart;
-	string text;
-	while (tok && (tok != mnext)) {
-		if (!tok->isData()) {
-			tok = tok->getNextToken();
-			continue;
-		}
-		if (tok->isNull()) {
-			tok = tok->getNextToken();
-			continue;
-		}
-		text = tok->getText();
-		if ((!text.empty()) && (text.back() == '-')) {
-			text.pop_back();
-			text += m_marker;
-			text += '-';
-		} else {
-			text += m_marker;
-		}
-		tok->setText(text);
-		tok = tok->getNextToken();
+
+//////////////////////////////
+//
+// operator<< -- print MSearchQueryToken item.
+//
+
+ostream& operator<<(ostream& out, MSearchQueryToken& item) {
+	out << "ITEM: "           << endl;
+	out << "\tANYTHING:\t"    << item.anything    << endl;
+	out << "\tANYPITCH:\t"    << item.anypitch    << endl;
+	out << "\tANYINTERVAL:\t" << item.anyinterval << endl;
+	out << "\tANYRHYTHM:\t"   << item.anyrhythm   << endl;
+	out << "\tPC:\t\t"        << item.pc          << endl;
+	out << "\tBASE:\t\t"      << item.base        << endl;
+	out << "\tDIRECTION:\t"   << item.direction   << endl;
+	out << "\tDINTERVAL:\t"   << item.dinterval   << endl;
+	out << "\tCINTERVAL:\t"   << item.cinterval   << endl;
+	out << "\tRHYTHM:\t\t"    << item.rhythm      << endl;
+	out << "\tDURATION:\t"    << item.duration    << endl;
+	if (!item.harmonic.empty()) {
+		out << "\tHARMONIC:\t" << item.harmonic    << endl;
 	}
+	return out;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::checkForMusicMatch -- See if the given position
-//    in the music matches the query.
+// Tool_musedata2hum::Tool_musedata2hum --
 //
 
-bool Tool_msearch::checkForMusicMatch(vector<NoteCell*>& notes, int index,
-		vector<MSearchQueryToken>& query, vector<NoteCell*>& match) {
+Tool_musedata2hum::Tool_musedata2hum(void) {
+	// Options& options = m_options;
+	// options.define("k|kern=b","display corresponding **kern data");
 
-	match.clear();
-	int maxi = (int)notes.size() - index;
-	if ((int)query.size() > maxi) {
-		// Search would extend off of the end of the music, so cannot be a match.
-		match.clear();
-		return false;
-	}
+	define("g|group=s:score", "the data group to process");
+	define("r|recip=b",       "output **recip spine");
+	define("s|stems=b",       "include stems in output");
+	define("omv|no-omv=b",    "exclude extracted OMV record in output data");
+}
 
-	int c = 0;
 
-	for (int i=0; i<(int)query.size(); i++) {
-		int currindex = index + i - c;
-		int lastindex = index + i -c - 1;
-		int nextindex = index + i -c + 1;
-		if (nextindex >= (int)notes.size()) {
-			nextindex = -1;
-		}
 
-		if (currindex < 0) {
-			cerr << "STRANGE NEGATIVE INDEX " << currindex << endl;
-			break;
-		}
+//////////////////////////////
+//
+// initialize --
+//
 
-		// If the query item can be anything, it automatically matches:
-		if (query[i].anything) {
-			match.push_back(notes[currindex]);
-			continue;
-		}
+void Tool_musedata2hum::initialize(void) {
+	m_stemsQ = getBoolean("stems");
+	m_recipQ = getBoolean("recip");
+	m_group  = getString("group");
+	m_noOmvQ = getBoolean("no-omv");
+}
 
-		//////////////////////////////
-		//
-		// RHYTHM
-		//
 
-		if (!query[i].anyrhythm) {
-			if (notes[currindex]->getDuration() != query[i].duration) {
-				match.clear();
-				return false;
-			}
-		}
 
-		//////////////////////////////
-		//
-		// INTERVALS
-		//
+//////////////////////////////
+//
+// Tool_musedata2hum::setOptions --
+//
 
-		if (query[i].dinterval > -1000) {
-			// match to a specific diatonic interval to the next note
+void Tool_musedata2hum::setOptions(int argc, char** argv) {
+	m_options.process(argc, argv);
+}
 
-			double currpitch;
-			double nextpitch;
 
-			currpitch = notes[currindex]->getAbsDiatonicPitch();
+void Tool_musedata2hum::setOptions(const vector<string>& argvlist) {
+    m_options.process(argvlist);
+}
 
-			if (nextindex >= 0) {
-				nextpitch = notes[nextindex]->getAbsDiatonicPitch();
-			} else {
-				nextpitch = -123456789.0;
-			}
 
-			// maybe be careful of rests getting into this calculation:
-			int interval = (int)(nextpitch - currpitch);
 
-			if (interval != query[i].dinterval) {
-				match.clear();
-				return false;
-			}
-		} else if (query[i].cinterval > -1000) {
-			// match to a specific chromatic interval to the next note
+//////////////////////////////
+//
+// Tool_musedata2hum::getOptionDefinitions -- Used to avoid
+//     duplicating the definitions in the test main() function.
+//
 
-			double currpitch;
-			double nextpitch;
+Options Tool_musedata2hum::getOptionDefinitions(void) {
+	return m_options;
+}
 
-			currpitch = notes[currindex]->getAbsBase40Pitch();
 
-			if (nextindex >= 0) {
-				nextpitch = notes[nextindex]->getAbsBase40Pitch();
-			} else {
-				nextpitch = -123456789.0;
-			}
 
-			// maybe be careful of rests getting into this calculation:
-			int interval = (int)(nextpitch - currpitch);
+//////////////////////////////
+//
+// Tool_musedata2hum::convert -- Convert a MusicXML file into
+//     Humdrum content.
+//
 
-			if (interval != query[i].cinterval) {
-				match.clear();
-				return false;
-			}
+bool Tool_musedata2hum::convertFile(ostream& out, const string& filename) {
+	MuseDataSet mds;
+	int result = mds.readFile(filename);
+	if (!result) {
+		cerr << "\nMuseData file [" << filename << "] has syntax errors\n";
+		cerr << "Error description:\t" << mds.getError() << "\n";
+		exit(1);
+	}
+	return convert(out, mds);
+}
 
-		} else if (!query[i].anyinterval) {
 
-			double currpitch;
-			double nextpitch;
-			double lastpitch;
+bool Tool_musedata2hum::convert(ostream& out, istream& input) {
+	MuseDataSet mds;
+	mds.read(input);
+	return convert(out, mds);
+}
 
-			currpitch = notes[currindex]->getAbsDiatonicPitchClass();
 
-			if (nextindex >= 0) {
-				nextpitch = notes[nextindex]->getAbsDiatonicPitchClass();
-			} else {
-				nextpitch = -123456789.0;
-			}
+bool Tool_musedata2hum::convertString(ostream& out, const string& input) {
+	MuseDataSet mds;
+	int result = mds.readString(input);
+	if (!result) {
+		cout << "\nXML content has syntax errors\n";
+		cout << "Error description:\t" << mds.getError() << "\n";
+		exit(1);
+	}
+	return convert(out, mds);
+}
 
-			if (lastindex >= 0) {
-				lastpitch = notes[nextindex]->getAbsDiatonicPitchClass();
-			} else {
-				lastpitch = -987654321.0;
-			}
 
-			if (query[i].anypitch) {
-				// search forward interval
-				if (nextindex < 0) {
-					// Match can not go off the edge of the music.
-					match.clear();
-					return false;
-				} else {
-					// check here if either note is a rest
-					if (notes[currindex]->isRest() || notes[nextindex]->isRest()) {
-						match.clear();
-						return false;
-					}
+bool Tool_musedata2hum::convert(ostream& out, MuseDataSet& mds) {
+	int partcount = mds.getFileCount();
+	if (partcount == 0) {
+		cerr << "Error: No parts found in data:" << endl;
+		cerr << mds << endl;
+		return false;
+	}
+	initialize();
 
-					if (query[i].direction > 0) {
-						if (nextpitch - currpitch <= 0.0) {
-							match.clear();
-							return false;
-						}
-					} if (query[i].direction < 0) {
-						if (nextpitch - currpitch >= 0.0) {
-							match.clear();
-							return false;
-						}
-					} else if (query[i].direction == 0.0) {
-						if (nextpitch - currpitch != 0) {
-							match.clear();
-							return false;
-						}
-					}
-				}
-			} else {
-				// search backward interval
-				if (lastindex < 0) {
-					// Match can not go off the edge of the music.
-					match.clear();
-					return false;
-				} else {
-					// check here if either note is a rest.
-					if (notes[currindex]->isRest() || notes[nextindex]->isRest()) {
-						match.clear();
-						return false;
-					}
+	m_tempo = mds.getMidiTempo();
 
-					if (query[i].direction > 0) {
-						if (lastpitch - currpitch <= 0.0) {
-							match.clear();
-							return false;
-						}
-					} if (query[i].direction < 0) {
-						if (lastpitch - currpitch >= 0.0) {
-							match.clear();
-							return false;
-						}
-					} else if (query[i].direction == 0.0) {
-						if (lastpitch - currpitch != 0) {
-							match.clear();
-							return false;
-						}
-					}
-				}
-			}
-		}
+	vector<int> groupMemberIndex = mds.getGroupIndexList(m_group);
+	if (groupMemberIndex.empty()) {
+		cerr << "Error: no files in the " << m_group << " membership." << endl;
+		return false;
+	}
 
-		//////////////////////////////
-		//
-		// PITCH
-		//
+	HumGrid outdata;
+	bool status = true;
+	for (int i=0; i<(int)groupMemberIndex.size(); i++) {
+		status &= convertPart(outdata, mds, groupMemberIndex[i], i, (int)groupMemberIndex.size());
+	}
 
-		if (!query[i].anypitch) {
-			double qpitch = query[i].pc;
-			double npitch = 0;
-			if (notes[currindex]->isRest()) {
-				if (Convert::isNaN(qpitch)) {
-					// both notes are rests, so they match
-					match.push_back(notes[currindex]);
-					continue;
+	HumdrumFile outfile;
+	outdata.transferTokens(outfile);
+	outfile.generateLinesFromTokens();
+	stringstream sss;
+	sss << outfile;
+	outfile.readString(sss.str());
+
+	if (needsAboveBelowKernRdf()) {
+		outfile.appendLine("!!!RDF**kern: > = above");
+		outfile.appendLine("!!!RDF**kern: < = above");
+	}
+
+	outfile.createLinesFromTokens();
+
+	Tool_trillspell trillspell;
+	trillspell.run(outfile);
+
+	// Convert comments in header of first part:
+	int ii = groupMemberIndex[0];
+	bool ending = false;
+	HumRegex hre;
+	for (int i=0; i< mds[ii].getLineCount(); i++) {
+		if (mds[ii][i].isAnyNote()) {
+			break;
+		}
+		if (mds[ii].getLine(i).compare(0, 2, "@@") == 0) {
+			string output = mds[ii].getLine(i);
+			if (output == "@@@") {
+				ending = true;
+				continue;
+			}
+			for (int j=0; j<(int)output.size(); j++) {
+				if (output[j] == '@') {
+					output[j] = '!';
 				} else {
-					// query is not a rest but test note is
-					match.clear();
-					return false;
+					break;
 				}
-			} else if (Convert::isNaN(qpitch)) {
-				// query is a rest but test note is not
-				match.clear();
-				return false;
 			}
-
-			if (query[i].base == 40) {
-				npitch = notes[currindex]->getAbsBase40PitchClass();
-			} else if (query[i].base == 12) {
-				npitch = ((int)notes[currindex]->getAbsMidiPitch()) % 12;
-			} else if (query[i].base == 7) {
-				npitch = ((int)notes[currindex]->getAbsDiatonicPitch()) % 7;
-			} else {
-				npitch = notes[currindex]->getAbsBase40PitchClass();
+			if (hre.search(output, "!!!\\s*([^!:]+)\\s*:")) {
+				string key = hre.getMatch(1);
+				m_usedReferences[key] = true;
 			}
-
-			if (qpitch != npitch) {
-				match.clear();
-				return false;
+			if (ending) {
+           m_postReferences.push_back(output);
+			} else {
+				out << output << endl;
 			}
 		}
+	}
 
-		if (!query[i].harmonic.empty()) {
-			query[i].parseHarmonicQuery();
-			bool status = doHarmonicPitchSearch(query[i], notes[currindex]->getToken());
-			if (!status) {
-				return false;
-			}
+	if (!m_usedReferences["COM"]) {
+		string composer = mds[ii].getComposer();
+		if (!composer.empty()) {
+				out << "!!!COM: " << composer << endl;
 		}
-
-		// All requirements for the note were matched, so store note
-		// and continue to next note if needed.
-		match.push_back(notes[currindex]);
 	}
 
-	// Add extra token for marking tied notes at end of match
-	if (index + (int)query.size() < (int)notes.size()) {
-		match.push_back(notes[index + (int)query.size() - c]);
-	} else {
-		match.push_back(NULL);
+	if (!m_usedReferences["CDT"]) {
+		string cdate = mds[ii].getComposerDate();
+		if (!cdate.empty()) {
+			out << "!!!CDT: " << cdate << endl;
+		}
 	}
 
-	return true;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_msearch::doHarmonicPitchSearch --
-//
-
-bool Tool_msearch::doHarmonicPitchSearch(MSearchQueryToken& query, HTp token) {
-	if (query.harmonic.empty()) {
-		return true;
+	if (!m_usedReferences["OTL"]) {
+		string worktitle = mds[ii].getWorkTitle();
+		if (!worktitle.empty()) {
+			out << "!!!OTL: " << worktitle << endl;
+		}
 	}
 
-	int lindex = token->getLineIndex();
-	if (m_verticalOnlyQ && m_sonoritiesChecked[lindex]) {
-		// Only count once if searching only for vertical sonoroties
-		// Later make this more efficient perhaps by not searching every
-		// note for vertical-only searches, but rather search
-		// the sonorities in one pass (but maybe this will not actually
-		// be more efficient).
-		return false;
-	}
-	m_sonoritiesChecked[lindex] = true;
-	SonorityDatabase& sonorities = m_sonorities[lindex];
-	if (sonorities.isEmpty()) {
-		sonorities.buildDatabase(token->getLine());
+	if (!m_noOmvQ) {
+		if (!m_usedReferences["OMV"]) {
+			string movementtitle = mds[ii].getMovementTitle();
+			if (!movementtitle.empty()) {
+				out << "!!!OMV: " << movementtitle << endl;
+			}
+		}
 	}
 
-	bool exactQ = false;
-	bool onlyQ = false;
-
-	if (query.harmonic.find("==") != string::npos) {
-		exactQ = true;
-	} else if (query.harmonic.find("=") != string::npos) {
-		onlyQ = true;
+	if (!m_usedReferences["OPS"]) {
+		string opus = mds[ii].getOpus();
+		if (!opus.empty()) {
+			out << "!!!OPS: " << opus << endl;
+		}
 	}
 
-	vector<int> diatonicCountsQuery(7, 0);
-	vector<int> diatonicCountsMatch(7, 0);
-	vector<int> diatonicCountsData(7, 0);
-	vector<int> chromaticCountsQuery(40, 0);
-	vector<int> chromaticCountsMatch(40, 0);
-	vector<int> chromaticCountsData(40, 0);
+	if (!m_usedReferences["ONM"]) {
+		string number = mds[ii].getNumber();
+		if (!number.empty()) {
+			out << "!!!ONM: " << number << endl;
+		}
+	}
 
-	for (int i=0; i<sonorities.getNoteCount(); i++) {
-		diatonicCountsData.at(sonorities[i].getBase7Pc())++;
-		chromaticCountsData.at(sonorities[i].getBase40Pc())++;
+	if (!m_usedReferences["OMD"]) {
+		if (!m_omd.empty()) {
+			out << "!!!OMD: " << m_omd << endl;
+		}
 	}
 
-	int sum = 0;
-	for(int i=0; i<(int)query.hquery.size(); i++) {
-		if (query.hquery[i].hasAccidental()) {
-			diatonicCountsQuery.at(query.hquery[i].getBase7Pc())++;
-			if (query.hquery[i].hasUpperCase()) {
-				if (query.hquery[i].getBase7Pc() != sonorities.getLowest().getBase7Pc()) {
-					return false;
+	bool foundDataQ = false;
+	for (int i=0; i<outfile.getLineCount(); i++) {
+		if (outfile[i].isData()) {
+			foundDataQ = true;
+		}
+		if (outfile[i].isBarline() && !foundDataQ) {
+			HTp token = outfile.token(i, 0);
+			if (*token == "=") {
+				HTp nextBar = NULL;
+				for (int j=i+1; j<outfile.getLineCount(); j++) {
+					if (outfile[j].isBarline()) {
+						nextBar = outfile.token(j, 0);
+						break;
+					}
 				}
-			}
-
-			// Don't check for same pitch-class twice:
-			if (chromaticCountsMatch.at(query.hquery[i].getBase40Pc())) {
-				continue;
-			}
-		} else {
-			diatonicCountsQuery.at(query.hquery[i].getBase7Pc())++;
-			if (query.hquery[i].hasUpperCase()) {
-				if (query.hquery[i].getBase7Pc() != sonorities.getLowest().getBase7Pc()) {
-					return false;
+				if (nextBar) {
+					HumRegex hre;
+					if (hre.search(nextBar, "\\b1\\b")) {
+						continue;
+					} else if (hre.search(nextBar, "\\b2\\b")) {
+						for (int j=0; j<outfile[i].getFieldCount(); j++) {
+							out << "=1";
+							if (j < outfile[i].getFieldCount() - 1) {
+								out << "\t";
+							}
+						}
+						out << endl;
+						continue;
+					}
+					// also deal with repeat barlines at the start of the music.
 				}
 			}
-
-			// Don't check for same pitch-class twice:
-			if (diatonicCountsMatch.at(query.hquery[i].getBase7Pc())) {
-				continue;
-			}
 		}
+		printLine(out, outfile[i]);
+	}
 
-		int status = checkHarmonicPitchMatch(query.hquery[i], sonorities, false);
-
-		if (!status) {
-			return false;
+	if (!m_usedReferences["SMS"]) {
+		string source = mds[ii].getSource();
+		if (!source.empty()) {
+			out << "!!!SMS: " << source << endl;
 		}
+	}
 
-		if (query.hquery[i].hasAccidental()) {
-			chromaticCountsMatch.at(query.hquery[i].getBase40Pc()) += status;
-		} else {
-			diatonicCountsMatch.at(query.hquery[i].getBase7Pc()) += status;
+	if (!m_usedReferences["ENC"]) {
+		string encoder = mds[ii].getEncoderName();
+		if (!encoder.empty()) {
+			out << "!!!ENC: " << encoder << endl;
 		}
-		sum += status;
+	}
 
+	if (!m_usedReferences["END"]) {
+		string edate = mds[ii].getEncoderDate();
+		if (!edate.empty()) {
+			out << "!!!END: " << edate << endl;
+		}
 	}
 
-	if ((!exactQ) && (!onlyQ)) {
-		return true;
+	for (int i=0; i<(int)m_postReferences.size(); i++) {
+		out << m_postReferences[i] << endl;
 	}
+	m_postReferences.clear();
 
+	stringstream ss;
+	auto nowtime = std::chrono::system_clock::now();
+	time_t currenttime = std::chrono::system_clock::to_time_t(nowtime);
+	ss << std::ctime(&currenttime);
+	out << "!!!ONB: Converted from MuseData with musedata2hum on " << ss.str();
 
-	if (exactQ && (sum != sonorities.getNoteCount())) {
-		return false;
+	string copyright = mds[ii].getCopyright();
+	if (!copyright.empty()) {
+		out << "!!!YEM: " << copyright << endl;
 	}
 
-	if (exactQ) {
-		for (int i=0; i<(int)diatonicCountsMatch.size(); i++) {
-			if (diatonicCountsMatch[i] != diatonicCountsQuery[i]) {
-				return false;
-			}
+	// Convert comments in footer of last part:
+	int lastone = groupMemberIndex.back();
+	vector<string> outputs;
+	for (int i=mds[lastone].getLineCount() - 1; i>=0; i--) {
+		if (mds[lastone][i].isAnyNote()) {
+			break;
 		}
-		for (int i=0; i<(int)chromaticCountsMatch.size(); i++) {
-			if (chromaticCountsMatch[i] != chromaticCountsQuery[i]) {
-				return false;
+		if (mds[lastone].getLine(i).compare(0, 2, "@@") == 0) {
+			string output = mds[lastone].getLine(i);
+			for (int j=0; j<(int)output.size(); j++) {
+				if (output[j] == '@') {
+					output[j] = '!';
+				} else {
+					break;
+				}
 			}
+			outputs.push_back(output);
 		}
-	} else if (onlyQ) {
-		SonorityDatabase son2;
-		for (int i=0; i<(int)query.hpieces.size(); i++) {
-			son2.addNote(query.hpieces[i]);
-		}
+	}
 
-		for (int k=0; k<sonorities.getNoteCount(); k++) {
-			int status2 = checkHarmonicPitchMatch(sonorities[k], son2, true);
-			if (!status2) {
-				return false;
-			}
-		}
+	for (int i=(int)outputs.size() - 1; i>=0; i--) {
+		out << outputs[i] << endl;
 	}
 
-	return true;
+	return status;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::checkHarmonicPitchMatch -- Returns the number of matched notes.
+// Tool_musedata2hum::printLine -- Print line of Humdrum file
+//     contents.  If there is any layout parameter in the line tokens,
+//     then print an extra line with these.  Currently only checking for
+//     a single parameter.
 //
 
-int Tool_msearch::checkHarmonicPitchMatch(SonorityNoteData& query,
-		SonorityDatabase& sonorities, bool suppressQ) {
-	bool isChromatic = query.hasAccidental();
-	bool isLowest = query.hasUpperCase();
-
-	if (isLowest) {
-		if (isChromatic) {
-			int cpc = query.getBase40Pc();
-			if (cpc != sonorities.getLowest().getBase40Pc()) {
-				return 0;
-			}
-		} else {
-			int dpc = query.getBase7Pc();
-			if (dpc != sonorities.getLowest().getBase7Pc()) {
-				return 0;
-			}
+void Tool_musedata2hum::printLine(ostream& out, HumdrumLine& line) {
+	vector<string> lo(line.getFieldCount());
+	int count = 0;
+	for (int i=0; i<line.getFieldCount(); i++) {
+		HTp token = line.token(i);
+		string value = token->getValue("auto", "LO");
+		if (!value.empty()) {
+			lo.at(i) = value;
+			count++;
 		}
 	}
-
-	pair<HTp, int> tomark;
-
-	// this algorithm highlights all vertical sonorities of given pitch class.
-	int output = 0;
-	if (isChromatic) {
-		int cpitch = query.getBase40Pc();
-		int cpc = cpitch % 40;
-		for (int i=0; i<sonorities.getCount(); i++) {
-			if (cpc == sonorities[i].getBase40Pc()) {
-				if (!suppressQ) {
-					tomark.first = sonorities[i].getToken();
-					tomark.second = sonorities[i].getIndex();
-					m_tomark.push_back(tomark);
-				}
-				output += 1;
+	if (count > 0) {
+		for (int i=0; i<(int)lo.size(); i++) {
+			if (lo[i].empty()) {
+				out << "!";
+			} else {
+				out << lo[i];
 			}
-		}
-	} else {
-		int dpitch = query.getBase7Pc();
-		int dpc = dpitch % 7;
-		for (int i=0; i<sonorities.getCount(); i++) {
-			if (dpc == sonorities[i].getBase7Pc()) {
-				if (!suppressQ) {
-					tomark.first = sonorities[i].getToken();
-					tomark.second = sonorities[i].getIndex();
-					m_tomark.push_back(tomark);
-				}
-				output += 1;
+			if (i < (int)lo.size() - 1) {
+				out << "\t";
 			}
 		}
+		out << endl;
 	}
-
-	return output;
+	out << line << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::fillTextQuery --
+// Tool_musedata2hum::convertPart --
 //
 
-void Tool_msearch::fillTextQuery(vector<MSearchTextQuery>& query,
-		const string& input) {
-	query.clear();
-	bool inquote = false;
-
-	query.resize(1);
+bool Tool_musedata2hum::convertPart(HumGrid& outdata, MuseDataSet& mds, int index, int partindex, int maxstaff) {
+	MuseData& part = mds[index];
+	m_lastfigure = NULL;
+	m_lastnote = NULL;
+	m_lastbarnum = -1;
+	m_part = partindex;
+	// maybe maxpart?
+	m_maxstaff = maxstaff;
 
-	for (int i=0; i<(int)input.size(); i++) {
-		if (input[i] == '"') {
-			inquote = !inquote;
-			query.resize(query.size() + 1);
-			continue;
-		}
-		if (isspace(input[i])) {
-			query.resize(query.size() + 1);
-		}
-		query.back().word.push_back(input[i]);
-		if (inquote) {
-			query.back().link = true;
-		}
+	bool status = true;
+	int i = 0;
+	while (i < part.getLineCount()) {
+		m_measureLineIndex = i;
+		i = convertMeasure(outdata, part, partindex, i);
 	}
+
+	storePartName(outdata, part, partindex);
+
+	return status;
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_msearch::fillMusicQuery --
+// Tool_musedata2hum::storePartName --
 //
 
-void Tool_msearch::fillMusicQuery(vector<MSearchQueryToken>& query) {
-	query.clear();
-
-	string qinput;
-	string pinput;
-	string iinput;
-	string rinput;
-
-	if (getBoolean("query")) {
-		qinput = getString("query");
+void Tool_musedata2hum::storePartName(HumGrid& outdata, MuseData& part, int index) {
+	string name = part.getPartName();
+	if (!name.empty()) {
+		outdata.setPartName(index, name);
 	}
+}
 
-	if (getBoolean("pitch")) {
-		pinput = getString("pitch");
-		m_verticalOnlyQ = checkVerticalOnly(pinput);
-	}
 
-	if (getBoolean("interval")) {
-		iinput = getString("interval");
-	}
 
-	if (getBoolean("rhythm")) {
-		rinput = getString("rhythm");
-	}
+//////////////////////////////
+//
+// Tool_musedata2hum::convertMeasure --
+//
 
-	if (!rinput.empty()) {
-		fillMusicQueryRhythm(query, rinput);
+int Tool_musedata2hum::convertMeasure(HumGrid& outdata, MuseData& part, int partindex, int startindex) {
+	if (part.getLineCount() == 0) {
+		return 1;
 	}
-
-	if (!qinput.empty()) {
-		fillMusicQueryInterleaved(query, qinput);
+	HumNum starttime = part[startindex].getAbsBeat();
+	HumNum filedur = part.getFileDuration();
+	HumNum diff = filedur - starttime;
+	if (diff == 0) {
+		// last barline in score, so ignore
+		return startindex + 1;;
 	}
 
-	if (!pinput.empty()) {
-		fillMusicQueryPitch(query, pinput);
+	GridMeasure* gm = getMeasure(outdata, starttime);
+	int i = startindex;
+	for (i=startindex; i<part.getLineCount(); i++) {
+		if ((i != startindex) && part[i].isBarline()) {
+			break;
+		}
+		convertLine(gm, part[i]);
 	}
-
-	if (!iinput.empty()) {
-		fillMusicQueryInterval(query, iinput);
+	HumNum endtime = starttime;
+	if (i >= part.getLineCount()) {
+		endtime = part[i-1].getAbsBeat();
+	} else {
+		endtime = part[i].getAbsBeat();
 	}
 
-	if (query.size() == 1) {
-		if (query[0].anything) {
-			query.clear();
+	// set duration of measures (so it will be printed in conversion to Humdrum):
+	gm->setDuration(endtime - starttime);
+	gm->setTimestamp(starttime);
+	gm->setTimeSigDur(m_timesigdur);
+
+	if ((i < part.getLineCount()) && part[i].isBarline()) {
+		if (partindex == 0) {
+			// For now setting the barline style from the
+			// lowest staff.  This is mostly because
+			// MEI/verovio can handle only one style
+			// on a system barline.  But also because
+			// GridMeasure objects only has a setting
+			// for a single barline style.
+			setMeasureStyle(outdata.back(), part[i]);
+			setMeasureNumber(outdata.back(), part[i]);
+			// gm->setBarStyle(MeasureStyle::Plain);
 		}
 	}
 
+	return i;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::fillMusicQueryPitch --
+// Tool_musedata2hum::setMeasureNumber --
 //
 
-void Tool_msearch::fillMusicQueryPitch(vector<MSearchQueryToken>& query,
-		const string& input) {
-	fillMusicQueryInterleaved(query, input);
+void Tool_musedata2hum::setMeasureNumber(GridMeasure* gm, MuseRecord& mr) {
+	int pos = -1;
+	string line = mr.getLine();
+	bool space = false;
+	for (int i=0; i<(int)line.size(); i++) {
+		if (isspace(line[i])) {
+			space = true;
+			continue;
+		}
+		if (!space) {
+			continue;
+		}
+		if (isdigit(line[i])) {
+			pos = i;
+			break;
+		}
+	}
+	if (pos < 0) {
+		gm->setMeasureNumber(-1);
+		return;
+	}
+	int num = stoi(line.substr(pos));
+	if (m_lastbarnum >= 0) {
+		int temp = num;
+		num = m_lastbarnum;
+		m_lastbarnum = temp;
+	}
+	gm->setMeasureNumber(num);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::fillMusicQueryRhythm --
+// Tool_musedata2hum::setMeasureStyle --
 //
 
-void Tool_msearch::fillMusicQueryRhythm(vector<MSearchQueryToken>& query,
-		const string& input) {
-	string output;
-	output.reserve(input.size() * 4);
-
-	for (int i=0; i<(int)input.size(); i++) {
-		output += input[i];
-		output += ' ';
-	}
-
-	// remove spaces to allow rhythms:
-	// 64 => 64
-   // 32 => 32
-	// 16 => 16
-	for (int i=0; i<(int)output.size(); i++) {
-		if ((i > 1) && (output[i] == '6') && (output[i-1] == ' ') && (output[i-2] == '1')) {
-			output.erase(i-1, 1);
-			i--;
-		}
-		if ((i > 1) && (output[i] == '2') && (output[i-1] == ' ') && (output[i-2] == '3')) {
-			output.erase(i-1, 1);
-			i--;
+void Tool_musedata2hum::setMeasureStyle(GridMeasure* gm, MuseRecord& mr) {
+	string line = mr.getLine();
+	string barstyle = mr.getMeasureFlags();
+	if (line.compare(0, 7, "mheavy2") == 0) {
+		if (barstyle.find(":|") != string::npos) {
+			gm->setStyle(MeasureStyle::RepeatBackward);
+		} else {
+			gm->setStyle(MeasureStyle::Final);
 		}
-		if ((i > 1) && (output[i] == '4') && (output[i-1] == ' ') && (output[i-2] == '6')) {
-			output.erase(i-1, 1);
-			i--;
+	} else if (line.compare(0, 7, "mheavy3") == 0) {
+		if (barstyle.find("|:") != string::npos) {
+			gm->setStyle(MeasureStyle::RepeatForward);
 		}
-      if ((i > 0) && (output[i] == '.')) {
-			output.erase(i-1, 1);
-			i--;
+	} else if (line.compare(0, 7, "mheavy4") == 0) {
+		if (barstyle.find(":|:") != string::npos) {
+			gm->setStyle(MeasureStyle::RepeatBoth);
+		} else if (barstyle.find("|: :|") != string::npos) {
+			// Vivaldi op. 1, no. 1, mvmt. 1, m. 10: mheavy4          |: :|
+			gm->setStyle(MeasureStyle::RepeatBoth);
 		}
+	} else if (line.compare(0, 7, "mdouble") == 0) {
+		gm->setStyle(MeasureStyle::Double);
 	}
-
-	fillMusicQueryInterleaved(query, output, true);
-
 }
 
 
-
 //////////////////////////////
 //
-// Tool_msearch::convertPitchesToIntervals --
+// Tool_musedata2hum::convertLine --
 //
 
-string Tool_msearch::convertPitchesToIntervals(const string& input) {
-	if (input.empty()) {
-		return "";
+void Tool_musedata2hum::convertLine(GridMeasure* gm, MuseRecord& mr) {
+	int part         = m_part;
+	int staff        = 0;
+	int maxstaff     = m_maxstaff;
+	int layer        = mr.getLayer();
+	if (layer > 0) {
+		// convert to an index:
+		layer = layer - 1;
 	}
-	for (int i=0; i<(int)input.size(); i++) {
-		if (isdigit(input[i])) {
-			return input;
-		}
-		if (tolower(input[i] == 'r')) {
-			// not allowing rests for now
-			return input;
-		}
+
+	if (mr.isAnyNoteOrRest()) {
+		m_figureOffset = 0;
 	}
-	vector<string> pitches;
 
-	for (int i=0; i<(int)input.size(); i++) {
-		char ch = tolower(input[i]);
-		if (ch >= 'a' && ch <= 'g') {
-			string val;
-			val += ch;
-			pitches.push_back(val);
-			if (i > 0) {
-				if (input[i-1] == '^') {
-					pitches.back().insert(0, "^");
-				}
-				if (input[i-1] == 'v') {
-					pitches.back().insert(0, "v");
-				}
-			}
-			continue;
-		}
-		if (!pitches.empty()) {
-			if (ch == 'n') {
-				pitches.back() += 'n';
-			} else if (ch == '-') {
-				pitches.back() += '-';
-			} else if (ch == '#') {
-				pitches.back() += '#';
+	if (mr.isDirection()) {
+		return;
+	}
+
+	HumNum timestamp = mr.getAbsBeat();
+	// cerr << "CONVERTING LINE " << timestamp << "\t" << mr << endl;
+	string tok;
+	GridSlice* slice = NULL;
+
+	if (mr.isBarline()) {
+		// barline handled elsewhere
+		// tok = mr.getKernMeasure();
+	} else if (mr.isAttributes()) {
+		map<string, string> attributes;
+		mr.getAttributeMap(attributes);
+
+		string mtempo = cleanString(attributes["D"]);
+		if (!mtempo.empty()) {
+			if (timestamp != 0) {
+				string value = "!!!OMD: " + mtempo;
+				gm->addGlobalComment(value, timestamp);
+			} else {
+				setInitialOmd(mtempo);
 			}
 		}
-	}
 
-	if (pitches.size() <= 1) {
-		return "";
-	}
+		if (!attributes["Q"].empty()) {
+			m_quarterDivisions = std::stoi(attributes["Q"]);
+		}
 
-	vector<bool> chromatic(pitches.size(), false);
-	for (int i=0; i<(int)pitches.size(); i++) {
-		for (int j=(int)pitches[i].size()-1; j>0; j--) {
-			int ch = pitches[i][j];
-			if ((ch == 'n') || (ch == '-') || (ch == '#')) {
-				chromatic[i] = true;
-				break;
+		string mclef = attributes["C"];
+		if (!mclef.empty()) {
+			string kclef = Convert::museClefToKernClef(mclef);
+			if (!kclef.empty()) {
+				gm->addClefToken(kclef, timestamp, part, staff, layer, maxstaff);
 			}
 		}
-	}
 
-	string output;
-	int p1;
-	int p2;
-	int base40;
-	int base7;
-	int sign;
-	for (int i=0; i<(int)pitches.size() - 1; i++) {
-		if (chromatic[i] && chromatic[i+1]) {
-			p1 = Convert::kernToBase40(pitches[i]);
-			p2 = Convert::kernToBase40(pitches[i+1]);
-			base40 = p2 - p1;
-			sign = base40 < 0 ? -1 : +1;
-			if (sign < 0) {
-				base40 = -base40;
+		string mkeysig = attributes["K"];
+		if (!mkeysig.empty()) {
+			string kkeysig = Convert::museKeySigToKernKeySig(mkeysig);
+			gm->addKeySigToken(kkeysig, timestamp, part, staff, layer, maxstaff);
+		}
+
+		string mtimesig = attributes["T"];
+		if (!mtimesig.empty()) {
+			string ktimesig = Convert::museTimeSigToKernTimeSig(mtimesig);
+			slice = gm->addTimeSigToken(ktimesig, timestamp, part, staff, layer, maxstaff);
+			setTimeSigDurInfo(ktimesig);
+			string kmeter = Convert::museMeterSigToKernMeterSig(mtimesig);
+			if (!kmeter.empty()) {
+				slice = gm->addMeterSigToken(kmeter, timestamp, part, staff, layer, maxstaff);
+			}
+			if (m_tempo > 0.00) {
+				int value = (int)(m_tempo + 0.5);
+				string tempotok = "*MM" + to_string(value);
+				slice = gm->addTempoToken(tempotok, timestamp, part, staff, layer, maxstaff);
 			}
-			string value = "";
-			if (sign < 0) {
-				value += "-";
+		}
+	} else if (mr.isRegularNote()) {
+		tok = mr.getKernNoteStyle(1, 1);
+		string other = mr.getKernNoteOtherNotations();
+		if (!needsAboveBelowKernRdf()) {
+			if (other.find("<") != string::npos) {
+				addAboveBelowKernRdf();
+			} else if (other.find(">") != string::npos) {
+				addAboveBelowKernRdf();
 			}
-			value += Convert::base40ToIntervalAbbr(base40);
-			output += value;
-			output += " ";
-		} else {
-			p1 = Convert::kernToBase7(pitches[i]);
-			p2 = Convert::kernToBase7(pitches[i+1]);
-			base7 = p2 - p1;
-			sign = base7 < 0 ? -1 : +1;
-			if (sign < 0) {
-				base7 = -base7;
+		}
+		if (!other.empty()) {
+			tok += other;
+		}
+		slice = gm->addDataToken(tok, timestamp, part, staff, layer, maxstaff);
+		if (slice) {
+			mr.setVoice(slice->at(part)->at(staff)->at(layer));
+			string gr = mr.getLayoutVis();
+			if (gr.size() > 0) {
+				// Visual and performance durations are not equal:
+				HTp token = slice->at(part)->at(staff)->at(layer)->getToken();
+				string text = "!LO:N:vis=" + gr;
+				token->setValue("auto", "LO", text);
 			}
-			string value = "";
-			if (sign < 0) {
-				value += "-";
+		}
+		m_lastnote = slice->at(part)->at(staff)->at(layer)->getToken();
+		addNoteDynamics(slice, part, mr);
+		addDirectionDynamics(slice, part, mr);
+		addLyrics(slice, part, staff, mr);
+	} else if (mr.isFiguredHarmony()) {
+		addFiguredHarmony(mr, gm, timestamp, part, maxstaff);
+	} else if (mr.isChordNote()) {
+		tok = mr.getKernNoteStyle(1, 1);
+		if (m_lastnote) {
+			string text = m_lastnote->getText();
+			text += " ";
+			text += tok;
+			m_lastnote->setText(text);
+		} else {
+			cerr << "Warning: found chord note with no regular note to attach to" << endl;
+		}
+	} else if (mr.isCueNote()) {
+		cerr << "PROCESS CUE NOTE HERE: " << mr << endl;
+	} else if (mr.isGraceNote()) {
+		cerr << "PROCESS GRACE NOTE HERE: " << mr << endl;
+	} else if (mr.isChordGraceNote()) {
+		cerr << "PROCESS GRACE CHORD NOTE HERE: " << mr << endl;
+	} else if (mr.isAnyRest()) {
+		tok  = mr.getKernRestStyle();
+		slice = gm->addDataToken(tok, timestamp, part, staff, layer, maxstaff);
+		if (slice) {
+			mr.setVoice(slice->at(part)->at(staff)->at(layer));
+			string gr = mr.getLayoutVis();
+			if (gr.size() > 0) {
+				cerr << "GRAPHIC VERSION OF NOTEB " << gr << endl;
 			}
-			value += to_string(base7 + 1);
-			output += value;
-			output += " ";
 		}
-	}
-
-	if (output.size() > 0) {
-		if (output.back() == ' ') {
-			output.resize((int)output.size() - 1);
+	} else if (mr.isDirection()) {
+		if (mr.isTextDirection()) {
+			addTextDirection(gm, part, staff, mr, timestamp);
 		}
 	}
-
-	return output;
 }
 
 
-
 //////////////////////////////
 //
-// Tool_msearch::fillMusicQueryInterval --
+// Tool_musedata2hum::addDirectionDynamics -- search for a dynamic
+//     marking before the current line and after any previous note
+//     or similar line.   These lines are store in "musical directions"
+//     which start the line with a "*" character.
+//
+// Example for "p" dyamic, with print suggesting.
+//             1         2
+//    12345678901234567890123456789
+//    *               G       p
+//    P    C17:Y57
 //
 
-void Tool_msearch::fillMusicQueryInterval(vector<MSearchQueryToken>& query,
-		const string& input) {
-
-	string newinput = convertPitchesToIntervals(input);
-
-	char ch;
-	int counter = 0;
-	MSearchQueryToken temp;
-	MSearchQueryToken *active = &temp;
-
-	if (query.size() > 0) {
-		active = &query.at(counter);
-	} else {
-		// what is this for?
+void Tool_musedata2hum::addDirectionDynamics(GridSlice* slice, int part, MuseRecord& mr) {
+	MuseRecord* direction = mr.getMusicalDirection();
+	if (direction == NULL) {
+		return;
 	}
 
-	int sign = 1;
-	string alteration;
-	for (int i=0; i<(int)newinput.size(); i++) {
-		ch = newinput[i];
-		if (ch == ' ') {
-			// skip over spaces
-			continue;
-		}
-		if ((ch == 'P') ||  (ch == 'p')) {
-			alteration = "P";
-			continue;
-		}
-		if ((ch == 'd') ||  (ch == 'D')) {
-			if ((!alteration.empty()) && (alteration[0] == 'd')) {
-				alteration += "d";
-			} else {
-				alteration = "d";
-			}
-			continue;
-		}
-		if ((ch == 'A') ||  (ch == 'a')) {
-			if ((!alteration.empty()) && (alteration[0] == 'A')) {
-				alteration += "A";
-			} else {
-				alteration = "A";
+	if (direction->isDynamic()) {
+		string dynamicText = direction->getDynamicText();
+		if (!dynamicText.empty()) {
+			slice->at(part)->setDynamics(dynamicText);
+			HumGrid* grid = slice->getOwner();
+			if (grid) {
+				grid->setDynamicsPresent(part);
 			}
-			continue;
-		}
-		if ((ch == 'M') ||  (ch == 'm')) {
-			alteration = ch;
-			continue;
-		}
-		if (ch == '-') {
-			sign = -1;
-			continue;
-		}
-		if (ch == '+') {
-			sign = +1;
-			continue;
-		}
-		ch = tolower(ch);
-
-		if (!isdigit(ch)) {
-			// skip over non-digits (sign of interval
-			// will be read retroactively).
-			continue;
 		}
+	}
+}
 
-		// check for intervals.  Intervals will trigger a
-		// new element in the query list
-
-		active->anything = false;
-		active->anyinterval = false;
-		// active->direction = 1;
-
-		if (alteration.empty()) {
-			// store a diatonic interval
-			active->dinterval = (ch - '0') - 1; // zero-indexed interval
-			active->dinterval *= sign;
-		} else {
-			active->cinterval = makeBase40Interval((ch - '0') - 1, alteration);
-			active->cinterval *= sign;
-		}
-		sign = 1;
-		alteration.clear();
 
-		if (active == &temp) {
-			query.push_back(temp);
-			temp.clear();
-		}
-		counter++;
-		if ((int)query.size() > counter) {
-			active = &query.at(counter);
-		} else {
-			active = &temp;
-		}
-	}
 
-	// The last element in the interval search is set to
-	// any pitch, because the interval was already checked
-	// to the next note, and this value is needed to highlight
-	// the next note of the interval.
-	active->anything = true;
-	active->anyinterval = true;
-	if (active == &temp) {
-		query.push_back(temp);
-		temp.clear();
-	}
+//////////////////////////////
+//
+// Tool_musedata2hum::addAboveBelowKernRdf -- Save for later that
+//      !!!RDF**kern: > = above
+//      !!!RDF**kern: < = below
+//    in the output Humdrum data file.
+//
 
+void Tool_musedata2hum::addAboveBelowKernRdf(void) {
+	m_aboveBelowKernRdf = true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::makeBase40Interval --
+// Tool_musedata2hum::needsAboveBelowKernRdf -- Function name says it all.
 //
 
-int Tool_msearch::makeBase40Interval(int diatonic, const string& alteration) {
-	int sign = 1;
-	if (diatonic < 0) {
-		sign = -1;
-		diatonic = -diatonic;
-	}
-	bool perfectQ = false;
-	int base40 = 0;
-	switch (diatonic) {
-		case 0:  // unison
-			base40 = 0;
-			perfectQ = true;
-			break;
-		case 1:  // second
-			base40 = 6;
-			perfectQ = false;
-			break;
-		case 2:  // third
-			base40 = 12;
-			perfectQ = false;
-			break;
-		case 3:  // fourth
-			base40 = 17;
-			perfectQ = true;
-			break;
-		case 4:  // fifth
-			base40 = 23;
-			perfectQ = true;
-			break;
-		case 5:  // sixth
-			base40 = 29;
-			perfectQ = false;
-			break;
-		case 6:  // seventh
-			base40 = 35;
-			perfectQ = false;
-			break;
-		case 7:  // octave
-			base40 = 40;
-			perfectQ = true;
-			break;
-		case 8:  // ninth
-			base40 = 46;
-			perfectQ = false;
-			break;
-		case 9:  // tenth
-			base40 = 52;
-			perfectQ = false;
-			break;
-		default:
-			cerr << "cannot handle this interval yet.  Setting to unison" << endl;
-			base40 = 0;
-			perfectQ = 1;
-	}
-
-	if (perfectQ) {
-		if (alteration == "P") {
-			// do nothing since the interval is already perfect
-		} else if ((!alteration.empty()) && (alteration[0] == 'd')) {
-			if (alteration.size() <= 2) {
-				base40 -= (int)alteration.size();
-			} else {
-				cerr << "TOO MUCH DIMINISHED, IGNORING" << endl;
-			}
-		} else if ((!alteration.empty()) && (alteration[0] == 'A')) {
-			if (alteration.size() <= 2) {
-				base40 += (int)alteration.size();
-			} else {
-				cerr << "TOO MUCH AUGMENTED, IGNORING" << endl;
-			}
-		}
-	} else {
-		if (alteration == "M") {
-			// do nothing since the interval is already major
-		} else if (alteration == "m") {
-			base40--;
-		} else if ((!alteration.empty()) && (alteration[0] == 'd')) {
-			if (alteration.size() <= 2) {
-				base40 -= (int)alteration.size() + 1;
-			} else {
-				cerr << "TOO MUCH DIMINISHED, IGNORING" << endl;
-			}
-		} else if ((!alteration.empty()) && (alteration[0] == 'A')) {
-			if (alteration.size() <= 2) {
-				base40 += (int)alteration.size();
-			} else {
-				cerr << "TOO MUCH AUGMENTED, IGNORING" << endl;
-			}
-		}
-	}
-	base40 *= sign;
-	return base40;
+bool Tool_musedata2hum::needsAboveBelowKernRdf(void) {
+	return m_aboveBelowKernRdf;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::fillMusicQueryInterleaved --
+// Tool_musedata2hum::addTextDirection --
 //
 
-void Tool_msearch::fillMusicQueryInterleaved(vector<MSearchQueryToken>& query,
-		const string& input, bool rhythmQ) {
-
-	string newinput = input;
-	char ch;
-	int counter = 0;
-	MSearchQueryToken temp;
-	MSearchQueryToken *active = &temp;
-	string paren;
+void Tool_musedata2hum::addTextDirection(GridMeasure* gm, int part, int staff,
+		MuseRecord& mr, HumNum timestamp) {
 
-	if (query.size() > 0) {
-		active = &query.at(counter);
-	} else {
-		// what is this for?
+	if (!mr.isTextDirection()) {
+		return;
+	}
+	string text = mr.getTextDirection();
+	if (text == "") {
+		// no text direction to process
+		return;
 	}
+	HumRegex hre;
+	hre.replaceDestructive(text, "&colon;", ":", "g");
+	string output = "!LO:TX";
+	output += ":b";   // text below (figure out above cases)
+	output += ":t=";
+	output += text;
 
-	for (int i=0; i<(int)newinput.size(); i++) {
-		paren.clear();
-		ch = tolower(newinput[i]);
-		if (ch == '(') {
-			paren += ch;
-			newinput[i] = ' ';
-			// A harmonic search initiated
-			int j = i;
-			bool keepQ = true;
-			bool diatonicQ = false;
-			for (j=i+1; j<(int)newinput.size(); j++) {
-				char ch2 = tolower(newinput[j]);
-				if (ch2 == ')') {
-					paren += ch2;
-					newinput[j] = ' ';
-					break;
-				}
-				if (ch2 >= 'a' && ch2 <= 'g') {
-					if (diatonicQ) {
-						keepQ = false;
-					} else {
-						diatonicQ = true;
-					}
-				}
-				if (keepQ) {
-					paren += newinput[j];
-					continue;
-				} else {
-					paren += newinput[j];
-					newinput[j] = ' ';
-				}
-			}
-			if (!paren.empty()) {
-				active->harmonic = paren;
-				paren.clear();
-			}
-			continue;
-		}
+	// add staff index later
+	gm->addLayoutParameter(NULL, part, output);
 
-		if (ch == '=') {
-			continue;
-		}
-		if (ch == ' ') {
-			// skip over multiple spaces
-			if (i > 0) {
-            if (newinput[i-1] == ' ') {
-					continue;
-				}
-			}
-		}
 
-		if (ch == '^') {
-			active->anything = false;
-			active->anyinterval = false;
-			active->direction = -1;
-			continue;
-		}
-		if (ch == 'v') {
-			active->anything = false;
-			active->anyinterval = false;
-			active->direction = 1;
-			continue;
-		}
+}
 
-		// process rhythm.  This must go first then intervals then pitches
-		if (isdigit(ch) || (ch == '.')) {
-			active->anything = false;
-			active->anyrhythm = false;
-			active->rhythm += ch;
-			if (i < (int)newinput.size() - 1) {
-				if (newinput[i+1] == ' ') {
-					if (active == &temp) {
-						query.push_back(temp);
-						temp.clear();
-					}
-					counter++;
-					if ((int)query.size() > counter) {
-						active = &query.at(counter);
-					} else {
-						active = &temp;
-					}
-					continue;
-				}
-			} else {
-				// this is the last charcter in the input string
-				if (active == &temp) {
-						query.push_back(temp);
-						temp.clear();
-				}
-				counter++;
-				if ((int)query.size() > counter) {
-					active = &query.at(counter);
-				} else {
-					active = &temp;
-				}
-			}
+
+//////////////////////////////
+//
+// Tool_musedata2hum::addFiguredHarmony --
+//
+
+void Tool_musedata2hum::addFiguredHarmony(MuseRecord& mr, GridMeasure* gm,
+		HumNum timestamp, int part, int maxstaff) {
+	string fh = mr.getFigureString();
+	int figureDuration = mr.getFigureDuration();
+	fh = Convert::museFiguredBassToKernFiguredBass(fh);
+	if (m_figureOffset > 0) {
+		if (m_quarterDivisions > 0) {
+			HumNum offset(m_figureOffset, m_quarterDivisions);
+			timestamp + offset;
 		}
+	}
+	if (fh.find(":") == string::npos) {
+		HTp fhtok = new HumdrumToken(fh);
+		m_lastfigure = fhtok;
+		gm->addFiguredBass(fhtok, timestamp, part, maxstaff);
+		m_figureOffset += figureDuration;
+		return;
+	}
 
-		// check for intervals.  Intervals will trigger a
-		// new element in the query list
-		// A new type ^ or v will not increment the query list
-		// (and they will expect a pitch after them).
-		if (ch == '/') {
-			active->anything = false;
-			active->anyinterval = false;
-			active->direction = 1;
-			if (active == &temp) {
-				query.push_back(temp);
-				temp.clear();
-			}
-			counter++;
-			if ((int)query.size() > counter) {
-				active = &query.at(counter);
-			} else {
-				active = &temp;
-			}
-			continue;
-		} else if (ch == '\\') {
-			active->anything = false;
-			active->anyinterval = false;
-			active->direction = -1;
-			if (active == &temp) {
-				query.push_back(temp);
-				temp.clear();
-			}
-			counter++;
-			if ((int)query.size() > counter) {
-				active = &query.at(counter);
-			} else {
-				active = &temp;
-			}
-			continue;
-		} else if (ch == '=') {
-			active->anything = false;
-			active->anyinterval = false;
-			active->direction = 0;
-			if (active == &temp) {
-				query.push_back(temp);
-				temp.clear();
+	if (!m_lastfigure) {
+		HTp fhtok = new HumdrumToken(fh);
+		m_lastfigure = fhtok;
+		gm->addFiguredBass(fhtok, timestamp, part, maxstaff);
+		m_figureOffset += figureDuration;
+		return;
+	}
+
+	// For now assuming only one line extension needs to be transferred.
+
+	// Has a line extension that should be moved to the previous token:
+	int position = 0;
+	int colpos = -1;
+	if (fh[0] == ':') {
+		colpos = 0;
+	} else {
+		for (int i=1; i<(int)fh.size(); i++) {
+			if (isspace(fh[i]) && !isspace(fh[i-1])) {
+				position++;
 			}
-			counter++;
-			if ((int)query.size() > counter) {
-				active = &query.at(counter);
-			} else {
-				active = &temp;
+			if (fh[i] == ':') {
+				colpos = i;
+				break;
 			}
-			continue;
 		}
+	}
 
-		// check for actual pitches
-		if ((ch >= 'a' && ch <= 'g')) {
-			active->anything = false;
-			active->anypitch = false;
-			active->base = 7;
-			active->pc = (ch - 'a' + 5) % 7;
-			if (active == &temp) {
-				query.push_back(temp);
-				temp.clear();
-			}
-			counter++;
-			if ((int)query.size() > counter) {
-				active = &query.at(counter);
+	string lastfh = m_lastfigure->getText();
+	vector<string> pieces;
+	int state = 0;
+	for (int i=0; i<(int)lastfh.size(); i++) {
+		if (state) {
+			if (isspace(lastfh[i])) {
+				state = 0;
 			} else {
-				active = &temp;
-			}
-			continue;
-		} else if (ch == 'r') {
-			active->anything = false;
-			active->anypitch = false;
-			active->base = 7;
-			active->pc = GRIDREST;
-			if (active == &temp) {
-				query.push_back(temp);
-				temp.clear();
+				pieces.back() += lastfh[i];
 			}
-			counter++;
-			if ((int)query.size() > counter) {
-				active = &query.at(counter);
+		} else {
+			if (isspace(lastfh[i])) {
+				// do nothing
 			} else {
-				active = &temp;
+				pieces.resize(pieces.size()+1);
+				pieces.back() += lastfh[i];
+				state = 1;
 			}
-			continue;
 		}
+	}
 
-		// accidentals:
-		if ((!query.empty()) && (ch == 'n') && (!Convert::isNaN(query.back().pc))) {
-			query.back().base = 40;
-			query.back().pc = Convert::base7ToBase40((int)query.back().pc + 70) % 40;
-		} else if ((!query.empty()) && (ch == '#') && (!Convert::isNaN(query.back().pc))) {
-			query.back().base = 40;
-			query.back().pc = (Convert::base7ToBase40((int)query.back().pc + 70) + 1) % 40;
-		} else if ((!query.empty()) && (ch == '-') && (!Convert::isNaN(query.back().pc))) {
-			query.back().base = 40;
-			query.back().pc = (Convert::base7ToBase40((int)query.back().pc + 70) - 1) % 40;
-		}
-		// deal with double sharps and double flats here
+	if (pieces.empty() || (position >= (int)pieces.size())) {
+		HTp fhtok = new HumdrumToken(fh);
+		m_lastfigure = fhtok;
+		gm->addFiguredBass(fhtok, timestamp, part, maxstaff);
+		m_figureOffset += figureDuration;
+		return;
 	}
 
-	// Convert rhythms to durations
-	for (int i=0; i<(int)query.size(); i++) {
-		if (query[i].anyrhythm) {
-			continue;
-		}
-		if (query[i].rhythm.empty()) {
-			continue;
+	pieces[position] += ':';
+	string oldtok;
+	for (int i=0; i<(int)pieces.size(); i++) {
+		oldtok += pieces[i];
+		if (i<(int)pieces.size() - 1) {
+			oldtok += ' ';
 		}
-		query[i].duration = Convert::recipToDuration(query[i].rhythm);
 	}
 
-	// what is this for (end condition)?
-	//if ((!query.empty()) && (query[0].base <= 0)) {
-	//	temp.clear();
-	//	temp.anything = true;
-	//	query.insert(query.begin(), temp);
-	//}
+	m_lastfigure->setText(oldtok);
+
+	fh.erase(colpos, 1);
+	HTp newtok = new HumdrumToken(fh);
+	m_lastfigure = newtok;
+	gm->addFiguredBass(newtok, timestamp, part, maxstaff);
+	m_figureOffset += figureDuration;
 }
 
 
 
 //////////////////////////////
 //
-// checkVerticalOnly --
+// Tool_musedata2hum::addLyrics --
 //
 
-bool Tool_msearch::checkVerticalOnly(const string& input) {
-	if (input.empty()) {
-		return false;
-	}
-	if (input.size() < 2) {
-		return false;
+void Tool_musedata2hum::addLyrics(GridSlice* slice, int part, int staff, MuseRecord& mr) {
+	int versecount = mr.getVerseCount();
+	if (versecount == 0) {
+		return;
 	}
-	if (input[0] != '(') {
-		return false;
+	for (int i=0; i<versecount; i++) {
+		string verse = mr.getVerseUtf8(i);
+		slice->at(part)->at(staff)->setVerse(i, verse);
 	}
-	if (input.back() != ')') {
-		return false;
+	slice->reportVerseCount(part, staff, versecount);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_musedata2hum::addNoteDynamics -- only one contiguous dynamic allowed
+//
+
+void Tool_musedata2hum::addNoteDynamics(GridSlice* slice, int part,
+		MuseRecord& mr) {
+	string notations = mr.getAdditionalNotationsField();
+	vector<string> dynamics(1);
+	vector<int> column(1, -1);
+	int state = 0;
+	for (int i=0; i<(int)notations.size(); i++) {
+		if (state) {
+			switch (notations[i]) {
+				case 'p':
+				case 'm':
+				case 'f':
+					dynamics.back() += notations[i];
+					break;
+				default:
+					state = 0;
+					dynamics.resize(dynamics.size() + 1);
+			}
+		} else {
+			switch (notations[i]) {
+				case 'p':
+				case 'm':
+				case 'f':
+					state = 1;
+					dynamics.back() = notations[i];
+					column.back() = i;
+					break;
+			}
+		}
 	}
-	for (int i=1; i<(int)input.size()-1; i++) {
-		// Maybe allow internal () if there is nothing outside of them.
-		if (input[i] == '(') {
-			return false;
+
+	bool setdynamics = false;
+	vector<string> ps;
+	HumRegex hre;
+	for (int i=0; i<(int)dynamics.size(); i++) {
+		if (dynamics[i].empty()) {
+			continue;
 		}
-		if (input[i] == ')') {
-			return false;
+		mr.getPrintSuggestions(ps, column[i]+32);
+		if (ps.size() > 0) {
+			cerr << "\tPRINT SUGGESTION: " << ps[0] << endl;
+			// only checking the first entry (first parameter):
+			if (hre.search(ps[0], "Y(-?\\d+)")) {
+				int y = hre.getMatchInt(1);
+				cerr << "Y = " << y << endl;
+			}
+		}
+
+		slice->at(part)->setDynamics(dynamics[i]);
+		setdynamics = true;
+		break;  // only one dynamic allowed (at least for now)
+	}
+
+	if (setdynamics) {
+		HumGrid* grid = slice->getOwner();
+		if (grid) {
+			grid->setDynamicsPresent(part);
 		}
 	}
-	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_msearch::storeMatch -- Store a search result for later printing
-//    in the input file footer.
+// Tool_musedata2hum::setTimeSigDurInfo --
 //
 
-void Tool_msearch::storeMatch(vector<NoteCell*>& match) {
-	m_matches.resize(m_matches.size() + 1);
-	m_matches.back().resize(match.size());
-	for (int i=0; i<(int)match.size(); i++) {
-		m_matches.back().at(i) = match.at(i);
+void Tool_musedata2hum::setTimeSigDurInfo(const string& ktimesig) {
+	HumRegex hre;
+	if (hre.search(ktimesig, "(\\d+)/(\\d+)")) {
+		int top = hre.getMatchInt(1);
+		int bot = hre.getMatchInt(2);
+		HumNum value = 1;
+		value /= bot;
+		value *= top;
+		value.invert();
+		value *= 4;  // convert from whole notes to quarter notes
+		m_timesigdur = value;
 	}
 }
 
@@ -102730,1414 +106351,1943 @@ void Tool_msearch::storeMatch(vector<NoteCell*>& match) {
 
 //////////////////////////////
 //
-// operator<< -- print MSearchQueryToken item.
+// Tool_musedata2hum::getMeasure --  Could be imporoved by NlogN search.
 //
 
-ostream& operator<<(ostream& out, MSearchQueryToken& item) {
-	out << "ITEM: "           << endl;
-	out << "\tANYTHING:\t"    << item.anything    << endl;
-	out << "\tANYPITCH:\t"    << item.anypitch    << endl;
-	out << "\tANYINTERVAL:\t" << item.anyinterval << endl;
-	out << "\tANYRHYTHM:\t"   << item.anyrhythm   << endl;
-	out << "\tPC:\t\t"        << item.pc          << endl;
-	out << "\tBASE:\t\t"      << item.base        << endl;
-	out << "\tDIRECTION:\t"   << item.direction   << endl;
-	out << "\tDINTERVAL:\t"   << item.dinterval   << endl;
-	out << "\tCINTERVAL:\t"   << item.cinterval   << endl;
-	out << "\tRHYTHM:\t\t"    << item.rhythm      << endl;
-	out << "\tDURATION:\t"    << item.duration    << endl;
-	if (!item.harmonic.empty()) {
-		out << "\tHARMONIC:\t" << item.harmonic    << endl;
+GridMeasure* Tool_musedata2hum::getMeasure(HumGrid& outdata, HumNum starttime) {
+	for (int i=0; i<(int)outdata.size(); i++) {
+		if (outdata[i]->getTimestamp() == starttime) {
+			return outdata[i];
+		}
+	}
+	// Did not find measure in data, so append to end of list.
+	// Assuming that unknown measures are at a later timestamp
+	// than those in current list, but should fix this later perhaps.
+	GridMeasure* gm = new GridMeasure(&outdata);
+	outdata.push_back(gm);
+	return gm;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_musedata2hum::setInitialOmd --
+//
+
+void Tool_musedata2hum::setInitialOmd(const string& omd) {
+	m_omd = omd;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_musedata2hum::cleanString --
+//
+
+string Tool_musedata2hum::cleanString(const string& input) {
+	return MuseData::cleanString(input);
+}
+
+
+
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::Tool_musicxml2hum --
+//
+
+Tool_musicxml2hum::Tool_musicxml2hum(void) {
+	// Options& options = m_options;
+	// options.define("k|kern=b","display corresponding **kern data");
+
+	define("r|recip=b", "output **recip spine");
+	define("s|stems=b", "include stems in output");
+
+	VoiceDebugQ = false;
+	DebugQ = false;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::convert -- Convert a MusicXML file into
+//     Humdrum content.
+//
+
+bool Tool_musicxml2hum::convertFile(ostream& out, const char* filename) {
+	xml_document doc;
+	auto result = doc.load_file(filename);
+	if (!result) {
+		cerr << "\nXML file [" << filename << "] has syntax errors\n";
+		cerr << "Error description:\t" << result.description() << "\n";
+		cerr << "Error offset:\t" << result.offset << "\n\n";
+		exit(1);
+	}
+
+	return convert(out, doc);
+}
+
+
+bool Tool_musicxml2hum::convert(ostream& out, istream& input) {
+	string s(istreambuf_iterator<char>(input), {});
+	return convert(out, s.c_str());
+}
+
+
+bool Tool_musicxml2hum::convert(ostream& out, const char* input) {
+	xml_document doc;
+	auto result = doc.load_string(input);
+	if (!result) {
+		cout << "\nXML content has syntax errors\n";
+		cout << "Error description:\t" << result.description() << "\n";
+		cout << "Error offset:\t" << result.offset << "\n\n";
+		exit(1);
+	}
+
+	return convert(out, doc);
+}
+
+
+
+bool Tool_musicxml2hum::convert(ostream& out, xml_document& doc) {
+	initialize();
+
+	bool status = true; // for keeping track of problems in conversion process.
+
+	setSoftwareInfo(doc);
+	vector<string> partids;            // list of part IDs
+	map<string, xml_node> partinfo;    // mapping if IDs to score-part elements
+	map<string, xml_node> partcontent; // mapping of IDs to part elements
+
+	getPartInfo(partinfo, partids, doc);
+	m_used_hairpins.resize(partinfo.size());
+
+	m_current_dynamic.resize(partids.size());
+	m_current_brackets.resize(partids.size());
+	m_current_figured_bass.resize(partids.size());
+	m_stop_char.resize(partids.size(), "[");
+
+	getPartContent(partcontent, partids, doc);
+	vector<MxmlPart> partdata;
+	partdata.resize(partids.size());
+	m_last_ottava_direction.resize(partids.size());
+
+	fillPartData(partdata, partids, partinfo, partcontent);
+
+	// for debugging:
+	//printPartInfo(partids, partinfo, partcontent, partdata);
+
+	m_maxstaff = 0;
+	// check the voice info
+	for (int i=0; i<(int)partdata.size(); i++) {
+		partdata[i].prepareVoiceMapping();
+		m_maxstaff += partdata[i].getStaffCount();
+		// for debugging:
+		if (VoiceDebugQ) {
+			partdata[i].printStaffVoiceInfo();
+		}
+	}
+
+	// re-index voices to disallow empty intermediate voices.
+	reindexVoices(partdata);
+
+	HumGrid outdata;
+	status &= stitchParts(outdata, partids, partinfo, partcontent, partdata);
+
+	if (outdata.size() > 2) {
+		if (outdata.at(0)->getDuration() == 0) {
+			while (!outdata.at(0)->empty()) {
+				outdata.at(1)->push_front(outdata.at(0)->back());
+				outdata.at(0)->pop_back();
+			}
+			outdata.deleteMeasure(0);
+		}
+	}
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		m_hasOrnamentsQ |= partdata[i].hasOrnaments();
+	}
+
+	outdata.removeRedundantClefChanges();
+	outdata.removeSibeliusIncipit();
+	m_systemDecoration = getSystemDecoration(doc, outdata, partids);
+
+	// tranfer verse counts from parts/staves to HumGrid:
+	// should also do part verse counts here (-1 staffindex).
+	int versecount;
+	for (int p=0; p<(int)partdata.size(); p++) {
+		for (int s=0; s<partdata[p].getStaffCount(); s++) {
+			versecount = partdata[p].getVerseCount(s);
+			outdata.setVerseCount(p, s, versecount);
+		}
+	}
+
+	// transfer harmony counts from parts to HumGrid:
+	for (int p=0; p<(int)partdata.size(); p++) {
+		int harmonyCount = partdata[p].getHarmonyCount();
+		outdata.setHarmonyCount(p, harmonyCount);
+	}
+
+	// transfer dynamics boolean for part to HumGrid
+	for (int p = 0; p<(int)partdata.size(); p++) {
+		bool dynstate = partdata[p].hasDynamics();
+		if (dynstate) {
+			outdata.setDynamicsPresent(p);
+		}
+	}
+
+	// transfer figured bass boolean for part to HumGrid
+	for (int p=0; p<(int)partdata.size(); p++) {
+		bool fbstate = partdata[p].hasFiguredBass();
+		if (fbstate) {
+			outdata.setFiguredBassPresent(p);
+			// break;
+		}
+	}
+
+	if (m_recipQ || m_forceRecipQ) {
+		outdata.enableRecipSpine();
+	}
+
+	outdata.buildSingleList();
+	outdata.expandLocalCommentLayers();
+
+	// set the duration of the last slice
+
+	HumdrumFile outfile;
+	outdata.transferTokens(outfile);
+
+	addHeaderRecords(outfile, doc);
+	addFooterRecords(outfile, doc);
+
+	Tool_ruthfix ruthfix;
+	ruthfix.run(outfile);
+
+	addMeasureOneNumber(outfile);
+
+	// Maybe implement barnum tool and apply here based on options.
+
+	Tool_chord chord;
+	chord.run(outfile);
+
+	if (m_hasOrnamentsQ) {
+		Tool_trillspell trillspell;
+		trillspell.run(outfile);
+	}
+
+	if (m_hasTremoloQ) {
+		Tool_tremolo tremolo;
+		tremolo.run(outfile);
+	}
+
+	if (m_software == "sibelius") {
+		// Needed at least for Sibelius 19.5/Dolet 6.6 for Sibelius
+		// where grace note groups are not beamed in the MusicXML export.
+		Tool_autobeam gracebeam;
+		vector<string> argv;
+		argv.push_back("autobeam"); // name of program (placeholder)
+		argv.push_back("-g");       // beam adjacent grace notes
+		gracebeam.process(argv);
+		// Need to force a reparsing of the files contents to
+		// analyze strands.  For now just create a temporary
+		// Humdrum file to force the analysis of the strands.
+		stringstream sstream;
+		sstream << outfile;
+		HumdrumFile outfile2;
+		outfile2.readString(sstream.str());
+		gracebeam.run(outfile2);
+		outfile = outfile2;
+	}
+
+	if (m_hasTransposition) {
+		Tool_transpose transpose;
+		vector<string> argv;
+		argv.push_back("transpose"); // name of program (placeholder)
+		argv.push_back("-C");        // transpose to concert pitch
+		transpose.process(argv);
+		stringstream sstream;
+		sstream << outfile;
+		HumdrumFile outfile2;
+		outfile2.readString(sstream.str());
+		transpose.run(outfile2);
+		if (transpose.hasHumdrumText()) {
+			stringstream ss;
+			transpose.getHumdrumText(ss);
+			outfile.readString(ss.str());
+			printResult(out, outfile);
+		}
+	} else {
+		for (int i=0; i<outfile.getLineCount(); i++) {
+			outfile[i].createLineFromTokens();
+		}
+		printResult(out, outfile);
+	}
+
+	// add RDFs
+	if (m_slurabove || m_staffabove) {
+		out << "!!!RDF**kern: > = above" << endl;
+	}
+	if (m_slurbelow || m_staffbelow) {
+		out << "!!!RDF**kern: < = below" << endl;
 	}
-	return out;
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		if (partdata[i].hasEditorialAccidental()) {
+			out << "!!!RDF**kern: i = editorial accidental" << endl;
+			break;
+		}
+	}
+
+	// put the above code in here some time:
+	prepareRdfs(partdata);
+	printRdfs(out);
+
+	return status;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::Tool_musedata2hum --
+// Tool_musicxml2hum::addMeasureOneNumber -- For the first measure if it occurs before
+//    the first data, change = to =1.  Maybe check next measure for a number and
+//    addd one less than that number instead of 1.
 //
 
-Tool_musedata2hum::Tool_musedata2hum(void) {
-	// Options& options = m_options;
-	// options.define("k|kern=b","display corresponding **kern data");
+void Tool_musicxml2hum::addMeasureOneNumber(HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			break;
+		}
+		if (!infile[i].isBarline()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		string value = *token;
+		bool hasdigit = false;
+		for (int j=0; j<(int)value.size(); j++) {
+			if (isdigit(value[j])) {
+				hasdigit = true;
+				break;
+			}
+		}
+		if (hasdigit) {
+			break;
+		}
+		// there is no digit on barline, so add one.
 
-	define("g|group=s:score", "the data group to process");
-	define("r|recip=b",       "output **recip spine");
-	define("s|stems=b",       "include stems in output");
-	define("omv|no-omv=b",    "exclude extracted OMV record in output data");
+		string newvalue = "=";
+		if (value.size() < 2) {
+			newvalue += "1";
+		} else if (value[1] != '=') {
+			newvalue += "1";
+			newvalue += value.substr(1);
+		}
+		token->setText(newvalue);
+
+		// Add "1" to other spines here:
+		for (int j=1; j<infile[i].getFieldCount(); j++) {
+			HTp tok = infile.token(i, j);
+			tok->setText(newvalue);
+		}
+		break;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// initialize --
+// Tool_musicxml2hum::printResult -- filter out
+//      some item if not necessary:
+//
+// MuseScore calls everything "Piano" by default, so suppress
+// this instrument name if there is only one **kern spine in
+// the file.
 //
 
-void Tool_musedata2hum::initialize(void) {
-	m_stemsQ = getBoolean("stems");
-	m_recipQ = getBoolean("recip");
-	m_group  = getString("group");
-	m_noOmvQ = getBoolean("no-omv");
+void Tool_musicxml2hum::printResult(ostream& out, HumdrumFile& outfile) {
+	vector<HTp> kernspines = outfile.getKernSpineStartList();
+	if (kernspines.size() > 1) {
+		out << outfile;
+	} else {
+		for (int i=0; i<outfile.getLineCount(); i++) {
+			bool isPianoLabel = false;
+			bool isPianoAbbr  = false;
+			bool isPartNum    = false;
+			bool isStaffNum   = false;
+			if (!outfile[i].isInterpretation()) {
+				out << outfile[i] << "\n";
+				continue;
+			}
+			for (int j=0; j<outfile[i].getFieldCount(); j++) {
+				if (*outfile.token(i, j) == "*I\"Piano") {
+					isPianoLabel = true;
+				} else if (*outfile.token(i, j) == "*I'Pno.") {
+					isPianoAbbr = true;
+				} else if (*outfile.token(i, j) == "*staff1") {
+					isStaffNum = true;
+				} else if (*outfile.token(i, j) == "*part1") {
+					isPartNum = true;
+				}
+			}
+			if (isPianoLabel || isPianoAbbr || isStaffNum || isPartNum) {
+				continue;
+			}
+			out << outfile[i] << "\n";
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::setOptions --
+// Tool_musicxml2hum::printRdfs --
 //
 
-void Tool_musedata2hum::setOptions(int argc, char** argv) {
-	m_options.process(argc, argv);
-}
-
-
-void Tool_musedata2hum::setOptions(const vector<string>& argvlist) {
-    m_options.process(argvlist);
+void Tool_musicxml2hum::printRdfs(ostream& out) {
+	if (!m_caesura_rdf.empty()) {
+		out << m_caesura_rdf << "\n";
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::getOptionDefinitions -- Used to avoid
-//     duplicating the definitions in the test main() function.
+// Tool_muisicxml2hum::setSoftwareInfo -- Store which software program generated the
+//    MusicXML data to handle locale variants.  There can be more than one
+//    <software> entry, so desired information is not necessarily in the first one.
 //
 
-Options Tool_musedata2hum::getOptionDefinitions(void) {
-	return m_options;
+void Tool_musicxml2hum::setSoftwareInfo(xml_document& doc) {
+	string xpath = "/score-partwise/identification/encoding/software";
+	string software = doc.select_node(xpath.c_str()).node().child_value();
+	HumRegex hre;
+	if (hre.search(software, "sibelius", "i")) {
+		m_software = "sibelius";
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::convert -- Convert a MusicXML file into
-//     Humdrum content.
+// Tool_musicxml2hum::cleanSpaces -- Converts newlines and tabs to spaces, and removes
+//     trailing spaces from the string.  Does not remove leading spaces, but this could
+//     be added.  Another variation would be to use \n to encode newlines if they need
+//     to be preserved, but for now converting them to spaces.
 //
 
-bool Tool_musedata2hum::convertFile(ostream& out, const string& filename) {
-	MuseDataSet mds;
-	int result = mds.readFile(filename);
-	if (!result) {
-		cerr << "\nMuseData file [" << filename << "] has syntax errors\n";
-		cerr << "Error description:\t" << mds.getError() << "\n";
-		exit(1);
+string& Tool_musicxml2hum::cleanSpaces(string& input) {
+	for (int i=0; i<(int)input.size(); i++) {
+		if (std::isspace(input[i])) {
+			input[i] = ' ';
+		}
 	}
-	return convert(out, mds);
+	while ((!input.empty()) && std::isspace(input.back())) {
+		input.resize(input.size() - 1);
+	}
+	return input;
 }
 
 
-bool Tool_musedata2hum::convert(ostream& out, istream& input) {
-	MuseDataSet mds;
-	mds.read(input);
-	return convert(out, mds);
-}
 
+//////////////////////////////
+//
+// Tool_musicxml2hum::cleanSpacesAndColons -- Converts newlines and
+//     tabs to spaces, and removes leading and trailing spaces from the
+//     string.  Another variation would be to use \n to encode newlines
+//     if they need to be preserved, but for now converting them to spaces.
+//     Colons (:) are also converted to &colon;.
 
-bool Tool_musedata2hum::convertString(ostream& out, const string& input) {
-	MuseDataSet mds;
-	int result = mds.readString(input);
-	if (!result) {
-		cout << "\nXML content has syntax errors\n";
-		cout << "Error description:\t" << mds.getError() << "\n";
-		exit(1);
+string Tool_musicxml2hum::cleanSpacesAndColons(const string& input) {
+	string output;
+	bool foundnonspace = false;
+	for (int i=0; i<(int)input.size(); i++) {
+		if (std::isspace(input[i])) {
+			if (!foundnonspace) {
+				output += ' ';
+			}
+		}
+		if (input[i] == ':') {
+			foundnonspace = true;
+			output += "&colon;";
+		} else {
+			output += input[i];
+			foundnonspace = true;
+		}
 	}
-	return convert(out, mds);
-}
-
-
-bool Tool_musedata2hum::convert(ostream& out, MuseDataSet& mds) {
-	int partcount = mds.getFileCount();
-	if (partcount == 0) {
-		cerr << "Error: No parts found in data:" << endl;
-		cerr << mds << endl;
-		return false;
+	while ((!output.empty()) && std::isspace(output.back())) {
+		output.resize(output.size() - 1);
 	}
-	initialize();
+	return output;
+}
 
-	m_tempo = mds.getMidiTempo();
 
-	vector<int> groupMemberIndex = mds.getGroupIndexList(m_group);
-	if (groupMemberIndex.empty()) {
-		cerr << "Error: no files in the " << m_group << " membership." << endl;
-		return false;
-	}
 
-	HumGrid outdata;
-	bool status = true;
-	for (int i=0; i<(int)groupMemberIndex.size(); i++) {
-		status &= convertPart(outdata, mds, groupMemberIndex[i], i, (int)groupMemberIndex.size());
-	}
+//////////////////////////////
+//
+// Tool_musicxml2hum::addHeaderRecords -- Inserted in reverse order
+//      (last record inserted first).
+//
 
-	HumdrumFile outfile;
-	outdata.transferTokens(outfile);
-	outfile.generateLinesFromTokens();
-	stringstream sss;
-	sss << outfile;
-	outfile.readString(sss.str());
+void Tool_musicxml2hum::addHeaderRecords(HumdrumFile& outfile, xml_document& doc) {
+	string xpath;
+	HumRegex hre;
 
-	if (needsAboveBelowKernRdf()) {
-		outfile.appendLine("!!!RDF**kern: > = above");
-		outfile.appendLine("!!!RDF**kern: < = above");
+	if (!m_systemDecoration.empty()) {
+		// outfile.insertLine(0, "!!!system-decoration: " + m_systemDecoration);
+		if (m_systemDecoration != "s1") {
+			outfile.appendLine("!!!system-decoration: " + m_systemDecoration);
+		}
 	}
 
-	outfile.createLinesFromTokens();
-
-	Tool_trillspell trillspell;
-	trillspell.run(outfile);
-
-	// Convert comments in header of first part:
-	int ii = groupMemberIndex[0];
-	bool ending = false;
-	HumRegex hre;
-	for (int i=0; i< mds[ii].getLineCount(); i++) {
-		if (mds[ii][i].isAnyNote()) {
-			break;
+	xpath = "/score-partwise/credit/credit-words";
+   pugi::xpath_node_set credits = doc.select_nodes(xpath.c_str());
+	map<string, int> keys;
+	vector<string> refs;
+	vector<int> positions; // +1 = above, -1 = below;
+	for (auto it = credits.begin(); it != credits.end(); it++) {
+		string contents = cleanSpaces(it->node().child_value());
+		if (contents.empty()) {
+			continue;
 		}
-		if (mds[ii].getLine(i).compare(0, 2, "@@") == 0) {
-			string output = mds[ii].getLine(i);
-			if (output == "@@@") {
-				ending = true;
-				continue;
-			}
-			for (int j=0; j<(int)output.size(); j++) {
-				if (output[j] == '@') {
-					output[j] = '!';
-				} else {
-					break;
-				}
-			}
-			if (hre.search(output, "!!!\\s*([^!:]+)\\s*:")) {
-				string key = hre.getMatch(1);
-				m_usedReferences[key] = true;
-			}
-			if (ending) {
-           m_postReferences.push_back(output);
+		if ((contents[0] != '@') && (contents[0] != '!')) {
+			continue;
+		}
+
+		if (contents.size() >= 3) {
+			// If line starts with "@@" then place at end of score.
+			if ((contents[0] == '@') && (contents[1] == '@')) {
+				positions.push_back(-1);
 			} else {
-				out << output << endl;
+				positions.push_back(1);
 			}
+		} else {
+			positions.push_back(1);
 		}
-	}
 
-	if (!m_usedReferences["COM"]) {
-		string composer = mds[ii].getComposer();
-		if (!composer.empty()) {
-				out << "!!!COM: " << composer << endl;
+		if (hre.search(contents, "^[@!]+([^\\s]+):")) {
+			// reference record
+			string key = hre.getMatch(1);
+			keys[key] = 1;
+			hre.replaceDestructive(contents, "!!!", "^[!@]+");
+			refs.push_back(contents);
+		} else {
+			// global comment
+			hre.replaceDestructive(contents, "!!", "^[!@]+");
+			refs.push_back(contents);
 		}
 	}
 
-	if (!m_usedReferences["CDT"]) {
-		string cdate = mds[ii].getComposerDate();
-		if (!cdate.empty()) {
-			out << "!!!CDT: " << cdate << endl;
-		}
+	// OTL: title //////////////////////////////////////////////////////////
+
+	// Sibelius method
+	xpath = "/score-partwise/work/work-title";
+	string worktitle = cleanSpaces(doc.select_node(xpath.c_str()).node().child_value());
+	string otl_record;
+	string omv_record;
+	bool worktitleQ = false;
+	if ((worktitle != "") && (worktitle != "Title")) {
+		otl_record = "!!!OTL: ";
+		otl_record += worktitle;
+		worktitleQ = true;
 	}
 
-	if (!m_usedReferences["OTL"]) {
-		string worktitle = mds[ii].getWorkTitle();
-		if (!worktitle.empty()) {
-			out << "!!!OTL: " << worktitle << endl;
+	xpath = "/score-partwise/movement-title";
+	string mtitle = cleanSpaces(doc.select_node(xpath.c_str()).node().child_value());
+	if (mtitle != "") {
+		if (worktitleQ) {
+			omv_record = "!!!OMV: ";
+			omv_record += mtitle;
+		} else {
+			otl_record = "!!!OTL: ";
+			otl_record += mtitle;
 		}
 	}
 
-	if (!m_noOmvQ) {
-		if (!m_usedReferences["OMV"]) {
-			string movementtitle = mds[ii].getMovementTitle();
-			if (!movementtitle.empty()) {
-				out << "!!!OMV: " << movementtitle << endl;
+	// COM: composer /////////////////////////////////////////////////////////
+	// CDT: composer's dates
+	xpath = "/score-partwise/identification/creator[@type='composer']";
+	string composer = cleanSpaces(doc.select_node(xpath.c_str()).node().child_value());
+	string cdt_record;
+	if (composer != "") {
+		if (hre.search(composer, R"(\((.*?\d.*?)\))")) {
+			string dates = hre.getMatch(1);
+			// hre.replaceDestructive(composer, "", R"(\()" + dates + R"(\))");
+			auto loc = composer.find(dates);
+			if (loc != std::string::npos) {
+				composer.replace(loc-1, dates.size()+2, "");
+			}
+			hre.replaceDestructive(composer, "", R"(^\s+)");
+			hre.replaceDestructive(composer, "", R"(\s+$)");
+			if (hre.search(composer, R"(([^\s]+) +([^\s]+))")) {
+				composer = hre.getMatch(2) + ", " + hre.getMatch(1);
+			}
+			if (dates != "") {
+				if (hre.search(dates, R"(\b(\d{4})\?)")) {
+					string replacement = "~";
+					replacement += hre.getMatch(1);
+					hre.replaceDestructive(dates, replacement, R"(\b\d{4}\?)");
+					cdt_record = "!!!CDT: ";
+					cdt_record += dates;
+				}
 			}
 		}
 	}
 
-	if (!m_usedReferences["OPS"]) {
-		string opus = mds[ii].getOpus();
-		if (!opus.empty()) {
-			out << "!!!OPS: " << opus << endl;
-		}
-	}
 
-	if (!m_usedReferences["ONM"]) {
-		string number = mds[ii].getNumber();
-		if (!number.empty()) {
-			out << "!!!ONM: " << number << endl;
+	for (int i=(int)refs.size()-1; i>=0; i--) {
+		if (positions.at(i) > 0) {
+			// place at start of file
+			outfile.insertLine(0, refs[i]);
 		}
 	}
 
-	if (!m_usedReferences["OMD"]) {
-		if (!m_omd.empty()) {
-			out << "!!!OMD: " << m_omd << endl;
+	for (int i=0; i<(int)refs.size(); i++) {
+		if (positions.at(i) < 0) {
+			// place at end of file
+			outfile.appendLine(refs[i]);
 		}
 	}
 
-	bool foundDataQ = false;
-	for (int i=0; i<outfile.getLineCount(); i++) {
-		if (outfile[i].isData()) {
-			foundDataQ = true;
-		}
-		if (outfile[i].isBarline() && !foundDataQ) {
-			HTp token = outfile.token(i, 0);
-			if (*token == "=") {
-				HTp nextBar = NULL;
-				for (int j=i+1; j<outfile.getLineCount(); j++) {
-					if (outfile[j].isBarline()) {
-						nextBar = outfile.token(j, 0);
-						break;
-					}
-				}
-				if (nextBar) {
-					HumRegex hre;
-					if (hre.search(nextBar, "\\b1\\b")) {
-						continue;
-					} else if (hre.search(nextBar, "\\b2\\b")) {
-						for (int j=0; j<outfile[i].getFieldCount(); j++) {
-							out << "=1";
-							if (j < outfile[i].getFieldCount() - 1) {
-								out << "\t";
-							}
-						}
-						out << endl;
-						continue;
-					}
-					// also deal with repeat barlines at the start of the music.
-				}
-			}
-		}
-		printLine(out, outfile[i]);
+	if ((!omv_record.empty()) && (!keys["OMV"])) {
+		outfile.insertLine(0, omv_record);
 	}
 
-	if (!m_usedReferences["SMS"]) {
-		string source = mds[ii].getSource();
-		if (!source.empty()) {
-			out << "!!!SMS: " << source << endl;
-		}
+	if ((!otl_record.empty()) && (!keys["OTL"])) {
+		outfile.insertLine(0, otl_record);
 	}
 
-	if (!m_usedReferences["ENC"]) {
-		string encoder = mds[ii].getEncoderName();
-		if (!encoder.empty()) {
-			out << "!!!ENC: " << encoder << endl;
-		}
+	if ((!cdt_record.empty()) && (!keys["CDT"])) {
+		outfile.insertLine(0, cdt_record);
 	}
 
-	if (!m_usedReferences["END"]) {
-		string edate = mds[ii].getEncoderDate();
-		if (!edate.empty()) {
-			out << "!!!END: " << edate << endl;
+	if ((!composer.empty()) && (!keys["COM"])) {
+		// Don't print composer name if it is "Composer".
+		if (composer != "Composer") {
+			string com_record = "!!!COM: " + composer;
+			outfile.insertLine(0, com_record);
 		}
 	}
 
-	for (int i=0; i<(int)m_postReferences.size(); i++) {
-		out << m_postReferences[i] << endl;
-	}
-	m_postReferences.clear();
+}
 
-	stringstream ss;
-	auto nowtime = std::chrono::system_clock::now();
-	time_t currenttime = std::chrono::system_clock::to_time_t(nowtime);
-	ss << std::ctime(&currenttime);
-	out << "!!!ONB: Converted from MuseData with musedata2hum on " << ss.str();
 
-	string copyright = mds[ii].getCopyright();
-	if (!copyright.empty()) {
-		out << "!!!YEM: " << copyright << endl;
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::addFooterRecords --
+//
+
+void Tool_musicxml2hum::addFooterRecords(HumdrumFile& outfile, xml_document& doc) {
+
+	// YEM: copyright
+	string copy = doc.select_node("/score-partwise/identification/rights").node().child_value();
+	bool validcopy = true;
+	if (copy == "") {
+		validcopy = false;
+	}
+	if ((copy.length() == 2) && ((unsigned char)copy[0] == 0xc2) && ((unsigned char)copy[1] == 0xa9)) {
+		validcopy = false;
+	}
+	if ((copy.find("opyright") != std::string::npos) && (copy.size() < 15)) {
+		validcopy = false;
 	}
 
-	// Convert comments in footer of last part:
-	int lastone = groupMemberIndex.back();
-	vector<string> outputs;
-	for (int i=mds[lastone].getLineCount() - 1; i>=0; i--) {
-		if (mds[lastone][i].isAnyNote()) {
-			break;
-		}
-		if (mds[lastone].getLine(i).compare(0, 2, "@@") == 0) {
-			string output = mds[lastone].getLine(i);
-			for (int j=0; j<(int)output.size(); j++) {
-				if (output[j] == '@') {
-					output[j] = '!';
-				} else {
-					break;
-				}
-			}
-			outputs.push_back(output);
-		}
+	if (validcopy) {
+		string yem_record = "!!!YEM: ";
+		yem_record += cleanSpaces(copy);
+		outfile.appendLine(yem_record);
 	}
 
-	for (int i=(int)outputs.size() - 1; i>=0; i--) {
-		out << outputs[i] << endl;
+	// RDF:
+	if (m_hasEditorial) {
+		string rdf_record = "!!!RDF**kern: i = editorial accidental";
+		outfile.appendLine(rdf_record);
 	}
+}
+
+
+
+//////////////////////////////
+//
+// initialize --
+//
 
-	return status;
+void Tool_musicxml2hum::initialize(void) {
+	m_recipQ = getBoolean("recip");
+	m_stemsQ = getBoolean("stems");
+	m_hasOrnamentsQ = false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::printLine -- Print line of Humdrum file
-//     contents.  If there is any layout parameter in the line tokens,
-//     then print an extra line with these.  Currently only checking for
-//     a single parameter.
+// Tool_musicxml2hum::reindexVoices --
 //
 
-void Tool_musedata2hum::printLine(ostream& out, HumdrumLine& line) {
-	vector<string> lo(line.getFieldCount());
-	int count = 0;
-	for (int i=0; i<line.getFieldCount(); i++) {
-		HTp token = line.token(i);
-		string value = token->getValue("auto", "LO");
-		if (!value.empty()) {
-			lo.at(i) = value;
-			count++;
-		}
-	}
-	if (count > 0) {
-		for (int i=0; i<(int)lo.size(); i++) {
-			if (lo[i].empty()) {
-				out << "!";
-			} else {
-				out << lo[i];
-			}
-			if (i < (int)lo.size() - 1) {
-				out << "\t";
+void Tool_musicxml2hum::reindexVoices(vector<MxmlPart>& partdata) {
+	for (int p=0; p<(int)partdata.size(); p++) {
+		for (int m=0; m<(int)partdata[p].getMeasureCount(); m++) {
+			MxmlMeasure* measure = partdata[p].getMeasure(m);
+			if (!measure) {
+				continue;
 			}
+			reindexMeasure(measure);
 		}
-		out << endl;
 	}
-	out << line << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::convertPart --
+// Tool_musicxml2hum::prepareRdfs --
 //
 
-bool Tool_musedata2hum::convertPart(HumGrid& outdata, MuseDataSet& mds, int index, int partindex, int maxstaff) {
-	MuseData& part = mds[index];
-	m_lastfigure = NULL;
-	m_lastnote = NULL;
-	m_lastbarnum = -1;
-	m_part = partindex;
-	// maybe maxpart?
-	m_maxstaff = maxstaff;
-
-	bool status = true;
-	int i = 0;
-	while (i < part.getLineCount()) {
-		m_measureLineIndex = i;
-		i = convertMeasure(outdata, part, partindex, i);
+void Tool_musicxml2hum::prepareRdfs(vector<MxmlPart>& partdata) {
+	string caesura;
+	for (int i=0; i<(int)partdata.size(); i++) {
+		caesura = partdata[i].getCaesura();
+		if (!caesura.empty()) {
+		}
 	}
 
-	storePartName(outdata, part, partindex);
+	if (!caesura.empty()) {
+		m_caesura_rdf = "!!!RDF**kern: " + caesura + " = caesura";
+	}
 
-	return status;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_musedata2hum::storePartName --
+// Tool_musicxml2hum::reindexMeasure --
 //
 
-void Tool_musedata2hum::storePartName(HumGrid& outdata, MuseData& part, int index) {
-	string name = part.getPartName();
-	if (!name.empty()) {
-		outdata.setPartName(index, name);
+void Tool_musicxml2hum::reindexMeasure(MxmlMeasure* measure) {
+	if (!measure) {
+		return;
 	}
-}
-
 
+	vector<vector<int> > staffVoiceCounts;
+	vector<MxmlEvent*>& elist = measure->getEventList();
 
-//////////////////////////////
-//
-// Tool_musedata2hum::convertMeasure --
-//
+	for (int i=0; i<(int)elist.size(); i++) {
+		int staff = elist[i]->getStaffIndex();
+		int voice = elist[i]->getVoiceIndex();
 
-int Tool_musedata2hum::convertMeasure(HumGrid& outdata, MuseData& part, int partindex, int startindex) {
-	if (part.getLineCount() == 0) {
-		return 1;
-	}
-	HumNum starttime = part[startindex].getAbsBeat();
-	HumNum filedur = part.getFileDuration();
-	HumNum diff = filedur - starttime;
-	if (diff == 0) {
-		// last barline in score, so ignore
-		return startindex + 1;;
+		if ((voice >= 0) && (staff >= 0)) {
+			if (staff >= (int)staffVoiceCounts.size()) {
+				int newsize = staff + 1;
+				staffVoiceCounts.resize(newsize);
+			}
+			if (voice >= (int)staffVoiceCounts[staff].size()) {
+				int oldsize = (int)staffVoiceCounts[staff].size();
+				int newsize = voice + 1;
+				staffVoiceCounts[staff].resize(newsize);
+				for (int i=oldsize; i<newsize; i++) {
+					staffVoiceCounts[staff][voice] = 0;
+				}
+			}
+			staffVoiceCounts[staff][voice]++;
+		}
 	}
 
-	GridMeasure* gm = getMeasure(outdata, starttime);
-	int i = startindex;
-	for (i=startindex; i<part.getLineCount(); i++) {
-		if ((i != startindex) && part[i].isBarline()) {
+	bool needreindexing = false;
+
+	for (int i=0; i<(int)staffVoiceCounts.size(); i++) {
+		if (staffVoiceCounts[i].size() < 2) {
+			continue;
+		}
+		for (int j=1; j<(int)staffVoiceCounts[i].size(); j++) {
+			if (staffVoiceCounts[i][j] == 0) {
+				needreindexing = true;
+				break;
+			}
+		}
+		if (needreindexing) {
 			break;
 		}
-		convertLine(gm, part[i]);
 	}
-	HumNum endtime = starttime;
-	if (i >= part.getLineCount()) {
-		endtime = part[i-1].getAbsBeat();
-	} else {
-		endtime = part[i].getAbsBeat();
+
+	if (!needreindexing) {
+		return;
 	}
 
-	// set duration of measures (so it will be printed in conversion to Humdrum):
-	gm->setDuration(endtime - starttime);
-	gm->setTimestamp(starttime);
-	gm->setTimeSigDur(m_timesigdur);
+	vector<vector<int> > remapping;
+	remapping.resize(staffVoiceCounts.size());
+	int reindex;
+	for (int i=0; i<(int)staffVoiceCounts.size(); i++) {
+		remapping[i].resize(staffVoiceCounts[i].size());
+		reindex = 0;
+		for (int j=0; j<(int)remapping[i].size(); j++) {
+			if (remapping[i].size() == 1) {
+				remapping[i][j] = 0;
+				continue;
+			}
+			if (staffVoiceCounts[i][j]) {
+				remapping[i][j] = reindex++;
+			} else {
+				remapping[i][j] = -1;  // invalidate voice
+			}
+		}
+	}
 
-	if ((i < part.getLineCount()) && part[i].isBarline()) {
-		if (partindex == 0) {
-			// For now setting the barline style from the
-			// lowest staff.  This is mostly because
-			// MEI/verovio can handle only one style
-			// on a system barline.  But also because
-			// GridMeasure objects only has a setting
-			// for a single barline style.
-			setMeasureStyle(outdata.back(), part[i]);
-			setMeasureNumber(outdata.back(), part[i]);
-			// gm->setBarStyle(MeasureStyle::Plain);
+	// Go back and remap the voice indexes of elements.
+	// Presuming that the staff does not need to be reindex.
+	for (int i=0; i<(int)elist.size(); i++) {
+		int oldvoice = elist[i]->getVoiceIndex();
+		int staff = elist[i]->getStaffIndex();
+		if (oldvoice < 0) {
+			continue;
+		}
+		int newvoice = remapping[staff][oldvoice];
+		if (newvoice == oldvoice) {
+			continue;
 		}
+		elist[i]->setVoiceIndex(newvoice);
 	}
 
-	return i;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::setMeasureNumber --
+// Tool_musicxml2hum::setOptions --
 //
 
-void Tool_musedata2hum::setMeasureNumber(GridMeasure* gm, MuseRecord& mr) {
-	int pos = -1;
-	string line = mr.getLine();
-	bool space = false;
-	for (int i=0; i<(int)line.size(); i++) {
-		if (isspace(line[i])) {
-			space = true;
-			continue;
-		}
-		if (!space) {
-			continue;
-		}
-		if (isdigit(line[i])) {
-			pos = i;
-			break;
-		}
-	}
-	if (pos < 0) {
-		gm->setMeasureNumber(-1);
-		return;
-	}
-	int num = stoi(line.substr(pos));
-	if (m_lastbarnum >= 0) {
-		int temp = num;
-		num = m_lastbarnum;
-		m_lastbarnum = temp;
-	}
-	gm->setMeasureNumber(num);
+void Tool_musicxml2hum::setOptions(int argc, char** argv) {
+	m_options.process(argc, argv);
+}
+
+
+void Tool_musicxml2hum::setOptions(const vector<string>& argvlist) {
+    m_options.process(argvlist);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::setMeasureStyle --
+// Tool_musicxml2hum::getOptionDefinitions -- Used to avoid
+//     duplicating the definitions in the test main() function.
 //
 
-void Tool_musedata2hum::setMeasureStyle(GridMeasure* gm, MuseRecord& mr) {
-	string line = mr.getLine();
-	string barstyle = mr.getMeasureFlags();
-	if (line.compare(0, 7, "mheavy2") == 0) {
-		if (barstyle.find(":|") != string::npos) {
-			gm->setStyle(MeasureStyle::RepeatBackward);
-		} else {
-			gm->setStyle(MeasureStyle::Final);
-		}
-	} else if (line.compare(0, 7, "mheavy3") == 0) {
-		if (barstyle.find("|:") != string::npos) {
-			gm->setStyle(MeasureStyle::RepeatForward);
-		}
-	} else if (line.compare(0, 7, "mheavy4") == 0) {
-		if (barstyle.find(":|:") != string::npos) {
-			gm->setStyle(MeasureStyle::RepeatBoth);
-		} else if (barstyle.find("|: :|") != string::npos) {
-			// Vivaldi op. 1, no. 1, mvmt. 1, m. 10: mheavy4          |: :|
-			gm->setStyle(MeasureStyle::RepeatBoth);
-		}
-	} else if (line.compare(0, 7, "mdouble") == 0) {
-		gm->setStyle(MeasureStyle::Double);
-	}
+Options Tool_musicxml2hum::getOptionDefinitions(void) {
+	return m_options;
 }
 
 
+///////////////////////////////////////////////////////////////////////////
+
+
 //////////////////////////////
 //
-// Tool_musedata2hum::convertLine --
+// Tool_musicxml2hum::fillPartData --
 //
 
-void Tool_musedata2hum::convertLine(GridMeasure* gm, MuseRecord& mr) {
-	int part         = m_part;
-	int staff        = 0;
-	int maxstaff     = m_maxstaff;
-	int layer        = mr.getLayer();
-	if (layer > 0) {
-		// convert to an index:
-		layer = layer - 1;
-	}
+bool Tool_musicxml2hum::fillPartData(vector<MxmlPart>& partdata,
+		const vector<string>& partids, map<string, xml_node>& partinfo,
+		map<string, xml_node>& partcontent) {
 
-	if (mr.isAnyNoteOrRest()) {
-		m_figureOffset = 0;
+	bool output = true;
+	for (int i=0; i<(int)partinfo.size(); i++) {
+		partdata[i].setPartNumber(i+1);
+		output &= fillPartData(partdata[i], partids[i], partinfo[partids[i]],
+				partcontent[partids[i]]);
 	}
+	return output;
+}
 
-	if (mr.isDirection()) {
-		return;
-	}
 
-	HumNum timestamp = mr.getAbsBeat();
-	// cerr << "CONVERTING LINE " << timestamp << "\t" << mr << endl;
-	string tok;
-	GridSlice* slice = NULL;
+bool Tool_musicxml2hum::fillPartData(MxmlPart& partdata,
+		const string& id, xml_node partdeclaration, xml_node partcontent) {
+	if (m_stemsQ) {
+		partdata.enableStems();
+	}
 
-	if (mr.isBarline()) {
-		// barline handled elsewhere
-		// tok = mr.getKernMeasure();
-	} else if (mr.isAttributes()) {
-		map<string, string> attributes;
-		mr.getAttributeMap(attributes);
+	partdata.parsePartInfo(partdeclaration);
+	// m_last_ottava_direction.at(partdata.getPartIndex()).resize(partdata.getStaffCount());
+	// staff count is incorrect at this point? Just assume 32 staves in the part, which should
+	// be 28-30 staffs too many.
+	m_last_ottava_direction.at(partdata.getPartIndex()).resize(32);
 
-		string mtempo = cleanString(attributes["D"]);
-		if (!mtempo.empty()) {
-			if (timestamp != 0) {
-				string value = "!!!OMD: " + mtempo;
-				gm->addGlobalComment(value, timestamp);
-			} else {
-				setInitialOmd(mtempo);
+	int count;
+	auto measures = partcontent.select_nodes("./measure");
+	for (int i=0; i<(int)measures.size(); i++) {
+		partdata.addMeasure(measures[i].node());
+		count = partdata.getMeasureCount();
+		if (count > 1) {
+			HumNum dur = partdata.getMeasure(count-1)->getTimeSigDur();
+			if (dur == 0) {
+				HumNum dur = partdata.getMeasure(count-2)
+						->getTimeSigDur();
+				if (dur > 0) {
+					partdata.getMeasure(count - 1)->setTimeSigDur(dur);
+				}
 			}
 		}
 
-		if (!attributes["Q"].empty()) {
-			m_quarterDivisions = std::stoi(attributes["Q"]);
-		}
+	}
+	return true;
+}
 
-		string mclef = attributes["C"];
-		if (!mclef.empty()) {
-			string kclef = Convert::museClefToKernClef(mclef);
-			if (!kclef.empty()) {
-				gm->addClefToken(kclef, timestamp, part, staff, layer, maxstaff);
-			}
-		}
 
-		string mkeysig = attributes["K"];
-		if (!mkeysig.empty()) {
-			string kkeysig = Convert::museKeySigToKernKeySig(mkeysig);
-			gm->addKeySigToken(kkeysig, timestamp, part, staff, layer, maxstaff);
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::printPartInfo -- Debug information.
+//
+
+void Tool_musicxml2hum::printPartInfo(vector<string>& partids,
+		map<string, xml_node>& partinfo, map<string, xml_node>& partcontent,
+		vector<MxmlPart>& partdata) {
+	cout << "\nPart information in the file:" << endl;
+	int maxmeasure = 0;
+	for (int i=0; i<(int)partids.size(); i++) {
+		cout << "\tPART " << i+1 << " id = " << partids[i] << endl;
+		cout << "\tMAXSTAFF " << partdata[i].getStaffCount() << endl;
+		cout << "\t\tpart name:\t"
+		     << getChildElementText(partinfo[partids[i]], "part-name") << endl;
+		cout << "\t\tpart abbr:\t"
+		     << getChildElementText(partinfo[partids[i]], "part-abbreviation")
+		     << endl;
+		auto node = partcontent[partids[i]];
+		auto measures = node.select_nodes("./measure");
+		cout << "\t\tMeasure count:\t" << measures.size() << endl;
+		if (maxmeasure < (int)measures.size()) {
+			maxmeasure = (int)measures.size();
 		}
+		cout << "\t\tTotal duration:\t" << partdata[i].getDuration() << endl;
+	}
 
-		string mtimesig = attributes["T"];
-		if (!mtimesig.empty()) {
-			string ktimesig = Convert::museTimeSigToKernTimeSig(mtimesig);
-			slice = gm->addTimeSigToken(ktimesig, timestamp, part, staff, layer, maxstaff);
-			setTimeSigDurInfo(ktimesig);
-			string kmeter = Convert::museMeterSigToKernMeterSig(mtimesig);
-			if (!kmeter.empty()) {
-				slice = gm->addMeterSigToken(kmeter, timestamp, part, staff, layer, maxstaff);
+	MxmlMeasure* measure;
+	for (int i=0; i<maxmeasure; i++) {
+		cout << "m" << i+1 << "\t";
+		for (int j=0; j<(int)partdata.size(); j++) {
+			measure = partdata[j].getMeasure(i);
+			if (measure) {
+				cout << measure->getDuration();
 			}
-			if (m_tempo > 0.00) {
-				int value = (int)(m_tempo + 0.5);
-				string tempotok = "*MM" + to_string(value);
-				slice = gm->addTempoToken(tempotok, timestamp, part, staff, layer, maxstaff);
+			if (j < (int)partdata.size() - 1) {
+				cout << "\t";
 			}
 		}
-	} else if (mr.isRegularNote()) {
-		tok = mr.getKernNoteStyle(1, 1);
-		string other = mr.getKernNoteOtherNotations();
-		if (!needsAboveBelowKernRdf()) {
-			if (other.find("<") != string::npos) {
-				addAboveBelowKernRdf();
-			} else if (other.find(">") != string::npos) {
-				addAboveBelowKernRdf();
-			}
+		cout << endl;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::insertPartNames --
+//
+
+void Tool_musicxml2hum::insertPartNames(HumGrid& outdata, vector<MxmlPart>& partdata) {
+
+	bool hasname = false;
+	bool hasabbr = false;
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		string value;
+		value = partdata[i].getPartName();
+		if (!value.empty()) {
+			hasname = true;
+			break;
 		}
-		if (!other.empty()) {
-			tok += other;
+	}
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		string value;
+		value = partdata[i].getPartAbbr();
+		if (!value.empty()) {
+			hasabbr = true;
+			break;
 		}
-		slice = gm->addDataToken(tok, timestamp, part, staff, layer, maxstaff);
-		if (slice) {
-			mr.setVoice(slice->at(part)->at(staff)->at(layer));
-			string gr = mr.getLayoutVis();
-			if (gr.size() > 0) {
-				// Visual and performance durations are not equal:
-				HTp token = slice->at(part)->at(staff)->at(layer)->getToken();
-				string text = "!LO:N:vis=" + gr;
-				token->setValue("auto", "LO", text);
+	}
+
+	if (!(hasabbr || hasname)) {
+		return;
+	}
+
+	GridMeasure* gm;
+	if (outdata.empty()) {
+		gm = new GridMeasure(&outdata);
+		outdata.push_back(gm);
+	} else {
+		gm = outdata[0];
+	}
+
+	int maxstaff;
+
+	if (hasabbr) {
+		for (int i=0; i<(int)partdata.size(); i++) {
+			string partabbr = partdata[i].getPartAbbr();
+			if (partabbr.empty()) {
+				continue;
 			}
+			string abbr = "*I'" + partabbr;
+			maxstaff = outdata.getStaffCount(i);
+			gm->addLabelAbbrToken(abbr, 0, i, maxstaff-1, 0, (int)partdata.size(), maxstaff);
 		}
-		m_lastnote = slice->at(part)->at(staff)->at(layer)->getToken();
-		addNoteDynamics(slice, part, mr);
-		addDirectionDynamics(slice, part, mr);
-		addLyrics(slice, part, staff, mr);
-	} else if (mr.isFiguredHarmony()) {
-		addFiguredHarmony(mr, gm, timestamp, part, maxstaff);
-	} else if (mr.isChordNote()) {
-		tok = mr.getKernNoteStyle(1, 1);
-		if (m_lastnote) {
-			string text = m_lastnote->getText();
-			text += " ";
-			text += tok;
-			m_lastnote->setText(text);
-		} else {
-			cerr << "Warning: found chord note with no regular note to attach to" << endl;
-		}
-	} else if (mr.isCueNote()) {
-		cerr << "PROCESS CUE NOTE HERE: " << mr << endl;
-	} else if (mr.isGraceNote()) {
-		cerr << "PROCESS GRACE NOTE HERE: " << mr << endl;
-	} else if (mr.isChordGraceNote()) {
-		cerr << "PROCESS GRACE CHORD NOTE HERE: " << mr << endl;
-	} else if (mr.isAnyRest()) {
-		tok  = mr.getKernRestStyle();
-		slice = gm->addDataToken(tok, timestamp, part, staff, layer, maxstaff);
-		if (slice) {
-			mr.setVoice(slice->at(part)->at(staff)->at(layer));
-			string gr = mr.getLayoutVis();
-			if (gr.size() > 0) {
-				cerr << "GRAPHIC VERSION OF NOTEB " << gr << endl;
+	}
+
+	if (hasname) {
+		for (int i=0; i<(int)partdata.size(); i++) {
+			string partname = partdata[i].getPartName();
+			if (partname.empty()) {
+				continue;
 			}
-		}
-	} else if (mr.isDirection()) {
-		if (mr.isTextDirection()) {
-			addTextDirection(gm, part, staff, mr, timestamp);
+			if (partname.find("MusicXML") != string::npos) {
+				// ignore Finale dummy part names
+				continue;
+			}
+			if (partname.find("Part_") != string::npos) {
+				// ignore SharpEye dummy part names
+				continue;
+			}
+			if (partname.find("Unnamed") != string::npos) {
+				// ignore Sibelius dummy part names
+				continue;
+			}
+			string name = "*I\"" + partname;
+			maxstaff = outdata.getStaffCount(i);
+			gm->addLabelToken(name, 0, i, maxstaff-1, 0, (int)partdata.size(), maxstaff);
 		}
 	}
+
 }
 
 
+
 //////////////////////////////
 //
-// Tool_musedata2hum::addDirectionDynamics -- search for a dynamic
-//     marking before the current line and after any previous note
-//     or similar line.   These lines are store in "musical directions"
-//     which start the line with a "*" character.
-//
-// Example for "p" dyamic, with print suggesting.
-//             1         2
-//    12345678901234567890123456789
-//    *               G       p
-//    P    C17:Y57
+// Tool_musicxml2hum::stitchParts -- Merge individual parts into a
+//     single score sequence.
 //
 
-void Tool_musedata2hum::addDirectionDynamics(GridSlice* slice, int part, MuseRecord& mr) {
-	MuseRecord* direction = mr.getMusicalDirection();
-	if (direction == NULL) {
-		return;
+bool Tool_musicxml2hum::stitchParts(HumGrid& outdata,
+		vector<string>& partids, map<string, xml_node>& partinfo,
+		map<string, xml_node>& partcontent, vector<MxmlPart>& partdata) {
+	if (partdata.size() == 0) {
+		return false;
 	}
 
-	if (direction->isDynamic()) {
-		string dynamicText = direction->getDynamicText();
-		if (!dynamicText.empty()) {
-			slice->at(part)->setDynamics(dynamicText);
-			HumGrid* grid = slice->getOwner();
-			if (grid) {
-				grid->setDynamicsPresent(part);
-			}
+	int i;
+	int measurecount = partdata[0].getMeasureCount();
+	// i used to start at 1 for some strange reason.
+	for (i=0; i<(int)partdata.size(); i++) {
+		if (measurecount != partdata[i].getMeasureCount()) {
+			cerr << "ERROR: cannot handle parts with different measure\n";
+			cerr << "counts yet. Compare MM" << measurecount << " to MM";
+			cerr << partdata[i].getMeasureCount() << endl;
+			exit(1);
 		}
 	}
+
+	vector<int> partstaves(partdata.size(), 0);
+	for (i=0; i<(int)partstaves.size(); i++) {
+		partstaves[i] = partdata[i].getStaffCount();
+	}
+
+	bool status = true;
+	int m;
+	for (m=0; m<partdata[0].getMeasureCount(); m++) {
+		status &= insertMeasure(outdata, m, partdata, partstaves);
+		// a hack for now:
+		// insertSingleMeasure(outfile);
+		// measures.push_back(&outfile[outfile.getLineCount()-1]);
+	}
+
+	moveBreaksToEndOfPreviousMeasure(outdata);
+
+	insertPartNames(outdata, partdata);
+
+	return status;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::addAboveBelowKernRdf -- Save for later that
-//      !!!RDF**kern: > = above
-//      !!!RDF**kern: < = below
-//    in the output Humdrum data file.
+// moveBreaksToEndOfPreviousMeasure --
 //
 
-void Tool_musedata2hum::addAboveBelowKernRdf(void) {
-	m_aboveBelowKernRdf = true;
+void Tool_musicxml2hum::moveBreaksToEndOfPreviousMeasure(HumGrid& outdata) {
+	for (int i=1; i<(int)outdata.size(); i++) {
+		GridMeasure* gm = outdata[i];
+		GridMeasure* gmlast = outdata[i-1];
+		if (!gm || !gmlast) {
+			continue;
+		}
+		if (gm->begin() == gm->end()) {
+			// empty measure
+			return;
+		}
+		GridSlice *firstit = *(gm->begin());
+		HumNum starttime = firstit->getTimestamp();
+		for (auto it = gm->begin(); it != gm->end(); it++) {
+			HumNum time2 = (*it)->getTimestamp();
+			if (time2 > starttime) {
+				break;
+			}
+			if (!(*it)->isGlobalComment()) {
+				continue;
+			}
+			HTp token = (*it)->at(0)->at(0)->at(0)->getToken();
+			if (!token) {
+				continue;
+			}
+			if ((*token == "!!linebreak:original") ||
+			    (*token == "!!pagebreak:original")) {
+				GridSlice *swapper = *it;
+				gm->erase(it);
+				gmlast->push_back(swapper);
+				// there can be only one break, so quit the loop now.
+				break;
+			}
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::needsAboveBelowKernRdf -- Function name says it all.
+// Tool_musicxml2hum::cleanupMeasures --
+//     Also add barlines here (keeping track of the
+//     duration of each measure).
 //
 
-bool Tool_musedata2hum::needsAboveBelowKernRdf(void) {
-	return m_aboveBelowKernRdf;
+void Tool_musicxml2hum::cleanupMeasures(HumdrumFile& outfile,
+		vector<HLp> measures) {
+
+   HumdrumToken* token;
+	for (int i=0; i<outfile.getLineCount(); i++) {
+		if (!outfile[i].isBarline()) {
+			continue;
+		}
+		if (!outfile[i+1].isInterpretation()) {
+			int fieldcount = outfile[i+1].getFieldCount();
+			for (int j=1; j<fieldcount; j++) {
+				token = new HumdrumToken("=");
+				outfile[i].appendToken(token);
+			}
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::addTextDirection --
+// Tool_musicxml2hum::insertSingleMeasure --
 //
 
-void Tool_musedata2hum::addTextDirection(GridMeasure* gm, int part, int staff,
-		MuseRecord& mr, HumNum timestamp) {
+void Tool_musicxml2hum::insertSingleMeasure(HumdrumFile& outfile) {
+	HLp line = new HumdrumLine;
+	HumdrumToken* token;
+	token = new HumdrumToken("=");
+	line->appendToken(token);
+	line->createLineFromTokens();
+	outfile.appendLine(line);
+}
 
-	if (!mr.isTextDirection()) {
-		return;
-	}
-	string text = mr.getTextDirection();
-	if (text == "") {
-		// no text direction to process
-		return;
-	}
-	HumRegex hre;
-	hre.replaceDestructive(text, "&colon;", ":", "g");
-	string output = "!LO:TX";
-	output += ":b";   // text below (figure out above cases)
-	output += ":t=";
-	output += text;
 
-	// add staff index later
-	gm->addLayoutParameter(NULL, part, output);
 
+//////////////////////////////
+//
+// Tool_musicxml2hum::insertAllToken --
+//
+
+void Tool_musicxml2hum::insertAllToken(HumdrumFile& outfile,
+		vector<MxmlPart>& partdata, const string& common) {
+
+	HLp line = new HumdrumLine;
+	HumdrumToken* token;
 
+	int i, j;
+	for (i=0; i<(int)partdata.size(); i++) {
+		for (j=0; j<(int)partdata[i].getStaffCount(); j++) {
+			token = new HumdrumToken(common);
+			line->appendToken(token);
+		}
+		for (j=0; j<(int)partdata[i].getVerseCount(); j++) {
+			token = new HumdrumToken(common);
+			line->appendToken(token);
+		}
+	}
+	outfile.appendLine(line);
 }
 
 
+
 //////////////////////////////
 //
-// Tool_musedata2hum::addFiguredHarmony --
+// Tool_musicxml2hum::insertMeasure --
 //
 
-void Tool_musedata2hum::addFiguredHarmony(MuseRecord& mr, GridMeasure* gm,
-		HumNum timestamp, int part, int maxstaff) {
-	string fh = mr.getFigureString();
-	int figureDuration = mr.getFigureDuration();
-	fh = Convert::museFiguredBassToKernFiguredBass(fh);
-	if (m_figureOffset > 0) {
-		if (m_quarterDivisions > 0) {
-			HumNum offset(m_figureOffset, m_quarterDivisions);
-			timestamp + offset;
+bool Tool_musicxml2hum::insertMeasure(HumGrid& outdata, int mnum,
+		vector<MxmlPart>& partdata, vector<int> partstaves) {
+
+	GridMeasure* gm = outdata.addMeasureToBack();
+
+	MxmlMeasure* xmeasure;
+	vector<MxmlMeasure*> measuredata;
+	vector<vector<SimultaneousEvents>* > sevents;
+	int i;
+
+	for (i=0; i<(int)partdata.size(); i++) {
+		xmeasure = partdata[i].getMeasure(mnum);
+		measuredata.push_back(xmeasure);
+		if (i==0) {
+			gm->setDuration(partdata[i].getMeasure(mnum)->getDuration());
+			gm->setTimestamp(partdata[i].getMeasure(mnum)->getTimestamp());
+			gm->setTimeSigDur(partdata[i].getMeasure(mnum)->getTimeSigDur());
+		}
+		checkForDummyRests(xmeasure);
+		sevents.push_back(xmeasure->getSortedEvents());
+		if (i == 0) {
+			// only checking measure style of first barline
+			gm->setBarStyle(xmeasure->getBarStyle());
 		}
-	}
-	if (fh.find(":") == string::npos) {
-		HTp fhtok = new HumdrumToken(fh);
-		m_lastfigure = fhtok;
-		gm->addFiguredBass(fhtok, timestamp, part, maxstaff);
-		m_figureOffset += figureDuration;
-		return;
 	}
 
-	if (!m_lastfigure) {
-		HTp fhtok = new HumdrumToken(fh);
-		m_lastfigure = fhtok;
-		gm->addFiguredBass(fhtok, timestamp, part, maxstaff);
-		m_figureOffset += figureDuration;
-		return;
-	}
+	vector<HumNum> curtime(partdata.size());
+	vector<HumNum> measuredurs(partdata.size());
+	vector<int> curindex(partdata.size(), 0); // assuming data in a measure...
+	HumNum nexttime = -1;
 
-	// For now assuming only one line extension needs to be transferred.
+	vector<vector<MxmlEvent*>> endingDirections(partdata.size());
 
-	// Has a line extension that should be moved to the previous token:
-	int position = 0;
-	int colpos = -1;
-	if (fh[0] == ':') {
-		colpos = 0;
-	} else {
-		for (int i=1; i<(int)fh.size(); i++) {
-			if (isspace(fh[i]) && !isspace(fh[i-1])) {
-				position++;
-			}
-			if (fh[i] == ':') {
-				colpos = i;
+	HumNum tsdur;
+	for (i=0; i<(int)curtime.size(); i++) {
+		tsdur = measuredata[i]->getTimeSigDur();
+		if ((tsdur == 0) && (i > 0)) {
+			tsdur = measuredata[i-1]->getTimeSigDur();
+			measuredata[i]->setTimeSigDur(tsdur);
+		}
+
+		// Keep track of hairpin endings that should be attached
+		// the the previous note (and doubling the ending marker
+		// to indicate that the timestamp of the ending is at the
+		// end rather than the start of the note.
+		vector<MxmlEvent*>& events = measuredata[i]->getEventList();
+		xml_node hairpin = xml_node(NULL);
+		for (int j=(int)events.size() - 1; j >= 0; j--) {
+			if (events[j]->getElementName() == "note") {
+				if (hairpin) {
+					events[j]->setHairpinEnding(hairpin);
+					hairpin = xml_node(NULL);
+				}
 				break;
+			} else if (events[j]->getElementName() == "direction") {
+				stringstream ss;
+				ss.str("");
+				events[j]->getNode().print(ss);
+				if (ss.str().find("wedge") != string::npos) {
+					if (ss.str().find("stop") != string::npos) {
+						hairpin = events[j]->getNode();
+					}
+				}
 			}
 		}
-	}
 
-	string lastfh = m_lastfigure->getText();
-	vector<string> pieces;
-	int state = 0;
-	for (int i=0; i<(int)lastfh.size(); i++) {
-		if (state) {
-			if (isspace(lastfh[i])) {
-				state = 0;
-			} else {
-				pieces.back() += lastfh[i];
+		if (VoiceDebugQ) {
+			for (int j=0; j<(int)events.size(); j++) {
+				cerr << "!!ELEMENT: ";
+				cerr << "\tTIME:  " << events[j]->getStartTime();
+				cerr << "\tSTi:   " << events[j]->getStaffIndex();
+				cerr << "\tVi:    " << events[j]->getVoiceIndex();
+				cerr << "\tTS:    " << events[j]->getStartTime();
+				cerr << "\tDUR:   " << events[j]->getDuration();
+				cerr << "\tPITCH: " << events[j]->getKernPitch();
+				cerr << "\tNAME:  " << events[j]->getElementName();
+				cerr << endl;
 			}
+			cerr << "======================================" << endl;
+		}
+		if (!(*sevents[i]).empty()) {
+			curtime[i] = (*sevents[i])[curindex[i]].starttime;
 		} else {
-			if (isspace(lastfh[i])) {
-				// do nothing
-			} else {
-				pieces.resize(pieces.size()+1);
-				pieces.back() += lastfh[i];
-				state = 1;
-			}
+			curtime[i] = tsdur;
 		}
-	}
-
-	if (pieces.empty() || (position >= (int)pieces.size())) {
-		HTp fhtok = new HumdrumToken(fh);
-		m_lastfigure = fhtok;
-		gm->addFiguredBass(fhtok, timestamp, part, maxstaff);
-		m_figureOffset += figureDuration;
-		return;
-	}
-
-	pieces[position] += ':';
-	string oldtok;
-	for (int i=0; i<(int)pieces.size(); i++) {
-		oldtok += pieces[i];
-		if (i<(int)pieces.size() - 1) {
-			oldtok += ' ';
+		if (nexttime < 0) {
+			nexttime = curtime[i];
+		} else if (curtime[i] < nexttime) {
+			nexttime = curtime[i];
 		}
+		measuredurs[i] = measuredata[i]->getDuration();
 	}
 
-	m_lastfigure->setText(oldtok);
+	bool allend = false;
+	vector<SimultaneousEvents*> nowevents;
+	vector<int> nowparts;
+	bool status = true;
 
-	fh.erase(colpos, 1);
-	HTp newtok = new HumdrumToken(fh);
-	m_lastfigure = newtok;
-	gm->addFiguredBass(newtok, timestamp, part, maxstaff);
-	m_figureOffset += figureDuration;
-}
+	HumNum processtime = nexttime;
+	while (!allend) {
+		nowevents.resize(0);
+		nowparts.resize(0);
+		allend = true;
+		processtime = nexttime;
+		nexttime = -1;
+		for (i = (int)partdata.size()-1; i >= 0; i--) {
+			if (curindex[i] >= (int)(*sevents[i]).size()) {
+				continue;
+			}
 
+			if ((*sevents[i])[curindex[i]].starttime == processtime) {
+				SimultaneousEvents* thing = &(*sevents[i])[curindex[i]];
+				nowevents.push_back(thing);
+				nowparts.push_back(i);
+				curindex[i]++;
+			}
 
+			if (curindex[i] < (int)(*sevents[i]).size()) {
+				allend = false;
+				if ((nexttime < 0) ||
+						((*sevents[i])[curindex[i]].starttime < nexttime)) {
+					nexttime = (*sevents[i])[curindex[i]].starttime;
+				}
+			}
+		}
+		status &= convertNowEvents(outdata.back(),
+				nowevents, nowparts, processtime, partdata, partstaves);
 
-//////////////////////////////
-//
-// Tool_musedata2hum::addLyrics --
-//
+		// Remove all figured bass numbers for this nowtime so that they are not
+		// accidentally displayed in the next nowtime, which can currently
+		// happen if there are no nonzerodur events in the same part
+		for (int i=0; i<(int)m_current_figured_bass.size(); i++) {
+			m_current_figured_bass[i].clear();
+		}
+	}
 
-void Tool_musedata2hum::addLyrics(GridSlice* slice, int part, int staff, MuseRecord& mr) {
-	int versecount = mr.getVerseCount();
-	if (versecount == 0) {
-		return;
+	if (offsetHarmony.size() > 0) {
+		insertOffsetHarmonyIntoMeasure(outdata.back());
 	}
-	for (int i=0; i<versecount; i++) {
-		string verse = mr.getVerseUtf8(i);
-		slice->at(part)->at(staff)->setVerse(i, verse);
+	if (m_offsetFiguredBass.size() > 0) {
+		insertOffsetFiguredBassIntoMeasure(outdata.back());
 	}
-	slice->reportVerseCount(part, staff, versecount);
+	return status;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::addNoteDynamics -- only one contiguous dynamic allowed
+// Tool_musicxml2hum::insertOffsetFiguredBassIntoMeasure --
 //
 
-void Tool_musedata2hum::addNoteDynamics(GridSlice* slice, int part,
-		MuseRecord& mr) {
-	string notations = mr.getAdditionalNotationsField();
-	vector<string> dynamics(1);
-	vector<int> column(1, -1);
-	int state = 0;
-	for (int i=0; i<(int)notations.size(); i++) {
-		if (state) {
-			switch (notations[i]) {
-				case 'p':
-				case 'm':
-				case 'f':
-					dynamics.back() += notations[i];
-					break;
-				default:
-					state = 0;
-					dynamics.resize(dynamics.size() + 1);
-			}
-		} else {
-			switch (notations[i]) {
-				case 'p':
-				case 'm':
-				case 'f':
-					state = 1;
-					dynamics.back() = notations[i];
-					column.back() = i;
-					break;
-			}
-		}
+void Tool_musicxml2hum::insertOffsetFiguredBassIntoMeasure(GridMeasure* gm) {
+	if (m_offsetFiguredBass.empty()) {
+		return;
 	}
 
-	bool setdynamics = false;
-	vector<string> ps;
-	HumRegex hre;
-	for (int i=0; i<(int)dynamics.size(); i++) {
-		if (dynamics[i].empty()) {
+	bool beginQ = true;
+	for (auto it = gm->begin(); it != gm->end(); ++it) {
+		GridSlice* gs = *it;
+		if (!gs->isNoteSlice()) {
+			// Only attached harmony to data lines.
 			continue;
 		}
-		mr.getPrintSuggestions(ps, column[i]+32);
-		if (ps.size() > 0) {
-			cerr << "\tPRINT SUGGESTION: " << ps[0] << endl;
-			// only checking the first entry (first parameter):
-			if (hre.search(ps[0], "Y(-?\\d+)")) {
-				int y = hre.getMatchInt(1);
-				cerr << "Y = " << y << endl;
+		HumNum timestamp = gs->getTimestamp();
+		for (int i=0; i<(int)m_offsetFiguredBass.size(); i++) {
+			if (m_offsetFiguredBass[i].token == NULL) {
+				continue;
+ 			}
+			if (m_offsetFiguredBass[i].timestamp == timestamp) {
+				// this is the slice to insert the harmony
+				gs->at(m_offsetFiguredBass[i].partindex)->setFiguredBass(m_offsetFiguredBass[i].token);
+				m_offsetFiguredBass[i].token = NULL;
+			} else if (m_offsetFiguredBass[i].timestamp < timestamp) {
+				if (beginQ) {
+					cerr << "Error: Cannot insert harmony " << m_offsetFiguredBass[i].token
+					     << " at timestamp " << m_offsetFiguredBass[i].timestamp
+					     << " since first timestamp in measure is " << timestamp << endl;
+				} else {
+					m_forceRecipQ = true;
+					// go back to previous note line and insert
+					// new slice to store the harmony token
+					auto tempit = it;
+					tempit--;
+					while (tempit != gm->end()) {
+						if ((*tempit)->getTimestamp() == (*it)->getTimestamp()) {
+							tempit--;
+							continue;
+						}
+						int partcount = (int)(*tempit)->size();
+						tempit++;
+						GridSlice* newgs = new GridSlice(gm, m_offsetFiguredBass[i].timestamp,
+								SliceType::Notes, partcount);
+						newgs->at(m_offsetFiguredBass[i].partindex)->setFiguredBass(m_offsetFiguredBass[i].token);
+						gm->insert(tempit, newgs);
+						m_offsetFiguredBass[i].token = NULL;
+						break;
+					}
+				}
 			}
 		}
-
-		slice->at(part)->setDynamics(dynamics[i]);
-		setdynamics = true;
-		break;  // only one dynamic allowed (at least for now)
-	}
-
-	if (setdynamics) {
-		HumGrid* grid = slice->getOwner();
-		if (grid) {
-			grid->setDynamicsPresent(part);
-		}
+		beginQ = false;
 	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_musedata2hum::setTimeSigDurInfo --
-//
-
-void Tool_musedata2hum::setTimeSigDurInfo(const string& ktimesig) {
-	HumRegex hre;
-	if (hre.search(ktimesig, "(\\d+)/(\\d+)")) {
-		int top = hre.getMatchInt(1);
-		int bot = hre.getMatchInt(2);
-		HumNum value = 1;
-		value /= bot;
-		value *= top;
-		value.invert();
-		value *= 4;  // convert from whole notes to quarter notes
-		m_timesigdur = value;
+	// If there are still valid harmonies in the input list, apppend
+	// them to the end of the measure.
+	for (int i=0; i<(int)m_offsetFiguredBass.size(); i++) {
+		if (m_offsetFiguredBass[i].token == NULL) {
+			continue;
+ 		}
+		m_forceRecipQ = true;
+		int partcount = (int)gm->back()->size();
+		GridSlice* newgs = new GridSlice(gm, m_offsetFiguredBass[i].timestamp,
+				SliceType::Notes, partcount);
+		newgs->at(m_offsetFiguredBass[i].partindex)->setFiguredBass(m_offsetFiguredBass[i].token);
+		gm->insert(gm->end(), newgs);
+		m_offsetFiguredBass[i].token = NULL;
 	}
+	m_offsetFiguredBass.clear();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::getMeasure --  Could be imporoved by NlogN search.
+// Tool_musicxml2hum::insertOffsetHarmonyIntoMeasure --
 //
 
-GridMeasure* Tool_musedata2hum::getMeasure(HumGrid& outdata, HumNum starttime) {
-	for (int i=0; i<(int)outdata.size(); i++) {
-		if (outdata[i]->getTimestamp() == starttime) {
-			return outdata[i];
+void Tool_musicxml2hum::insertOffsetHarmonyIntoMeasure(GridMeasure* gm) {
+	if (offsetHarmony.empty()) {
+		return;
+	}
+	// the offsetHarmony list should probably be time sorted first, and then
+	// iterate through the slices once.  But there should not be many offset
+	bool beginQ = true;
+	for (auto it = gm->begin(); it != gm->end(); ++it) {
+		GridSlice* gs = *it;
+		if (!gs->isNoteSlice()) {
+			// Only attached harmony to data lines.
+			continue;
+		}
+		HumNum timestamp = gs->getTimestamp();
+		for (int i=0; i<(int)offsetHarmony.size(); i++) {
+			if (offsetHarmony[i].token == NULL) {
+				continue;
+ 			}
+			if (offsetHarmony[i].timestamp == timestamp) {
+				// this is the slice to insert the harmony
+				gs->at(offsetHarmony[i].partindex)->setHarmony(offsetHarmony[i].token);
+				offsetHarmony[i].token = NULL;
+			} else if (offsetHarmony[i].timestamp < timestamp) {
+				if (beginQ) {
+					cerr << "Error: Cannot insert harmony " << offsetHarmony[i].token
+					     << " at timestamp " << offsetHarmony[i].timestamp
+					     << " since first timestamp in measure is " << timestamp << endl;
+				} else {
+					m_forceRecipQ = true;
+					// go back to previous note line and insert
+					// new slice to store the harmony token
+					auto tempit = it;
+					tempit--;
+					while (tempit != gm->end()) {
+						if ((*tempit)->getTimestamp() == (*it)->getTimestamp()) {
+							tempit--;
+							continue;
+						}
+						int partcount = (int)(*tempit)->size();
+						tempit++;
+						GridSlice* newgs = new GridSlice(gm, offsetHarmony[i].timestamp,
+								SliceType::Notes, partcount);
+						newgs->at(offsetHarmony[i].partindex)->setHarmony(offsetHarmony[i].token);
+						gm->insert(tempit, newgs);
+						offsetHarmony[i].token = NULL;
+						break;
+					}
+				}
+			}
 		}
+		beginQ = false;
 	}
-	// Did not find measure in data, so append to end of list.
-	// Assuming that unknown measures are at a later timestamp
-	// than those in current list, but should fix this later perhaps.
-	GridMeasure* gm = new GridMeasure(&outdata);
-	outdata.push_back(gm);
-	return gm;
+	// If there are still valid harmonies in the input list, apppend
+	// them to the end of the measure.
+	for (int i=0; i<(int)offsetHarmony.size(); i++) {
+		if (offsetHarmony[i].token == NULL) {
+			continue;
+ 		}
+		m_forceRecipQ = true;
+		int partcount = (int)gm->back()->size();
+		GridSlice* newgs = new GridSlice(gm, offsetHarmony[i].timestamp,
+				SliceType::Notes, partcount);
+		newgs->at(offsetHarmony[i].partindex)->setHarmony(offsetHarmony[i].token);
+		gm->insert(gm->end(), newgs);
+		offsetHarmony[i].token = NULL;
+	}
+	offsetHarmony.clear();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musedata2hum::setInitialOmd --
+// Tool_musicxml2hum::checkForDummyRests --
 //
 
-void Tool_musedata2hum::setInitialOmd(const string& omd) {
-	m_omd = omd;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_musedata2hum::cleanString --
-//
+void Tool_musicxml2hum::checkForDummyRests(MxmlMeasure* measure) {
+	vector<MxmlEvent*>& events = measure->getEventList();
 
-string Tool_musedata2hum::cleanString(const string& input) {
-	return MuseData::cleanString(input);
-}
+	MxmlPart* owner = measure->getOwner();
+	int maxstaff = owner->getStaffCount();
+	vector<vector<int> > itemcounts(maxstaff);
+	for (int i=0; i<(int)itemcounts.size(); i++) {
+		itemcounts[i].resize(1);
+		itemcounts[i][0] = 0;
+	}
 
+	for (int i=0; i<(int)events.size(); i++) {
+		if (!nodeType(events[i]->getNode(), "note")) {
+			// only counting notes/(rests) for now.  <forward> may
+			// need to be counted.
+			continue;
+		}
+     	int voiceindex = events[i]->getVoiceIndex();
+		int staffindex = events[i]->getStaffIndex();
 
+		if (voiceindex < 0) {
+			continue;
+		}
+		if (staffindex < 0) {
+			continue;
+		}
 
+		if (staffindex >= (int)itemcounts.size()) {
+			itemcounts.resize(staffindex+1);
+		}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::Tool_musicxml2hum --
-//
+		if (voiceindex >= (int)itemcounts[staffindex].size()) {
+			int oldsize = (int)itemcounts[staffindex].size();
+			int newsize = voiceindex + 1;
+			itemcounts[staffindex].resize(newsize);
+			for (int j=oldsize; j<newsize; j++) {
+					  itemcounts[staffindex][j] = 0;
+			}
+		}
+		itemcounts[staffindex][voiceindex]++;
+  	}
 
-Tool_musicxml2hum::Tool_musicxml2hum(void) {
-	// Options& options = m_options;
-	// options.define("k|kern=b","display corresponding **kern data");
+	bool dummy = false;
+	for (int i=0; i<(int)itemcounts.size(); i++) {
+		for (int j=0; j<(int)itemcounts[i].size(); j++) {
+			if (itemcounts[i][j]) {
+				continue;
+			}
+			HumNum mdur = measure->getDuration();
+			HumNum starttime = measure->getStartTime();
+      	measure->addDummyRest(starttime, mdur, i, j);
+			measure->forceLastInvisible();
+			dummy = true;
+		}
+	}
 
-	define("r|recip=b", "output **recip spine");
-	define("s|stems=b", "include stems in output");
+	if (dummy) {
+		measure->sortEvents();
+	}
 
-	VoiceDebugQ = false;
-	DebugQ = false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::convert -- Convert a MusicXML file into
-//     Humdrum content.
+// Tool_musicxml2hum::convertNowEvents --
 //
 
-bool Tool_musicxml2hum::convertFile(ostream& out, const char* filename) {
-	xml_document doc;
-	auto result = doc.load_file(filename);
-	if (!result) {
-		cerr << "\nXML file [" << filename << "] has syntax errors\n";
-		cerr << "Error description:\t" << result.description() << "\n";
-		cerr << "Error offset:\t" << result.offset << "\n\n";
-		exit(1);
-	}
-
-	return convert(out, doc);
-}
-
-
-bool Tool_musicxml2hum::convert(ostream& out, istream& input) {
-	string s(istreambuf_iterator<char>(input), {});
-	return convert(out, s.c_str());
-}
-
+bool Tool_musicxml2hum::convertNowEvents(GridMeasure* outdata,
+		vector<SimultaneousEvents*>& nowevents, vector<int>& nowparts,
+		HumNum nowtime, vector<MxmlPart>& partdata, vector<int>& partstaves) {
 
-bool Tool_musicxml2hum::convert(ostream& out, const char* input) {
-	xml_document doc;
-	auto result = doc.load_string(input);
-	if (!result) {
-		cout << "\nXML content has syntax errors\n";
-		cout << "Error description:\t" << result.description() << "\n";
-		cout << "Error offset:\t" << result.offset << "\n\n";
-		exit(1);
+	if (nowevents.size() == 0) {
+		// cout << "NOW EVENTS ARE EMPTY" << endl;
+		return true;
 	}
 
-	return convert(out, doc);
-}
-
-
+	//if (0 && VoiceDebugQ) {
+	//	for (int j=0; j<(int)nowevents.size(); j++) {
+	//		vector<MxmlEvent*> nz = nowevents[j]->nonzerodur;
+	//		for (int i=0; i<(int)nz.size(); i++) {
+	//			cerr << "NOWEVENT NZ NAME: " << nz[i]->getElementName()
+	//			     << "<\t" << nz[i]->getKernPitch() << endl;
+	//		}
+	//	}
+	//}
 
-bool Tool_musicxml2hum::convert(ostream& out, xml_document& doc) {
-	initialize();
+	appendZeroEvents(outdata, nowevents, nowtime, partdata);
 
-	bool status = true; // for keeping track of problems in conversion process.
+	bool hasNonZeroDurElements = false;
+	for (const SimultaneousEvents* event : nowevents) {
+		if (event->nonzerodur.size() != 0) {
+			hasNonZeroDurElements = true;
+			break;
+		}
+	}
+	if (!hasNonZeroDurElements) {
+		// no duration events (should be a terminal barline)
+		// ignore and deal with in calling function.
+		return true;
+	}
 
-	setSoftwareInfo(doc);
-	vector<string> partids;            // list of part IDs
-	map<string, xml_node> partinfo;    // mapping if IDs to score-part elements
-	map<string, xml_node> partcontent; // mapping of IDs to part elements
+	appendNonZeroEvents(outdata, nowevents, nowtime, partdata);
 
-	getPartInfo(partinfo, partids, doc);
-	m_used_hairpins.resize(partinfo.size());
+	handleFiguredBassWithoutNonZeroEvent(nowevents, nowtime);
 
-	m_current_dynamic.resize(partids.size());
-	m_current_brackets.resize(partids.size());
-	m_current_figured_bass.resize(partids.size());
-	m_stop_char.resize(partids.size(), "[");
+	return true;
+}
 
-	getPartContent(partcontent, partids, doc);
-	vector<MxmlPart> partdata;
-	partdata.resize(partids.size());
-	m_last_ottava_direction.resize(partids.size());
 
-	fillPartData(partdata, partids, partinfo, partcontent);
 
-	// for debugging:
-	//printPartInfo(partids, partinfo, partcontent, partdata);
+/////////////////////////////
+//
+// Tool_musicxml2hum::handleFiguredBassWithoutNonZeroEvent --
+//
 
-	m_maxstaff = 0;
-	// check the voice info
-	for (int i=0; i<(int)partdata.size(); i++) {
-		partdata[i].prepareVoiceMapping();
-		m_maxstaff += partdata[i].getStaffCount();
-		// for debugging:
-		if (VoiceDebugQ) {
-			partdata[i].printStaffVoiceInfo();
+void Tool_musicxml2hum::handleFiguredBassWithoutNonZeroEvent(vector<SimultaneousEvents*>& nowevents, HumNum nowtime) {
+	vector<int> nonZeroParts;
+	vector<MxmlEvent> floatingFiguredBass;
+	for (const SimultaneousEvents* sevent : nowevents) {
+		for (MxmlEvent* mxmlEvent : sevent->nonzerodur) {
+			nonZeroParts.push_back(mxmlEvent->getPartIndex());
 		}
-	}
-
-	// re-index voices to disallow empty intermediate voices.
-	reindexVoices(partdata);
-
-	HumGrid outdata;
-	status &= stitchParts(outdata, partids, partinfo, partcontent, partdata);
-
-	if (outdata.size() > 2) {
-		if (outdata.at(0)->getDuration() == 0) {
-			while (!outdata.at(0)->empty()) {
-				outdata.at(1)->push_front(outdata.at(0)->back());
-				outdata.at(0)->pop_back();
+		for (MxmlEvent* mxmlEvent : sevent->zerodur) {
+			if ("figured-bass" == mxmlEvent->getElementName()) {
+				if (std::find(nonZeroParts.begin(), nonZeroParts.end(), mxmlEvent->getPartIndex()) == nonZeroParts.end()) {
+					// cerr << mxmlEvent->getNode() << "\n";
+					string fstring = getFiguredBassString(mxmlEvent->getNode());
+					HTp ftok = new HumdrumToken(fstring);
+					MusicXmlFiguredBassInfo finfo;
+					finfo.timestamp = nowtime;
+					finfo.partindex = mxmlEvent->getPartIndex();
+					finfo.token = ftok;
+					m_offsetFiguredBass.push_back(finfo);
+					// cerr << "ADD FLOATING FB NUM " << fstring << " " << nowtime << "\n";
+				}
 			}
-			outdata.deleteMeasure(0);
 		}
 	}
+}
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		m_hasOrnamentsQ |= partdata[i].hasOrnaments();
-	}
 
-	outdata.removeRedundantClefChanges();
-	outdata.removeSibeliusIncipit();
-	m_systemDecoration = getSystemDecoration(doc, outdata, partids);
 
-	// tranfer verse counts from parts/staves to HumGrid:
-	// should also do part verse counts here (-1 staffindex).
-	int versecount;
-	for (int p=0; p<(int)partdata.size(); p++) {
-		for (int s=0; s<partdata[p].getStaffCount(); s++) {
-			versecount = partdata[p].getVerseCount(s);
-			outdata.setVerseCount(p, s, versecount);
-		}
-	}
+/////////////////////////////
+//
+// Tool_musicxml2hum::appendNonZeroEvents --
+//
 
-	// transfer harmony counts from parts to HumGrid:
-	for (int p=0; p<(int)partdata.size(); p++) {
-		int harmonyCount = partdata[p].getHarmonyCount();
-		outdata.setHarmonyCount(p, harmonyCount);
-	}
+void Tool_musicxml2hum::appendNonZeroEvents(GridMeasure* outdata,
+		vector<SimultaneousEvents*>& nowevents, HumNum nowtime,
+		vector<MxmlPart>& partdata) {
 
-	// transfer dynamics boolean for part to HumGrid
-	for (int p = 0; p<(int)partdata.size(); p++) {
-		bool dynstate = partdata[p].hasDynamics();
-		if (dynstate) {
-			outdata.setDynamicsPresent(p);
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+			SliceType::Notes);
+	if (outdata->empty()) {
+		outdata->push_back(slice);
+	} else {
+		HumNum lasttime = outdata->back()->getTimestamp();
+		if (nowtime >= lasttime) {
+			outdata->push_back(slice);
+		} else {
+			// travel backwards in the measure until the correct
+			// time position is found.
+			auto it = outdata->rbegin();
+			while (it != outdata->rend()) {
+				lasttime = (*it)->getTimestamp();
+				if (nowtime >= lasttime) {
+					outdata->insert(it.base(), slice);
+					break;
+				}
+				it++;
+			}
 		}
 	}
+	slice->initializePartStaves(partdata);
 
-	// transfer figured bass boolean for part to HumGrid
-	for (int p=0; p<(int)partdata.size(); p++) {
-		bool fbstate = partdata[p].hasFiguredBass();
-		if (fbstate) {
-			outdata.setFiguredBassPresent(p);
-			// break;
+	for (int i=0; i<(int)nowevents.size(); i++) {
+		vector<MxmlEvent*>& events = nowevents[i]->nonzerodur;
+		for (int j=0; j<(int)events.size(); j++) {
+			addEvent(slice, outdata, events[j], nowtime);
 		}
 	}
+}
 
-	if (m_recipQ || m_forceRecipQ) {
-		outdata.enableRecipSpine();
-	}
-
-	outdata.buildSingleList();
-	outdata.expandLocalCommentLayers();
-
-	// set the duration of the last slice
 
-	HumdrumFile outfile;
-	outdata.transferTokens(outfile);
 
-	addHeaderRecords(outfile, doc);
-	addFooterRecords(outfile, doc);
+//////////////////////////////
+//
+// Tool_musicxml2hum::addEvent -- Add a note or rest.
+//
 
-	Tool_ruthfix ruthfix;
-	ruthfix.run(outfile);
+void Tool_musicxml2hum::addEvent(GridSlice* slice, GridMeasure* outdata, MxmlEvent* event,
+		HumNum nowtime) {
+	int partindex;  // which part the event occurs in
+	int staffindex; // which staff the event occurs in (need to fix)
+	int voiceindex; // which voice the event occurs in (use for staff)
 
-	addMeasureOneNumber(outfile);
+	partindex  = event->getPartIndex();
+	staffindex = event->getStaffIndex();
+	voiceindex = event->getVoiceIndex();
 
-	// Maybe implement barnum tool and apply here based on options.
+	string recip;
+	string pitch;
+	string prefix;
+	string postfix;
+	bool invisible = false;
+	bool primarynote = true;
+	vector<int> slurdirs;
 
-	Tool_chord chord;
-	chord.run(outfile);
+	if (!event->isFloating()) {
+		recip     = event->getRecip();
+		HumRegex hre;
+		if (hre.search(recip, "(\\d+)%(\\d+)(\\.*)")) {
+			int first = hre.getMatchInt(1);
+			int second = hre.getMatchInt(2);
+			string dots = hre.getMatch(3);
+			if (dots.empty()) {
+				if ((first == 1) && (second == 2)) {
+					hre.replaceDestructive(recip, "0", "1%2");
+				}
+				if ((first == 1) && (second == 4)) {
+					hre.replaceDestructive(recip, "00", "1%4");
+				}
+				if ((first == 1) && (second == 3)) {
+					hre.replaceDestructive(recip, "0.", "1%3");
+				}
+				if ((first == 2) && (second == 3)) {
+					hre.replaceDestructive(recip, "1.", "2%3");
+				}
+			} else {
+				if ((first == 1) && (second == 2)) {
+					string original = "1%2" + dots;
+					string replacement = "0" + dots;
+					hre.replaceDestructive(recip, replacement, original);
+				}
+			}
+		}
 
-	if (m_hasOrnamentsQ) {
-		Tool_trillspell trillspell;
-		trillspell.run(outfile);
-	}
+		pitch     = event->getKernPitch();
+		prefix    = event->getPrefixNoteInfo();
+		postfix   = event->getPostfixNoteInfo(primarynote, recip);
+		if (postfix.find("@") != string::npos) {
+			m_hasTremoloQ = true;
+		}
+		bool grace     = event->isGrace();
+		int slurstarts = event->hasSlurStart(slurdirs);
+		int slurstops = event->hasSlurStop();
 
-	if (m_hasTremoloQ) {
-		Tool_tremolo tremolo;
-		tremolo.run(outfile);
-	}
+		if (pitch.find('r') != std::string::npos) {
+			string restpitch =  event->getRestPitch();
+			pitch += restpitch;
+		}
 
-	if (m_software == "sibelius") {
-		// Needed at least for Sibelius 19.5/Dolet 6.6 for Sibelius
-		// where grace note groups are not beamed in the MusicXML export.
-		Tool_autobeam gracebeam;
-		vector<string> argv;
-		argv.push_back("autobeam"); // name of program (placeholder)
-		argv.push_back("-g");       // beam adjacent grace notes
-		gracebeam.process(argv);
-		// Need to force a reparsing of the files contents to
-		// analyze strands.  For now just create a temporary
-		// Humdrum file to force the analysis of the strands.
-		stringstream sstream;
-		sstream << outfile;
-		HumdrumFile outfile2;
-		outfile2.readString(sstream.str());
-		gracebeam.run(outfile2);
-		outfile = outfile2;
-	}
+		for (int i=0; i<slurstarts; i++) {
+			prefix.insert(0, "(");
+		// Ignoring slur direction for now.
+		// if (slurstart) {
+		// 	prefix.insert(0, "(");
+		// 	if (slurdir) {
+		// 		if (slurdir > 0) {
+		// 			prefix.insert(1, ">");
+		// 			m_slurabove++;
+		// 		} else if (slurdir < 0) {
+		// 			prefix.insert(1, "<");
+		// 			m_slurbelow++;
+		// 		}
+		// 	}
+		// }
+		}
+		for (int i=0; i<slurstops; i++) {
+			postfix.push_back(')');
+		}
+		//if (slurstop) {
+		//	postfix.push_back(')');
+		//}
 
-	if (m_hasTransposition) {
-		Tool_transpose transpose;
-		vector<string> argv;
-		argv.push_back("transpose"); // name of program (placeholder)
-		argv.push_back("-C");        // transpose to concert pitch
-		transpose.process(argv);
-		stringstream sstream;
-		sstream << outfile;
-		HumdrumFile outfile2;
-		outfile2.readString(sstream.str());
-		transpose.run(outfile2);
-		if (transpose.hasHumdrumText()) {
-			stringstream ss;
-			transpose.getHumdrumText(ss);
-			outfile.readString(ss.str());
-			printResult(out, outfile);
+		invisible = isInvisible(event);
+		if (event->isInvisible()) {
+			invisible = true;
 		}
-	} else {
-		for (int i=0; i<outfile.getLineCount(); i++) {
-			outfile[i].createLineFromTokens();
+
+		if (grace) {
+			HumNum modification;
+			HumNum dur = event->getEmbeddedDuration(modification, event->getNode()) / 4;
+			if (dur.getNumerator() == 1) {
+				recip = to_string(dur.getDenominator()) + "q";
+			} else {
+				recip = "q";
+			}
+			if (!event->hasGraceSlash()) {
+				recip += "q";
+			}
 		}
-		printResult(out, outfile);
 	}
 
-	// add RDFs
-	if (m_slurabove || m_staffabove) {
-		out << "!!!RDF**kern: > = above" << endl;
-	}
-	if (m_slurbelow || m_staffbelow) {
-		out << "!!!RDF**kern: < = below" << endl;
+	if (event->getCrossStaffOffset() > 0) {
+		m_staffbelow = true;
+	} else if (event->getCrossStaffOffset() < 0) {
+		m_staffabove = true;
 	}
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		if (partdata[i].hasEditorialAccidental()) {
-			out << "!!!RDF**kern: i = editorial accidental" << endl;
-			break;
+	stringstream ss;
+	if (event->isFloating()) {
+		ss << ".";
+		HTp token = new HumdrumToken(ss.str());
+		slice->at(partindex)->at(staffindex)->setTokenLayer(voiceindex, token,
+			event->getDuration());
+	} else {
+		ss << prefix << recip << pitch << postfix;
+		if (invisible) {
+			ss << "yy";
 		}
-	}
-
-	// put the above code in here some time:
-	prepareRdfs(partdata);
-	printRdfs(out);
 
-	return status;
-}
+		// check for chord notes.
+		HTp token;
+		if (event->isChord()) {
+			addSecondaryChordNotes(ss, event, recip);
+			token = new HumdrumToken(ss.str());
+			slice->at(partindex)->at(staffindex)->setTokenLayer(voiceindex, token,
+				event->getDuration());
+		} else {
+			token = new HumdrumToken(ss.str());
+			slice->at(partindex)->at(staffindex)->setTokenLayer(voiceindex, token,
+				event->getDuration());
+		}
+	}
 
+	if (DebugQ) {
+		cerr << "!!TOKEN: " << ss.str();
+		cerr << "\tTS: "    << event->getStartTime();
+		cerr << "\tDUR: "   << event->getDuration();
+		cerr << "\tSTn: "   << event->getStaffNumber();
+		cerr << "\tVn: "    << event->getVoiceNumber();
+		cerr << "\tSTi: "   << event->getStaffIndex();
+		cerr << "\tVi: "    << event->getVoiceIndex();
+		cerr << "\teNAME: " << event->getElementName();
+		cerr << endl;
+	}
 
+	int vcount = addLyrics(slice->at(partindex)->at(staffindex), event);
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::addMeasureOneNumber -- For the first measure if it occurs before
-//    the first data, change = to =1.  Maybe check next measure for a number and
-//    addd one less than that number instead of 1.
-//
+	if (vcount > 0) {
+		event->reportVerseCountToOwner(staffindex, vcount);
+	}
 
-void Tool_musicxml2hum::addMeasureOneNumber(HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			break;
-		}
-		if (!infile[i].isBarline()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		string value = *token;
-		bool hasdigit = false;
-		for (int j=0; j<(int)value.size(); j++) {
-			if (isdigit(value[j])) {
-				hasdigit = true;
-				break;
-			}
-		}
-		if (hasdigit) {
-			break;
-		}
-		// there is no digit on barline, so add one.
+	int hcount = addHarmony(slice->at(partindex), event, nowtime, partindex);
+	if (hcount > 0) {
+		event->reportHarmonyCountToOwner(hcount);
+	}
 
-		string newvalue = "=";
-		if (value.size() < 2) {
-			newvalue += "1";
-		} else if (value[1] != '=') {
-			newvalue += "1";
-			newvalue += value.substr(1);
-		}
-		token->setText(newvalue);
+	int fcount = addFiguredBass(slice->at(partindex), event, nowtime, partindex);
+	if (fcount > 0) {
+		event->reportFiguredBassToOwner();
+	}
 
-		// Add "1" to other spines here:
-		for (int j=1; j<infile[i].getFieldCount(); j++) {
-			HTp tok = infile.token(i, j);
-			tok->setText(newvalue);
+	if (m_current_brackets[partindex].size() > 0) {
+		for (int i=0; i<(int)m_current_brackets[partindex].size(); i++) {
+			event->setBracket(m_current_brackets[partindex].at(i));
 		}
-		break;
+		m_current_brackets[partindex].clear();
+		addBrackets(slice, outdata, event, nowtime, partindex);
 	}
-}
 
+	if (m_current_text.size() > 0) {
+		event->setTexts(m_current_text);
+		m_current_text.clear();
+		addTexts(slice, outdata, event->getPartIndex(), staffindex, voiceindex, event);
+	}
 
+	if (m_current_tempo.size() > 0) {
+		event->setTempos(m_current_tempo);
+		m_current_tempo.clear();
+		addTempos(slice, outdata, event->getPartIndex(), staffindex, voiceindex, event);
+	}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::printResult -- filter out
-//      some item if not necessary:
-//
-// MuseScore calls everything "Piano" by default, so suppress
-// this instrument name if there is only one **kern spine in
-// the file.
-//
+	if (m_current_dynamic[partindex].size()) {
+		// only processing the first dynamic at the current time point for now.
+		// Fix later so that multiple dynamics are handleded in the part at the
+		// same time.  The LO parameters for multiple dynamics will need to be
+		// qualified with "n=#".
+		for (int i=0; i<(int)m_current_dynamic[partindex].size(); i++) {
+			event->setDynamics(m_current_dynamic[partindex][i]);
+			string dparam = getDynamicsParameters(m_current_dynamic[partindex][i]);
 
-void Tool_musicxml2hum::printResult(ostream& out, HumdrumFile& outfile) {
-	vector<HTp> kernspines = outfile.getKernSpineStartList();
-	if (kernspines.size() > 1) {
-		out << outfile;
-	} else {
-		for (int i=0; i<outfile.getLineCount(); i++) {
-			bool isPianoLabel = false;
-			bool isPianoAbbr  = false;
-			bool isPartNum    = false;
-			bool isStaffNum   = false;
-			if (!outfile[i].isInterpretation()) {
-				out << outfile[i] << "\n";
-				continue;
-			}
-			for (int j=0; j<outfile[i].getFieldCount(); j++) {
-				if (*outfile.token(i, j) == "*I\"Piano") {
-					isPianoLabel = true;
-				} else if (*outfile.token(i, j) == "*I'Pno.") {
-					isPianoAbbr = true;
-				} else if (*outfile.token(i, j) == "*staff1") {
-					isStaffNum = true;
-				} else if (*outfile.token(i, j) == "*part1") {
-					isPartNum = true;
+			event->reportDynamicToOwner();
+			addDynamic(slice->at(partindex), event, partindex);
+			if (dparam != "") {
+				// deal with multiple layout entries here...
+				GridMeasure *gm = slice->getMeasure();
+				string fullparam = "!LO:DY" + dparam;
+				if (gm) {
+					gm->addDynamicsLayoutParameters(slice, partindex, fullparam);
 				}
 			}
-			if (isPianoLabel || isPianoAbbr || isStaffNum || isPartNum) {
-				continue;
-			}
-			out << outfile[i] << "\n";
 		}
+		m_current_dynamic[partindex].clear();
 	}
-}
 
+	// see if a hairpin ending needs to be added before end of measure:
+	xml_node enode = event->getHairpinEnding();
+	if (enode) {
+		event->reportDynamicToOwner();  // shouldn't be necessary
+		addHairpinEnding(slice->at(partindex), event, partindex);
+		// shouldn't need dynamics layout parameter
+	}
 
+	if (m_post_note_text.empty()) {
+		return;
+	}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::printRdfs --
-//
+	// check the text buffer for text which needs to be moved
+	// after the current note.
+	string index;
+	index = to_string(partindex);
+	index += ' ';
+	index += to_string(staffindex);
+	index += ' ';
+	index += to_string(voiceindex);
 
-void Tool_musicxml2hum::printRdfs(ostream& out) {
-	if (!m_caesura_rdf.empty()) {
-		out << m_caesura_rdf << "\n";
+	auto it = m_post_note_text.find(index);
+	if (it == m_post_note_text.end()) {
+		// There is text waiting, but not for this note
+		// (for some strange reason).
+		return;
+	}
+	vector<xml_node>& tnodes = it->second;
+	for (int i=0; i<(int)tnodes.size(); i++) {
+		addText(slice, outdata, partindex, staffindex, voiceindex, tnodes[i], true);
 	}
+	m_post_note_text.erase(it);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_muisicxml2hum::setSoftwareInfo -- Store which software program generated the
-//    MusicXML data to handle locale variants.  There can be more than one
-//    <software> entry, so desired information is not necessarily in the first one.
+// Tool_musicxml2hum::addTexts -- Add all text direction for a note.
 //
 
-void Tool_musicxml2hum::setSoftwareInfo(xml_document& doc) {
-	string xpath = "/score-partwise/identification/encoding/software";
-	string software = doc.select_node(xpath.c_str()).node().child_value();
-	HumRegex hre;
-	if (hre.search(software, "sibelius", "i")) {
-		m_software = "sibelius";
+void Tool_musicxml2hum::addTexts(GridSlice* slice, GridMeasure* measure, int partindex,
+		int staffindex, int voiceindex, MxmlEvent* event) {
+	vector<pair<int, xml_node>>& nodes = event->getTexts();
+	for (auto item : nodes) {
+		int newpartindex = item.first;
+		int newstaffindex = 0; // Not allowing addressing text by layer (could be changed).
+		addText(slice, measure, newpartindex, newstaffindex, voiceindex, item.second, false);
 	}
 }
 
@@ -104145,239 +108295,349 @@ void Tool_musicxml2hum::setSoftwareInfo(xml_document& doc) {
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::cleanSpaces -- Converts newlines and tabs to spaces, and removes
-//     trailing spaces from the string.  Does not remove leading spaces, but this could
-//     be added.  Another variation would be to use \n to encode newlines if they need
-//     to be preserved, but for now converting them to spaces.
+// Tool_musicxml2hum::addTempos -- Add all text direction for a note.
 //
 
-string& Tool_musicxml2hum::cleanSpaces(string& input) {
-	for (int i=0; i<(int)input.size(); i++) {
-		if (std::isspace(input[i])) {
-			input[i] = ' ';
-		}
-	}
-	while ((!input.empty()) && std::isspace(input.back())) {
-		input.resize(input.size() - 1);
+void Tool_musicxml2hum::addTempos(GridSlice* slice, GridMeasure* measure, int partindex,
+		int staffindex, int voiceindex, MxmlEvent* event) {
+	vector<pair<int, xml_node>>& nodes = event->getTempos();
+	for (auto item : nodes) {
+		int newpartindex = item.first;
+		int newstaffindex = 0; // Not allowing addressing text by layer (could be changed).
+		addTempo(slice, measure, newpartindex, newstaffindex, voiceindex, item.second);
 	}
-	return input;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::cleanSpacesAndColons -- Converts newlines and
-//     tabs to spaces, and removes leading and trailing spaces from the
-//     string.  Another variation would be to use \n to encode newlines
-//     if they need to be preserved, but for now converting them to spaces.
-//     Colons (:) are also converted to &colon;.
+// Tool_musicxml2hum::addBrackets --
+//
+//
+//    <direction placement="above">
+//      <direction-type>
+//        <bracket default-y="12" line-end="down" line-type="dashed" number="1" type="start"/>
+//      </direction-type>
+//      <offset>4</offset>
+//    </direction>
+//
+//    <direction placement="above">
+//      <direction-type>
+//        <bracket line-end="down" number="1" type="stop"/>
+//      </direction-type>
+//      <offset>5</offset>
+//    </direction>
+//
 
-string Tool_musicxml2hum::cleanSpacesAndColons(const string& input) {
-	string output;
-	bool foundnonspace = false;
-	for (int i=0; i<(int)input.size(); i++) {
-		if (std::isspace(input[i])) {
-			if (!foundnonspace) {
-				output += ' ';
-			}
+void Tool_musicxml2hum::addBrackets(GridSlice* slice, GridMeasure* measure, MxmlEvent* event,
+	HumNum nowtime, int partindex) {
+	int staffindex = 0;
+	int voiceindex = 0;
+	string token;
+	HumNum timestamp;
+	vector<xml_node> brackets = event->getBrackets();
+	for (int i=0; i<(int)brackets.size(); i++) {
+		xml_node bracket = brackets[i].child("direction-type").child("bracket");
+		if (!bracket) {
+			continue;
 		}
-		if (input[i] == ':') {
-			foundnonspace = true;
-			output += "&colon;";
+		string linetype = bracket.attribute("line-type").as_string();
+		string endtype = bracket.attribute("type").as_string();
+		int number = bracket.attribute("number").as_int();
+		if (endtype == "stop") {
+			linetype = m_bracket_type_buffer[number];
 		} else {
-			output += input[i];
-			foundnonspace = true;
+			m_bracket_type_buffer[number] = linetype;
+		}
+
+		if (linetype == "solid") {
+			if (endtype == "start") {
+				token = "*lig";
+				measure->addInterpretationBefore(slice, partindex, staffindex, voiceindex, token);
+			} else if (endtype == "stop") {
+				token = "*Xlig";
+				timestamp = nowtime + event->getDuration();
+				measure->addInterpretationAfter(slice, partindex, staffindex, voiceindex, token, timestamp);
+			}
+		} else if (linetype == "dashed") {
+			if (endtype == "start") {
+				token = "*col";
+				measure->addInterpretationBefore(slice, partindex, staffindex, voiceindex, token);
+			} else if (endtype == "stop") {
+				token = "*Xcol";
+				timestamp = nowtime + event->getDuration();
+				measure->addInterpretationAfter(slice, partindex, staffindex, voiceindex, token, timestamp);
+			}
 		}
 	}
-	while ((!output.empty()) && std::isspace(output.back())) {
-		output.resize(output.size() - 1);
-	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addHeaderRecords -- Inserted in reverse order
-//      (last record inserted first).
+// Tool_musicxml2hum::addText -- Add a text direction to the grid.
+//
+//      <direction placement="below">
+//        <direction-type>
+//          <words font-style="italic">Some Text</words>
+//        </direction-type>
+//      </direction>
+//
+// Multi-line example:
+//
+// <direction placement="above">
+//         <direction-type>
+//           <words default-y="40.00" relative-x="-9.47" relative-y="2.71">note</words>
+//           <words>with newline</words>
+//         </direction-type>
+//       <staff>2</staff>
+//       </direction>
 //
 
-void Tool_musicxml2hum::addHeaderRecords(HumdrumFile& outfile, xml_document& doc) {
-	string xpath;
-	HumRegex hre;
-
-	if (!m_systemDecoration.empty()) {
-		// outfile.insertLine(0, "!!!system-decoration: " + m_systemDecoration);
-		if (m_systemDecoration != "s1") {
-			outfile.appendLine("!!!system-decoration: " + m_systemDecoration);
+void Tool_musicxml2hum::addText(GridSlice* slice, GridMeasure* measure, int partindex,
+		int staffindex, int voiceindex, xml_node node, bool force) {
+	string placementstring;
+	xml_attribute placement = node.attribute("placement");
+	if (placement) {
+		string value = placement.value();
+		if (value == "above") {
+			placementstring = ":a";
+		} else if (value == "below") {
+			placementstring = ":b";
 		}
 	}
 
-	xpath = "/score-partwise/credit/credit-words";
-   pugi::xpath_node_set credits = doc.select_nodes(xpath.c_str());
-	map<string, int> keys;
-	vector<string> refs;
-	vector<int> positions; // +1 = above, -1 = below;
-	for (auto it = credits.begin(); it != credits.end(); it++) {
-		string contents = cleanSpaces(it->node().child_value());
-		if (contents.empty()) {
-			continue;
-		}
-		if ((contents[0] != '@') && (contents[0] != '!')) {
-			continue;
-		}
+	xml_node child = node.first_child();
+	if (!child) {
+		return;
+	}
+	if (!nodeType(child, "direction-type")) {
+		return;
+	}
 
-		if (contents.size() >= 3) {
-			// If line starts with "@@" then place at end of score.
-			if ((contents[0] == '@') && (contents[1] == '@')) {
-				positions.push_back(-1);
-			} else {
-				positions.push_back(1);
-			}
-		} else {
-			positions.push_back(1);
-		}
+	xml_node grandchild = child.first_child();
+	if (!grandchild) {
+		return;
+	}
 
-		if (hre.search(contents, "^[@!]+([^\\s]+):")) {
-			// reference record
-			string key = hre.getMatch(1);
-			keys[key] = 1;
-			hre.replaceDestructive(contents, "!!!", "^[!@]+");
-			refs.push_back(contents);
-		} else {
-			// global comment
-			hre.replaceDestructive(contents, "!!", "^[!@]+");
-			refs.push_back(contents);
+	xml_node sibling = grandchild;
+
+	bool dyQ = false;
+	xml_attribute defaulty;
+
+	string text;
+	while (sibling) {
+		if (nodeType(sibling, "words")) {
+			text += sibling.child_value();
+			if (!dyQ) {
+				defaulty = sibling.attribute("default-y");
+				if (defaulty) {
+					dyQ = true;
+					double number = std::stod(defaulty.value());
+					if (number >= 0.0) {
+						placementstring = ":a";
+					} else if (number < 0.0) {
+						placementstring = ":b";
+					}
+				}
+			}
 		}
+		sibling = sibling.next_sibling();
 	}
 
-	// OTL: title //////////////////////////////////////////////////////////
-
-	// Sibelius method
-	xpath = "/score-partwise/work/work-title";
-	string worktitle = cleanSpaces(doc.select_node(xpath.c_str()).node().child_value());
-	string otl_record;
-	string omv_record;
-	bool worktitleQ = false;
-	if ((worktitle != "") && (worktitle != "Title")) {
-		otl_record = "!!!OTL: ";
-		otl_record += worktitle;
-		worktitleQ = true;
+	if (text == "") {
+		// Don't insert an empty text
+		return;
 	}
 
-	xpath = "/score-partwise/movement-title";
-	string mtitle = cleanSpaces(doc.select_node(xpath.c_str()).node().child_value());
-	if (mtitle != "") {
-		if (worktitleQ) {
-			omv_record = "!!!OMV: ";
-			omv_record += mtitle;
-		} else {
-			otl_record = "!!!OTL: ";
-			otl_record += mtitle;
+	// Mapping \n (0x0a) to newline (ignoring \r, (0x0d))
+	string newtext;
+	for (int i=0; i<(int)text.size(); i++) {
+		switch (text[i]) {
+			case 0x0a:
+				newtext += "\\n";
+			case 0x0d:
+				break;
+			default:
+				newtext += text[i];
 		}
 	}
+	text = newtext;
 
-	// COM: composer /////////////////////////////////////////////////////////
-	// CDT: composer's dates
-	xpath = "/score-partwise/identification/creator[@type='composer']";
-	string composer = cleanSpaces(doc.select_node(xpath.c_str()).node().child_value());
-	string cdt_record;
-	if (composer != "") {
-		if (hre.search(composer, R"(\((.*?\d.*?)\))")) {
-			string dates = hre.getMatch(1);
-			// hre.replaceDestructive(composer, "", R"(\()" + dates + R"(\))");
-			auto loc = composer.find(dates);
-			if (loc != std::string::npos) {
-				composer.replace(loc-1, dates.size()+2, "");
-			}
-			hre.replaceDestructive(composer, "", R"(^\s+)");
-			hre.replaceDestructive(composer, "", R"(\s+$)");
-			if (hre.search(composer, R"(([^\s]+) +([^\s]+))")) {
-				composer = hre.getMatch(2) + ", " + hre.getMatch(1);
-			}
-			if (dates != "") {
-				if (hre.search(dates, R"(\b(\d{4})\?)")) {
-					string replacement = "~";
-					replacement += hre.getMatch(1);
-					hre.replaceDestructive(dates, replacement, R"(\b\d{4}\?)");
-					cdt_record = "!!!CDT: ";
-					cdt_record += dates;
-				}
-			}
-		}
+	// Remove newlines encodings at end of text.
+	HumRegex hre;
+	hre.replaceDestructive(text, "", "(\\\\n)+\\s*$");
+
+	/* Problem: these are also possibly signs for figured bass
+	if (text == "#") {
+		// interpret as an editorial sharp marker
+		setEditorialAccidental(+1, slice, partindex, staffindex, voiceindex);
+		return;
+	} else if (text == "b") {
+		// interpret as an editorial flat marker
+		setEditorialAccidental(-1, slice, partindex, staffindex, voiceindex);
+		return;
+	// } else if (text == u8"§") {
+	} else if (text == "\xc2\xa7") {
+		// interpret as an editorial natural marker
+		setEditorialAccidental(0, slice, partindex, staffindex, voiceindex);
+		return;
 	}
+	*/
 
+	//
+	// The following code should be merged into the loop to apply
+	// font changes within the text.  Internal formatting code for
+	// the string would need to be developed if so.  For now, just
+	// the first word's style will be processed.
+	//
 
-	for (int i=(int)refs.size()-1; i>=0; i--) {
-		if (positions.at(i) > 0) {
-			// place at start of file
-			outfile.insertLine(0, refs[i]);
-		}
-	}
+	string stylestring;
+	bool italic = false;
+	bool bold = false;
 
-	for (int i=0; i<(int)refs.size(); i++) {
-		if (positions.at(i) < 0) {
-			// place at end of file
-			outfile.appendLine(refs[i]);
+	xml_attribute fontstyle = grandchild.attribute("font-style");
+	if (fontstyle) {
+		string value = fontstyle.value();
+		if (value == "italic") {
+			italic = true;
 		}
 	}
 
-	if ((!omv_record.empty()) && (!keys["OMV"])) {
-		outfile.insertLine(0, omv_record);
-	}
-
-	if ((!otl_record.empty()) && (!keys["OTL"])) {
-		outfile.insertLine(0, otl_record);
+	xml_attribute fontweight = grandchild.attribute("font-weight");
+	if (fontweight) {
+		string value = fontweight.value();
+		if (value == "bold") {
+			bold = true;
+		}
 	}
 
-	if ((!cdt_record.empty()) && (!keys["CDT"])) {
-		outfile.insertLine(0, cdt_record);
+	if (italic && bold) {
+		stylestring = ":Bi";
+	} else if (italic) {
+		stylestring = ":i";
+	} else if (bold) {
+		stylestring = ":B";
 	}
 
-	if ((!composer.empty()) && (!keys["COM"])) {
-		// Don't print composer name if it is "Composer".
-		if (composer != "Composer") {
-			string com_record = "!!!COM: " + composer;
-			outfile.insertLine(0, com_record);
+	bool interpQ = false;
+	bool specialQ = false;
+	bool globalQ = false;
+	bool afterQ = false;
+	string output;
+	if (text == "!") {
+		// null local comment
+		output = text;
+		specialQ = true;
+	} else if (text == "*") {
+		// null interpretation
+		output = text;
+		specialQ = true;
+		interpQ = true;
+	} else if ((text.size() > 1) && (text[0] == '*') && (text[1] != '*')) {
+		// regular tandem interpretation, but disallow manipulators:
+		if (text == "*^") {
+			specialQ = false;
+		} else if (text == "*+") {
+			specialQ = false;
+		} else if (text == "*-") {
+			specialQ = false;
+		} else if (text == "*v") {
+			specialQ = false;
+		} else {
+			specialQ = true;
+			interpQ = true;
+			output = text;
+		}
+	} else if ((text.size() > 2) && (text[0] == '*') && (text[1] == '*')) {
+		hre.replaceDestructive(text, "*", "^\\*+");
+		output = text;
+		specialQ = true;
+		afterQ = true;
+		interpQ = true;
+		if (force == false) {
+			// store text for later processing after the next note in the data.
+			string index;
+			index += to_string(partindex);
+			index += ' ';
+			index += to_string(staffindex);
+			index += ' ';
+			index += to_string(voiceindex);
+			m_post_note_text[index].push_back(node);
+			return;
 		}
+	} else if ((text.size() > 1) && (text[0] == '!') && (text[1] != '!')) {
+		// embedding a local comment
+		output = text;
+		specialQ = true;
+	} else if ((text.size() >= 2) && (text[0] == '!') && (text[1] == '!')) {
+		// embedding a global comment (or bibliographic record, etc.).
+		output = text;
+		globalQ = true;
+		specialQ = true;
+	} else if (hre.search(text, "\\s*problem\\s*:\\s*(.*)\\s*$")) {
+		specialQ = true;
+		output = "!LO:TX:t=P:problem:";
+		output += hre.getMatch(1);
+		hre.replaceDestructive(output, "\\n", "\n", "g");
+		hre.replaceDestructive(output, " ", "\t", "g");
 	}
 
-}
-
-
-
-//////////////////////////////
-//
-// Tool_musicxml2hum::addFooterRecords --
-//
+	if (!specialQ) {
+		text = cleanSpacesAndColons(text);
+		if (text.empty()) {
+			// no text to display after removing whitespace
+			return;
+		}
 
-void Tool_musicxml2hum::addFooterRecords(HumdrumFile& outfile, xml_document& doc) {
+		if (placementstring.empty()) {
+			// force above if no placement specified
+			placementstring = ":a";
+		}
 
-	// YEM: copyright
-	string copy = doc.select_node("/score-partwise/identification/rights").node().child_value();
-	bool validcopy = true;
-	if (copy == "") {
-		validcopy = false;
-	}
-	if ((copy.length() == 2) && ((unsigned char)copy[0] == 0xc2) && ((unsigned char)copy[1] == 0xa9)) {
-		validcopy = false;
-	}
-	if ((copy.find("opyright") != std::string::npos) && (copy.size() < 15)) {
-		validcopy = false;
+		output = "!LO:TX";
+		output += placementstring;
+		output += stylestring;
+		output += ":t=";
+		output += text;
 	}
 
-	if (validcopy) {
-		string yem_record = "!!!YEM: ";
-		yem_record += cleanSpaces(copy);
-		outfile.appendLine(yem_record);
-	}
+	// The text direction needs to be added before the last line
+	// in the measure object.  If there is already an empty layout
+	// slice before the current one (with no spine manipulators
+	// in between), then insert onto the existing layout slice;
+	// otherwise create a new layout slice.
 
-	// RDF:
-	if (m_hasEditorial) {
-		string rdf_record = "!!!RDF**kern: i = editorial accidental";
-		outfile.appendLine(rdf_record);
+	if (interpQ) {
+		if (afterQ) {
+			int voicecount = (int)slice->at(partindex)->at(staffindex)->size();
+			if (voiceindex >= voicecount) {
+				// Adding voices in the new slice.  It might be
+				// better to first check for a previous text line
+				// at the current timestamp that is empty (because there
+				// is text at the same time in another spine).
+				GridStaff* gs = slice->at(partindex)->at(staffindex);
+				gs->resize(voiceindex+1);
+				string null = slice->getNullTokenForSlice();
+				for (int m=voicecount; m<voiceindex+1; m++) {
+					gs->at(m) = new GridVoice(null, 0);
+				}
+			}
+			HTp token = slice->at(partindex)->at(staffindex)->at(voiceindex)->getToken();
+			HumNum tokdur = Convert::recipToDuration(token);
+			HumNum timestamp = slice->getTimestamp() + tokdur;
+			measure->addInterpretationAfter(slice, partindex, staffindex, voiceindex, output, timestamp);
+		} else {
+			measure->addInterpretationBefore(slice, partindex, staffindex, voiceindex, output);
+		}
+	} else if (globalQ) {
+		HumNum timestamp = slice->getTimestamp();
+		measure->addGlobalComment(text, timestamp);
+	} else {
+		// adding local comment that is not a layout parameter also goes here:
+		measure->addLayoutParameter(slice, partindex, output);
 	}
 }
 
@@ -104385,273 +108645,252 @@ void Tool_musicxml2hum::addFooterRecords(HumdrumFile& outfile, xml_document& doc
 
 //////////////////////////////
 //
-// initialize --
+// Tool_musicxml2hum::addTempo -- Add a tempo direction to the grid.
 //
-
-void Tool_musicxml2hum::initialize(void) {
-	m_recipQ = getBoolean("recip");
-	m_stemsQ = getBoolean("stems");
-	m_hasOrnamentsQ = false;
-}
-
-
-
-//////////////////////////////
+// <direction placement="above">
+//    <direction-type>
+//       <metronome parentheses="no" default-x="-35.96" relative-y="20.00">
+//          <beat-unit>half</beat-unit>
+//          <per-minute>80</per-minute>
+//       </metronome>
+//    </direction-type>
+//    <sound tempo="160"/>
+// </direction>
 //
-// Tool_musicxml2hum::reindexVoices --
+// Dotted tempo example:
 //
-
-void Tool_musicxml2hum::reindexVoices(vector<MxmlPart>& partdata) {
-	for (int p=0; p<(int)partdata.size(); p++) {
-		for (int m=0; m<(int)partdata[p].getMeasureCount(); m++) {
-			MxmlMeasure* measure = partdata[p].getMeasure(m);
-			if (!measure) {
-				continue;
-			}
-			reindexMeasure(measure);
-		}
-	}
-}
-
-
-
-//////////////////////////////
+// <direction placement="above">
+//    <direction-type>
+//       <metronome parentheses="no" default-x="-39.10" relative-y="20.00">
+//          <beat-unit>quarter</beat-unit>
+//          <beat-unit-dot/>
+//          <per-minute>80</per-minute>
+//       </metronome>
+//    </direction-type>
+//    <sound tempo="120"/>
+// </direction>
 //
-// Tool_musicxml2hum::prepareRdfs --
 //
 
-void Tool_musicxml2hum::prepareRdfs(vector<MxmlPart>& partdata) {
-	string caesura;
-	for (int i=0; i<(int)partdata.size(); i++) {
-		caesura = partdata[i].getCaesura();
-		if (!caesura.empty()) {
+void Tool_musicxml2hum::addTempo(GridSlice* slice, GridMeasure* measure, int partindex,
+		int staffindex, int voiceindex, xml_node node) {
+	string placementstring;
+	xml_attribute placement = node.attribute("placement");
+	if (placement) {
+		string value = placement.value();
+		if (value == "above") {
+			placementstring = ":a";
+		} else if (value == "below") {
+			placementstring = ":b";
+		} else {
+			// force above if no explicit placement:
+			placementstring = ":a";
 		}
 	}
 
-	if (!caesura.empty()) {
-		m_caesura_rdf = "!!!RDF**kern: " + caesura + " = caesura";
+	xml_node child = node.first_child();
+	if (!child) {
+		return;
+	}
+	if (!nodeType(child, "direction-type")) {
+		return;
 	}
 
-}
-
-
+	xml_node sound(NULL);
+	xml_node sibling = child;
+	while (sibling) {
+		if (nodeType(sibling, "sound")) {
+			sound = sibling;
+			break;
+		}
+		sibling = sibling.next_sibling();
+	}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::reindexMeasure --
-//
+	// grandchild should be <metronome> (containing textual display)
+	// and <sound @tempo> which gives *MM data.
+	xml_node metronome(NULL);
 
-void Tool_musicxml2hum::reindexMeasure(MxmlMeasure* measure) {
-	if (!measure) {
+	xml_node grandchild = child.first_child();
+	if (!grandchild) {
 		return;
 	}
+	sibling = grandchild;
 
-	vector<vector<int> > staffVoiceCounts;
-	vector<MxmlEvent*>& elist = measure->getEventList();
-
-	for (int i=0; i<(int)elist.size(); i++) {
-		int staff = elist[i]->getStaffIndex();
-		int voice = elist[i]->getVoiceIndex();
-
-		if ((voice >= 0) && (staff >= 0)) {
-			if (staff >= (int)staffVoiceCounts.size()) {
-				int newsize = staff + 1;
-				staffVoiceCounts.resize(newsize);
-			}
-			if (voice >= (int)staffVoiceCounts[staff].size()) {
-				int oldsize = (int)staffVoiceCounts[staff].size();
-				int newsize = voice + 1;
-				staffVoiceCounts[staff].resize(newsize);
-				for (int i=oldsize; i<newsize; i++) {
-					staffVoiceCounts[staff][voice] = 0;
-				}
-			}
-			staffVoiceCounts[staff][voice]++;
+	while (sibling) {
+		if (nodeType(sibling, "metronome")) {
+			metronome = sibling;
 		}
+		sibling = sibling.next_sibling();
 	}
 
-	bool needreindexing = false;
+	// get metronome parameters
 
-	for (int i=0; i<(int)staffVoiceCounts.size(); i++) {
-		if (staffVoiceCounts[i].size() < 2) {
-			continue;
-		}
-		for (int j=1; j<(int)staffVoiceCounts[i].size(); j++) {
-			if (staffVoiceCounts[i][j] == 0) {
-				needreindexing = true;
-				break;
+	xml_node beatunit(NULL);
+	xml_node beatunitdot(NULL);
+	xml_node perminute(NULL);
+
+	if (metronome) {
+		sibling = metronome.first_child();
+		while (sibling) {
+			if (nodeType(sibling, "beat-unit")) {
+				beatunit = sibling;
+			} else if (nodeType(sibling, "beat-unit-dot")) {
+				beatunitdot = sibling;
+			} else if (nodeType(sibling, "per-minute")) {
+				perminute = sibling;
 			}
-		}
-		if (needreindexing) {
-			break;
+			sibling = sibling.next_sibling();
 		}
 	}
 
-	if (!needreindexing) {
-		return;
+	string mmvalue;
+	if (sound) {
+		mmvalue = getAttributeValue(sound, "tempo");
 	}
 
-	vector<vector<int> > remapping;
-	remapping.resize(staffVoiceCounts.size());
-	int reindex;
-	for (int i=0; i<(int)staffVoiceCounts.size(); i++) {
-		remapping[i].resize(staffVoiceCounts[i].size());
-		reindex = 0;
-		for (int j=0; j<(int)remapping[i].size(); j++) {
-			if (remapping[i].size() == 1) {
-				remapping[i][j] = 0;
-				continue;
-			}
-			if (staffVoiceCounts[i][j]) {
-				remapping[i][j] = reindex++;
-			} else {
-				remapping[i][j] = -1;  // invalidate voice
-			}
-		}
+	if (!beatunit) {
+		cerr << "Warning: missing beat-unit in tempo setting" << endl;
+		return;
 	}
-
-	// Go back and remap the voice indexes of elements.
-	// Presuming that the staff does not need to be reindex.
-	for (int i=0; i<(int)elist.size(); i++) {
-		int oldvoice = elist[i]->getVoiceIndex();
-		int staff = elist[i]->getStaffIndex();
-		if (oldvoice < 0) {
-			continue;
-		}
-		int newvoice = remapping[staff][oldvoice];
-		if (newvoice == oldvoice) {
-			continue;
-		}
-		elist[i]->setVoiceIndex(newvoice);
+	if (!perminute) {
+		cerr << "Warning: missing per-minute in tempo setting" << endl;
+		return;
 	}
 
-}
-
+	int staff = 0;
+	int voice = 0;
 
+	if (sound) {
+		string mmtok = "*MM";
+		double mmv = stod(mmvalue);
+		double mmi = int(mmv + 0.001);
+		if (fabs(mmv - mmi) < 0.01) {
+			stringstream sstream;
+			sstream << mmi;
+			mmtok += sstream.str();
+		} else {
+			mmtok += mmvalue;
+		}
+		HumNum timestamp = slice->getTimestamp();
+		measure->addTempoToken(mmtok, timestamp, partindex, staff, voice, m_maxstaff);
+	}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::setOptions --
-//
+	string butext = beatunit.child_value();
+	string pmtext = perminute.child_value();
+	string stylestring;
 
-void Tool_musicxml2hum::setOptions(int argc, char** argv) {
-	m_options.process(argc, argv);
-}
+	// create textual tempo marking
+	string text;
+	text = "[";
+	text += butext;
+	if (beatunitdot) {
+		text += "-dot";
+	}
+	text += "]";
+	text += "=";
+	text += pmtext;
 
+	string output = "!LO:TX";
+	output += placementstring;
+	output += stylestring;
+	output += ":t=";
+	output += text;
 
-void Tool_musicxml2hum::setOptions(const vector<string>& argvlist) {
-    m_options.process(argvlist);
+	// The text direction needs to be added before the last line in the measure object.
+	// If there is already an empty layout slice before the current one (with no spine manipulators
+	// in between), then insert onto the existing layout slice; otherwise create a new layout slice.
+	measure->addTempoToken(slice, partindex, output);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getOptionDefinitions -- Used to avoid
-//     duplicating the definitions in the test main() function.
+// setEditorialAccidental --
 //
 
-Options Tool_musicxml2hum::getOptionDefinitions(void) {
-	return m_options;
-}
-
-
-///////////////////////////////////////////////////////////////////////////
-
-
-//////////////////////////////
-//
-// Tool_musicxml2hum::fillPartData --
-//
+void Tool_musicxml2hum::setEditorialAccidental(int accidental, GridSlice* slice,
+		int partindex, int staffindex, int voiceindex) {
 
-bool Tool_musicxml2hum::fillPartData(vector<MxmlPart>& partdata,
-		const vector<string>& partids, map<string, xml_node>& partinfo,
-		map<string, xml_node>& partcontent) {
+	HTp tok = slice->at(partindex)->at(staffindex)->at(voiceindex)->getToken();
 
-	bool output = true;
-	for (int i=0; i<(int)partinfo.size(); i++) {
-		partdata[i].setPartNumber(i+1);
-		output &= fillPartData(partdata[i], partids[i], partinfo[partids[i]],
-				partcontent[partids[i]]);
+	if ((accidental < 0) && (tok->find("-") == string::npos))  {
+		cerr << "Editorial error for " << tok << ": no flat to mark" << endl;
+		return;
 	}
-	return output;
-}
-
-
-bool Tool_musicxml2hum::fillPartData(MxmlPart& partdata,
-		const string& id, xml_node partdeclaration, xml_node partcontent) {
-	if (m_stemsQ) {
-		partdata.enableStems();
+	if ((accidental > 0) && (tok->find("#") == string::npos))  {
+		cerr << "Editorial error for " << tok << ": no sharp to mark" << endl;
+		return;
+	}
+	if ((accidental == 0) &&
+			((tok->find("#") != string::npos) || (tok->find("-") != string::npos)))  {
+		cerr << "Editorial error for " << tok << ": requesting a natural accidental" << endl;
+		return;
 	}
 
-	partdata.parsePartInfo(partdeclaration);
-	// m_last_ottava_direction.at(partdata.getPartIndex()).resize(partdata.getStaffCount());
-	// staff count is incorrect at this point? Just assume 32 staves in the part, which should
-	// be 28-30 staffs too many.
-	m_last_ottava_direction.at(partdata.getPartIndex()).resize(32);
+	string newtok = *tok;
 
-	int count;
-	auto measures = partcontent.select_nodes("./measure");
-	for (int i=0; i<(int)measures.size(); i++) {
-		partdata.addMeasure(measures[i].node());
-		count = partdata.getMeasureCount();
-		if (count > 1) {
-			HumNum dur = partdata.getMeasure(count-1)->getTimeSigDur();
-			if (dur == 0) {
-				HumNum dur = partdata.getMeasure(count-2)
-						->getTimeSigDur();
-				if (dur > 0) {
-					partdata.getMeasure(count - 1)->setTimeSigDur(dur);
-				}
+	if (accidental == -1) {
+		auto loc = newtok.find("-");
+		if (loc < newtok.size()) {
+			if (newtok[loc+1] == 'X') {
+				// replace explicit accidental with editorial accidental
+				newtok[loc+1] = 'i';
+				tok->setText(newtok);
+				m_hasEditorial = 'i';
+			} else {
+				// append i after -:
+				newtok.insert(loc+1, "i");
+				tok->setText(newtok);
+				m_hasEditorial = 'i';
 			}
 		}
-
+		return;
 	}
-	return true;
-}
 
-
-
-//////////////////////////////
-//
-// Tool_musicxml2hum::printPartInfo -- Debug information.
-//
-
-void Tool_musicxml2hum::printPartInfo(vector<string>& partids,
-		map<string, xml_node>& partinfo, map<string, xml_node>& partcontent,
-		vector<MxmlPart>& partdata) {
-	cout << "\nPart information in the file:" << endl;
-	int maxmeasure = 0;
-	for (int i=0; i<(int)partids.size(); i++) {
-		cout << "\tPART " << i+1 << " id = " << partids[i] << endl;
-		cout << "\tMAXSTAFF " << partdata[i].getStaffCount() << endl;
-		cout << "\t\tpart name:\t"
-		     << getChildElementText(partinfo[partids[i]], "part-name") << endl;
-		cout << "\t\tpart abbr:\t"
-		     << getChildElementText(partinfo[partids[i]], "part-abbreviation")
-		     << endl;
-		auto node = partcontent[partids[i]];
-		auto measures = node.select_nodes("./measure");
-		cout << "\t\tMeasure count:\t" << measures.size() << endl;
-		if (maxmeasure < (int)measures.size()) {
-			maxmeasure = (int)measures.size();
+	if (accidental == +1) {
+		auto loc = newtok.find("#");
+		if (loc < newtok.size()) {
+			if (newtok[loc+1] == 'X') {
+				// replace explicit accidental with editorial accidental
+				newtok[loc+1] = 'i';
+				tok->setText(newtok);
+				m_hasEditorial = 'i';
+			} else {
+				// append i after -:
+				newtok.insert(loc+1, "i");
+				tok->setText(newtok);
+				m_hasEditorial = 'i';
+			}
 		}
-		cout << "\t\tTotal duration:\t" << partdata[i].getDuration() << endl;
+		return;
 	}
 
-	MxmlMeasure* measure;
-	for (int i=0; i<maxmeasure; i++) {
-		cout << "m" << i+1 << "\t";
-		for (int j=0; j<(int)partdata.size(); j++) {
-			measure = partdata[j].getMeasure(i);
-			if (measure) {
-				cout << measure->getDuration();
-			}
-			if (j < (int)partdata.size() - 1) {
-				cout << "\t";
+	if (accidental == 0) {
+		auto loc = newtok.find("n");
+		if (loc < newtok.size()) {
+			if (newtok[loc+1] == 'X') {
+				// replace explicit accidental with editorial accidental
+				newtok[loc+1] = 'i';
+				tok->setText(newtok);
+				m_hasEditorial = 'i';
+			} else {
+				// append i after -:
+				newtok.insert(loc+1, "i");
+				tok->setText(newtok);
+				m_hasEditorial = 'i';
 			}
+		} else {
+			// no natural sign, so add it after any pitch classes.
+			HumRegex hre;
+			hre.search(newtok, R"(([a-gA-G]+))");
+			string diatonic = hre.getMatch(1);
+			string newacc = diatonic + "i";
+			hre.replaceDestructive(newtok, newacc, diatonic);
+			tok->setText(newtok);
+			m_hasEditorial = 'i';
 		}
-		cout << endl;
+		return;
 	}
 }
 
@@ -104659,172 +108898,203 @@ void Tool_musicxml2hum::printPartInfo(vector<string>& partids,
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::insertPartNames --
+// Tool_musicxml2hum::addDynamic -- extract any dynamics for the event
+//
+// Such as:
+//    <direction placement="below">
+//      <direction-type>
+//        <dynamics>
+//          <fff/>
+//          </dynamics>
+//        </direction-type>
+//      <sound dynamics="140.00"/>
+//      </direction>
+//
+// Hairpins:
+//      <direction placement="below">
+//        <direction-type>
+//          <wedge default-y="-75" number="2" spread="15" type="diminuendo"/>
+//        </direction-type>
+//      </direction>
+//
+//      <direction>
+//        <direction-type>
+//          <wedge spread="15" type="stop"/>
+//        </direction-type>
+//      </direction>
 //
 
-void Tool_musicxml2hum::insertPartNames(HumGrid& outdata, vector<MxmlPart>& partdata) {
+void Tool_musicxml2hum::addDynamic(GridPart* part, MxmlEvent* event, int partindex) {
+	vector<xml_node> directions = event->getDynamics();
+	if (directions.empty()) {
+		return;
+	}
 
-	bool hasname = false;
-	bool hasabbr = false;
+	HTp tok = NULL;
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		string value;
-		value = partdata[i].getPartName();
-		if (!value.empty()) {
-			hasname = true;
-			break;
+	for (int i=0; i<(int)directions.size(); i++) {
+		xml_node direction = directions[i];
+		xml_attribute placement = direction.attribute("placement");
+		bool above = false;
+		if (placement) {
+			string value = placement.value();
+			if (value == "above") {
+				above = true;
+			}
 		}
-	}
-
-	for (int i=0; i<(int)partdata.size(); i++) {
-		string value;
-		value = partdata[i].getPartAbbr();
-		if (!value.empty()) {
-			hasabbr = true;
-			break;
+		xml_node child = direction.first_child();
+		if (!child) {
+			continue;
+		}
+		if (!nodeType(child, "direction-type")) {
+			continue;
+		}
+		xml_node grandchild = child.first_child();
+		if (!grandchild) {
+			continue;
 		}
-	}
-
-	if (!(hasabbr || hasname)) {
-		return;
-	}
-
-	GridMeasure* gm;
-	if (outdata.empty()) {
-		gm = new GridMeasure(&outdata);
-		outdata.push_back(gm);
-	} else {
-		gm = outdata[0];
-	}
 
-	int maxstaff;
+		if (!(nodeType(grandchild, "dynamics") || nodeType(grandchild, "wedge"))) {
+			continue;
+		}
 
-	if (hasabbr) {
-		for (int i=0; i<(int)partdata.size(); i++) {
-			string partabbr = partdata[i].getPartAbbr();
-			if (partabbr.empty()) {
+		if (nodeType(grandchild, "dynamics")) {
+			xml_node dynamic = grandchild.first_child();
+			if (!dynamic) {
 				continue;
 			}
-			string abbr = "*I'" + partabbr;
-			maxstaff = outdata.getStaffCount(i);
-			gm->addLabelAbbrToken(abbr, 0, i, maxstaff-1, 0, (int)partdata.size(), maxstaff);
-		}
-	}
+			string dstring = getDynamicString(dynamic);
+			if (!tok) {
+				tok = new HumdrumToken(dstring);
+			} else {
+				string oldtext = tok->getText();
+				string newtext = oldtext + " " + dstring;
+				tok->setText(newtext);
+			}
+		} else if ( nodeType(grandchild, "wedge")) {
+			xml_node hairpin = grandchild;
 
-	if (hasname) {
-		for (int i=0; i<(int)partdata.size(); i++) {
-			string partname = partdata[i].getPartName();
-			if (partname.empty()) {
+			if (isUsedHairpin(hairpin, partindex)) {
+				// need to suppress wedge ending if already used in [[ or ]]
 				continue;
 			}
-			if (partname.find("MusicXML") != string::npos) {
-				// ignore Finale dummy part names
+			if (!hairpin) {
+				cerr << "Warning: Expecting a hairpin, but found nothing" << endl;
 				continue;
 			}
-			if (partname.find("Part_") != string::npos) {
-				// ignore SharpEye dummy part names
-				continue;
+			string hstring = getHairpinString(hairpin, partindex);
+			if (!tok) {
+				tok = new HumdrumToken(hstring);
+			} else {
+				string oldtext = tok->getText();
+				string newtext = oldtext + " " + hstring;
+				tok->setText(newtext);
 			}
-			if (partname.find("Unnamed") != string::npos) {
-				// ignore Sibelius dummy part names
-				continue;
+
+			// Deal here with adding an index if there is more than one hairpin.
+			if ((hstring != "[") && (hstring != "]") && above) {
+				tok->setValue("LO", "HP", "a", "true");
 			}
-			string name = "*I\"" + partname;
-			maxstaff = outdata.getStaffCount(i);
-			gm->addLabelToken(name, 0, i, maxstaff-1, 0, (int)partdata.size(), maxstaff);
 		}
 	}
-
+	if (tok) {
+		part->setDynamics(tok);
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::stitchParts -- Merge individual parts into a
-//     single score sequence.
+// Tool_musicxml::isUsedHairpin --  Needed to avoid double-insertion
+//    of hairpins which were stored before a barline so that they
+//    are not also repeated on the first beat of the next barline.
+//    This fuction will remove the hairpin from the used array
+//    when it is checked.  The used array is only for storing
+//    hairpins that end on measures, so in theory there should not
+//    be too many, and they will be removed fairly quickly.
 //
 
-bool Tool_musicxml2hum::stitchParts(HumGrid& outdata,
-		vector<string>& partids, map<string, xml_node>& partinfo,
-		map<string, xml_node>& partcontent, vector<MxmlPart>& partdata) {
-	if (partdata.size() == 0) {
-		return false;
-	}
-
-	int i;
-	int measurecount = partdata[0].getMeasureCount();
-	// i used to start at 1 for some strange reason.
-	for (i=0; i<(int)partdata.size(); i++) {
-		if (measurecount != partdata[i].getMeasureCount()) {
-			cerr << "ERROR: cannot handle parts with different measure\n";
-			cerr << "counts yet. Compare MM" << measurecount << " to MM";
-			cerr << partdata[i].getMeasureCount() << endl;
-			exit(1);
+bool Tool_musicxml2hum::isUsedHairpin(xml_node hairpin, int partindex) {
+	for (int i=0; i<(int)m_used_hairpins.at(partindex).size(); i++) {
+		if (hairpin == m_used_hairpins.at(partindex).at(i)) {
+			// Cannot delete yet: the hairpin endings are being double accessed somewhere.
+			//m_used_hairpins[partindex].erase(m_used_hairpins[partindex].begin() + i);
+			return true;
 		}
 	}
-
-	vector<int> partstaves(partdata.size(), 0);
-	for (i=0; i<(int)partstaves.size(); i++) {
-		partstaves[i] = partdata[i].getStaffCount();
-	}
-
-	bool status = true;
-	int m;
-	for (m=0; m<partdata[0].getMeasureCount(); m++) {
-		status &= insertMeasure(outdata, m, partdata, partstaves);
-		// a hack for now:
-		// insertSingleMeasure(outfile);
-		// measures.push_back(&outfile[outfile.getLineCount()-1]);
-	}
-
-	moveBreaksToEndOfPreviousMeasure(outdata);
-
-	insertPartNames(outdata, partdata);
-
-	return status;
+	return false;
 }
 
 
 
 //////////////////////////////
 //
-// moveBreaksToEndOfPreviousMeasure --
+// Tool_musicxml2hum::addHairpinEnding -- extract any hairpin ending
+//   at the end of a measure.
+//
+// Hairpins:
+//      <direction>
+//        <direction-type>
+//          <wedge spread="15" type="stop"/>
+//        </direction-type>
+//      </direction>
 //
 
-void Tool_musicxml2hum::moveBreaksToEndOfPreviousMeasure(HumGrid& outdata) {
-	for (int i=1; i<(int)outdata.size(); i++) {
-		GridMeasure* gm = outdata[i];
-		GridMeasure* gmlast = outdata[i-1];
-		if (!gm || !gmlast) {
-			continue;
-		}
-		if (gm->begin() == gm->end()) {
-			// empty measure
+void Tool_musicxml2hum::addHairpinEnding(GridPart* part, MxmlEvent* event, int partindex) {
+
+	xml_node direction = event->getHairpinEnding();
+	if (!direction) {
+		return;
+	}
+
+	xml_node child = direction.first_child();
+	if (!child) {
+		return;
+	}
+	if (!nodeType(child, "direction-type")) {
+		return;
+	}
+	xml_node grandchild = child.first_child();
+	if (!grandchild) {
+		return;
+	}
+
+	if (!nodeType(grandchild, "wedge")) {
+		return;
+	}
+
+	if (nodeType(grandchild, "wedge")) {
+		xml_node hairpin = grandchild;
+		if (!hairpin) {
 			return;
 		}
-		GridSlice *firstit = *(gm->begin());
-		HumNum starttime = firstit->getTimestamp();
-		for (auto it = gm->begin(); it != gm->end(); it++) {
-			HumNum time2 = (*it)->getTimestamp();
-			if (time2 > starttime) {
-				break;
-			}
-			if (!(*it)->isGlobalComment()) {
-				continue;
-			}
-			HTp token = (*it)->at(0)->at(0)->at(0)->getToken();
-			if (!token) {
-				continue;
-			}
-			if ((*token == "!!linebreak:original") ||
-			    (*token == "!!pagebreak:original")) {
-				GridSlice *swapper = *it;
-				gm->erase(it);
-				gmlast->push_back(swapper);
-				// there can be only one break, so quit the loop now.
-				break;
+		string hstring = getHairpinString(hairpin, partindex);
+		if (hstring == "[") {
+			hstring = "[[";
+		} else if (hstring == "]") {
+			hstring = "]]";
+		}
+		m_used_hairpins.at(partindex).push_back(hairpin);
+		HTp current = part->getDynamics();
+		if (!current) {
+			HTp htok = new HumdrumToken(hstring);
+			part->setDynamics(htok);
+		} else {
+			string text = current->getText();
+			text += " ";
+			text += hstring;
+			// Set single-note crescendos
+			if (text == "< [[") {
+				text = "<[";
+			} else if (text == "> ]]") {
+				text = ">]";
+			} else if (text == "< [") {
+				text = "<[";
+			} else if (text == "> ]") {
+				text = ">]";
 			}
+			current->setText(text);
 		}
 	}
 }
@@ -104833,1432 +109103,1188 @@ void Tool_musicxml2hum::moveBreaksToEndOfPreviousMeasure(HumGrid& outdata) {
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::cleanupMeasures --
-//     Also add barlines here (keeping track of the
-//     duration of each measure).
+// Tool_musicxml2hum::convertFiguredBassNumber --
 //
 
-void Tool_musicxml2hum::cleanupMeasures(HumdrumFile& outfile,
-		vector<HLp> measures) {
+string Tool_musicxml2hum::convertFiguredBassNumber(const xml_node& figure) {
+	string output;
+	xml_node fnum = figure.select_node("figure-number").node();
+	// assuming one each of prefix/suffix:
+	xml_node prefixelement = figure.select_node("prefix").node();
+	xml_node suffixelement = figure.select_node("suffix").node();
 
-   HumdrumToken* token;
-	for (int i=0; i<outfile.getLineCount(); i++) {
-		if (!outfile[i].isBarline()) {
-			continue;
+	string prefix;
+	if (prefixelement) {
+		prefix = prefixelement.child_value();
+	}
+
+	string suffix;
+	if (suffixelement) {
+		suffix = suffixelement.child_value();
+	}
+
+	string number;
+	if (fnum) {
+		number = fnum.child_value();
+	}
+
+	string accidental;
+	string slash;
+
+	if (prefix == "flat-flat") {
+		accidental = "--";
+	} else if (prefix == "flat") {
+		accidental = "-";
+	} else if (prefix == "double-sharp" || prefix == "sharp-sharp") {
+		accidental = "##";
+	} else if (prefix == "sharp") {
+		accidental = "#";
+	} else if (prefix == "natural") {
+		accidental = "n";
+	} else if (suffix == "flat-flat") {
+		accidental = "--r";
+	} else if (suffix == "flat") {
+		accidental = "-r";
+	} else if (suffix == "double-sharp" || suffix == "sharp-sharp") {
+		accidental = "##r";
+	} else if (suffix == "sharp") {
+		accidental = "#r";
+	} else if (suffix == "natural") {
+		accidental = "nr";
+	}
+
+	// If suffix is "cross", "slash" or "backslash",  then an accidental
+	// should be given (probably either a natural or a sharp in general, but
+	// could be a flat).  At the moment do not assign the accidental, but
+	// in the future assign an accidental to the slashed figure, probably
+	// with a post-processing tool.
+	if (suffix == "cross" || prefix == "cross" || suffix == "vertical" || prefix == "vertical") {
+		slash = "|";
+		if (accidental.empty()) {
+			accidental = "#";
 		}
-		if (!outfile[i+1].isInterpretation()) {
-			int fieldcount = outfile[i+1].getFieldCount();
-			for (int j=1; j<fieldcount; j++) {
-				token = new HumdrumToken("=");
-				outfile[i].appendToken(token);
-			}
+	} else if ((suffix == "backslash" || suffix == "back-slash") || (prefix == "backslash" || prefix == "back-slash")) {
+		slash = "\\";
+		if (accidental.empty()) {
+			accidental = "#";
+		}
+	} else if ((suffix == "slash") || (prefix == "slash")) {
+		slash = "/";
+		if (accidental.empty()) {
+			accidental = "-";
 		}
 	}
-}
 
+	string editorial;
+	string extension;
 
+	xml_node extendelement = figure.select_node("extend").node();
+	if (extendelement) {
+		string typestring = extendelement.attribute("type").value();
+		if (typestring == "start") {
+			extension = "_";
+		}
+	}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::insertSingleMeasure --
-//
+	output += accidental + number + slash + editorial + extension;
 
-void Tool_musicxml2hum::insertSingleMeasure(HumdrumFile& outfile) {
-	HLp line = new HumdrumLine;
-	HumdrumToken* token;
-	token = new HumdrumToken("=");
-	line->appendToken(token);
-	line->createLineFromTokens();
-	outfile.appendLine(line);
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::insertAllToken --
+// Tool_musicxml2hum::getDynanmicsParameters --  Already presumed to be
+//     a dynamic.
 //
 
-void Tool_musicxml2hum::insertAllToken(HumdrumFile& outfile,
-		vector<MxmlPart>& partdata, const string& common) {
-
-	HLp line = new HumdrumLine;
-	HumdrumToken* token;
+string Tool_musicxml2hum::getDynamicsParameters(xml_node element) {
+	string output;
+	if (!nodeType(element, "direction")) {
+		return output;
+	}
 
-	int i, j;
-	for (i=0; i<(int)partdata.size(); i++) {
-		for (j=0; j<(int)partdata[i].getStaffCount(); j++) {
-			token = new HumdrumToken(common);
-			line->appendToken(token);
-		}
-		for (j=0; j<(int)partdata[i].getVerseCount(); j++) {
-			token = new HumdrumToken(common);
-			line->appendToken(token);
-		}
+	xml_attribute placement = element.attribute("placement");
+	if (!placement) {
+		return output;
+	}
+	string value = placement.value();
+	if (value == "above") {
+		output = ":a";
+	}
+	xml_node child = element.first_child();
+	if (!child) {
+		return output;
+	}
+	if (!nodeType(child, "direction-type")) {
+		return output;
+	}
+	xml_node grandchild = child.first_child();
+	if (!grandchild) {
+		return output;
+	}
+	if (!nodeType(grandchild, "wedge")) {
+		return output;
 	}
-	outfile.appendLine(line);
+
+	xml_attribute wtype = grandchild.attribute("type");
+	if (!wtype) {
+		return output;
+	}
+	string value2 = wtype.value();
+	if (value2 == "stop") {
+		// don't apply parameters to ends of hairpins.
+		output = "";
+	}
+
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::insertMeasure --
+// Tool_musicxml2hum::getFiguredBassParameters --  Already presumed to be
+//     figured bass.
 //
 
-bool Tool_musicxml2hum::insertMeasure(HumGrid& outdata, int mnum,
-		vector<MxmlPart>& partdata, vector<int> partstaves) {
-
-	GridMeasure* gm = outdata.addMeasureToBack();
-
-	MxmlMeasure* xmeasure;
-	vector<MxmlMeasure*> measuredata;
-	vector<vector<SimultaneousEvents>* > sevents;
-	int i;
-
-	for (i=0; i<(int)partdata.size(); i++) {
-		xmeasure = partdata[i].getMeasure(mnum);
-		measuredata.push_back(xmeasure);
-		if (i==0) {
-			gm->setDuration(partdata[i].getMeasure(mnum)->getDuration());
-			gm->setTimestamp(partdata[i].getMeasure(mnum)->getTimestamp());
-			gm->setTimeSigDur(partdata[i].getMeasure(mnum)->getTimeSigDur());
-		}
-		checkForDummyRests(xmeasure);
-		sevents.push_back(xmeasure->getSortedEvents());
-		if (i == 0) {
-			// only checking measure style of first barline
-			gm->setBarStyle(xmeasure->getBarStyle());
-		}
+string Tool_musicxml2hum::getFiguredBassParameters(xml_node element) {
+	string output;
+	if (!nodeType(element, "figured-bass")) {
+		return output;
 	}
+	return output;
+}
 
-	vector<HumNum> curtime(partdata.size());
-	vector<HumNum> measuredurs(partdata.size());
-	vector<int> curindex(partdata.size(), 0); // assuming data in a measure...
-	HumNum nexttime = -1;
-
-	vector<vector<MxmlEvent*>> endingDirections(partdata.size());
 
-	HumNum tsdur;
-	for (i=0; i<(int)curtime.size(); i++) {
-		tsdur = measuredata[i]->getTimeSigDur();
-		if ((tsdur == 0) && (i > 0)) {
-			tsdur = measuredata[i-1]->getTimeSigDur();
-			measuredata[i]->setTimeSigDur(tsdur);
-		}
 
-		// Keep track of hairpin endings that should be attached
-		// the the previous note (and doubling the ending marker
-		// to indicate that the timestamp of the ending is at the
-		// end rather than the start of the note.
-		vector<MxmlEvent*>& events = measuredata[i]->getEventList();
-		xml_node hairpin = xml_node(NULL);
-		for (int j=(int)events.size() - 1; j >= 0; j--) {
-			if (events[j]->getElementName() == "note") {
-				if (hairpin) {
-					events[j]->setHairpinEnding(hairpin);
-					hairpin = xml_node(NULL);
-				}
-				break;
-			} else if (events[j]->getElementName() == "direction") {
-				stringstream ss;
-				ss.str("");
-				events[j]->getNode().print(ss);
-				if (ss.str().find("wedge") != string::npos) {
-					if (ss.str().find("stop") != string::npos) {
-						hairpin = events[j]->getNode();
-					}
-				}
-			}
-		}
+//////////////////////////////
+//
+// Tool_musicxml2hum::getHairpinString --
+//
+// Hairpins:
+//      <direction placement="below">
+//        <direction-type>
+//          <wedge default-y="-75" number="2" spread="15" type="diminuendo"/>
+//        </direction-type>
+//      </direction>
+//
+//      <direction>
+//        <direction-type>
+//          <wedge spread="15" type="stop"/>
+//        </direction-type>
+//      </direction>
+//
 
-		if (VoiceDebugQ) {
-			for (int j=0; j<(int)events.size(); j++) {
-				cerr << "!!ELEMENT: ";
-				cerr << "\tTIME:  " << events[j]->getStartTime();
-				cerr << "\tSTi:   " << events[j]->getStaffIndex();
-				cerr << "\tVi:    " << events[j]->getVoiceIndex();
-				cerr << "\tTS:    " << events[j]->getStartTime();
-				cerr << "\tDUR:   " << events[j]->getDuration();
-				cerr << "\tPITCH: " << events[j]->getKernPitch();
-				cerr << "\tNAME:  " << events[j]->getElementName();
-				cerr << endl;
-			}
-			cerr << "======================================" << endl;
+string Tool_musicxml2hum::getHairpinString(xml_node element, int partindex) {
+	if (nodeType(element, "wedge")) {
+		xml_attribute wtype = element.attribute("type");
+		if (!wtype) {
+			return "???";
 		}
-		if (!(*sevents[i]).empty()) {
-			curtime[i] = (*sevents[i])[curindex[i]].starttime;
+		string output;
+		string wstring = wtype.value();
+		if (wstring == "diminuendo") {
+			m_stop_char.at(partindex) = "]";
+			output = ">";
+		} else if (wstring == "crescendo") {
+			m_stop_char.at(partindex) = "[";
+			output = "<";
+		} else if (wstring == "stop") {
+			output = m_stop_char.at(partindex);
 		} else {
-			curtime[i] = tsdur;
-		}
-		if (nexttime < 0) {
-			nexttime = curtime[i];
-		} else if (curtime[i] < nexttime) {
-			nexttime = curtime[i];
+			output = "???";
 		}
-		measuredurs[i] = measuredata[i]->getDuration();
+		return output;
 	}
 
-	bool allend = false;
-	vector<SimultaneousEvents*> nowevents;
-	vector<int> nowparts;
-	bool status = true;
+	return "???";
+}
 
-	HumNum processtime = nexttime;
-	while (!allend) {
-		nowevents.resize(0);
-		nowparts.resize(0);
-		allend = true;
-		processtime = nexttime;
-		nexttime = -1;
-		for (i = (int)partdata.size()-1; i >= 0; i--) {
-			if (curindex[i] >= (int)(*sevents[i]).size()) {
-				continue;
-			}
 
-			if ((*sevents[i])[curindex[i]].starttime == processtime) {
-				SimultaneousEvents* thing = &(*sevents[i])[curindex[i]];
-				nowevents.push_back(thing);
-				nowparts.push_back(i);
-				curindex[i]++;
-			}
 
-			if (curindex[i] < (int)(*sevents[i]).size()) {
-				allend = false;
-				if ((nexttime < 0) ||
-						((*sevents[i])[curindex[i]].starttime < nexttime)) {
-					nexttime = (*sevents[i])[curindex[i]].starttime;
-				}
-			}
-		}
-		status &= convertNowEvents(outdata.back(),
-				nowevents, nowparts, processtime, partdata, partstaves);
+//////////////////////////////
+//
+// Tool_musicxml2hum::getDynamicString --
+//
 
-		// Remove all figured bass numbers for this nowtime so that they are not
-		// accidentally displayed in the next nowtime, which can currently
-		// happen if there are no nonzerodur events in the same part
-		for (int i=0; i<(int)m_current_figured_bass.size(); i++) {
-			m_current_figured_bass[i].clear();
-		}
-	}
+string Tool_musicxml2hum::getDynamicString(xml_node element) {
 
-	if (offsetHarmony.size() > 0) {
-		insertOffsetHarmonyIntoMeasure(outdata.back());
-	}
-	if (m_offsetFiguredBass.size() > 0) {
-		insertOffsetFiguredBassIntoMeasure(outdata.back());
+	if (nodeType(element, "f")) {
+		return "f";
+	} else if (nodeType(element, "p")) {
+		return "p";
+	} else if (nodeType(element, "mf")) {
+		return "mf";
+	} else if (nodeType(element, "mp")) {
+		return "mp";
+	} else if (nodeType(element, "ff")) {
+		return "ff";
+	} else if (nodeType(element, "pp")) {
+		return "pp";
+	} else if (nodeType(element, "sf")) {
+		return "sf";
+	} else if (nodeType(element, "sfp")) {
+		return "sfp";
+	} else if (nodeType(element, "sfpp")) {
+		return "sfpp";
+	} else if (nodeType(element, "fp")) {
+		return "fp";
+	} else if (nodeType(element, "rf")) {
+		return "rfz";
+	} else if (nodeType(element, "rfz")) {
+		return "rfz";
+	} else if (nodeType(element, "sfz")) {
+		return "sfz";
+	} else if (nodeType(element, "sffz")) {
+		return "sffz";
+	} else if (nodeType(element, "fz")) {
+		return "fz";
+	} else if (nodeType(element, "fff")) {
+		return "fff";
+	} else if (nodeType(element, "ppp")) {
+		return "ppp";
+	} else if (nodeType(element, "ffff")) {
+		return "ffff";
+	} else if (nodeType(element, "pppp")) {
+		return "pppp";
+	} else {
+		return "???";
 	}
-	return status;
 }
 
 
-
 //////////////////////////////
 //
-// Tool_musicxml2hum::insertOffsetFiguredBassIntoMeasure --
+// Tool_musicxml2hum::addFiguredBass --
+//
+// Such as:
+//
+//      <figured-bass>
+//        <figure>
+//          <figure-number>0</figure-number>
+//        </figure>
+//      </figured-bass>
+// or:
+//      <figured-bass>
+//        <figure>
+//          <figure-number>5</figure-number>
+//          <suffix>backslash</suffix>
+//        </figure>
+//        <figure>
+//          <figure-number>2</figure-number>
+//          <suffix>cross</suffix>
+//        </figure>
+//      </figured-bass>
+//
+//      <figured-bass parentheses="yes">
+//        <figure>
+//          <prefix>flat</prefix>
+//        </figure>
+//      </figured-bass>
+//
+// Case where there is more than one figure attached to a note:
+// (notice <duration> element)
+//
+//      <figured-bass>
+//        <figure>
+//          <figure-number>6</figure-number>
+//          <extend type="start" />
+//        </figure>
+//        <duration>2</duration>
+//      <figured-bass>
+//
 //
 
-void Tool_musicxml2hum::insertOffsetFiguredBassIntoMeasure(GridMeasure* gm) {
-	if (m_offsetFiguredBass.empty()) {
-		return;
+int Tool_musicxml2hum::addFiguredBass(GridPart* part, MxmlEvent* event, HumNum nowtime, int partindex) {
+	if (m_current_figured_bass[partindex].empty()) {
+		return 0;
 	}
 
-	bool beginQ = true;
-	for (auto it = gm->begin(); it != gm->end(); ++it) {
-		GridSlice* gs = *it;
-		if (!gs->isNoteSlice()) {
-			// Only attached harmony to data lines.
+	int dursum = 0;
+	for (int i=0; i<(int)m_current_figured_bass[partindex].size(); i++) {
+		xml_node fnode = m_current_figured_bass[partindex].at(i);
+		if (!fnode) {
+			// strange problem
 			continue;
 		}
-		HumNum timestamp = gs->getTimestamp();
-		for (int i=0; i<(int)m_offsetFiguredBass.size(); i++) {
-			if (m_offsetFiguredBass[i].token == NULL) {
-				continue;
- 			}
-			if (m_offsetFiguredBass[i].timestamp == timestamp) {
-				// this is the slice to insert the harmony
-				gs->at(m_offsetFiguredBass[i].partindex)->setFiguredBass(m_offsetFiguredBass[i].token);
-				m_offsetFiguredBass[i].token = NULL;
-			} else if (m_offsetFiguredBass[i].timestamp < timestamp) {
-				if (beginQ) {
-					cerr << "Error: Cannot insert harmony " << m_offsetFiguredBass[i].token
-					     << " at timestamp " << m_offsetFiguredBass[i].timestamp
-					     << " since first timestamp in measure is " << timestamp << endl;
-				} else {
-					m_forceRecipQ = true;
-					// go back to previous note line and insert
-					// new slice to store the harmony token
-					auto tempit = it;
-					tempit--;
-					while (tempit != gm->end()) {
-						if ((*tempit)->getTimestamp() == (*it)->getTimestamp()) {
-							tempit--;
-							continue;
-						}
-						int partcount = (int)(*tempit)->size();
-						tempit++;
-						GridSlice* newgs = new GridSlice(gm, m_offsetFiguredBass[i].timestamp,
-								SliceType::Notes, partcount);
-						newgs->at(m_offsetFiguredBass[i].partindex)->setFiguredBass(m_offsetFiguredBass[i].token);
-						gm->insert(tempit, newgs);
-						m_offsetFiguredBass[i].token = NULL;
-						break;
-					}
-				}
-			}
+		string fstring = getFiguredBassString(fnode);
+
+		HTp ftok = new HumdrumToken(fstring);
+		if (i == 0) {
+			part->setFiguredBass(ftok);
+		} else {
+			// store the figured bass for later handling at end of
+			// measure processing.
+			MusicXmlFiguredBassInfo finfo;
+			finfo.timestamp = dursum;
+			finfo.timestamp /= (int)event->getQTicks();
+			finfo.timestamp += nowtime;
+			finfo.partindex = partindex;
+			finfo.token = ftok;
+			m_offsetFiguredBass.push_back(finfo);
+		}
+		if (i < (int)m_current_figured_bass[partindex].size() - 1) {
+			dursum += getFiguredBassDuration(fnode);
 		}
-		beginQ = false;
-	}
-	// If there are still valid harmonies in the input list, apppend
-	// them to the end of the measure.
-	for (int i=0; i<(int)m_offsetFiguredBass.size(); i++) {
-		if (m_offsetFiguredBass[i].token == NULL) {
-			continue;
- 		}
-		m_forceRecipQ = true;
-		int partcount = (int)gm->back()->size();
-		GridSlice* newgs = new GridSlice(gm, m_offsetFiguredBass[i].timestamp,
-				SliceType::Notes, partcount);
-		newgs->at(m_offsetFiguredBass[i].partindex)->setFiguredBass(m_offsetFiguredBass[i].token);
-		gm->insert(gm->end(), newgs);
-		m_offsetFiguredBass[i].token = NULL;
 	}
-	m_offsetFiguredBass.clear();
-}
-
-
+	m_current_figured_bass[partindex].clear();
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::insertOffsetHarmonyIntoMeasure --
-//
+	return 1;
 
-void Tool_musicxml2hum::insertOffsetHarmonyIntoMeasure(GridMeasure* gm) {
-	if (offsetHarmony.empty()) {
-		return;
-	}
-	// the offsetHarmony list should probably be time sorted first, and then
-	// iterate through the slices once.  But there should not be many offset
-	bool beginQ = true;
-	for (auto it = gm->begin(); it != gm->end(); ++it) {
-		GridSlice* gs = *it;
-		if (!gs->isNoteSlice()) {
-			// Only attached harmony to data lines.
-			continue;
-		}
-		HumNum timestamp = gs->getTimestamp();
-		for (int i=0; i<(int)offsetHarmony.size(); i++) {
-			if (offsetHarmony[i].token == NULL) {
-				continue;
- 			}
-			if (offsetHarmony[i].timestamp == timestamp) {
-				// this is the slice to insert the harmony
-				gs->at(offsetHarmony[i].partindex)->setHarmony(offsetHarmony[i].token);
-				offsetHarmony[i].token = NULL;
-			} else if (offsetHarmony[i].timestamp < timestamp) {
-				if (beginQ) {
-					cerr << "Error: Cannot insert harmony " << offsetHarmony[i].token
-					     << " at timestamp " << offsetHarmony[i].timestamp
-					     << " since first timestamp in measure is " << timestamp << endl;
-				} else {
-					m_forceRecipQ = true;
-					// go back to previous note line and insert
-					// new slice to store the harmony token
-					auto tempit = it;
-					tempit--;
-					while (tempit != gm->end()) {
-						if ((*tempit)->getTimestamp() == (*it)->getTimestamp()) {
-							tempit--;
-							continue;
-						}
-						int partcount = (int)(*tempit)->size();
-						tempit++;
-						GridSlice* newgs = new GridSlice(gm, offsetHarmony[i].timestamp,
-								SliceType::Notes, partcount);
-						newgs->at(offsetHarmony[i].partindex)->setHarmony(offsetHarmony[i].token);
-						gm->insert(tempit, newgs);
-						offsetHarmony[i].token = NULL;
-						break;
-					}
+/* deal with figured bass layout parameters?:
+			string fparam = getFiguredBassParameters(fnode);
+			if (fparam != "") {
+				GridMeasure *gm = slice->getMeasure();
+				string fullparam = "!LO:FB" + fparam;
+				if (gm) {
+					gm->addFiguredBassLayoutParameters(slice, partindex, fullparam);
 				}
 			}
 		}
-		beginQ = false;
-	}
-	// If there are still valid harmonies in the input list, apppend
-	// them to the end of the measure.
-	for (int i=0; i<(int)offsetHarmony.size(); i++) {
-		if (offsetHarmony[i].token == NULL) {
-			continue;
- 		}
-		m_forceRecipQ = true;
-		int partcount = (int)gm->back()->size();
-		GridSlice* newgs = new GridSlice(gm, offsetHarmony[i].timestamp,
-				SliceType::Notes, partcount);
-		newgs->at(offsetHarmony[i].partindex)->setHarmony(offsetHarmony[i].token);
-		gm->insert(gm->end(), newgs);
-		offsetHarmony[i].token = NULL;
-	}
-	offsetHarmony.clear();
+*/
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::checkForDummyRests --
+// Tool_musicxml2hum::getFiguredBassString -- extract any figured bass string
+//   from XML node.
 //
 
-void Tool_musicxml2hum::checkForDummyRests(MxmlMeasure* measure) {
-	vector<MxmlEvent*>& events = measure->getEventList();
-
-	MxmlPart* owner = measure->getOwner();
-	int maxstaff = owner->getStaffCount();
-	vector<vector<int> > itemcounts(maxstaff);
-	for (int i=0; i<(int)itemcounts.size(); i++) {
-		itemcounts[i].resize(1);
-		itemcounts[i][0] = 0;
-	}
-
-	for (int i=0; i<(int)events.size(); i++) {
-		if (!nodeType(events[i]->getNode(), "note")) {
-			// only counting notes/(rests) for now.  <forward> may
-			// need to be counted.
-			continue;
-		}
-     	int voiceindex = events[i]->getVoiceIndex();
-		int staffindex = events[i]->getStaffIndex();
-
-		if (voiceindex < 0) {
-			continue;
-		}
-		if (staffindex < 0) {
-			continue;
-		}
+string Tool_musicxml2hum::getFiguredBassString(xml_node fnode) {
+	string output;
 
-		if (staffindex >= (int)itemcounts.size()) {
-			itemcounts.resize(staffindex+1);
+	// Parentheses can only enclose an entire figure stack, not
+	// individual numbers or accidentals on numbers in MusicXML,
+	// so apply an editorial mark for parentheses.
+	string editorial;
+	xml_attribute pattr = fnode.attribute("parentheses");
+	if (pattr) {
+		string pval = pattr.value();
+		if (pval == "yes") {
+			editorial = "i";
 		}
+	}
+	// There is no bracket for FB in musicxml (3.0).
 
-		if (voiceindex >= (int)itemcounts[staffindex].size()) {
-			int oldsize = (int)itemcounts[staffindex].size();
-			int newsize = voiceindex + 1;
-			itemcounts[staffindex].resize(newsize);
-			for (int j=oldsize; j<newsize; j++) {
-					  itemcounts[staffindex][j] = 0;
-			}
+	auto children = fnode.select_nodes("figure");
+	for (int i=0; i<(int)children.size(); i++) {
+		output += convertFiguredBassNumber(children[i].node());
+		output += editorial;
+		if (i < (int)children.size() - 1) {
+			output += " ";
 		}
-		itemcounts[staffindex][voiceindex]++;
-  	}
+	}
 
-	bool dummy = false;
-	for (int i=0; i<(int)itemcounts.size(); i++) {
-		for (int j=0; j<(int)itemcounts[i].size(); j++) {
-			if (itemcounts[i][j]) {
-				continue;
-			}
-			HumNum mdur = measure->getDuration();
-			HumNum starttime = measure->getStartTime();
-      	measure->addDummyRest(starttime, mdur, i, j);
-			measure->forceLastInvisible();
-			dummy = true;
+	HumRegex hre;
+	hre.replaceDestructive(output, "", R"(^\s+|\s+$)");
+
+	if (output.empty()) {
+		if (children.size()) {
+			cerr << "WARNING: figured bass string is empty but has "
+				<< children.size() << " figure elements as children. "
+				<< "The output has been replaced with \".\"" << endl;
 		}
+		output = ".";
 	}
 
-	if (dummy) {
-		measure->sortEvents();
-	}
+	return output;
 
+	// HTp fbtok = new HumdrumToken(fbstring);
+	// part->setFiguredBass(fbtok);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::convertNowEvents --
+// Tool_musicxml2hum::addHarmony --
 //
 
-bool Tool_musicxml2hum::convertNowEvents(GridMeasure* outdata,
-		vector<SimultaneousEvents*>& nowevents, vector<int>& nowparts,
-		HumNum nowtime, vector<MxmlPart>& partdata, vector<int>& partstaves) {
-
-	if (nowevents.size() == 0) {
-		// cout << "NOW EVENTS ARE EMPTY" << endl;
-		return true;
+int Tool_musicxml2hum::addHarmony(GridPart* part, MxmlEvent* event, HumNum nowtime,
+		int partindex) {
+	xml_node hnode = event->getHNode();
+	if (!hnode) {
+		return 0;
 	}
 
-	//if (0 && VoiceDebugQ) {
-	//	for (int j=0; j<(int)nowevents.size(); j++) {
-	//		vector<MxmlEvent*> nz = nowevents[j]->nonzerodur;
-	//		for (int i=0; i<(int)nz.size(); i++) {
-	//			cerr << "NOWEVENT NZ NAME: " << nz[i]->getElementName()
-	//			     << "<\t" << nz[i]->getKernPitch() << endl;
-	//		}
-	//	}
-	//}
-
-	appendZeroEvents(outdata, nowevents, nowtime, partdata);
-
-	bool hasNonZeroDurElements = false;
-	for (const SimultaneousEvents* event : nowevents) {
-		if (event->nonzerodur.size() != 0) {
-			hasNonZeroDurElements = true;
-			break;
-		}
-	}
-	if (!hasNonZeroDurElements) {
-		// no duration events (should be a terminal barline)
-		// ignore and deal with in calling function.
-		return true;
+	// fill in X with the harmony values from the <harmony> node
+	string hstring = getHarmonyString(hnode);
+	int offset = getHarmonyOffset(hnode);
+	HTp htok = new HumdrumToken(hstring);
+	if (offset == 0) {
+		part->setHarmony(htok);
+	} else {
+		MusicXmlHarmonyInfo hinfo;
+		hinfo.timestamp = offset;
+		hinfo.timestamp /= (int)event->getQTicks();
+		hinfo.timestamp += nowtime;
+		hinfo.partindex = partindex;
+		hinfo.token = htok;
+		offsetHarmony.push_back(hinfo);
 	}
 
-	appendNonZeroEvents(outdata, nowevents, nowtime, partdata);
-
-	handleFiguredBassWithoutNonZeroEvent(nowevents, nowtime);
-
-	return true;
+	return 1;
 }
 
 
 
-/////////////////////////////
+//////////////////////////////
 //
-// Tool_musicxml2hum::handleFiguredBassWithoutNonZeroEvent --
+// Tool_musicxml2hum::getHarmonyOffset --
+//   <harmony default-y="40">
+//       <root>
+//           <root-step>C</root-step>
+//       </root>
+//       <kind>major-ninth</kind>
+//       <bass>
+//           <bass-step>E</bass-step>
+//       </bass>
+//       <offset>-8</offset>
+//   </harmony>
 //
 
-void Tool_musicxml2hum::handleFiguredBassWithoutNonZeroEvent(vector<SimultaneousEvents*>& nowevents, HumNum nowtime) {
-	vector<int> nonZeroParts;
-	vector<MxmlEvent> floatingFiguredBass;
-	for (const SimultaneousEvents* sevent : nowevents) {
-		for (MxmlEvent* mxmlEvent : sevent->nonzerodur) {
-			nonZeroParts.push_back(mxmlEvent->getPartIndex());
-		}
-		for (MxmlEvent* mxmlEvent : sevent->zerodur) {
-			if ("figured-bass" == mxmlEvent->getElementName()) {
-				if (std::find(nonZeroParts.begin(), nonZeroParts.end(), mxmlEvent->getPartIndex()) == nonZeroParts.end()) {
-					// cerr << mxmlEvent->getNode() << "\n";
-					string fstring = getFiguredBassString(mxmlEvent->getNode());
-					HTp ftok = new HumdrumToken(fstring);
-					MusicXmlFiguredBassInfo finfo;
-					finfo.timestamp = nowtime;
-					finfo.partindex = mxmlEvent->getPartIndex();
-					finfo.token = ftok;
-					m_offsetFiguredBass.push_back(finfo);
-					// cerr << "ADD FLOATING FB NUM " << fstring << " " << nowtime << "\n";
-				}
-			}
+int Tool_musicxml2hum::getHarmonyOffset(xml_node hnode) {
+	if (!hnode) {
+		return 0;
+	}
+	xml_node child = hnode.first_child();
+	if (!child) {
+		return 0;
+	}
+	while (child) {
+		if (nodeType(child, "offset")) {
+			return atoi(child.child_value());
 		}
+		child = child.next_sibling();
 	}
+
+	return 0;
 }
 
 
 
-/////////////////////////////
+//////////////////////////////
 //
-// Tool_musicxml2hum::appendNonZeroEvents --
+// Tool_musicxml2hum::getFiguredBaseDuration -- Needed for cases where there is more
+//   than one figure attached to a note.  Return value is the integer of the duration
+//   element.  If will need to be converted to quarter notes later.
+//
+//   <figured-bass>
+//      <figure>
+//         <figure-number>5</figure-number>
+//      </figure>
+//      <figure>
+//         <figure-number>3</figure-number>
+//      </figure>
+//      <duration>2</duration>    <-- get this field if it exists.
+//   </figured-bass>
 //
 
-void Tool_musicxml2hum::appendNonZeroEvents(GridMeasure* outdata,
-		vector<SimultaneousEvents*>& nowevents, HumNum nowtime,
-		vector<MxmlPart>& partdata) {
-
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-			SliceType::Notes);
-	if (outdata->empty()) {
-		outdata->push_back(slice);
-	} else {
-		HumNum lasttime = outdata->back()->getTimestamp();
-		if (nowtime >= lasttime) {
-			outdata->push_back(slice);
-		} else {
-			// travel backwards in the measure until the correct
-			// time position is found.
-			auto it = outdata->rbegin();
-			while (it != outdata->rend()) {
-				lasttime = (*it)->getTimestamp();
-				if (nowtime >= lasttime) {
-					outdata->insert(it.base(), slice);
-					break;
-				}
-				it++;
-			}
-		}
+int Tool_musicxml2hum::getFiguredBassDuration(xml_node fnode) {
+	if (!fnode) {
+		return 0;
 	}
-	slice->initializePartStaves(partdata);
-
-	for (int i=0; i<(int)nowevents.size(); i++) {
-		vector<MxmlEvent*>& events = nowevents[i]->nonzerodur;
-		for (int j=0; j<(int)events.size(); j++) {
-			addEvent(slice, outdata, events[j], nowtime);
+	xml_node child = fnode.first_child();
+	if (!child) {
+		return 0;
+	}
+	while (child) {
+		if (nodeType(child, "duration")) {
+			return atoi(child.child_value());
 		}
+		child = child.next_sibling();
 	}
+
+	return 0;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addEvent -- Add a note or rest.
+// Tool_musicxml2hum::getHarmonyString --
+//   <harmony default-y="40">
+//       <root>
+//           <root-step>C</root-step>
+//       </root>
+//       <kind>major-ninth</kind>
+//       <bass>
+//           <bass-step>E</bass-step>
+//       </bass>
+//       <offset>-8</offset>
+//   </harmony>
+//
+// For harmony labels from Musescore:
+//
+//    <harmony print-frame="no">
+//      <root>
+//        <root-step text="">C</root-step>
+//        </root>
+//      <kind text="V43">none</kind>
+//      </harmony>
+//
+// Converts to: "V43" ignoring the root-step and kind contents
+// if they are both "C" and "none".
 //
 
-void Tool_musicxml2hum::addEvent(GridSlice* slice, GridMeasure* outdata, MxmlEvent* event,
-		HumNum nowtime) {
-	int partindex;  // which part the event occurs in
-	int staffindex; // which staff the event occurs in (need to fix)
-	int voiceindex; // which voice the event occurs in (use for staff)
-
-	partindex  = event->getPartIndex();
-	staffindex = event->getStaffIndex();
-	voiceindex = event->getVoiceIndex();
-
-	string recip;
-	string pitch;
-	string prefix;
-	string postfix;
-	bool invisible = false;
-	bool primarynote = true;
-	vector<int> slurdirs;
-
-	if (!event->isFloating()) {
-		recip     = event->getRecip();
-		HumRegex hre;
-		if (hre.search(recip, "(\\d+)%(\\d+)(\\.*)")) {
-			int first = hre.getMatchInt(1);
-			int second = hre.getMatchInt(2);
-			string dots = hre.getMatch(3);
-			if (dots.empty()) {
-				if ((first == 1) && (second == 2)) {
-					hre.replaceDestructive(recip, "0", "1%2");
-				}
-				if ((first == 1) && (second == 4)) {
-					hre.replaceDestructive(recip, "00", "1%4");
-				}
-				if ((first == 1) && (second == 3)) {
-					hre.replaceDestructive(recip, "0.", "1%3");
-				}
-				if ((first == 2) && (second == 3)) {
-					hre.replaceDestructive(recip, "1.", "2%3");
+string Tool_musicxml2hum::getHarmonyString(xml_node hnode) {
+	if (!hnode) {
+		return "";
+	}
+	xml_node child = hnode.first_child();
+	if (!child) {
+		return "";
+	}
+	string root;
+	string kind;
+	string kindtext;
+	string bass;
+	int rootalter = 0;
+	int bassalter = 0;
+	xml_node grandchild;
+	while (child) {
+		if (nodeType(child, "root")) {
+			grandchild = child.first_child();
+			while (grandchild) {
+				if (nodeType(grandchild, "root-step")) {
+					root = grandchild.child_value();
+				} if (nodeType(grandchild, "root-alter")) {
+					rootalter = atoi(grandchild.child_value());
 				}
-			} else {
-				if ((first == 1) && (second == 2)) {
-					string original = "1%2" + dots;
-					string replacement = "0" + dots;
-					hre.replaceDestructive(recip, replacement, original);
+				grandchild = grandchild.next_sibling();
+			}
+		} else if (nodeType(child, "kind")) {
+			kindtext = getAttributeValue(child, "text");
+			kind = child.child_value();
+			if (kind == "") {
+				kind = child.attribute("text").value();
+				transform(kind.begin(), kind.end(), kind.begin(), ::tolower);
+			}
+		} else if (nodeType(child, "bass")) {
+			grandchild = child.first_child();
+			while (grandchild) {
+				if (nodeType(grandchild, "bass-step")) {
+					bass = grandchild.child_value();
+				} if (nodeType(grandchild, "bass-alter")) {
+					bassalter = atoi(grandchild.child_value());
 				}
+				grandchild = grandchild.next_sibling();
 			}
 		}
+		child = child.next_sibling();
+	}
+	stringstream ss;
 
-		pitch     = event->getKernPitch();
-		prefix    = event->getPrefixNoteInfo();
-		postfix   = event->getPostfixNoteInfo(primarynote, recip);
-		if (postfix.find("@") != string::npos) {
-			m_hasTremoloQ = true;
-		}
-		bool grace     = event->isGrace();
-		int slurstarts = event->hasSlurStart(slurdirs);
-		int slurstops = event->hasSlurStop();
-
-		if (pitch.find('r') != std::string::npos) {
-			string restpitch =  event->getRestPitch();
-			pitch += restpitch;
-		}
+	if ((kind == "none") && (root == "C") && !kindtext.empty()) {
+		ss << kindtext;
+		string output = cleanSpaces(ss.str());
+		return output;
+	}
 
-		for (int i=0; i<slurstarts; i++) {
-			prefix.insert(0, "(");
-		// Ignoring slur direction for now.
-		// if (slurstart) {
-		// 	prefix.insert(0, "(");
-		// 	if (slurdir) {
-		// 		if (slurdir > 0) {
-		// 			prefix.insert(1, ">");
-		// 			m_slurabove++;
-		// 		} else if (slurdir < 0) {
-		// 			prefix.insert(1, "<");
-		// 			m_slurbelow++;
-		// 		}
-		// 	}
-		// }
-		}
-		for (int i=0; i<slurstops; i++) {
-			postfix.push_back(')');
-		}
-		//if (slurstop) {
-		//	postfix.push_back(')');
-		//}
+	ss << root;
 
-		invisible = isInvisible(event);
-		if (event->isInvisible()) {
-			invisible = true;
+	if (rootalter > 0) {
+		for (int i=0; i<rootalter; i++) {
+			ss << "#";
 		}
-
-		if (grace) {
-			HumNum modification;
-			HumNum dur = event->getEmbeddedDuration(modification, event->getNode()) / 4;
-			if (dur.getNumerator() == 1) {
-				recip = to_string(dur.getDenominator()) + "q";
-			} else {
-				recip = "q";
-			}
-			if (!event->hasGraceSlash()) {
-				recip += "q";
-			}
+	} else if (rootalter < 0) {
+		for (int i=0; i<-rootalter; i++) {
+			ss << "-";
 		}
 	}
 
-	if (event->getCrossStaffOffset() > 0) {
-		m_staffbelow = true;
-	} else if (event->getCrossStaffOffset() < 0) {
-		m_staffabove = true;
+	if (root.size() && kind.size()) {
+		ss << " ";
+	}
+	ss << kind;
+	if (bass.size()) {
+		ss << "/";
 	}
+	ss << bass;
 
-	stringstream ss;
-	if (event->isFloating()) {
-		ss << ".";
-		HTp token = new HumdrumToken(ss.str());
-		slice->at(partindex)->at(staffindex)->setTokenLayer(voiceindex, token,
-			event->getDuration());
-	} else {
-		ss << prefix << recip << pitch << postfix;
-		if (invisible) {
-			ss << "yy";
+	if (bassalter > 0) {
+		for (int i=0; i<bassalter; i++) {
+			ss << "#";
 		}
-
-		// check for chord notes.
-		HTp token;
-		if (event->isChord()) {
-			addSecondaryChordNotes(ss, event, recip);
-			token = new HumdrumToken(ss.str());
-			slice->at(partindex)->at(staffindex)->setTokenLayer(voiceindex, token,
-				event->getDuration());
-		} else {
-			token = new HumdrumToken(ss.str());
-			slice->at(partindex)->at(staffindex)->setTokenLayer(voiceindex, token,
-				event->getDuration());
+	} else if (bassalter < 0) {
+		for (int i=0; i<-bassalter; i++) {
+			ss << "-";
 		}
 	}
 
-	if (DebugQ) {
-		cerr << "!!TOKEN: " << ss.str();
-		cerr << "\tTS: "    << event->getStartTime();
-		cerr << "\tDUR: "   << event->getDuration();
-		cerr << "\tSTn: "   << event->getStaffNumber();
-		cerr << "\tVn: "    << event->getVoiceNumber();
-		cerr << "\tSTi: "   << event->getStaffIndex();
-		cerr << "\tVi: "    << event->getVoiceIndex();
-		cerr << "\teNAME: " << event->getElementName();
-		cerr << endl;
-	}
+	string output = cleanSpaces(ss.str());
+	return output;
+}
 
-	int vcount = addLyrics(slice->at(partindex)->at(staffindex), event);
 
-	if (vcount > 0) {
-		event->reportVerseCountToOwner(staffindex, vcount);
-	}
 
-	int hcount = addHarmony(slice->at(partindex), event, nowtime, partindex);
-	if (hcount > 0) {
-		event->reportHarmonyCountToOwner(hcount);
-	}
+//////////////////////////////
+//
+// Tool_musicxml2hum::addLyrics --
+//
 
-	int fcount = addFiguredBass(slice->at(partindex), event, nowtime, partindex);
-	if (fcount > 0) {
-		event->reportFiguredBassToOwner();
+int Tool_musicxml2hum::addLyrics(GridStaff* staff, MxmlEvent* event) {
+	xml_node node = event->getNode();
+	if (!node) {
+		return 0;
 	}
-
-	if (m_current_brackets[partindex].size() > 0) {
-		for (int i=0; i<(int)m_current_brackets[partindex].size(); i++) {
-			event->setBracket(m_current_brackets[partindex].at(i));
+	HumRegex hre;
+	xml_node child = node.first_child();
+	xml_node grandchild;
+	// int max;
+	int number = 0;
+	vector<xml_node> verses;
+	string syllabic;
+	string text;
+	while (child) {
+		if (!nodeType(child, "lyric")) {
+			child = child.next_sibling();
+			continue;
 		}
-		m_current_brackets[partindex].clear();
-		addBrackets(slice, outdata, event, nowtime, partindex);
-	}
-
-	if (m_current_text.size() > 0) {
-		event->setTexts(m_current_text);
-		m_current_text.clear();
-		addTexts(slice, outdata, event->getPartIndex(), staffindex, voiceindex, event);
-	}
-
-	if (m_current_tempo.size() > 0) {
-		event->setTempos(m_current_tempo);
-		m_current_tempo.clear();
-		addTempos(slice, outdata, event->getPartIndex(), staffindex, voiceindex, event);
+		string value = child.attribute("number").value();
+		if (hre.search(value, R"(verse(\d+))")) {
+			// Fix for Sibelius which uses number="part8verse5" format.
+			number = stoi(hre.getMatch(1));
+		} else {
+			number = atoi(child.attribute("number").value());
+		}
+		if (number > 100) {
+			cerr << "Error: verse number is too large: number" << endl;
+			return 0;
+		}
+		if (number == (int)verses.size() + 1) {
+			verses.push_back(child);
+		} else if ((number > 0) && (number < (int)verses.size())) {
+			// replace a verse for some reason.
+			verses[number-1] = child;
+		} else if (number > 0) {
+			int oldsize = (int)verses.size();
+			int newsize = number;
+			verses.resize(newsize);
+			for (int i=oldsize; i<newsize; i++) {
+				verses[i] = xml_node(NULL);
+			}
+			verses[number-1] = child;
+		}
+		child = child.next_sibling();
 	}
 
-	if (m_current_dynamic[partindex].size()) {
-		// only processing the first dynamic at the current time point for now.
-		// Fix later so that multiple dynamics are handleded in the part at the
-		// same time.  The LO parameters for multiple dynamics will need to be
-		// qualified with "n=#".
-		for (int i=0; i<(int)m_current_dynamic[partindex].size(); i++) {
-			event->setDynamics(m_current_dynamic[partindex][i]);
-			string dparam = getDynamicsParameters(m_current_dynamic[partindex][i]);
-
-			event->reportDynamicToOwner();
-			addDynamic(slice->at(partindex), event, partindex);
-			if (dparam != "") {
-				// deal with multiple layout entries here...
-				GridMeasure *gm = slice->getMeasure();
-				string fullparam = "!LO:DY" + dparam;
-				if (gm) {
-					gm->addDynamicsLayoutParameters(slice, partindex, fullparam);
+	string finaltext;
+	string fontstyle;
+	HTp token;
+	for (int i=0; i<(int)verses.size(); i++) {
+		if (!verses[i]) {
+			// no verse so doing an empty slot.
+		} else {
+			child = verses[i].first_child();
+			finaltext = "";
+			while (child) {
+				if (nodeType(child, "syllabic")) {
+					syllabic = child.child_value();
+					child = child.next_sibling();
+					continue;
+				} else if (nodeType(child, "text")) {
+					fontstyle = child.attribute("font-style").value();
+					text = cleanSpaces(child.child_value());
+					if (fontstyle == "italic") {
+						text = "<i>" + text + "</i>";
+					}
+				} else if (nodeType(child, "elision")) {
+					finaltext += " ";
+					child = child.next_sibling();
+					continue;
+				} else {
+					// such as <extend>
+					child = child.next_sibling();
+					continue;
+				}
+				// escape text which would otherwise be reinterpreated
+				// as Humdrum syntax.
+				if (!text.empty()) {
+					if (text[0] == '!') {
+						text.insert(0, 1, '\\');
+					} else if (text[0] == '*') {
+						text.insert(0, 1, '\\');
+					}
+				}
+				child = child.next_sibling();
+				if (syllabic == "middle" ) {
+					finaltext += "-";
+					finaltext += text;
+					finaltext += "-";
+				} else if (syllabic == "end") {
+					finaltext += "-";
+					finaltext += text;
+				} else if (syllabic == "begin") {
+					finaltext += text;
+					finaltext += "-";
+				} else {
+					finaltext += text;
 				}
+				syllabic.clear();
 			}
 		}
-		m_current_dynamic[partindex].clear();
-	}
 
-	// see if a hairpin ending needs to be added before end of measure:
-	xml_node enode = event->getHairpinEnding();
-	if (enode) {
-		event->reportDynamicToOwner();  // shouldn't be necessary
-		addHairpinEnding(slice->at(partindex), event, partindex);
-		// shouldn't need dynamics layout parameter
-	}
+		if (finaltext.empty()) {
+			continue;
+		}
+		if (m_software == "sibelius") {
+			hre.replaceDestructive(finaltext, " ", "_", "g");
+		}
 
-	if (m_post_note_text.empty()) {
-		return;
+		if (verses[i]) {
+			token = new HumdrumToken(finaltext);
+			staff->setVerse(i,token);
+		} else {
+			token = new HumdrumToken(".");
+			staff->setVerse(i,token);
+		}
 	}
 
-	// check the text buffer for text which needs to be moved
-	// after the current note.
-	string index;
-	index = to_string(partindex);
-	index += ' ';
-	index += to_string(staffindex);
-	index += ' ';
-	index += to_string(voiceindex);
-
-	auto it = m_post_note_text.find(index);
-	if (it == m_post_note_text.end()) {
-		// There is text waiting, but not for this note
-		// (for some strange reason).
-		return;
-	}
-	vector<xml_node>& tnodes = it->second;
-	for (int i=0; i<(int)tnodes.size(); i++) {
-		addText(slice, outdata, partindex, staffindex, voiceindex, tnodes[i], true);
-	}
-	m_post_note_text.erase(it);
+	return (int)staff->getVerseCount();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addTexts -- Add all text direction for a note.
+// cleanSpaces -- remove trailing and leading spaces from text.
+//    Also removed doubled spaces, and converts tabs and newlines
+//    into spaces.
 //
 
-void Tool_musicxml2hum::addTexts(GridSlice* slice, GridMeasure* measure, int partindex,
-		int staffindex, int voiceindex, MxmlEvent* event) {
-	vector<pair<int, xml_node>>& nodes = event->getTexts();
-	for (auto item : nodes) {
-		int newpartindex = item.first;
-		int newstaffindex = 0; // Not allowing addressing text by layer (could be changed).
-		addText(slice, measure, newpartindex, newstaffindex, voiceindex, item.second, false);
+string Tool_musicxml2hum::cleanSpaces(const string& input) {
+	int endi = (int)input.size() - 1;
+	while (endi >= 0) {
+		if (isspace(input[endi])) {
+			endi--;
+			continue;
+		}
+		break;
+	}
+	int starti = 0;
+	while (starti <= endi) {
+		if (isspace(input[starti])) {
+			starti++;
+			continue;
+		}
+		break;
+
+	}
+	string output;
+   for (int i=starti; i<=endi; i++) {
+		if (!isspace(input[i])) {
+			output += input[i];
+			continue;
+		}
+		output += " ";
+		i++;
+		while ((i < endi) && isspace(input[i])) {
+			i++;
+		}
+		i--;
+	}
+	if ((output.size() == 3) && ((unsigned char)output[0] == 0xee) &&
+			((unsigned char)output[1] == 0x95) && ((unsigned char)output[2] == 0x91)) {
+		// MuseScore elision character:
+		// <text font-family="MScore Text"></text>
+		output = " ";
 	}
+
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addTempos -- Add all text direction for a note.
+// Tool_musicxml2hum::isInvisible --
 //
 
-void Tool_musicxml2hum::addTempos(GridSlice* slice, GridMeasure* measure, int partindex,
-		int staffindex, int voiceindex, MxmlEvent* event) {
-	vector<pair<int, xml_node>>& nodes = event->getTempos();
-	for (auto item : nodes) {
-		int newpartindex = item.first;
-		int newstaffindex = 0; // Not allowing addressing text by layer (could be changed).
-		addTempo(slice, measure, newpartindex, newstaffindex, voiceindex, item.second);
+bool Tool_musicxml2hum::isInvisible(MxmlEvent* event) {
+	xml_node node = event->getNode();
+	if (!node) {
+		return false;
+	}
+	if (strcmp(node.attribute("print-object").value(), "no") == 0) {
+		return true;
 	}
+
+	return false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addBrackets --
-//
-//
-//    <direction placement="above">
-//      <direction-type>
-//        <bracket default-y="12" line-end="down" line-type="dashed" number="1" type="start"/>
-//      </direction-type>
-//      <offset>4</offset>
-//    </direction>
-//
-//    <direction placement="above">
-//      <direction-type>
-//        <bracket line-end="down" number="1" type="stop"/>
-//      </direction-type>
-//      <offset>5</offset>
-//    </direction>
+// Tool_musicxml2hum::addSecondaryChordNotes --
 //
 
-void Tool_musicxml2hum::addBrackets(GridSlice* slice, GridMeasure* measure, MxmlEvent* event,
-	HumNum nowtime, int partindex) {
-	int staffindex = 0;
-	int voiceindex = 0;
-	string token;
-	HumNum timestamp;
-	vector<xml_node> brackets = event->getBrackets();
-	for (int i=0; i<(int)brackets.size(); i++) {
-		xml_node bracket = brackets[i].child("direction-type").child("bracket");
-		if (!bracket) {
-			continue;
-		}
-		string linetype = bracket.attribute("line-type").as_string();
-		string endtype = bracket.attribute("type").as_string();
-		int number = bracket.attribute("number").as_int();
-		if (endtype == "stop") {
-			linetype = m_bracket_type_buffer[number];
-		} else {
-			m_bracket_type_buffer[number] = linetype;
-		}
+void Tool_musicxml2hum::addSecondaryChordNotes(ostream& output,
+		MxmlEvent* head, const string& recip) {
+	vector<MxmlEvent*> links = head->getLinkedNotes();
+	MxmlEvent* note;
+	string pitch;
+	string prefix;
+	string postfix;
+	int slurstarts = 0;
+	int slurstops  = 0;
+	vector<int> slurdirs;
 
-		if (linetype == "solid") {
-			if (endtype == "start") {
-				token = "*lig";
-				measure->addInterpretationBefore(slice, partindex, staffindex, voiceindex, token);
-			} else if (endtype == "stop") {
-				token = "*Xlig";
-				timestamp = nowtime + event->getDuration();
-				measure->addInterpretationAfter(slice, partindex, staffindex, voiceindex, token, timestamp);
-			}
-		} else if (linetype == "dashed") {
-			if (endtype == "start") {
-				token = "*col";
-				measure->addInterpretationBefore(slice, partindex, staffindex, voiceindex, token);
-			} else if (endtype == "stop") {
-				token = "*Xcol";
-				timestamp = nowtime + event->getDuration();
-				measure->addInterpretationAfter(slice, partindex, staffindex, voiceindex, token, timestamp);
+	bool primarynote = false;
+	for (int i=0; i<(int)links.size(); i++) {
+		note       = links.at(i);
+		pitch      = note->getKernPitch();
+		prefix     = note->getPrefixNoteInfo();
+		postfix    = note->getPostfixNoteInfo(primarynote, recip);
+		slurstarts = note->hasSlurStart(slurdirs);
+		slurstops  = note->hasSlurStop();
+
+		// or maybe walk backwards in the following loop?
+		for (int i=0; i<slurstarts; i++) {
+			prefix.insert(0, "(");
+			if (slurdirs[i] > 0) {
+				prefix.insert(1, ">");
+				m_slurabove++;
+			} else if (slurdirs[i] < 0) {
+				prefix.insert(1, "<");
+				m_slurbelow++;
 			}
 		}
+		for (int i=0; i<slurstops; i++) {
+			postfix.push_back(')');
+		}
+		//if (slurstart) {
+		//	prefix.insert(0, "(");
+		//	if (slurdir) {
+		//		if (slurdir > 0) {
+		//			prefix.insert(1, ">");
+		//			m_slurabove++;
+		//		} else if (slurdir < 0) {
+		//			prefix.insert(1, "<");
+		//			m_slurbelow++;
+		//		}
+		//	}
+		//}
+		//if (slurstop) {
+		//	postfix.push_back(')');
+		//}
+
+		output << " " << prefix << recip << pitch << postfix;
 	}
 }
 
 
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::addText -- Add a text direction to the grid.
-//
-//      <direction placement="below">
-//        <direction-type>
-//          <words font-style="italic">Some Text</words>
-//        </direction-type>
-//      </direction>
-//
-// Multi-line example:
+/////////////////////////////
 //
-// <direction placement="above">
-//         <direction-type>
-//           <words default-y="40.00" relative-x="-9.47" relative-y="2.71">note</words>
-//           <words>with newline</words>
-//         </direction-type>
-//       <staff>2</staff>
-//       </direction>
+// Tool_musicxml2hum::appendZeroEvents --
 //
 
-void Tool_musicxml2hum::addText(GridSlice* slice, GridMeasure* measure, int partindex,
-		int staffindex, int voiceindex, xml_node node, bool force) {
-	string placementstring;
-	xml_attribute placement = node.attribute("placement");
-	if (placement) {
-		string value = placement.value();
-		if (value == "above") {
-			placementstring = ":a";
-		} else if (value == "below") {
-			placementstring = ":b";
-		}
-	}
-
-	xml_node child = node.first_child();
-	if (!child) {
-		return;
-	}
-	if (!nodeType(child, "direction-type")) {
-		return;
-	}
-
-	xml_node grandchild = child.first_child();
-	if (!grandchild) {
-		return;
-	}
-
-	xml_node sibling = grandchild;
-
-	bool dyQ = false;
-	xml_attribute defaulty;
-
-	string text;
-	while (sibling) {
-		if (nodeType(sibling, "words")) {
-			text += sibling.child_value();
-			if (!dyQ) {
-				defaulty = sibling.attribute("default-y");
-				if (defaulty) {
-					dyQ = true;
-					double number = std::stod(defaulty.value());
-					if (number >= 0.0) {
-						placementstring = ":a";
-					} else if (number < 0.0) {
-						placementstring = ":b";
-					}
-				}
-			}
-		}
-		sibling = sibling.next_sibling();
-	}
+void Tool_musicxml2hum::appendZeroEvents(GridMeasure* outdata,
+		vector<SimultaneousEvents*>& nowevents, HumNum nowtime,
+		vector<MxmlPart>& partdata) {
 
-	if (text == "") {
-		// Don't insert an empty text
-		return;
-	}
+	bool hasclef           = false;
+	bool haskeysig         = false;
+	bool haskeydesignation = false;
+	bool hastransposition  = false;
+	bool hastimesig        = false;
+	bool hasottava         = false;
+	bool hasstafflines     = false;
 
-	// Mapping \n (0x0a) to newline (ignoring \r, (0x0d))
-	string newtext;
-	for (int i=0; i<(int)text.size(); i++) {
-		switch (text[i]) {
-			case 0x0a:
-				newtext += "\\n";
-			case 0x0d:
-				break;
-			default:
-				newtext += text[i];
-		}
-	}
-	text = newtext;
+	vector<vector<xml_node>> clefs(partdata.size());
+	vector<vector<xml_node>> keysigs(partdata.size());
+	vector<vector<xml_node>> transpositions(partdata.size());
+	vector<vector<xml_node>> timesigs(partdata.size());
+	vector<vector<vector<xml_node>>> ottavas(partdata.size());
+	vector<vector<xml_node>> hairpins(partdata.size());
+	vector<vector<xml_node>> stafflines(partdata.size());
 
-	// Remove newlines encodings at end of text.
-	HumRegex hre;
-	hre.replaceDestructive(text, "", "(\\\\n)+\\s*$");
+	vector<vector<vector<vector<MxmlEvent*>>>> gracebefore(partdata.size());
+	vector<vector<vector<vector<MxmlEvent*>>>> graceafter(partdata.size());
+	bool foundnongrace = false;
 
-	/* Problem: these are also possibly signs for figured bass
-	if (text == "#") {
-		// interpret as an editorial sharp marker
-		setEditorialAccidental(+1, slice, partindex, staffindex, voiceindex);
-		return;
-	} else if (text == "b") {
-		// interpret as an editorial flat marker
-		setEditorialAccidental(-1, slice, partindex, staffindex, voiceindex);
-		return;
-	// } else if (text == u8"§") {
-	} else if (text == "\xc2\xa7") {
-		// interpret as an editorial natural marker
-		setEditorialAccidental(0, slice, partindex, staffindex, voiceindex);
-		return;
-	}
-	*/
+	int pindex = 0;
+	xml_node child;
+	xml_node grandchild;
 
-	//
-	// The following code should be merged into the loop to apply
-	// font changes within the text.  Internal formatting code for
-	// the string would need to be developed if so.  For now, just
-	// the first word's style will be processed.
-	//
+	for (int i=0; i<(int)nowevents.size(); i++) {
+		for (int j=0; j<(int)nowevents[i]->zerodur.size(); j++) {
+			xml_node element = nowevents[i]->zerodur[j]->getNode();
+			pindex = nowevents[i]->zerodur[j]->getPartIndex();
 
-	string stylestring;
-	bool italic = false;
-	bool bold = false;
+			if (nodeType(element, "attributes")) {
+				child = element.first_child();
+				while (child) {
+					if (nodeType(child, "clef")) {
+						clefs[pindex].push_back(child);
+						hasclef = true;
+						foundnongrace = true;
+					}
 
-	xml_attribute fontstyle = grandchild.attribute("font-style");
-	if (fontstyle) {
-		string value = fontstyle.value();
-		if (value == "italic") {
-			italic = true;
-		}
-	}
+					if (nodeType(child, "key")) {
+						keysigs[pindex].push_back(child);
+						haskeysig = true;
+						string xpath = "mode";
+						string mode = child.select_node(xpath.c_str()).node().child_value();
+						if (mode != "") {
+							haskeydesignation = true;
+						}
+						foundnongrace = true;
+					}
 
-	xml_attribute fontweight = grandchild.attribute("font-weight");
-	if (fontweight) {
-		string value = fontweight.value();
-		if (value == "bold") {
-			bold = true;
-		}
-	}
+					if (nodeType(child, "transpose")) {
+						transpositions[pindex].push_back(child);
+						hastransposition = true;
+						foundnongrace = true;
+					}
 
-	if (italic && bold) {
-		stylestring = ":Bi";
-	} else if (italic) {
-		stylestring = ":i";
-	} else if (bold) {
-		stylestring = ":B";
-	}
+					if (nodeType(child, "staff-details")) {
+						grandchild = child.first_child();
+						while (grandchild) {
+							if (nodeType(grandchild, "staff-lines")) {
+								stafflines[pindex].push_back(grandchild);
+								hasstafflines = true;
+							}
+							grandchild = grandchild.next_sibling();
+						}
+					}
 
-	bool interpQ = false;
-	bool specialQ = false;
-	bool globalQ = false;
-	bool afterQ = false;
-	string output;
-	if (text == "!") {
-		// null local comment
-		output = text;
-		specialQ = true;
-	} else if (text == "*") {
-		// null interpretation
-		output = text;
-		specialQ = true;
-		interpQ = true;
-	} else if ((text.size() > 1) && (text[0] == '*') && (text[1] != '*')) {
-		// regular tandem interpretation, but disallow manipulators:
-		if (text == "*^") {
-			specialQ = false;
-		} else if (text == "*+") {
-			specialQ = false;
-		} else if (text == "*-") {
-			specialQ = false;
-		} else if (text == "*v") {
-			specialQ = false;
-		} else {
-			specialQ = true;
-			interpQ = true;
-			output = text;
-		}
-	} else if ((text.size() > 2) && (text[0] == '*') && (text[1] == '*')) {
-		hre.replaceDestructive(text, "*", "^\\*+");
-		output = text;
-		specialQ = true;
-		afterQ = true;
-		interpQ = true;
-		if (force == false) {
-			// store text for later processing after the next note in the data.
-			string index;
-			index += to_string(partindex);
-			index += ' ';
-			index += to_string(staffindex);
-			index += ' ';
-			index += to_string(voiceindex);
-			m_post_note_text[index].push_back(node);
-			return;
+					if (nodeType(child, "time")) {
+						timesigs[pindex].push_back(child);
+						hastimesig = true;
+						foundnongrace = true;
+					}
+					child = child.next_sibling();
+				}
+			} else if (nodeType(element, "direction")) {
+				// direction -> direction-type -> words
+				// direction -> direction-type -> dynamics
+				// direction -> direction-type -> octave-shift
+				child = element.first_child();
+				if (nodeType(child, "direction-type")) {
+					grandchild = child.first_child();
+					if (nodeType(grandchild, "words")) {
+						m_current_text.emplace_back(std::make_pair(pindex, element));
+					} else if (nodeType(grandchild, "metronome")) {
+						m_current_tempo.emplace_back(std::make_pair(pindex, element));
+					} else if (nodeType(grandchild, "dynamics")) {
+						m_current_dynamic[pindex].push_back(element);
+					} else if (nodeType(grandchild, "octave-shift")) {
+						storeOttava(pindex, grandchild, element, ottavas);
+						hasottava = true;
+					} else if (nodeType(grandchild, "wedge")) {
+						m_current_dynamic[pindex].push_back(element);
+					} else if (nodeType(grandchild, "bracket")) {
+						m_current_brackets[pindex].push_back(element);
+					}
+				}
+			} else if (nodeType(element, "figured-bass")) {
+				m_current_figured_bass[pindex].push_back(element);
+			} else if (nodeType(element, "note")) {
+				if (foundnongrace) {
+					addEventToList(graceafter, nowevents[i]->zerodur[j]);
+				} else {
+					addEventToList(gracebefore, nowevents[i]->zerodur[j]);
+				}
+			} else if (nodeType(element, "print")) {
+				processPrintElement(outdata, element, nowtime);
+			}
 		}
-	} else if ((text.size() > 1) && (text[0] == '!') && (text[1] != '!')) {
-		// embedding a local comment
-		output = text;
-		specialQ = true;
-	} else if ((text.size() >= 2) && (text[0] == '!') && (text[1] == '!')) {
-		// embedding a global comment (or bibliographic record, etc.).
-		output = text;
-		globalQ = true;
-		specialQ = true;
-	} else if (hre.search(text, "\\s*problem\\s*:\\s*(.*)\\s*$")) {
-		specialQ = true;
-		output = "!LO:TX:t=P:problem:";
-		output += hre.getMatch(1);
-		hre.replaceDestructive(output, "\\n", "\n", "g");
-		hre.replaceDestructive(output, " ", "\t", "g");
 	}
 
-	if (!specialQ) {
-		text = cleanSpacesAndColons(text);
-		if (text.empty()) {
-			// no text to display after removing whitespace
-			return;
-		}
+	addGraceLines(outdata, gracebefore, partdata, nowtime);
 
-		if (placementstring.empty()) {
-			// force above if no placement specified
-			placementstring = ":a";
-		}
+	if (hasstafflines) {
+		addStriaLine(outdata, stafflines, partdata, nowtime);
+	}
 
-		output = "!LO:TX";
-		output += placementstring;
-		output += stylestring;
-		output += ":t=";
-		output += text;
+	if (hasclef) {
+		addClefLine(outdata, clefs, partdata, nowtime);
 	}
 
-	// The text direction needs to be added before the last line
-	// in the measure object.  If there is already an empty layout
-	// slice before the current one (with no spine manipulators
-	// in between), then insert onto the existing layout slice;
-	// otherwise create a new layout slice.
+	if (hastransposition) {
+		addTranspositionLine(outdata, transpositions, partdata, nowtime);
+	}
 
-	if (interpQ) {
-		if (afterQ) {
-			int voicecount = (int)slice->at(partindex)->at(staffindex)->size();
-			if (voiceindex >= voicecount) {
-				// Adding voices in the new slice.  It might be
-				// better to first check for a previous text line
-				// at the current timestamp that is empty (because there
-				// is text at the same time in another spine).
-				GridStaff* gs = slice->at(partindex)->at(staffindex);
-				gs->resize(voiceindex+1);
-				string null = slice->getNullTokenForSlice();
-				for (int m=voicecount; m<voiceindex+1; m++) {
-					gs->at(m) = new GridVoice(null, 0);
-				}
-			}
-			HTp token = slice->at(partindex)->at(staffindex)->at(voiceindex)->getToken();
-			HumNum tokdur = Convert::recipToDuration(token);
-			HumNum timestamp = slice->getTimestamp() + tokdur;
-			measure->addInterpretationAfter(slice, partindex, staffindex, voiceindex, output, timestamp);
-		} else {
-			measure->addInterpretationBefore(slice, partindex, staffindex, voiceindex, output);
-		}
-	} else if (globalQ) {
-		HumNum timestamp = slice->getTimestamp();
-		measure->addGlobalComment(text, timestamp);
-	} else {
-		// adding local comment that is not a layout parameter also goes here:
-		measure->addLayoutParameter(slice, partindex, output);
+	if (haskeysig) {
+		addKeySigLine(outdata, keysigs, partdata, nowtime);
+	}
+
+	if (haskeydesignation) {
+		addKeyDesignationLine(outdata, keysigs, partdata, nowtime);
+	}
+
+	if (hastimesig) {
+		addTimeSigLine(outdata, timesigs, partdata, nowtime);
+	}
+
+	if (hasottava) {
+		addOttavaLine(outdata, ottavas, partdata, nowtime);
 	}
+
+	addGraceLines(outdata, graceafter, partdata, nowtime);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addTempo -- Add a tempo direction to the grid.
-//
-// <direction placement="above">
-//    <direction-type>
-//       <metronome parentheses="no" default-x="-35.96" relative-y="20.00">
-//          <beat-unit>half</beat-unit>
-//          <per-minute>80</per-minute>
-//       </metronome>
-//    </direction-type>
-//    <sound tempo="160"/>
-// </direction>
+// Tool_musicxml2hum::storeOcttava -- store an ottava mark which has this structure:
 //
-// Dotted tempo example:
+//  octaveShift:
+//     <octave-shift type="down" size="8" number="1" default-y="30.00"/>
 //
-// <direction placement="above">
-//    <direction-type>
-//       <metronome parentheses="no" default-x="-39.10" relative-y="20.00">
-//          <beat-unit>quarter</beat-unit>
-//          <beat-unit-dot/>
-//          <per-minute>80</per-minute>
-//       </metronome>
-//    </direction-type>
-//    <sound tempo="120"/>
-// </direction>
+//  For grand staff or multi-staff parts, the staff number needs to be extracted from an uncle element:
+//       <direction placement="below">
+//        <direction-type>
+//          <octave-shift type="up" size="8" number="1" default-y="-83.10"/>
+//        </direction-type>
+//        <staff>2</staff>
+//      </direction>
 //
+// ottavas array has three dimensions: (1) is the part, (2) is the staff, and (3) is the list of ottavas.
 //
 
-void Tool_musicxml2hum::addTempo(GridSlice* slice, GridMeasure* measure, int partindex,
-		int staffindex, int voiceindex, xml_node node) {
-	string placementstring;
-	xml_attribute placement = node.attribute("placement");
-	if (placement) {
-		string value = placement.value();
-		if (value == "above") {
-			placementstring = ":a";
-		} else if (value == "below") {
-			placementstring = ":b";
-		} else {
-			// force above if no explicit placement:
-			placementstring = ":a";
-		}
-	}
-
-	xml_node child = node.first_child();
-	if (!child) {
-		return;
-	}
-	if (!nodeType(child, "direction-type")) {
-		return;
-	}
-
-	xml_node sound(NULL);
-	xml_node sibling = child;
-	while (sibling) {
-		if (nodeType(sibling, "sound")) {
-			sound = sibling;
-			break;
+void Tool_musicxml2hum::storeOttava(int pindex, xml_node octaveShift, xml_node direction,
+	vector<vector<vector<xml_node>>>& ottavas) {
+	int staffindex = 0;
+	xml_node staffnode = direction.select_node("staff").node();
+	if (staffnode && staffnode.text()) {
+		int staffnum = staffnode.text().as_int();
+		if (staffnum > 0) {
+			staffindex = staffnum - 1;
 		}
-		sibling = sibling.next_sibling();
 	}
-
-	// grandchild should be <metronome> (containing textual display)
-	// and <sound @tempo> which gives *MM data.
-	xml_node metronome(NULL);
-
-	xml_node grandchild = child.first_child();
-	if (!grandchild) {
-		return;
+	// ottavas presumed to be allocated by part, but not by staff.
+	if ((int)ottavas[pindex].size() <= staffindex) {
+		ottavas[pindex].resize(staffindex+1);
 	}
-	sibling = grandchild;
+	ottavas[pindex][staffindex].push_back(octaveShift);
+}
 
-	while (sibling) {
-		if (nodeType(sibling, "metronome")) {
-			metronome = sibling;
-		}
-		sibling = sibling.next_sibling();
-	}
 
-	// get metronome parameters
 
-	xml_node beatunit(NULL);
-	xml_node beatunitdot(NULL);
-	xml_node perminute(NULL);
+//////////////////////////////
+//
+// Tool_musicxml2hum::processPrintElement --
+//      <print new-page="yes">
+//      <print new-system="yes">
+//
 
-	if (metronome) {
-		sibling = metronome.first_child();
-		while (sibling) {
-			if (nodeType(sibling, "beat-unit")) {
-				beatunit = sibling;
-			} else if (nodeType(sibling, "beat-unit-dot")) {
-				beatunitdot = sibling;
-			} else if (nodeType(sibling, "per-minute")) {
-				perminute = sibling;
-			}
-			sibling = sibling.next_sibling();
-		}
+void Tool_musicxml2hum::processPrintElement(GridMeasure* outdata, xml_node element,
+		HumNum timestamp) {
+	bool isPageBreak = false;
+	bool isSystemBreak = false;
+	string pageparam = element.attribute("new-page").value();
+	string systemparam = element.attribute("new-system").value();
+	if (pageparam == "yes") {
+		isPageBreak = true;
 	}
-
-	string mmvalue;
-	if (sound) {
-		mmvalue = getAttributeValue(sound, "tempo");
+	if (systemparam == "yes") {
+		isSystemBreak = true;
 	}
 
-	if (!beatunit) {
-		cerr << "Warning: missing beat-unit in tempo setting" << endl;
-		return;
-	}
-	if (!perminute) {
-		cerr << "Warning: missing per-minute in tempo setting" << endl;
+	if (!(isPageBreak || isSystemBreak)) {
 		return;
 	}
+	GridSlice* gs = outdata->back();
 
-	int staff = 0;
-	int voice = 0;
-
-	if (sound) {
-		string mmtok = "*MM";
-		double mmv = stod(mmvalue);
-		double mmi = int(mmv + 0.001);
-		if (fabs(mmv - mmi) < 0.01) {
-			stringstream sstream;
-			sstream << mmi;
-			mmtok += sstream.str();
-		} else {
-			mmtok += mmvalue;
+	HTp token = NULL;
+	if (gs && gs->size() > 0) {
+		if (gs->at(0)->size() > 0) {
+			if (gs->at(0)->at(0)->size() > 0) {
+				token = gs->at(0)->at(0)->at(0)->getToken();
+			}
 		}
-		HumNum timestamp = slice->getTimestamp();
-		measure->addTempoToken(mmtok, timestamp, partindex, staff, voice, m_maxstaff);
 	}
 
-	string butext = beatunit.child_value();
-	string pmtext = perminute.child_value();
-	string stylestring;
-
-	// create textual tempo marking
-	string text;
-	text = "[";
-	text += butext;
-	if (beatunitdot) {
-		text += "-dot";
+	if (isPageBreak) {
+		if (!token || *token != "!!pagebreak:original")  {
+			outdata->addGlobalComment("!!pagebreak:original", timestamp);
+		}
+	} else if (isSystemBreak) {
+		if (!token || *token != "!!linebreak:original")  {
+			outdata->addGlobalComment("!!linebreak:original", timestamp);
+		}
 	}
-	text += "]";
-	text += "=";
-	text += pmtext;
-
-	string output = "!LO:TX";
-	output += placementstring;
-	output += stylestring;
-	output += ":t=";
-	output += text;
-
-	// The text direction needs to be added before the last line in the measure object.
-	// If there is already an empty layout slice before the current one (with no spine manipulators
-	// in between), then insert onto the existing layout slice; otherwise create a new layout slice.
-	measure->addTempoToken(slice, partindex, output);
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// setEditorialAccidental --
+// Tool_musicxml2hum::addEventToList --
 //
 
-void Tool_musicxml2hum::setEditorialAccidental(int accidental, GridSlice* slice,
-		int partindex, int staffindex, int voiceindex) {
-
-	HTp tok = slice->at(partindex)->at(staffindex)->at(voiceindex)->getToken();
-
-	if ((accidental < 0) && (tok->find("-") == string::npos))  {
-		cerr << "Editorial error for " << tok << ": no flat to mark" << endl;
-		return;
+void Tool_musicxml2hum::addEventToList(vector<vector<vector<vector<MxmlEvent*> > > >& list,
+		MxmlEvent* event) {
+	int pindex = event->getPartIndex();
+	int staffindex = event->getStaffIndex();
+	int voiceindex = event->getVoiceIndex();
+	if (pindex >= (int)list.size()) {
+		list.resize(pindex+1);
 	}
-	if ((accidental > 0) && (tok->find("#") == string::npos))  {
-		cerr << "Editorial error for " << tok << ": no sharp to mark" << endl;
-		return;
+	if (staffindex >= (int)list[pindex].size()) {
+		list[pindex].resize(staffindex+1);
 	}
-	if ((accidental == 0) &&
-			((tok->find("#") != string::npos) || (tok->find("-") != string::npos)))  {
-		cerr << "Editorial error for " << tok << ": requesting a natural accidental" << endl;
-		return;
+	if (voiceindex >= (int)list[pindex][staffindex].size()) {
+		list[pindex][staffindex].resize(voiceindex+1);
 	}
+	list[pindex][staffindex][voiceindex].push_back(event);
+}
 
-	string newtok = *tok;
 
-	if (accidental == -1) {
-		auto loc = newtok.find("-");
-		if (loc < newtok.size()) {
-			if (newtok[loc+1] == 'X') {
-				// replace explicit accidental with editorial accidental
-				newtok[loc+1] = 'i';
-				tok->setText(newtok);
-				m_hasEditorial = 'i';
-			} else {
-				// append i after -:
-				newtok.insert(loc+1, "i");
-				tok->setText(newtok);
-				m_hasEditorial = 'i';
+
+///////////////////////////////
+//
+// Tool_musicxml2hum::addGraceLines -- Add grace note lines.  The number of
+//     lines is equal to the maximum number of successive grace notes in
+//     any part.  Grace notes are filled in reverse sequence.
+//
+
+void Tool_musicxml2hum::addGraceLines(GridMeasure* outdata,
+		vector<vector<vector<vector<MxmlEvent*> > > >& notes,
+		vector<MxmlPart>& partdata, HumNum nowtime) {
+
+	int maxcount = 0;
+
+	for (int i=0; i<(int)notes.size(); i++) {
+		for (int j=0; j<(int)notes.at(i).size(); j++) {
+			for (int k=0; k<(int)notes.at(i).at(j).size(); k++) {
+				if (maxcount < (int)notes.at(i).at(j).at(k).size()) {
+					maxcount = (int)notes.at(i).at(j).at(k).size();
+				}
 			}
 		}
-		return;
 	}
 
-	if (accidental == +1) {
-		auto loc = newtok.find("#");
-		if (loc < newtok.size()) {
-			if (newtok[loc+1] == 'X') {
-				// replace explicit accidental with editorial accidental
-				newtok[loc+1] = 'i';
-				tok->setText(newtok);
-				m_hasEditorial = 'i';
-			} else {
-				// append i after -:
-				newtok.insert(loc+1, "i");
-				tok->setText(newtok);
-				m_hasEditorial = 'i';
-			}
-		}
+	if (maxcount == 0) {
 		return;
 	}
 
-	if (accidental == 0) {
-		auto loc = newtok.find("n");
-		if (loc < newtok.size()) {
-			if (newtok[loc+1] == 'X') {
-				// replace explicit accidental with editorial accidental
-				newtok[loc+1] = 'i';
-				tok->setText(newtok);
-				m_hasEditorial = 'i';
-			} else {
-				// append i after -:
-				newtok.insert(loc+1, "i");
-				tok->setText(newtok);
-				m_hasEditorial = 'i';
+	vector<GridSlice*> slices(maxcount);
+	for (int i=0; i<(int)slices.size(); i++) {
+		slices[i] = new GridSlice(outdata, nowtime, SliceType::GraceNotes);
+		outdata->push_back(slices[i]);
+		slices[i]->initializePartStaves(partdata);
+	}
+
+	for (int i=0; i<(int)notes.size(); i++) {
+		for (int j=0; j<(int)notes[i].size(); j++) {
+			for (int k=0; k<(int)notes[i][j].size(); k++) {
+				int startm = maxcount - (int)notes[i][j][k].size();
+				for (int m=0; m<(int)notes[i][j][k].size(); m++) {
+					addEvent(slices.at(startm+m), outdata, notes[i][j][k][m], nowtime);
+				}
 			}
-		} else {
-			// no natural sign, so add it after any pitch classes.
-			HumRegex hre;
-			hre.search(newtok, R"(([a-gA-G]+))");
-			string diatonic = hre.getMatch(1);
-			string newacc = diatonic + "i";
-			hre.replaceDestructive(newtok, newacc, diatonic);
-			tok->setText(newtok);
-			m_hasEditorial = 'i';
 		}
-		return;
 	}
 }
 
@@ -106266,203 +110292,95 @@ void Tool_musicxml2hum::setEditorialAccidental(int accidental, GridSlice* slice,
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addDynamic -- extract any dynamics for the event
-//
-// Such as:
-//    <direction placement="below">
-//      <direction-type>
-//        <dynamics>
-//          <fff/>
-//          </dynamics>
-//        </direction-type>
-//      <sound dynamics="140.00"/>
-//      </direction>
-//
-// Hairpins:
-//      <direction placement="below">
-//        <direction-type>
-//          <wedge default-y="-75" number="2" spread="15" type="diminuendo"/>
-//        </direction-type>
-//      </direction>
-//
-//      <direction>
-//        <direction-type>
-//          <wedge spread="15" type="stop"/>
-//        </direction-type>
-//      </direction>
+// Tool_musicxml2hum::addClefLine --
 //
 
-void Tool_musicxml2hum::addDynamic(GridPart* part, MxmlEvent* event, int partindex) {
-	vector<xml_node> directions = event->getDynamics();
-	if (directions.empty()) {
-		return;
-	}
-
-	HTp tok = NULL;
-
-	for (int i=0; i<(int)directions.size(); i++) {
-		xml_node direction = directions[i];
-		xml_attribute placement = direction.attribute("placement");
-		bool above = false;
-		if (placement) {
-			string value = placement.value();
-			if (value == "above") {
-				above = true;
-			}
-		}
-		xml_node child = direction.first_child();
-		if (!child) {
-			continue;
-		}
-		if (!nodeType(child, "direction-type")) {
-			continue;
-		}
-		xml_node grandchild = child.first_child();
-		if (!grandchild) {
-			continue;
-		}
-
-		if (!(nodeType(grandchild, "dynamics") || nodeType(grandchild, "wedge"))) {
-			continue;
-		}
-
-		if (nodeType(grandchild, "dynamics")) {
-			xml_node dynamic = grandchild.first_child();
-			if (!dynamic) {
-				continue;
-			}
-			string dstring = getDynamicString(dynamic);
-			if (!tok) {
-				tok = new HumdrumToken(dstring);
-			} else {
-				string oldtext = tok->getText();
-				string newtext = oldtext + " " + dstring;
-				tok->setText(newtext);
-			}
-		} else if ( nodeType(grandchild, "wedge")) {
-			xml_node hairpin = grandchild;
+void Tool_musicxml2hum::addClefLine(GridMeasure* outdata,
+		vector<vector<xml_node> >& clefs, vector<MxmlPart>& partdata,
+		HumNum nowtime) {
 
-			if (isUsedHairpin(hairpin, partindex)) {
-				// need to suppress wedge ending if already used in [[ or ]]
-				continue;
-			}
-			if (!hairpin) {
-				cerr << "Warning: Expecting a hairpin, but found nothing" << endl;
-				continue;
-			}
-			string hstring = getHairpinString(hairpin, partindex);
-			if (!tok) {
-				tok = new HumdrumToken(hstring);
-			} else {
-				string oldtext = tok->getText();
-				string newtext = oldtext + " " + hstring;
-				tok->setText(newtext);
-			}
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+		SliceType::Clefs);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
 
-			// Deal here with adding an index if there is more than one hairpin.
-			if ((hstring != "[") && (hstring != "]") && above) {
-				tok->setValue("LO", "HP", "a", "true");
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)clefs[i].size(); j++) {
+			if (clefs[i][j]) {
+				insertPartClefs(clefs[i][j], *slice->at(i));
 			}
 		}
 	}
-	if (tok) {
-		part->setDynamics(tok);
-	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml::isUsedHairpin --  Needed to avoid double-insertion
-//    of hairpins which were stored before a barline so that they
-//    are not also repeated on the first beat of the next barline.
-//    This fuction will remove the hairpin from the used array
-//    when it is checked.  The used array is only for storing
-//    hairpins that end on measures, so in theory there should not
-//    be too many, and they will be removed fairly quickly.
+// Tool_musicxml2hum::addStriaLine --
 //
 
-bool Tool_musicxml2hum::isUsedHairpin(xml_node hairpin, int partindex) {
-	for (int i=0; i<(int)m_used_hairpins.at(partindex).size(); i++) {
-		if (hairpin == m_used_hairpins.at(partindex).at(i)) {
-			// Cannot delete yet: the hairpin endings are being double accessed somewhere.
-			//m_used_hairpins[partindex].erase(m_used_hairpins[partindex].begin() + i);
-			return true;
+void Tool_musicxml2hum::addStriaLine(GridMeasure* outdata,
+		vector<vector<xml_node> >& stafflines, vector<MxmlPart>& partdata,
+		HumNum nowtime) {
+
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+		SliceType::Stria);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)stafflines[i].size(); j++) {
+			if (stafflines[i][j]) {
+				string lines = stafflines[i][j].child_value();
+				int linecount = stoi(lines);
+				insertPartStria(linecount, *slice->at(i));
+			}
 		}
 	}
-	return false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addHairpinEnding -- extract any hairpin ending
-//   at the end of a measure.
-//
-// Hairpins:
-//      <direction>
-//        <direction-type>
-//          <wedge spread="15" type="stop"/>
-//        </direction-type>
-//      </direction>
+// Tool_musicxml2hum::addTimeSigLine --
 //
 
-void Tool_musicxml2hum::addHairpinEnding(GridPart* part, MxmlEvent* event, int partindex) {
+void Tool_musicxml2hum::addTimeSigLine(GridMeasure* outdata,
+		vector<vector<xml_node> >& timesigs, vector<MxmlPart>& partdata,
+		HumNum nowtime) {
 
-	xml_node direction = event->getHairpinEnding();
-	if (!direction) {
-		return;
-	}
+	GridSlice* slice = new GridSlice(outdata, nowtime, SliceType::TimeSigs);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
 
-	xml_node child = direction.first_child();
-	if (!child) {
-		return;
-	}
-	if (!nodeType(child, "direction-type")) {
-		return;
-	}
-	xml_node grandchild = child.first_child();
-	if (!grandchild) {
-		return;
+	bool status = false;
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)timesigs[i].size(); j++) {
+			if (timesigs[i][j]) {
+				status |= insertPartTimeSigs(timesigs[i][j], *slice->at(i));
+			}
+		}
 	}
 
-	if (!nodeType(grandchild, "wedge")) {
+	if (!status) {
 		return;
 	}
 
-	if (nodeType(grandchild, "wedge")) {
-		xml_node hairpin = grandchild;
-		if (!hairpin) {
-			return;
-		}
-		string hstring = getHairpinString(hairpin, partindex);
-		if (hstring == "[") {
-			hstring = "[[";
-		} else if (hstring == "]") {
-			hstring = "]]";
-		}
-		m_used_hairpins.at(partindex).push_back(hairpin);
-		HTp current = part->getDynamics();
-		if (!current) {
-			HTp htok = new HumdrumToken(hstring);
-			part->setDynamics(htok);
-		} else {
-			string text = current->getText();
-			text += " ";
-			text += hstring;
-			// Set single-note crescendos
-			if (text == "< [[") {
-				text = "<[";
-			} else if (text == "> ]]") {
-				text = ">]";
-			} else if (text == "< [") {
-				text = "<[";
-			} else if (text == "> ]") {
-				text = ">]";
+	// Add mensurations related to time signatures
+
+	slice = new GridSlice(outdata, nowtime, SliceType::MeterSigs);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
+
+	// now add mensuration symbols associated with time signatures
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)timesigs[i].size(); j++) {
+			if (timesigs[i][j]) {
+				insertPartMensurations(timesigs[i][j], *slice->at(i));
 			}
-			current->setText(text);
 		}
 	}
 }
@@ -106471,1946 +110389,2083 @@ void Tool_musicxml2hum::addHairpinEnding(GridPart* part, MxmlEvent* event, int p
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::convertFiguredBassNumber --
+// Tool_musicxml2hum::addOttavaLine -- Probably there will be a problem if
+//    an ottava line ends and another one starts at the same timestamp.
+//    Maybe may OttavaStart and OttavaEnd be separate categories?
 //
 
-string Tool_musicxml2hum::convertFiguredBassNumber(const xml_node& figure) {
-	string output;
-	xml_node fnum = figure.select_node("figure-number").node();
-	// assuming one each of prefix/suffix:
-	xml_node prefixelement = figure.select_node("prefix").node();
-	xml_node suffixelement = figure.select_node("suffix").node();
+void Tool_musicxml2hum::addOttavaLine(GridMeasure* outdata,
+		vector<vector<vector<xml_node>>>& ottavas, vector<MxmlPart>& partdata,
+		HumNum nowtime) {
 
-	string prefix;
-	if (prefixelement) {
-		prefix = prefixelement.child_value();
-	}
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+		SliceType::Ottavas);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
 
-	string suffix;
-	if (suffixelement) {
-		suffix = suffixelement.child_value();
+	for (int p=0; p<(int)ottavas.size(); p++) { // part loop
+		for (int s=0; s<(int)ottavas[p].size(); s++) { // staff loop
+			for (int j=0; j<(int)ottavas[p][s].size(); j++) { // ottava loop
+				if (ottavas[p][s][j]) {
+					// int scount = partdata[p].getStaffCount();
+					// int ss = scount - s - 1;
+					insertPartOttavas(ottavas[p][s][j], *slice->at(p), p, s, partdata[p].getStaffCount());
+				}
+			}
+		}
 	}
+}
 
-	string number;
-	if (fnum) {
-		number = fnum.child_value();
-	}
 
-	string accidental;
-	string slash;
 
-	if (prefix == "flat-flat") {
-		accidental = "--";
-	} else if (prefix == "flat") {
-		accidental = "-";
-	} else if (prefix == "double-sharp" || prefix == "sharp-sharp") {
-		accidental = "##";
-	} else if (prefix == "sharp") {
-		accidental = "#";
-	} else if (prefix == "natural") {
-		accidental = "n";
-	} else if (suffix == "flat-flat") {
-		accidental = "--r";
-	} else if (suffix == "flat") {
-		accidental = "-r";
-	} else if (suffix == "double-sharp" || suffix == "sharp-sharp") {
-		accidental = "##r";
-	} else if (suffix == "sharp") {
-		accidental = "#r";
-	} else if (suffix == "natural") {
-		accidental = "nr";
-	}
+//////////////////////////////
+//
+// Tool_musicxml2hum::addKeySigLine -- Only adding one key signature
+//   for each part for now.
+//
 
-	// If suffix is "cross", "slash" or "backslash",  then an accidental
-	// should be given (probably either a natural or a sharp in general, but
-	// could be a flat).  At the moment do not assign the accidental, but
-	// in the future assign an accidental to the slashed figure, probably
-	// with a post-processing tool.
-	if (suffix == "cross" || prefix == "cross" || suffix == "vertical" || prefix == "vertical") {
-		slash = "|";
-		if (accidental.empty()) {
-			accidental = "#";
-		}
-	} else if ((suffix == "backslash" || suffix == "back-slash") || (prefix == "backslash" || prefix == "back-slash")) {
-		slash = "\\";
-		if (accidental.empty()) {
-			accidental = "#";
-		}
-	} else if ((suffix == "slash") || (prefix == "slash")) {
-		slash = "/";
-		if (accidental.empty()) {
-			accidental = "-";
-		}
-	}
+void Tool_musicxml2hum::addKeySigLine(GridMeasure* outdata,
+		vector<vector<xml_node> >& keysigs,
+		vector<MxmlPart>& partdata, HumNum nowtime) {
 
-	string editorial;
-	string extension;
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+		SliceType::KeySigs);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
 
-	xml_node extendelement = figure.select_node("extend").node();
-	if (extendelement) {
-		string typestring = extendelement.attribute("type").value();
-		if (typestring == "start") {
-			extension = "_";
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)keysigs[i].size(); j++) {
+			if (keysigs[i][j]) {
+				insertPartKeySigs(keysigs[i][j], *slice->at(i));
+			}
 		}
 	}
-
-	output += accidental + number + slash + editorial + extension;
-
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getDynanmicsParameters --  Already presumed to be
-//     a dynamic.
+// Tool_musicxml2hum::addKeyDesignationLine -- Only adding one key designation line
+//   for each part for now.
 //
 
-string Tool_musicxml2hum::getDynamicsParameters(xml_node element) {
-	string output;
-	if (!nodeType(element, "direction")) {
-		return output;
-	}
+void Tool_musicxml2hum::addKeyDesignationLine(GridMeasure* outdata,
+		vector<vector<xml_node> >& keydesigs,
+		vector<MxmlPart>& partdata, HumNum nowtime) {
 
-	xml_attribute placement = element.attribute("placement");
-	if (!placement) {
-		return output;
-	}
-	string value = placement.value();
-	if (value == "above") {
-		output = ":a";
-	}
-	xml_node child = element.first_child();
-	if (!child) {
-		return output;
-	}
-	if (!nodeType(child, "direction-type")) {
-		return output;
-	}
-	xml_node grandchild = child.first_child();
-	if (!grandchild) {
-		return output;
-	}
-	if (!nodeType(grandchild, "wedge")) {
-		return output;
-	}
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+		SliceType::KeyDesignations);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
 
-	xml_attribute wtype = grandchild.attribute("type");
-	if (!wtype) {
-		return output;
-	}
-	string value2 = wtype.value();
-	if (value2 == "stop") {
-		// don't apply parameters to ends of hairpins.
-		output = "";
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)keydesigs[i].size(); j++) {
+			if (keydesigs[i][j]) {
+				insertPartKeyDesignations(keydesigs[i][j], *slice->at(i));
+			}
+		}
 	}
-
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getFiguredBassParameters --  Already presumed to be
-//     figured bass.
+// Tool_musicxml2hum::addTranspositionLine -- Transposition codes to
+//   produce written parts.
 //
 
-string Tool_musicxml2hum::getFiguredBassParameters(xml_node element) {
-	string output;
-	if (!nodeType(element, "figured-bass")) {
-		return output;
+void Tool_musicxml2hum::addTranspositionLine(GridMeasure* outdata,
+		vector<vector<xml_node> >& transpositions,
+		vector<MxmlPart>& partdata, HumNum nowtime) {
+
+	GridSlice* slice = new GridSlice(outdata, nowtime,
+		SliceType::Transpositions);
+	outdata->push_back(slice);
+	slice->initializePartStaves(partdata);
+
+	for (int i=0; i<(int)partdata.size(); i++) {
+		for (int j=0; j<(int)transpositions[i].size(); j++) {
+			if (transpositions[i][j]) {
+				insertPartTranspositions(transpositions[i][j], *slice->at(i));
+			}
+		}
 	}
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getHairpinString --
-//
-// Hairpins:
-//      <direction placement="below">
-//        <direction-type>
-//          <wedge default-y="-75" number="2" spread="15" type="diminuendo"/>
-//        </direction-type>
-//      </direction>
-//
-//      <direction>
-//        <direction-type>
-//          <wedge spread="15" type="stop"/>
-//        </direction-type>
-//      </direction>
+// Tool_musicxml2hum::insertPartClefs --
 //
 
-string Tool_musicxml2hum::getHairpinString(xml_node element, int partindex) {
-	if (nodeType(element, "wedge")) {
-		xml_attribute wtype = element.attribute("type");
-		if (!wtype) {
-			return "???";
-		}
-		string output;
-		string wstring = wtype.value();
-		if (wstring == "diminuendo") {
-			m_stop_char.at(partindex) = "]";
-			output = ">";
-		} else if (wstring == "crescendo") {
-			m_stop_char.at(partindex) = "[";
-			output = "<";
-		} else if (wstring == "stop") {
-			output = m_stop_char.at(partindex);
-		} else {
-			output = "???";
-		}
-		return output;
+void Tool_musicxml2hum::insertPartClefs(xml_node clef, GridPart& part) {
+	if (!clef) {
+		// no clef for some reason.
+		return;
 	}
 
-	return "???";
+	HTp token;
+	int staffnum = 0;
+	while (clef) {
+		clef = convertClefToHumdrum(clef, token, staffnum);
+		part[staffnum]->setTokenLayer(0, token, 0);
+	}
+
+	// go back and fill in all NULL pointers with null interpretations
+	fillEmpties(&part, "*");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getDynamicString --
+// Tool_musicxml2hum::insertPartStria --
 //
 
-string Tool_musicxml2hum::getDynamicString(xml_node element) {
+void Tool_musicxml2hum::insertPartStria(int lines, GridPart& part) {
+	HTp token = new HumdrumToken;
+	string value = "*stria" + to_string(lines);
+	token->setText(value);
+	part[0]->setTokenLayer(0, token, 0);
 
-	if (nodeType(element, "f")) {
-		return "f";
-	} else if (nodeType(element, "p")) {
-		return "p";
-	} else if (nodeType(element, "mf")) {
-		return "mf";
-	} else if (nodeType(element, "mp")) {
-		return "mp";
-	} else if (nodeType(element, "ff")) {
-		return "ff";
-	} else if (nodeType(element, "pp")) {
-		return "pp";
-	} else if (nodeType(element, "sf")) {
-		return "sf";
-	} else if (nodeType(element, "sfp")) {
-		return "sfp";
-	} else if (nodeType(element, "sfpp")) {
-		return "sfpp";
-	} else if (nodeType(element, "fp")) {
-		return "fp";
-	} else if (nodeType(element, "rf")) {
-		return "rfz";
-	} else if (nodeType(element, "rfz")) {
-		return "rfz";
-	} else if (nodeType(element, "sfz")) {
-		return "sfz";
-	} else if (nodeType(element, "sffz")) {
-		return "sffz";
-	} else if (nodeType(element, "fz")) {
-		return "fz";
-	} else if (nodeType(element, "fff")) {
-		return "fff";
-	} else if (nodeType(element, "ppp")) {
-		return "ppp";
-	} else if (nodeType(element, "ffff")) {
-		return "ffff";
-	} else if (nodeType(element, "pppp")) {
-		return "pppp";
-	} else {
-		return "???";
-	}
+	// go back and fill in all NULL pointers with null interpretations
+	fillEmpties(&part, "*");
 }
 
 
+
 //////////////////////////////
 //
-// Tool_musicxml2hum::addFiguredBass --
-//
-// Such as:
-//
-//      <figured-bass>
-//        <figure>
-//          <figure-number>0</figure-number>
-//        </figure>
-//      </figured-bass>
-// or:
-//      <figured-bass>
-//        <figure>
-//          <figure-number>5</figure-number>
-//          <suffix>backslash</suffix>
-//        </figure>
-//        <figure>
-//          <figure-number>2</figure-number>
-//          <suffix>cross</suffix>
-//        </figure>
-//      </figured-bass>
-//
-//      <figured-bass parentheses="yes">
-//        <figure>
-//          <prefix>flat</prefix>
-//        </figure>
-//      </figured-bass>
-//
-// Case where there is more than one figure attached to a note:
-// (notice <duration> element)
-//
-//      <figured-bass>
-//        <figure>
-//          <figure-number>6</figure-number>
-//          <extend type="start" />
-//        </figure>
-//        <duration>2</duration>
-//      <figured-bass>
-//
+// Tool_musicxml2hum::insertPartOttavas --
 //
 
-int Tool_musicxml2hum::addFiguredBass(GridPart* part, MxmlEvent* event, HumNum nowtime, int partindex) {
-	if (m_current_figured_bass[partindex].empty()) {
-		return 0;
+void Tool_musicxml2hum::insertPartOttavas(xml_node ottava, GridPart& part, int partindex,
+		int partstaffindex, int staffcount) {
+	if (!ottava) {
+		// no ottava for some reason.
+		return;
 	}
 
-	int dursum = 0;
-	for (int i=0; i<(int)m_current_figured_bass[partindex].size(); i++) {
-		xml_node fnode = m_current_figured_bass[partindex].at(i);
-		if (!fnode) {
-			// strange problem
-			continue;
-		}
-		string fstring = getFiguredBassString(fnode);
-
-		HTp ftok = new HumdrumToken(fstring);
-		if (i == 0) {
-			part->setFiguredBass(ftok);
-		} else {
-			// store the figured bass for later handling at end of
-			// measure processing.
-			MusicXmlFiguredBassInfo finfo;
-			finfo.timestamp = dursum;
-			finfo.timestamp /= (int)event->getQTicks();
-			finfo.timestamp += nowtime;
-			finfo.partindex = partindex;
-			finfo.token = ftok;
-			m_offsetFiguredBass.push_back(finfo);
-		}
-		if (i < (int)m_current_figured_bass[partindex].size() - 1) {
-			dursum += getFiguredBassDuration(fnode);
-		}
+	HTp token = NULL;
+	while (ottava) {
+		ottava = convertOttavaToHumdrum(ottava, token, partstaffindex, partindex, partstaffindex, staffcount);
+		part[partstaffindex]->setTokenLayer(0, token, 0);
 	}
-	m_current_figured_bass[partindex].clear();
 
-	return 1;
+	// go back and fill in all NULL pointers with null interpretations
+	fillEmpties(&part, "*");
+}
 
-/* deal with figured bass layout parameters?:
-			string fparam = getFiguredBassParameters(fnode);
-			if (fparam != "") {
-				GridMeasure *gm = slice->getMeasure();
-				string fullparam = "!LO:FB" + fparam;
-				if (gm) {
-					gm->addFiguredBassLayoutParameters(slice, partindex, fullparam);
+
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::fillEmpties --
+//
+
+void Tool_musicxml2hum::fillEmpties(GridPart* part, const char* string) {
+	int staffcount = (int)part->size();
+	GridVoice* gv;
+	int vcount;
+
+ 	for (int s=0; s<staffcount; s++) {
+		GridStaff* staff = part->at(s);
+		if (staff == NULL) {
+			cerr << "Strange error here" << endl;
+			continue;
+		}
+		vcount = (int)staff->size();
+		if (vcount == 0) {
+			gv = new GridVoice(string, 0);
+			staff->push_back(gv);
+		} else {
+			for (int v=0; v<vcount; v++) {
+				gv = staff->at(v);
+				if (gv == NULL) {
+					gv = new GridVoice(string, 0);
+					staff->at(v) = gv;
 				}
 			}
 		}
-*/
-
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getFiguredBassString -- extract any figured bass string
-//   from XML node.
+// Tool_musicxml2hum::insertPartKeySigs --
 //
 
-string Tool_musicxml2hum::getFiguredBassString(xml_node fnode) {
-	string output;
-
-	// Parentheses can only enclose an entire figure stack, not
-	// individual numbers or accidentals on numbers in MusicXML,
-	// so apply an editorial mark for parentheses.
-	string editorial;
-	xml_attribute pattr = fnode.attribute("parentheses");
-	if (pattr) {
-		string pval = pattr.value();
-		if (pval == "yes") {
-			editorial = "i";
-		}
-	}
-	// There is no bracket for FB in musicxml (3.0).
-
-	auto children = fnode.select_nodes("figure");
-	for (int i=0; i<(int)children.size(); i++) {
-		output += convertFiguredBassNumber(children[i].node());
-		output += editorial;
-		if (i < (int)children.size() - 1) {
-			output += " ";
-		}
+void Tool_musicxml2hum::insertPartKeySigs(xml_node keysig, GridPart& part) {
+	if (!keysig) {
+		return;
 	}
 
-	HumRegex hre;
-	hre.replaceDestructive(output, "", R"(^\s+|\s+$)");
-
-	if (output.empty()) {
-		if (children.size()) {
-			cerr << "WARNING: figured bass string is empty but has "
-				<< children.size() << " figure elements as children. "
-				<< "The output has been replaced with \".\"" << endl;
+	HTp token;
+	int staffnum = 0;
+	while (keysig) {
+		keysig = convertKeySigToHumdrum(keysig, token, staffnum);
+		if (staffnum < 0) {
+			// key signature applies to all staves in part (most common case)
+			for (int s=0; s<(int)part.size(); s++) {
+				if (s==0) {
+					part[s]->setTokenLayer(0, token, 0);
+				} else {
+					HTp token2 = new HumdrumToken(*token);
+					part[s]->setTokenLayer(0, token2, 0);
+				}
+			}
+		} else {
+			part[staffnum]->setTokenLayer(0, token, 0);
 		}
-		output = ".";
 	}
-
-	return output;
-
-	// HTp fbtok = new HumdrumToken(fbstring);
-	// part->setFiguredBass(fbtok);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addHarmony --
+// Tool_musicxml2hum::insertPartKeyDesignations --
 //
 
-int Tool_musicxml2hum::addHarmony(GridPart* part, MxmlEvent* event, HumNum nowtime,
-		int partindex) {
-	xml_node hnode = event->getHNode();
-	if (!hnode) {
-		return 0;
+void Tool_musicxml2hum::insertPartKeyDesignations(xml_node keydesig, GridPart& part) {
+	if (!keydesig) {
+		return;
 	}
 
-	// fill in X with the harmony values from the <harmony> node
-	string hstring = getHarmonyString(hnode);
-	int offset = getHarmonyOffset(hnode);
-	HTp htok = new HumdrumToken(hstring);
-	if (offset == 0) {
-		part->setHarmony(htok);
-	} else {
-		MusicXmlHarmonyInfo hinfo;
-		hinfo.timestamp = offset;
-		hinfo.timestamp /= (int)event->getQTicks();
-		hinfo.timestamp += nowtime;
-		hinfo.partindex = partindex;
-		hinfo.token = htok;
-		offsetHarmony.push_back(hinfo);
+	HTp token;
+	int staffnum = 0;
+	while (keydesig) {
+		token = NULL;
+		keydesig = convertKeySigToHumdrumKeyDesignation(keydesig, token, staffnum);
+		if (token == NULL) {
+			return;
+		}
+		if (staffnum < 0) {
+			// key signature applies to all staves in part (most common case)
+			for (int s=0; s<(int)part.size(); s++) {
+				if (s==0) {
+					part[s]->setTokenLayer(0, token, 0);
+				} else {
+					string value = *token;
+					HTp token2 = new HumdrumToken(value);
+					part[s]->setTokenLayer(0, token2, 0);
+				}
+			}
+		} else {
+			part[staffnum]->setTokenLayer(0, token, 0);
+		}
 	}
-
-	return 1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getHarmonyOffset --
-//   <harmony default-y="40">
-//       <root>
-//           <root-step>C</root-step>
-//       </root>
-//       <kind>major-ninth</kind>
-//       <bass>
-//           <bass-step>E</bass-step>
-//       </bass>
-//       <offset>-8</offset>
-//   </harmony>
+// Tool_musicxml2hum::insertPartTranspositions --
 //
 
-int Tool_musicxml2hum::getHarmonyOffset(xml_node hnode) {
-	if (!hnode) {
-		return 0;
-	}
-	xml_node child = hnode.first_child();
-	if (!child) {
-		return 0;
+void Tool_musicxml2hum::insertPartTranspositions(xml_node transposition, GridPart& part) {
+	if (!transposition) {
+		return;
 	}
-	while (child) {
-		if (nodeType(child, "offset")) {
-			return atoi(child.child_value());
+
+	HTp token;
+	int staffnum = 0;
+	while (transposition) {
+		transposition = convertTranspositionToHumdrum(transposition, token, staffnum);
+		if (staffnum < 0) {
+			// Transposition applies to all staves in part (most common case)
+			for (int s=0; s<(int)part.size(); s++) {
+				if (s==0) {
+					part[s]->setTokenLayer(0, token, 0);
+				} else {
+					HTp token2 = new HumdrumToken(*token);
+					part[s]->setTokenLayer(0, token2, 0);
+				}
+			}
+		} else {
+			part[staffnum]->setTokenLayer(0, token, 0);
 		}
-		child = child.next_sibling();
 	}
-
-	return 0;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getFiguredBaseDuration -- Needed for cases where there is more
-//   than one figure attached to a note.  Return value is the integer of the duration
-//   element.  If will need to be converted to quarter notes later.
-//
-//   <figured-bass>
-//      <figure>
-//         <figure-number>5</figure-number>
-//      </figure>
-//      <figure>
-//         <figure-number>3</figure-number>
-//      </figure>
-//      <duration>2</duration>    <-- get this field if it exists.
-//   </figured-bass>
+// Tool_musicxml2hum::insertPartTimeSigs -- Only allowing one
+//		time signature per part for now.
 //
 
-int Tool_musicxml2hum::getFiguredBassDuration(xml_node fnode) {
-	if (!fnode) {
-		return 0;
-	}
-	xml_node child = fnode.first_child();
-	if (!child) {
-		return 0;
+bool Tool_musicxml2hum::insertPartTimeSigs(xml_node timesig, GridPart& part) {
+	if (!timesig) {
+		// no timesig
+		return false;
 	}
-	while (child) {
-		if (nodeType(child, "duration")) {
-			return atoi(child.child_value());
+
+	bool hasmensuration = false;
+	HTp token;
+	int staffnum = 0;
+
+	while (timesig) {
+		hasmensuration |= checkForMensuration(timesig);
+		timesig = convertTimeSigToHumdrum(timesig, token, staffnum);
+		if (token && (staffnum < 0)) {
+			// time signature applies to all staves in part (most common case)
+			for (int s=0; s<(int)part.size(); s++) {
+				if (s==0) {
+					part[s]->setTokenLayer(0, token, 0);
+				} else {
+					HTp token2 = new HumdrumToken(*token);
+					part[s]->setTokenLayer(0, token2, 0);
+				}
+			}
+		} else if (token) {
+			part[staffnum]->setTokenLayer(0, token, 0);
 		}
-		child = child.next_sibling();
 	}
 
-	return 0;
+	return hasmensuration;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getHarmonyString --
-//   <harmony default-y="40">
-//       <root>
-//           <root-step>C</root-step>
-//       </root>
-//       <kind>major-ninth</kind>
-//       <bass>
-//           <bass-step>E</bass-step>
-//       </bass>
-//       <offset>-8</offset>
-//   </harmony>
-//
-// For harmony labels from Musescore:
-//
-//    <harmony print-frame="no">
-//      <root>
-//        <root-step text="">C</root-step>
-//        </root>
-//      <kind text="V43">none</kind>
-//      </harmony>
-//
-// Converts to: "V43" ignoring the root-step and kind contents
-// if they are both "C" and "none".
+// Tool_musicxml2hum::insertPartMensurations --
 //
 
-string Tool_musicxml2hum::getHarmonyString(xml_node hnode) {
-	if (!hnode) {
-		return "";
-	}
-	xml_node child = hnode.first_child();
-	if (!child) {
-		return "";
+void Tool_musicxml2hum::insertPartMensurations(xml_node timesig,
+		GridPart& part) {
+	if (!timesig) {
+		// no timesig
+		return;
 	}
-	string root;
-	string kind;
-	string kindtext;
-	string bass;
-	int rootalter = 0;
-	int bassalter = 0;
-	xml_node grandchild;
-	while (child) {
-		if (nodeType(child, "root")) {
-			grandchild = child.first_child();
-			while (grandchild) {
-				if (nodeType(grandchild, "root-step")) {
-					root = grandchild.child_value();
-				} if (nodeType(grandchild, "root-alter")) {
-					rootalter = atoi(grandchild.child_value());
-				}
-				grandchild = grandchild.next_sibling();
-			}
-		} else if (nodeType(child, "kind")) {
-			kindtext = getAttributeValue(child, "text");
-			kind = child.child_value();
-			if (kind == "") {
-				kind = child.attribute("text").value();
-				transform(kind.begin(), kind.end(), kind.begin(), ::tolower);
-			}
-		} else if (nodeType(child, "bass")) {
-			grandchild = child.first_child();
-			while (grandchild) {
-				if (nodeType(grandchild, "bass-step")) {
-					bass = grandchild.child_value();
-				} if (nodeType(grandchild, "bass-alter")) {
-					bassalter = atoi(grandchild.child_value());
+
+	HTp token = NULL;
+	int staffnum = 0;
+
+	while (timesig) {
+		timesig = convertMensurationToHumdrum(timesig, token, staffnum);
+		if (staffnum < 0) {
+			// time signature applies to all staves in part (most common case)
+			for (int s=0; s<(int)part.size(); s++) {
+				if (s==0) {
+					part[s]->setTokenLayer(0, token, 0);
+				} else {
+					HTp token2 = new HumdrumToken(*token);
+					part[s]->setTokenLayer(0, token2, 0);
 				}
-				grandchild = grandchild.next_sibling();
 			}
+		} else {
+			part[staffnum]->setTokenLayer(0, token, 0);
 		}
-		child = child.next_sibling();
 	}
-	stringstream ss;
 
-	if ((kind == "none") && (root == "C") && !kindtext.empty()) {
-		ss << kindtext;
-		string output = cleanSpaces(ss.str());
-		return output;
-	}
+}
 
-	ss << root;
 
-	if (rootalter > 0) {
-		for (int i=0; i<rootalter; i++) {
-			ss << "#";
-		}
-	} else if (rootalter < 0) {
-		for (int i=0; i<-rootalter; i++) {
-			ss << "-";
-		}
-	}
+//////////////////////////////
+//
+// Tool_musicxml::checkForMensuration --
+//    Examples:
+//        <time symbol="common">
+//        <time symbol="cut">
+//
 
-	if (root.size() && kind.size()) {
-		ss << " ";
-	}
-	ss << kind;
-	if (bass.size()) {
-		ss << "/";
+bool Tool_musicxml2hum::checkForMensuration(xml_node timesig) {
+	if (!timesig) {
+		return false;
 	}
-	ss << bass;
 
-	if (bassalter > 0) {
-		for (int i=0; i<bassalter; i++) {
-			ss << "#";
-		}
-	} else if (bassalter < 0) {
-		for (int i=0; i<-bassalter; i++) {
-			ss << "-";
-		}
+	xml_attribute mens = timesig.attribute("symbol");
+	if (mens) {
+		return true;
+	} else {
+		return false;
 	}
-
-	string output = cleanSpaces(ss.str());
-	return output;
 }
 
 
-
 //////////////////////////////
 //
-// Tool_musicxml2hum::addLyrics --
+//	Tool_musicxml2hum::convertTranspositionToHumdrum --
+//
+//  <transpose>
+//     <diatonic>-1</diatonic>
+//     <chromatic>-2</chromatic>
 //
 
-int Tool_musicxml2hum::addLyrics(GridStaff* staff, MxmlEvent* event) {
-	xml_node node = event->getNode();
-	if (!node) {
-		return 0;
+xml_node Tool_musicxml2hum::convertTranspositionToHumdrum(xml_node transpose,
+		HTp& token, int& staffindex) {
+
+	if (!transpose) {
+		return transpose;
 	}
-	HumRegex hre;
-	xml_node child = node.first_child();
-	xml_node grandchild;
-	// int max;
-	int number = 0;
-	vector<xml_node> verses;
-	string syllabic;
-	string text;
+
+	staffindex = -1;
+	xml_attribute sn = transpose.attribute("number");
+	if (sn) {
+		staffindex = atoi(sn.value()) - 1;
+	}
+
+	int diatonic = 0;
+	int chromatic = 0;
+
+	xml_node child = transpose.first_child();
 	while (child) {
-		if (!nodeType(child, "lyric")) {
-			child = child.next_sibling();
-			continue;
-		}
-		string value = child.attribute("number").value();
-		if (hre.search(value, R"(verse(\d+))")) {
-			// Fix for Sibelius which uses number="part8verse5" format.
-			number = stoi(hre.getMatch(1));
-		} else {
-			number = atoi(child.attribute("number").value());
-		}
-		if (number > 100) {
-			cerr << "Error: verse number is too large: number" << endl;
-			return 0;
-		}
-		if (number == (int)verses.size() + 1) {
-			verses.push_back(child);
-		} else if ((number > 0) && (number < (int)verses.size())) {
-			// replace a verse for some reason.
-			verses[number-1] = child;
-		} else if (number > 0) {
-			int oldsize = (int)verses.size();
-			int newsize = number;
-			verses.resize(newsize);
-			for (int i=oldsize; i<newsize; i++) {
-				verses[i] = xml_node(NULL);
-			}
-			verses[number-1] = child;
+		if (nodeType(child, "diatonic")) {
+			diatonic = atoi(child.child_value());
+		} else if (nodeType(child, "chromatic")) {
+			chromatic = atoi(child.child_value());
 		}
 		child = child.next_sibling();
 	}
 
-	string finaltext;
-	string fontstyle;
-	HTp token;
-	for (int i=0; i<(int)verses.size(); i++) {
-		if (!verses[i]) {
-			// no verse so doing an empty slot.
-		} else {
-			child = verses[i].first_child();
-			finaltext = "";
-			while (child) {
-				if (nodeType(child, "syllabic")) {
-					syllabic = child.child_value();
-					child = child.next_sibling();
-					continue;
-				} else if (nodeType(child, "text")) {
-					fontstyle = child.attribute("font-style").value();
-					text = cleanSpaces(child.child_value());
-					if (fontstyle == "italic") {
-						text = "<i>" + text + "</i>";
-					}
-				} else if (nodeType(child, "elision")) {
-					finaltext += " ";
-					child = child.next_sibling();
-					continue;
-				} else {
-					// such as <extend>
-					child = child.next_sibling();
-					continue;
-				}
-				// escape text which would otherwise be reinterpreated
-				// as Humdrum syntax.
-				if (!text.empty()) {
-					if (text[0] == '!') {
-						text.insert(0, 1, '\\');
-					} else if (text[0] == '*') {
-						text.insert(0, 1, '\\');
-					}
-				}
-				child = child.next_sibling();
-				if (syllabic == "middle" ) {
-					finaltext += "-";
-					finaltext += text;
-					finaltext += "-";
-				} else if (syllabic == "end") {
-					finaltext += "-";
-					finaltext += text;
-				} else if (syllabic == "begin") {
-					finaltext += text;
-					finaltext += "-";
-				} else {
-					finaltext += text;
-				}
-				syllabic.clear();
-			}
-		}
 
-		if (finaltext.empty()) {
-			continue;
-		}
-		if (m_software == "sibelius") {
-			hre.replaceDestructive(finaltext, " ", "_", "g");
-		}
+	// Switching to sounding viewpoint: transposition to get written pitch:
+	diatonic = -diatonic;
+	chromatic = -chromatic;
 
-		if (verses[i]) {
-			token = new HumdrumToken(finaltext);
-			staff->setVerse(i,token);
-		} else {
-			token = new HumdrumToken(".");
-			staff->setVerse(i,token);
-		}
+	stringstream ss;
+	ss << "*Trd" << diatonic << "c" << chromatic;
+
+	token = new HumdrumToken(ss.str());
+
+	int base40 = -Convert::transToBase40(ss.str());
+	if (base40 != 0) {
+		m_hasTransposition = true;
+	}
+
+	transpose = transpose.next_sibling();
+	if (!transpose) {
+		return transpose;
+	}
+	if (nodeType(transpose, "transpose")) {
+		return transpose;
+	} else {
+		return xml_node(NULL);
 	}
-
-	return (int)staff->getVerseCount();
 }
 
 
 
 //////////////////////////////
 //
-// cleanSpaces -- remove trailing and leading spaces from text.
-//    Also removed doubled spaces, and converts tabs and newlines
-//    into spaces.
+//	Tool_musicxml2hum::convertKeySigToHumdrumKeyDesignation --
+//
+//  <key>
+//     <fifths>4</fifths>
+// and sometimes:
+//     <mode>major</mode>
+// or
+//     <mode>minor</mode>
 //
 
-string Tool_musicxml2hum::cleanSpaces(const string& input) {
-	int endi = (int)input.size() - 1;
-	while (endi >= 0) {
-		if (isspace(input[endi])) {
-			endi--;
-			continue;
-		}
-		break;
+xml_node Tool_musicxml2hum::convertKeySigToHumdrumKeyDesignation(xml_node keysig,
+		HTp& token, int& staffindex) {
+
+	if (!keysig) {
+		token = new HumdrumToken("*");
+		return keysig;
 	}
-	int starti = 0;
-	while (starti <= endi) {
-		if (isspace(input[starti])) {
-			starti++;
-			continue;
-		}
-		break;
 
+	staffindex = -1;
+	xml_attribute sn = keysig.attribute("number");
+	if (sn) {
+		staffindex = atoi(sn.value()) - 1;
 	}
-	string output;
-   for (int i=starti; i<=endi; i++) {
-		if (!isspace(input[i])) {
-			output += input[i];
-			continue;
+
+	int fifths = 0;
+	int mode = -1;
+
+	xml_node child = keysig.first_child();
+	while (child) {
+		if (nodeType(child, "fifths")) {
+			fifths = atoi(child.child_value());
 		}
-		output += " ";
-		i++;
-		while ((i < endi) && isspace(input[i])) {
-			i++;
+		if (nodeType(child, "mode")) {
+			string value = child.child_value();
+			if (value == "major") {
+				mode = 0;
+			} else if (value == "minor") {
+				mode = 1;
+			}
 		}
-		i--;
-	}
-	if ((output.size() == 3) && ((unsigned char)output[0] == 0xee) &&
-			((unsigned char)output[1] == 0x95) && ((unsigned char)output[2] == 0x91)) {
-		// MuseScore elision character:
-		// <text font-family="MScore Text"></text>
-		output = " ";
+		child = child.next_sibling();
 	}
 
-	return output;
-}
+	if (mode < 0) {
+		token = new HumdrumToken("*");
+		return xml_node(NULL);
+	}
 
+	stringstream ss;
+	ss << "*";
 
+	if (mode == 0) { // major:
+		switch (fifths) {
+			case +7: ss << "C#"; break;
+			case +6: ss << "F#"; break;
+			case +5: ss << "B"; break;
+			case +4: ss << "E"; break;
+			case +3: ss << "A"; break;
+			case +2: ss << "D"; break;
+			case +1: ss << "G"; break;
+			case  0: ss << "C"; break;
+			case -1: ss << "F"; break;
+			case -2: ss << "B-"; break;
+			case -3: ss << "E-"; break;
+			case -4: ss << "A-"; break;
+			case -5: ss << "D-"; break;
+			case -6: ss << "G-"; break;
+			case -7: ss << "C-"; break;
+			default:
+				token = new HumdrumToken("*");
+				return xml_node(NULL);
+		}
+	} else if (mode == 1) { // minor:
+		switch (fifths) {
+			case +7: ss << "a#"; break;
+			case +6: ss << "d#"; break;
+			case +5: ss << "g#"; break;
+			case +4: ss << "c#"; break;
+			case +3: ss << "f#"; break;
+			case +2: ss << "b"; break;
+			case +1: ss << "e"; break;
+			case  0: ss << "a"; break;
+			case -1: ss << "d"; break;
+			case -2: ss << "g"; break;
+			case -3: ss << "c"; break;
+			case -4: ss << "f"; break;
+			case -5: ss << "b-"; break;
+			case -6: ss << "e-"; break;
+			case -7: ss << "a-"; break;
+			default:
+				token = new HumdrumToken("*");
+				return xml_node(NULL);
+		}
+	}
+	ss << ":";
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::isInvisible --
-//
+	token = new HumdrumToken(ss.str());
 
-bool Tool_musicxml2hum::isInvisible(MxmlEvent* event) {
-	xml_node node = event->getNode();
-	if (!node) {
-		return false;
+	keysig = keysig.next_sibling();
+	if (!keysig) {
+		return keysig;
 	}
-	if (strcmp(node.attribute("print-object").value(), "no") == 0) {
-		return true;
+	if (nodeType(keysig, "key")) {
+		return keysig;
+	} else {
+		return xml_node(NULL);
 	}
-
-	return false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addSecondaryChordNotes --
+//	Tool_musicxml2hum::convertKeySigToHumdrum --
+//
+//  <key>
+//     <fifths>4</fifths>
+// and sometimes:
+//     <mode>major</mode>
+// or
+//     <mode>minor</mode>
 //
 
-void Tool_musicxml2hum::addSecondaryChordNotes(ostream& output,
-		MxmlEvent* head, const string& recip) {
-	vector<MxmlEvent*> links = head->getLinkedNotes();
-	MxmlEvent* note;
-	string pitch;
-	string prefix;
-	string postfix;
-	int slurstarts = 0;
-	int slurstops  = 0;
-	vector<int> slurdirs;
+xml_node Tool_musicxml2hum::convertKeySigToHumdrum(xml_node keysig,
+		HTp& token, int& staffindex) {
 
-	bool primarynote = false;
-	for (int i=0; i<(int)links.size(); i++) {
-		note       = links.at(i);
-		pitch      = note->getKernPitch();
-		prefix     = note->getPrefixNoteInfo();
-		postfix    = note->getPostfixNoteInfo(primarynote, recip);
-		slurstarts = note->hasSlurStart(slurdirs);
-		slurstops  = note->hasSlurStop();
+	if (!keysig) {
+		return keysig;
+	}
 
-		// or maybe walk backwards in the following loop?
-		for (int i=0; i<slurstarts; i++) {
-			prefix.insert(0, "(");
-			if (slurdirs[i] > 0) {
-				prefix.insert(1, ">");
-				m_slurabove++;
-			} else if (slurdirs[i] < 0) {
-				prefix.insert(1, "<");
-				m_slurbelow++;
-			}
+	staffindex = -1;
+	xml_attribute sn = keysig.attribute("number");
+	if (sn) {
+		staffindex = atoi(sn.value()) - 1;
+	}
+
+	int fifths = 0;
+	//int mode = -1;
+
+	xml_node child = keysig.first_child();
+	while (child) {
+		if (nodeType(child, "fifths")) {
+			fifths = atoi(child.child_value());
 		}
-		for (int i=0; i<slurstops; i++) {
-			postfix.push_back(')');
+		if (nodeType(child, "mode")) {
+			string value = child.child_value();
+			if (value == "major") {
+				// mode = 0;
+			} else if (value == "minor") {
+				// mode = 1;
+			}
 		}
-		//if (slurstart) {
-		//	prefix.insert(0, "(");
-		//	if (slurdir) {
-		//		if (slurdir > 0) {
-		//			prefix.insert(1, ">");
-		//			m_slurabove++;
-		//		} else if (slurdir < 0) {
-		//			prefix.insert(1, "<");
-		//			m_slurbelow++;
-		//		}
-		//	}
-		//}
-		//if (slurstop) {
-		//	postfix.push_back(')');
-		//}
+		child = child.next_sibling();
+	}
 
-		output << " " << prefix << recip << pitch << postfix;
+	stringstream ss;
+	ss << "*k[";
+	if (fifths > 0) {
+		if (fifths > 0) { ss << "f#"; }
+		if (fifths > 1) { ss << "c#"; }
+		if (fifths > 2) { ss << "g#"; }
+		if (fifths > 3) { ss << "d#"; }
+		if (fifths > 4) { ss << "a#"; }
+		if (fifths > 5) { ss << "e#"; }
+		if (fifths > 6) { ss << "b#"; }
+	} else if (fifths < 0) {
+		if (fifths < 0)  { ss << "b-"; }
+		if (fifths < -1) { ss << "e-"; }
+		if (fifths < -2) { ss << "a-"; }
+		if (fifths < -3) { ss << "d-"; }
+		if (fifths < -4) { ss << "g-"; }
+		if (fifths < -5) { ss << "c-"; }
+		if (fifths < -6) { ss << "f-"; }
+	}
+	ss << "]";
+
+	token = new HumdrumToken(ss.str());
+
+	keysig = keysig.next_sibling();
+	if (!keysig) {
+		return keysig;
+	}
+	if (nodeType(keysig, "key")) {
+		return keysig;
+	} else {
+		return xml_node(NULL);
 	}
 }
 
 
 
-/////////////////////////////
+//////////////////////////////
 //
-// Tool_musicxml2hum::appendZeroEvents --
+//	Tool_musicxml2hum::convertTimeSigToHumdrum --
+//
+//  <time symbol="common">
+//     <beats>4</beats>
+//     <beat-type>4</beat-type>
+//
+// also:
+//  <time symbol="common">
 //
 
-void Tool_musicxml2hum::appendZeroEvents(GridMeasure* outdata,
-		vector<SimultaneousEvents*>& nowevents, HumNum nowtime,
-		vector<MxmlPart>& partdata) {
+xml_node Tool_musicxml2hum::convertTimeSigToHumdrum(xml_node timesig,
+		HTp& token, int& staffindex) {
 
-	bool hasclef           = false;
-	bool haskeysig         = false;
-	bool haskeydesignation = false;
-	bool hastransposition  = false;
-	bool hastimesig        = false;
-	bool hasottava         = false;
-	bool hasstafflines     = false;
+	token = NULL;
 
-	vector<vector<xml_node>> clefs(partdata.size());
-	vector<vector<xml_node>> keysigs(partdata.size());
-	vector<vector<xml_node>> transpositions(partdata.size());
-	vector<vector<xml_node>> timesigs(partdata.size());
-	vector<vector<vector<xml_node>>> ottavas(partdata.size());
-	vector<vector<xml_node>> hairpins(partdata.size());
-	vector<vector<xml_node>> stafflines(partdata.size());
+	if (!timesig) {
+		return xml_node(NULL);
+	}
 
-	vector<vector<vector<vector<MxmlEvent*>>>> gracebefore(partdata.size());
-	vector<vector<vector<vector<MxmlEvent*>>>> graceafter(partdata.size());
-	bool foundnongrace = false;
+	staffindex = -1;
+	xml_attribute sn = timesig.attribute("number");
+	if (sn) {
+		staffindex = atoi(sn.value()) - 1;
+	}
 
-	int pindex = 0;
-	xml_node child;
-	xml_node grandchild;
+	int beats = -1;
+	int beattype = -1;
 
-	for (int i=0; i<(int)nowevents.size(); i++) {
-		for (int j=0; j<(int)nowevents[i]->zerodur.size(); j++) {
-			xml_node element = nowevents[i]->zerodur[j]->getNode();
-			pindex = nowevents[i]->zerodur[j]->getPartIndex();
+	xml_node child = timesig.first_child();
+	while (child) {
+		if (nodeType(child, "beats")) {
+			beats = atoi(child.child_value());
+		} else if (nodeType(child, "beat-type")) {
+			beattype = atoi(child.child_value());
+		}
+		child = child.next_sibling();
+	}
 
-			if (nodeType(element, "attributes")) {
-				child = element.first_child();
-				while (child) {
-					if (nodeType(child, "clef")) {
-						clefs[pindex].push_back(child);
-						hasclef = true;
-						foundnongrace = true;
-					}
+	if ((beats == -1) && (beattype == -1)) {
+		// No time signature, such as:
+		// <time print-object="no">
+		//   <senza-misura/>
+		// </time>
+		return xml_node(NULL);
+	}
 
-					if (nodeType(child, "key")) {
-						keysigs[pindex].push_back(child);
-						haskeysig = true;
-						string xpath = "mode";
-						string mode = child.select_node(xpath.c_str()).node().child_value();
-						if (mode != "") {
-							haskeydesignation = true;
-						}
-						foundnongrace = true;
-					}
+	stringstream ss;
+	ss << "*M" << beats<< "/" << beattype;
+	token = new HumdrumToken(ss.str());
 
-					if (nodeType(child, "transpose")) {
-						transpositions[pindex].push_back(child);
-						hastransposition = true;
-						foundnongrace = true;
-					}
+	timesig = timesig.next_sibling();
+	if (!timesig) {
+		return timesig;
+	}
+	if (nodeType(timesig, "time")) {
+		return timesig;
+	} else {
+		return xml_node(NULL);
+	}
+}
 
-					if (nodeType(child, "staff-details")) {
-						grandchild = child.first_child();
-						while (grandchild) {
-							if (nodeType(grandchild, "staff-lines")) {
-								stafflines[pindex].push_back(grandchild);
-								hasstafflines = true;
-							}
-							grandchild = grandchild.next_sibling();
-						}
-					}
 
-					if (nodeType(child, "time")) {
-						timesigs[pindex].push_back(child);
-						hastimesig = true;
-						foundnongrace = true;
-					}
-					child = child.next_sibling();
-				}
-			} else if (nodeType(element, "direction")) {
-				// direction -> direction-type -> words
-				// direction -> direction-type -> dynamics
-				// direction -> direction-type -> octave-shift
-				child = element.first_child();
-				if (nodeType(child, "direction-type")) {
-					grandchild = child.first_child();
-					if (nodeType(grandchild, "words")) {
-						m_current_text.emplace_back(std::make_pair(pindex, element));
-					} else if (nodeType(grandchild, "metronome")) {
-						m_current_tempo.emplace_back(std::make_pair(pindex, element));
-					} else if (nodeType(grandchild, "dynamics")) {
-						m_current_dynamic[pindex].push_back(element);
-					} else if (nodeType(grandchild, "octave-shift")) {
-						storeOttava(pindex, grandchild, element, ottavas);
-						hasottava = true;
-					} else if (nodeType(grandchild, "wedge")) {
-						m_current_dynamic[pindex].push_back(element);
-					} else if (nodeType(grandchild, "bracket")) {
-						m_current_brackets[pindex].push_back(element);
-					}
-				}
-			} else if (nodeType(element, "figured-bass")) {
-				m_current_figured_bass[pindex].push_back(element);
-			} else if (nodeType(element, "note")) {
-				if (foundnongrace) {
-					addEventToList(graceafter, nowevents[i]->zerodur[j]);
-				} else {
-					addEventToList(gracebefore, nowevents[i]->zerodur[j]);
-				}
-			} else if (nodeType(element, "print")) {
-				processPrintElement(outdata, element, nowtime);
-			}
+
+//////////////////////////////
+//
+//	Tool_musicxml2hum::convertMensurationToHumdrum --
+//
+//  <time symbol="common">
+//     <beats>4</beats>
+//     <beat-type>4</beat-type>
+//
+// also:
+//  <time symbol="common">
+//
+
+xml_node Tool_musicxml2hum::convertMensurationToHumdrum(xml_node timesig,
+		HTp& token, int& staffindex) {
+
+	if (!timesig) {
+		return timesig;
+	}
+
+	staffindex = -1;
+	xml_attribute mens = timesig.attribute("symbol");
+	if (!mens) {
+		token = new HumdrumToken("*");
+	} else {
+		string text = mens.value();
+		if (text == "cut") {
+			token = new HumdrumToken("*met(c|)");
+		} else if (text == "common") {
+			token = new HumdrumToken("*met(c)");
+		} else {
+			token = new HumdrumToken("*");
 		}
 	}
 
-	addGraceLines(outdata, gracebefore, partdata, nowtime);
+	timesig = timesig.next_sibling();
+	if (!timesig) {
+		return timesig;
+	}
+	if (nodeType(timesig, "time")) {
+		return timesig;
+	} else {
+		return xml_node(NULL);
+	}
+}
 
-	if (hasstafflines) {
-		addStriaLine(outdata, stafflines, partdata, nowtime);
+
+
+//////////////////////////////
+//
+//	Tool_musicxml2hum::convertClefToHumdrum --
+//
+
+xml_node Tool_musicxml2hum::convertClefToHumdrum(xml_node clef,
+		HTp& token, int& staffindex) {
+
+	if (!clef) {
+		// no clef for some reason.
+		return clef;
 	}
 
-	if (hasclef) {
-		addClefLine(outdata, clefs, partdata, nowtime);
+	staffindex = 0;
+	xml_attribute sn = clef.attribute("number");
+	if (sn) {
+		staffindex = atoi(sn.value()) - 1;
 	}
 
-	if (hastransposition) {
-		addTranspositionLine(outdata, transpositions, partdata, nowtime);
+	string sign;
+	int line = -1000;
+	int octadjust = 0;
+
+	xml_node child = clef.first_child();
+	while (child) {
+		if (nodeType(child, "sign")) {
+			sign = child.child_value();
+		} else if (nodeType(child, "line")) {
+			line = atoi(child.child_value());
+		} else if (nodeType(child, "clef-octave-change")) {
+			octadjust = atoi(child.child_value());
+		}
+		child = child.next_sibling();
 	}
 
-	if (haskeysig) {
-		addKeySigLine(outdata, keysigs, partdata, nowtime);
+	// Check for percussion clefs, etc., here.
+	if (sign == "percussion") {
+		sign = "X";
+		// ignore line of percussion clef (assume it is centered on staff).
+		line = -1000;
+	}
+	stringstream ss;
+	ss << "*clef" << sign;
+	if (octadjust < 0) {
+		for (int i=0; i < -octadjust; i++) {
+			ss << "v";
+		}
+	} else if (octadjust > 0) {
+		for (int i=0; i<octadjust; i++) {
+			ss << "^";
+		}
 	}
-
-	if (haskeydesignation) {
-		addKeyDesignationLine(outdata, keysigs, partdata, nowtime);
+	if (line > 0) {
+		ss << line;
 	}
+	token = new HumdrumToken(ss.str());
 
-	if (hastimesig) {
-		addTimeSigLine(outdata, timesigs, partdata, nowtime);
+	clef = clef.next_sibling();
+	if (!clef) {
+		return clef;
 	}
-
-	if (hasottava) {
-		addOttavaLine(outdata, ottavas, partdata, nowtime);
+	if (nodeType(clef, "clef")) {
+		return clef;
+	} else {
+		return xml_node(NULL);
 	}
-
-	addGraceLines(outdata, graceafter, partdata, nowtime);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::storeOcttava -- store an ottava mark which has this structure:
-//
-//  octaveShift:
-//     <octave-shift type="down" size="8" number="1" default-y="30.00"/>
-//
-//  For grand staff or multi-staff parts, the staff number needs to be extracted from an uncle element:
-//       <direction placement="below">
+//	Tool_musicxml2hum::convertOttavaToHumdrum --
+//    Example:
+//      <direction placement="above">
 //        <direction-type>
-//          <octave-shift type="up" size="8" number="1" default-y="-83.10"/>
+//          <octave-shift type="down" size="8" number="1"/>
+//        </direction-type>
+//      </direction>
+//      ...
+//      <direction placement="above">
+//        <direction-type>
+//          <octave-shift type="stop" size="8" number="1"/>
 //        </direction-type>
-//        <staff>2</staff>
 //      </direction>
 //
-// ottavas array has three dimensions: (1) is the part, (2) is the staff, and (3) is the list of ottavas.
 //
 
-void Tool_musicxml2hum::storeOttava(int pindex, xml_node octaveShift, xml_node direction,
-	vector<vector<vector<xml_node>>>& ottavas) {
-	int staffindex = 0;
-	xml_node staffnode = direction.select_node("staff").node();
-	if (staffnode && staffnode.text()) {
-		int staffnum = staffnode.text().as_int();
-		if (staffnum > 0) {
-			staffindex = staffnum - 1;
-		}
-	}
-	// ottavas presumed to be allocated by part, but not by staff.
-	if ((int)ottavas[pindex].size() <= staffindex) {
-		ottavas[pindex].resize(staffindex+1);
-	}
-	ottavas[pindex][staffindex].push_back(octaveShift);
-}
+xml_node Tool_musicxml2hum::convertOttavaToHumdrum(xml_node ottava,
+		HTp& token, int& staffindex, int partindex, int partstaffindex, int staffcount) {
+
+	// partstaffindex is useless or incorrect? At least for grand staff parts.
+	// The staffindex calculated below is the one to used.
 
+	if (!ottava) {
+		// no clef for some reason.
+		return ottava;
+	}
 
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::processPrintElement --
-//      <print new-page="yes">
-//      <print new-system="yes">
-//
+	// Don't use "number" to set the staff index, now use the input parameter
+	// which comes from the direction/staff element that is an uncle of the ottava node.
+	//staffindex = 0;
+	//xml_attribute sn = ottava.attribute("number");
+	//if (sn) {
+	//	staffindex = atoi(sn.value()) - 1;
+	//}
+	//staffindex = staffcount - staffindex - 1;
 
-void Tool_musicxml2hum::processPrintElement(GridMeasure* outdata, xml_node element,
-		HumNum timestamp) {
-	bool isPageBreak = false;
-	bool isSystemBreak = false;
-	string pageparam = element.attribute("new-page").value();
-	string systemparam = element.attribute("new-system").value();
-	if (pageparam == "yes") {
-		isPageBreak = true;
-	}
-	if (systemparam == "yes") {
-		isSystemBreak = true;
-	}
+	int interval = 0;
 
-	if (!(isPageBreak || isSystemBreak)) {
-		return;
-	}
-	GridSlice* gs = outdata->back();
+	interval = ottava.attribute("size").as_int();
+	string otype = ottava.attribute("type").as_string();
+	string lastotype = m_last_ottava_direction.at(partindex).at(staffindex);
 
-	HTp token = NULL;
-	if (gs && gs->size() > 0) {
-		if (gs->at(0)->size() > 0) {
-			if (gs->at(0)->at(0)->size() > 0) {
-				token = gs->at(0)->at(0)->at(0)->getToken();
+	string ss;
+	ss = "*";
+	if (otype == "stop") {
+		ss += "X";
+	} else {
+	   m_last_ottava_direction.at(partindex).at(staffindex) = otype;
+   }
+	if (interval == 15) {
+		ss += "15";
+		if (otype == "down") {
+			ss += "ma";
+		} else if (otype == "up") {
+			ss += "ba";
+		} else if (otype == "stop") {
+			if (m_last_ottava_direction.at(partindex).at(staffindex) == "up") {
+				ss += "ba";
+			} else if (m_last_ottava_direction.at(partindex).at(staffindex) == "down") {
+				ss += "ma";
 			}
 		}
-	}
-
-	if (isPageBreak) {
-		if (!token || *token != "!!pagebreak:original")  {
-			outdata->addGlobalComment("!!pagebreak:original", timestamp);
+	} else if (interval == 8) {
+		ss += "8";
+		if (otype == "down") {
+			ss += "va";
+		} else if (otype == "up") {
+			ss += "ba";
+		} else if (otype == "stop") {
+			if (m_last_ottava_direction.at(partindex).at(staffindex) == "up") {
+				ss += "ba";
+			} else if (m_last_ottava_direction.at(partindex).at(staffindex) == "down") {
+				ss += "va";
+			}
 		}
-	} else if (isSystemBreak) {
-		if (!token || *token != "!!linebreak:original")  {
-			outdata->addGlobalComment("!!linebreak:original", timestamp);
+	} else {
+		ss += "*8";
+		if (otype == "down") {
+			ss += "va";
+		} else if (otype == "up") {
+			ss += "ba";
+		} else if (otype == "stop") {
+			if (m_last_ottava_direction.at(partindex).at(staffindex) == "up") {
+				ss += "ba";
+			} else if (m_last_ottava_direction.at(partindex).at(staffindex) == "down") {
+				ss += "va";
+			}
 		}
 	}
+	token = new HumdrumToken(ss);
+
+	ottava = ottava.next_sibling();
+	if (!ottava) {
+		return ottava;
+	}
+	if (nodeType(ottava, "octave-shift")) {
+		return ottava;
+	} else {
+		return xml_node(NULL);
+	}
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_musicxml2hum::addEventToList --
+// Tool_musicxml2hum::nodeType -- return true if node type matches
+//     string.
 //
 
-void Tool_musicxml2hum::addEventToList(vector<vector<vector<vector<MxmlEvent*> > > >& list,
-		MxmlEvent* event) {
-	int pindex = event->getPartIndex();
-	int staffindex = event->getStaffIndex();
-	int voiceindex = event->getVoiceIndex();
-	if (pindex >= (int)list.size()) {
-		list.resize(pindex+1);
+bool Tool_musicxml2hum::nodeType(xml_node node, const char* testname) {
+	if (strcmp(node.name(), testname) == 0) {
+		return true;
+	} else {
+		return false;
 	}
-	if (staffindex >= (int)list[pindex].size()) {
-		list[pindex].resize(staffindex+1);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::appendNullTokens --
+//
+
+void Tool_musicxml2hum::appendNullTokens(HLp line,
+		MxmlPart& part) {
+	int i;
+	int staffcount = part.getStaffCount();
+	int versecount = part.getVerseCount();
+	for (i=staffcount-1; i>=0; i--) {
+		line->appendToken(".");
 	}
-	if (voiceindex >= (int)list[pindex][staffindex].size()) {
-		list[pindex][staffindex].resize(voiceindex+1);
+	for (i=0; i<versecount; i++) {
+		line->appendToken(".");
 	}
-	list[pindex][staffindex][voiceindex].push_back(event);
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_musicxml2hum::addGraceLines -- Add grace note lines.  The number of
-//     lines is equal to the maximum number of successive grace notes in
-//     any part.  Grace notes are filled in reverse sequence.
+// Tool_musicxml2hum::getPartContent -- Extract the part elements in
+//     the file indexed by part ID.
 //
 
-void Tool_musicxml2hum::addGraceLines(GridMeasure* outdata,
-		vector<vector<vector<vector<MxmlEvent*> > > >& notes,
-		vector<MxmlPart>& partdata, HumNum nowtime) {
+bool Tool_musicxml2hum::getPartContent(
+		map<string, xml_node>& partcontent,
+		vector<string>& partids, xml_document& doc) {
 
-	int maxcount = 0;
+	auto parts = doc.select_nodes("/score-partwise/part");
+	int count = (int)parts.size();
+	if (count != (int)partids.size()) {
+		cerr << "Warning: part element count does not match part IDs count: "
+		     << parts.size() << " compared to " << partids.size() << endl;
+	}
 
-	for (int i=0; i<(int)notes.size(); i++) {
-		for (int j=0; j<(int)notes.at(i).size(); j++) {
-			for (int k=0; k<(int)notes.at(i).at(j).size(); k++) {
-				if (maxcount < (int)notes.at(i).at(j).at(k).size()) {
-					maxcount = (int)notes.at(i).at(j).at(k).size();
-				}
-			}
+	string partid;
+	for (int i=0; i<(int)parts.size(); i++) {
+		partid = getAttributeValue(parts[i], "id");
+		if (partid.size() == 0) {
+			cerr << "Warning: Part " << i << " has no ID" << endl;
+		}
+		auto status = partcontent.insert(make_pair(partid, parts[i].node()));
+		if (status.second == false) {
+			cerr << "Error: ID " << partids.back()
+			     << " is duplicated and secondary part will be ignored" << endl;
+		}
+		if (find(partids.begin(), partids.end(), partid) == partids.end()) {
+			cerr << "Error: Part ID " << partid
+			     << " is not present in part-list element list" << endl;
+			continue;
 		}
 	}
 
-	if (maxcount == 0) {
-		return;
+	if (partcontent.size() != partids.size()) {
+		cerr << "Error: part-list count does not match part count "
+		     << partcontent.size() << " compared to " << partids.size() << endl;
+		return false;
+	} else {
+		return true;
 	}
+}
 
-	vector<GridSlice*> slices(maxcount);
-	for (int i=0; i<(int)slices.size(); i++) {
-		slices[i] = new GridSlice(outdata, nowtime, SliceType::GraceNotes);
-		outdata->push_back(slices[i]);
-		slices[i]->initializePartStaves(partdata);
-	}
 
-	for (int i=0; i<(int)notes.size(); i++) {
-		for (int j=0; j<(int)notes[i].size(); j++) {
-			for (int k=0; k<(int)notes[i][j].size(); k++) {
-				int startm = maxcount - (int)notes[i][j][k].size();
-				for (int m=0; m<(int)notes[i][j][k].size(); m++) {
-					addEvent(slices.at(startm+m), outdata, notes[i][j][k][m], nowtime);
-				}
-			}
+
+//////////////////////////////
+//
+// Tool_musicxml2hum::getPartInfo -- Extract a list of the part ids,
+//    and a reverse mapping to the <score-part> element to which is refers.
+//
+//	   part-list structure:
+//        <part-list>
+//          <score-part id="P1"/>
+//          <score-part id="P2"/>
+//          etc.
+//        </part-list>
+//
+
+bool Tool_musicxml2hum::getPartInfo(map<string, xml_node>& partinfo,
+		vector<string>& partids, xml_document& doc) {
+	auto scoreparts = doc.select_nodes("/score-partwise/part-list/score-part");
+	partids.reserve(scoreparts.size());
+	bool output = true;
+	for (auto el : scoreparts) {
+		partids.emplace_back(getAttributeValue(el.node(), "id"));
+		auto status = partinfo.insert(make_pair(partids.back(), el.node()));
+		if (status.second == false) {
+			cerr << "Error: ID " << partids.back()
+			     << " is duplicated and secondary part will be ignored" << endl;
 		}
+		output &= status.second;
+		partinfo[partids.back()] = el.node();
 	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addClefLine --
+// Tool_musicxml2hum::getChildElementText -- Return the (first)
+//    matching child element's text content.
 //
 
-void Tool_musicxml2hum::addClefLine(GridMeasure* outdata,
-		vector<vector<xml_node> >& clefs, vector<MxmlPart>& partdata,
-		HumNum nowtime) {
-
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-		SliceType::Clefs);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
+string Tool_musicxml2hum::getChildElementText(xml_node root,
+		const char* xpath) {
+	return root.select_node(xpath).node().child_value();
+}
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)clefs[i].size(); j++) {
-			if (clefs[i][j]) {
-				insertPartClefs(clefs[i][j], *slice->at(i));
-			}
-		}
-	}
+string Tool_musicxml2hum::getChildElementText(xpath_node root,
+		const char* xpath) {
+	return root.node().select_node(xpath).node().child_value();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addStriaLine --
+// Tool_musicxml2hum::getAttributeValue -- For an xml_node, return
+//     the value for the given attribute name.
 //
 
-void Tool_musicxml2hum::addStriaLine(GridMeasure* outdata,
-		vector<vector<xml_node> >& stafflines, vector<MxmlPart>& partdata,
-		HumNum nowtime) {
+string Tool_musicxml2hum::getAttributeValue(xml_node xnode,
+		const string& target) {
+	for (auto at = xnode.first_attribute(); at; at = at.next_attribute()) {
+		if (target == at.name()) {
+			return at.value();
+		}
+	}
+	return "";
+}
 
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-		SliceType::Stria);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)stafflines[i].size(); j++) {
-			if (stafflines[i][j]) {
-				string lines = stafflines[i][j].child_value();
-				int linecount = stoi(lines);
-				insertPartStria(linecount, *slice->at(i));
-			}
+string Tool_musicxml2hum::getAttributeValue(xpath_node xnode,
+		const string& target) {
+	auto node = xnode.node();
+	for (auto at = node.first_attribute(); at; at = at.next_attribute()) {
+		if (target == at.name()) {
+			return at.value();
 		}
 	}
+	return "";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addTimeSigLine --
+// Tool_musicxml2hum::printAttributes -- Print list of all attributes
+//     for an xml_node.
 //
 
-void Tool_musicxml2hum::addTimeSigLine(GridMeasure* outdata,
-		vector<vector<xml_node> >& timesigs, vector<MxmlPart>& partdata,
-		HumNum nowtime) {
+void Tool_musicxml2hum::printAttributes(xml_node node) {
+	int counter = 1;
+	for (auto at = node.first_attribute(); at; at = at.next_attribute()) {
+		cout << "\tattribute " << counter++
+		     << "\tname  = " << at.name()
+		     << "\tvalue = " << at.value()
+		     << endl;
+	}
+}
 
-	GridSlice* slice = new GridSlice(outdata, nowtime, SliceType::TimeSigs);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
 
-	bool status = false;
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)timesigs[i].size(); j++) {
-			if (timesigs[i][j]) {
-				status |= insertPartTimeSigs(timesigs[i][j], *slice->at(i));
-			}
-		}
-	}
+//////////////////////////////
+//
+// Tool_musicxml2hum::getSystemDecoration --
+//
+// Example:  [1,2]{(3,4)}
+//
+//  <part-list>
+//    <part-group type="start" number="1">
+//      <group-symbol>bracket</group-symbol>
+//    </part-group>
+//
+//    <score-part id="P1">
+//      <part-name>S A</part-name>
+//      <score-instrument id="P1-I1">
+//        <instrument-name>Soprano/Alto</instrument-name>
+//      </score-instrument>
+//      <midi-device id="P1-I1" port="1"></midi-device>
+//      <midi-instrument id="P1-I1">
+//        <midi-channel>1</midi-channel>
+//        <midi-program>53</midi-program>
+//        <volume>78.7402</volume>
+//        <pan>0</pan>
+//      </midi-instrument>
+//    </score-part>
+//
+//    <score-part id="P2">
+//      <part-name>T B</part-name>
+//      <score-instrument id="P2-I1">
+//        <instrument-name>Tenor/Bass</instrument-name>
+//      </score-instrument>
+//      <midi-device id="P2-I1" port="1"></midi-device>
+//      <midi-instrument id="P2-I1">
+//        <midi-channel>2</midi-channel>
+//        <midi-program>53</midi-program>
+//        <volume>78.7402</volume>
+//        <pan>0</pan>
+//      </midi-instrument>
+//    </score-part>
+//
+//    <part-group type="stop" number="1"/>
+//
+//    <score-part id="P3">
+//      <part-name>Organ</part-name>
+//      <part-abbreviation>Org.</part-abbreviation>
+//      <score-instrument id="P3-I1">
+//        <instrument-name>Pipe Organ</instrument-name>
+//      </score-instrument>
+//      <midi-device id="P3-I1" port="1"></midi-device>
+//      <midi-instrument id="P3-I1">
+//        <midi-channel>3</midi-channel>
+//        <midi-program>76</midi-program>
+//        <volume>78.7402</volume>
+//        <pan>0</pan>
+//      </midi-instrument>
+//    </score-part>
+//
+//  </part-list>
+//
 
-	if (!status) {
-		return;
+string Tool_musicxml2hum::getSystemDecoration(xml_document& doc, HumGrid& grid,
+	vector<string>& partids) {
+
+	xml_node partlist = doc.select_node("/score-partwise/part-list").node();
+	if (!partlist) {
+		cerr << "Error: cannot find partlist\n";
+		return "";
 	}
+	vector<xml_node> children;
+	getChildrenVector(children, partlist);
 
-	// Add mensurations related to time signatures
+	vector<vector<int>> staffnumbers;
+	int pcount = grid.getPartCount();
+	staffnumbers.resize(pcount);
 
-	slice = new GridSlice(outdata, nowtime, SliceType::MeterSigs);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
+	int scounter = 1;
+	for (int i=0; i<pcount; i++) {
+		int staffcount = grid.getStaffCount(i);
+		for (int j=0; j<staffcount; j++) {
+			staffnumbers[i].push_back(scounter++);
+		}
+	}
 
-	// now add mensuration symbols associated with time signatures
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)timesigs[i].size(); j++) {
-			if (timesigs[i][j]) {
-				insertPartMensurations(timesigs[i][j], *slice->at(i));
+	string output;
+
+	// part-group @type=start @number=1
+   //   <group-symbol>bracket</group-symbol>
+	// score-part
+	// score-part
+	// part-group @type=stop @number=1
+	// score-part
+	int pcounter = 0;
+	scounter = 1;
+	vector<string> typeendings(100);
+	for (int i=0; i<(int)children.size(); i++) {
+		string name = children[i].name();
+		if (name == "part-group") {
+			string grouptype = children[i].attribute("type").value();
+			string gsymbol = "";
+			int number = children[i].attribute("number").as_int();
+			if (grouptype == "start") {
+				string g = children[i].select_node("//group-symbol").node().child_value();
+				if (g == "bracket") {
+					output += "[(";
+					typeendings[number] = ")]";
+				} else if (g == "brace") {
+					output += "{(";
+					typeendings[number] = ")}";
+				} else {
+					cerr << "Unknown part grouping symbol: " << g << endl;
+				}
+			} else if (grouptype == "stop") {
+				output += typeendings[number];
+				typeendings[number].clear();
+			}
+		} else if (name == "score-part") {
+			pcounter++;
+			int staffcount = grid.getStaffCount(pcounter-1);
+			if (staffcount == 1) {
+				output += "s" + to_string(scounter++);
+			} else if (staffcount > 1) {
+				output += "{(";
+				for (int k=0; k<staffcount; k++) {
+					output += "s" + to_string(scounter++);
+				}
+				output += ")}";
 			}
 		}
 	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_musicxml2hum::addOttavaLine -- Probably there will be a problem if
-//    an ottava line ends and another one starts at the same timestamp.
-//    Maybe may OttavaStart and OttavaEnd be separate categories?
-//
-
-void Tool_musicxml2hum::addOttavaLine(GridMeasure* outdata,
-		vector<vector<vector<xml_node>>>& ottavas, vector<MxmlPart>& partdata,
-		HumNum nowtime) {
-
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-		SliceType::Ottavas);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
 
-	for (int p=0; p<(int)ottavas.size(); p++) { // part loop
-		for (int s=0; s<(int)ottavas[p].size(); s++) { // staff loop
-			for (int j=0; j<(int)ottavas[p][s].size(); j++) { // ottava loop
-				if (ottavas[p][s][j]) {
-					// int scount = partdata[p].getStaffCount();
-					// int ss = scount - s - 1;
-					insertPartOttavas(ottavas[p][s][j], *slice->at(p), p, s, partdata[p].getStaffCount());
-				}
-			}
+	string newoutput;
+	for (int i=0; i<(int)output.size(); i++) {
+		if ((i>0) && (output[i] == 's') && isdigit(output[i-1])) {
+			newoutput += ',';
 		}
+		newoutput += output[i];
 	}
+
+	return newoutput;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::addKeySigLine -- Only adding one key signature
-//   for each part for now.
+// Tool_musicxml2hum::getChildrenVector -- Return a list of all children
+//   elements of a given element.  Pugixml does not allow random access,
+//   but storing them in a vector allows that possibility.
 //
 
-void Tool_musicxml2hum::addKeySigLine(GridMeasure* outdata,
-		vector<vector<xml_node> >& keysigs,
-		vector<MxmlPart>& partdata, HumNum nowtime) {
-
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-		SliceType::KeySigs);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
-
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)keysigs[i].size(); j++) {
-			if (keysigs[i][j]) {
-				insertPartKeySigs(keysigs[i][j], *slice->at(i));
-			}
-		}
+void Tool_musicxml2hum::getChildrenVector(vector<xml_node>& children,
+		xml_node parent) {
+	children.clear();
+	for (xml_node child : parent.children()) {
+		children.push_back(child);
 	}
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_musicxml2hum::addKeyDesignationLine -- Only adding one key designation line
-//   for each part for now.
+// Tool_myank::Tool_myank -- Set the recognized options for the tool.
 //
 
-void Tool_musicxml2hum::addKeyDesignationLine(GridMeasure* outdata,
-		vector<vector<xml_node> >& keydesigs,
-		vector<MxmlPart>& partdata, HumNum nowtime) {
-
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-		SliceType::KeyDesignations);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
-
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)keydesigs[i].size(); j++) {
-			if (keydesigs[i][j]) {
-				insertPartKeyDesignations(keydesigs[i][j], *slice->at(i));
-			}
-		}
-	}
+Tool_myank::Tool_myank(void) {
+	define("v|verbose=b",                        "verbose output of data");
+	define("debug=b",                            "debugging information");
+	define("inlist=b",                           "show input measure list");
+	define("outlist=b",                          "show output measure list");
+	define("mark|marks=b",                       "yank measure with marked notes");
+	define("T|M|bar-number-text=b",              "print barnum with LO text above system ");
+	define("d|double|dm|md|mdsep|mdseparator=b", "put double barline between non-consecutive measure segments");
+	define("m|b|measures|bars|measure|bar=s",    "measures to yank");
+	define("l|lines|line-range=s",               "line numbers range to yank (e.g. 40-50)");
+	define("I|i|instrument=b",                   "Include instrument codes from start of data");
+	define("visible|not-invisible=b",            "do not make initial measure invisible");
+	define("B|noendbar=b",                       "do not print barline at end of data");
+	define("max=b",                              "print maximum measure number");
+	define("min=b",                              "print minimum measure number");
+	define("section-count=b",                    "count the number of sections, JRP style");
+	define("section=i:0",                        "extract given section number (indexed from 1");
+	define("author=b",                           "program author");
+	define("version=b",                          "program version");
+	define("example=b",                          "program examples");
+	define("h|help=b",                           "short description");
+	define("hide-starting=b",                    "prevent printStarting");
+	define("hide-ending=b",                      "prevent printEnding");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_musicxml2hum::addTranspositionLine -- Transposition codes to
-//   produce written parts.
+// Tool_myank::run -- Primary interfaces to the tool.
 //
 
-void Tool_musicxml2hum::addTranspositionLine(GridMeasure* outdata,
-		vector<vector<xml_node> >& transpositions,
-		vector<MxmlPart>& partdata, HumNum nowtime) {
+bool Tool_myank::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
 
-	GridSlice* slice = new GridSlice(outdata, nowtime,
-		SliceType::Transpositions);
-	outdata->push_back(slice);
-	slice->initializePartStaves(partdata);
 
-	for (int i=0; i<(int)partdata.size(); i++) {
-		for (int j=0; j<(int)transpositions[i].size(); j++) {
-			if (transpositions[i][j]) {
-				insertPartTranspositions(transpositions[i][j], *slice->at(i));
-			}
-		}
+bool Tool_myank::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
 }
 
 
+bool Tool_myank::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
 
-//////////////////////////////
 //
-// Tool_musicxml2hum::insertPartClefs --
+// In-place processing of file:
 //
 
-void Tool_musicxml2hum::insertPartClefs(xml_node clef, GridPart& part) {
-	if (!clef) {
-		// no clef for some reason.
-		return;
-	}
+bool Tool_myank::run(HumdrumFile& infile) {
+	// Max track in enscripten is wrong for some reason,
+	// so making a copy and forcing reanalysis:
+	//perhaps not needed anymore:
+	//stringstream ss;
+	//ss << infile;
+	//infile.read(ss);
+	initialize(infile);
+	processFile(infile);
+	// Re-load the text for each line from their tokens.
+	infile.createLinesFromTokens();
+	return true;
+}
 
-	HTp token;
-	int staffnum = 0;
-	while (clef) {
-		clef = convertClefToHumdrum(clef, token, staffnum);
-		part[staffnum]->setTokenLayer(0, token, 0);
-	}
 
-	// go back and fill in all NULL pointers with null interpretations
-	fillEmpties(&part, "*");
+///////////////////////////////////////////////////////////////////////////
+
+ostream& operator<<(ostream& out, MyCoord& value) {
+	out << "(" << value.x << "," << value.y << ")";
+	return out;
 }
 
 
+ostream& operator<<(ostream& out, MeasureInfo& info) {
+	if (info.file == NULL) {
+		return out;
+	}
+	HumdrumFile& infile = *(info.file);
+	out << "================================== " << endl;
+	out << "NUMBER      = " << info.num   << endl;
+	out << "SEGMENT     = " << info.seg   << endl;
+	out << "START       = " << info.start << endl;
+	out << "STOP        = " << info.stop  << endl;
+	out << "STOP_STYLE  = " << info.stopStyle << endl;
+	out << "START_STYLE = " << info.startStyle << endl;
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::insertPartStria --
-//
+	for (int i=1; i<(int)info.sclef.size(); i++) {
+		out << "TRACK " << i << ":" << endl;
+		if (info.sclef[i].isValid()) {
+			out << "   START CLEF    = " << infile.token(info.sclef[i].x, info.sclef[i].y)       << endl;
+		}
+		if (info.skeysig[i].isValid()) {
+			out << "   START KEYSIG  = " << infile.token(info.skeysig[i].x, info.skeysig[i].y)   << endl;
+		}
+		if (info.skey[i].isValid()) {
+			out << "   START KEY     = " << infile.token(info.skey[i].x, info.skey[i].y)         << endl;
+		}
+		if (info.stimesig[i].isValid()) {
+			out << "   START TIMESIG = " << infile.token(info.stimesig[i].x, info.stimesig[i].y) << endl;
+		}
+		if (info.smet[i].isValid()) {
+			out << "   START MET     = " << infile.token(info.smet[i].x, info.smet[i].y)         << endl;
+		}
+		if (info.stempo[i].isValid()) {
+			out << "   START TEMPO   = " << infile.token(info.stempo[i].x, info.stempo[i].y)     << endl;
+		}
 
-void Tool_musicxml2hum::insertPartStria(int lines, GridPart& part) {
-	HTp token = new HumdrumToken;
-	string value = "*stria" + to_string(lines);
-	token->setText(value);
-	part[0]->setTokenLayer(0, token, 0);
+		if (info.eclef[i].isValid()) {
+			out << "   END CLEF    = " << infile.token(info.eclef[i].x, info.eclef[i].y)       << endl;
+		}
+		if (info.ekeysig[i].isValid()) {
+			out << "   END KEYSIG  = " << infile.token(info.ekeysig[i].x, info.ekeysig[i].y)   << endl;
+		}
+		if (info.ekey[i].isValid()) {
+			out << "   END KEY     = " << infile.token(info.ekey[i].x, info.ekey[i].y)         << endl;
+		}
+		if (info.etimesig[i].isValid()) {
+			out << "   END TIMESIG = " << infile.token(info.etimesig[i].x, info.etimesig[i].y) << endl;
+		}
+		if (info.emet[i].isValid()) {
+			out << "   END MET     = " << infile.token(info.emet[i].x, info.emet[i].y)         << endl;
+		}
+		if (info.etempo[i].isValid()) {
+			out << "   END TEMPO   = " << infile.token(info.etempo[i].x, info.etempo[i].y)     << endl;
+		}
+	}
 
-	// go back and fill in all NULL pointers with null interpretations
-	fillEmpties(&part, "*");
+	return out;
 }
 
+///////////////////////////////////////////////////////////////////////////
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::insertPartOttavas --
+// Tool_myank::initialize -- extract time signature lines for
+//    each **kern spine in file.
 //
 
-void Tool_musicxml2hum::insertPartOttavas(xml_node ottava, GridPart& part, int partindex,
-		int partstaffindex, int staffcount) {
-	if (!ottava) {
-		// no ottava for some reason.
+void Tool_myank::initialize(HumdrumFile& infile) {
+	// handle basic options:
+	if (getBoolean("author")) {
+		m_free_text << "Written by Craig Stuart Sapp, "
+			  << "craig@ccrma.stanford.edu, December 2010" << endl;
+		return;
+	} else if (getBoolean("version")) {
+		m_free_text << getCommand() << ", version: 26 December 2010" << endl;
+		m_free_text << "compiled: " << __DATE__ << endl;
+		return;
+	} else if (getBoolean("help")) {
+		usage(getCommand());
+		return;
+	} else if (getBoolean("example")) {
+		example();
 		return;
 	}
 
-	HTp token = NULL;
-	while (ottava) {
-		ottava = convertOttavaToHumdrum(ottava, token, partstaffindex, partindex, partstaffindex, staffcount);
-		part[partstaffindex]->setTokenLayer(0, token, 0);
-	}
-
-	// go back and fill in all NULL pointers with null interpretations
-	fillEmpties(&part, "*");
-}
-
+	m_debugQ        = getBoolean("debug");
+	m_inlistQ       = getBoolean("inlist");
+	m_outlistQ      = getBoolean("outlist");
+	m_verboseQ      = getBoolean("verbose");
+	m_maxQ          = getBoolean("max");
+	m_minQ          = getBoolean("min");
 
+	m_invisibleQ    = !getBoolean("not-invisible");
+	m_instrumentQ   =  getBoolean("instrument");
+	m_nolastbarQ    =  getBoolean("noendbar");
+	m_markQ         =  getBoolean("mark");
+	m_doubleQ       =  getBoolean("mdsep");
+	m_barnumtextQ   =  getBoolean("bar-number-text");
+	m_sectionCountQ =  getBoolean("section-count");
+	m_section       =  getInteger("section");
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::fillEmpties --
-//
+	m_lineRange     = getString("lines");
+	m_hideStarting  = getBoolean("hide-starting");
+	m_hideEnding    = getBoolean("hide-ending");
 
-void Tool_musicxml2hum::fillEmpties(GridPart* part, const char* string) {
-	int staffcount = (int)part->size();
-	GridVoice* gv;
-	int vcount;
 
- 	for (int s=0; s<staffcount; s++) {
-		GridStaff* staff = part->at(s);
-		if (staff == NULL) {
-			cerr << "Strange error here" << endl;
-			continue;
-		}
-		vcount = (int)staff->size();
-		if (vcount == 0) {
-			gv = new GridVoice(string, 0);
-			staff->push_back(gv);
-		} else {
-			for (int v=0; v<vcount; v++) {
-				gv = staff->at(v);
-				if (gv == NULL) {
-					gv = new GridVoice(string, 0);
-					staff->at(v) = gv;
-				}
-			}
+	if (!m_section) {
+		if (!(getBoolean("measures") || m_markQ) && !getBoolean("lines")) {
+			// if -m option is not given, then --mark option presumed
+			m_markQ = 1;
+			// cerr << "Error: the -m option is required" << endl;
+			// exit(1);
 		}
 	}
+
 }
 
 
 
-//////////////////////////////
+////////////////////////
 //
-// Tool_musicxml2hum::insertPartKeySigs --
+// Tool_myank::processFile --
 //
 
-void Tool_musicxml2hum::insertPartKeySigs(xml_node keysig, GridPart& part) {
-	if (!keysig) {
+void Tool_myank::processFile(HumdrumFile& infile) {
+	if (m_sectionCountQ) {
+		int sections = getSectionCount(infile);
+		m_humdrum_text << sections << endl;
 		return;
 	}
 
-	HTp token;
-	int staffnum = 0;
-	while (keysig) {
-		keysig = convertKeySigToHumdrum(keysig, token, staffnum);
-		if (staffnum < 0) {
-			// key signature applies to all staves in part (most common case)
-			for (int s=0; s<(int)part.size(); s++) {
-				if (s==0) {
-					part[s]->setTokenLayer(0, token, 0);
-				} else {
-					HTp token2 = new HumdrumToken(*token);
-					part[s]->setTokenLayer(0, token2, 0);
-				}
-			}
-		} else {
-			part[staffnum]->setTokenLayer(0, token, 0);
+	getMetStates(m_metstates, infile);
+	getMeasureStartStop(m_measureInList, infile);
+
+	string measurestring = getString("measures");
+
+	if (getBoolean("lines")) {
+		int startLineNumber = getStartLineNumber();
+		int endLineNumber = getEndLineNumber();
+		if ((startLineNumber > endLineNumber) || (endLineNumber > infile.getLineCount())) {
+			// Disallow when end line number is bigger then line count or when
+			// start line number greather than end line number
+			return;
 		}
+		m_barNumbersPerLine = analyzeBarNumbers(infile);
+		int startBarNumber = getBarNumberForLineNumber(startLineNumber);
+		int endBarNumber = getBarNumberForLineNumber(endLineNumber);
+		measurestring = to_string(startBarNumber) + "-" + to_string(endBarNumber);
 	}
-}
 
+	measurestring = expandMultipliers(measurestring);
+	if (m_markQ) {
+		stringstream mstring;
+		getMarkString(mstring, infile);
+		measurestring = mstring.str();
+		if (m_debugQ) {
+			m_free_text << "MARK STRING: " << mstring.str() << endl;
+		}
+	} else if (m_section) {
+		string sstring;
+		getSectionString(sstring, infile, m_section);
+		measurestring = sstring;
+	}
+	if (m_debugQ) {
+		m_free_text << "MARK MEASURES: " << measurestring << endl;
+	}
 
+	// expand to multiple measures later.
+	expandMeasureOutList(m_measureOutList, m_measureInList, infile,
+			measurestring);
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::insertPartKeyDesignations --
-//
+	if (m_inlistQ) {
+		m_free_text << "INPUT MEASURE MAP: " << endl;
+		for (int i=0; i<(int)m_measureInList.size(); i++) {
+			m_free_text << m_measureInList[i];
+		}
+	}
+	if (m_outlistQ) {
+		m_free_text << "OUTPUT MEASURE MAP: " << endl;
+		for (int i=0; i<(int)m_measureOutList.size(); i++) {
+			m_free_text << m_measureOutList[i];
+		}
+	}
 
-void Tool_musicxml2hum::insertPartKeyDesignations(xml_node keydesig, GridPart& part) {
-	if (!keydesig) {
+	if (m_measureOutList.size() == 0) {
+		// disallow processing files with no barlines
 		return;
 	}
 
-	HTp token;
-	int staffnum = 0;
-	while (keydesig) {
-		token = NULL;
-		keydesig = convertKeySigToHumdrumKeyDesignation(keydesig, token, staffnum);
-		if (token == NULL) {
-			return;
-		}
-		if (staffnum < 0) {
-			// key signature applies to all staves in part (most common case)
-			for (int s=0; s<(int)part.size(); s++) {
-				if (s==0) {
-					part[s]->setTokenLayer(0, token, 0);
-				} else {
-					string value = *token;
-					HTp token2 = new HumdrumToken(value);
-					part[s]->setTokenLayer(0, token2, 0);
-				}
-			}
-		} else {
-			part[staffnum]->setTokenLayer(0, token, 0);
-		}
+	// move stopStyle to startStyle of next measure group.
+	for (int i=(int)m_measureOutList.size()-1; i>0; i--) {
+		m_measureOutList[i].startStyle = m_measureOutList[i-1].stopStyle;
+		m_measureOutList[i-1].stopStyle = "";
 	}
+
+	myank(infile, m_measureOutList);
 }
 
 
 
-//////////////////////////////
+////////////////////////
 //
-// Tool_musicxml2hum::insertPartTranspositions --
+// Tool_myank::analyzeBarNumbers -- Stores the bar number of each line in a vector
 //
 
-void Tool_musicxml2hum::insertPartTranspositions(xml_node transposition, GridPart& part) {
-	if (!transposition) {
-		return;
-	}
-
-	HTp token;
-	int staffnum = 0;
-	while (transposition) {
-		transposition = convertTranspositionToHumdrum(transposition, token, staffnum);
-		if (staffnum < 0) {
-			// Transposition applies to all staves in part (most common case)
-			for (int s=0; s<(int)part.size(); s++) {
-				if (s==0) {
-					part[s]->setTokenLayer(0, token, 0);
-				} else {
-					HTp token2 = new HumdrumToken(*token);
-					part[s]->setTokenLayer(0, token2, 0);
-				}
-			}
-		} else {
-			part[staffnum]->setTokenLayer(0, token, 0);
+vector<int> Tool_myank::analyzeBarNumbers(HumdrumFile& infile) {
+	vector<int> m_barnum;
+	m_barnum.resize(infile.getLineCount());
+	int current = 0;
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isBarline()) {
+			m_barnum.at(i) = current;
+			continue;
 		}
+		if (hre.search(infile[i].token(0), "=(\\d+)")) {
+			current = hre.getMatchInt(1);
+		}
+		m_barnum.at(i) = current;
 	}
+	return m_barnum;
 }
 
 
 
-//////////////////////////////
+////////////////////////
 //
-// Tool_musicxml2hum::insertPartTimeSigs -- Only allowing one
-//		time signature per part for now.
+// Tool_myank::getBarNumberForLineNumber --
 //
 
-bool Tool_musicxml2hum::insertPartTimeSigs(xml_node timesig, GridPart& part) {
-	if (!timesig) {
-		// no timesig
-		return false;
-	}
-
-	bool hasmensuration = false;
-	HTp token;
-	int staffnum = 0;
-
-	while (timesig) {
-		hasmensuration |= checkForMensuration(timesig);
-		timesig = convertTimeSigToHumdrum(timesig, token, staffnum);
-		if (token && (staffnum < 0)) {
-			// time signature applies to all staves in part (most common case)
-			for (int s=0; s<(int)part.size(); s++) {
-				if (s==0) {
-					part[s]->setTokenLayer(0, token, 0);
-				} else {
-					HTp token2 = new HumdrumToken(*token);
-					part[s]->setTokenLayer(0, token2, 0);
-				}
-			}
-		} else if (token) {
-			part[staffnum]->setTokenLayer(0, token, 0);
-		}
-	}
-
-	return hasmensuration;
+int Tool_myank::getBarNumberForLineNumber(int lineNumber) {
+	return m_barNumbersPerLine[lineNumber-1];
 }
 
 
 
-//////////////////////////////
+////////////////////////
 //
-// Tool_musicxml2hum::insertPartMensurations --
+// Tool_myank::getStartLineNumber -- Get start line number from --lines
 //
 
-void Tool_musicxml2hum::insertPartMensurations(xml_node timesig,
-		GridPart& part) {
-	if (!timesig) {
-		// no timesig
-		return;
+int Tool_myank::getStartLineNumber(void) {
+	HumRegex hre;
+	if (hre.search(m_lineRange, "^(\\d+)\\-(\\d+)$")) {
+		return hre.getMatchInt(1);
 	}
+	return -1;
+}
 
-	HTp token = NULL;
-	int staffnum = 0;
 
-	while (timesig) {
-		timesig = convertMensurationToHumdrum(timesig, token, staffnum);
-		if (staffnum < 0) {
-			// time signature applies to all staves in part (most common case)
-			for (int s=0; s<(int)part.size(); s++) {
-				if (s==0) {
-					part[s]->setTokenLayer(0, token, 0);
-				} else {
-					HTp token2 = new HumdrumToken(*token);
-					part[s]->setTokenLayer(0, token2, 0);
-				}
-			}
-		} else {
-			part[staffnum]->setTokenLayer(0, token, 0);
-		}
-	}
 
+////////////////////////
+//
+// Tool_myank::getEndLineNumber -- Get end line number from --lines
+//
+
+int Tool_myank::getEndLineNumber(void) {
+	HumRegex hre;
+	if (hre.search(m_lineRange, "^(\\d+)\\-(\\d+)$")) {
+		return hre.getMatchInt(2);
+	}
+	return -1;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_musicxml::checkForMensuration --
-//    Examples:
-//        <time symbol="common">
-//        <time symbol="cut">
+// Tool_myank::expandMultipliers -- 2*5 => 2,2,2,2,2
+//    Limit of 100 times expansion
 //
 
-bool Tool_musicxml2hum::checkForMensuration(xml_node timesig) {
-	if (!timesig) {
-		return false;
+string Tool_myank::expandMultipliers(const string& inputstring) {
+	HumRegex hre;
+	if (!hre.search(inputstring, "\\*")) {
+		return inputstring;
 	}
-
-	xml_attribute mens = timesig.attribute("symbol");
-	if (mens) {
-		return true;
-	} else {
-		return false;
+	string outputstring = inputstring;
+	while (hre.search(outputstring, "(\\d+)\\*([1-9]+[0-9]*)")) {
+		string measurenum = hre.getMatch(1);
+		int multiplier = hre.getMatchInt(2);
+		if (multiplier > 100) {
+			cerr << "Reducing multiplier from " << multiplier << " to 100" << endl;
+			multiplier = 100;
+		}
+		string expansion = measurenum;
+		for (int i=1; i<multiplier; i++) {
+			expansion += ",";
+			expansion += measurenum;
+		}
+		hre.replaceDestructive(outputstring, expansion, "(\\d+)\\*([1-9]+[0-9]*)");
 	}
+	return outputstring;
 }
 
 
 //////////////////////////////
 //
-//	Tool_musicxml2hum::convertTranspositionToHumdrum --
-//
-//  <transpose>
-//     <diatonic>-1</diatonic>
-//     <chromatic>-2</chromatic>
+// Tool_myank::getMetStates --  Store the current *met for every token
+// in the score, keeping track of meter without metric symbols.
 //
 
-xml_node Tool_musicxml2hum::convertTranspositionToHumdrum(xml_node transpose,
-		HTp& token, int& staffindex) {
+void Tool_myank::getMetStates(vector<vector<MyCoord> >& metstates,
+		HumdrumFile& infile) {
+	vector<MyCoord> current;
+	current.resize(infile.getMaxTrack()+1);
+	metstates.resize(infile.getLineCount());
+	HumRegex hre;
 
-	if (!transpose) {
-		return transpose;
-	}
+	int track;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isInterpretation()) {
+			for (int j=0; j<infile[i].getFieldCount(); j++) {
+				track = infile.token(i, j)->getTrack();
+				if (hre.search(infile.token(i, j), R"(^\*met\([^\)]+\))")) {
+					current[track].x = i;
+					current[track].y = j;
+				} else if (hre.search(infile.token(i, j), R"(^\*M\d+\d+)")) {
+					current[track] = getLocalMetInfo(infile, i, track);
+				}
+			}
+		}
 
-	staffindex = -1;
-	xml_attribute sn = transpose.attribute("number");
-	if (sn) {
-		staffindex = atoi(sn.value()) - 1;
+		// metstates[i].resize(infile[i].getFieldCount());
+		// for (j=0; j<infile[i].getFieldCount(); j++) {
+		//    track = infile.token(i, j)->getTrack();
+		//    metstates[i][j] = current[track];
+		// }
+		metstates[i].resize(infile.getMaxTrack()+1);
+		for (int j=1; j<=infile.getMaxTrack(); j++) {
+			metstates[i][j] = current[j];
+		}
 	}
 
-	int diatonic = 0;
-	int chromatic = 0;
-
-	xml_node child = transpose.first_child();
-	while (child) {
-		if (nodeType(child, "diatonic")) {
-			diatonic = atoi(child.child_value());
-		} else if (nodeType(child, "chromatic")) {
-			chromatic = atoi(child.child_value());
+	if (m_debugQ) {
+		for (int i=0; i<infile.getLineCount(); i++) {
+			for (int j=1; j<(int)metstates[i].size(); j++) {
+				if (metstates[i][j].x < 0) {
+					m_humdrum_text << ".";
+				} else {
+					m_humdrum_text << infile.token(metstates[i][j].x, metstates[i][j].y);
+				}
+				m_humdrum_text << "\t";
+			}
+			m_humdrum_text << infile[i] << endl;
 		}
-		child = child.next_sibling();
+
 	}
+}
 
 
-	// Switching to sounding viewpoint: transposition to get written pitch:
-	diatonic = -diatonic;
-	chromatic = -chromatic;
 
-	stringstream ss;
-	ss << "*Trd" << diatonic << "c" << chromatic;
+//////////////////////////////
+//
+// Tool_myank::getLocalMetInfo -- search in the non-data region indicated by the
+// input row for a *met entry in the input track.  Return empty
+// value if none found.
+//
 
-	token = new HumdrumToken(ss.str());
+MyCoord Tool_myank::getLocalMetInfo(HumdrumFile& infile, int row, int track) {
+	MyCoord output;
+	int startline = -1;
+	int stopline = -1;
+	int i = row;
+	int j;
+	int xtrac;
+	HumRegex hre;
 
-	int base40 = -Convert::transToBase40(ss.str());
-	if (base40 != 0) {
-		m_hasTransposition = true;
+	while (i>=0) {
+		if (infile[i].isData()) {
+			startline = i+1;
+			break;
+		}
+		i--;
 	}
-
-	transpose = transpose.next_sibling();
-	if (!transpose) {
-		return transpose;
+	if (startline < 0) {
+		startline = 0;
 	}
-	if (nodeType(transpose, "transpose")) {
-		return transpose;
-	} else {
-		return xml_node(NULL);
+	i = row;
+	while (i<infile.getLineCount()){
+		if (infile[i].isData()) {
+			stopline = i-1;
+			break;
+		}
+		i++;
+	}
+	if (stopline >= infile.getLineCount()) {
+		stopline = infile.getLineCount()-1;
+	}
+	for (i=startline; i<=stopline; i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			xtrac = infile.token(i, j)->getTrack();
+			if (track != xtrac) {
+				continue;
+			}
+			if (hre.search(infile.token(i, j), R"(^\*met\([^\)]+\))")) {
+				output.x = i;
+				output.x = j;
+			}
+		}
 	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-//	Tool_musicxml2hum::convertKeySigToHumdrumKeyDesignation --
-//
-//  <key>
-//     <fifths>4</fifths>
-// and sometimes:
-//     <mode>major</mode>
+// Tool_myank::getMarkString -- return a list of measures which contain marked
+//    notes (primarily from search matches).
+// This function scans for reference records in this form:
+// !!!RDF**kern: @= matched note
 // or
-//     <mode>minor</mode>
+// !!!RDF**kern: i= marked note
+// If it finds any lines like that, it will extract the character before
+// the equals sign, and scan for it in the **kern data in the file.
+// any measure which contains such a mark will be stored in the output
+// string.
 //
 
-xml_node Tool_musicxml2hum::convertKeySigToHumdrumKeyDesignation(xml_node keysig,
-		HTp& token, int& staffindex) {
-
-	if (!keysig) {
-		token = new HumdrumToken("*");
-		return keysig;
+void Tool_myank::getMarkString(ostream& out, HumdrumFile& infile)  {
+	string mchar; // list of characters which are marks
+	char target;
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReference()) {
+			continue;
+		}
+		if (hre.search(infile.token(i, 0),
+				R"(!!!RDF\*\*kern\s*:\s*([^=])\s*=\s*match)", "i")) {
+			target = hre.getMatch(1)[0];
+			mchar.push_back(target);
+		} else if (hre.search(infile.token(i, 0),
+				R"(!!!RDF\*\*kern\s*:\s*([^=])\s*=\s*mark)", "i")) {
+			target = hre.getMatch(1)[0];
+			mchar.push_back(target);
+		}
 	}
 
-	staffindex = -1;
-	xml_attribute sn = keysig.attribute("number");
-	if (sn) {
-		staffindex = atoi(sn.value()) - 1;
+	if (m_debugQ) {
+		for (int i=0; i<(int)mchar.size(); i++) {
+			m_free_text << "\tMARK CHARCTER: " << mchar[i] << endl;
+		}
 	}
 
-	int fifths = 0;
-	int mode = -1;
+	if (mchar.size() == 0) {
+		return;
+	}
 
-	xml_node child = keysig.first_child();
-	while (child) {
-		if (nodeType(child, "fifths")) {
-			fifths = atoi(child.child_value());
+	// now search for measures which contains any of those character
+	// in **kern data:
+	int curmeasure = 0;
+	int inserted = 0;
+	int hasmark = 0;
+	string str;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isBarline()) {
+			if (hre.search(infile.token(i, 0), "^=.*?(\\d+)", "")) {
+				curmeasure = stoi(hre.getMatch(1));
+				hasmark = 0;
+			}
 		}
-		if (nodeType(child, "mode")) {
-			string value = child.child_value();
-			if (value == "major") {
-				mode = 0;
-			} else if (value == "minor") {
-				mode = 1;
+		if (hasmark) {
+			continue;
+		}
+		if (!infile[i].isData()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			if (infile.token(i, j)->isKern()) {
+				int k=0;
+				str = *infile.token(i, j);
+				while (str[k] != '\0') {
+					for (int m=0; m<(int)mchar.size(); m++) {
+						if (str[k] == mchar[m]) {
+							if (inserted) {
+								out << ',';
+							} else {
+								inserted++;
+							}
+							out << curmeasure;
+							hasmark = 1;
+							goto outerforloop;
+						}
+					}
+					k++;
+				}
 			}
 		}
-		child = child.next_sibling();
+outerforloop: ;
 	}
+}
 
-	if (mode < 0) {
-		token = new HumdrumToken("*");
-		return xml_node(NULL);
-	}
 
-	stringstream ss;
-	ss << "*";
 
-	if (mode == 0) { // major:
-		switch (fifths) {
-			case +7: ss << "C#"; break;
-			case +6: ss << "F#"; break;
-			case +5: ss << "B"; break;
-			case +4: ss << "E"; break;
-			case +3: ss << "A"; break;
-			case +2: ss << "D"; break;
-			case +1: ss << "G"; break;
-			case  0: ss << "C"; break;
-			case -1: ss << "F"; break;
-			case -2: ss << "B-"; break;
-			case -3: ss << "E-"; break;
-			case -4: ss << "A-"; break;
-			case -5: ss << "D-"; break;
-			case -6: ss << "G-"; break;
-			case -7: ss << "C-"; break;
-			default:
-				token = new HumdrumToken("*");
-				return xml_node(NULL);
-		}
-	} else if (mode == 1) { // minor:
-		switch (fifths) {
-			case +7: ss << "a#"; break;
-			case +6: ss << "d#"; break;
-			case +5: ss << "g#"; break;
-			case +4: ss << "c#"; break;
-			case +3: ss << "f#"; break;
-			case +2: ss << "b"; break;
-			case +1: ss << "e"; break;
-			case  0: ss << "a"; break;
-			case -1: ss << "d"; break;
-			case -2: ss << "g"; break;
-			case -3: ss << "c"; break;
-			case -4: ss << "f"; break;
-			case -5: ss << "b-"; break;
-			case -6: ss << "e-"; break;
-			case -7: ss << "a-"; break;
-			default:
-				token = new HumdrumToken("*");
-				return xml_node(NULL);
-		}
-	}
-	ss << ":";
+//////////////////////////////
+//
+// Tool_myank::myank -- yank the specified measures.
+//
 
-	token = new HumdrumToken(ss.str());
+void Tool_myank::myank(HumdrumFile& infile, vector<MeasureInfo>& outmeasures) {
 
-	keysig = keysig.next_sibling();
-	if (!keysig) {
-		return keysig;
+	if (outmeasures.size() > 0) {
+		printStarting(infile);
 	}
-	if (nodeType(keysig, "key")) {
-		return keysig;
-	} else {
-		return xml_node(NULL);
+
+	int lastline = -1;
+	int lastDataLine = -1;
+	int h, i, j;
+	int counter;
+	int printed = 0;
+	int mcount = 0;
+	int measurestart = 1;
+	int lastbarnum = -1;
+	int barnum = -1;
+	int datastart = 0;
+	int bartextcount = 0;
+	bool startLineHandled = false;
+
+	int lastLineIndex = getBoolean("lines") ? getEndLineNumber() - 1 : outmeasures[outmeasures.size() - 1].stop;
+
+	// Find the actual last line of the selected section that is a line with
+	// data tokens
+	while (infile.getLine(lastLineIndex)->isData() == false) {
+		lastLineIndex--;
 	}
-}
 
+	// Mapping with with the start token for each spine
+	vector<int> lastLineResolvedTokenLineIndex;
+	// Mapping with the later needed durations of the note that fits within the
+	// selected section
+	vector<HumNum> lastLineDurationsFromNoteStart;
 
+	lastLineResolvedTokenLineIndex.resize(infile.getLine(lastLineIndex)->getTokenCount());
+	lastLineDurationsFromNoteStart.resize(infile.getLine(lastLineIndex)->getTokenCount());
 
-//////////////////////////////
-//
-//	Tool_musicxml2hum::convertKeySigToHumdrum --
-//
-//  <key>
-//     <fifths>4</fifths>
-// and sometimes:
-//     <mode>major</mode>
-// or
-//     <mode>minor</mode>
-//
+	for (int a = 0; a < infile.getLine(lastLineIndex)->getTokenCount(); a++) {
+		HTp token = infile.token(lastLineIndex, a);
+		// Get lineIndex for last data token with an attack
+		lastLineResolvedTokenLineIndex[a] = infile.token(lastLineIndex, a)->resolveNull()->getLineIndex();
+		// Get needed duration for this token until section end
+		lastLineDurationsFromNoteStart[a] = token->getDurationFromNoteStart() + token->getLine()->getDuration();
+	}
 
-xml_node Tool_musicxml2hum::convertKeySigToHumdrum(xml_node keysig,
-		HTp& token, int& staffindex) {
+	int startLineNumber = getStartLineNumber();
+	int endLineNumber = getEndLineNumber();
 
-	if (!keysig) {
-		return keysig;
+	if (getBoolean("lines")) {
+		int firstDataLineIndex = -1;
+		for (int b = startLineNumber - 1; b <= endLineNumber - 1; b++) {
+			if (infile.getLine(b)->isData()) {
+				firstDataLineIndex = b;
+				break;
+			}
+		}
+		if (firstDataLineIndex >= 0) {
+			if (infile.getLine(firstDataLineIndex)->getDurationFromBarline() == 0) {
+				for (int c = startLineNumber - 1; c >=  0; c--) {
+					if (infile.getLine(c)->isBarline()) {
+						startLineNumber = c + 1;
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	for (h=0; h<(int)outmeasures.size(); h++) {
+		barnum = outmeasures[h].num;
+		measurestart = 1;
+		printed = 0;
+		counter = 0;
+		if (m_debugQ) {
+			m_humdrum_text << "!! =====================================\n";
+			m_humdrum_text << "!! processing " << outmeasures[h].num << endl;
+		}
+		if (h > 0) {
+			reconcileSpineBoundary(infile, outmeasures[h-1].stop,
+				outmeasures[h].start);
+		} else {
+			reconcileStartingPosition(infile, outmeasures[0].start);
+		}
+		int startLine = getBoolean("lines") ? std::max(startLineNumber-1, outmeasures[h].start)
+			: outmeasures[h].start;
+		int endLine = getBoolean("lines") ? std::min(endLineNumber, outmeasures[h].stop)
+			: outmeasures[h].stop;
+		for (i=startLine; i<endLine; i++) {
+			counter++;
+			if ((!printed) && ((mcount == 0) || (counter == 2))) {
+				if ((datastart == 0) && outmeasures[h].num == 0) {
+					// not ideal setup...
+					datastart = 1;
+				} else{
+					// Fix adjustGlobalInterpretations when line is a global comment
+					int nextLineIndexWithSpines = i;
+					if (infile.getLine(i)->isCommentGlobal()) {
+						for (int d = i; d <= endLineNumber - 1; d++) {
+							if (!infile.getLine(d)->isCommentGlobal()) {
+								nextLineIndexWithSpines = d;
+								break;
+							}
+						}
+					}
+					adjustGlobalInterpretations(infile, nextLineIndexWithSpines, outmeasures, h);
+					printed = 1;
+				}
+			}
+			if (infile[i].isData() && (mcount == 0)) {
+				mcount++;
+			}
+			if (infile[i].isBarline()) {
+				mcount++;
+			}
+			if ((mcount == 1) && m_invisibleQ && infile[i].isBarline()) {
+				printInvisibleMeasure(infile, i);
+				measurestart = 0;
+				if ((bartextcount++ == 0) && infile[i].isBarline()) {
+					int barline = 0;
+					sscanf(infile.token(i, 0)->c_str(), "=%d", &barline);
+					if (m_barnumtextQ && (barline > 0)) {
+						m_humdrum_text << "!!LO:TX:Z=20:X=-90:t=" << barline << endl;
+					}
+				}
+			} else if (m_doubleQ && (lastbarnum > -1) && (abs(barnum - lastbarnum) > 1)) {
+				printDoubleBarline(infile, i);
+				measurestart = 0;
+			} else if (measurestart && infile[i].isBarline()) {
+				printMeasureStart(infile, i, outmeasures[h].startStyle);
+				measurestart = 0;
+			} else {
+				printDataLine(infile.getLine(i), startLineHandled, lastLineResolvedTokenLineIndex, lastLineDurationsFromNoteStart);
+				if (m_barnumtextQ && (bartextcount++ == 0) && infile[i].isBarline()) {
+					int barline = 0;
+					sscanf(infile.token(i, 0)->c_str(), "=%d", &barline);
+					if (barline > 0) {
+						m_humdrum_text << "!!LO:TX:Z=20:X=-25:t=" << barline << endl;
+					}
+				}
+			}
+			lastline = i;
+			if (infile.getLine(i)->isData()) {
+				lastDataLine = i;
+			}
+		}
+		lastbarnum = barnum;
 	}
 
-	staffindex = -1;
-	xml_attribute sn = keysig.attribute("number");
-	if (sn) {
-		staffindex = atoi(sn.value()) - 1;
+	if (getBoolean("lines") && (lastDataLine >= 0) &&
+			(infile.getLine(lastDataLine)->getDurationToBarline() > infile.getLine(lastDataLine)->getDuration())) {
+		m_nolastbarQ = true;
 	}
 
-	int fifths = 0;
-	//int mode = -1;
-
-	xml_node child = keysig.first_child();
-	while (child) {
-		if (nodeType(child, "fifths")) {
-			fifths = atoi(child.child_value());
-		}
-		if (nodeType(child, "mode")) {
-			string value = child.child_value();
-			if (value == "major") {
-				// mode = 0;
-			} else if (value == "minor") {
-				// mode = 1;
+	HumRegex hre;
+	string token;
+	int lasti;
+	if (outmeasures.size() > 0) {
+		lasti = outmeasures.back().stop;
+	} else {
+		lasti = -1;
+	}
+	if ((!m_nolastbarQ) &&  (lasti >= 0) && infile[lasti].isBarline()) {
+		for (j=0; j<infile[lasti].getFieldCount(); j++) {
+			token = *infile.token(lasti, j);
+			hre.replaceDestructive(token, outmeasures.back().stopStyle, "\\d+.*");
+			// collapse final barlines
+			hre.replaceDestructive(token, "==", "===+");
+			if (m_doubleQ) {
+				if (hre.search(token, "=(.+)")) {
+					// don't add double barline, there is already
+					// some style on the barline
+				} else {
+					// add a double barline
+					hre.replaceDestructive(token, "||", "$");
+				}
+			}
+			m_humdrum_text << token;
+			if (j < infile[lasti].getFieldCount() - 1) {
+				m_humdrum_text << '\t';
 			}
 		}
-		child = child.next_sibling();
-	}
-
-	stringstream ss;
-	ss << "*k[";
-	if (fifths > 0) {
-		if (fifths > 0) { ss << "f#"; }
-		if (fifths > 1) { ss << "c#"; }
-		if (fifths > 2) { ss << "g#"; }
-		if (fifths > 3) { ss << "d#"; }
-		if (fifths > 4) { ss << "a#"; }
-		if (fifths > 5) { ss << "e#"; }
-		if (fifths > 6) { ss << "b#"; }
-	} else if (fifths < 0) {
-		if (fifths < 0)  { ss << "b-"; }
-		if (fifths < -1) { ss << "e-"; }
-		if (fifths < -2) { ss << "a-"; }
-		if (fifths < -3) { ss << "d-"; }
-		if (fifths < -4) { ss << "g-"; }
-		if (fifths < -5) { ss << "c-"; }
-		if (fifths < -6) { ss << "f-"; }
+		m_humdrum_text << '\n';
 	}
-	ss << "]";
 
-	token = new HumdrumToken(ss.str());
+	collapseSpines(infile, lasti);
 
-	keysig = keysig.next_sibling();
-	if (!keysig) {
-		return keysig;
+	if (m_debugQ) {
+		m_free_text << "PROCESSING ENDING" << endl;
 	}
-	if (nodeType(keysig, "key")) {
-		return keysig;
-	} else {
-		return xml_node(NULL);
+
+	if (lastline >= 0) {
+		printEnding(infile, outmeasures.back().stop, lasti);
 	}
 }
 
@@ -108418,64 +112473,51 @@ xml_node Tool_musicxml2hum::convertKeySigToHumdrum(xml_node keysig,
 
 //////////////////////////////
 //
-//	Tool_musicxml2hum::convertTimeSigToHumdrum --
-//
-//  <time symbol="common">
-//     <beats>4</beats>
-//     <beat-type>4</beat-type>
-//
-// also:
-//  <time symbol="common">
+// Tool_myank::collapseSpines -- Shrink all sub-spines to single spine.
 //
 
-xml_node Tool_musicxml2hum::convertTimeSigToHumdrum(xml_node timesig,
-		HTp& token, int& staffindex) {
-
-	token = NULL;
-
-	if (!timesig) {
-		return xml_node(NULL);
+void Tool_myank::collapseSpines(HumdrumFile& infile, int line) {
+	if (line < 0) {
+		return;
 	}
-
-	staffindex = -1;
-	xml_attribute sn = timesig.attribute("number");
-	if (sn) {
-		staffindex = atoi(sn.value()) - 1;
+	vector<int> counts(infile.getMaxTrack() + 1, 0);
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		int track = infile.token(line, i)->getTrack();
+		counts.at(track)++;
 	}
-
-	int beats = -1;
-	int beattype = -1;
-
-	xml_node child = timesig.first_child();
-	while (child) {
-		if (nodeType(child, "beats")) {
-			beats = atoi(child.child_value());
-		} else if (nodeType(child, "beat-type")) {
-			beattype = atoi(child.child_value());
+	for (int i=1; i<(int)counts.size(); i++) {
+		if (counts[i] <= 1) {
+			continue;
 		}
-		child = child.next_sibling();
-	}
-
-	if ((beats == -1) && (beattype == -1)) {
-		// No time signature, such as:
-		// <time print-object="no">
-		//   <senza-misura/>
-		// </time>
-		return xml_node(NULL);
-	}
-
-	stringstream ss;
-	ss << "*M" << beats<< "/" << beattype;
-	token = new HumdrumToken(ss.str());
-
-	timesig = timesig.next_sibling();
-	if (!timesig) {
-		return timesig;
-	}
-	if (nodeType(timesig, "time")) {
-		return timesig;
-	} else {
-		return xml_node(NULL);
+		bool started = false;
+		for (int j=1; j<(int)counts.size(); j++) {
+			if (j < i) {
+				if (started) {
+					m_humdrum_text << "\t";
+				}
+				m_humdrum_text << "*";
+				started = true;
+				continue;
+			} else if (j == i) {
+				for (int k=0; k<counts[j]; k++) {
+					if (started) {
+						m_humdrum_text << "\t";
+					}
+					m_humdrum_text << "*v";
+					started = true;
+				}
+			} else if (j > i) {
+				for (int k=0; k<counts[j]; k++) {
+					if (started) {
+						m_humdrum_text << "\t";
+					}
+					m_humdrum_text << "*";
+					started = true;
+				}
+			}
+		}
+		m_humdrum_text << "\n";
+		counts[i] = 1;
 	}
 }
 
@@ -108483,404 +112525,448 @@ xml_node Tool_musicxml2hum::convertTimeSigToHumdrum(xml_node timesig,
 
 //////////////////////////////
 //
-//	Tool_musicxml2hum::convertMensurationToHumdrum --
-//
-//  <time symbol="common">
-//     <beats>4</beats>
-//     <beat-type>4</beat-type>
-//
-// also:
-//  <time symbol="common">
+// Tool_myank::adjustGlobalInterpretations --
 //
 
-xml_node Tool_musicxml2hum::convertMensurationToHumdrum(xml_node timesig,
-		HTp& token, int& staffindex) {
-
-	if (!timesig) {
-		return timesig;
-	}
-
-	staffindex = -1;
-	xml_attribute mens = timesig.attribute("symbol");
-	if (!mens) {
-		token = new HumdrumToken("*");
-	} else {
-		string text = mens.value();
-		if (text == "cut") {
-			token = new HumdrumToken("*met(c|)");
-		} else if (text == "common") {
-			token = new HumdrumToken("*met(c)");
-		} else {
-			token = new HumdrumToken("*");
-		}
-	}
+void Tool_myank::adjustGlobalInterpretations(HumdrumFile& infile, int ii,
+		vector<MeasureInfo>& outmeasures, int index) {
 
-	timesig = timesig.next_sibling();
-	if (!timesig) {
-		return timesig;
-	}
-	if (nodeType(timesig, "time")) {
-		return timesig;
-	} else {
-		return xml_node(NULL);
+	if (index <= 0) {
+		adjustGlobalInterpretationsStart(infile, ii, outmeasures, index);
+		return;
 	}
-}
 
+	// the following lines will not work when non-contiguous measures are
+	// elided.
+	//   if (!infile[ii].isInterpretation()) {
+	//      return;
+	//   }
 
+	int clefQ    = 0;
+	int keysigQ  = 0;
+	int keyQ     = 0;
+	int timesigQ = 0;
+	int metQ     = 0;
+	int tempoQ   = 0;
 
-//////////////////////////////
-//
-//	Tool_musicxml2hum::convertClefToHumdrum --
-//
+	int x, y;
+	int xo, yo;
 
-xml_node Tool_musicxml2hum::convertClefToHumdrum(xml_node clef,
-		HTp& token, int& staffindex) {
+	int tracks = infile.getMaxTrack();
 
-	if (!clef) {
-		// no clef for some reason.
-		return clef;
-	}
+	// these lines may cause bugs, but they get rid of zeroth measure
+	// problem.
+// ggg
+//   if ((outmeasures.size() > 1) && (outmeasures[index-1].num == 0)) {
+//      return;
+//   }
+//   if ((outmeasures.size() > 0) && (outmeasures[index].num == 0)) {
+//      return;
+//   }
 
-	staffindex = 0;
-	xml_attribute sn = clef.attribute("number");
-	if (sn) {
-		staffindex = atoi(sn.value()) - 1;
-	}
+	for (int i=1; i<=tracks; i++) {
+		if (!clefQ && (outmeasures[index].sclef.size() > 0)) {
+			x  = outmeasures[index].sclef[i].x;
+			y  = outmeasures[index].sclef[i].y;
+			xo = outmeasures[index-1].eclef[i].x;
+			yo = outmeasures[index-1].eclef[i].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					clefQ = 1;
+				}
+			}
+		}
 
-	string sign;
-	int line = -1000;
-	int octadjust = 0;
+		if (!keysigQ && (outmeasures[index].skeysig.size() > 0)) {
+			x  = outmeasures[index].skeysig[i].x;
+			y  = outmeasures[index].skeysig[i].y;
+			xo = outmeasures[index-1].ekeysig[i].x;
+			yo = outmeasures[index-1].ekeysig[i].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					keysigQ = 1;
+				}
+			}
+		}
 
-	xml_node child = clef.first_child();
-	while (child) {
-		if (nodeType(child, "sign")) {
-			sign = child.child_value();
-		} else if (nodeType(child, "line")) {
-			line = atoi(child.child_value());
-		} else if (nodeType(child, "clef-octave-change")) {
-			octadjust = atoi(child.child_value());
+		if (!keyQ && (outmeasures[index].skey.size() > 0)) {
+			x  = outmeasures[index].skey[i].x;
+			y  = outmeasures[index].skey[i].y;
+			xo = outmeasures[index-1].ekey[i].x;
+			yo = outmeasures[index-1].ekey[i].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					keyQ = 1;
+				}
+			}
 		}
-		child = child.next_sibling();
-	}
 
-	// Check for percussion clefs, etc., here.
-	if (sign == "percussion") {
-		sign = "X";
-		// ignore line of percussion clef (assume it is centered on staff).
-		line = -1000;
-	}
-	stringstream ss;
-	ss << "*clef" << sign;
-	if (octadjust < 0) {
-		for (int i=0; i < -octadjust; i++) {
-			ss << "v";
+		if (!timesigQ && (outmeasures[index].stimesig.size() > 0)) {
+			x  = outmeasures[index].stimesig[i].x;
+			y  = outmeasures[index].stimesig[i].y;
+			xo = outmeasures[index-1].etimesig[i].x;
+			yo = outmeasures[index-1].etimesig[i].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					timesigQ = 1;
+				}
+			}
 		}
-	} else if (octadjust > 0) {
-		for (int i=0; i<octadjust; i++) {
-			ss << "^";
+
+		if (!metQ && (outmeasures[index].smet.size() > 0)) {
+			x  = outmeasures[index].smet[i].x;
+			y  = outmeasures[index].smet[i].y;
+			xo = outmeasures[index-1].emet[i].x;
+			yo = outmeasures[index-1].emet[i].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					metQ = 1;
+				}
+			}
 		}
-	}
-	if (line > 0) {
-		ss << line;
-	}
-	token = new HumdrumToken(ss.str());
 
-	clef = clef.next_sibling();
-	if (!clef) {
-		return clef;
-	}
-	if (nodeType(clef, "clef")) {
-		return clef;
-	} else {
-		return xml_node(NULL);
+		if (!tempoQ && (outmeasures[index].stempo.size() > 0)) {
+			x  = outmeasures[index].stempo[i].x;
+			y  = outmeasures[index].stempo[i].y;
+			xo = outmeasures[index-1].etempo[i].x;
+			yo = outmeasures[index-1].etempo[i].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					tempoQ = 1;
+				}
+			}
+		}
 	}
-}
-
-
 
-//////////////////////////////
-//
-//	Tool_musicxml2hum::convertOttavaToHumdrum --
-//    Example:
-//      <direction placement="above">
-//        <direction-type>
-//          <octave-shift type="down" size="8" number="1"/>
-//        </direction-type>
-//      </direction>
-//      ...
-//      <direction placement="above">
-//        <direction-type>
-//          <octave-shift type="stop" size="8" number="1"/>
-//        </direction-type>
-//      </direction>
-//
-//
-
-xml_node Tool_musicxml2hum::convertOttavaToHumdrum(xml_node ottava,
-		HTp& token, int& staffindex, int partindex, int partstaffindex, int staffcount) {
-
-	// partstaffindex is useless or incorrect? At least for grand staff parts.
-	// The staffindex calculated below is the one to used.
+	int track;
 
-	if (!ottava) {
-		// no clef for some reason.
-		return ottava;
+	if (clefQ) {
+		for (int i=0; i<infile[ii].getFieldCount(); i++) {
+			track = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].sclef[track].x;
+			y  = outmeasures[index].sclef[track].y;
+			xo = outmeasures[index-1].eclef[track].x;
+			yo = outmeasures[index-1].eclef[track].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					m_humdrum_text << infile.token(x, y);
+				} else {
+					m_humdrum_text << "*";
+				}
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+		}
+		m_humdrum_text << "\n";
 	}
 
-
-	// Don't use "number" to set the staff index, now use the input parameter
-	// which comes from the direction/staff element that is an uncle of the ottava node.
-	//staffindex = 0;
-	//xml_attribute sn = ottava.attribute("number");
-	//if (sn) {
-	//	staffindex = atoi(sn.value()) - 1;
-	//}
-	//staffindex = staffcount - staffindex - 1;
-
-	int interval = 0;
-
-	interval = ottava.attribute("size").as_int();
-	string otype = ottava.attribute("type").as_string();
-	string lastotype = m_last_ottava_direction.at(partindex).at(staffindex);
-
-	string ss;
-	ss = "*";
-	if (otype == "stop") {
-		ss += "X";
-	} else {
-	   m_last_ottava_direction.at(partindex).at(staffindex) = otype;
-   }
-	if (interval == 15) {
-		ss += "15";
-		if (otype == "down") {
-			ss += "ma";
-		} else if (otype == "up") {
-			ss += "ba";
-		} else if (otype == "stop") {
-			if (m_last_ottava_direction.at(partindex).at(staffindex) == "up") {
-				ss += "ba";
-			} else if (m_last_ottava_direction.at(partindex).at(staffindex) == "down") {
-				ss += "ma";
+	if (keysigQ) {
+		for (int i=0; i<infile[ii].getFieldCount(); i++) {
+			track = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].skeysig[track].x;
+			y  = outmeasures[index].skeysig[track].y;
+			xo = outmeasures[index-1].ekeysig[track].x;
+			yo = outmeasures[index-1].ekeysig[track].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					m_humdrum_text << infile.token(x, y);
+				} else {
+					m_humdrum_text << "*";
+				}
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
 			}
 		}
-	} else if (interval == 8) {
-		ss += "8";
-		if (otype == "down") {
-			ss += "va";
-		} else if (otype == "up") {
-			ss += "ba";
-		} else if (otype == "stop") {
-			if (m_last_ottava_direction.at(partindex).at(staffindex) == "up") {
-				ss += "ba";
-			} else if (m_last_ottava_direction.at(partindex).at(staffindex) == "down") {
-				ss += "va";
+		m_humdrum_text << "\n";
+	}
+
+	if (keyQ) {
+		for (int i=0; i<infile[ii].getFieldCount(); i++) {
+			track = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].skey[track].x;
+			y  = outmeasures[index].skey[track].y;
+			xo = outmeasures[index-1].ekey[track].x;
+			yo = outmeasures[index-1].ekey[track].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					m_humdrum_text << infile.token(x, y);
+				} else {
+					m_humdrum_text << "*";
+				}
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
 			}
 		}
-	} else {
-		ss += "*8";
-		if (otype == "down") {
-			ss += "va";
-		} else if (otype == "up") {
-			ss += "ba";
-		} else if (otype == "stop") {
-			if (m_last_ottava_direction.at(partindex).at(staffindex) == "up") {
-				ss += "ba";
-			} else if (m_last_ottava_direction.at(partindex).at(staffindex) == "down") {
-				ss += "va";
+		m_humdrum_text << "\n";
+	}
+
+	if (timesigQ) {
+		for (int i=0; i<infile[ii].getFieldCount(); i++) {
+			track = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].stimesig[track].x;
+			y  = outmeasures[index].stimesig[track].y;
+			xo = outmeasures[index-1].etimesig[track].x;
+			yo = outmeasures[index-1].etimesig[track].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					m_humdrum_text << infile.token(x, y);
+				} else {
+					m_humdrum_text << "*";
+				}
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
 			}
 		}
+		m_humdrum_text << "\n";
 	}
-	token = new HumdrumToken(ss);
 
-	ottava = ottava.next_sibling();
-	if (!ottava) {
-		return ottava;
+	if (metQ) {
+		for (int i=0; i<infile[ii].getFieldCount(); i++) {
+			track = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].smet[track].x;
+			y  = outmeasures[index].smet[track].y;
+			xo = outmeasures[index-1].emet[track].x;
+			yo = outmeasures[index-1].emet[track].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					m_humdrum_text << infile.token(x, y);
+				} else {
+					m_humdrum_text << "*";
+				}
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+		}
+		m_humdrum_text << "\n";
 	}
-	if (nodeType(ottava, "octave-shift")) {
-		return ottava;
-	} else {
-		return xml_node(NULL);
+
+	if (tempoQ) {
+		for (int i=0; i<infile[ii].getFieldCount(); i++) {
+			track = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].stempo[track].x;
+			y  = outmeasures[index].stempo[track].y;
+			xo = outmeasures[index-1].etempo[track].x;
+			yo = outmeasures[index-1].etempo[track].y;
+			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
+				if (*infile.token(x, y) != *infile.token(xo, yo)) {
+					m_humdrum_text << infile.token(x, y);
+				} else {
+					m_humdrum_text << "*";
+				}
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+		}
+		m_humdrum_text << "\n";
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::nodeType -- return true if node type matches
-//     string.
+// Tool_myank::adjustGlobalInterpretationsStart --
 //
 
-bool Tool_musicxml2hum::nodeType(xml_node node, const char* testname) {
-	if (strcmp(node.name(), testname) == 0) {
-		return true;
-	} else {
-		return false;
+void Tool_myank::adjustGlobalInterpretationsStart(HumdrumFile& infile, int ii,
+		vector<MeasureInfo>& outmeasures, int index) {
+	if (index != 0) {
+		cerr << "Error in adjustGlobalInterpetationsStart" << endl;
+		exit(1);
 	}
-}
 
+	int i;
 
+	int clefQ    = 0;
+	int keysigQ  = 0;
+	int keyQ     = 0;
+	int timesigQ = 0;
+	int metQ     = 0;
+	int tempoQ   = 0;
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::appendNullTokens --
-//
+	int x, y;
 
-void Tool_musicxml2hum::appendNullTokens(HLp line,
-		MxmlPart& part) {
-	int i;
-	int staffcount = part.getStaffCount();
-	int versecount = part.getVerseCount();
-	for (i=staffcount-1; i>=0; i--) {
-		line->appendToken(".");
-	}
-	for (i=0; i<versecount; i++) {
-		line->appendToken(".");
+	// ignore the zeroth measure
+	// (may not be proper).
+// ggg
+	if (outmeasures[index].num == 0) {
+		return;
 	}
-}
 
+	int tracks = infile.getMaxTrack();
 
+	for (i=1; i<=tracks; i++) {
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::getPartContent -- Extract the part elements in
-//     the file indexed by part ID.
-//
+		if (!clefQ) {
+			x  = outmeasures[index].sclef[i].x;
+			y  = outmeasures[index].sclef[i].y;
 
-bool Tool_musicxml2hum::getPartContent(
-		map<string, xml_node>& partcontent,
-		vector<string>& partids, xml_document& doc) {
+			if ((x>=0)&&(y>=0)) {
+				clefQ = 1;
+			}
+		}
 
-	auto parts = doc.select_nodes("/score-partwise/part");
-	int count = (int)parts.size();
-	if (count != (int)partids.size()) {
-		cerr << "Warning: part element count does not match part IDs count: "
-		     << parts.size() << " compared to " << partids.size() << endl;
-	}
+		if (!keysigQ) {
+			x  = outmeasures[index].skeysig[i].x;
+			y  = outmeasures[index].skeysig[i].y;
+			if ((x>=0)&&(y>=0)) {
+				keysigQ = 1;
+			}
+		}
 
-	string partid;
-	for (int i=0; i<(int)parts.size(); i++) {
-		partid = getAttributeValue(parts[i], "id");
-		if (partid.size() == 0) {
-			cerr << "Warning: Part " << i << " has no ID" << endl;
+		if (!keyQ) {
+			x  = outmeasures[index].skey[i].x;
+			y  = outmeasures[index].skey[i].y;
+			if ((x>=0)&&(y>=0)) {
+				keyQ = 1;
+			}
 		}
-		auto status = partcontent.insert(make_pair(partid, parts[i].node()));
-		if (status.second == false) {
-			cerr << "Error: ID " << partids.back()
-			     << " is duplicated and secondary part will be ignored" << endl;
+
+		if (!timesigQ) {
+			x  = outmeasures[index].stimesig[i].x;
+			y  = outmeasures[index].stimesig[i].y;
+			if ((x>=0)&&(y>=0)) {
+				timesigQ = 1;
+			}
 		}
-		if (find(partids.begin(), partids.end(), partid) == partids.end()) {
-			cerr << "Error: Part ID " << partid
-			     << " is not present in part-list element list" << endl;
-			continue;
+
+		if (!metQ) {
+			x  = outmeasures[index].smet[i].x;
+			y  = outmeasures[index].smet[i].y;
+			if ((x>=0)&&(y>=0)) {
+				metQ = 1;
+			}
 		}
-	}
 
-	if (partcontent.size() != partids.size()) {
-		cerr << "Error: part-list count does not match part count "
-		     << partcontent.size() << " compared to " << partids.size() << endl;
-		return false;
-	} else {
-		return true;
+		if (!tempoQ) {
+			x  = outmeasures[index].stempo[i].x;
+			y  = outmeasures[index].stempo[i].y;
+			if ((x>=0)&&(y>=0)) {
+				tempoQ = 1;
+			}
+		}
 	}
-}
-
 
+	int ptrack;
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::getPartInfo -- Extract a list of the part ids,
-//    and a reverse mapping to the <score-part> element to which is refers.
-//
-//	   part-list structure:
-//        <part-list>
-//          <score-part id="P1"/>
-//          <score-part id="P2"/>
-//          etc.
-//        </part-list>
-//
-
-bool Tool_musicxml2hum::getPartInfo(map<string, xml_node>& partinfo,
-		vector<string>& partids, xml_document& doc) {
-	auto scoreparts = doc.select_nodes("/score-partwise/part-list/score-part");
-	partids.reserve(scoreparts.size());
-	bool output = true;
-	for (auto el : scoreparts) {
-		partids.emplace_back(getAttributeValue(el.node(), "id"));
-		auto status = partinfo.insert(make_pair(partids.back(), el.node()));
-		if (status.second == false) {
-			cerr << "Error: ID " << partids.back()
-			     << " is duplicated and secondary part will be ignored" << endl;
+	if (clefQ) {
+		for (i=0; i<infile[ii].getFieldCount(); i++) {
+			ptrack = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].sclef[ptrack].x;
+			y  = outmeasures[index].sclef[ptrack].y;
+			if ((x>=0)&&(y>=0)) {
+				m_humdrum_text << infile.token(x, y);
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
 		}
-		output &= status.second;
-		partinfo[partids.back()] = el.node();
+		m_humdrum_text << "\n";
 	}
-	return output;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_musicxml2hum::getChildElementText -- Return the (first)
-//    matching child element's text content.
-//
-
-string Tool_musicxml2hum::getChildElementText(xml_node root,
-		const char* xpath) {
-	return root.select_node(xpath).node().child_value();
-}
-
-string Tool_musicxml2hum::getChildElementText(xpath_node root,
-		const char* xpath) {
-	return root.node().select_node(xpath).node().child_value();
-}
-
 
-
-//////////////////////////////
-//
-// Tool_musicxml2hum::getAttributeValue -- For an xml_node, return
-//     the value for the given attribute name.
-//
-
-string Tool_musicxml2hum::getAttributeValue(xml_node xnode,
-		const string& target) {
-	for (auto at = xnode.first_attribute(); at; at = at.next_attribute()) {
-		if (target == at.name()) {
-			return at.value();
+	if (keysigQ) {
+		for (i=0; i<infile[ii].getFieldCount(); i++) {
+			ptrack = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].skeysig[ptrack].x;
+			y  = outmeasures[index].skeysig[ptrack].y;
+			if ((x>=0)&&(y>=0)) {
+				m_humdrum_text << infile.token(x, y);
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
 		}
+		m_humdrum_text << "\n";
 	}
-	return "";
-}
 
-
-string Tool_musicxml2hum::getAttributeValue(xpath_node xnode,
-		const string& target) {
-	auto node = xnode.node();
-	for (auto at = node.first_attribute(); at; at = at.next_attribute()) {
-		if (target == at.name()) {
-			return at.value();
+	if (keyQ) {
+		for (i=0; i<infile[ii].getFieldCount(); i++) {
+			ptrack = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].skey[ptrack].x;
+			y  = outmeasures[index].skey[ptrack].y;
+			if ((x>=0)&&(y>=0)) {
+				m_humdrum_text << infile.token(x, y);
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
 		}
+		m_humdrum_text << "\n";
 	}
-	return "";
-}
-
 
+	if (timesigQ) {
+		for (i=0; i<infile[ii].getFieldCount(); i++) {
+			ptrack = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].stimesig[ptrack].x;
+			y  = outmeasures[index].stimesig[ptrack].y;
+			if ((x>=0)&&(y>=0)) {
+				m_humdrum_text << infile.token(x, y);
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+		}
+		m_humdrum_text << "\n";
+	}
+	if (metQ) {
+		for (i=0; i<infile[ii].getFieldCount(); i++) {
+			ptrack = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].smet[ptrack].x;
+			y  = outmeasures[index].smet[ptrack].y;
+			if ((x>=0)&&(y>=0)) {
+				m_humdrum_text << infile.token(x, y);
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+		}
+		m_humdrum_text << "\n";
+	}
 
-//////////////////////////////
-//
-// Tool_musicxml2hum::printAttributes -- Print list of all attributes
-//     for an xml_node.
-//
-
-void Tool_musicxml2hum::printAttributes(xml_node node) {
-	int counter = 1;
-	for (auto at = node.first_attribute(); at; at = at.next_attribute()) {
-		cout << "\tattribute " << counter++
-		     << "\tname  = " << at.name()
-		     << "\tvalue = " << at.value()
-		     << endl;
+	if (tempoQ) {
+		for (i=0; i<infile[ii].getFieldCount(); i++) {
+			ptrack = infile.token(ii, i)->getTrack();
+			x  = outmeasures[index].stempo[ptrack].x;
+			y  = outmeasures[index].stempo[ptrack].y;
+			if ((x>=0)&&(y>=0)) {
+				m_humdrum_text << infile.token(x, y);
+			} else {
+				m_humdrum_text << "*";
+			}
+			if (i < infile[ii].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+		}
+		m_humdrum_text << "\n";
 	}
 }
 
@@ -108888,1636 +112974,1456 @@ void Tool_musicxml2hum::printAttributes(xml_node node) {
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getSystemDecoration --
-//
-// Example:  [1,2]{(3,4)}
-//
-//  <part-list>
-//    <part-group type="start" number="1">
-//      <group-symbol>bracket</group-symbol>
-//    </part-group>
-//
-//    <score-part id="P1">
-//      <part-name>S A</part-name>
-//      <score-instrument id="P1-I1">
-//        <instrument-name>Soprano/Alto</instrument-name>
-//      </score-instrument>
-//      <midi-device id="P1-I1" port="1"></midi-device>
-//      <midi-instrument id="P1-I1">
-//        <midi-channel>1</midi-channel>
-//        <midi-program>53</midi-program>
-//        <volume>78.7402</volume>
-//        <pan>0</pan>
-//      </midi-instrument>
-//    </score-part>
-//
-//    <score-part id="P2">
-//      <part-name>T B</part-name>
-//      <score-instrument id="P2-I1">
-//        <instrument-name>Tenor/Bass</instrument-name>
-//      </score-instrument>
-//      <midi-device id="P2-I1" port="1"></midi-device>
-//      <midi-instrument id="P2-I1">
-//        <midi-channel>2</midi-channel>
-//        <midi-program>53</midi-program>
-//        <volume>78.7402</volume>
-//        <pan>0</pan>
-//      </midi-instrument>
-//    </score-part>
-//
-//    <part-group type="stop" number="1"/>
-//
-//    <score-part id="P3">
-//      <part-name>Organ</part-name>
-//      <part-abbreviation>Org.</part-abbreviation>
-//      <score-instrument id="P3-I1">
-//        <instrument-name>Pipe Organ</instrument-name>
-//      </score-instrument>
-//      <midi-device id="P3-I1" port="1"></midi-device>
-//      <midi-instrument id="P3-I1">
-//        <midi-channel>3</midi-channel>
-//        <midi-program>76</midi-program>
-//        <volume>78.7402</volume>
-//        <pan>0</pan>
-//      </midi-instrument>
-//    </score-part>
-//
-//  </part-list>
+// Tool_myank::printDataLine -- Print line with data tokens of selected section
 //
 
-string Tool_musicxml2hum::getSystemDecoration(xml_document& doc, HumGrid& grid,
-	vector<string>& partids) {
-
-	xml_node partlist = doc.select_node("/score-partwise/part-list").node();
-	if (!partlist) {
-		cerr << "Error: cannot find partlist\n";
-		return "";
-	}
-	vector<xml_node> children;
-	getChildrenVector(children, partlist);
-
-	vector<vector<int>> staffnumbers;
-	int pcount = grid.getPartCount();
-	staffnumbers.resize(pcount);
-
-	int scounter = 1;
-	for (int i=0; i<pcount; i++) {
-		int staffcount = grid.getStaffCount(i);
-		for (int j=0; j<staffcount; j++) {
-			staffnumbers[i].push_back(scounter++);
-		}
-	}
-
-	string output;
-
-	// part-group @type=start @number=1
-   //   <group-symbol>bracket</group-symbol>
-	// score-part
-	// score-part
-	// part-group @type=stop @number=1
-	// score-part
-	int pcounter = 0;
-	scounter = 1;
-	vector<string> typeendings(100);
-	for (int i=0; i<(int)children.size(); i++) {
-		string name = children[i].name();
-		if (name == "part-group") {
-			string grouptype = children[i].attribute("type").value();
-			string gsymbol = "";
-			int number = children[i].attribute("number").as_int();
-			if (grouptype == "start") {
-				string g = children[i].select_node("//group-symbol").node().child_value();
-				if (g == "bracket") {
-					output += "[(";
-					typeendings[number] = ")]";
-				} else if (g == "brace") {
-					output += "{(";
-					typeendings[number] = ")}";
-				} else {
-					cerr << "Unknown part grouping symbol: " << g << endl;
+void Tool_myank::printDataLine(HLp line,
+		bool& startLineHandled,
+		const vector<int>& lastLineResolvedTokenLineIndex,
+		const vector<HumNum>& lastLineDurationsFromNoteStart) {
+	bool lineChange = false;
+	string recipRegex = R"re(([\d%.]+))re";
+	// Handle cutting the previeous token of a note that hangs into the selected
+	// section
+	if (startLineHandled == false) {
+		if (line->isData()) {
+			vector<HTp> tokens;
+			line->getTokens(tokens);
+			for (HTp token : tokens) {
+				if (token->isKern() && token->isNull()) {
+					HTp resolvedToken = token->resolveNull();
+					if (resolvedToken->isNull()) {
+						continue;
+					}
+					HumRegex hre;
+					string recip = Convert::durationToRecip(token->getDurationToNoteEnd());
+					vector<string> subtokens = resolvedToken->getSubtokens();
+					string tokenText;
+					for (int i=0; i<(int)subtokens.size(); i++) {
+						if (hre.search(subtokens[i], recipRegex)) {
+							string before = hre.getPrefix();
+							string after = hre.getSuffix();
+							hre.replaceDestructive(after, "", recipRegex, "g");
+							string subtokenText;
+							// Replace the old duration with the clipped one
+							subtokenText += before + recip + after;
+							// Add a tie end if not already in a tie group
+							if (!hre.search(subtokens[i], "[_\\]]")) {
+									subtokenText += "]";
+							}
+							tokenText += subtokenText;
+							if (i < (int)subtokens.size() - 1) {
+								tokenText += " ";
+							}
+						}
+					}
+					token->setText(tokenText);
+					lineChange = true;
 				}
-			} else if (grouptype == "stop") {
-				output += typeendings[number];
-				typeendings[number].clear();
 			}
-		} else if (name == "score-part") {
-			pcounter++;
-			int staffcount = grid.getStaffCount(pcounter-1);
-			if (staffcount == 1) {
-				output += "s" + to_string(scounter++);
-			} else if (staffcount > 1) {
-				output += "{(";
-				for (int k=0; k<staffcount; k++) {
-					output += "s" + to_string(scounter++);
+			startLineHandled = true;
+		}
+	// Handle cutting the last attacked note of the selected section
+	} else {
+		// Check if line has a note that needs to be handled
+		if (find(lastLineResolvedTokenLineIndex.begin(), lastLineResolvedTokenLineIndex.end(), line->getLineIndex()) !=
+				lastLineResolvedTokenLineIndex.end()) {
+			for (int i = 0; i < line->getTokenCount(); i++) {
+				HTp token = line->token(i);
+				// Check if token need the be handled and is of type **kern
+				if (token->isKern() && (lastLineResolvedTokenLineIndex[i] == line->getLineIndex())) {
+					HTp resolvedToken = token->resolveNull();
+					if (resolvedToken->isNull()) {
+						continue;
+					}
+					HumNum dur = lastLineDurationsFromNoteStart[i];
+					HumRegex hre;
+					string recip = Convert::durationToRecip(dur);
+					vector<string> subtokens = resolvedToken->getSubtokens();
+					for (int i=0; i<(int)subtokens.size(); i++) {
+						if (hre.search(subtokens[i], recipRegex)) {
+							string before = hre.getPrefix();
+							string after = hre.getSuffix();
+							hre.replaceDestructive(after, "", recipRegex, "g");
+							string subtokenText;
+							if (resolvedToken->getDuration() > dur) {
+								// Add a tie start if not already in a tie group
+								if (!hre.search(subtokens[i], "[_\\[]")) {
+										subtokenText += "[";
+								}
+							}
+							// Replace the old duration with the clipped one
+							subtokenText += before + recip + after;
+							token->replaceSubtoken(i, subtokenText);
+							lineChange = true;
+						}
+					}
 				}
-				output += ")}";
 			}
 		}
 	}
-
-	string newoutput;
-	for (int i=0; i<(int)output.size(); i++) {
-		if ((i>0) && (output[i] == 's') && isdigit(output[i-1])) {
-			newoutput += ',';
-		}
-		newoutput += output[i];
+	if (lineChange) {
+		line->createLineFromTokens();
 	}
-
-	return newoutput;
+	m_humdrum_text << line << "\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_musicxml2hum::getChildrenVector -- Return a list of all children
-//   elements of a given element.  Pugixml does not allow random access,
-//   but storing them in a vector allows that possibility.
+// Tool_myank::printMeasureStart -- print a starting measure of a segment.
 //
 
-void Tool_musicxml2hum::getChildrenVector(vector<xml_node>& children,
-		xml_node parent) {
-	children.clear();
-	for (xml_node child : parent.children()) {
-		children.push_back(child);
+void Tool_myank::printMeasureStart(HumdrumFile& infile, int line, const string& style) {
+	if (!infile[line].isBarline()) {
+		m_humdrum_text << infile[line] << "\n";
+		return;
 	}
-}
-
-
-
-
-/////////////////////////////////
-//
-// Tool_myank::Tool_myank -- Set the recognized options for the tool.
-//
-
-Tool_myank::Tool_myank(void) {
-	define("v|verbose=b",                        "verbose output of data");
-	define("debug=b",                            "debugging information");
-	define("inlist=b",                           "show input measure list");
-	define("outlist=b",                          "show output measure list");
-	define("mark|marks=b",                       "yank measure with marked notes");
-	define("T|M|bar-number-text=b",              "print barnum with LO text above system ");
-	define("d|double|dm|md|mdsep|mdseparator=b", "put double barline between non-consecutive measure segments");
-	define("m|b|measures|bars|measure|bar=s",    "measures to yank");
-	define("l|lines|line-range=s",               "line numbers range to yank (e.g. 40-50)");
-	define("I|i|instrument=b",                   "Include instrument codes from start of data");
-	define("visible|not-invisible=b",            "do not make initial measure invisible");
-	define("B|noendbar=b",                       "do not print barline at end of data");
-	define("max=b",                              "print maximum measure number");
-	define("min=b",                              "print minimum measure number");
-	define("section-count=b",                    "count the number of sections, JRP style");
-	define("section=i:0",                        "extract given section number (indexed from 1");
-	define("author=b",                           "program author");
-	define("version=b",                          "program version");
-	define("example=b",                          "program examples");
-	define("h|help=b",                           "short description");
-	define("hide-starting=b",                    "prevent printStarting");
-	define("hide-ending=b",                      "prevent printEnding");
-}
-
-
-
-/////////////////////////////////
-//
-// Tool_myank::run -- Primary interfaces to the tool.
-//
 
-bool Tool_myank::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	HumRegex hre;
+	int j;
+	for (j=0; j<infile[line].getFieldCount(); j++) {
+		if (hre.search(infile.token(line, j), "=(\\d*)(.*)", "")) {
+			if (style == "==") {
+				m_humdrum_text << "==";
+				m_humdrum_text << hre.getMatch(1);
+			} else {
+				m_humdrum_text << "=";
+				m_humdrum_text << hre.getMatch(1);
+				m_humdrum_text << style;
+			}
+		} else {
+			if (style == "==") {
+				m_humdrum_text << "==";
+			} else {
+				m_humdrum_text << "=" << style;
+			}
+		}
+		if (j < infile[line].getFieldCount()-1) {
+			m_humdrum_text << "\t";
+		}
 	}
-	return status;
-}
-
+	m_humdrum_text << "\n";
 
-bool Tool_myank::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (m_barnumtextQ) {
+		int barline = 0;
+		sscanf(infile.token(line, 0)->c_str(), "=%d", &barline);
+		if (barline > 0) {
+			m_humdrum_text << "!!LO:TX:Z=20:X=-25:t=" << barline << endl;
+		}
 	}
-	return status;
 }
 
 
-bool Tool_myank::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
-
+//////////////////////////////
 //
-// In-place processing of file:
+// Tool_myank::printDoubleBarline --
 //
 
-bool Tool_myank::run(HumdrumFile& infile) {
-	// Max track in enscripten is wrong for some reason,
-	// so making a copy and forcing reanalysis:
-	//perhaps not needed anymore:
-	//stringstream ss;
-	//ss << infile;
-	//infile.read(ss);
-	initialize(infile);
-	processFile(infile);
-	// Re-load the text for each line from their tokens.
-	infile.createLinesFromTokens();
-	return true;
-}
-
-
-///////////////////////////////////////////////////////////////////////////
-
-ostream& operator<<(ostream& out, MyCoord& value) {
-	out << "(" << value.x << "," << value.y << ")";
-	return out;
-}
-
+void Tool_myank::printDoubleBarline(HumdrumFile& infile, int line) {
 
-ostream& operator<<(ostream& out, MeasureInfo& info) {
-	if (info.file == NULL) {
-		return out;
+	if (!infile[line].isBarline()) {
+		m_humdrum_text << infile[line] << "\n";
+		return;
 	}
-	HumdrumFile& infile = *(info.file);
-	out << "================================== " << endl;
-	out << "NUMBER      = " << info.num   << endl;
-	out << "SEGMENT     = " << info.seg   << endl;
-	out << "START       = " << info.start << endl;
-	out << "STOP        = " << info.stop  << endl;
-	out << "STOP_STYLE  = " << info.stopStyle << endl;
-	out << "START_STYLE = " << info.startStyle << endl;
 
-	for (int i=1; i<(int)info.sclef.size(); i++) {
-		out << "TRACK " << i << ":" << endl;
-		if (info.sclef[i].isValid()) {
-			out << "   START CLEF    = " << infile.token(info.sclef[i].x, info.sclef[i].y)       << endl;
-		}
-		if (info.skeysig[i].isValid()) {
-			out << "   START KEYSIG  = " << infile.token(info.skeysig[i].x, info.skeysig[i].y)   << endl;
-		}
-		if (info.skey[i].isValid()) {
-			out << "   START KEY     = " << infile.token(info.skey[i].x, info.skey[i].y)         << endl;
-		}
-		if (info.stimesig[i].isValid()) {
-			out << "   START TIMESIG = " << infile.token(info.stimesig[i].x, info.stimesig[i].y) << endl;
-		}
-		if (info.smet[i].isValid()) {
-			out << "   START MET     = " << infile.token(info.smet[i].x, info.smet[i].y)         << endl;
+	HumRegex hre;
+	int j;
+	for (j=0; j<infile[line].getFieldCount(); j++) {
+		if (hre.search(infile.token(line, j), "(=\\d*)(.*)", "")) {
+			m_humdrum_text << hre.getMatch(1);
+			m_humdrum_text << "||";
+		} else {
+			m_humdrum_text << "=||";
 		}
-		if (info.stempo[i].isValid()) {
-			out << "   START TEMPO   = " << infile.token(info.stempo[i].x, info.stempo[i].y)     << endl;
+		if (j < infile[line].getFieldCount()-1) {
+			m_humdrum_text << "\t";
 		}
+	}
+	m_humdrum_text << "\n";
 
-		if (info.eclef[i].isValid()) {
-			out << "   END CLEF    = " << infile.token(info.eclef[i].x, info.eclef[i].y)       << endl;
-		}
-		if (info.ekeysig[i].isValid()) {
-			out << "   END KEYSIG  = " << infile.token(info.ekeysig[i].x, info.ekeysig[i].y)   << endl;
-		}
-		if (info.ekey[i].isValid()) {
-			out << "   END KEY     = " << infile.token(info.ekey[i].x, info.ekey[i].y)         << endl;
-		}
-		if (info.etimesig[i].isValid()) {
-			out << "   END TIMESIG = " << infile.token(info.etimesig[i].x, info.etimesig[i].y) << endl;
-		}
-		if (info.emet[i].isValid()) {
-			out << "   END MET     = " << infile.token(info.emet[i].x, info.emet[i].y)         << endl;
-		}
-		if (info.etempo[i].isValid()) {
-			out << "   END TEMPO   = " << infile.token(info.etempo[i].x, info.etempo[i].y)     << endl;
+	if (m_barnumtextQ) {
+		int barline = 0;
+		sscanf(infile.token(line, 0)->c_str(), "=%d", &barline);
+		if (barline > 0) {
+			m_humdrum_text << "!!LO:TX:Z=20:X=-25:t=" << barline << endl;
 		}
 	}
 
-	return out;
 }
 
-///////////////////////////////////////////////////////////////////////////
 
 
 //////////////////////////////
 //
-// Tool_myank::initialize -- extract time signature lines for
-//    each **kern spine in file.
+// Tool_myank::printInvisibleMeasure --
 //
 
-void Tool_myank::initialize(HumdrumFile& infile) {
-	// handle basic options:
-	if (getBoolean("author")) {
-		m_free_text << "Written by Craig Stuart Sapp, "
-			  << "craig@ccrma.stanford.edu, December 2010" << endl;
-		return;
-	} else if (getBoolean("version")) {
-		m_free_text << getCommand() << ", version: 26 December 2010" << endl;
-		m_free_text << "compiled: " << __DATE__ << endl;
-		return;
-	} else if (getBoolean("help")) {
-		usage(getCommand());
-		return;
-	} else if (getBoolean("example")) {
-		example();
+void Tool_myank::printInvisibleMeasure(HumdrumFile& infile, int line) {
+	if (!infile[line].isBarline()) {
+		m_humdrum_text << infile[line] << "\n";
 		return;
 	}
 
-	m_debugQ        = getBoolean("debug");
-	m_inlistQ       = getBoolean("inlist");
-	m_outlistQ      = getBoolean("outlist");
-	m_verboseQ      = getBoolean("verbose");
-	m_maxQ          = getBoolean("max");
-	m_minQ          = getBoolean("min");
-
-	m_invisibleQ    = !getBoolean("not-invisible");
-	m_instrumentQ   =  getBoolean("instrument");
-	m_nolastbarQ    =  getBoolean("noendbar");
-	m_markQ         =  getBoolean("mark");
-	m_doubleQ       =  getBoolean("mdsep");
-	m_barnumtextQ   =  getBoolean("bar-number-text");
-	m_sectionCountQ =  getBoolean("section-count");
-	m_section       =  getInteger("section");
-
-	m_lineRange     = getString("lines");
-	m_hideStarting  = getBoolean("hide-starting");
-	m_hideEnding    = getBoolean("hide-ending");
-
-
-	if (!m_section) {
-		if (!(getBoolean("measures") || m_markQ) && !getBoolean("lines")) {
-			// if -m option is not given, then --mark option presumed
-			m_markQ = 1;
-			// cerr << "Error: the -m option is required" << endl;
-			// exit(1);
+	HumRegex hre;
+	int j;
+	for (j=0; j<infile[line].getFieldCount(); j++) {
+		if (infile.token(line, j)->find('-') != string::npos) {
+			m_humdrum_text << infile.token(line, j);
+			if (j < infile[line].getFieldCount()-1) {
+				m_humdrum_text << "\t";
+			}
+			continue;
+		}
+		if (hre.search(infile.token(line, j), "(=\\d*)(.*)", "")) {
+			m_humdrum_text << hre.getMatch(1);
+			// m_humdrum_text << "-";
+			m_humdrum_text << hre.getMatch(2);
+		} else {
+			m_humdrum_text << infile.token(line, j);
+		}
+		if (j < infile[line].getFieldCount()-1) {
+			m_humdrum_text << "\t";
 		}
 	}
-
+	m_humdrum_text << "\n";
 }
 
 
 
-////////////////////////
+//////////////////////////////
 //
-// Tool_myank::processFile --
+// Tool_myank::reconcileSpineBoundary -- merge spines correctly between segments.
+//    will not be able to handle all permutations of spine manipulators.
+//    So don't expect exotic manipulators to work...
 //
 
-void Tool_myank::processFile(HumdrumFile& infile) {
-	if (m_sectionCountQ) {
-		int sections = getSectionCount(infile);
-		m_humdrum_text << sections << endl;
-		return;
-	}
-
-	getMetStates(m_metstates, infile);
-	getMeasureStartStop(m_measureInList, infile);
+void Tool_myank::reconcileSpineBoundary(HumdrumFile& infile, int index1, int index2) {
 
-	string measurestring = getString("measures");
+	if (m_debugQ) {
+		m_humdrum_text << "RECONCILING LINES " << index1+1 << " and " << index2+1 << endl;
+		m_humdrum_text << "FIELD COUNT OF " << index1+1 << " is "
+			            << infile[index1].getFieldCount() << endl;
+		m_humdrum_text << "FIELD COUNT OF " << index2+1 << " is "
+			            << infile[index2].getFieldCount() << endl;
+	}
 
-	if (getBoolean("lines")) {
-		int startLineNumber = getStartLineNumber();
-		int endLineNumber = getEndLineNumber();
-		if ((startLineNumber > endLineNumber) || (endLineNumber > infile.getLineCount())) {
-			// Disallow when end line number is bigger then line count or when
-			// start line number greather than end line number
+	// check to see if any changes need reconciling; otherwise, exit function
+	int i, j;
+	if (infile[index1].getFieldCount() == infile[index2].getFieldCount()) {
+		int same = 1;
+		for (i=0; i<infile[index1].getFieldCount(); i++) {
+			if (infile.token(index1,i)->getSpineInfo() != infile.token(index2, i)->getSpineInfo()) {
+				same = 0;
+			}
+		}
+		if (same != 0) {
 			return;
 		}
-		m_barNumbersPerLine = analyzeBarNumbers(infile);
-		int startBarNumber = getBarNumberForLineNumber(startLineNumber);
-		int endBarNumber = getBarNumberForLineNumber(endLineNumber);
-		measurestring = to_string(startBarNumber) + "-" + to_string(endBarNumber);
 	}
 
-	measurestring = expandMultipliers(measurestring);
-	if (m_markQ) {
-		stringstream mstring;
-		getMarkString(mstring, infile);
-		measurestring = mstring.str();
-		if (m_debugQ) {
-			m_free_text << "MARK STRING: " << mstring.str() << endl;
-		}
-	} else if (m_section) {
-		string sstring;
-		getSectionString(sstring, infile, m_section);
-		measurestring = sstring;
-	}
-	if (m_debugQ) {
-		m_free_text << "MARK MEASURES: " << measurestring << endl;
-	}
+	// handle splits all at once
+	string buff1;
+	string buff2;
 
-	// expand to multiple measures later.
-	expandMeasureOutList(m_measureOutList, m_measureInList, infile,
-			measurestring);
+	vector<int> splits(infile[index1].getFieldCount());
+	fill(splits.begin(), splits.end(), 0);
 
-	if (m_inlistQ) {
-		m_free_text << "INPUT MEASURE MAP: " << endl;
-		for (int i=0; i<(int)m_measureInList.size(); i++) {
-			m_free_text << m_measureInList[i];
-		}
-	}
-	if (m_outlistQ) {
-		m_free_text << "OUTPUT MEASURE MAP: " << endl;
-		for (int i=0; i<(int)m_measureOutList.size(); i++) {
-			m_free_text << m_measureOutList[i];
+	int hassplit = 0;
+	for (i=0; i<infile[index1].getFieldCount(); i++) {
+		buff1 = "(";
+		buff1 += infile.token(index1, i)->getSpineInfo();
+		buff1 += ")";
+		buff2 = buff1;
+		buff1 += "a";
+		buff2 += "b";
+		for (j=0; j<infile[index2].getFieldCount()-1; j++) {
+			if ((buff1 == infile.token(index2, j)->getSpineInfo()
+					&& (buff2 == infile.token(index2,j+1)->getSpineInfo()))) {
+				splits[i] = 1;
+				hassplit++;
+			}
 		}
 	}
 
-	if (m_measureOutList.size() == 0) {
-		// disallow processing files with no barlines
-		return;
-	}
-
-	// move stopStyle to startStyle of next measure group.
-	for (int i=(int)m_measureOutList.size()-1; i>0; i--) {
-		m_measureOutList[i].startStyle = m_measureOutList[i-1].stopStyle;
-		m_measureOutList[i-1].stopStyle = "";
+	if (hassplit) {
+		for (i=0; i<(int)splits.size(); i++) {
+			if (splits[i]) {
+				m_humdrum_text << "*^";
+			} else {
+				m_humdrum_text << '*';
+			}
+			if (i < (int)splits.size()-1) {
+				m_humdrum_text << '\t';
+			}
+		}
+		m_humdrum_text << '\n';
 	}
 
-	myank(infile, m_measureOutList);
-}
-
-
-
-////////////////////////
-//
-// Tool_myank::analyzeBarNumbers -- Stores the bar number of each line in a vector
-//
+	// make splits cumulative;
+	//for (i=1; i<(int)splits.size(); i++) {
+	//   splits[i] += splits[i-1];
+	//}
 
-vector<int> Tool_myank::analyzeBarNumbers(HumdrumFile& infile) {
-	vector<int> m_barnum;
-	m_barnum.resize(infile.getLineCount());
-	int current = 0;
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isBarline()) {
-			m_barnum.at(i) = current;
+	HumRegex hre1;
+	HumRegex hre2;
+	// handle joins one at a time, only look for binary joins at the moment.
+	// assuming that no *x has been used to mix the voices up.
+	for (i=0; i<infile[index1].getFieldCount()-1; i++) {
+		if (!hre1.search(infile.token(index1, i)->getSpineInfo(), "\\((.*)\\)a")) {
 			continue;
 		}
-		if (hre.search(infile[i].token(0), "=(\\d+)")) {
-			current = hre.getMatchInt(1);
+		if (!hre2.search(infile.token(index1, i+1)->getSpineInfo(), "\\((.*)\\)b")) {
+			continue;
+		}
+		if (hre1.getMatch(1) != hre2.getMatch(1)) {
+			// spines are not split from same source
+			continue;
+		}
+
+		// found an "a" and "b" portion of a spine split, now search
+		// through the target line for a joint of those two sub-spines
+		for (j=0; j<infile[index2].getFieldCount(); j++) {
+			if (infile.token(index2, j)->getSpineInfo() != hre1.getMatch(1)) {
+				continue;
+			}
+			// found a simple binary spine join: emmit a spine manipulator line
+			printJoinLine(splits, i, 2);
 		}
-		m_barnum.at(i) = current;
 	}
-	return m_barnum;
-}
-
-
 
-////////////////////////
-//
-// Tool_myank::getBarNumberForLineNumber --
-//
+	// handle *x switches, not perfect since ordering might need to be
+	// handled between manipulators...
 
-int Tool_myank::getBarNumberForLineNumber(int lineNumber) {
-	return m_barNumbersPerLine[lineNumber-1];
 }
 
 
 
-////////////////////////
+//////////////////////////////
 //
-// Tool_myank::getStartLineNumber -- Get start line number from --lines
+// Tool_myank::printJoinLine -- count is currently ignored, but may in the future
+//    allow for more than two spines to join at the same time.
 //
 
-int Tool_myank::getStartLineNumber(void) {
-	HumRegex hre;
-	if (hre.search(m_lineRange, "^(\\d+)\\-(\\d+)$")) {
-		return hre.getMatchInt(1);
+void Tool_myank::printJoinLine(vector<int>& splits, int index, int count) {
+	int i;
+	for (i=0; i<(int)splits.size(); i++) {
+		if (i == index) {
+			m_humdrum_text << "*v\t*v";
+			i+=count-1;
+		} else {
+			m_humdrum_text << "*";
+		}
+		if (i<(int)splits.size()-1) {
+			m_humdrum_text << "\t";
+		}
 	}
-	return -1;
-}
-
-
-
-////////////////////////
-//
-// Tool_myank::getEndLineNumber -- Get end line number from --lines
-//
+	m_humdrum_text << "\n";
 
-int Tool_myank::getEndLineNumber(void) {
-	HumRegex hre;
-	if (hre.search(m_lineRange, "^(\\d+)\\-(\\d+)$")) {
-		return hre.getMatchInt(2);
+	// merge splits by one element
+	for (i=index+1; i<(int)splits.size()-1; i++) {
+		splits[i] = splits[i+1];
 	}
-	return -1;
+	splits.resize(splits.size()-1);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::expandMultipliers -- 2*5 => 2,2,2,2,2
-//    Limit of 100 times expansion
+// Tool_myank::reconcileStartingPosition -- merge spines from start of data and
+//    first measure in output.
 //
 
-string Tool_myank::expandMultipliers(const string& inputstring) {
-	HumRegex hre;
-	if (!hre.search(inputstring, "\\*")) {
-		return inputstring;
-	}
-	string outputstring = inputstring;
-	while (hre.search(outputstring, "(\\d+)\\*([1-9]+[0-9]*)")) {
-		string measurenum = hre.getMatch(1);
-		int multiplier = hre.getMatchInt(2);
-		if (multiplier > 100) {
-			cerr << "Reducing multiplier from " << multiplier << " to 100" << endl;
-			multiplier = 100;
-		}
-		string expansion = measurenum;
-		for (int i=1; i<multiplier; i++) {
-			expansion += ",";
-			expansion += measurenum;
+void Tool_myank::reconcileStartingPosition(HumdrumFile& infile, int index2) {
+	int i;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isInterpretation()) {
+			reconcileSpineBoundary(infile, i, index2);
+			break;
 		}
-		hre.replaceDestructive(outputstring, expansion, "(\\d+)\\*([1-9]+[0-9]*)");
 	}
-	return outputstring;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_myank::getMetStates --  Store the current *met for every token
-// in the score, keeping track of meter without metric symbols.
+// Tool_myank::printStarting -- print header information before start of data.
 //
 
-void Tool_myank::getMetStates(vector<vector<MyCoord> >& metstates,
-		HumdrumFile& infile) {
-	vector<MyCoord> current;
-	current.resize(infile.getMaxTrack()+1);
-	metstates.resize(infile.getLineCount());
-	HumRegex hre;
-
-	int track;
-	for (int i=0; i<infile.getLineCount(); i++) {
+void Tool_myank::printStarting(HumdrumFile& infile) {
+	int i, j;
+	int exi = -1;
+	for (i=0; i<infile.getLineCount(); i++) {
 		if (infile[i].isInterpretation()) {
-			for (int j=0; j<infile[i].getFieldCount(); j++) {
-				track = infile.token(i, j)->getTrack();
-				if (hre.search(infile.token(i, j), R"(^\*met\([^\)]+\))")) {
-					current[track].x = i;
-					current[track].y = j;
-				} else if (hre.search(infile.token(i, j), R"(^\*M\d+\d+)")) {
-					current[track] = getLocalMetInfo(infile, i, track);
-				}
-			}
+			// the first interpretation is the exclusive one
+			m_humdrum_text << infile[i] << "\n";
+			exi = i;
+			break;
 		}
-
-		// metstates[i].resize(infile[i].getFieldCount());
-		// for (j=0; j<infile[i].getFieldCount(); j++) {
-		//    track = infile.token(i, j)->getTrack();
-		//    metstates[i][j] = current[track];
-		// }
-		metstates[i].resize(infile.getMaxTrack()+1);
-		for (int j=1; j<=infile.getMaxTrack(); j++) {
-			metstates[i][j] = current[j];
+		if (!m_hideStarting) {
+			m_humdrum_text << infile[i] << "\n";
+		} else {
+			if (infile[i].rfind("!!!RDF", 0) == 0 || infile[i].rfind("!!!system-decoration", 0) == 0) {
+				m_humdrum_text << infile[i] << "\n";
+			}
 		}
 	}
 
-	if (m_debugQ) {
-		for (int i=0; i<infile.getLineCount(); i++) {
-			for (int j=1; j<(int)metstates[i].size(); j++) {
-				if (metstates[i][j].x < 0) {
-					m_humdrum_text << ".";
+	// keep *part interpretations
+	bool hasPart = false;
+	for (i=exi+1; i<infile.getLineCount(); i++) {
+		hasPart = false;
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			if (infile.token(i, j)->compare(0, 5, "*part") == 0) {
+				hasPart = true;
+				break;
+			}
+		}
+		if (hasPart) {
+			for (j=0; j<infile[i].getFieldCount(); j++) {
+				if (infile.token(i, j)->compare(0, 5, "*part") == 0) {
+					m_humdrum_text << infile.token(i, j);
 				} else {
-					m_humdrum_text << infile.token(metstates[i][j].x, metstates[i][j].y);
+					m_humdrum_text << "*";
+				}
+				if (j < infile[i].getFieldCount() - 1) {
+					m_humdrum_text << "\t";
 				}
-				m_humdrum_text << "\t";
 			}
-			m_humdrum_text << infile[i] << endl;
+			m_humdrum_text << "\n";
 		}
-
 	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_myank::getLocalMetInfo -- search in the non-data region indicated by the
-// input row for a *met entry in the input track.  Return empty
-// value if none found.
-//
-
-MyCoord Tool_myank::getLocalMetInfo(HumdrumFile& infile, int row, int track) {
-	MyCoord output;
-	int startline = -1;
-	int stopline = -1;
-	int i = row;
-	int j;
-	int xtrac;
-	HumRegex hre;
 
-	while (i>=0) {
-		if (infile[i].isData()) {
-			startline = i+1;
-			break;
+	// keep *staff interpretations
+	bool hasStaff = false;
+	for (i=exi+1; i<infile.getLineCount(); i++) {
+		hasStaff = false;
+		for (j=0; j<infile[i].getFieldCount(); j++) {
+			if (infile.token(i, j)->compare(0, 6, "*staff") == 0) {
+				hasStaff = true;
+				break;
+			}
 		}
-		i--;
-	}
-	if (startline < 0) {
-		startline = 0;
-	}
-	i = row;
-	while (i<infile.getLineCount()){
-		if (infile[i].isData()) {
-			stopline = i-1;
-			break;
+		if (hasStaff) {
+			for (j=0; j<infile[i].getFieldCount(); j++) {
+				if (infile.token(i, j)->compare(0, 6, "*staff") == 0) {
+					m_humdrum_text << infile.token(i, j);
+				} else {
+					m_humdrum_text << "*";
+				}
+				if (j < infile[i].getFieldCount() - 1) {
+					m_humdrum_text << "\t";
+				}
+			}
+			m_humdrum_text << "\n";
 		}
-		i++;
-	}
-	if (stopline >= infile.getLineCount()) {
-		stopline = infile.getLineCount()-1;
 	}
-	for (i=startline; i<=stopline; i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			xtrac = infile.token(i, j)->getTrack();
-			if (track != xtrac) {
+
+	int hasI = 0;
+
+	if (m_instrumentQ) {
+		// print any tandem interpretations which start with *I found
+		// at the start of the data before measures, notes, or any
+		// spine manipulator lines
+		for (i=exi+1; i<infile.getLineCount(); i++) {
+			if (infile[i].isData()) {
+				break;
+			}
+			if (infile[i].isBarline()) {
+				break;
+			}
+			if (!infile[i].isInterpretation()) {
 				continue;
 			}
-			if (hre.search(infile.token(i, j), R"(^\*met\([^\)]+\))")) {
-				output.x = i;
-				output.x = j;
+			if (infile[i].isManipulator()) {
+				break;
+			}
+			hasI = 0;
+			for (j=0; j<infile[i].getFieldCount(); j++) {
+				if (infile.token(i, j)->compare(0, 2, "*I") == 0) {
+					hasI = 1;
+					break;
+				}
+			}
+			if (hasI) {
+				for (j=0; j<infile[i].getFieldCount(); j++) {
+					if (infile.token(i, j)->compare(0, 2, "*I") == 0) {
+						m_humdrum_text << infile.token(i, j);
+					} else {
+						m_humdrum_text << "*";
+					}
+					if (j < infile[i].getFieldCount() - 1) {
+						m_humdrum_text << "\t";
+					}
+				}
+				m_humdrum_text << "\n";
 			}
 		}
 	}
-	return output;
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::getMarkString -- return a list of measures which contain marked
-//    notes (primarily from search matches).
-// This function scans for reference records in this form:
-// !!!RDF**kern: @= matched note
-// or
-// !!!RDF**kern: i= marked note
-// If it finds any lines like that, it will extract the character before
-// the equals sign, and scan for it in the **kern data in the file.
-// any measure which contains such a mark will be stored in the output
-// string.
+// Tool_myank::printEnding -- print the spine terminators and any
+//     content after the end of the data.
 //
 
-void Tool_myank::getMarkString(ostream& out, HumdrumFile& infile)  {
-	string mchar; // list of characters which are marks
-	char target;
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReference()) {
-			continue;
+void Tool_myank::printEnding(HumdrumFile& infile, int lastline, int adjlin) {
+	if (m_debugQ) {
+		m_humdrum_text << "IN printEnding" << endl;
+	}
+	int ending = -1;
+	int marker = -1;
+	int i;
+	for (i=infile.getLineCount()-1; i>=0; i--) {
+		if (infile[i].isInterpretation() && (ending <0)
+				&& (*infile.token(i, 0) == "*-")) {
+			ending = i;
 		}
-		if (hre.search(infile.token(i, 0),
-				R"(!!!RDF\*\*kern\s*:\s*([^=])\s*=\s*match)", "i")) {
-			target = hre.getMatch(1)[0];
-			mchar.push_back(target);
-		} else if (hre.search(infile.token(i, 0),
-				R"(!!!RDF\*\*kern\s*:\s*([^=])\s*=\s*mark)", "i")) {
-			target = hre.getMatch(1)[0];
-			mchar.push_back(target);
+		if (infile[i].isData()) {
+			marker = i+1;
+			break;
+		}
+		if (infile[i].isBarline()) {
+			marker = i+1;
+			break;
 		}
 	}
 
-	if (m_debugQ) {
-		for (int i=0; i<(int)mchar.size(); i++) {
-			m_free_text << "\tMARK CHARCTER: " << mchar[i] << endl;
-		}
+	if (ending >= 0) {
+		reconcileSpineBoundary(infile, adjlin, ending);
 	}
 
-	if (mchar.size() == 0) {
-		return;
+	int startline  = ending;
+	if (marker >= 0) {
+		// capture any comment which occur after the last measure
+		// line in the data.
+		startline = marker;
 	}
 
-	// now search for measures which contains any of those character
-	// in **kern data:
-	int curmeasure = 0;
-	int inserted = 0;
-	int hasmark = 0;
-	string str;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isBarline()) {
-			if (hre.search(infile.token(i, 0), "^=.*?(\\d+)", "")) {
-				curmeasure = stoi(hre.getMatch(1));
-				hasmark = 0;
-			}
-		}
-		if (hasmark) {
-			continue;
-		}
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			if (infile.token(i, j)->isKern()) {
-				int k=0;
-				str = *infile.token(i, j);
-				while (str[k] != '\0') {
-					for (int m=0; m<(int)mchar.size(); m++) {
-						if (str[k] == mchar[m]) {
-							if (inserted) {
-								out << ',';
-							} else {
-								inserted++;
-							}
-							out << curmeasure;
-							hasmark = 1;
-							goto outerforloop;
-						}
-					}
-					k++;
+	// reconcileSpineBoundary(infile, lastline, startline);
+
+	if (startline >= 0) {
+		for (i=startline; i<infile.getLineCount(); i++) {
+			if (m_hideEnding && (i > ending)) {
+				if (infile[i].rfind("!!!RDF", 0) == 0 || infile[i].rfind("!!!system-decoration", 0) == 0) {
+					m_humdrum_text << infile[i] << "\n";
 				}
+			} else {
+				m_humdrum_text << infile[i] << "\n";
 			}
 		}
-outerforloop: ;
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::myank -- yank the specified measures.
+// Tool_myank::getMeasureStartStop --  Get a list of the (numbered) measures in the
+//    input file, and store the start/stop lines for those measures.
+//    All data before the first numbered measure is in measure 0.
+//    although, if the first measure is not labeled, then ...
 //
 
-void Tool_myank::myank(HumdrumFile& infile, vector<MeasureInfo>& outmeasures) {
-
-	if (outmeasures.size() > 0) {
-		printStarting(infile);
-	}
-
-	int lastline = -1;
-	int lastDataLine = -1;
-	int h, i, j;
-	int counter;
-	int printed = 0;
-	int mcount = 0;
-	int measurestart = 1;
-	int lastbarnum = -1;
-	int barnum = -1;
-	int datastart = 0;
-	int bartextcount = 0;
-	bool startLineHandled = false;
-
-	int lastLineIndex = getBoolean("lines") ? getEndLineNumber() - 1 : outmeasures[outmeasures.size() - 1].stop;
-
-	// Find the actual last line of the selected section that is a line with
-	// data tokens
-	while (infile.getLine(lastLineIndex)->isData() == false) {
-		lastLineIndex--;
-	}
-
-	// Mapping with with the start token for each spine
-	vector<int> lastLineResolvedTokenLineIndex;
-	// Mapping with the later needed durations of the note that fits within the
-	// selected section
-	vector<HumNum> lastLineDurationsFromNoteStart;
-
-	lastLineResolvedTokenLineIndex.resize(infile.getLine(lastLineIndex)->getTokenCount());
-	lastLineDurationsFromNoteStart.resize(infile.getLine(lastLineIndex)->getTokenCount());
+void Tool_myank::getMeasureStartStop(vector<MeasureInfo>& measurelist, HumdrumFile& infile) {
+	measurelist.reserve(infile.getLineCount());
+	measurelist.resize(0);
 
-	for (int a = 0; a < infile.getLine(lastLineIndex)->getTokenCount(); a++) {
-		HTp token = infile.token(lastLineIndex, a);
-		// Get lineIndex for last data token with an attack
-		lastLineResolvedTokenLineIndex[a] = infile.token(lastLineIndex, a)->resolveNull()->getLineIndex();
-		// Get needed duration for this token until section end
-		lastLineDurationsFromNoteStart[a] = token->getDurationFromNoteStart() + token->getLine()->getDuration();
-	}
+	MeasureInfo current;
+	int i, ii;
+	int lastend = -1;
+	int dataend = -1;
+	int barnum1 = -1;
+	int barnum2 = -1;
+	HumRegex hre;
 
-	int startLineNumber = getStartLineNumber();
-	int endLineNumber = getEndLineNumber();
+	insertZerothMeasure(measurelist, infile);
 
-	if (getBoolean("lines")) {
-		int firstDataLineIndex = -1;
-		for (int b = startLineNumber - 1; b <= endLineNumber - 1; b++) {
-			if (infile.getLine(b)->isData()) {
-				firstDataLineIndex = b;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isInterpretation()) {
+			if (*infile.token(i, 0) == "*-") {
+				dataend = i;
 				break;
 			}
 		}
-		if (firstDataLineIndex >= 0) {
-			if (infile.getLine(firstDataLineIndex)->getDurationFromBarline() == 0) {
-				for (int c = startLineNumber - 1; c >=  0; c--) {
-					if (infile.getLine(c)->isBarline()) {
-						startLineNumber = c + 1;
-						break;
-					}
-				}
-			}
-		}
-	}
-
-	for (h=0; h<(int)outmeasures.size(); h++) {
-		barnum = outmeasures[h].num;
-		measurestart = 1;
-		printed = 0;
-		counter = 0;
-		if (m_debugQ) {
-			m_humdrum_text << "!! =====================================\n";
-			m_humdrum_text << "!! processing " << outmeasures[h].num << endl;
-		}
-		if (h > 0) {
-			reconcileSpineBoundary(infile, outmeasures[h-1].stop,
-				outmeasures[h].start);
-		} else {
-			reconcileStartingPosition(infile, outmeasures[0].start);
+		if (!infile[i].isBarline()) {
+			continue;
 		}
-		int startLine = getBoolean("lines") ? std::max(startLineNumber-1, outmeasures[h].start)
-			: outmeasures[h].start;
-		int endLine = getBoolean("lines") ? std::min(endLineNumber, outmeasures[h].stop)
-			: outmeasures[h].stop;
-		for (i=startLine; i<endLine; i++) {
-			counter++;
-			if ((!printed) && ((mcount == 0) || (counter == 2))) {
-				if ((datastart == 0) && outmeasures[h].num == 0) {
-					// not ideal setup...
-					datastart = 1;
-				} else{
-					// Fix adjustGlobalInterpretations when line is a global comment
-					int nextLineIndexWithSpines = i;
-					if (infile.getLine(i)->isCommentGlobal()) {
-						for (int d = i; d <= endLineNumber - 1; d++) {
-							if (!infile.getLine(d)->isCommentGlobal()) {
-								nextLineIndexWithSpines = d;
-								break;
-							}
-						}
-					}
-					adjustGlobalInterpretations(infile, nextLineIndexWithSpines, outmeasures, h);
-					printed = 1;
-				}
-			}
-			if (infile[i].isData() && (mcount == 0)) {
-				mcount++;
-			}
-			if (infile[i].isBarline()) {
-				mcount++;
-			}
-			if ((mcount == 1) && m_invisibleQ && infile[i].isBarline()) {
-				printInvisibleMeasure(infile, i);
-				measurestart = 0;
-				if ((bartextcount++ == 0) && infile[i].isBarline()) {
-					int barline = 0;
-					sscanf(infile.token(i, 0)->c_str(), "=%d", &barline);
-					if (m_barnumtextQ && (barline > 0)) {
-						m_humdrum_text << "!!LO:TX:Z=20:X=-90:t=" << barline << endl;
-					}
-				}
-			} else if (m_doubleQ && (lastbarnum > -1) && (abs(barnum - lastbarnum) > 1)) {
-				printDoubleBarline(infile, i);
-				measurestart = 0;
-			} else if (measurestart && infile[i].isBarline()) {
-				printMeasureStart(infile, i, outmeasures[h].startStyle);
-				measurestart = 0;
+		//if (!hre.search(infile.token(i, 0), "^=.*(\\d+)")) {
+		//   continue;
+		//}
+		//barnum1 = stoi(hre.getMatch(1));
+		if (!sscanf(infile.token(i, 0)->c_str(), "=%d", &barnum1)) {
+			continue;
+		}
+		current.clear();
+		current.start = i;
+		current.num   = barnum1;
+		for (ii=i+1; ii<infile.getLineCount(); ii++) {
+			if (!infile[ii].isBarline()) {
+				continue;
+			}
+			//if (hre.search(infile.token(ii, 0), "^=.*(\\d+)")) {
+			//   barnum2 = stoi(hre.getMatch(1));
+			//   current.stop = ii;
+			//   lastend = ii;
+			//   i = ii - 1;
+			//   measurelist.push_back(current);
+			//   break;
+			//}
+			if (hre.search(infile.token(ii, 0), "=[^\\d]*(\\d+)")) {
+			// if (sscanf(infile.token(ii, 0), "=%d", &barnum2)) {
+				barnum2 = stoi(hre.getMatch(1));
+				current.stop = ii;
+				lastend = ii;
+				i = ii - 1;
+				current.file = &infile;
+				measurelist.push_back(current);
+				break;
 			} else {
-				printDataLine(infile.getLine(i), startLineHandled, lastLineResolvedTokenLineIndex, lastLineDurationsFromNoteStart);
-				if (m_barnumtextQ && (bartextcount++ == 0) && infile[i].isBarline()) {
-					int barline = 0;
-					sscanf(infile.token(i, 0)->c_str(), "=%d", &barline);
-					if (barline > 0) {
-						m_humdrum_text << "!!LO:TX:Z=20:X=-25:t=" << barline << endl;
-					}
+				if (atEndOfFile(infile, ii)) {
+					break;
 				}
 			}
-			lastline = i;
-			if (infile.getLine(i)->isData()) {
-				lastDataLine = i;
-			}
 		}
-		lastbarnum = barnum;
 	}
 
-	if (getBoolean("lines") && (lastDataLine >= 0) &&
-			(infile.getLine(lastDataLine)->getDurationToBarline() > infile.getLine(lastDataLine)->getDuration())) {
-		m_nolastbarQ = true;
-	}
+	int lastdata    = -1;   // last line in file with data
+	int lastmeasure = -1;   // last line in file with measure
 
-	HumRegex hre;
-	string token;
-	int lasti;
-	if (outmeasures.size() > 0) {
-		lasti = outmeasures.back().stop;
-	} else {
-		lasti = -1;
-	}
-	if ((!m_nolastbarQ) &&  (lasti >= 0) && infile[lasti].isBarline()) {
-		for (j=0; j<infile[lasti].getFieldCount(); j++) {
-			token = *infile.token(lasti, j);
-			hre.replaceDestructive(token, outmeasures.back().stopStyle, "\\d+.*");
-			// collapse final barlines
-			hre.replaceDestructive(token, "==", "===+");
-			if (m_doubleQ) {
-				if (hre.search(token, "=(.+)")) {
-					// don't add double barline, there is already
-					// some style on the barline
-				} else {
-					// add a double barline
-					hre.replaceDestructive(token, "||", "$");
-				}
-			}
-			m_humdrum_text << token;
-			if (j < infile[lasti].getFieldCount() - 1) {
-				m_humdrum_text << '\t';
-			}
+	for (i=infile.getLineCount()-1; i>=0; i--) {
+		if ((lastdata < 0) && infile[i].isData()) {
+			lastdata = i;
+		}
+		if ((lastmeasure < 0) && infile[i].isBarline()) {
+			lastmeasure = i;
+		}
+		if ((lastmeasure >= 0) && (lastdata >= 0)) {
+			break;
 		}
-		m_humdrum_text << '\n';
 	}
 
-	collapseSpines(infile, lasti);
+	if (lastmeasure < lastdata) {
+		// no final barline, so set to ignore
+		lastmeasure = -1;
+		lastdata    = -1;
+	}
 
-	if (m_debugQ) {
-		m_free_text << "PROCESSING ENDING" << endl;
+	if ((barnum2 >= 0) && (lastend >= 0) && (dataend >= 0)) {
+		current.clear();
+		current.num = barnum2;
+		current.start = lastend;
+		current.stop = dataend;
+		if (lastmeasure > lastdata) {
+			current.stop = lastmeasure;
+		}
+		current.file = &infile;
+		measurelist.push_back(current);
 	}
 
-	if (lastline >= 0) {
-		printEnding(infile, outmeasures.back().stop, lasti);
+	// allow "myank -l" when there are no measure numbers
+	if (getBoolean("lines") && measurelist.size() == 0) {
+		current.clear();
+		current.num = 0;
+		current.start = 0;
+		current.stop = dataend;
+		current.file = &infile;
+		measurelist.push_back(current);
 	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::collapseSpines -- Shrink all sub-spines to single spine.
+// Tool_myank::getSectionCount -- Count the number of sections in a file according to
+//     JRP rules: sections are defined by double barlines. There may be some
+//     corner cases to consider.
 //
 
-void Tool_myank::collapseSpines(HumdrumFile& infile, int line) {
-	if (line < 0) {
-		return;
-	}
-	vector<int> counts(infile.getMaxTrack() + 1, 0);
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		int track = infile.token(line, i)->getTrack();
-		counts.at(track)++;
-	}
-	for (int i=1; i<(int)counts.size(); i++) {
-		if (counts[i] <= 1) {
+int Tool_myank::getSectionCount(HumdrumFile& infile) {
+	int i;
+	int count = 0;
+	int dataQ = 0;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (!dataQ && infile[i].isData()) {
+			dataQ = 1;
+			count++;
 			continue;
 		}
-		bool started = false;
-		for (int j=1; j<(int)counts.size(); j++) {
-			if (j < i) {
-				if (started) {
-					m_humdrum_text << "\t";
-				}
-				m_humdrum_text << "*";
-				started = true;
-				continue;
-			} else if (j == i) {
-				for (int k=0; k<counts[j]; k++) {
-					if (started) {
-						m_humdrum_text << "\t";
-					}
-					m_humdrum_text << "*v";
-					started = true;
-				}
-			} else if (j > i) {
-				for (int k=0; k<counts[j]; k++) {
-					if (started) {
-						m_humdrum_text << "\t";
-					}
-					m_humdrum_text << "*";
-					started = true;
-				}
+		if (infile[i].isBarline()) {
+			if (infile.token(i, 0)->find("||") != string::npos) {
+				dataQ = 0;
 			}
 		}
-		m_humdrum_text << "\n";
-		counts[i] = 1;
 	}
+	return count;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::adjustGlobalInterpretations --
+// Tool_myank::getSectionString -- return the measure range of a section.
 //
 
-void Tool_myank::adjustGlobalInterpretations(HumdrumFile& infile, int ii,
-		vector<MeasureInfo>& outmeasures, int index) {
-
-	if (index <= 0) {
-		adjustGlobalInterpretationsStart(infile, ii, outmeasures, index);
-		return;
+void Tool_myank::getSectionString(string& sstring, HumdrumFile& infile, int sec) {
+	int i;
+	int first = -1;
+	int second = -1;
+	int barnum = 0;
+	int count = 0;
+	int dataQ = 0;
+	HumRegex hre;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if (!dataQ && infile[i].isData()) {
+			dataQ = 1;
+			count++;
+			if (count == sec) {
+				first = barnum;
+			} else if (count == sec+1) {
+				second = barnum - 1;
+			}
+			continue;
+		}
+		if (infile[i].isBarline()) {
+			if (infile.token(i, 0)->find("||") != string::npos) {
+				dataQ = 0;
+			}
+			if (hre.search(infile.token(i, 0), "(\\d+)")) {
+				barnum = hre.getMatchInt(1);
+			}
+		}
+	}
+	if (second < 0) {
+		second = barnum;
 	}
+	sstring = to_string(first);
+	sstring += "-";
+	sstring += to_string(second);
+}
 
-	// the following lines will not work when non-contiguous measures are
-	// elided.
-	//   if (!infile[ii].isInterpretation()) {
-	//      return;
-	//   }
 
-	int clefQ    = 0;
-	int keysigQ  = 0;
-	int keyQ     = 0;
-	int timesigQ = 0;
-	int metQ     = 0;
-	int tempoQ   = 0;
 
-	int x, y;
-	int xo, yo;
+//////////////////////////////
+//
+// Tool_myank::atEndOfFile --
+//
 
-	int tracks = infile.getMaxTrack();
+int Tool_myank::atEndOfFile(HumdrumFile& infile, int line) {
+	int i;
+	for (i=line+1; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			return 0;
+		}
+	}
 
-	// these lines may cause bugs, but they get rid of zeroth measure
-	// problem.
-// ggg
-//   if ((outmeasures.size() > 1) && (outmeasures[index-1].num == 0)) {
-//      return;
-//   }
-//   if ((outmeasures.size() > 0) && (outmeasures[index].num == 0)) {
-//      return;
-//   }
+	return 1;
+}
 
-	for (int i=1; i<=tracks; i++) {
-		if (!clefQ && (outmeasures[index].sclef.size() > 0)) {
-			x  = outmeasures[index].sclef[i].x;
-			y  = outmeasures[index].sclef[i].y;
-			xo = outmeasures[index-1].eclef[i].x;
-			yo = outmeasures[index-1].eclef[i].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					clefQ = 1;
-				}
-			}
-		}
 
-		if (!keysigQ && (outmeasures[index].skeysig.size() > 0)) {
-			x  = outmeasures[index].skeysig[i].x;
-			y  = outmeasures[index].skeysig[i].y;
-			xo = outmeasures[index-1].ekeysig[i].x;
-			yo = outmeasures[index-1].ekeysig[i].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					keysigQ = 1;
-				}
-			}
-		}
 
-		if (!keyQ && (outmeasures[index].skey.size() > 0)) {
-			x  = outmeasures[index].skey[i].x;
-			y  = outmeasures[index].skey[i].y;
-			xo = outmeasures[index-1].ekey[i].x;
-			yo = outmeasures[index-1].ekey[i].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					keyQ = 1;
-				}
-			}
-		}
+//////////////////////////////
+//
+// Tool_myank::insertZerothMeasure --
+//
 
-		if (!timesigQ && (outmeasures[index].stimesig.size() > 0)) {
-			x  = outmeasures[index].stimesig[i].x;
-			y  = outmeasures[index].stimesig[i].y;
-			xo = outmeasures[index-1].etimesig[i].x;
-			yo = outmeasures[index-1].etimesig[i].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					timesigQ = 1;
-				}
-			}
-		}
+void Tool_myank::insertZerothMeasure(vector<MeasureInfo>& measurelist,
+		HumdrumFile& infile) {
 
-		if (!metQ && (outmeasures[index].smet.size() > 0)) {
-			x  = outmeasures[index].smet[i].x;
-			y  = outmeasures[index].smet[i].y;
-			xo = outmeasures[index-1].emet[i].x;
-			yo = outmeasures[index-1].emet[i].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					metQ = 1;
-				}
-			}
+	HumRegex hre;
+	int exinterpline = -1;
+	int startline = -1;
+	int stopline = -1;
+	int i;
+	for (i=0; i<infile.getLineCount(); i++) {
+		if ((exinterpline < 0) && infile[i].isInterpretation()) {
+			exinterpline = i;
 		}
-
-		if (!tempoQ && (outmeasures[index].stempo.size() > 0)) {
-			x  = outmeasures[index].stempo[i].x;
-			y  = outmeasures[index].stempo[i].y;
-			xo = outmeasures[index-1].etempo[i].x;
-			yo = outmeasures[index-1].etempo[i].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					tempoQ = 1;
-				}
-			}
+		if ((startline < 0) && (infile[i].isData())) {
+			startline = i;
 		}
-	}
-
-	int track;
-
-	if (clefQ) {
-		for (int i=0; i<infile[ii].getFieldCount(); i++) {
-			track = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].sclef[track].x;
-			y  = outmeasures[index].sclef[track].y;
-			xo = outmeasures[index-1].eclef[track].x;
-			yo = outmeasures[index-1].eclef[track].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					m_humdrum_text << infile.token(x, y);
-				} else {
-					m_humdrum_text << "*";
-				}
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
+		if (infile[i].isBarline() && hre.search(infile.token(i, 0), "^=.*\\d+", "")) {
+			stopline = i;
+			break;
 		}
-		m_humdrum_text << "\n";
 	}
 
-	if (keysigQ) {
-		for (int i=0; i<infile[ii].getFieldCount(); i++) {
-			track = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].skeysig[track].x;
-			y  = outmeasures[index].skeysig[track].y;
-			xo = outmeasures[index-1].ekeysig[track].x;
-			yo = outmeasures[index-1].ekeysig[track].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					m_humdrum_text << infile.token(x, y);
-				} else {
-					m_humdrum_text << "*";
-				}
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
-		}
-		m_humdrum_text << "\n";
+	if (exinterpline < 0) {
+		// somethind weird happend, just return
+		return;
 	}
-
-	if (keyQ) {
-		for (int i=0; i<infile[ii].getFieldCount(); i++) {
-			track = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].skey[track].x;
-			y  = outmeasures[index].skey[track].y;
-			xo = outmeasures[index-1].ekey[track].x;
-			yo = outmeasures[index-1].ekey[track].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					m_humdrum_text << infile.token(x, y);
-				} else {
-					m_humdrum_text << "*";
-				}
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
-		}
-		m_humdrum_text << "\n";
+	if (startline < 0) {
+		// no zeroth measure;
+		return;
 	}
-
-	if (timesigQ) {
-		for (int i=0; i<infile[ii].getFieldCount(); i++) {
-			track = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].stimesig[track].x;
-			y  = outmeasures[index].stimesig[track].y;
-			xo = outmeasures[index-1].etimesig[track].x;
-			yo = outmeasures[index-1].etimesig[track].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					m_humdrum_text << infile.token(x, y);
-				} else {
-					m_humdrum_text << "*";
-				}
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
-		}
-		m_humdrum_text << "\n";
+	if (stopline < 0) {
+		// strange situation, no measure numbers
+		// consider what to do later...
+		return;
 	}
 
-	if (metQ) {
-		for (int i=0; i<infile[ii].getFieldCount(); i++) {
-			track = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].smet[track].x;
-			y  = outmeasures[index].smet[track].y;
-			xo = outmeasures[index-1].emet[track].x;
-			yo = outmeasures[index-1].emet[track].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					m_humdrum_text << infile.token(x, y);
-				} else {
-					m_humdrum_text << "*";
-				}
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
+	MeasureInfo current;
+	current.clear();
+	current.num = 0;
+	current.start = startline;
+	// current.start = exinterpline+1;
+	current.stop = stopline;
+	current.file = &infile;
+	measurelist.push_back(current);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_myank::expandMeasureOutList -- read the measure list for the sequence of measures
+//     to extract.
+//
+
+void Tool_myank::expandMeasureOutList(vector<MeasureInfo>& measureout,
+		vector<MeasureInfo>& measurein, HumdrumFile& infile,
+		const string& optionstring) {
+
+	HumRegex hre;
+	// find the largest measure number in the score
+	int maxmeasure = -1;
+	int minmeasure = -1;
+	for (int i=0; i<(int)measurein.size(); i++) {
+		if (maxmeasure < measurein[i].num) {
+			maxmeasure = measurein[i].num;
+		}
+		if ((minmeasure == -1) || (minmeasure > measurein[i].num)) {
+			minmeasure = measurein[i].num;
 		}
-		m_humdrum_text << "\n";
 	}
-
-	if (tempoQ) {
-		for (int i=0; i<infile[ii].getFieldCount(); i++) {
-			track = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].stempo[track].x;
-			y  = outmeasures[index].stempo[track].y;
-			xo = outmeasures[index-1].etempo[track].x;
-			yo = outmeasures[index-1].etempo[track].y;
-			if ((x>=0)&&(y>=0)&&(xo>=0)&&(yo>=0)) {
-				if (*infile.token(x, y) != *infile.token(xo, yo)) {
-					m_humdrum_text << infile.token(x, y);
+	if (maxmeasure <= 0 && !getBoolean("lines")) {
+		cerr << "Error: There are no measure numbers present in the data" << endl;
+		exit(1);
+	}
+	if (maxmeasure > 1123123) {
+		cerr << "Error: ridiculusly large measure number: " << maxmeasure << endl;
+		exit(1);
+	}
+	if (m_maxQ) {
+		if (measurein.size() == 0) {
+			m_humdrum_text << 0 << endl;
+		} else {
+			m_humdrum_text << maxmeasure << endl;
+		}
+		exit(0);
+	} else if (m_minQ) {
+		for (int ii=0; ii<infile.getLineCount(); ii++) {
+			if (infile[ii].isBarline()) {
+				if (hre.search(infile.token(ii, 0), "=\\d", "")) {
+					break;
 				} else {
-					m_humdrum_text << "*";
+					m_humdrum_text << 0 << endl;
+					exit(0);
 				}
-			} else {
-				m_humdrum_text << "*";
 			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
+			if (infile[ii].isData()) {
+				m_humdrum_text << 0 << endl;
+				exit(0);
 			}
 		}
-		m_humdrum_text << "\n";
+		if (measurein.size() == 0) {
+			m_humdrum_text << 0 << endl;
+		} else {
+			m_humdrum_text << minmeasure << endl;
+		}
+		exit(0);
+	}
+
+	// create reverse-lookup list
+	vector<int> inmap(maxmeasure+1);
+	fill(inmap.begin(), inmap.end(), -1);
+	for (int i=0; i<(int)measurein.size(); i++) {
+		inmap[measurein[i].num] = i;
+	}
+
+	fillGlobalDefaults(infile, measurein, inmap);
+	string ostring = optionstring;
+	removeDollarsFromString(ostring, maxmeasure);
+
+	if (m_debugQ) {
+		m_free_text << "Option string expanded: " << ostring << endl;
 	}
 
+	hre.replaceDestructive(ostring, "", "\\s+", "g");  // remove any spaces between items.
+	hre.replaceDestructive(ostring, "-", "--+", "g");  // remove extra dashes
+	int value = 0;
+	int start = 0;
+	vector<MeasureInfo>& range = measureout;
+	range.reserve(10000);
+	string searchexp = "^([\\d$-]+[^\\d$-]*)";
+	value = hre.search(ostring, searchexp);
+	while (value != 0) {
+		start += value - 1;
+		start += (int)hre.getMatch(1).size();
+		processFieldEntry(range, hre.getMatch(1), infile, maxmeasure, measurein, inmap);
+		value = hre.search(ostring, start, searchexp);
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::adjustGlobalInterpretationsStart --
+// Tool_myank::fillGlobalDefaults -- keep track of the clef, key signature, key, etc.
 //
 
-void Tool_myank::adjustGlobalInterpretationsStart(HumdrumFile& infile, int ii,
-		vector<MeasureInfo>& outmeasures, int index) {
-	if (index != 0) {
-		cerr << "Error in adjustGlobalInterpetationsStart" << endl;
-		exit(1);
-	}
-
-	int i;
-
-	int clefQ    = 0;
-	int keysigQ  = 0;
-	int keyQ     = 0;
-	int timesigQ = 0;
-	int metQ     = 0;
-	int tempoQ   = 0;
+void Tool_myank::fillGlobalDefaults(HumdrumFile& infile, vector<MeasureInfo>& measurein,
+		vector<int>& inmap) {
+	int i, j;
+	HumRegex hre;
 
-	int x, y;
+	int tracks = infile.getMaxTrack();
+	// cerr << "MAX TRACKS " << tracks << " ===============================" << endl;
 
-	// ignore the zeroth measure
-	// (may not be proper).
-// ggg
-	if (outmeasures[index].num == 0) {
-		return;
-	}
+	vector<MyCoord> currclef(tracks+1);
+	vector<MyCoord> currkeysig(tracks+1);
+	vector<MyCoord> currkey(tracks+1);
+	vector<MyCoord> currtimesig(tracks+1);
+	vector<MyCoord> currmet(tracks+1);
+	vector<MyCoord> currtempo(tracks+1);
 
-	int tracks = infile.getMaxTrack();
+	MyCoord undefMyCoord;
+	undefMyCoord.clear();
 
-	for (i=1; i<=tracks; i++) {
+	fill(currclef.begin(), currclef.end(), undefMyCoord);
+	fill(currkeysig.begin(), currkeysig.end(), undefMyCoord);
+	fill(currkey.begin(), currkey.end(), undefMyCoord);
+	fill(currtimesig.begin(), currtimesig.end(), undefMyCoord);
+	fill(currmet.begin(), currmet.end(), undefMyCoord);
+	fill(currtempo.begin(), currtempo.end(), undefMyCoord);
 
-		if (!clefQ) {
-			x  = outmeasures[index].sclef[i].x;
-			y  = outmeasures[index].sclef[i].y;
+	int currmeasure = -1;
+	int lastmeasure = -1;
+	int datafound   = 0;
+	int track;
+	int thingy = 0;
 
-			if ((x>=0)&&(y>=0)) {
-				clefQ = 1;
-			}
+	for (i=0; i<infile.getLineCount(); i++) {
+		if ((currmeasure == -1) && (thingy == 0) && infile[i].isData()) {
+			currmeasure = 0;
 		}
-
-		if (!keysigQ) {
-			x  = outmeasures[index].skeysig[i].x;
-			y  = outmeasures[index].skeysig[i].y;
-			if ((x>=0)&&(y>=0)) {
-				keysigQ = 1;
+		if (infile[i].isBarline()) {
+			if (!hre.search(infile.token(i, 0), "(\\d+)", "")) {
+				continue;
 			}
-		}
+			thingy = 1;
 
-		if (!keyQ) {
-			x  = outmeasures[index].skey[i].x;
-			y  = outmeasures[index].skey[i].y;
-			if ((x>=0)&&(y>=0)) {
-				keyQ = 1;
+			// store state of global music values at end of measure
+			if (currmeasure >= 0) {
+				measurein[inmap[currmeasure]].eclef    = currclef;
+				measurein[inmap[currmeasure]].ekeysig  = currkeysig;
+				measurein[inmap[currmeasure]].ekey     = currkey;
+				measurein[inmap[currmeasure]].etimesig = currtimesig;
+				measurein[inmap[currmeasure]].emet     = currmet;
+				measurein[inmap[currmeasure]].etempo   = currtempo;
 			}
-		}
 
-		if (!timesigQ) {
-			x  = outmeasures[index].stimesig[i].x;
-			y  = outmeasures[index].stimesig[i].y;
-			if ((x>=0)&&(y>=0)) {
-				timesigQ = 1;
-			}
-		}
+			lastmeasure = currmeasure;
+			currmeasure = hre.getMatchInt(1);
 
-		if (!metQ) {
-			x  = outmeasures[index].smet[i].x;
-			y  = outmeasures[index].smet[i].y;
-			if ((x>=0)&&(y>=0)) {
-				metQ = 1;
+			if (currmeasure < (int)inmap.size()) {
+				// [20120818] Had to compensate for last measure being single
+				// and un-numbered.
+				if (inmap[currmeasure] < 0) {
+					// [20111008] Had to compensate for "==85" barline
+					datafound = 0;
+					break;
+				}
+// cerr << "CURRCLEF: ";
+// for (int z=0; z<(int)currclef.size(); z++) {
+// cerr << "(" << currclef[z].x << "," << currclef[z].y << ") ";
+// }
+// cerr << endl;
+				measurein[inmap[currmeasure]].sclef    = currclef;
+				measurein[inmap[currmeasure]].skeysig  = currkeysig;
+				measurein[inmap[currmeasure]].skey     = currkey;
+				measurein[inmap[currmeasure]].stimesig = currtimesig;
+				// measurein[inmap[currmeasure]].smet     = metstates[i];
+				measurein[inmap[currmeasure]].smet     = currmet;
+				measurein[inmap[currmeasure]].stempo   = currtempo;
 			}
+
+			datafound   = 0;
+			continue;
 		}
+		if (infile[i].isInterpretation()) {
+			for (j=0; j<infile[i].getFieldCount(); j++) {
+				if (!infile.token(i, j)->isKern()) {
+					continue;
+				}
+				track = infile.token(i, j)->getTrack();
+
+				if ((datafound == 0) && (lastmeasure >= 0)) {
+					if (infile.token(i, j)->compare(0, 5, "*clef") == 0) {
+						measurein[inmap[currmeasure]].sclef[track].x = -1;
+						measurein[inmap[currmeasure]].sclef[track].y = -1;
+					} else if (hre.search(infile.token(i, j), "^\\*k\\[.*\\]", "")) {
+						measurein[inmap[currmeasure]].skeysig[track].x = -1;
+						measurein[inmap[currmeasure]].skeysig[track].y = -1;
+					} else if (hre.search(infile.token(i, j), "^\\*[A-G][#-]?:", "i")) {
+						measurein[inmap[currmeasure]].skey[track].x = -1;
+						measurein[inmap[currmeasure]].skey[track].y = -1;
+					} else if (hre.search(infile.token(i, j), R"(^\*M\d+/\d+)")) {
+						measurein[inmap[currmeasure]].stimesig[track].x = -1;
+						measurein[inmap[currmeasure]].stimesig[track].y = -1;
+					} else if (hre.search(infile.token(i, j), R"(^\*met\(.*\))")) {
+						measurein[inmap[currmeasure]].smet[track].x = -1;
+						measurein[inmap[currmeasure]].smet[track].y = -1;
+					} else if (hre.search(infile.token(i, j), "^\\*MM\\d+", "i")) {
+						measurein[inmap[currmeasure]].stempo[track].x = -1;
+						measurein[inmap[currmeasure]].stempo[track].y = -1;
+					}
+				}
+
+				if (infile.token(i, j)->compare(0, 5, "*clef") == 0) {
+					currclef[track].x = i;
+					currclef[track].y = j;
+					continue;
+				}
+				if (hre.search(infile.token(i, j), R"(^\*k\[.*\])")) {
+					currkeysig[track].x = i;
+					currkeysig[track].y = j;
+					continue;
+				}
+				if (hre.search(infile.token(i, j), "^\\*[A-G][#-]?:", "i")) {
+					currkey[track].x = i;
+					currkey[track].y = j;
+					continue;
+				}
+				if (hre.search(infile.token(i, j), R"(^\*M\d+/\d+)")) {
+					currtimesig[track].x = i;
+					currtimesig[track].y = j;
+					continue;
+				}
+				if (hre.search(infile.token(i, j), R"(^\*met\(.*\))")) {
+					currmet[track].x = i;
+					currmet[track].y = j;
+					continue;
+				}
+				if (hre.search(infile.token(i, j), R"(^\*MM[\d.]+)")) {
+					currtempo[track].x = i;
+					currtempo[track].y = j;
+					continue;
+				}
 
-		if (!tempoQ) {
-			x  = outmeasures[index].stempo[i].x;
-			y  = outmeasures[index].stempo[i].y;
-			if ((x>=0)&&(y>=0)) {
-				tempoQ = 1;
 			}
 		}
+		if (infile[i].isData()) {
+			datafound = 1;
+		}
 	}
 
-	int ptrack;
+	// store state of global music values at end of music
+	if ((currmeasure >= 0) && (currmeasure < (int)inmap.size())
+			&& (inmap[currmeasure] >= 0)) {
+		measurein[inmap[currmeasure]].eclef    = currclef;
+		measurein[inmap[currmeasure]].ekeysig  = currkeysig;
+		measurein[inmap[currmeasure]].ekey     = currkey;
+		measurein[inmap[currmeasure]].etimesig = currtimesig;
+		measurein[inmap[currmeasure]].emet     = currmet;
+		measurein[inmap[currmeasure]].etempo   = currtempo;
+	}
 
-	if (clefQ) {
-		for (i=0; i<infile[ii].getFieldCount(); i++) {
-			ptrack = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].sclef[ptrack].x;
-			y  = outmeasures[index].sclef[ptrack].y;
-			if ((x>=0)&&(y>=0)) {
-				m_humdrum_text << infile.token(x, y);
-			} else {
-				m_humdrum_text << "*";
+	// go through the measure list and clean up start/end states
+	for (i=0; i<(int)measurein.size()-2; i++) {
+
+		if (measurein[i].sclef.size() == 0) {
+			measurein[i].sclef.resize(tracks+1);
+			fill(measurein[i].sclef.begin(), measurein[i].sclef.end(), undefMyCoord);
+		}
+		if (measurein[i].eclef.size() == 0) {
+			measurein[i].eclef.resize(tracks+1);
+			fill(measurein[i].eclef.begin(), measurein[i].eclef.end(), undefMyCoord);
+		}
+		if (measurein[i+1].sclef.size() == 0) {
+			measurein[i+1].sclef.resize(tracks+1);
+			fill(measurein[i+1].sclef.begin(), measurein[i+1].sclef.end(), undefMyCoord);
+		}
+		if (measurein[i+1].eclef.size() == 0) {
+			measurein[i+1].eclef.resize(tracks+1);
+			fill(measurein[i+1].eclef.begin(), measurein[i+1].eclef.end(), undefMyCoord);
+		}
+		for (j=1; j<(int)measurein[i].sclef.size(); j++) {
+			if (!measurein[i].eclef[j].isValid()) {
+				if (measurein[i].sclef[j].isValid()) {
+					measurein[i].eclef[j] = measurein[i].sclef[j];
+				}
 			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
+			if (!measurein[i+1].sclef[j].isValid()) {
+				if (measurein[i].eclef[j].isValid()) {
+					measurein[i+1].sclef[j] = measurein[i].eclef[j];
+				}
 			}
 		}
-		m_humdrum_text << "\n";
-	}
 
-	if (keysigQ) {
-		for (i=0; i<infile[ii].getFieldCount(); i++) {
-			ptrack = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].skeysig[ptrack].x;
-			y  = outmeasures[index].skeysig[ptrack].y;
-			if ((x>=0)&&(y>=0)) {
-				m_humdrum_text << infile.token(x, y);
-			} else {
-				m_humdrum_text << "*";
+		if (measurein[i].skeysig.size() == 0) {
+			measurein[i].skeysig.resize(tracks+1);
+			fill(measurein[i].skeysig.begin(), measurein[i].skeysig.end(), undefMyCoord);
+		}
+		if (measurein[i].ekeysig.size() == 0) {
+			measurein[i].ekeysig.resize(tracks+1);
+			fill(measurein[i].ekeysig.begin(), measurein[i].ekeysig.end(), undefMyCoord);
+		}
+		if (measurein[i+1].skeysig.size() == 0) {
+			measurein[i+1].skeysig.resize(tracks+1);
+			fill(measurein[i+1].skeysig.begin(), measurein[i+1].skeysig.end(), undefMyCoord);
+		}
+		if (measurein[i+1].ekeysig.size() == 0) {
+			measurein[i+1].ekeysig.resize(tracks+1);
+			fill(measurein[i+1].ekeysig.begin(), measurein[i+1].ekeysig.end(), undefMyCoord);
+		}
+		for (j=1; j<(int)measurein[i].skeysig.size(); j++) {
+			if (!measurein[i].ekeysig[j].isValid()) {
+				if (measurein[i].skeysig[j].isValid()) {
+					measurein[i].ekeysig[j] = measurein[i].skeysig[j];
+				}
 			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
+			if (!measurein[i+1].skeysig[j].isValid()) {
+				if (measurein[i].ekeysig[j].isValid()) {
+					measurein[i+1].skeysig[j] = measurein[i].ekeysig[j];
+				}
 			}
 		}
-		m_humdrum_text << "\n";
-	}
 
-	if (keyQ) {
-		for (i=0; i<infile[ii].getFieldCount(); i++) {
-			ptrack = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].skey[ptrack].x;
-			y  = outmeasures[index].skey[ptrack].y;
-			if ((x>=0)&&(y>=0)) {
-				m_humdrum_text << infile.token(x, y);
-			} else {
-				m_humdrum_text << "*";
+		if (measurein[i].skey.size() == 0) {
+			measurein[i].skey.resize(tracks+1);
+			fill(measurein[i].skey.begin(), measurein[i].skey.end(), undefMyCoord);
+		}
+		if (measurein[i].ekey.size() == 0) {
+			measurein[i].ekey.resize(tracks+1);
+			fill(measurein[i].ekey.begin(), measurein[i].ekey.end(), undefMyCoord);
+		}
+		if (measurein[i+1].skey.size() == 0) {
+			measurein[i+1].skey.resize(tracks+1);
+			fill(measurein[i+1].skey.begin(), measurein[i+1].skey.end(), undefMyCoord);
+		}
+		if (measurein[i+1].ekey.size() == 0) {
+			measurein[i+1].ekey.resize(tracks+1);
+			fill(measurein[i+1].ekey.begin(), measurein[i+1].ekey.end(), undefMyCoord);
+		}
+		for (j=1; j<(int)measurein[i].skey.size(); j++) {
+			if (!measurein[i].ekey[j].isValid()) {
+				if (measurein[i].skey[j].isValid()) {
+					measurein[i].ekey[j] = measurein[i].skey[j];
+				}
 			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
+			if (!measurein[i+1].skey[j].isValid()) {
+				if (measurein[i].ekey[j].isValid()) {
+					measurein[i+1].skey[j] = measurein[i].ekey[j];
+				}
 			}
 		}
-		m_humdrum_text << "\n";
-	}
 
-	if (timesigQ) {
-		for (i=0; i<infile[ii].getFieldCount(); i++) {
-			ptrack = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].stimesig[ptrack].x;
-			y  = outmeasures[index].stimesig[ptrack].y;
-			if ((x>=0)&&(y>=0)) {
-				m_humdrum_text << infile.token(x, y);
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
+		if (measurein[i].stimesig.size() == 0) {
+			measurein[i].stimesig.resize(tracks+1);
+			fill(measurein[i].stimesig.begin(), measurein[i].stimesig.end(), undefMyCoord);
 		}
-		m_humdrum_text << "\n";
-	}
-	if (metQ) {
-		for (i=0; i<infile[ii].getFieldCount(); i++) {
-			ptrack = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].smet[ptrack].x;
-			y  = outmeasures[index].smet[ptrack].y;
-			if ((x>=0)&&(y>=0)) {
-				m_humdrum_text << infile.token(x, y);
-			} else {
-				m_humdrum_text << "*";
+		if (measurein[i].etimesig.size() == 0) {
+			measurein[i].etimesig.resize(tracks+1);
+			fill(measurein[i].etimesig.begin(), measurein[i].etimesig.end(), undefMyCoord);
+		}
+		if (measurein[i+1].stimesig.size() == 0) {
+			measurein[i+1].stimesig.resize(tracks+1);
+			fill(measurein[i+1].stimesig.begin(), measurein[i+1].stimesig.end(), undefMyCoord);
+		}
+		if (measurein[i+1].etimesig.size() == 0) {
+			measurein[i+1].etimesig.resize(tracks+1);
+			fill(measurein[i+1].etimesig.begin(), measurein[i+1].etimesig.end(), undefMyCoord);
+		}
+		for (j=1; j<(int)measurein[i].stimesig.size(); j++) {
+			if (!measurein[i].etimesig[j].isValid()) {
+				if (measurein[i].stimesig[j].isValid()) {
+					measurein[i].etimesig[j] = measurein[i].stimesig[j];
+				}
 			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
+			if (!measurein[i+1].stimesig[j].isValid()) {
+				if (measurein[i].etimesig[j].isValid()) {
+					measurein[i+1].stimesig[j] = measurein[i].etimesig[j];
+				}
 			}
 		}
-		m_humdrum_text << "\n";
-	}
 
-	if (tempoQ) {
-		for (i=0; i<infile[ii].getFieldCount(); i++) {
-			ptrack = infile.token(ii, i)->getTrack();
-			x  = outmeasures[index].stempo[ptrack].x;
-			y  = outmeasures[index].stempo[ptrack].y;
-			if ((x>=0)&&(y>=0)) {
-				m_humdrum_text << infile.token(x, y);
-			} else {
-				m_humdrum_text << "*";
+		if (measurein[i].smet.size() == 0) {
+			measurein[i].smet.resize(tracks+1);
+			fill(measurein[i].smet.begin(), measurein[i].smet.end(), undefMyCoord);
+		}
+		if (measurein[i].emet.size() == 0) {
+			measurein[i].emet.resize(tracks+1);
+			fill(measurein[i].emet.begin(), measurein[i].emet.end(), undefMyCoord);
+		}
+		if (measurein[i+1].smet.size() == 0) {
+			measurein[i+1].smet.resize(tracks+1);
+			fill(measurein[i+1].smet.begin(), measurein[i+1].smet.end(), undefMyCoord);
+		}
+		if (measurein[i+1].emet.size() == 0) {
+			measurein[i+1].emet.resize(tracks+1);
+			fill(measurein[i+1].emet.begin(), measurein[i+1].emet.end(), undefMyCoord);
+		}
+		for (j=1; j<(int)measurein[i].smet.size(); j++) {
+			if (!measurein[i].emet[j].isValid()) {
+				if (measurein[i].smet[j].isValid()) {
+					measurein[i].emet[j] = measurein[i].smet[j];
+				}
 			}
-			if (i < infile[ii].getFieldCount()-1) {
-				m_humdrum_text << "\t";
+			if (!measurein[i+1].smet[j].isValid()) {
+				if (measurein[i].emet[j].isValid()) {
+					measurein[i+1].smet[j] = measurein[i].emet[j];
+				}
 			}
 		}
-		m_humdrum_text << "\n";
-	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_myank::printDataLine -- Print line with data tokens of selected section
-//
 
-void Tool_myank::printDataLine(HLp line,
-		bool& startLineHandled,
-		const vector<int>& lastLineResolvedTokenLineIndex,
-		const vector<HumNum>& lastLineDurationsFromNoteStart) {
-	bool lineChange = false;
-	string recipRegex = R"re(([\d%.]+))re";
-	// Handle cutting the previeous token of a note that hangs into the selected
-	// section
-	if (startLineHandled == false) {
-		if (line->isData()) {
-			vector<HTp> tokens;
-			line->getTokens(tokens);
-			for (HTp token : tokens) {
-				if (token->isKern() && token->isNull()) {
-					HTp resolvedToken = token->resolveNull();
-					if (resolvedToken->isNull()) {
-						continue;
-					}
-					HumRegex hre;
-					string recip = Convert::durationToRecip(token->getDurationToNoteEnd());
-					vector<string> subtokens = resolvedToken->getSubtokens();
-					string tokenText;
-					for (int i=0; i<(int)subtokens.size(); i++) {
-						if (hre.search(subtokens[i], recipRegex)) {
-							string before = hre.getPrefix();
-							string after = hre.getSuffix();
-							hre.replaceDestructive(after, "", recipRegex, "g");
-							string subtokenText;
-							// Replace the old duration with the clipped one
-							subtokenText += before + recip + after;
-							// Add a tie end if not already in a tie group
-							if (!hre.search(subtokens[i], "[_\\]]")) {
-									subtokenText += "]";
-							}
-							tokenText += subtokenText;
-							if (i < (int)subtokens.size() - 1) {
-								tokenText += " ";
-							}
-						}
-					}
-					token->setText(tokenText);
-					lineChange = true;
+		if (measurein[i].stempo.size() == 0) {
+			measurein[i].stempo.resize(tracks+1);
+			fill(measurein[i].stempo.begin(), measurein[i].stempo.end(), undefMyCoord);
+		}
+		if (measurein[i].etempo.size() == 0) {
+			measurein[i].etempo.resize(tracks+1);
+			fill(measurein[i].etempo.begin(), measurein[i].etempo.end(), undefMyCoord);
+		}
+		if (measurein[i+1].stempo.size() == 0) {
+			measurein[i+1].stempo.resize(tracks+1);
+			fill(measurein[i+1].stempo.begin(), measurein[i+1].stempo.end(), undefMyCoord);
+		}
+		if (measurein[i+1].etempo.size() == 0) {
+			measurein[i+1].etempo.resize(tracks+1);
+			fill(measurein[i+1].etempo.begin(), measurein[i+1].etempo.end(), undefMyCoord);
+		}
+		for (j=1; j<(int)measurein[i].stempo.size(); j++) {
+			if (!measurein[i].etempo[j].isValid()) {
+				if (measurein[i].stempo[j].isValid()) {
+					measurein[i].etempo[j] = measurein[i].stempo[j];
 				}
 			}
-			startLineHandled = true;
-		}
-	// Handle cutting the last attacked note of the selected section
-	} else {
-		// Check if line has a note that needs to be handled
-		if (find(lastLineResolvedTokenLineIndex.begin(), lastLineResolvedTokenLineIndex.end(), line->getLineIndex()) !=
-				lastLineResolvedTokenLineIndex.end()) {
-			for (int i = 0; i < line->getTokenCount(); i++) {
-				HTp token = line->token(i);
-				// Check if token need the be handled and is of type **kern
-				if (token->isKern() && (lastLineResolvedTokenLineIndex[i] == line->getLineIndex())) {
-					HTp resolvedToken = token->resolveNull();
-					if (resolvedToken->isNull()) {
-						continue;
-					}
-					HumNum dur = lastLineDurationsFromNoteStart[i];
-					HumRegex hre;
-					string recip = Convert::durationToRecip(dur);
-					vector<string> subtokens = resolvedToken->getSubtokens();
-					for (int i=0; i<(int)subtokens.size(); i++) {
-						if (hre.search(subtokens[i], recipRegex)) {
-							string before = hre.getPrefix();
-							string after = hre.getSuffix();
-							hre.replaceDestructive(after, "", recipRegex, "g");
-							string subtokenText;
-							if (resolvedToken->getDuration() > dur) {
-								// Add a tie start if not already in a tie group
-								if (!hre.search(subtokens[i], "[_\\[]")) {
-										subtokenText += "[";
-								}
-							}
-							// Replace the old duration with the clipped one
-							subtokenText += before + recip + after;
-							token->replaceSubtoken(i, subtokenText);
-							lineChange = true;
-						}
-					}
+			if (!measurein[i+1].stempo[j].isValid()) {
+				if (measurein[i].etempo[j].isValid()) {
+					measurein[i+1].stempo[j] = measurein[i].etempo[j];
 				}
 			}
 		}
 	}
-	if (lineChange) {
-		line->createLineFromTokens();
-	}
-	m_humdrum_text << line << "\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::printMeasureStart -- print a starting measure of a segment.
+// Tool_myank::processFieldEntry --
+//   3-6 expands to 3 4 5 6
+//   $   expands to maximum spine track
+//   $0  expands to maximum spine track
+//   $1  expands to maximum spine track minus 1, etc.
+//   2-$1 expands to 2 through the maximum minus one.
+//   6-3 expands to 6 5 4 3
+//   $2-5 expands to the maximum minus 2 down through 5.
+//   Ignore negative values and values which exceed the maximum value.
 //
 
-void Tool_myank::printMeasureStart(HumdrumFile& infile, int line, const string& style) {
-	if (!infile[line].isBarline()) {
-		m_humdrum_text << infile[line] << "\n";
-		return;
-	}
+void Tool_myank::processFieldEntry(vector<MeasureInfo>& field,
+		const string& str, HumdrumFile& infile, int maxmeasure,
+		vector<MeasureInfo>& inmeasures, vector<int>& inmap) {
+
+	MeasureInfo current;
 
 	HumRegex hre;
-	int j;
-	for (j=0; j<infile[line].getFieldCount(); j++) {
-		if (hre.search(infile.token(line, j), "=(\\d*)(.*)", "")) {
-			if (style == "==") {
-				m_humdrum_text << "==";
-				m_humdrum_text << hre.getMatch(1);
-			} else {
-				m_humdrum_text << "=";
-				m_humdrum_text << hre.getMatch(1);
-				m_humdrum_text << style;
+	string buffer = str;
+
+	// remove any comma left at end of input string (or anywhere else)
+	hre.replaceDestructive(buffer, "", ",", "g");
+
+	string measureStyling = "";
+	if (hre.search(buffer, "([|:!=]+)$")) {
+		measureStyling = hre.getMatch(1);
+		hre.replaceDestructive(buffer, "", "([|:!=]+)$");
+	}
+
+	if (hre.search(buffer, "^(\\d+)[a-z]?-(\\d+)[a-z]?$")) {
+		// processing a measure range
+		int firstone = hre.getMatchInt(1);
+		int lastone  = hre.getMatchInt(2);
+
+		// limit the range to 0 to maxmeasure
+		if (firstone > maxmeasure) { firstone = maxmeasure; }
+		if (lastone  > maxmeasure) { lastone  = maxmeasure; }
+		if (firstone < 0         ) { firstone = 0         ; }
+		if (lastone  < 0         ) { lastone  = 0         ; }
+
+		if ((firstone < 1) && (firstone != 0)) {
+			cerr << "Error: range token: \"" << str << "\""
+				  << " contains too small a number at start: " << firstone << endl;
+			cerr << "Minimum number allowed is " << 1 << endl;
+			exit(1);
+		}
+		if ((lastone < 1) && (lastone != 0)) {
+			cerr << "Error: range token: \"" << str << "\""
+				  << " contains too small a number at end: " << lastone << endl;
+			cerr << "Minimum number allowed is " << 1 << endl;
+			exit(1);
+		}
+
+		if (firstone > lastone) {
+			// reverse the order of the measures
+			for (int i=firstone; i>=lastone; i--) {
+				if (inmap[i] >= 0) {
+					current.clear();
+					current.file = &infile;
+					current.num = i;
+					current.start = inmeasures[inmap[i]].start;
+					current.stop = inmeasures[inmap[i]].stop;
+
+					current.sclef    = inmeasures[inmap[i]].sclef;
+					current.skeysig  = inmeasures[inmap[i]].skeysig;
+					current.skey     = inmeasures[inmap[i]].skey;
+					current.stimesig = inmeasures[inmap[i]].stimesig;
+					current.smet     = inmeasures[inmap[i]].smet;
+					current.stempo   = inmeasures[inmap[i]].stempo;
+
+					current.eclef    = inmeasures[inmap[i]].eclef;
+					current.ekeysig  = inmeasures[inmap[i]].ekeysig;
+					current.ekey     = inmeasures[inmap[i]].ekey;
+					current.etimesig = inmeasures[inmap[i]].etimesig;
+					current.emet     = inmeasures[inmap[i]].emet;
+					current.etempo   = inmeasures[inmap[i]].etempo;
+
+					field.push_back(current);
+				}
 			}
 		} else {
-			if (style == "==") {
-				m_humdrum_text << "==";
-			} else {
-				m_humdrum_text << "=" << style;
+			// measure range not reversed
+			for (int i=firstone; i<=lastone; i++) {
+				if (inmap[i] >= 0) {
+					current.clear();
+					current.file = &infile;
+					current.num = i;
+					current.start = inmeasures[inmap[i]].start;
+					current.stop = inmeasures[inmap[i]].stop;
+
+					current.sclef    = inmeasures[inmap[i]].sclef;
+					current.skeysig  = inmeasures[inmap[i]].skeysig;
+					current.skey     = inmeasures[inmap[i]].skey;
+					current.stimesig = inmeasures[inmap[i]].stimesig;
+					current.smet     = inmeasures[inmap[i]].smet;
+					current.stempo   = inmeasures[inmap[i]].stempo;
+
+					current.eclef    = inmeasures[inmap[i]].eclef;
+					current.ekeysig  = inmeasures[inmap[i]].ekeysig;
+					current.ekey     = inmeasures[inmap[i]].ekey;
+					current.etimesig = inmeasures[inmap[i]].etimesig;
+					current.emet     = inmeasures[inmap[i]].emet;
+					current.etempo   = inmeasures[inmap[i]].etempo;
+
+					field.push_back(current);
+				}
 			}
 		}
-		if (j < infile[line].getFieldCount()-1) {
-			m_humdrum_text << "\t";
+	} else if (hre.search(buffer, "^(\\d+)([a-z]*)")) {
+		// processing a single measure number
+		int value = hre.getMatchInt(1);
+		// do something with letter later...
+
+		if ((value < 1) && (value != 0)) {
+			cerr << "Error: range token: \"" << str << "\""
+				  << " contains too small a number at end: " << value << endl;
+			cerr << "Minimum number allowed is " << 1 << endl;
+			exit(1);
 		}
-	}
-	m_humdrum_text << "\n";
+		if (inmap[value] >= 0) {
+			current.clear();
+			current.file = &infile;
+			current.num = value;
+			current.start = inmeasures[inmap[value]].start;
+			current.stop = inmeasures[inmap[value]].stop;
 
-	if (m_barnumtextQ) {
-		int barline = 0;
-		sscanf(infile.token(line, 0)->c_str(), "=%d", &barline);
-		if (barline > 0) {
-			m_humdrum_text << "!!LO:TX:Z=20:X=-25:t=" << barline << endl;
+			current.sclef    = inmeasures[inmap[value]].sclef;
+			current.skeysig  = inmeasures[inmap[value]].skeysig;
+			current.skey     = inmeasures[inmap[value]].skey;
+			current.stimesig = inmeasures[inmap[value]].stimesig;
+			current.smet     = inmeasures[inmap[value]].smet;
+			current.stempo   = inmeasures[inmap[value]].stempo;
+
+			current.eclef    = inmeasures[inmap[value]].eclef;
+			current.ekeysig  = inmeasures[inmap[value]].ekeysig;
+			current.ekey     = inmeasures[inmap[value]].ekey;
+			current.etimesig = inmeasures[inmap[value]].etimesig;
+			current.emet     = inmeasures[inmap[value]].emet;
+			current.etempo   = inmeasures[inmap[value]].etempo;
+
+			field.push_back(current);
 		}
 	}
+
+	field.back().stopStyle = measureStyling;
+
 }
 
 
+
 //////////////////////////////
 //
-// Tool_myank::printDoubleBarline --
+// Tool_myank::removeDollarsFromString -- substitute $ sign for maximum track count.
 //
 
-void Tool_myank::printDoubleBarline(HumdrumFile& infile, int line) {
+void Tool_myank::removeDollarsFromString(string& buffer, int maxx) {
+	HumRegex hre;
+	HumRegex hre2;
+	string tbuf;
+	string obuf;
+	int outval;
+	int value;
 
-	if (!infile[line].isBarline()) {
-		m_humdrum_text << infile[line] << "\n";
-		return;
+	if (m_debugQ) {
+		m_free_text << "MEASURE STRING BEFORE DOLLAR REMOVAL: " << buffer << endl;
 	}
 
-	HumRegex hre;
-	int j;
-	for (j=0; j<infile[line].getFieldCount(); j++) {
-		if (hre.search(infile.token(line, j), "(=\\d*)(.*)", "")) {
-			m_humdrum_text << hre.getMatch(1);
-			m_humdrum_text << "||";
+	while (hre.search(buffer, "(\\$\\d*)", "")) {
+		tbuf = hre.getMatch(1);
+		if (hre2.search(tbuf, "(\\$\\d+)")) {
+		  sscanf(hre2.getMatch(1).c_str(), "$%d", &value);
+		  outval = maxx - value;
 		} else {
-			m_humdrum_text << "=||";
-		}
-		if (j < infile[line].getFieldCount()-1) {
-			m_humdrum_text << "\t";
+			outval = maxx;
 		}
-	}
-	m_humdrum_text << "\n";
 
-	if (m_barnumtextQ) {
-		int barline = 0;
-		sscanf(infile.token(line, 0)->c_str(), "=%d", &barline);
-		if (barline > 0) {
-			m_humdrum_text << "!!LO:TX:Z=20:X=-25:t=" << barline << endl;
+		if (outval < 0) {
+			outval = 0;
 		}
+
+		tbuf = to_string(outval);
+		obuf = "\\";
+		obuf += hre.getMatch(1);
+		hre.replaceDestructive(buffer, tbuf, obuf);
+	}
+	if (m_debugQ) {
+		m_free_text << "DOLLAR EXPAND: " << buffer << endl;
 	}
+}
+
+
+
+
+
+
+//////////////////////////////
+//
+// Tool_myank::example -- example function calls to the program.
+//
+
+void Tool_myank::example(void) {
+
 
 }
 
@@ -110525,311 +114431,617 @@ void Tool_myank::printDoubleBarline(HumdrumFile& infile, int line) {
 
 //////////////////////////////
 //
-// Tool_myank::printInvisibleMeasure --
+// Tool_myank::usage -- command-line usage description and brief summary
 //
 
-void Tool_myank::printInvisibleMeasure(HumdrumFile& infile, int line) {
-	if (!infile[line].isBarline()) {
-		m_humdrum_text << infile[line] << "\n";
-		return;
+void Tool_myank::usage(const string& ommand) {
+
+}
+
+
+
+
+/////////////////////////////////
+//
+// Tool_nproof::Tool_nproof -- Set the recognized options for the tool.
+//
+
+Tool_nproof::Tool_nproof(void) {
+	define("B|no-blank|no-blanks=b",                 "do not check for blank lines.\n");
+	define("b|only-blank|only-blanks=b",             "only check for blank lines.\n");
+
+	define("I|no-instrument|no-instruments=b",       "do not check instrument interpretations.\n");
+	define("i|only-instrument|only-instruments=b",   "only check instrument interpretations.\n");
+
+	define("K|no-key=b",                             "do not check for !!!key: manual initial key designation.\n");
+	define("k|only-key=b",                           "only check for !!!key: manual initial key designation.\n");
+
+	define("R|no-reference=b",                       "do not check for reference records.\n");
+	define("r|only-reference=b",                     "only check for reference records.\n");
+
+	define("T|no-termination|no-terminations=b",     "do not check spine terminations.\n");
+	define("t|only-termination|only-terminations=b", "only check spine terminations.\n");
+
+	define("file|filename=b",                        "print filename with raw count (if available).\n");
+	define("raw=b",                                  "only print error count.\n");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_nproof::run -- Do the main work of the tool.
+//
+
+bool Tool_nproof::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
+}
 
-	HumRegex hre;
-	int j;
-	for (j=0; j<infile[line].getFieldCount(); j++) {
-		if (infile.token(line, j)->find('-') != string::npos) {
-			m_humdrum_text << infile.token(line, j);
-			if (j < infile[line].getFieldCount()-1) {
-				m_humdrum_text << "\t";
-			}
-			continue;
-		}
-		if (hre.search(infile.token(line, j), "(=\\d*)(.*)", "")) {
-			m_humdrum_text << hre.getMatch(1);
-			// m_humdrum_text << "-";
-			m_humdrum_text << hre.getMatch(2);
-		} else {
-			m_humdrum_text << infile.token(line, j);
-		}
-		if (j < infile[line].getFieldCount()-1) {
-			m_humdrum_text << "\t";
-		}
+
+bool Tool_nproof::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	m_humdrum_text << "\n";
+	return status;
+}
+
+
+bool Tool_nproof::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_nproof::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::reconcileSpineBoundary -- merge spines correctly between segments.
-//    will not be able to handle all permutations of spine manipulators.
-//    So don't expect exotic manipulators to work...
+// Tool_nproof::initialize --
 //
 
-void Tool_myank::reconcileSpineBoundary(HumdrumFile& infile, int index1, int index2) {
+void Tool_nproof::initialize(void) {
+	m_noblankQ       = getBoolean("no-blank");
+	m_noinstrumentQ  = getBoolean("no-instrument");
+	m_nokeyQ         = getBoolean("no-key");
+	m_noreferenceQ   = getBoolean("no-reference");
+	m_noterminationQ = getBoolean("no-termination");
 
-	if (m_debugQ) {
-		m_humdrum_text << "RECONCILING LINES " << index1+1 << " and " << index2+1 << endl;
-		m_humdrum_text << "FIELD COUNT OF " << index1+1 << " is "
-			            << infile[index1].getFieldCount() << endl;
-		m_humdrum_text << "FIELD COUNT OF " << index2+1 << " is "
-			            << infile[index2].getFieldCount() << endl;
-	}
+	bool onlyBlank       = getBoolean("only-blank");
+	bool onlyInstrument  = getBoolean("only-instrument");
+	bool onlyKey         = getBoolean("only-key");
+	bool onlyReference   = getBoolean("only-reference");
+	bool onlyTermination = getBoolean("only-termination");
 
-	// check to see if any changes need reconciling; otherwise, exit function
-	int i, j;
-	if (infile[index1].getFieldCount() == infile[index2].getFieldCount()) {
-		int same = 1;
-		for (i=0; i<infile[index1].getFieldCount(); i++) {
-			if (infile.token(index1,i)->getSpineInfo() != infile.token(index2, i)->getSpineInfo()) {
-				same = 0;
-			}
-		}
-		if (same != 0) {
-			return;
-		}
+	if (onlyBlank || onlyInstrument || onlyKey || onlyReference || onlyTermination) {
+		m_noblankQ       = !onlyBlank;
+		m_noinstrumentQ  = !onlyInstrument;
+		m_nokeyQ         = !onlyKey;
+		m_noreferenceQ   = !onlyReference;
+		m_noterminationQ = !onlyTermination;
 	}
 
-	// handle splits all at once
-	string buff1;
-	string buff2;
+	m_fileQ          = getBoolean("file");
+	m_rawQ           = getBoolean("raw");
+}
 
-	vector<int> splits(infile[index1].getFieldCount());
-	fill(splits.begin(), splits.end(), 0);
 
-	int hassplit = 0;
-	for (i=0; i<infile[index1].getFieldCount(); i++) {
-		buff1 = "(";
-		buff1 += infile.token(index1, i)->getSpineInfo();
-		buff1 += ")";
-		buff2 = buff1;
-		buff1 += "a";
-		buff2 += "b";
-		for (j=0; j<infile[index2].getFieldCount()-1; j++) {
-			if ((buff1 == infile.token(index2, j)->getSpineInfo()
-					&& (buff2 == infile.token(index2,j+1)->getSpineInfo()))) {
-				splits[i] = 1;
-				hassplit++;
-			}
-		}
-	}
 
-	if (hassplit) {
-		for (i=0; i<(int)splits.size(); i++) {
-			if (splits[i]) {
-				m_humdrum_text << "*^";
-			} else {
-				m_humdrum_text << '*';
-			}
-			if (i < (int)splits.size()-1) {
-				m_humdrum_text << '\t';
-			}
-		}
-		m_humdrum_text << '\n';
-	}
+//////////////////////////////
+//
+// Tool_nproof::processFile --
+//
 
-	// make splits cumulative;
-	//for (i=1; i<(int)splits.size(); i++) {
-	//   splits[i] += splits[i-1];
-	//}
+void Tool_nproof::processFile(HumdrumFile& infile) {
+	m_errorCount = 0;
+	m_errorList = "";
+	m_errorHtml = "";
 
-	HumRegex hre1;
-	HumRegex hre2;
-	// handle joins one at a time, only look for binary joins at the moment.
-	// assuming that no *x has been used to mix the voices up.
-	for (i=0; i<infile[index1].getFieldCount()-1; i++) {
-		if (!hre1.search(infile.token(index1, i)->getSpineInfo(), "\\((.*)\\)a")) {
-			continue;
-		}
-		if (!hre2.search(infile.token(index1, i+1)->getSpineInfo(), "\\((.*)\\)b")) {
-			continue;
-		}
-		if (hre1.getMatch(1) != hre2.getMatch(1)) {
-			// spines are not split from same source
-			continue;
-		}
+	if (!m_noblankQ) {
+		checkForBlankLines(infile);
+	}
+	if (!m_nokeyQ) {
+		checkKeyInformation(infile);
+	}
+	if (!m_noinstrumentQ) {
+		checkInstrumentInformation(infile);
+	}
+	if (!m_noreferenceQ) {
+		checkReferenceRecords(infile);
+	}
+	if (!m_noterminationQ) {
+		checkSpineTerminations(infile);
+	}
 
-		// found an "a" and "b" portion of a spine split, now search
-		// through the target line for a joint of those two sub-spines
-		for (j=0; j<infile[index2].getFieldCount(); j++) {
-			if (infile.token(index2, j)->getSpineInfo() != hre1.getMatch(1)) {
-				continue;
-			}
-			// found a simple binary spine join: emmit a spine manipulator line
-			printJoinLine(splits, i, 2);
+	m_humdrum_text << infile;
+
+	if (m_rawQ) {
+		// print error count only.
+		if (m_fileQ) {
+			m_free_text << infile.getFilename() << "\t";
 		}
+		m_free_text << m_errorCount << endl;
+		return;
 	}
 
-	// handle *x switches, not perfect since ordering might need to be
-	// handled between manipulators...
-
+	if (m_errorCount > 0) {
+		m_humdrum_text << m_errorList;
+		m_humdrum_text << "!!!TOOL-nproof-error-count: " << m_errorCount << endl;
+		m_humdrum_text << "!!@@BEGIN: PREHTML\n";
+		m_humdrum_text << "!!@TOOL: nproof\n";
+		m_humdrum_text << "!!@CONTENT:\n";
+		m_humdrum_text << "!! <h2 style='color:red'> @{TOOL-nproof-error-count} problem";
+		if (m_errorCount != 1) {
+			m_humdrum_text << "s";
+		}
+		m_humdrum_text << " detected </h2>\n";
+		m_humdrum_text << "!! <ul style='color:darkred'>\n";
+		m_humdrum_text << m_errorHtml;
+		m_humdrum_text << "!! </ul>\n";
+		m_humdrum_text << "!!@@END: PREHTML\n";
+	} else {
+		m_humdrum_text << "!!@@BEGIN: PREHTML\n";
+		m_humdrum_text << "!!@TOOL: nproof\n";
+		m_humdrum_text << "!!@CONTENT:\n";
+		m_humdrum_text << "!! <h2 style='color:red'> No problems detected </h2>\n";
+		m_humdrum_text << "!!@@END: PREHTML\n";
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::printJoinLine -- count is currently ignored, but may in the future
-//    allow for more than two spines to join at the same time.
+// Tool_nproof::checkForBlankLines --
 //
 
-void Tool_myank::printJoinLine(vector<int>& splits, int index, int count) {
-	int i;
-	for (i=0; i<(int)splits.size(); i++) {
-		if (i == index) {
-			m_humdrum_text << "*v\t*v";
-			i+=count-1;
-		} else {
-			m_humdrum_text << "*";
+void Tool_nproof::checkForBlankLines(HumdrumFile& infile) {
+	vector<int> blanks;
+	// -1: Not checking for a blank line at the very end of the score.
+	for (int i=0; i<infile.getLineCount() - 1; i++) {
+		if (infile[i].hasSpines()) {
+			continue;
 		}
-		if (i<(int)splits.size()-1) {
-			m_humdrum_text << "\t";
+		HTp token = infile.token(i, 0);
+		if (*token == "") {
+			blanks.push_back(i+1);
 		}
 	}
-	m_humdrum_text << "\n";
 
-	// merge splits by one element
-	for (i=index+1; i<(int)splits.size()-1; i++) {
-		splits[i] = splits[i+1];
+	if (blanks.empty()) {
+		return;
 	}
-	splits.resize(splits.size()-1);
+
+	m_errorCount++;
+	m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Blank lines on row";
+	if (blanks.size() != 1) {
+		m_errorList += "s";
+	}
+	m_errorList += ": ";
+	for (int i=0; i<(int)blanks.size(); i++) {
+		m_errorList += to_string(blanks[i]);
+		if (i < (int)blanks.size() - 1) {
+			m_errorList += ", ";
+		}
+	}
+	m_errorList += ".\n";
+	m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::reconcileStartingPosition -- merge spines from start of data and
-//    first measure in output.
+// Tool_nproof::checkForValidInstrumentCode --
 //
 
-void Tool_myank::reconcileStartingPosition(HumdrumFile& infile, int index2) {
-	int i;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isInterpretation()) {
-			reconcileSpineBoundary(infile, i, index2);
-			break;
+void Tool_nproof::checkForValidInstrumentCode(HTp token,
+		vector<pair<string, string>>& instrumentList) {
+
+	if ((token->find("&") == string::npos) && (token->find("|") == string::npos)) {
+		string code = token->substr(2);
+		for (int i=0; i<(int)instrumentList.size(); i++) {
+			if (instrumentList[i].first == code) {
+				return;
+			}
+		}
+
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown instrument code \"" + code + "\" on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ". See list of codes at <a target='_blank' href='https://bit.ly/humdrum-instrument-codes'>https://bit.ly/humdrum-instrument-codes</a>.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		return;
+	}
+
+	bool found1 = false;
+	bool found2 = false;
+	string inst1;
+	string inst2;
+	HumRegex hre;
+	if (hre.match(token, "^\\*I(.*)[&|](I.*)")) {
+		inst1 = hre.getMatch(1);
+		inst2 = hre.getMatch(2);
+
+		for (int i=0; i<(int)instrumentList.size(); i++) {
+			if (instrumentList[i].first == inst1) {
+				found1 = true;
+			}
+			if (instrumentList[i].first == inst2) {
+				found2 = true;
+			}
 		}
 	}
+
+	if (!found1) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown instrument code \"" + inst1 + "\" in token " + *token + " on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ". See list of codes at <a target='_blank' href='https://bit.ly/humdrum-instrument-codes'>https://bit.ly/humdrum-instrument-codes</a>.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	}
+
+	if (!found2) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown instrument code \"" + inst2 + "\" in token " + *token + " on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ". See list of codes at <a target='_blank' href='https://bit.ly/humdrum-instrument-codes'>https://bit.ly/humdrum-instrument-codes</a>.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	}
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::printStarting -- print header information before start of data.
+// Tool_nproof::checkInstrumentInformation --
 //
 
-void Tool_myank::printStarting(HumdrumFile& infile) {
-	int i, j;
-	int exi = -1;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isInterpretation()) {
-			// the first interpretation is the exclusive one
-			m_humdrum_text << infile[i] << "\n";
-			exi = i;
+void Tool_nproof::checkInstrumentInformation(HumdrumFile& infile) {
+	int codeLine = -1;
+	int classLine = -1;
+	HumRegex hre;
+
+	vector<pair<string, string>> instrumentList = Convert::getInstrumentList();
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
 			break;
 		}
-		if (!m_hideStarting) {
-			m_humdrum_text << infile[i] << "\n";
-		} else {
-			if (infile[i].rfind("!!!RDF", 0) == 0 || infile[i].rfind("!!!system-decoration", 0) == 0) {
-				m_humdrum_text << infile[i] << "\n";
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		if (infile[i].isManipulator()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->compare(0, 3, "*IC") == 0) {
+				if (classLine < 0) {
+					classLine = i;
+				}
+			} else if (hre.search(token, "^\\*I[a-z]")) {
+				if (codeLine < 0) {
+					codeLine = i;
+				}
 			}
 		}
 	}
 
-	// keep *part interpretations
-	bool hasPart = false;
-	for (i=exi+1; i<infile.getLineCount(); i++) {
-		hasPart = false;
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			if (infile.token(i, j)->compare(0, 5, "*part") == 0) {
-				hasPart = true;
-				break;
-			}
-		}
-		if (hasPart) {
-			for (j=0; j<infile[i].getFieldCount(); j++) {
-				if (infile.token(i, j)->compare(0, 5, "*part") == 0) {
-					m_humdrum_text << infile.token(i, j);
+	if (codeLine < 0) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No instrument code line.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	} else {
+		for (int i=0; i<infile[codeLine].getFieldCount(); i++) {
+			HTp token = infile.token(codeLine, i);
+			if (token->isKern()) {
+				if (!hre.search(token, "^\\*I[a-z]")) {
+					m_errorCount++;
+					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": expected instrument code on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
+					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 				} else {
-					m_humdrum_text << "*";
+					checkForValidInstrumentCode(token, instrumentList);
 				}
-				if (j < infile[i].getFieldCount() - 1) {
-					m_humdrum_text << "\t";
+			} else {
+				if (*token != "*") {
+					m_errorCount++;
+					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Expected null interpretation on instrument code line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
+					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 				}
 			}
-			m_humdrum_text << "\n";
 		}
 	}
 
-	// keep *staff interpretations
-	bool hasStaff = false;
-	for (i=exi+1; i<infile.getLineCount(); i++) {
-		hasStaff = false;
-		for (j=0; j<infile[i].getFieldCount(); j++) {
-			if (infile.token(i, j)->compare(0, 6, "*staff") == 0) {
-				hasStaff = true;
-				break;
-			}
-		}
-		if (hasStaff) {
-			for (j=0; j<infile[i].getFieldCount(); j++) {
-				if (infile.token(i, j)->compare(0, 6, "*staff") == 0) {
-					m_humdrum_text << infile.token(i, j);
-				} else {
-					m_humdrum_text << "*";
+	if (classLine < 0) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No instrument class line.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	} else {
+		for (int i=0; i<infile[classLine].getFieldCount(); i++) {
+			HTp token = infile.token(classLine, i);
+			if (token->isKern()) {
+				if (!hre.search(token, "^\\*IC[a-z]")) {
+					m_errorCount++;
+					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": expected instrument class on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
+					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 				}
-				if (j < infile[i].getFieldCount() - 1) {
-					m_humdrum_text << "\t";
+			} else {
+				if (*token != "*") {
+					m_errorCount++;
+					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Expected null interpretation on instrument class line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
+					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 				}
 			}
-			m_humdrum_text << "\n";
 		}
 	}
+}
 
-	int hasI = 0;
 
-	if (m_instrumentQ) {
-		// print any tandem interpretations which start with *I found
-		// at the start of the data before measures, notes, or any
-		// spine manipulator lines
-		for (i=exi+1; i<infile.getLineCount(); i++) {
-			if (infile[i].isData()) {
-				break;
+
+//////////////////////////////
+//
+// Tool_nproof::checkReferenceRecords --
+//
+
+void Tool_nproof::checkReferenceRecords(HumdrumFile& infile) {
+	vector<int> foundENC;  // Musescore encoder's name
+	vector<int> foundEND;  // Musescore encdoer's date
+	vector<int> foundEED;  // VHV editor's name
+	vector<int> foundEEV;  // VHV editor's date
+
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReferenceRecord()) {
+			continue;
+		}
+		string key = infile[i].getReferenceKey();
+
+		if (hre.search(key, "^EED\\d*$")) {
+			if (key == "EED") {
+				foundEED.push_back(i);
 			}
-			if (infile[i].isBarline()) {
-				break;
+			string value = infile[i].getReferenceValue();
+			if (hre.search(value, "^\\d\\d\\d\\d")) {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For EED (Electronic EDitor) record on line " + to_string(i+1) + ", found a date rather than a name: " + value + ".\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 			}
-			if (!infile[i].isInterpretation()) {
-				continue;
+		}
+		if (hre.search(key, "^EEV\\d*$")) {
+			if (key == "EEV") {
+				foundEEV.push_back(i);;
 			}
-			if (infile[i].isManipulator()) {
-				break;
+			string value = infile[i].getReferenceValue();
+			if (!hre.search(value, "^\\d\\d\\d\\d-\\d\\d-\\d\\d")) {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For EEV (ElEctronic Version) record on line " + to_string(i+1) + ", found a name rather than a date (or invalid date): " + value + ".\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 			}
-			hasI = 0;
-			for (j=0; j<infile[i].getFieldCount(); j++) {
-				if (infile.token(i, j)->compare(0, 2, "*I") == 0) {
-					hasI = 1;
-					break;
+		}
+		if (hre.search(key, "^ENC\\d*(-modern|-iiif)?$")) {
+			string value = infile[i].getReferenceValue();
+			if (hre.search(value, "^\\d\\d\\d\\d-\\d\\d-\\d\\d")) {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For ENC (Electronic eNCoder) record on line " + to_string(i+1) + ", found a date rather than a name: " + value + ".\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+			}
+		}
+		if (hre.search(key, "^END\\d*(-modern|-iiif)?$")) {
+			string value = infile[i].getReferenceValue();
+			if (!hre.search(value, "^\\d\\d\\d\\d-\\d\\d-\\d\\d")) {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For END (Electronic eNcoding Date) record on line " + to_string(i+1) + ", found a name rather than a date (or an invalid date): " + value + ".\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+			}
+		}
+		if (hre.search(key, "^ENC(\\d*.*)$")) {
+				if (key == "ENC") {
+					foundENC.push_back(i);
+				}
+		}
+		if (hre.search(key, "^ENC-(\\d+.*)$")) {
+				string newvalue = "ENC" + hre.getMatch(1);
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": " + key + " reference record on line " + to_string(i+1) + " should not include a dash and instead be: " + newvalue + ".\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+		if (hre.search(key, "END(\\d*.*)")) {
+				if (key == "END") {
+					foundEND.push_back(i);
 				}
+		}
+		if (hre.search(key, "^END-(\\d+.*)$")) {
+				string newvalue = "END" + hre.getMatch(1);
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": " + key + " reference record on line " + to_string(i+1) + " should not include a dash and instead be: " + newvalue + ".\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+		if (key == "filter-") {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": \"filter-\" reference record on line " + to_string(i+1) + " should probably be \"filter-modern\" instead.\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+		if (key == "ENC-mod") {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": ENC-mod reference record on line " + to_string(i+1) + " should be ENC-modern instead.\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+		if (key == "END-mod") {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": END-mod reference record on line " + to_string(i+1) + " should be END-modern instead.\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+		if (key == "AIN-mod") {
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": AIN-mod reference record on line " + to_string(i+1) + " should be AIN-modern instead.\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+		if (hre.search(key, "^(.*)-ori$")) {
+				string piece = hre.getMatch(1);
+				m_errorCount++;
+				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": " + key + " reference record on line " + to_string(i+1) + " should not be used (either use " + piece + "-mod or don't add -ori qualifier to " + piece + ").\n";
+				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+	}
+
+	// vector<int> foundENC;  // Musescore encoder's name
+	// vector<int> foundEND;  // Musescore encdoer's date
+	// vector<int> foundEED;  // VHV editor's name
+	// vector<int> foundEEV;  // VHV editor's date
+
+	if (foundENC.empty()) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing ENC (initial encoder's name) reference record.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	}
+	if (foundEND.empty()) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing END (initial encoding date) reference record.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	}
+	if (foundEED.empty()) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing EED (Humdrum electronic editor's name) reference record.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	}
+	if (foundEEV.empty()) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing EEV (Humdrum electronic edition date) reference record.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	}
+
+	if ((foundENC.size() == 1) && (foundEED.size() == 1)) {
+		if (foundENC[0] > foundEED[0]) {
+			m_errorCount++;
+			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": ENC reference record on line " + to_string(foundENC[0]+1) + " should come before EED reference record on line " + to_string(foundEED[0]+1) + "\n";
+			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+	}
+
+	if ((foundEND.size() == 1) && (foundEEV.size() == 1)) {
+		if (foundEND[0] > foundEEV[0]) {
+			m_errorCount++;
+			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": END reference record on line " + to_string(foundEND[0]+1) + " should come before EEV reference record on line " + to_string(foundEEV[0]+1) + "\n";
+			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+	}
+
+
+	if ((foundENC.size() == 2) && (foundEED.size() == 0)) {
+		string date1;
+		string date2;
+		if (foundEND.size() == 2) {
+			date1 = infile[foundEND[0]].getReferenceValue();
+			date2 = infile[foundEND[1]].getReferenceValue();
+			hre.replaceDestructive(date1, "", "-", "g");
+			hre.replaceDestructive(date2, "", "-", "g");
+			int number1 = 0;
+			int number2 = 0;
+			if (hre.search(date1, "^(20\\d{6})$")) {
+				number1 = hre.getMatchInt(1);
 			}
-			if (hasI) {
-				for (j=0; j<infile[i].getFieldCount(); j++) {
-					if (infile.token(i, j)->compare(0, 2, "*I") == 0) {
-						m_humdrum_text << infile.token(i, j);
-					} else {
-						m_humdrum_text << "*";
-					}
-					if (j < infile[i].getFieldCount() - 1) {
-						m_humdrum_text << "\t";
-					}
+			if (hre.search(date2, "^(20\\d{6})$")) {
+				number2 = hre.getMatchInt(1);
+			}
+			if ((number1 > 0) && (number2 > 0)) {
+				if (number1 > number2) {
+					m_errorCount++;
+					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Second ENC reference record on line " + to_string(foundENC[1]+1) + " should probably be changed to EED reference record (and second END reference record on line " + to_string(foundEND[1]+1) + " changed to EEV).\n";
+					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 				}
-				m_humdrum_text << "\n";
 			}
+		} else {
+			m_errorCount++;
+			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": There are two ENC records on lines " + to_string(foundENC[0]+1) + " and " + to_string(foundENC[1]+1) + ". The Humdrum editor's name should be changed to EED, and the editing date should be changed from END to EEV if necessary.\n";
+			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		}
+	}
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_nproof::checkKeyInformation --
+//
+
+void Tool_nproof::checkKeyInformation(HumdrumFile& infile) {
+	int foundKey = -1;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].hasSpines()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		if (token->compare(0, 7, "!!!key:") == 0) {
+			foundKey = i;
+			break;
+		}
+	}
+
+	if (foundKey < 0) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No <tt>!!!key:</tt> reference record.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		return;
+	}
+
+	string value = infile[foundKey].getReferenceValue();
+	if (value.empty()) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": <tt>!!!key:</tt> reference record on line " + to_string(foundKey+1) + " should not be empty.  If no key, then use \"none\".\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		return;
+	}
+
+	HumRegex hre;
+	if (hre.search(value, "^([a-gA-G][#-n]?):(dor|phr|lyd|mix|aeo|loc|ion)$")) {
+		string tonic = hre.getMatch(1);
+		string mode  = hre.getMatch(2);
+		int major = 0;
+		if ((mode == "lyd") || (mode == "mix") || (mode == "ion")) {
+			major = 1;
+		}
+		int uppercase = isupper(tonic[0]);
+		if ((major == 1) && (uppercase == 0)) {
+			tonic[0] = toupper(tonic[0]);
+			string correct = tonic + ":" + mode;
+			m_errorCount++;
+			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": <tt>!!!key:</tt> reference record on line " + to_string(foundKey + 1) + " should be \"" + correct + "\".\n";
+			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		} else if ((major == 0) && (uppercase == 1)) {
+			tonic[0] = tolower(tonic[0]);
+			string correct = tonic + ":" + mode;
+			m_errorCount++;
+			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": <tt>!!!key:</tt> reference record on line " + to_string(foundKey + 1) + " should be \"" + correct + "\".\n";
+			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 		}
+	} else if (hre.search(value, "([a-gA-G][#-n]?):(.+)")) {
+		string mode = hre.getMatch(2);
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown mode in <tt>!!!key:</tt> reference record contents on line " + to_string(foundKey + 1) + ": \"" + mode + "\".\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+	} else if (!hre.search(value, "([a-gA-G][#-n]?):?")) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown key designation in <tt>!!!key:</tt> reference record contents on line " + to_string(foundKey + 1) + ": \"" + value + "\".\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 	}
 
 }
@@ -110838,408 +115050,569 @@ void Tool_myank::printStarting(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_myank::printEnding -- print the spine terminators and any
-//     content after the end of the data.
+// Tool_nproof::checkSpineTerminations --
 //
 
-void Tool_myank::printEnding(HumdrumFile& infile, int lastline, int adjlin) {
-	if (m_debugQ) {
-		m_humdrum_text << "IN printEnding" << endl;
-	}
-	int ending = -1;
-	int marker = -1;
-	int i;
-	for (i=infile.getLineCount()-1; i>=0; i--) {
-		if (infile[i].isInterpretation() && (ending <0)
-				&& (*infile.token(i, 0) == "*-")) {
-			ending = i;
+void Tool_nproof::checkSpineTerminations(HumdrumFile& infile) {
+	int foundTerminal = 0;
+	for (int i=infile.getLineCount() - 1; i>0; i--) {
+		if (!infile[i].isInterpretation()) {
+			continue;
 		}
-		if (infile[i].isData()) {
-			marker = i+1;
+		HTp token = infile.token(i, 0);
+		if (*token == "*-") {
+			foundTerminal = i;
 			break;
 		}
-		if (infile[i].isBarline()) {
-			marker = i+1;
+	}
+
+	if (!foundTerminal) {
+		m_errorCount++;
+		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No spine terminators.\n";
+		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		return;
+	}
+
+	bool problem = false;
+	for (int i=0; i<infile[foundTerminal].getFieldCount(); i++) {
+		HTp token = infile[foundTerminal].token(i);
+		string value = token->getSpineInfo();
+		if (value.find(" ") != string::npos) {
+			problem = true;
 			break;
 		}
 	}
 
-	if (ending >= 0) {
-		reconcileSpineBoundary(infile, adjlin, ending);
+	if (!problem) {
+		return;
 	}
 
-	int startline  = ending;
-	if (marker >= 0) {
-		// capture any comment which occur after the last measure
-		// line in the data.
-		startline = marker;
+	m_errorCount++;
+	m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Incorrect spine merger(s): ";
+	for (int i=0; i<infile[foundTerminal].getFieldCount(); i++) {
+		HTp token = infile[foundTerminal].token(i);
+		m_errorList += "<" + token->getSpineInfo() + ">";
+		if (i < infile[foundTerminal].getFieldCount() - 1) {
+			m_errorList += " ";
+		}
+	}
+	m_errorList += "\n";
+	m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+}
+
+
+
+
+
+/////////////////////////////////
+//
+// Tool_ordergps::Tool_ordergps -- Set the recognized options for the tool.
+//
+
+Tool_ordergps::Tool_ordergps(void) {
+	define("e|empty=b",   "list files that have no group/part/staff (used with -p option).");
+	define("f|file=b",    "list input files only.");
+	define("l|list=b",    "list files that will be changed.");
+	define("p|problem=b", "list files that have mixed content for *group, *part, *staff info.");
+	define("r|reverse=b", "order *staff, *part, *group");
+	define("s|staff=b",   "Add staff line if none present already in score.");
+	define("t|top=b",     "Place group/part/staff lines first after exinterp.");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_ordergps::run -- Do the main work of the tool.
+//
+
+bool Tool_ordergps::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
+}
 
-	// reconcileSpineBoundary(infile, lastline, startline);
 
-	if (startline >= 0) {
-		for (i=startline; i<infile.getLineCount(); i++) {
-			if (m_hideEnding && (i > ending)) {
-				if (infile[i].rfind("!!!RDF", 0) == 0 || infile[i].rfind("!!!system-decoration", 0) == 0) {
-					m_humdrum_text << infile[i] << "\n";
-				}
-			} else {
-				m_humdrum_text << infile[i] << "\n";
-			}
-		}
+bool Tool_ordergps::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_ordergps::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
+}
 
+
+bool Tool_ordergps::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::getMeasureStartStop --  Get a list of the (numbered) measures in the
-//    input file, and store the start/stop lines for those measures.
-//    All data before the first numbered measure is in measure 0.
-//    although, if the first measure is not labeled, then ...
+// Tool_ordergps::initialize -- Setup to do before processing a file.
 //
 
-void Tool_myank::getMeasureStartStop(vector<MeasureInfo>& measurelist, HumdrumFile& infile) {
-	measurelist.reserve(infile.getLineCount());
-	measurelist.resize(0);
+void Tool_ordergps::initialize(void) {
+	m_emptyQ   = getBoolean("empty");
+	m_fileQ    = getBoolean("file");
+	m_listQ    = getBoolean("list");
+	m_problemQ = getBoolean("problem");
+	m_reverseQ = getBoolean("reverse");
+	m_staffQ   = getBoolean("staff");
+	m_topQ     = getBoolean("top");
+}
 
-	MeasureInfo current;
-	int i, ii;
-	int lastend = -1;
-	int dataend = -1;
-	int barnum1 = -1;
-	int barnum2 = -1;
-	HumRegex hre;
 
-	insertZerothMeasure(measurelist, infile);
 
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isInterpretation()) {
-			if (*infile.token(i, 0) == "*-") {
-				dataend = i;
+//////////////////////////////
+//
+// Tool_ordergps::processFile -- Analyze an input file.
+//
+
+void Tool_ordergps::processFile(HumdrumFile& infile) {
+	vector<int> groupIndex;
+	vector<int> partIndex;
+	vector<int> staffIndex;
+	bool foundProblem = false;
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			break;
+		}
+		if (infile[i].isManipulator()) {
+			// Don't deall with spine splits/mergers/exchanges/additions.
+			if (!infile[i].isExclusiveInterpretation()) {
 				break;
 			}
 		}
-		if (!infile[i].isBarline()) {
+		if (infile[i].isCommentLocal()) {
+			// Don't process after local comments.   The file header
+			// is too complex perhaps, so do not alter anything
+			// after the local comment.  This can be related to modori
+			// assignment for groups, for example.
+			break;
+		}
+		if (!infile[i].hasSpines()) {
 			continue;
 		}
-		//if (!hre.search(infile.token(i, 0), "^=.*(\\d+)")) {
-		//   continue;
-		//}
-		//barnum1 = stoi(hre.getMatch(1));
-		if (!sscanf(infile.token(i, 0)->c_str(), "=%d", &barnum1)) {
+		if (infile[i].isExclusiveInterpretation()) {
 			continue;
 		}
-		current.clear();
-		current.start = i;
-		current.num   = barnum1;
-		for (ii=i+1; ii<infile.getLineCount(); ii++) {
-			if (!infile[ii].isBarline()) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		int hasGroup = false;
+		int hasPart  = false;
+		int hasStaff = false;
+		int hasOther = false;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (*token == "*") {
 				continue;
 			}
-			//if (hre.search(infile.token(ii, 0), "^=.*(\\d+)")) {
-			//   barnum2 = stoi(hre.getMatch(1));
-			//   current.stop = ii;
-			//   lastend = ii;
-			//   i = ii - 1;
-			//   measurelist.push_back(current);
-			//   break;
-			//}
-			if (hre.search(infile.token(ii, 0), "=[^\\d]*(\\d+)")) {
-			// if (sscanf(infile.token(ii, 0), "=%d", &barnum2)) {
-				barnum2 = stoi(hre.getMatch(1));
-				current.stop = ii;
-				lastend = ii;
-				i = ii - 1;
-				current.file = &infile;
-				measurelist.push_back(current);
-				break;
+			if (token->compare(0, 6, "*group") == 0) {
+				hasGroup = true;
+			} else if (token->compare(0, 5, "*part") == 0) {
+				hasPart = true;
+			} else if (token->compare(0, 6, "*staff") == 0) {
+				hasStaff = true;
 			} else {
-				if (atEndOfFile(infile, ii)) {
-					break;
-				}
+				hasOther = true;
 			}
 		}
-	}
 
-	int lastdata    = -1;   // last line in file with data
-	int lastmeasure = -1;   // last line in file with measure
+		if (hasOther && hasGroup) {
+			foundProblem = true;
+			if (m_problemQ) {
+				cerr << infile.getFilename() << " HAS MIXED GROUP LINE:" << endl;
+				cerr << "\t" << infile[i] << endl;
+			}
+		}
 
-	for (i=infile.getLineCount()-1; i>=0; i--) {
-		if ((lastdata < 0) && infile[i].isData()) {
-			lastdata = i;
+		if (hasOther && hasPart) {
+			foundProblem = true;
+			if (m_problemQ) {
+				cerr << infile.getFilename() << " HAS MIXED PART LINE:" << endl;
+				cerr << "\t" << infile[i] << endl;
+			}
 		}
-		if ((lastmeasure < 0) && infile[i].isBarline()) {
-			lastmeasure = i;
+
+		if (hasOther && hasStaff) {
+			foundProblem = true;
+			if (m_problemQ) {
+				cerr << infile.getFilename() << " HAS MIXED STAFF LINE:" << endl;
+				cerr << "\t" << infile[i] << endl;
+			}
 		}
-		if ((lastmeasure >= 0) && (lastdata >= 0)) {
-			break;
+
+		if (hasOther) {
+			continue;
+		}
+
+		if (hasGroup && hasPart) {
+			foundProblem = true;
+			if (m_problemQ) {
+				cerr << infile.getFilename() << " HAS MIXED GROUP AND PART LINE:" << endl;
+				cerr << "\t" << infile[i] << endl;
+			}
+		}
+
+		if (hasGroup && hasStaff) {
+			foundProblem = true;
+			if (m_problemQ) {
+				cerr << infile.getFilename() << " HAS MIXED GROUP AND STAFF LINE:" << endl;
+				cerr << "\t" << infile[i] << endl;
+			}
+		}
+
+		if (hasPart && hasStaff) {
+			foundProblem = true;
+			if (m_problemQ) {
+				cerr << infile.getFilename() << " HAS MIXED PART AND STAFF LINE:" << endl;
+				cerr << "\t" << infile[i] << endl;
+			}
+		}
+
+		if (hasGroup) {
+			groupIndex.push_back(i);
+		}
+
+		if (hasPart)  {
+			partIndex.push_back(i);
+		}
+
+		if (hasStaff) {
+			staffIndex.push_back(i);
 		}
 	}
 
-	if (lastmeasure < lastdata) {
-		// no final barline, so set to ignore
-		lastmeasure = -1;
-		lastdata    = -1;
+	if (groupIndex.size() > 1) {
+		foundProblem = true;
+		if (m_problemQ) {
+			cerr << infile.getFilename() << " HAS MORE THAN ONE GROUP LINE:" << endl;
+			for (int i=0; i<(int)groupIndex.size(); i++) {
+				cerr << "\t" << infile[groupIndex[i]] << endl;
+			}
+		}
 	}
 
-	if ((barnum2 >= 0) && (lastend >= 0) && (dataend >= 0)) {
-		current.clear();
-		current.num = barnum2;
-		current.start = lastend;
-		current.stop = dataend;
-		if (lastmeasure > lastdata) {
-			current.stop = lastmeasure;
+	if (partIndex.size() > 1) {
+		foundProblem = true;
+		if (m_problemQ) {
+			cerr << infile.getFilename() << " HAS MORE THAN ONE PART LINE:" << endl;
+			for (int i=0; i<(int)partIndex.size(); i++) {
+				cerr << "\t" << infile[partIndex[i]] << endl;
+			}
 		}
-		current.file = &infile;
-		measurelist.push_back(current);
 	}
 
-	// allow "myank -l" when there are no measure numbers
-	if (getBoolean("lines") && measurelist.size() == 0) {
-		current.clear();
-		current.num = 0;
-		current.start = 0;
-		current.stop = dataend;
-		current.file = &infile;
-		measurelist.push_back(current);
+	if (staffIndex.size() > 1) {
+		foundProblem = true;
+		if (m_problemQ) {
+			cerr << infile.getFilename() << " HAS MORE THAN ONE STAFF LINE:" << endl;
+			for (int i=0; i<(int)staffIndex.size(); i++) {
+				cerr << "\t" << infile[staffIndex[i]] << endl;
+			}
+		}
 	}
 
+	if (m_problemQ) {
+		if (m_emptyQ) {
+			if (groupIndex.empty() && partIndex.empty() && staffIndex.empty()) {
+				cerr << infile.getFilename() << " HAS NO GROUP/PART/STAFF INFO" << endl;
+			}
+		}
+	} else {
+		if (foundProblem) {
+			// Don try to fix anything, just echo the input:
+			m_humdrum_text << infile;
+		} else {
+			if (m_staffQ && groupIndex.empty() && partIndex.empty() && staffIndex.empty()) {
+				printStaffLine(infile);
+			} else {
+				// Process further here
+				// Check the order of the group/part/staff lines.
+				int gindex = groupIndex.empty() ? -1 : groupIndex.at(0);
+				int pindex = partIndex.empty() ? -1 : partIndex.at(0);
+				int sindex = staffIndex.empty() ? -1 : staffIndex.at(0);
+				if (m_topQ) {
+					printFileTop(infile, gindex, pindex, sindex);
+				} else {
+					printFile(infile, gindex, pindex, sindex);
+				}
+			}
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::getSectionCount -- Count the number of sections in a file according to
-//     JRP rules: sections are defined by double barlines. There may be some
-//     corner cases to consider.
+// Tool_ordergps::printFileTop -- Print group/part/staff first after exclusive
+//     interpretations.
 //
 
-int Tool_myank::getSectionCount(HumdrumFile& infile) {
-	int i;
-	int count = 0;
-	int dataQ = 0;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (!dataQ && infile[i].isData()) {
-			dataQ = 1;
-			count++;
+void Tool_ordergps::printFileTop(HumdrumFile& infile, int gindex, int pindex, int sindex) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (i == gindex) {
 			continue;
-		}
-		if (infile[i].isBarline()) {
-			if (infile.token(i, 0)->find("||") != string::npos) {
-				dataQ = 0;
+		} else if (i == pindex) {
+			continue;
+		} else if (i == sindex) {
+			continue;
+		} else if (infile[i].isExclusiveInterpretation()) {
+			m_humdrum_text << infile[i] << endl;
+			if (m_reverseQ) {
+				if (sindex >= 0) {
+					m_humdrum_text << infile[sindex] << endl;
+				}
+				if (pindex >= 0) {
+					m_humdrum_text << infile[pindex] << endl;
+				}
+				if (gindex >= 0) {
+					m_humdrum_text << infile[gindex] << endl;
+				}
+			} else {
+				if (gindex >= 0) {
+					m_humdrum_text << infile[gindex] << endl;
+				}
+				if (pindex >= 0) {
+					m_humdrum_text << infile[pindex] << endl;
+				}
+				if (sindex >= 0) {
+					m_humdrum_text << infile[sindex] << endl;
+				}
 			}
+		} else {
+			m_humdrum_text << infile[i] << endl;
 		}
 	}
-	return count;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::getSectionString -- return the measure range of a section.
+// Tool_ordergps::printFile -- Check to see if the group/part/staff
+//    lines need to be adjusted, and the print the file.  Lines
+//    will be ordered group/part/staff, placing the lines where
+//    the first of group/part/staff is found.
 //
 
-void Tool_myank::getSectionString(string& sstring, HumdrumFile& infile, int sec) {
-	int i;
-	int first = -1;
-	int second = -1;
-	int barnum = 0;
-	int count = 0;
-	int dataQ = 0;
-	HumRegex hre;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if (!dataQ && infile[i].isData()) {
-			dataQ = 1;
-			count++;
-			if (count == sec) {
-				first = barnum;
-			} else if (count == sec+1) {
-				second = barnum - 1;
+void Tool_ordergps::printFile(HumdrumFile& infile, int gindex, int pindex, int sindex) {
+	int startIndex = gindex;
+	if (pindex >= 0) {
+		if (startIndex < 0) {
+			startIndex = pindex;
+		} else if (pindex < startIndex) {
+			startIndex = pindex;
+		}
+	}
+	if (sindex >= 0) {
+		if (startIndex < 0) {
+			startIndex = sindex;
+		} else if (sindex < startIndex) {
+			startIndex = sindex;
+		}
+	}
+	if (startIndex < 0) {
+		// no group/part/staff lines in file, so just print it:
+		m_humdrum_text << infile;
+		return;
+	}
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (i == startIndex) {
+			if (m_reverseQ) {
+				if (sindex >= 0) {
+					m_humdrum_text << infile[sindex] << endl;
+				}
+				if (pindex >= 0) {
+					m_humdrum_text << infile[pindex] << endl;
+				}
+				if (gindex >= 0) {
+					m_humdrum_text << infile[gindex] << endl;
+				}
+			} else {
+				if (gindex >= 0) {
+					m_humdrum_text << infile[gindex] << endl;
+				}
+				if (pindex >= 0) {
+					m_humdrum_text << infile[pindex] << endl;
+				}
+				if (sindex >= 0) {
+					m_humdrum_text << infile[sindex] << endl;
+				}
 			}
+		} else if (i == gindex) {
+			continue;
+		} else if (i == pindex) {
+			continue;
+		} else if (i == sindex) {
+			continue;
+		} else {
+			m_humdrum_text << infile[i] << endl;
+		}
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_ordergps::printStaffLine --  Add a *staff at the start of the
+//     data since none was detected.  Does not label staff-like spines
+//     other than **kern (such as **kernyy, **kern-mod, **mens).
+
+void Tool_ordergps::printStaffLine(HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isExclusiveInterpretation()) {
+			m_humdrum_text << infile[i] << endl;
 			continue;
 		}
-		if (infile[i].isBarline()) {
-			if (infile.token(i, 0)->find("||") != string::npos) {
-				dataQ = 0;
+		m_humdrum_text << infile[i] << endl;
+		vector<string> staffLine(infile[i].getFieldCount(), "*");
+		int counter = 0;
+		for (int j=infile[i].getFieldCount() - 1; j>=0; j--) {
+			HTp token = infile.token(i, j);
+			if (token->isKern()) {
+				counter++;
+				string text = "*staff" + to_string(counter);
+				staffLine.at(j) = text;
 			}
-			if (hre.search(infile.token(i, 0), "(\\d+)")) {
-				barnum = hre.getMatchInt(1);
+		}
+		for (int j=0; j<(int)staffLine.size(); j++) {
+			m_humdrum_text << staffLine[j];
+			if (j < (int)staffLine.size() - 1) {
+				m_humdrum_text << '\t';
 			}
 		}
+		m_humdrum_text << endl;
 	}
-	if (second < 0) {
-		second = barnum;
-	}
-	sstring = to_string(first);
-	sstring += "-";
-	sstring += to_string(second);
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_myank::atEndOfFile --
+// Tool_pbar::Tool_pbar -- Set the recognized options for the tool.
 //
 
-int Tool_myank::atEndOfFile(HumdrumFile& infile, int line) {
-	int i;
-	for (i=line+1; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			return 0;
-		}
-	}
-
-	return 1;
+Tool_pbar::Tool_pbar(void) {
+	define("i|invisible-barlines=b", "make barlines invisible");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::insertZerothMeasure --
+// Tool_pbar::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_myank::insertZerothMeasure(vector<MeasureInfo>& measurelist,
-		HumdrumFile& infile) {
+void Tool_pbar::initialize(void) {
+	m_invisibleQ = getBoolean("invisible-barlines");
+}
 
-	HumRegex hre;
-	int exinterpline = -1;
-	int startline = -1;
-	int stopline = -1;
-	int i;
-	for (i=0; i<infile.getLineCount(); i++) {
-		if ((exinterpline < 0) && infile[i].isInterpretation()) {
-			exinterpline = i;
-		}
-		if ((startline < 0) && (infile[i].isData())) {
-			startline = i;
-		}
-		if (infile[i].isBarline() && hre.search(infile.token(i, 0), "^=.*\\d+", "")) {
-			stopline = i;
-			break;
-		}
-	}
 
-	if (exinterpline < 0) {
-		// somethind weird happend, just return
-		return;
+
+/////////////////////////////////
+//
+// Tool_pbar::run -- Do the main work of the tool.
+//
+
+bool Tool_pbar::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
-	if (startline < 0) {
-		// no zeroth measure;
-		return;
+	return status;
+}
+
+
+
+bool Tool_pbar::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	if (stopline < 0) {
-		// strange situation, no measure numbers
-		// consider what to do later...
-		return;
+	return status;
+}
+
+
+bool Tool_pbar::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
+}
 
-	MeasureInfo current;
-	current.clear();
-	current.num = 0;
-	current.start = startline;
-	// current.start = exinterpline+1;
-	current.stop = stopline;
-	current.file = &infile;
-	measurelist.push_back(current);
+
+bool Tool_pbar::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::expandMeasureOutList -- read the measure list for the sequence of measures
-//     to extract.
+// Tool_pbar::processFile --
 //
 
-void Tool_myank::expandMeasureOutList(vector<MeasureInfo>& measureout,
-		vector<MeasureInfo>& measurein, HumdrumFile& infile,
-		const string& optionstring) {
-
-	HumRegex hre;
-	// find the largest measure number in the score
-	int maxmeasure = -1;
-	int minmeasure = -1;
-	for (int i=0; i<(int)measurein.size(); i++) {
-		if (maxmeasure < measurein[i].num) {
-			maxmeasure = measurein[i].num;
-		}
-		if ((minmeasure == -1) || (minmeasure > measurein[i].num)) {
-			minmeasure = measurein[i].num;
-		}
-	}
-	if (maxmeasure <= 0 && !getBoolean("lines")) {
-		cerr << "Error: There are no measure numbers present in the data" << endl;
-		exit(1);
-	}
-	if (maxmeasure > 1123123) {
-		cerr << "Error: ridiculusly large measure number: " << maxmeasure << endl;
-		exit(1);
+void Tool_pbar::processFile(HumdrumFile& infile) {
+	vector<HTp> kstarts = infile.getKernSpineStartList();
+	for (int i=0; i<(int)kstarts.size(); i++) {
+		processSpine(kstarts[i]);
 	}
-	if (m_maxQ) {
-		if (measurein.size() == 0) {
-			m_humdrum_text << 0 << endl;
-		} else {
-			m_humdrum_text << maxmeasure << endl;
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << endl;
+			continue;
 		}
-		exit(0);
-	} else if (m_minQ) {
-		for (int ii=0; ii<infile.getLineCount(); ii++) {
-			if (infile[ii].isBarline()) {
-				if (hre.search(infile.token(ii, 0), "=\\d", "")) {
-					break;
-				} else {
-					m_humdrum_text << 0 << endl;
-					exit(0);
-				}
-			}
-			if (infile[ii].isData()) {
-				m_humdrum_text << 0 << endl;
-				exit(0);
+		if (infile[i].isData()) {
+			printDataLine(infile, i);
+		} else if (infile[i].isCommentLocal()) {
+			printLocalCommentLine(infile, i);
+		} else if (infile[i].isBarline()) {
+			printBarLine(infile, i);
+			if (m_invisibleQ) {
+				printInvisibleBarlines(infile, i);
+			} else {
+				m_humdrum_text << infile[i] << endl;
 			}
-		}
-		if (measurein.size() == 0) {
-			m_humdrum_text << 0 << endl;
 		} else {
-			m_humdrum_text << minmeasure << endl;
+			m_humdrum_text << infile[i] << endl;
 		}
-		exit(0);
-	}
-
-	// create reverse-lookup list
-	vector<int> inmap(maxmeasure+1);
-	fill(inmap.begin(), inmap.end(), -1);
-	for (int i=0; i<(int)measurein.size(); i++) {
-		inmap[measurein[i].num] = i;
-	}
-
-	fillGlobalDefaults(infile, measurein, inmap);
-	string ostring = optionstring;
-	removeDollarsFromString(ostring, maxmeasure);
-
-	if (m_debugQ) {
-		m_free_text << "Option string expanded: " << ostring << endl;
-	}
-
-	hre.replaceDestructive(ostring, "", "\\s+", "g");  // remove any spaces between items.
-	hre.replaceDestructive(ostring, "-", "--+", "g");  // remove extra dashes
-	int value = 0;
-	int start = 0;
-	vector<MeasureInfo>& range = measureout;
-	range.reserve(10000);
-	string searchexp = "^([\\d$-]+[^\\d$-]*)";
-	value = hre.search(ostring, searchexp);
-	while (value != 0) {
-		start += value - 1;
-		start += (int)hre.getMatch(1).size();
-		processFieldEntry(range, hre.getMatch(1), infile, maxmeasure, measurein, inmap);
-		value = hre.search(ostring, start, searchexp);
 	}
 }
 
@@ -111247,827 +115620,856 @@ void Tool_myank::expandMeasureOutList(vector<MeasureInfo>& measureout,
 
 //////////////////////////////
 //
-// Tool_myank::fillGlobalDefaults -- keep track of the clef, key signature, key, etc.
+// Tool_pbar::printInvisibleBarlines --
 //
 
-void Tool_myank::fillGlobalDefaults(HumdrumFile& infile, vector<MeasureInfo>& measurein,
-		vector<int>& inmap) {
-	int i, j;
+void Tool_pbar::printInvisibleBarlines(HumdrumFile& infile, int index) {
 	HumRegex hre;
-
-	int tracks = infile.getMaxTrack();
-	// cerr << "MAX TRACKS " << tracks << " ===============================" << endl;
-
-	vector<MyCoord> currclef(tracks+1);
-	vector<MyCoord> currkeysig(tracks+1);
-	vector<MyCoord> currkey(tracks+1);
-	vector<MyCoord> currtimesig(tracks+1);
-	vector<MyCoord> currmet(tracks+1);
-	vector<MyCoord> currtempo(tracks+1);
-
-	MyCoord undefMyCoord;
-	undefMyCoord.clear();
-
-	fill(currclef.begin(), currclef.end(), undefMyCoord);
-	fill(currkeysig.begin(), currkeysig.end(), undefMyCoord);
-	fill(currkey.begin(), currkey.end(), undefMyCoord);
-	fill(currtimesig.begin(), currtimesig.end(), undefMyCoord);
-	fill(currmet.begin(), currmet.end(), undefMyCoord);
-	fill(currtempo.begin(), currtempo.end(), undefMyCoord);
-
-	int currmeasure = -1;
-	int lastmeasure = -1;
-	int datafound   = 0;
-	int track;
-	int thingy = 0;
-
-	for (i=0; i<infile.getLineCount(); i++) {
-		if ((currmeasure == -1) && (thingy == 0) && infile[i].isData()) {
-			currmeasure = 0;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (hre.search(token, "-")) {
+			m_humdrum_text << token;
+		} else if (hre.search(token, "==")) {
+			m_humdrum_text << token;
+		} else if (hre.search(token, "\\|\\|")) {
+			m_humdrum_text << token;
+		} else {
+			m_humdrum_text << token << "-";
 		}
-		if (infile[i].isBarline()) {
-			if (!hre.search(infile.token(i, 0), "(\\d+)", "")) {
-				continue;
-			}
-			thingy = 1;
-
-			// store state of global music values at end of measure
-			if (currmeasure >= 0) {
-				measurein[inmap[currmeasure]].eclef    = currclef;
-				measurein[inmap[currmeasure]].ekeysig  = currkeysig;
-				measurein[inmap[currmeasure]].ekey     = currkey;
-				measurein[inmap[currmeasure]].etimesig = currtimesig;
-				measurein[inmap[currmeasure]].emet     = currmet;
-				measurein[inmap[currmeasure]].etempo   = currtempo;
-			}
-
-			lastmeasure = currmeasure;
-			currmeasure = hre.getMatchInt(1);
-
-			if (currmeasure < (int)inmap.size()) {
-				// [20120818] Had to compensate for last measure being single
-				// and un-numbered.
-				if (inmap[currmeasure] < 0) {
-					// [20111008] Had to compensate for "==85" barline
-					datafound = 0;
-					break;
-				}
-// cerr << "CURRCLEF: ";
-// for (int z=0; z<(int)currclef.size(); z++) {
-// cerr << "(" << currclef[z].x << "," << currclef[z].y << ") ";
-// }
-// cerr << endl;
-				measurein[inmap[currmeasure]].sclef    = currclef;
-				measurein[inmap[currmeasure]].skeysig  = currkeysig;
-				measurein[inmap[currmeasure]].skey     = currkey;
-				measurein[inmap[currmeasure]].stimesig = currtimesig;
-				// measurein[inmap[currmeasure]].smet     = metstates[i];
-				measurein[inmap[currmeasure]].smet     = currmet;
-				measurein[inmap[currmeasure]].stempo   = currtempo;
-			}
-
-			datafound   = 0;
-			continue;
+		if (i < infile[index].getFieldCount() - 1) {
+			m_humdrum_text << "\t";
 		}
-		if (infile[i].isInterpretation()) {
-			for (j=0; j<infile[i].getFieldCount(); j++) {
-				if (!infile.token(i, j)->isKern()) {
-					continue;
-				}
-				track = infile.token(i, j)->getTrack();
-
-				if ((datafound == 0) && (lastmeasure >= 0)) {
-					if (infile.token(i, j)->compare(0, 5, "*clef") == 0) {
-						measurein[inmap[currmeasure]].sclef[track].x = -1;
-						measurein[inmap[currmeasure]].sclef[track].y = -1;
-					} else if (hre.search(infile.token(i, j), "^\\*k\\[.*\\]", "")) {
-						measurein[inmap[currmeasure]].skeysig[track].x = -1;
-						measurein[inmap[currmeasure]].skeysig[track].y = -1;
-					} else if (hre.search(infile.token(i, j), "^\\*[A-G][#-]?:", "i")) {
-						measurein[inmap[currmeasure]].skey[track].x = -1;
-						measurein[inmap[currmeasure]].skey[track].y = -1;
-					} else if (hre.search(infile.token(i, j), R"(^\*M\d+/\d+)")) {
-						measurein[inmap[currmeasure]].stimesig[track].x = -1;
-						measurein[inmap[currmeasure]].stimesig[track].y = -1;
-					} else if (hre.search(infile.token(i, j), R"(^\*met\(.*\))")) {
-						measurein[inmap[currmeasure]].smet[track].x = -1;
-						measurein[inmap[currmeasure]].smet[track].y = -1;
-					} else if (hre.search(infile.token(i, j), "^\\*MM\\d+", "i")) {
-						measurein[inmap[currmeasure]].stempo[track].x = -1;
-						measurein[inmap[currmeasure]].stempo[track].y = -1;
-					}
-				}
+	}
+	m_humdrum_text << "\n";
+}
 
-				if (infile.token(i, j)->compare(0, 5, "*clef") == 0) {
-					currclef[track].x = i;
-					currclef[track].y = j;
-					continue;
-				}
-				if (hre.search(infile.token(i, j), R"(^\*k\[.*\])")) {
-					currkeysig[track].x = i;
-					currkeysig[track].y = j;
-					continue;
-				}
-				if (hre.search(infile.token(i, j), "^\\*[A-G][#-]?:", "i")) {
-					currkey[track].x = i;
-					currkey[track].y = j;
-					continue;
-				}
-				if (hre.search(infile.token(i, j), R"(^\*M\d+/\d+)")) {
-					currtimesig[track].x = i;
-					currtimesig[track].y = j;
-					continue;
-				}
-				if (hre.search(infile.token(i, j), R"(^\*met\(.*\))")) {
-					currmet[track].x = i;
-					currmet[track].y = j;
-					continue;
-				}
-				if (hre.search(infile.token(i, j), R"(^\*MM[\d.]+)")) {
-					currtempo[track].x = i;
-					currtempo[track].y = j;
-					continue;
-				}
 
-			}
-		}
-		if (infile[i].isData()) {
-			datafound = 1;
-		}
-	}
 
-	// store state of global music values at end of music
-	if ((currmeasure >= 0) && (currmeasure < (int)inmap.size())
-			&& (inmap[currmeasure] >= 0)) {
-		measurein[inmap[currmeasure]].eclef    = currclef;
-		measurein[inmap[currmeasure]].ekeysig  = currkeysig;
-		measurein[inmap[currmeasure]].ekey     = currkey;
-		measurein[inmap[currmeasure]].etimesig = currtimesig;
-		measurein[inmap[currmeasure]].emet     = currmet;
-		measurein[inmap[currmeasure]].etempo   = currtempo;
-	}
+///////////////////////////////
+//
+// Tool_pbar::printDataLine --
+//
 
-	// go through the measure list and clean up start/end states
-	for (i=0; i<(int)measurein.size()-2; i++) {
+void Tool_pbar::printDataLine(HumdrumFile& infile, int index) {
+	printBarLine(infile, index);
+	m_humdrum_text << infile[index] << endl;
+}
 
-		if (measurein[i].sclef.size() == 0) {
-			measurein[i].sclef.resize(tracks+1);
-			fill(measurein[i].sclef.begin(), measurein[i].sclef.end(), undefMyCoord);
-		}
-		if (measurein[i].eclef.size() == 0) {
-			measurein[i].eclef.resize(tracks+1);
-			fill(measurein[i].eclef.begin(), measurein[i].eclef.end(), undefMyCoord);
-		}
-		if (measurein[i+1].sclef.size() == 0) {
-			measurein[i+1].sclef.resize(tracks+1);
-			fill(measurein[i+1].sclef.begin(), measurein[i+1].sclef.end(), undefMyCoord);
-		}
-		if (measurein[i+1].eclef.size() == 0) {
-			measurein[i+1].eclef.resize(tracks+1);
-			fill(measurein[i+1].eclef.begin(), measurein[i+1].eclef.end(), undefMyCoord);
-		}
-		for (j=1; j<(int)measurein[i].sclef.size(); j++) {
-			if (!measurein[i].eclef[j].isValid()) {
-				if (measurein[i].sclef[j].isValid()) {
-					measurein[i].eclef[j] = measurein[i].sclef[j];
-				}
-			}
-			if (!measurein[i+1].sclef[j].isValid()) {
-				if (measurein[i].eclef[j].isValid()) {
-					measurein[i+1].sclef[j] = measurein[i].eclef[j];
-				}
-			}
-		}
 
-		if (measurein[i].skeysig.size() == 0) {
-			measurein[i].skeysig.resize(tracks+1);
-			fill(measurein[i].skeysig.begin(), measurein[i].skeysig.end(), undefMyCoord);
-		}
-		if (measurein[i].ekeysig.size() == 0) {
-			measurein[i].ekeysig.resize(tracks+1);
-			fill(measurein[i].ekeysig.begin(), measurein[i].ekeysig.end(), undefMyCoord);
-		}
-		if (measurein[i+1].skeysig.size() == 0) {
-			measurein[i+1].skeysig.resize(tracks+1);
-			fill(measurein[i+1].skeysig.begin(), measurein[i+1].skeysig.end(), undefMyCoord);
-		}
-		if (measurein[i+1].ekeysig.size() == 0) {
-			measurein[i+1].ekeysig.resize(tracks+1);
-			fill(measurein[i+1].ekeysig.begin(), measurein[i+1].ekeysig.end(), undefMyCoord);
-		}
-		for (j=1; j<(int)measurein[i].skeysig.size(); j++) {
-			if (!measurein[i].ekeysig[j].isValid()) {
-				if (measurein[i].skeysig[j].isValid()) {
-					measurein[i].ekeysig[j] = measurein[i].skeysig[j];
-				}
-			}
-			if (!measurein[i+1].skeysig[j].isValid()) {
-				if (measurein[i].ekeysig[j].isValid()) {
-					measurein[i+1].skeysig[j] = measurein[i].ekeysig[j];
-				}
-			}
-		}
 
-		if (measurein[i].skey.size() == 0) {
-			measurein[i].skey.resize(tracks+1);
-			fill(measurein[i].skey.begin(), measurein[i].skey.end(), undefMyCoord);
-		}
-		if (measurein[i].ekey.size() == 0) {
-			measurein[i].ekey.resize(tracks+1);
-			fill(measurein[i].ekey.begin(), measurein[i].ekey.end(), undefMyCoord);
-		}
-		if (measurein[i+1].skey.size() == 0) {
-			measurein[i+1].skey.resize(tracks+1);
-			fill(measurein[i+1].skey.begin(), measurein[i+1].skey.end(), undefMyCoord);
-		}
-		if (measurein[i+1].ekey.size() == 0) {
-			measurein[i+1].ekey.resize(tracks+1);
-			fill(measurein[i+1].ekey.begin(), measurein[i+1].ekey.end(), undefMyCoord);
-		}
-		for (j=1; j<(int)measurein[i].skey.size(); j++) {
-			if (!measurein[i].ekey[j].isValid()) {
-				if (measurein[i].skey[j].isValid()) {
-					measurein[i].ekey[j] = measurein[i].skey[j];
-				}
-			}
-			if (!measurein[i+1].skey[j].isValid()) {
-				if (measurein[i].ekey[j].isValid()) {
-					measurein[i+1].skey[j] = measurein[i].ekey[j];
-				}
-			}
-		}
+///////////////////////////////
+//
+// Tool_pbar::printBarLine -- Add *bar line.
+//
 
-		if (measurein[i].stimesig.size() == 0) {
-			measurein[i].stimesig.resize(tracks+1);
-			fill(measurein[i].stimesig.begin(), measurein[i].stimesig.end(), undefMyCoord);
-		}
-		if (measurein[i].etimesig.size() == 0) {
-			measurein[i].etimesig.resize(tracks+1);
-			fill(measurein[i].etimesig.begin(), measurein[i].etimesig.end(), undefMyCoord);
-		}
-		if (measurein[i+1].stimesig.size() == 0) {
-			measurein[i+1].stimesig.resize(tracks+1);
-			fill(measurein[i+1].stimesig.begin(), measurein[i+1].stimesig.end(), undefMyCoord);
-		}
-		if (measurein[i+1].etimesig.size() == 0) {
-			measurein[i+1].etimesig.resize(tracks+1);
-			fill(measurein[i+1].etimesig.begin(), measurein[i+1].etimesig.end(), undefMyCoord);
+void Tool_pbar::printBarLine(HumdrumFile& infile, int index) {
+	bool hasBarline = false;
+	for (int j=0; j<infile[index].getFieldCount(); j++) {
+		HTp token = infile.token(index, j);
+		string value = token->getValue("auto", "pbar");
+		if (value == "true") {
+			hasBarline = true;
+			break;
 		}
-		for (j=1; j<(int)measurein[i].stimesig.size(); j++) {
-			if (!measurein[i].etimesig[j].isValid()) {
-				if (measurein[i].stimesig[j].isValid()) {
-					measurein[i].etimesig[j] = measurein[i].stimesig[j];
-				}
+	}
+
+	if (hasBarline) {
+		for (int j=0; j<infile[index].getFieldCount(); j++) {
+			HTp token = infile.token(index, j);
+			string value = token->getValue("auto", "pbar");
+			if (value == "true") {
+				m_humdrum_text << "*bar";
+			} else {
+				m_humdrum_text << "*";
 			}
-			if (!measurein[i+1].stimesig[j].isValid()) {
-				if (measurein[i].etimesig[j].isValid()) {
-					measurein[i+1].stimesig[j] = measurein[i].etimesig[j];
-				}
+			if (j < infile[index].getFieldCount() - 1) {
+				m_humdrum_text << "\t";
 			}
 		}
+		m_humdrum_text << "\n";
+	}
+}
 
-		if (measurein[i].smet.size() == 0) {
-			measurein[i].smet.resize(tracks+1);
-			fill(measurein[i].smet.begin(), measurein[i].smet.end(), undefMyCoord);
-		}
-		if (measurein[i].emet.size() == 0) {
-			measurein[i].emet.resize(tracks+1);
-			fill(measurein[i].emet.begin(), measurein[i].emet.end(), undefMyCoord);
-		}
-		if (measurein[i+1].smet.size() == 0) {
-			measurein[i+1].smet.resize(tracks+1);
-			fill(measurein[i+1].smet.begin(), measurein[i+1].smet.end(), undefMyCoord);
-		}
-		if (measurein[i+1].emet.size() == 0) {
-			measurein[i+1].emet.resize(tracks+1);
-			fill(measurein[i+1].emet.begin(), measurein[i+1].emet.end(), undefMyCoord);
+
+
+///////////////////////////////
+//
+// Tool_pbar::printLocalCommentLine --
+//
+
+void Tool_pbar::printLocalCommentLine(HumdrumFile& infile, int index) {
+	HumRegex hre;
+	bool hasKp = false;
+	bool hasOther = false;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (hre.search(token, "kreska pseudotaktowa")) {
+			hasKp = true;
+		} else if (*token != "!") {
+			hasOther = true;
 		}
-		for (j=1; j<(int)measurein[i].smet.size(); j++) {
-			if (!measurein[i].emet[j].isValid()) {
-				if (measurein[i].smet[j].isValid()) {
-					measurein[i].emet[j] = measurein[i].smet[j];
-				}
+	}
+
+	if (!hasKp) {
+		m_humdrum_text << infile[index] << endl;
+		return;
+	}
+
+	if (hasOther) {
+		for (int i=0; i<infile[index].getFieldCount(); i++) {
+			HTp token = infile.token(index, i);
+			if (hre.search(token, "kreska pseudotaktowa")) {
+				m_humdrum_text << "!";
+			} else {
+				m_humdrum_text << token;
 			}
-			if (!measurein[i+1].smet[j].isValid()) {
-				if (measurein[i].emet[j].isValid()) {
-					measurein[i+1].smet[j] = measurein[i].emet[j];
-				}
+			if (i < infile[index].getFieldCount() - 1) {
+				m_humdrum_text << "\t";
 			}
 		}
+		m_humdrum_text << "\n";
+	}
+}
 
-		if (measurein[i].stempo.size() == 0) {
-			measurein[i].stempo.resize(tracks+1);
-			fill(measurein[i].stempo.begin(), measurein[i].stempo.end(), undefMyCoord);
-		}
-		if (measurein[i].etempo.size() == 0) {
-			measurein[i].etempo.resize(tracks+1);
-			fill(measurein[i].etempo.begin(), measurein[i].etempo.end(), undefMyCoord);
-		}
-		if (measurein[i+1].stempo.size() == 0) {
-			measurein[i+1].stempo.resize(tracks+1);
-			fill(measurein[i+1].stempo.begin(), measurein[i+1].stempo.end(), undefMyCoord);
+
+
+//////////////////////////////
+//
+// Tool_pbar::processSpine --
+//
+
+void Tool_pbar::processSpine(HTp spineStart) {
+	HTp current = spineStart;
+	HumRegex hre;
+	while (current) {
+		if (!current->isLocalComment()) {
+			current = current->getNextToken();
+			continue;
 		}
-		if (measurein[i+1].etempo.size() == 0) {
-			measurein[i+1].etempo.resize(tracks+1);
-			fill(measurein[i+1].etempo.begin(), measurein[i+1].etempo.end(), undefMyCoord);
+		if (hre.search(current, "kreska\\s*pseudotaktowa")) {
+			addBarLineToFollowingNoteOrRest(current);
 		}
-		for (j=1; j<(int)measurein[i].stempo.size(); j++) {
-			if (!measurein[i].etempo[j].isValid()) {
-				if (measurein[i].stempo[j].isValid()) {
-					measurein[i].etempo[j] = measurein[i].stempo[j];
-				}
-			}
-			if (!measurein[i+1].stempo[j].isValid()) {
-				if (measurein[i].etempo[j].isValid()) {
-					measurein[i+1].stempo[j] = measurein[i].etempo[j];
-				}
+		current = current->getNextToken();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_pbar::addBarLineToFollowingNoteOrRest --
+//
+
+void Tool_pbar::addBarLineToFollowingNoteOrRest(HTp token) {
+	HTp current = token->getNextToken();
+	int counter = 0;
+	while (current) {
+		if (!current->isBarline()) {
+			if (!current->isData() || current->isNull()) {
+				current = current->getNextToken();
+				continue;
 			}
 		}
+		counter++;
+		if (counter == 2) {
+			current->setValue("auto", "pbar", "true");
+			break;
+		}
+		current = current->getNextToken();
 	}
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_myank::processFieldEntry --
-//   3-6 expands to 3 4 5 6
-//   $   expands to maximum spine track
-//   $0  expands to maximum spine track
-//   $1  expands to maximum spine track minus 1, etc.
-//   2-$1 expands to 2 through the maximum minus one.
-//   6-3 expands to 6 5 4 3
-//   $2-5 expands to the maximum minus 2 down through 5.
-//   Ignore negative values and values which exceed the maximum value.
+// Tool_gridtest::Tool_pccount -- Set the recognized options for the tool.
 //
 
-void Tool_myank::processFieldEntry(vector<MeasureInfo>& field,
-		const string& str, HumdrumFile& infile, int maxmeasure,
-		vector<MeasureInfo>& inmeasures, vector<int>& inmap) {
+Tool_pccount::Tool_pccount(void) {
+	define("a|attacks=b",                 "count attacks instead of durations");
+	define("d|data|vega-data=b",          "display the vega-lite template.");
+	define("f|full=b",                    "full count attacks all single sharps and flats.");
+	define("ff|double-full=b",            "full count attacks all double sharps and flats.");
+	define("h|html=b",                    "generate vega-lite HTML content");
+	define("i|id=s:id",                   "ID for use as variable and in plot title");
+	define("K|no-key|no-final=b",         "do not label key tonic or final");
+	define("m|maximum=b",                 "normalize by maximum count");
+	define("n|normalize=b",               "normalize counts");
+	define("p|page=b",                    "generate vega-lite stand-alone HTML page");
+	define("r|ratio|aspect-ratio=d:0.67", "width*ratio=height of vega-lite plot");
+	define("s|script|vega-script=b",      "generate vega-lite javascript content");
+	define("title=s",                     "title for plot");
+	define("t|template|vega-template=b",  "display the vega-lite template.");
+	define("w|width=i:400",               "width of vega-lite plot");
+}
 
-	MeasureInfo current;
 
-	HumRegex hre;
-	string buffer = str;
 
-	// remove any comma left at end of input string (or anywhere else)
-	hre.replaceDestructive(buffer, "", ",", "g");
+///////////////////////////////
+//
+// Tool_pccount::run -- Primary interfaces to the tool.
+//
 
-	string measureStyling = "";
-	if (hre.search(buffer, "([|:!=]+)$")) {
-		measureStyling = hre.getMatch(1);
-		hre.replaceDestructive(buffer, "", "([|:!=]+)$");
+bool Tool_pccount::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
+}
 
-	if (hre.search(buffer, "^(\\d+)[a-z]?-(\\d+)[a-z]?$")) {
-		// processing a measure range
-		int firstone = hre.getMatchInt(1);
-		int lastone  = hre.getMatchInt(2);
 
-		// limit the range to 0 to maxmeasure
-		if (firstone > maxmeasure) { firstone = maxmeasure; }
-		if (lastone  > maxmeasure) { lastone  = maxmeasure; }
-		if (firstone < 0         ) { firstone = 0         ; }
-		if (lastone  < 0         ) { lastone  = 0         ; }
+bool Tool_pccount::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
+}
 
-		if ((firstone < 1) && (firstone != 0)) {
-			cerr << "Error: range token: \"" << str << "\""
-				  << " contains too small a number at start: " << firstone << endl;
-			cerr << "Minimum number allowed is " << 1 << endl;
-			exit(1);
+
+bool Tool_pccount::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	return status;
+}
+
+
+bool Tool_pccount::run(HumdrumFile& infile) {
+   initialize(infile);
+	processFile(infile);
+	return true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_pccount::initialize --
+//
+
+void Tool_pccount::initialize(HumdrumFile& infile) {
+	m_attack     = getBoolean("attacks");
+	m_full       = getBoolean("full");
+	m_doublefull = getBoolean("double-full");
+	m_normalize  = getBoolean("normalize");
+	m_maximum    = getBoolean("maximum");
+	m_template   = getBoolean("vega-template");
+	m_data       = getBoolean("vega-data");
+	m_script     = getBoolean("vega-script");
+	m_width      = getInteger("width");
+	m_ratio      = getDouble("aspect-ratio");
+	m_key        = !getBoolean("no-key");
+	if (getBoolean("title")) {
+		m_title = getString("title");
+	}
+	m_html       = getBoolean("html");
+	m_page       = getBoolean("page");
+	if (getBoolean("id")) {
+		m_id = getString("id");
+	} else {
+		string filename = infile.getFilename();
+	 	auto pos = filename.rfind("/");
+		if (pos != string::npos) {
+			filename = filename.substr(pos+1);
 		}
-		if ((lastone < 1) && (lastone != 0)) {
-			cerr << "Error: range token: \"" << str << "\""
-				  << " contains too small a number at end: " << lastone << endl;
-			cerr << "Minimum number allowed is " << 1 << endl;
-			exit(1);
+		pos = filename.find("-");
+		if (pos != string::npos) {
+			m_id = filename.substr(0, pos);
 		}
+	}
+	m_parttracks.clear();
+	m_names.clear();
+	m_abbreviations.clear();
+	initializePartInfo(infile);
 
-		if (firstone > lastone) {
-			// reverse the order of the measures
-			for (int i=firstone; i>=lastone; i--) {
-				if (inmap[i] >= 0) {
-					current.clear();
-					current.file = &infile;
-					current.num = i;
-					current.start = inmeasures[inmap[i]].start;
-					current.stop = inmeasures[inmap[i]].stop;
 
-					current.sclef    = inmeasures[inmap[i]].sclef;
-					current.skeysig  = inmeasures[inmap[i]].skeysig;
-					current.skey     = inmeasures[inmap[i]].skey;
-					current.stimesig = inmeasures[inmap[i]].stimesig;
-					current.smet     = inmeasures[inmap[i]].smet;
-					current.stempo   = inmeasures[inmap[i]].stempo;
+	// https://encycolorpedia.com/36cd27
+	m_vcolor.clear();
 
-					current.eclef    = inmeasures[inmap[i]].eclef;
-					current.ekeysig  = inmeasures[inmap[i]].ekeysig;
-					current.ekey     = inmeasures[inmap[i]].ekey;
-					current.etimesig = inmeasures[inmap[i]].etimesig;
-					current.emet     = inmeasures[inmap[i]].emet;
-					current.etempo   = inmeasures[inmap[i]].etempo;
+	m_vcolor["Canto"]		=	"#e49689";
+	m_vcolor["Canto (Canto I)"]	=	"#e49689";
+	m_vcolor["Canto I"]		=	"#e49689";
+	m_vcolor["Canto Primo"]		=	"#e49689";
+	m_vcolor["[Canto 1]"]		=	"#e49689";
+	m_vcolor["[Canto]"]		=	"#e49689";
+	m_vcolor["[Soprano o Tenore]"]	=	"#e49689";
+	m_vcolor["Soprano"]		=	"#e49689";
 
-					field.push_back(current);
-				}
-			}
-		} else {
-			// measure range not reversed
-			for (int i=firstone; i<=lastone; i++) {
-				if (inmap[i] >= 0) {
-					current.clear();
-					current.file = &infile;
-					current.num = i;
-					current.start = inmeasures[inmap[i]].start;
-					current.stop = inmeasures[inmap[i]].stop;
+	m_vcolor["Canto 2."]		=	"#d67365";
+	m_vcolor["Canto II"]		=	"#d67365";
+	m_vcolor["Canto II [Sesto]"]	=	"#d67365";
+	m_vcolor["Canto Sec."]		=	"#d67365";
+	m_vcolor["Canto Secondo"]	=	"#d67365";
+	m_vcolor["Canto secondo"]	=	"#d67365";
+	m_vcolor["[Canto 2]"]		=	"#d67365";
 
-					current.sclef    = inmeasures[inmap[i]].sclef;
-					current.skeysig  = inmeasures[inmap[i]].skeysig;
-					current.skey     = inmeasures[inmap[i]].skey;
-					current.stimesig = inmeasures[inmap[i]].stimesig;
-					current.smet     = inmeasures[inmap[i]].smet;
-					current.stempo   = inmeasures[inmap[i]].stempo;
+	m_vcolor["Canto III [Settimo]"]	=	"#c54f43";
 
-					current.eclef    = inmeasures[inmap[i]].eclef;
-					current.ekeysig  = inmeasures[inmap[i]].ekeysig;
-					current.ekey     = inmeasures[inmap[i]].ekey;
-					current.etimesig = inmeasures[inmap[i]].etimesig;
-					current.emet     = inmeasures[inmap[i]].emet;
-					current.etempo   = inmeasures[inmap[i]].etempo;
+	m_vcolor["Alto"]		=	"#f4c6a1";
+	m_vcolor["Alti"]		=	"#f4c6a1";
+	m_vcolor["Alto (Canto III)"]	=	"#f4c6a1";
 
-					field.push_back(current);
-				}
-			}
-		}
-	} else if (hre.search(buffer, "^(\\d+)([a-z]*)")) {
-		// processing a single measure number
-		int value = hre.getMatchInt(1);
-		// do something with letter later...
+	m_vcolor["Alto II"]		=	"#edb383";
 
-		if ((value < 1) && (value != 0)) {
-			cerr << "Error: range token: \"" << str << "\""
-				  << " contains too small a number at end: " << value << endl;
-			cerr << "Minimum number allowed is " << 1 << endl;
-			exit(1);
-		}
-		if (inmap[value] >= 0) {
-			current.clear();
-			current.file = &infile;
-			current.num = value;
-			current.start = inmeasures[inmap[value]].start;
-			current.stop = inmeasures[inmap[value]].stop;
+	m_vcolor["Tenor"]		=	"#ecdf7a";
+	m_vcolor["Tenore"]		=	"#ecdf7a";
+	m_vcolor["Tenore over Canto"]	=	"#ecdf7a";
+	m_vcolor["[Tenore]"]		=	"#ecdf7a";
 
-			current.sclef    = inmeasures[inmap[value]].sclef;
-			current.skeysig  = inmeasures[inmap[value]].skeysig;
-			current.skey     = inmeasures[inmap[value]].skey;
-			current.stimesig = inmeasures[inmap[value]].stimesig;
-			current.smet     = inmeasures[inmap[value]].smet;
-			current.stempo   = inmeasures[inmap[value]].stempo;
+	m_vcolor["Sesto"]		=	"#c8f0bb";
+	m_vcolor["Sesto (Canto II)"]	=	"#c8f0bb";
+	m_vcolor["Sesto Canto II"]	=	"#c8f0bb";
 
-			current.eclef    = inmeasures[inmap[value]].eclef;
-			current.ekeysig  = inmeasures[inmap[value]].ekeysig;
-			current.ekey     = inmeasures[inmap[value]].ekey;
-			current.etimesig = inmeasures[inmap[value]].etimesig;
-			current.emet     = inmeasures[inmap[value]].emet;
-			current.etempo   = inmeasures[inmap[value]].etempo;
+	m_vcolor["Quinto"]		=	"#e3f5f8";
+	m_vcolor["Qvinto"]		=	"#e3f5f8";
 
-			field.push_back(current);
-		}
-	}
+	m_vcolor["Ottava parte [Ottavo]"]	=	"#e0e4f7";
 
-	field.back().stopStyle = measureStyling;
+	m_vcolor["Nona parte [Nono]"]	=	"#a39ce5";
+
+	m_vcolor["Basso"]		=	"#d2aef7";
+	m_vcolor["Bass"]		=	"#d2aef7";
+
+	m_vcolor["Basso II"]		=	"#c69af5";
+	m_vcolor["Basso II [Decimo]"]	=	"#c69af5";
 
+	m_vcolor["Basso Continuo"]	=	"#a071ec";
+	m_vcolor["Basso continuo"]	=	"#a071ec";
+	m_vcolor["[B. C.]"]		=	"#a071ec";
+	m_vcolor["[Basso Continuo]"]	=	"#a071ec";
+	m_vcolor["[Basso continuo]"]	=	"#a071ec";
+	m_vcolor["B.C."]		=	"#a071ec";
+	m_vcolor["B.c."]		=	"#a071ec";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::removeDollarsFromString -- substitute $ sign for maximum track count.
+// Tool_pccount::getFinal -- Extract the last unparenthesed letter from a ref record like this:
+//
+// !!!final: (A)D
 //
 
-void Tool_myank::removeDollarsFromString(string& buffer, int maxx) {
+string Tool_pccount::getFinal(HumdrumFile& infile) {
+	string finalref = infile.getReferenceRecord("final");
 	HumRegex hre;
-	HumRegex hre2;
-	string tbuf;
-	string obuf;
-	int outval;
-	int value;
-
-	if (m_debugQ) {
-		m_free_text << "MEASURE STRING BEFORE DOLLAR REMOVAL: " << buffer << endl;
+	hre.replaceDestructive(finalref, "", "\\(.*?\\)", "g");
+	hre.replaceDestructive(finalref, "", "\\s+", "g");
+	if (hre.search(finalref, "^[A-G]$", "i")) {
+		return finalref;
+	} else {
+		return "";
 	}
+}
 
-	while (hre.search(buffer, "(\\$\\d*)", "")) {
-		tbuf = hre.getMatch(1);
-		if (hre2.search(tbuf, "(\\$\\d+)")) {
-		  sscanf(hre2.getMatch(1).c_str(), "$%d", &value);
-		  outval = maxx - value;
-		} else {
-			outval = maxx;
-		}
 
-		if (outval < 0) {
-			outval = 0;
-		}
 
-		tbuf = to_string(outval);
-		obuf = "\\";
-		obuf += hre.getMatch(1);
-		hre.replaceDestructive(buffer, tbuf, obuf);
+//////////////////////////////
+//
+// Tool_pccount::processFile --
+//
+
+void Tool_pccount::processFile(HumdrumFile& infile) {
+	countPitches(infile);
+
+	string datavar;
+	string target;
+	string jsonvar;
+
+	if (m_attack) {
+		datavar = "data_" + m_id + "_count";
+		target = "id_" + m_id + "_count";
+		jsonvar = "vega_" + m_id + "_count";
+	} else {
+		datavar = "data_" + m_id + "_dur";
+		target = "id_" + m_id + "_dur";
+		jsonvar = "vega_" + m_id + "_dur";
 	}
-	if (m_debugQ) {
-		m_free_text << "DOLLAR EXPAND: " << buffer << endl;
+
+	if (m_template) {
+		printVegaLiteJsonTemplate(datavar, infile);
+	} else if (m_data) {
+		printVegaLiteJsonData();
+	} else if (m_script) {
+		printVegaLiteScript(jsonvar, target, datavar, infile);
+	} else if (m_html) {
+		printVegaLiteHtml(jsonvar, target, datavar, infile);
+	} else if (m_page) {
+		printVegaLitePage(jsonvar, target, datavar, infile);
+	} else {
+		printHumdrumTable();
 	}
 }
 
 
 
+//////////////////////////////
+//
+// Tool_pccount::printVegaLitePage --
+//
+
+void Tool_pccount::printVegaLitePage(const string& jsonvar,
+		const string& target, const string& datavar, HumdrumFile& infile) {
+	stringstream& out = m_free_text;
+
+	out << "<!DOCTYPE html>\n";
+	out << "<html>\n";
+	out << "  <head>\n";
+	out << "    <title>Vega-Lite Bar Chart</title>\n";
+	out << "    <meta charset=\"utf-8\" />\n";
+	out << "\n";
+	out << "    <script src=\"https://cdn.jsdelivr.net/npm/vega@5.4.0\"></script>\n";
+	out << "    <script src=\"https://cdn.jsdelivr.net/npm/vega-lite@4.0.0-beta.1\"></script>\n";
+	out << "    <script src=\"https://cdn.jsdelivr.net/npm/vega-embed@5\"></script>\n";
+	out << "\n";
+	out << "    <style media=\"screen\">\n";
+	out << "      /* Add space between Vega-Embed links  */\n";
+	out << "      .vega-actions a {\n";
+	out << "        margin-right: 5px;\n";
+	out << "      }\n";
+	out << "    </style>\n";
+	out << "  </head>\n";
+	out << "  <body>\n";
+	out << "    <h1>Pitch-class histogram</h1>\n";
+	printVegaLiteHtml(jsonvar, target, datavar, infile);
+	out << "</body>\n";
+	out << "</html>\n";
+}
+
+
+
+//////////////////////////////
+//
+// Tool_pccount::printVegaLiteHtml --
+//
+
+void Tool_pccount::printVegaLiteHtml(const string& jsonvar,
+		const string& target, const string& datavar, HumdrumFile& infile) {
+	stringstream& out = m_free_text;
+
+	out << "<div class=\"vega-svg\" id=\"" << target << "\"></div>\n";
+	out << "\n";
+	out << "<script>\n";
+	printVegaLiteScript(jsonvar, target, datavar, infile);
+	out << "</script>\n";
+}
 
 
 
 //////////////////////////////
 //
-// Tool_myank::example -- example function calls to the program.
+// Tool_pccount::printVegaLiteScript --
 //
 
-void Tool_myank::example(void) {
-
+void Tool_pccount::printVegaLiteScript(const string& jsonvar,
+		const string& target, const string& datavar, HumdrumFile& infile) {
+	stringstream& out = m_free_text;
 
+	out << "var " << datavar << " =\n";
+	printVegaLiteJsonData();
+	out << ";\n";
+	out << "\n";
+	out << "var " << jsonvar << " =\n";
+	printVegaLiteJsonTemplate(datavar, infile);
+	out << ";\n";
+	out << "vegaEmbed('#" << target << "', " << jsonvar << ");\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_myank::usage -- command-line usage description and brief summary
+// Tool_pccount::printVegaLiteJsonData --
 //
 
-void Tool_myank::usage(const string& ommand) {
+void Tool_pccount::printVegaLiteJsonData(void) {
+	stringstream& out = m_free_text;
 
+	m_maxpc = 0;
+	for (int i=0; i<(int)m_counts[0].size(); i++) {
+		if (m_counts[0][i] > m_maxpc) {
+			m_maxpc = m_counts[0][i];
+		}
+	}
+	out << "[\n";
+	int commacounter = 0;
+	double percent = 100.0;
+	for (int i=1; i<(int)m_counts.size(); i++) {
+		for (int j=0; j<(int)m_counts[i].size(); j++) {
+			if (m_counts[i][j] == 0.0) {
+				continue;
+			}
+			if (commacounter > 0) {
+				out << ",\n\t";
+			} else {
+				out << "\t";
+			}
+			commacounter++;
+			if (m_attack) {
+				out << "{\"count\":" << m_counts[i][j] << ", ";
+			} else {
+				out << "{\"percent\":" << m_counts[i][j]/m_maxpc*percent << ", ";
+			}
+			out << "\"pitch class\":\"" << getPitchClassString(j) << "\", ";
+			out << "\"voice\":\"" << m_names[i] << "\"";
+			out << "}";
+		}
+	}
+	out << "\n]\n";
 }
 
 
 
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_nproof::Tool_nproof -- Set the recognized options for the tool.
+// Tool_pccount::setFactorMaximum -- normalize by the maximum pitch-class value.
 //
 
-Tool_nproof::Tool_nproof(void) {
-	define("B|no-blank|no-blanks=b",                 "do not check for blank lines.\n");
-	define("b|only-blank|only-blanks=b",             "only check for blank lines.\n");
-
-	define("I|no-instrument|no-instruments=b",       "do not check instrument interpretations.\n");
-	define("i|only-instrument|only-instruments=b",   "only check instrument interpretations.\n");
+void Tool_pccount::setFactorMaximum(void) {
+	m_factor = 0.0;
+	for (int i=0; i<(int)m_counts[0].size(); i++) {
+		if (m_counts[0][i] > m_factor) {
+			m_factor = m_counts[0][i];
+		}
+	}
+}
 
-	define("K|no-key=b",                             "do not check for !!!key: manual initial key designation.\n");
-	define("k|only-key=b",                           "only check for !!!key: manual initial key designation.\n");
 
-	define("R|no-reference=b",                       "do not check for reference records.\n");
-	define("r|only-reference=b",                     "only check for reference records.\n");
 
-	define("T|no-termination|no-terminations=b",     "do not check spine terminations.\n");
-	define("t|only-termination|only-terminations=b", "only check spine terminations.\n");
+//////////////////////////////
+//
+// Tool_pccount::setFactorNormalize -- normalize by the sum of all pitch class values.
+//
 
-	define("file|filename=b",                        "print filename with raw count (if available).\n");
-	define("raw=b",                                  "only print error count.\n");
+void Tool_pccount::setFactorNormalize(void) {
+	m_factor = 0.0;
+	for (int i=0; i<(int)m_counts[0].size(); i++) {
+		m_factor += m_counts[0][i];
+	}
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_nproof::run -- Do the main work of the tool.
+// Tool_pccount::printHumdrumTable --
 //
 
-bool Tool_nproof::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+void Tool_pccount::printHumdrumTable(void) {
+
+	double factor = 0.0;
+
+	if (m_maximum) {
+		setFactorMaximum();
+		m_free_text << "!!!MAX: " << m_factor << endl;
+	} else if (m_normalize) {
+		setFactorNormalize();
+		m_free_text << "!!!TOTAL: " << factor << endl;
 	}
-	return status;
-}
 
+	// exclusive interpretation
+	m_free_text << "**kern";
+	m_free_text << "\t**all";
+	for (int i=0; i<(int)m_counts.size() - 1; i++) {
+		m_free_text << "\t**part";
+	}
+	m_free_text << endl;
 
-bool Tool_nproof::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	// part names
+	m_free_text << "*";
+	for (int i=0; i<(int)m_counts.size(); i++) {
+		if (i < (int)m_names.size()) {
+			m_free_text << "\t*I\"" << m_names.at(i);
+		} else {
+			m_free_text << "\t*";
+		}
 	}
-	return status;
-}
+	m_free_text << endl;
 
+	if (!m_abbreviations.empty()) {
 
-bool Tool_nproof::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+		// part abbreviation
+		m_free_text << "*";
+		for (int i=0; i<(int)m_counts.size(); i++) {
+			if (i < (int)m_abbreviations.size()) {
+				m_free_text << "\t*I\'" << m_abbreviations.at(i);
+			} else {
+				m_free_text << "\t*";
+			}
+		}
+		m_free_text << endl;
 	}
-	return status;
-}
 
+	for (int i=0; i<(int)m_counts[0].size(); i++) {
+		if (m_counts[0][i] == 0) {
+			continue;
+		}
+		if ((i == 5) || (i == 11) || (i == 22) || (i == 28) || (i == 34)) {
+			continue;
+		}
+		string pitch = Convert::base40ToKern(i + 4*40);
+		m_free_text << pitch;
+		for (int j=0; j<(int)m_counts.size(); j++) {
+			if (m_normalize) {
+				m_free_text << "\t" << m_counts[j][i] / m_factor;
+			} else if (m_maximum) {
+				m_free_text << "\t" << m_counts[j][i] / m_factor;
+			} else {
+				m_free_text << "\t" << m_counts[j][i];
+			}
+		}
+		m_free_text << endl;
+	}
 
-bool Tool_nproof::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
+	int columns = (int)m_counts.size() + 1;
+	for (int i=0; i<columns; i++) {
+		m_free_text << "*-";
+		if (i < columns - 1) {
+			m_free_text << "\t";
+		}
+	}
+	m_free_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_nproof::initialize --
+// Tool_pccount::countPitches --
 //
 
-void Tool_nproof::initialize(void) {
-	m_noblankQ       = getBoolean("no-blank");
-	m_noinstrumentQ  = getBoolean("no-instrument");
-	m_nokeyQ         = getBoolean("no-key");
-	m_noreferenceQ   = getBoolean("no-reference");
-	m_noterminationQ = getBoolean("no-termination");
-
-	bool onlyBlank       = getBoolean("only-blank");
-	bool onlyInstrument  = getBoolean("only-instrument");
-	bool onlyKey         = getBoolean("only-key");
-	bool onlyReference   = getBoolean("only-reference");
-	bool onlyTermination = getBoolean("only-termination");
+void Tool_pccount::countPitches(HumdrumFile& infile) {
+	if (m_parttracks.size() == 0) {
+		return;
+	}
+	m_counts.clear();
+	m_counts.resize(m_parttracks.size());
+	for (int i=0; i<(int)m_parttracks.size(); i++) {
+		m_counts[i].resize(40);
+		fill(m_counts[i].begin(), m_counts[i].end(), 0.0);
+	}
+	for (int i=0; i<infile.getStrandCount(); i++) {
+		HTp sstart = infile.getStrandStart(i);
+		HTp send = infile.getStrandEnd(i);
+		addCounts(sstart, send);
+	}
 
-	if (onlyBlank || onlyInstrument || onlyKey || onlyReference || onlyTermination) {
-		m_noblankQ       = !onlyBlank;
-		m_noinstrumentQ  = !onlyInstrument;
-		m_nokeyQ         = !onlyKey;
-		m_noreferenceQ   = !onlyReference;
-		m_noterminationQ = !onlyTermination;
+	// fill in sum for all parts
+	for (int i=0; i<(int)m_counts[0].size(); i++) {
+		for (int j=1; j<(int)m_counts.size(); j++) {
+			m_counts[0][i] += m_counts[j][i];
+		}
 	}
 
-	m_fileQ          = getBoolean("file");
-	m_rawQ           = getBoolean("raw");
 }
 
 
-
 //////////////////////////////
 //
-// Tool_nproof::processFile --
+// Tool_pccount::addCounts --
 //
 
-void Tool_nproof::processFile(HumdrumFile& infile) {
-	m_errorCount = 0;
-	m_errorList = "";
-	m_errorHtml = "";
-
-	if (!m_noblankQ) {
-		checkForBlankLines(infile);
-	}
-	if (!m_nokeyQ) {
-		checkKeyInformation(infile);
-	}
-	if (!m_noinstrumentQ) {
-		checkInstrumentInformation(infile);
-	}
-	if (!m_noreferenceQ) {
-		checkReferenceRecords(infile);
-	}
-	if (!m_noterminationQ) {
-		checkSpineTerminations(infile);
+void Tool_pccount::addCounts(HTp sstart, HTp send) {
+	if (!sstart) {
+		return;
 	}
-
-	m_humdrum_text << infile;
-
-	if (m_rawQ) {
-		// print error count only.
-		if (m_fileQ) {
-			m_free_text << infile.getFilename() << "\t";
-		}
-		m_free_text << m_errorCount << endl;
+	if (!sstart->isKern()) {
 		return;
 	}
-
-	if (m_errorCount > 0) {
-		m_humdrum_text << m_errorList;
-		m_humdrum_text << "!!!TOOL-nproof-error-count: " << m_errorCount << endl;
-		m_humdrum_text << "!!@@BEGIN: PREHTML\n";
-		m_humdrum_text << "!!@TOOL: nproof\n";
-		m_humdrum_text << "!!@CONTENT:\n";
-		m_humdrum_text << "!! <h2 style='color:red'> @{TOOL-nproof-error-count} problem";
-		if (m_errorCount != 1) {
-			m_humdrum_text << "s";
+	int track = sstart->getTrack();
+	int kindex = m_rkern[track];
+	HTp current = sstart;
+	while (current && (current != send)) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		m_humdrum_text << " detected </h2>\n";
-		m_humdrum_text << "!! <ul style='color:darkred'>\n";
-		m_humdrum_text << m_errorHtml;
-		m_humdrum_text << "!! </ul>\n";
-		m_humdrum_text << "!!@@END: PREHTML\n";
-	} else {
-		m_humdrum_text << "!!@@BEGIN: PREHTML\n";
-		m_humdrum_text << "!!@TOOL: nproof\n";
-		m_humdrum_text << "!!@CONTENT:\n";
-		m_humdrum_text << "!! <h2 style='color:red'> No problems detected </h2>\n";
-		m_humdrum_text << "!!@@END: PREHTML\n";
+		if (current->isNull() || current->isRest()) {
+			current = current->getNextToken();
+			continue;
+		}
+		vector<string> subtokens = current->getSubtokens();
+		for (int i=0; i<(int)subtokens.size(); i++) {
+			if (m_attack) {
+				// ignore sustained parts of notes when counting attacks
+				if (subtokens[i].find("_") != string::npos) {
+					continue;
+				}
+				if (subtokens[i].find("]") != string::npos) {
+					continue;
+				}
+			}
+			int b40 = Convert::kernToBase40(subtokens[i]);
+			if (m_attack) {
+				m_counts[kindex][b40%40]++;
+			} else {
+				double duration = Convert::recipToDuration(subtokens[i]).getFloat();
+				m_counts[kindex][b40%40] += duration;
+			}
+		}
+		current = current->getNextToken();
 	}
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_nproof::checkForBlankLines --
+// Tool_pccount::initializePartInfo --
 //
 
-void Tool_nproof::checkForBlankLines(HumdrumFile& infile) {
-	vector<int> blanks;
-	// -1: Not checking for a blank line at the very end of the score.
-	for (int i=0; i<infile.getLineCount() - 1; i++) {
-		if (infile[i].hasSpines()) {
+void Tool_pccount::initializePartInfo(HumdrumFile& infile) {
+	m_names.clear();
+	m_abbreviations.clear();
+	m_parttracks.clear();
+	m_rkern.clear();
+
+	m_rkern.resize(infile.getTrackCount() + 1);
+	fill(m_rkern.begin(), m_rkern.end(), -1);
+
+	m_parttracks.push_back(-1);
+	m_names.push_back("all");
+	m_abbreviations.push_back("all");
+
+	vector<HTp> starts = infile.getKernSpineStartList();
+
+	int foundpart = false;
+	int foundabbr = false;
+
+	int track = 0;
+	for (int i=0; i<(int)starts.size(); i++) {
+		track = starts[i]->getTrack();
+		m_rkern[track] = i+1;
+		m_parttracks.push_back(track);
+		HTp current = starts[i];
+		foundpart = false;
+		foundabbr = false;
+		if (!current->isKern()) {
 			continue;
 		}
-		HTp token = infile.token(i, 0);
-		if (*token == "") {
-			blanks.push_back(i+1);
+		while (current) {
+			if (current->isData()) {
+				break;
+			}
+			if ((!foundpart) && (current->compare(0, 3, "*I\"") == 0)) {
+				m_names.emplace_back(current->substr(3));
+				foundpart = true;
+			} else if ((!foundabbr) && (current->compare(0, 3, "*I\'") == 0)) {
+				m_abbreviations.emplace_back(current->substr(3));
+				foundabbr = true;
+			}
+			current = current->getNextToken();
 		}
+		//if (!foundpart) {
+		//		m_names.emplace_back("");
+		//}
+		//if (!foundabbr) {
+		//		m_names.emplace_back("");
+		//}
 	}
 
-	if (blanks.empty()) {
-		return;
-	}
-
-	m_errorCount++;
-	m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Blank lines on row";
-	if (blanks.size() != 1) {
-		m_errorList += "s";
-	}
-	m_errorList += ": ";
-	for (int i=0; i<(int)blanks.size(); i++) {
-		m_errorList += to_string(blanks[i]);
-		if (i < (int)blanks.size() - 1) {
-			m_errorList += ", ";
-		}
-	}
-	m_errorList += ".\n";
-	m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
 }
 
 
-
 //////////////////////////////
 //
-// Tool_nproof::checkForValidInstrumentCode --
+// printVegaLiteJsonTemplate --
 //
 
-void Tool_nproof::checkForValidInstrumentCode(HTp token,
-		vector<pair<string, string>>& instrumentList) {
+void Tool_pccount::printVegaLiteJsonTemplate(const string& datavariable, HumdrumFile& infile) {
+	stringstream& out = m_free_text;
 
-	if ((token->find("&") == string::npos) && (token->find("|") == string::npos)) {
-		string code = token->substr(2);
-		for (int i=0; i<(int)instrumentList.size(); i++) {
-			if (instrumentList[i].first == code) {
-				return;
-			}
+	string idinfo;
+	if (m_id.empty() || m_id == "id") {
+		// do nothing
+	} else {
+		idinfo = "for " + m_id;
+	}
+	out << "{\n";
+	out << "	\"$schema\": \"https://vega.github.io/schema/vega-lite/v4.0.0-beta.1.json\",\n";
+	out << "	\"data\": {\"values\": " << datavariable << "},\n";
+	if (getBoolean("title")) {
+		out << "	\"title\": \"" << m_title << "\",\n";
+	} else {
+		if (m_attack) {
+			out << "	\"title\": \"Note-count pitch-class distribution " << idinfo <<" \",\n";
+		} else {
+			out << "	\"title\": \"Duration-weighted pitch-class distribution " << idinfo <<" \",\n";
 		}
-
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown instrument code \"" + code + "\" on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ". See list of codes at <a target='_blank' href='https://bit.ly/humdrum-instrument-codes'>https://bit.ly/humdrum-instrument-codes</a>.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		return;
 	}
+	out << "	\"width\": " << m_width << ",\n";
+	out << "	\"height\": " << int(m_width * m_ratio) << ",\n";
+	out << "	\"encoding\": {\n";
+	out << "		\"y\": {\n";
+	if (m_attack) {
+		out << "			\"field\": \"count\",\n";
+		out << "			\"title\": \"Number of note attacks\",\n";
+	} else {
+		out << "			\"field\": \"percent\",\n";
+		out << "			\"title\": \"Percent of maximum pitch class\",\n";
+	}
+	out << "			\"type\": \"quantitative\",\n";
+	if (m_attack) {
+		out << "			\"scale\": {\"domain\": [0, " << m_maxpc << "]},\n";
+	} else {
+		out << "			\"scale\": {\"domain\": [0, 100]},\n";
+	}
+	out << "			\"aggregate\": \"sum\"\n";
+	out << "		},\n";
+	out << "		\"x\": {\n";
+	out << "			\"field\": \"pitch class\",\n";
+	out << "			\"type\": \"nominal\",\n";
+	out << "			\"scale\": {\n";
+	out << "				\"domain\": [";
+		printPitchClassList();
+		out << "]\n";
+	out << "			},\n";
+	out << "			\"axis\": {\n";
+	out << "				\"labelAngle\": 0\n";
+	out << "			}\n";
+	out << "		},\n";
+	out << "		\"order\": {\"type\": \"quantitative\"},\n";
+	out << "		\"color\": {\n";
+	out << "			\"field\": \"voice\",\n";
+	out << "			\"type\": \"nominal\",\n";
+	if (m_counts.size() == 2) {
+		out << "			\"legend\": {\"title\": \"Voice\"},\n";
+	} else {
+		out << "			\"legend\": {\"title\": \"Voices\"},\n";
+	}
+	out << "			\"scale\": {\n";
+  	out << "				\"domain\": [";
+		printVoiceList();
+		out << "],\n";
+ 	out << "				\"range\": [";
+		printColorList();
+		out << "],\n";
+ 	out << "				}\n";
+	out << "		}\n";
+	out << "	},\n";
 
-	bool found1 = false;
-	bool found2 = false;
-	string inst1;
-	string inst2;
-	HumRegex hre;
-	if (hre.match(token, "^\\*I(.*)[&|](I.*)")) {
-		inst1 = hre.getMatch(1);
-		inst2 = hre.getMatch(2);
+	out << "	\"layer\": [\n";
+	out << "		{\"mark\": \"bar\"}";
 
-		for (int i=0; i<(int)instrumentList.size(); i++) {
-			if (instrumentList[i].first == inst1) {
-				found1 = true;
-			}
-			if (instrumentList[i].first == inst2) {
-				found2 = true;
-			}
+	string final = getFinal(infile);
+	if (m_key && !final.empty()) {
+		out << ",\n";
+		out << "		{\n";
+		out << "			\"mark\": {\"type\":\"text\", \"align\":\"center\", \"fill\":\"black\", \"baseline\":\"bottom\"},\n";
+		if (m_attack) {
+			int count = getCount(final);
+			out << "			\"data\": {\"values\": [ {\"pitch class\":\"" << final << "\", \"count\":" << count << "}]},\n";
+		} else {
+			double percent = getPercent(final);
+			out << "			\"data\": {\"values\": [ {\"pitch class\":\"" << final << "\", \"percent\":" << percent << "}]},\n";
 		}
+		out << "			\"encoding\": {\"text\": {\"value\":\"final\"}}\n";
+		out << "		}\n";
 	}
 
-	if (!found1) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown instrument code \"" + inst1 + "\" in token " + *token + " on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ". See list of codes at <a target='_blank' href='https://bit.ly/humdrum-instrument-codes'>https://bit.ly/humdrum-instrument-codes</a>.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
-
-	if (!found2) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown instrument code \"" + inst2 + "\" in token " + *token + " on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ". See list of codes at <a target='_blank' href='https://bit.ly/humdrum-instrument-codes'>https://bit.ly/humdrum-instrument-codes</a>.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
+	out << "	]\n";
+	out << "}\n";
 
 }
 
@@ -112075,426 +116477,234 @@ void Tool_nproof::checkForValidInstrumentCode(HTp token,
 
 //////////////////////////////
 //
-// Tool_nproof::checkInstrumentInformation --
+// Tool_pccount::getCount --
 //
 
-void Tool_nproof::checkInstrumentInformation(HumdrumFile& infile) {
-	int codeLine = -1;
-	int classLine = -1;
-	HumRegex hre;
+int Tool_pccount::getCount(const string& pitchclass) {
+	int b40 = Convert::kernToBase40(pitchclass);
+	int index = b40 % 40;
+	int output = (int)m_counts[0][index];
+	return output;
+}
 
-	vector<pair<string, string>> instrumentList = Convert::getInstrumentList();
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			break;
-		}
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		if (infile[i].isManipulator()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->compare(0, 3, "*IC") == 0) {
-				if (classLine < 0) {
-					classLine = i;
-				}
-			} else if (hre.search(token, "^\\*I[a-z]")) {
-				if (codeLine < 0) {
-					codeLine = i;
-				}
-			}
-		}
-	}
 
-	if (codeLine < 0) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No instrument code line.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	} else {
-		for (int i=0; i<infile[codeLine].getFieldCount(); i++) {
-			HTp token = infile.token(codeLine, i);
-			if (token->isKern()) {
-				if (!hre.search(token, "^\\*I[a-z]")) {
-					m_errorCount++;
-					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": expected instrument code on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
-					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-				} else {
-					checkForValidInstrumentCode(token, instrumentList);
-				}
-			} else {
-				if (*token != "*") {
-					m_errorCount++;
-					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Expected null interpretation on instrument code line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
-					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-				}
-			}
-		}
-	}
+//////////////////////////////
+//
+// Tool_pccount::getPercent --
+//
 
-	if (classLine < 0) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No instrument class line.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	} else {
-		for (int i=0; i<infile[classLine].getFieldCount(); i++) {
-			HTp token = infile.token(classLine, i);
-			if (token->isKern()) {
-				if (!hre.search(token, "^\\*IC[a-z]")) {
-					m_errorCount++;
-					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": expected instrument class on line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
-					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-				}
-			} else {
-				if (*token != "*") {
-					m_errorCount++;
-					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Expected null interpretation on instrument class line " + to_string(token->getLineNumber()) + ", field " + to_string(token->getFieldNumber()) + ".\n";
-					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-				}
-			}
-		}
-	}
+double Tool_pccount::getPercent(const string& pitchclass) {
+	setFactorMaximum();
+	int b40 = Convert::kernToBase40(pitchclass);
+	int index = b40 % 40;
+	double output = m_counts[0][index] / m_factor * 100.0;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_nproof::checkReferenceRecords --
+// Tool_pccount::printColorList --
 //
 
-void Tool_nproof::checkReferenceRecords(HumdrumFile& infile) {
-	vector<int> foundENC;  // Musescore encoder's name
-	vector<int> foundEND;  // Musescore encdoer's date
-	vector<int> foundEED;  // VHV editor's name
-	vector<int> foundEEV;  // VHV editor's date
-
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReferenceRecord()) {
-			continue;
-		}
-		string key = infile[i].getReferenceKey();
-
-		if (hre.search(key, "^EED\\d*$")) {
-			if (key == "EED") {
-				foundEED.push_back(i);
-			}
-			string value = infile[i].getReferenceValue();
-			if (hre.search(value, "^\\d\\d\\d\\d")) {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For EED (Electronic EDitor) record on line " + to_string(i+1) + ", found a date rather than a name: " + value + ".\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-			}
-		}
-		if (hre.search(key, "^EEV\\d*$")) {
-			if (key == "EEV") {
-				foundEEV.push_back(i);;
-			}
-			string value = infile[i].getReferenceValue();
-			if (!hre.search(value, "^\\d\\d\\d\\d-\\d\\d-\\d\\d")) {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For EEV (ElEctronic Version) record on line " + to_string(i+1) + ", found a name rather than a date (or invalid date): " + value + ".\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-			}
-		}
-		if (hre.search(key, "^ENC\\d*(-modern|-iiif)?$")) {
-			string value = infile[i].getReferenceValue();
-			if (hre.search(value, "^\\d\\d\\d\\d-\\d\\d-\\d\\d")) {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For ENC (Electronic eNCoder) record on line " + to_string(i+1) + ", found a date rather than a name: " + value + ".\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-			}
-		}
-		if (hre.search(key, "^END\\d*(-modern|-iiif)?$")) {
-			string value = infile[i].getReferenceValue();
-			if (!hre.search(value, "^\\d\\d\\d\\d-\\d\\d-\\d\\d")) {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": For END (Electronic eNcoding Date) record on line " + to_string(i+1) + ", found a name rather than a date (or an invalid date): " + value + ".\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-			}
-		}
-		if (hre.search(key, "^ENC(\\d*.*)$")) {
-				if (key == "ENC") {
-					foundENC.push_back(i);
-				}
-		}
-		if (hre.search(key, "^ENC-(\\d+.*)$")) {
-				string newvalue = "ENC" + hre.getMatch(1);
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": " + key + " reference record on line " + to_string(i+1) + " should not include a dash and instead be: " + newvalue + ".\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-		if (hre.search(key, "END(\\d*.*)")) {
-				if (key == "END") {
-					foundEND.push_back(i);
-				}
-		}
-		if (hre.search(key, "^END-(\\d+.*)$")) {
-				string newvalue = "END" + hre.getMatch(1);
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": " + key + " reference record on line " + to_string(i+1) + " should not include a dash and instead be: " + newvalue + ".\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-		if (key == "filter-") {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": \"filter-\" reference record on line " + to_string(i+1) + " should probably be \"filter-modern\" instead.\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-		if (key == "ENC-mod") {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": ENC-mod reference record on line " + to_string(i+1) + " should be ENC-modern instead.\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-		if (key == "END-mod") {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": END-mod reference record on line " + to_string(i+1) + " should be END-modern instead.\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-		if (key == "AIN-mod") {
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": AIN-mod reference record on line " + to_string(i+1) + " should be AIN-modern instead.\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+void Tool_pccount::printColorList(void) {
+	stringstream& out = m_free_text;
+	for (int i=(int)m_names.size() - 1; i>0; i--) {
+		string color = m_vcolor[m_names[i]];
+		out << "\"";
+		if (color.empty()) {
+			out << "black";
+		} else {
+			out << color;
 		}
-		if (hre.search(key, "^(.*)-ori$")) {
-				string piece = hre.getMatch(1);
-				m_errorCount++;
-				m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": " + key + " reference record on line " + to_string(i+1) + " should not be used (either use " + piece + "-mod or don't add -ori qualifier to " + piece + ").\n";
-				m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+		out << "\"";
+		if (i > 1) {
+			out << ", ";
 		}
 	}
+}
 
-	// vector<int> foundENC;  // Musescore encoder's name
-	// vector<int> foundEND;  // Musescore encdoer's date
-	// vector<int> foundEED;  // VHV editor's name
-	// vector<int> foundEEV;  // VHV editor's date
 
-	if (foundENC.empty()) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing ENC (initial encoder's name) reference record.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
-	if (foundEND.empty()) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing END (initial encoding date) reference record.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
-	if (foundEED.empty()) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing EED (Humdrum electronic editor's name) reference record.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
-	if (foundEEV.empty()) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Missing EEV (Humdrum electronic edition date) reference record.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
 
-	if ((foundENC.size() == 1) && (foundEED.size() == 1)) {
-		if (foundENC[0] > foundEED[0]) {
-			m_errorCount++;
-			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": ENC reference record on line " + to_string(foundENC[0]+1) + " should come before EED reference record on line " + to_string(foundEED[0]+1) + "\n";
-			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-	}
+//////////////////////////////
+//
+// Tool_pccount::printVoiceList --
+//
 
-	if ((foundEND.size() == 1) && (foundEEV.size() == 1)) {
-		if (foundEND[0] > foundEEV[0]) {
-			m_errorCount++;
-			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": END reference record on line " + to_string(foundEND[0]+1) + " should come before EEV reference record on line " + to_string(foundEEV[0]+1) + "\n";
-			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+void Tool_pccount::printVoiceList(void) {
+	stringstream& out = m_free_text;
+	for (int i=(int)m_names.size() - 1; i>0; i--) {
+		out << "\"";
+		out << m_names[i];
+		out << "\"";
+		if (i > 1) {
+			out << ", ";
 		}
 	}
+}
 
 
-	if ((foundENC.size() == 2) && (foundEED.size() == 0)) {
-		string date1;
-		string date2;
-		if (foundEND.size() == 2) {
-			date1 = infile[foundEND[0]].getReferenceValue();
-			date2 = infile[foundEND[1]].getReferenceValue();
-			hre.replaceDestructive(date1, "", "-", "g");
-			hre.replaceDestructive(date2, "", "-", "g");
-			int number1 = 0;
-			int number2 = 0;
-			if (hre.search(date1, "^(20\\d{6})$")) {
-				number1 = hre.getMatchInt(1);
-			}
-			if (hre.search(date2, "^(20\\d{6})$")) {
-				number2 = hre.getMatchInt(1);
-			}
-			if ((number1 > 0) && (number2 > 0)) {
-				if (number1 > number2) {
-					m_errorCount++;
-					m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Second ENC reference record on line " + to_string(foundENC[1]+1) + " should probably be changed to EED reference record (and second END reference record on line " + to_string(foundEND[1]+1) + " changed to EEV).\n";
-					m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-				}
-			}
-		} else {
-			m_errorCount++;
-			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": There are two ENC records on lines " + to_string(foundENC[0]+1) + " and " + to_string(foundENC[1]+1) + ". The Humdrum editor's name should be changed to EED, and the editing date should be changed from END to EEV if necessary.\n";
-			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+
+//////////////////////////////
+//
+// Tool_pccount::printReverseVoiceList --
+//
+
+void Tool_pccount::printReverseVoiceList(void) {
+	stringstream& out = m_free_text;
+	for (int i=1; i<(int)m_names.size(); i++) {
+		out << "\"";
+		out << m_names[i];
+		out << "\"";
+		if (i < (int)m_names.size() - 1) {
+			out << ", ";
 		}
 	}
-
 }
 
 
 
 //////////////////////////////
 //
-// Tool_nproof::checkKeyInformation --
+// Tool_pccount::printPitchClassList --
 //
 
-void Tool_nproof::checkKeyInformation(HumdrumFile& infile) {
-	int foundKey = -1;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].hasSpines()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (token->compare(0, 7, "!!!key:") == 0) {
-			foundKey = i;
-			break;
-		}
-	}
-
-	if (foundKey < 0) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No <tt>!!!key:</tt> reference record.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		return;
-	}
+void Tool_pccount::printPitchClassList(void) {
+	stringstream& out = m_free_text;
 
-	string value = infile[foundKey].getReferenceValue();
-	if (value.empty()) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": <tt>!!!key:</tt> reference record on line " + to_string(foundKey+1) + " should not be empty.  If no key, then use \"none\".\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		return;
-	}
+	if (m_counts[0][0] > 0.0)  { out << "\"C♭♭\", "; }
+	if (m_counts[0][1] > 0.0)  { out << "\"C♭\", "; }
+	out << "\"C\"";
+	if (m_counts[0][3] > 0.0)  { out << ", \"C♯\""; }
+	if (m_counts[0][4] > 0.0)  { out << ", \"C♯♯\""; }
+	// 5 is empty
 
-	HumRegex hre;
-	if (hre.search(value, "^([a-gA-G][#-n]?):(dor|phr|lyd|mix|aeo|loc|ion)$")) {
-		string tonic = hre.getMatch(1);
-		string mode  = hre.getMatch(2);
-		int major = 0;
-		if ((mode == "lyd") || (mode == "mix") || (mode == "ion")) {
-			major = 1;
-		}
-		int uppercase = isupper(tonic[0]);
-		if ((major == 1) && (uppercase == 0)) {
-			tonic[0] = toupper(tonic[0]);
-			string correct = tonic + ":" + mode;
-			m_errorCount++;
-			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": <tt>!!!key:</tt> reference record on line " + to_string(foundKey + 1) + " should be \"" + correct + "\".\n";
-			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		} else if ((major == 0) && (uppercase == 1)) {
-			tonic[0] = tolower(tonic[0]);
-			string correct = tonic + ":" + mode;
-			m_errorCount++;
-			m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": <tt>!!!key:</tt> reference record on line " + to_string(foundKey + 1) + " should be \"" + correct + "\".\n";
-			m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		}
-	} else if (hre.search(value, "([a-gA-G][#-n]?):(.+)")) {
-		string mode = hre.getMatch(2);
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown mode in <tt>!!!key:</tt> reference record contents on line " + to_string(foundKey + 1) + ": \"" + mode + "\".\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	} else if (!hre.search(value, "([a-gA-G][#-n]?):?")) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Unknown key designation in <tt>!!!key:</tt> reference record contents on line " + to_string(foundKey + 1) + ": \"" + value + "\".\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-	}
+	if (m_counts[0][6] > 0.0)  { out << ", \"D♭♭\""; }
+	if (m_counts[0][7] > 0.0)  { out << ", \"D♭\""; }
+	out << ", \"D\"";
+	if (m_counts[0][9] > 0.0)  { out << ", \"D♯\""; }
+	if (m_counts[0][10] > 0.0) { out << ", \"D♯♯\""; }
+	// 11 is empty
 
-}
+	if (m_counts[0][12] > 0.0) { out << ", \"E♭♭\""; }
+	if (m_counts[0][13] > 0.0) { out << ", \"E♭\""; }
+	out << ", \"E\"";
+	if (m_counts[0][15] > 0.0) { out << ", \"E♯\""; }
+	if (m_counts[0][16] > 0.0) { out << ", \"E♯♯\""; }
 
+	if (m_counts[0][17] > 0.0) { out << ", \"F♭♭\""; }
+	if (m_counts[0][18] > 0.0) { out << ", \"F♭\""; }
+	out << ", \"F\"";
+	if (m_counts[0][20] > 0.0) { out << ", \"F♯\""; }
+	if (m_counts[0][21] > 0.0) { out << ", \"F♯♯\""; }
+	// 22 is empty
 
+	if (m_counts[0][23] > 0.0) { out << ", \"G♭♭\""; }
+	if (m_counts[0][24] > 0.0) { out << ", \"G♭\""; }
+	out << ", \"G\"";
+	if (m_counts[0][26] > 0.0) { out << ", \"G♯\""; }
+	if (m_counts[0][27] > 0.0) { out << ", \"G♯♯\""; }
+	// 28 is empty
 
-//////////////////////////////
-//
-// Tool_nproof::checkSpineTerminations --
-//
+	if (m_counts[0][29] > 0.0) { out << ", \"A♭♭\""; }
+	if (m_counts[0][30] > 0.0) { out << ", \"A♭\""; }
+	out << ", \"A\"";
+	if (m_counts[0][32] > 0.0) { out << ", \"A♯\""; }
+	if (m_counts[0][33] > 0.0) { out << ", \"A♯♯\""; }
+	// 34 is empty
 
-void Tool_nproof::checkSpineTerminations(HumdrumFile& infile) {
-	int foundTerminal = 0;
-	for (int i=infile.getLineCount() - 1; i>0; i--) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (*token == "*-") {
-			foundTerminal = i;
-			break;
-		}
-	}
+	if (m_counts[0][35] > 0.0) { out << ", \"B♭♭\""; }
+	if (m_counts[0][36] > 0.0) { out << ", \"B♭\""; }
+	out << ", \"B\"";
+	if (m_counts[0][38] > 0.0) { out << ", \"B♯\""; }
+	if (m_counts[0][39] > 0.0) { out << ", \"B♯♯\""; }
 
-	if (!foundTerminal) {
-		m_errorCount++;
-		m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": No spine terminators.\n";
-		m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
-		return;
-	}
+}
 
-	bool problem = false;
-	for (int i=0; i<infile[foundTerminal].getFieldCount(); i++) {
-		HTp token = infile[foundTerminal].token(i);
-		string value = token->getSpineInfo();
-		if (value.find(" ") != string::npos) {
-			problem = true;
-			break;
-		}
-	}
 
-	if (!problem) {
-		return;
-	}
+//////////////////////////////
+//
+// Tool_pccount::getPitchClassString --
+//
 
-	m_errorCount++;
-	m_errorList += "!!!TOOL-nproof-error-" + to_string(m_errorCount) + ": Incorrect spine merger(s): ";
-	for (int i=0; i<infile[foundTerminal].getFieldCount(); i++) {
-		HTp token = infile[foundTerminal].token(i);
-		m_errorList += "<" + token->getSpineInfo() + ">";
-		if (i < infile[foundTerminal].getFieldCount() - 1) {
-			m_errorList += " ";
-		}
+string Tool_pccount::getPitchClassString(int b40) {
+	switch (b40%40) {
+		case 0: return "C♭♭";
+		case 1: return "C♭";
+		case 2: return "C";
+		case 3: return "C♯";
+		case 4: return "C♯♯";
+		// 5 is empty
+		case 6: return "D♭♭";
+		case 7: return "D♭";
+		case 8: return "D";
+		case 9: return "D♯";
+		case 10: return "D♯♯";
+		// 11 is empty
+		case 12: return "E♭♭";
+		case 13: return "E♭";
+		case 14: return "E";
+		case 15: return "E♯";
+		case 16: return "E♯♯";
+		case 17: return "F♭♭";
+		case 18: return "F♭";
+		case 19: return "F";
+		case 20: return "F♯";
+		case 21: return "F♯♯";
+		// 22 is empty
+		case 23: return "G♭♭";
+		case 24: return "G♭";
+		case 25: return "G";
+		case 26: return "G♯";
+		case 27: return "G♯♯";
+		// 28 is empty
+		case 29: return "A♭♭";
+		case 30: return "A♭";
+		case 31: return "A";
+		case 32: return "A♯";
+		case 33: return "A♯♯";
+		// 34 is empty
+		case 35: return "B♭♭";
+		case 36: return "B♭";
+		case 37: return "B";
+		case 38: return "B♯";
+		case 39: return "B♯♯";
 	}
-	m_errorList += "\n";
-	m_errorHtml += "!! <li> @{TOOL-nproof-error-" + to_string(m_errorCount) + "} </li>\n";
+
+	return "?";
 }
 
 
 
 
 
+
 /////////////////////////////////
 //
-// Tool_ordergps::Tool_ordergps -- Set the recognized options for the tool.
+// Tool_periodicity::Tool_periodicity -- Set the recognized options for the tool.
 //
 
-Tool_ordergps::Tool_ordergps(void) {
-	define("e|empty=b",   "list files that have no group/part/staff (used with -p option).");
-	define("f|file=b",    "list input files only.");
-	define("l|list=b",    "list files that will be changed.");
-	define("p|problem=b", "list files that have mixed content for *group, *part, *staff info.");
-	define("r|reverse=b", "order *staff, *part, *group");
-	define("s|staff=b",   "Add staff line if none present already in score.");
-	define("t|top=b",     "Place group/part/staff lines first after exinterp.");
+Tool_periodicity::Tool_periodicity(void) {
+	define("m|min=b",         "minimum time unit (other than grace notes)");
+	define("n|max-rows=i:-1", "maxumum number of rows in svg analysis display");
+	define("t|track=i:0",     "track to analyze");
+	define("attacks=b",       "extract attack grid)");
+	define("raw=b",           "show only raw period data");
+	define("s|svg=b",         "output svg image");
+	define("p|power=d:2.0",   "scaling power for visual display");
+	define("1|one=b",         "composite rhythms are not weighted by attack");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_ordergps::run -- Do the main work of the tool.
+// Tool_periodicity::run -- Primary interfaces to the tool.
 //
 
-bool Tool_ordergps::run(HumdrumFileSet& infiles) {
+bool Tool_periodicity::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -112503,9 +116713,10 @@ bool Tool_ordergps::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_ordergps::run(const string& indata, ostream& out) {
+bool Tool_periodicity::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
-	bool status = run(infile);
+	bool status;
+	status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
 	} else {
@@ -112515,8 +116726,9 @@ bool Tool_ordergps::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_ordergps::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
+bool Tool_periodicity::run(HumdrumFile& infile, ostream& out) {
+	bool status;
+	status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
 	} else {
@@ -112525,9 +116737,11 @@ bool Tool_ordergps::run(HumdrumFile& infile, ostream& out) {
 	return status;
 }
 
+//
+// In-place processing of file:
+//
 
-bool Tool_ordergps::run(HumdrumFile& infile) {
-	initialize();
+bool Tool_periodicity::run(HumdrumFile& infile) {
 	processFile(infile);
 	return true;
 }
@@ -112536,311 +116750,651 @@ bool Tool_ordergps::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_ordergps::initialize -- Setup to do before processing a file.
+// Tool_periodicity::processFile --
 //
 
-void Tool_ordergps::initialize(void) {
-	m_emptyQ   = getBoolean("empty");
-	m_fileQ    = getBoolean("file");
-	m_listQ    = getBoolean("list");
-	m_problemQ = getBoolean("problem");
-	m_reverseQ = getBoolean("reverse");
-	m_staffQ   = getBoolean("staff");
-	m_topQ     = getBoolean("top");
+void Tool_periodicity::processFile(HumdrumFile& infile) {
+	HumNum minrhy = infile.tpq() * 4;
+	if (getBoolean("min")) {
+		m_free_text << minrhy << endl;
+		return;
+	}
+
+	vector<vector<double>> attackgrids;
+	attackgrids.resize(infile.getTrackCount()+1);
+	fillAttackGrids(infile, attackgrids, minrhy);
+	if (getBoolean("attacks")) {
+		printAttackGrid(m_free_text, infile, attackgrids, minrhy);
+		return;
+	}
+
+	int atrack = getInteger("track");
+	vector<vector<double>> analysis;
+	doPeriodicityAnalysis(analysis, attackgrids[atrack], minrhy);
+
+	if (getBoolean("raw")) {
+		printPeriodicityAnalysis(m_free_text, analysis);
+		return;
+	}
+
+	printSvgAnalysis(m_free_text, analysis, minrhy);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_ordergps::processFile -- Analyze an input file.
+// Tool_periodicity::printPeriodicityAnalysis --
 //
 
-void Tool_ordergps::processFile(HumdrumFile& infile) {
-	vector<int> groupIndex;
-	vector<int> partIndex;
-	vector<int> staffIndex;
-	bool foundProblem = false;
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			break;
-		}
-		if (infile[i].isManipulator()) {
-			// Don't deall with spine splits/mergers/exchanges/additions.
-			if (!infile[i].isExclusiveInterpretation()) {
-				break;
+void Tool_periodicity::printPeriodicityAnalysis(ostream& out, vector<vector<double>>& analysis) {
+	for (int i=0; i<(int)analysis.size(); i++) {
+		for (int j=0; j<(int)analysis[i].size(); j++) {
+			out << analysis[i][j];
+			if (j < (int)analysis[i].size() - 1) {
+				out << "\t";
 			}
 		}
-		if (infile[i].isCommentLocal()) {
-			// Don't process after local comments.   The file header
-			// is too complex perhaps, so do not alter anything
-			// after the local comment.  This can be related to modori
-			// assignment for groups, for example.
-			break;
+		out << "\n";
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_periodicity::doPeriodicAnalysis --
+//
+
+void Tool_periodicity::doPeriodicityAnalysis(vector<vector<double>> &analysis, vector<double>& grid, HumNum minrhy) {
+	analysis.resize(minrhy.getNumerator());
+	for (int i=0; i<(int)analysis.size(); i++) {
+		doAnalysis(analysis, i, grid);
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_periodicity::doAnalysis --
+//
+
+void Tool_periodicity::doAnalysis(vector<vector<double>>& analysis, int level, vector<double>& grid) {
+	int period = level + 1;
+	analysis[level].resize(period);
+	std::fill(analysis[level].begin(), analysis[level].end(), 0.0);
+	for (int i=0; i<period; i++) {
+		int j = i;
+		while (j < (int)grid.size()) {
+			analysis[level][i] += grid[j];
+			j += period;
 		}
-		if (!infile[i].hasSpines()) {
-			continue;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_periodicity::printAttackGrid --
+//
+
+void Tool_periodicity::printAttackGrid(ostream& out, HumdrumFile& infile, vector<vector<double>>& grids, HumNum minrhy) {
+	out << "!!!minrhy: " << minrhy << endl;
+	out << "**all";
+	for (int i=1; i<(int)grids.size(); i++) {
+		out << "\t**track";
+	}
+	out << "\n";
+	for (int j=0; j<(int)grids[0].size(); j++) {
+		for (int i=0; i<(int)grids.size(); i++) {
+			out << grids[i][j];
+			if (i < (int)grids.size() - 1) {
+				out << "\t";
+			}
 		}
-		if (infile[i].isExclusiveInterpretation()) {
-			continue;
+		out << "\n";
+	}
+	for (int i=0; i<(int)grids.size(); i++) {
+		out << "*-";
+		if (i < (int)grids.size() - 1) {
+			out << "\t";
 		}
-		if (!infile[i].isInterpretation()) {
+	}
+	out << "\n";
+
+}
+
+
+
+//////////////////////////////
+//
+// Tool_periodicity::fillAttackGrids --
+//
+
+void Tool_periodicity::fillAttackGrids(HumdrumFile& infile, vector<vector<double>>& grids, HumNum minrhy) {
+	HumNum elements = minrhy * infile.getScoreDuration() / 4;
+
+	for (int t=0; t<(int)grids.size(); t++) {
+		grids[t].resize(elements.getNumerator());
+	}
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
 			continue;
 		}
-		int hasGroup = false;
-		int hasPart  = false;
-		int hasStaff = false;
-		int hasOther = false;
+		HumNum position = infile[i].getDurationFromStart() / 4 * minrhy;
 		for (int j=0; j<infile[i].getFieldCount(); j++) {
 			HTp token = infile.token(i, j);
-			if (*token == "*") {
+			if (!token->isKern()) {
 				continue;
 			}
-			if (token->compare(0, 6, "*group") == 0) {
-				hasGroup = true;
-			} else if (token->compare(0, 5, "*part") == 0) {
-				hasPart = true;
-			} else if (token->compare(0, 6, "*staff") == 0) {
-				hasStaff = true;
-			} else {
-				hasOther = true;
+			if (token->isNull()) {
+				continue;
+			}
+			if (!token->isNoteAttack()) {
+				continue;
 			}
+			int track = token->getTrack();
+			grids.at(track).at(position.getNumerator()) += 1;
 		}
+	}
 
-		if (hasOther && hasGroup) {
-			foundProblem = true;
-			if (m_problemQ) {
-				cerr << infile.getFilename() << " HAS MIXED GROUP LINE:" << endl;
-				cerr << "\t" << infile[i] << endl;
+	bool oneQ = getBoolean("one");
+	for (int j=0; j<(int)grids.at(0).size(); j++) {
+		grids.at(0).at(j) = 0;
+		for (int i=0; i<(int)grids.size(); i++) {
+			if (!grids.at(i).at(j)) {
+				continue;
+			}
+			if (oneQ) {
+				grids.at(0).at(j) = 1;
+			} else {
+				grids.at(0).at(j) += grids.at(i).at(j);
 			}
 		}
+	}
+}
 
-		if (hasOther && hasPart) {
-			foundProblem = true;
-			if (m_problemQ) {
-				cerr << infile.getFilename() << " HAS MIXED PART LINE:" << endl;
-				cerr << "\t" << infile[i] << endl;
+
+
+//////////////////////////////
+//
+// Tool_periodicity::printSvgAnalysis --
+//
+
+void Tool_periodicity::printSvgAnalysis(ostream& out, vector<vector<double>>& analysis, HumNum minrhy) {
+	pugi::xml_document image;
+	auto declaration = image.prepend_child(pugi::node_declaration);
+	declaration.append_attribute("version") = "1.0";
+	declaration.append_attribute("encoding") = "UTF-8";
+	declaration.append_attribute("standalone") = "no";
+
+	auto svgnode = image.append_child("svg");
+	svgnode.append_attribute("version") = "1.1";
+	svgnode.append_attribute("xmlns") = "http://www.w3.org/2000/svg";
+	svgnode.append_attribute("xmlns:xlink") = "http://www.w3.org/1999/xlink";
+	svgnode.append_attribute("overflow") = "visible";
+	svgnode.append_attribute("viewBox") = "0 0 1000 1000";
+	svgnode.append_attribute("width") = "1000px";
+	svgnode.append_attribute("height") = "1000px";
+
+	auto style = svgnode.append_child("style");
+	style.text().set(".label { font: 14px sans-serif; alignment-baseline: middle; text-anchor: left; }");
+
+	auto grid = svgnode.append_child("g");
+	grid.append_attribute("id") = "grid";
+
+	auto labels = svgnode.append_child("g");
+
+	double hue = 0.0;
+	double saturation = 100;
+	double lightness = 75;
+
+	pugi::xml_node crect;
+	double width;
+	double height;
+
+	stringstream ss;
+	stringstream ssl;
+	//stringstream css;
+	double x;
+	double y;
+
+	double imagewidth = 1000.0;
+	double imageheight = 1000.0;
+
+	int maxrow = getInteger("max-rows");
+	if (maxrow <= 0) {
+		maxrow = (int)analysis.back().size();
+	}
+
+
+	// double sdur = (double)analysis.back().size();
+	double sdur = (double)maxrow;
+
+	double maxscore = 0.0;
+	for (int i=0; i<maxrow; i++) {
+		for (int j=0; j<(int)analysis[i].size(); j++) {
+			if (maxscore < analysis[i][j]) {
+				maxscore = analysis[i][j];
 			}
 		}
+	}
 
-		if (hasOther && hasStaff) {
-			foundProblem = true;
-			if (m_problemQ) {
-				cerr << infile.getFilename() << " HAS MIXED STAFF LINE:" << endl;
-				cerr << "\t" << infile[i] << endl;
-			}
+	double power = getDouble("power");
+	for (int i=0; i<maxrow; i++) {
+		for (int j=0; j<(int)analysis[i].size(); j++) {
+			width = 1 / sdur * imagewidth;
+			height = 1 / sdur * imageheight;
+
+			x = j/sdur * imageheight;
+			y = i/sdur * imagewidth;
+
+			double value = analysis[i][j]/maxscore;
+			value = pow(value, 1.0/power);
+
+			getColorMapping(value, hue, saturation, lightness);
+			ss << "hsl(" << hue << "," << saturation << "%," << lightness << "%)";
+			crect = grid.append_child("rect");
+			crect.append_attribute("x") = to_string(x).c_str();
+			crect.append_attribute("y") = to_string(y).c_str();
+			crect.append_attribute("width") = to_string(width*0.99).c_str();
+			crect.append_attribute("height") = to_string(height*0.99).c_str();
+			crect.append_attribute("fill") = ss.str().c_str();
+			//css << "Xm" << getMeasure1(i) << " Ym" << getMeasure2(j);
+			//css << " X" << getQon1(i)     << " Y" << getQon2(j);
+			//css << " X" << getQoff1(i)    << " Y" << getQoff2(j);
+			//crect.append_attribute("class") = css.str().c_str();
+			ss.str("");
+			//css.str("");
 		}
 
-		if (hasOther) {
+		pugi::xml_node label = labels.append_child("text");
+		label.append_attribute("class") = "label";
+
+		HumNum rval = (i+1);
+		rval /= minrhy;
+		rval *= 4;
+
+		std::string rhythm = Convert::durationToRecip(rval);
+		rhythm += " (" + to_string(i+1) + ")";
+		label.text().set(rhythm.c_str());
+		x = (i+1+0.5)/sdur * imageheight;
+		y = (i+0.5)/sdur * imagewidth;
+		label.append_attribute("x") = to_string(x).c_str();
+		label.append_attribute("y") = to_string(y).c_str();
+	}
+
+	image.save(out);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_periodicity::getColorMapping --
+//
+
+void Tool_periodicity::getColorMapping(double input, double& hue,
+		double& saturation, double& lightness) {
+	double maxhue = 0.75 * 360.0;
+	hue = input;
+	if (hue < 0.0) {
+		hue = 0.0;
+	}
+	hue = hue * hue;
+	if (hue != 1.0) {
+		hue *= 0.95;
+	}
+
+	hue = (1.0 - hue) * 360.0;
+	if (hue == 0.0) {
+		// avoid -0.0;
+		hue = 0.0;
+	}
+
+	if (hue > maxhue) {
+		hue = maxhue;
+	}
+	if (hue < 0.0) {
+		hue = maxhue;
+	}
+
+	saturation = 100.0;
+	lightness = 50.0;
+
+	if (hue > 60) {
+		lightness = lightness - (hue-60) / (maxhue-60) * lightness / 1.5;
+	}
+}
+
+
+
+
+/////////////////////////////////
+//
+// Tool_gridtest::Tool_phrase -- Set the recognized options for the tool.
+//
+
+Tool_phrase::Tool_phrase(void) {
+	define("A|no-average=b", "do not do average phrase-length analysis");
+	define("R|remove2=b",    "remove phrase boundaries in data and do not do analysis");
+	define("m|mark=b",       "mark phrase boundaries based on rests");
+	define("r|remove=b",     "remove phrase boundaries in data");
+	define("c|color=s",      "display color of analysis data");
+}
+
+
+
+///////////////////////////////
+//
+// Tool_phrase::run -- Primary interfaces to the tool.
+//
+
+bool Tool_phrase::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_phrase::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
+}
+
+
+bool Tool_phrase::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	return status;
+}
+
+
+bool Tool_phrase::run(HumdrumFile& infile) {
+	initialize(infile);
+	for (int i=0; i<(int)m_starts.size(); i++) {
+		if (m_removeQ) {
+			removePhraseMarks(m_starts[i]);
+		}
+		if (m_remove2Q) {
 			continue;
 		}
-
-		if (hasGroup && hasPart) {
-			foundProblem = true;
-			if (m_problemQ) {
-				cerr << infile.getFilename() << " HAS MIXED GROUP AND PART LINE:" << endl;
-				cerr << "\t" << infile[i] << endl;
-			}
+		if (hasPhraseMarks(m_starts[i])) {
+			analyzeSpineByPhrase(i);
+		} else {
+			analyzeSpineByRests(i);
 		}
+	}
+	if (!m_remove2Q) {
+		prepareAnalysis(infile);
+	}
+	infile.createLinesFromTokens();
+	return true;
+}
 
-		if (hasGroup && hasStaff) {
-			foundProblem = true;
-			if (m_problemQ) {
-				cerr << infile.getFilename() << " HAS MIXED GROUP AND STAFF LINE:" << endl;
-				cerr << "\t" << infile[i] << endl;
+
+
+//////////////////////////////
+//
+// Tool_phrase::prepareAnalysis --
+//
+
+void Tool_phrase::prepareAnalysis(HumdrumFile& infile) {
+	string exinterp = "**cdata";
+	infile.appendDataSpine(m_results.back(), "", exinterp);
+	for (int i = (int)m_results.size()-1; i>0; i--) {
+		int track = m_starts[i]->getTrack();
+		infile.insertDataSpineBefore(track, m_results[i-1], "", exinterp);
+	}
+	if (m_averageQ) {
+		addAverageLines(infile);
+	}
+	if (!m_color.empty()) {
+		int insertline = -1;
+		for (int i=0; i<infile.getLineCount(); i++) {
+			if (infile[i].isData() || infile[i].isBarline()) {
+				insertline = i;
+				break;
 			}
 		}
-
-		if (hasPart && hasStaff) {
-			foundProblem = true;
-			if (m_problemQ) {
-				cerr << infile.getFilename() << " HAS MIXED PART AND STAFF LINE:" << endl;
-				cerr << "\t" << infile[i] << endl;
+		if (insertline > 0) {
+			stringstream ss;
+			int fsize = infile[insertline].getFieldCount();
+			for (int j=0; j<fsize; j++) {
+				ss << "*";
+				HTp token = infile.token(insertline, j);
+				string dt = token->getDataType();
+				if (dt.empty() || (dt == "**cdata")) {
+					ss << "color:" << m_color;
+				}
+				if (j < fsize  - 1) {
+					ss << "\t";
+				}
 			}
+			string output = ss.str();
+			infile.insertLine(insertline, output);
 		}
+	}
+}
 
-		if (hasGroup) {
-			groupIndex.push_back(i);
-		}
 
-		if (hasPart)  {
-			partIndex.push_back(i);
-		}
 
-		if (hasStaff) {
-			staffIndex.push_back(i);
-		}
-	}
+///////////////////////////////
+//
+// Tool_pharse::addAverageLines --
+//
 
-	if (groupIndex.size() > 1) {
-		foundProblem = true;
-		if (m_problemQ) {
-			cerr << infile.getFilename() << " HAS MORE THAN ONE GROUP LINE:" << endl;
-			for (int i=0; i<(int)groupIndex.size(); i++) {
-				cerr << "\t" << infile[groupIndex[i]] << endl;
-			}
+void Tool_phrase::addAverageLines(HumdrumFile& infile) {
+	vector<string> averages;
+	averages.resize(m_starts.size()+1);
+	int tcount = 0;
+	HumNum tsum = 0;
+	double average;
+	stringstream ss;
+	for (int i=0; i<(int)m_starts.size(); i++) {
+		if (m_pcount[i] > 0) {
+			average = m_psum[i].getFloat() / m_pcount[i];
+		} else {
+			average = 0.0;
 		}
+		ss.str("");
+		ss.clear();
+		ss << "!!average-phrase-length-k" << i+1 << ":\t" << average;
+		averages[i+1] = ss.str();
+		tcount += m_pcount[i];
+		tsum += m_psum[i];
 	}
+	average = tsum.getFloat() / tcount;
+	ss.str("");
+	ss.clear();
+	ss << "!!average-phrase-length:\t" << average;
+	averages[0] = ss.str();
 
-	if (partIndex.size() > 1) {
-		foundProblem = true;
-		if (m_problemQ) {
-			cerr << infile.getFilename() << " HAS MORE THAN ONE PART LINE:" << endl;
-			for (int i=0; i<(int)partIndex.size(); i++) {
-				cerr << "\t" << infile[partIndex[i]] << endl;
-			}
-		}
+	for (int i=0; i<(int)averages.size(); i++) {
+		infile.appendLine(averages[i]);
 	}
+}
 
-	if (staffIndex.size() > 1) {
-		foundProblem = true;
-		if (m_problemQ) {
-			cerr << infile.getFilename() << " HAS MORE THAN ONE STAFF LINE:" << endl;
-			for (int i=0; i<(int)staffIndex.size(); i++) {
-				cerr << "\t" << infile[staffIndex[i]] << endl;
-			}
-		}
-	}
 
-	if (m_problemQ) {
-		if (m_emptyQ) {
-			if (groupIndex.empty() && partIndex.empty() && staffIndex.empty()) {
-				cerr << infile.getFilename() << " HAS NO GROUP/PART/STAFF INFO" << endl;
-			}
-		}
-	} else {
-		if (foundProblem) {
-			// Don try to fix anything, just echo the input:
-			m_humdrum_text << infile;
-		} else {
-			if (m_staffQ && groupIndex.empty() && partIndex.empty() && staffIndex.empty()) {
-				printStaffLine(infile);
-			} else {
-				// Process further here
-				// Check the order of the group/part/staff lines.
-				int gindex = groupIndex.empty() ? -1 : groupIndex.at(0);
-				int pindex = partIndex.empty() ? -1 : partIndex.at(0);
-				int sindex = staffIndex.empty() ? -1 : staffIndex.at(0);
-				if (m_topQ) {
-					printFileTop(infile, gindex, pindex, sindex);
-				} else {
-					printFile(infile, gindex, pindex, sindex);
-				}
-			}
-		}
+
+///////////////////////////////
+//
+// Tool_phrase::initialize --
+//
+
+void Tool_phrase::initialize(HumdrumFile& infile) {
+	m_starts = infile.getKernSpineStartList();
+	m_results.resize(m_starts.size());
+	int lines = infile.getLineCount();
+	for (int i=0; i<(int)m_results.size(); i++) {
+		m_results[i].resize(lines);
+	}
+	m_pcount.resize(m_starts.size());
+	m_psum.resize(m_starts.size());
+	std::fill(m_pcount.begin(), m_pcount.end(), 0);
+	std::fill(m_psum.begin(), m_psum.end(), 0);
+	m_markQ = getBoolean("mark");
+	m_removeQ = getBoolean("remove");
+	m_averageQ = !getBoolean("no-average");
+	m_remove2Q = getBoolean("remove2");
+	if (getBoolean("color")) {
+		m_color = getString("color");
 	}
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_ordergps::printFileTop -- Print group/part/staff first after exclusive
-//     interpretations.
+// Tool_phrase::analyzeSpineByRests --
 //
 
-void Tool_ordergps::printFileTop(HumdrumFile& infile, int gindex, int pindex, int sindex) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (i == gindex) {
-			continue;
-		} else if (i == pindex) {
+void Tool_phrase::analyzeSpineByRests(int index) {
+	HTp start    = m_starts[index];
+	HTp current  = start;
+	HTp lastnote = NULL;   // last note to be processed
+	HTp pstart   = NULL;   // phrase start;
+	HumNum dur;
+	stringstream ss;
+	while (current) {
+		if (current->isBarline()) {
+			if (current->find("||") != std::string::npos) {
+				if (pstart) {
+					dur = current->getDurationFromStart()
+							- pstart->getDurationFromStart();
+					ss.str("");
+					ss.clear();
+					ss << dur.getFloat();
+					m_psum[index] += dur;
+					m_pcount[index]++;
+					m_results[index][pstart->getLineIndex()] = ss.str();
+					pstart = NULL;
+					if (m_markQ && lastnote) {
+						lastnote->setText(lastnote->getText() + "}");
+					}
+				}
+			}
+		}
+		if (!current->isData()) {
+			current = current->getNextToken();
 			continue;
-		} else if (i == sindex) {
+		}
+		if (current->isNull()) {
+			current = current->getNextToken();
 			continue;
-		} else if (infile[i].isExclusiveInterpretation()) {
-			m_humdrum_text << infile[i] << endl;
-			if (m_reverseQ) {
-				if (sindex >= 0) {
-					m_humdrum_text << infile[sindex] << endl;
-				}
-				if (pindex >= 0) {
-					m_humdrum_text << infile[pindex] << endl;
-				}
-				if (gindex >= 0) {
-					m_humdrum_text << infile[gindex] << endl;
-				}
-			} else {
-				if (gindex >= 0) {
-					m_humdrum_text << infile[gindex] << endl;
-				}
-				if (pindex >= 0) {
-					m_humdrum_text << infile[pindex] << endl;
-				}
-				if (sindex >= 0) {
-					m_humdrum_text << infile[sindex] << endl;
+		}
+		if (pstart && current->isRest()) {
+			if (lastnote) {
+				dur = current->getDurationFromStart()
+						- pstart->getDurationFromStart();
+				ss.str("");
+				ss.clear();
+				ss << dur.getFloat();
+				m_psum[index] += dur;
+				m_pcount[index]++;
+				m_results[index][pstart->getLineIndex()] = ss.str();
+				if (m_markQ) {
+					lastnote->setText(lastnote->getText() + "}");
 				}
 			}
-		} else {
-			m_humdrum_text << infile[i] << endl;
+			pstart = NULL;
+			lastnote = NULL;
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isRest()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isNote()) {
+			lastnote = current;
+		}
+		if (pstart && current->isNote() && (current->find(";") != std::string::npos)) {
+			// fermata at end of phrase.
+			dur = current->getDurationFromStart() + current->getDuration()
+					- pstart->getDurationFromStart();
+			ss.str("");
+			ss.clear();
+			ss << dur.getFloat();
+			m_psum[index] += dur;
+			m_pcount[index]++;
+			m_results[index][pstart->getLineIndex()] = ss.str();
+			if (m_markQ) {
+				current->setText(current->getText() + "}");
+			}
+			current = current->getNextToken();
+			pstart = NULL;
+			continue;
+		}
+		if (current->isNote() && pstart == NULL) {
+			pstart = current;
+			if (m_markQ) {
+				current->setText("{" + current->getText());
+			}
+		}
+		current = current->getNextToken();
+	}
+	if (pstart) {
+		dur = start->getOwner()->getOwner()->getScoreDuration()
+				- pstart->getDurationFromStart();
+		ss.str("");
+		ss.clear();
+		ss << dur.getFloat();
+		m_psum[index] += dur;
+		m_pcount[index]++;
+		m_results[index][pstart->getLineIndex()] = ss.str();
+		if (m_markQ && lastnote) {
+			lastnote->setText(lastnote->getText() + "}");
 		}
 	}
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_ordergps::printFile -- Check to see if the group/part/staff
-//    lines need to be adjusted, and the print the file.  Lines
-//    will be ordered group/part/staff, placing the lines where
-//    the first of group/part/staff is found.
+// Tool_phrase::analyzeSpineByPhrase --
 //
 
-void Tool_ordergps::printFile(HumdrumFile& infile, int gindex, int pindex, int sindex) {
-	int startIndex = gindex;
-	if (pindex >= 0) {
-		if (startIndex < 0) {
-			startIndex = pindex;
-		} else if (pindex < startIndex) {
-			startIndex = pindex;
-		}
-	}
-	if (sindex >= 0) {
-		if (startIndex < 0) {
-			startIndex = sindex;
-		} else if (sindex < startIndex) {
-			startIndex = sindex;
+void Tool_phrase::analyzeSpineByPhrase(int index) {
+	HTp start    = m_starts[index];
+	HTp current  = start;
+	HTp pstart   = NULL;   // phrase start;
+	HumNum dur;
+	stringstream ss;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-	}
-	if (startIndex < 0) {
-		// no group/part/staff lines in file, so just print it:
-		m_humdrum_text << infile;
-		return;
-	}
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (i == startIndex) {
-			if (m_reverseQ) {
-				if (sindex >= 0) {
-					m_humdrum_text << infile[sindex] << endl;
-				}
-				if (pindex >= 0) {
-					m_humdrum_text << infile[pindex] << endl;
-				}
-				if (gindex >= 0) {
-					m_humdrum_text << infile[gindex] << endl;
-				}
-			} else {
-				if (gindex >= 0) {
-					m_humdrum_text << infile[gindex] << endl;
-				}
-				if (pindex >= 0) {
-					m_humdrum_text << infile[pindex] << endl;
-				}
-				if (sindex >= 0) {
-					m_humdrum_text << infile[sindex] << endl;
-				}
-			}
-		} else if (i == gindex) {
+		if (current->isNull()) {
+			current = current->getNextToken();
 			continue;
-		} else if (i == pindex) {
+		}
+		if (current->find("{") != std::string::npos) {
+			pstart = current;
+			current = current->getNextToken();
 			continue;
-		} else if (i == sindex) {
+		}
+		if (current->find("}") != std::string::npos) {
+			if (pstart) {
+				dur = current->getDurationFromStart() + current->getDuration()
+						- pstart->getDurationFromStart();
+				ss.str("");
+				ss.clear();
+				ss << dur.getFloat();
+				m_psum[index] += dur;
+				m_pcount[index]++;
+				m_results[index][pstart->getLineIndex()] = ss.str();
+			}
+			current = current->getNextToken();
 			continue;
-		} else {
-			m_humdrum_text << infile[i] << endl;
 		}
+		current = current->getNextToken();
 	}
 }
 
@@ -112848,69 +117402,80 @@ void Tool_ordergps::printFile(HumdrumFile& infile, int gindex, int pindex, int s
 
 //////////////////////////////
 //
-// Tool_ordergps::printStaffLine --  Add a *staff at the start of the
-//     data since none was detected.  Does not label staff-like spines
-//     other than **kern (such as **kernyy, **kern-mod, **mens).
+// Tool_phrase::removePhraseMarks -- Remvoe { and } characters from **kern data.
+//
 
-void Tool_ordergps::printStaffLine(HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isExclusiveInterpretation()) {
-			m_humdrum_text << infile[i] << endl;
+void Tool_phrase::removePhraseMarks(HTp start) {
+	HTp current = start;
+	HumRegex hre;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
 			continue;
 		}
-		m_humdrum_text << infile[i] << endl;
-		vector<string> staffLine(infile[i].getFieldCount(), "*");
-		int counter = 0;
-		for (int j=infile[i].getFieldCount() - 1; j>=0; j--) {
-			HTp token = infile.token(i, j);
-			if (token->isKern()) {
-				counter++;
-				string text = "*staff" + to_string(counter);
-				staffLine.at(j) = text;
-			}
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
 		}
-		for (int j=0; j<(int)staffLine.size(); j++) {
-			m_humdrum_text << staffLine[j];
-			if (j < (int)staffLine.size() - 1) {
-				m_humdrum_text << '\t';
-			}
+		if (current->find("{") != std::string::npos) {
+			string data = *current;
+			hre.replaceDestructive(data, "", "\\{", "g");
+			current->setText(data);
 		}
-		m_humdrum_text << endl;
+		if (current->find("}") != std::string::npos) {
+			string data = *current;
+			hre.replaceDestructive(data, "", "\\}", "g");
+			current->setText(data);
+		}
+		current = current->getNextToken();
 	}
 }
 
 
 
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_pbar::Tool_pbar -- Set the recognized options for the tool.
+// Tool_phrase::hasPhraseMarks -- True if **kern data spine (primary layer), has
+//   "{" (or "}", but this is not checked) characters (phrase markers).
 //
 
-Tool_pbar::Tool_pbar(void) {
-	define("i|invisible-barlines=b", "make barlines invisible");
+bool Tool_phrase::hasPhraseMarks(HTp start) {
+	HTp current = start;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->find("{") != std::string::npos) {
+			return true;
+		}
+		current = current->getNextToken();
+	}
+	return false;
 }
 
 
 
-//////////////////////////////
+
+
+/////////////////////////////////
 //
-// Tool_pbar::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_pline::Tool_pline -- Set the recognized options for the tool.
 //
 
-void Tool_pbar::initialize(void) {
-	m_invisibleQ = getBoolean("invisible-barlines");
+Tool_pline::Tool_pline(void) {
+	define("c|color=b",   "color poetic lines (currently only by notes)");
+	define("o|overlap=b", "do overlap analysis/markup");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_pbar::run -- Do the main work of the tool.
+// Tool_pline::run -- Do the main work of the tool.
 //
 
-bool Tool_pbar::run(HumdrumFileSet& infiles) {
+bool Tool_pline::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -112919,8 +117484,7 @@ bool Tool_pbar::run(HumdrumFileSet& infiles) {
 }
 
 
-
-bool Tool_pbar::run(const string& indata, ostream& out) {
+bool Tool_pline::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -112932,7 +117496,7 @@ bool Tool_pbar::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_pbar::run(HumdrumFile& infile, ostream& out) {
+bool Tool_pline::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -112943,181 +117507,195 @@ bool Tool_pbar::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_pbar::run(HumdrumFile& infile) {
+bool Tool_pline::run(HumdrumFile& infile) {
 	initialize();
 	processFile(infile);
 	return true;
 }
 
 
-
 //////////////////////////////
 //
-// Tool_pbar::processFile --
+// Tool_pline::initialize --
 //
 
-void Tool_pbar::processFile(HumdrumFile& infile) {
-	vector<HTp> kstarts = infile.getKernSpineStartList();
-	for (int i=0; i<(int)kstarts.size(); i++) {
-		processSpine(kstarts[i]);
-	}
+void Tool_pline::initialize(void) {
+	m_colors.resize(14);
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
-		}
-		if (infile[i].isData()) {
-			printDataLine(infile, i);
-		} else if (infile[i].isCommentLocal()) {
-			printLocalCommentLine(infile, i);
-		} else if (infile[i].isBarline()) {
-			printBarLine(infile, i);
-			if (m_invisibleQ) {
-				printInvisibleBarlines(infile, i);
-			} else {
-				m_humdrum_text << infile[i] << endl;
-			}
-		} else {
-			m_humdrum_text << infile[i] << endl;
-		}
-	}
+	m_colors[0] = "red";        // red
+	m_colors[1] = "darkorange"; // orange
+	m_colors[2] = "gold";       // yellow
+	m_colors[3] = "limegreen";  // green
+	m_colors[4] = "skyblue";    // light blue
+	m_colors[5] = "mediumblue"; // dark blue
+	m_colors[6] = "purple";     // purple
+
+	m_colors[7]  = "darkred";        // red
+	m_colors[8]  = "lightsalmon";    // orange
+	m_colors[9]  = "darkgoldenrod";  // yellow
+	m_colors[10] = "olivedrab";      // green
+	m_colors[11] = "darkturquoise";  // light blue
+	m_colors[12] = "darkblue";       // dark blue
+	m_colors[13] = "indigo";         // purple
+
+	// lighter colors for staff highlighting:
+	//m_colors[0] = "#ffaaaa";  // red
+	//m_colors[1] = "#ffbb00";  // orange
+	//m_colors[2] = "#eeee00";  // yellow
+	//m_colors[3] = "#99cc01";  // green
+	//m_colors[4] = "#bbddff";  // light blue
+	//m_colors[5] = "#88aaff";  // dark blue
+	//m_colors[6] = "#cc88ff";  // purple
+
+	m_colorQ = getBoolean("color");
+	m_colorQ = true;  // default behavior for now.
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pbar::printInvisibleBarlines --
+// Tool_pline::processFile --
 //
 
-void Tool_pbar::printInvisibleBarlines(HumdrumFile& infile, int index) {
-	HumRegex hre;
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (hre.search(token, "-")) {
-			m_humdrum_text << token;
-		} else if (hre.search(token, "==")) {
-			m_humdrum_text << token;
-		} else if (hre.search(token, "\\|\\|")) {
-			m_humdrum_text << token;
-		} else {
-			m_humdrum_text << token << "-";
-		}
-		if (i < infile[index].getFieldCount() - 1) {
-			m_humdrum_text << "\t";
-		}
+void Tool_pline::processFile(HumdrumFile& infile) {
+	getPlineInterpretations(infile, m_ptokens);
+	fillLineInfo(infile, m_lineInfo);
+	if (m_colorQ) {
+		plineToColor(infile, m_ptokens);
+	}
+	infile.createLinesFromTokens();
+	m_humdrum_text << infile;
+	if (m_colorQ) {
+		m_humdrum_text << "!!!RDF**kern: 😀 = marked note, color=black" << endl;
 	}
-	m_humdrum_text << "\n";
 }
 
 
 
-///////////////////////////////
-//
-// Tool_pbar::printDataLine --
+//////////////////////////////
+//
+// Tool_pline::markRests --
 //
 
-void Tool_pbar::printDataLine(HumdrumFile& infile, int index) {
-	printBarLine(infile, index);
-	m_humdrum_text << infile[index] << endl;
-}
-
+void Tool_pline::markRests(HumdrumFile& infile) {
+	vector<HTp> spinestops;
+	infile.getSpineStopList(spinestops);
+	for (int i=0; i<(int)spinestops.size(); i++) {
+		if (!spinestops[i]->isKern()) {
+			continue;
+		}
+		markSpineRests(spinestops[i]);
+	}
+}
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_pbar::printBarLine -- Add *bar line.
+// Tool_pline::markSpineRests --
 //
 
-void Tool_pbar::printBarLine(HumdrumFile& infile, int index) {
-	bool hasBarline = false;
-	for (int j=0; j<infile[index].getFieldCount(); j++) {
-		HTp token = infile.token(index, j);
-		string value = token->getValue("auto", "pbar");
-		if (value == "true") {
-			hasBarline = true;
-			break;
+void Tool_pline::markSpineRests(HTp spineStop) {
+	string marker = "😀";
+	int track = spineStop->getTrack();
+	int lastValue = -1;
+	HTp current = spineStop->getPreviousToken();
+	int  line;
+	int  cvalue;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getPreviousToken();
+			continue;
+		}
+		if (current->isNull()) {
+			current = current->getPreviousToken();
+			continue;
 		}
-	}
 
-	if (hasBarline) {
-		for (int j=0; j<infile[index].getFieldCount(); j++) {
-			HTp token = infile.token(index, j);
-			string value = token->getValue("auto", "pbar");
-			if (value == "true") {
-				m_humdrum_text << "*bar";
-			} else {
-				m_humdrum_text << "*";
-			}
-			if (j < infile[index].getFieldCount() - 1) {
-				m_humdrum_text << "\t";
-			}
+		line = current->getLineIndex();
+		cvalue = m_lineInfo.at(line).at(track);
+
+		if (current->isRest() && (cvalue != lastValue)) {
+			string text = *current;
+			text += marker;
+			current->setText(text);
+		} else {
+			lastValue = cvalue;
+			string text = *current;
+			text += "@" + to_string(cvalue);
+			current->setText(text);
 		}
-		m_humdrum_text << "\n";
+		current = current->getPreviousToken();
 	}
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_pbar::printLocalCommentLine --
+// Tool_pline::fillLineInfo --
 //
 
-void Tool_pbar::printLocalCommentLine(HumdrumFile& infile, int index) {
+void Tool_pline::fillLineInfo(HumdrumFile& infile, vector<vector<int>>& lineinfo) {
+	lineinfo.clear();
+	lineinfo.resize(infile.getLineCount());
+	int maxtrack = infile.getMaxTrack();
 	HumRegex hre;
-	bool hasKp = false;
-	bool hasOther = false;
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (hre.search(token, "kreska pseudotaktowa")) {
-			hasKp = true;
-		} else if (*token != "!") {
-			hasOther = true;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		lineinfo[i].resize(maxtrack + 1);
+		fill(lineinfo[i].begin(), lineinfo[i].end(), 0);
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (hre.search(token, "^\\*pline:\\s*(\\d+)")) {
+				int digit = hre.getMatchInt(1);
+				int track = token->getTrack();
+				lineinfo[i][track] = digit;
+			}
 		}
 	}
 
-	if (!hasKp) {
-		m_humdrum_text << infile[index] << endl;
-		return;
-	}
-
-	if (hasOther) {
-		for (int i=0; i<infile[index].getFieldCount(); i++) {
-			HTp token = infile.token(index, i);
-			if (hre.search(token, "kreska pseudotaktowa")) {
-				m_humdrum_text << "!";
+	for (int i=1; i<(int)lineinfo.size() - 1; i++) {
+		for (int j=1; j<=maxtrack; j++) {
+			if (lineinfo.at(i).at(j)) {
+				continue;
 			} else {
-				m_humdrum_text << token;
-			}
-			if (i < infile[index].getFieldCount() - 1) {
-				m_humdrum_text << "\t";
+				lineinfo.at(i).at(j) = lineinfo.at(i-1).at(j);
 			}
 		}
-		m_humdrum_text << "\n";
 	}
+
+	// for (int i=0; i<(int)lineinfo.size() - 1; i++) {
+	// 	for (int j=1; j<=maxtrack; j++) {
+	// 		cerr << lineinfo[i][j] << "\t";
+	// 	}
+	// 	cerr << endl;
+	// }
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pbar::processSpine --
+// Tool_pline::plineToColor --
 //
 
-void Tool_pbar::processSpine(HTp spineStart) {
-	HTp current = spineStart;
+void Tool_pline::plineToColor(HumdrumFile& infile, vector<HTp>& tokens) {
 	HumRegex hre;
-	while (current) {
-		if (!current->isLocalComment()) {
-			current = current->getNextToken();
+	markRests(infile);
+	for (int i=0; i<(int)tokens.size(); i++) {
+		if (!hre.search(tokens[i], "^\\*pline:\\s*(\\d+)")) {
 			continue;
 		}
-		if (hre.search(current, "kreska\\s*pseudotaktowa")) {
-			addBarLineToFollowingNoteOrRest(current);
-		}
-		current = current->getNextToken();
+		int lineNum = hre.getMatchInt(1);
+		int colorIndex = (lineNum - 1) % m_colors.size();
+		string color = m_colors.at(colorIndex);
+		string text = "*color:";
+		text += color;
+		tokens[i]->setText(text);
 	}
 }
 
@@ -113125,62 +117703,54 @@ void Tool_pbar::processSpine(HTp spineStart) {
 
 //////////////////////////////
 //
-// Tool_pbar::addBarLineToFollowingNoteOrRest --
+// Tool_pline::getPlineInterpretations --
 //
 
-void Tool_pbar::addBarLineToFollowingNoteOrRest(HTp token) {
-	HTp current = token->getNextToken();
-	int counter = 0;
-	while (current) {
-		if (!current->isBarline()) {
-			if (!current->isData() || current->isNull()) {
-				current = current->getNextToken();
+void Tool_pline::getPlineInterpretations(HumdrumFile& infile, vector<HTp>& tokens) {
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
 				continue;
 			}
+			if (hre.search(token, "^\\*pline:\\s*(\\d+)")) {
+				tokens.push_back(token);
+			}
 		}
-		counter++;
-		if (counter == 2) {
-			current->setValue("auto", "pbar", "true");
-			break;
-		}
-		current = current->getNextToken();
 	}
 }
 
 
 
 
+
 /////////////////////////////////
 //
-// Tool_gridtest::Tool_pccount -- Set the recognized options for the tool.
+// Tool_gridtest::Tool_pnum -- Set the recognized options for the tool.
 //
 
-Tool_pccount::Tool_pccount(void) {
-	define("a|attacks=b",                 "count attacks instead of durations");
-	define("d|data|vega-data=b",          "display the vega-lite template.");
-	define("f|full=b",                    "full count attacks all single sharps and flats.");
-	define("ff|double-full=b",            "full count attacks all double sharps and flats.");
-	define("h|html=b",                    "generate vega-lite HTML content");
-	define("i|id=s:id",                   "ID for use as variable and in plot title");
-	define("K|no-key|no-final=b",         "do not label key tonic or final");
-	define("m|maximum=b",                 "normalize by maximum count");
-	define("n|normalize=b",               "normalize counts");
-	define("p|page=b",                    "generate vega-lite stand-alone HTML page");
-	define("r|ratio|aspect-ratio=d:0.67", "width*ratio=height of vega-lite plot");
-	define("s|script|vega-script=b",      "generate vega-lite javascript content");
-	define("title=s",                     "title for plot");
-	define("t|template|vega-template=b",  "display the vega-lite template.");
-	define("w|width=i:400",               "width of vega-lite plot");
+Tool_pnum::Tool_pnum(void) {
+	define("b|base=i:midi",    "numeric base of pitch to extract");
+	define("D|no-duration=b",  "do not include duration");
+	define("c|pitch-class=b",  "give numeric pitch-class rather than pitch");
+	define("o|octave=b",       "give octave rather than pitch");
+	define("r|rest=s:0",       "representation string for rests");
+	define("R|no-rests=b",     "do not include rests in conversion");
+	define("x|attacks-only=b", "only mark lines with note attacks");
 }
 
 
 
 ///////////////////////////////
 //
-// Tool_pccount::run -- Primary interfaces to the tool.
+// Tool_pnum::run -- Primary interfaces to the tool.
 //
 
-bool Tool_pccount::run(HumdrumFileSet& infiles) {
+bool Tool_pnum::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -113189,21 +117759,23 @@ bool Tool_pccount::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_pccount::run(const string& indata, ostream& out) {
+bool Tool_pnum::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	return run(infile, out);
 }
 
 
-bool Tool_pccount::run(HumdrumFile& infile, ostream& out) {
+bool Tool_pnum::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
+	out << infile;
 	return status;
 }
 
 
-bool Tool_pccount::run(HumdrumFile& infile) {
+bool Tool_pnum::run(HumdrumFile& infile) {
    initialize(infile);
 	processFile(infile);
+	infile.createLinesFromTokens();
 	return true;
 }
 
@@ -113211,161 +117783,68 @@ bool Tool_pccount::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_pccount::initialize --
+// Tool_pnum::initialize --
 //
 
-void Tool_pccount::initialize(HumdrumFile& infile) {
-	m_attack     = getBoolean("attacks");
-	m_full       = getBoolean("full");
-	m_doublefull = getBoolean("double-full");
-	m_normalize  = getBoolean("normalize");
-	m_maximum    = getBoolean("maximum");
-	m_template   = getBoolean("vega-template");
-	m_data       = getBoolean("vega-data");
-	m_script     = getBoolean("vega-script");
-	m_width      = getInteger("width");
-	m_ratio      = getDouble("aspect-ratio");
-	m_key        = !getBoolean("no-key");
-	if (getBoolean("title")) {
-		m_title = getString("title");
-	}
-	m_html       = getBoolean("html");
-	m_page       = getBoolean("page");
-	if (getBoolean("id")) {
-		m_id = getString("id");
+void Tool_pnum::initialize(HumdrumFile& infile) {
+	m_midiQ = false;
+	if (getString("base") == "midi") {
+		m_base = 12;
+		m_midiQ = true;
 	} else {
-		string filename = infile.getFilename();
-	 	auto pos = filename.rfind("/");
-		if (pos != string::npos) {
-			filename = filename.substr(pos+1);
-		}
-		pos = filename.find("-");
-		if (pos != string::npos) {
-			m_id = filename.substr(0, pos);
-		}
+		// check base for valid numbers, but for now default to 12 if unknown
+		m_base = getInteger("base");
 	}
-	m_parttracks.clear();
-	m_names.clear();
-	m_abbreviations.clear();
-	initializePartInfo(infile);
-
-
-	// https://encycolorpedia.com/36cd27
-	m_vcolor.clear();
-
-	m_vcolor["Canto"]		=	"#e49689";
-	m_vcolor["Canto (Canto I)"]	=	"#e49689";
-	m_vcolor["Canto I"]		=	"#e49689";
-	m_vcolor["Canto Primo"]		=	"#e49689";
-	m_vcolor["[Canto 1]"]		=	"#e49689";
-	m_vcolor["[Canto]"]		=	"#e49689";
-	m_vcolor["[Soprano o Tenore]"]	=	"#e49689";
-	m_vcolor["Soprano"]		=	"#e49689";
-
-	m_vcolor["Canto 2."]		=	"#d67365";
-	m_vcolor["Canto II"]		=	"#d67365";
-	m_vcolor["Canto II [Sesto]"]	=	"#d67365";
-	m_vcolor["Canto Sec."]		=	"#d67365";
-	m_vcolor["Canto Secondo"]	=	"#d67365";
-	m_vcolor["Canto secondo"]	=	"#d67365";
-	m_vcolor["[Canto 2]"]		=	"#d67365";
-
-	m_vcolor["Canto III [Settimo]"]	=	"#c54f43";
-
-	m_vcolor["Alto"]		=	"#f4c6a1";
-	m_vcolor["Alti"]		=	"#f4c6a1";
-	m_vcolor["Alto (Canto III)"]	=	"#f4c6a1";
-
-	m_vcolor["Alto II"]		=	"#edb383";
-
-	m_vcolor["Tenor"]		=	"#ecdf7a";
-	m_vcolor["Tenore"]		=	"#ecdf7a";
-	m_vcolor["Tenore over Canto"]	=	"#ecdf7a";
-	m_vcolor["[Tenore]"]		=	"#ecdf7a";
-
-	m_vcolor["Sesto"]		=	"#c8f0bb";
-	m_vcolor["Sesto (Canto II)"]	=	"#c8f0bb";
-	m_vcolor["Sesto Canto II"]	=	"#c8f0bb";
-
-	m_vcolor["Quinto"]		=	"#e3f5f8";
-	m_vcolor["Qvinto"]		=	"#e3f5f8";
-
-	m_vcolor["Ottava parte [Ottavo]"]	=	"#e0e4f7";
-
-	m_vcolor["Nona parte [Nono]"]	=	"#a39ce5";
-
-	m_vcolor["Basso"]		=	"#d2aef7";
-	m_vcolor["Bass"]		=	"#d2aef7";
-
-	m_vcolor["Basso II"]		=	"#c69af5";
-	m_vcolor["Basso II [Decimo]"]	=	"#c69af5";
-
-	m_vcolor["Basso Continuo"]	=	"#a071ec";
-	m_vcolor["Basso continuo"]	=	"#a071ec";
-	m_vcolor["[B. C.]"]		=	"#a071ec";
-	m_vcolor["[Basso Continuo]"]	=	"#a071ec";
-	m_vcolor["[Basso continuo]"]	=	"#a071ec";
-	m_vcolor["B.C."]		=	"#a071ec";
-	m_vcolor["B.c."]		=	"#a071ec";
-}
-
-
-
-//////////////////////////////
-//
-// Tool_pccount::getFinal -- Extract the last unparenthesed letter from a ref record like this:
-//
-// !!!final: (A)D
-//
 
-string Tool_pccount::getFinal(HumdrumFile& infile) {
-	string finalref = infile.getReferenceRecord("final");
-	HumRegex hre;
-	hre.replaceDestructive(finalref, "", "\\(.*?\\)", "g");
-	hre.replaceDestructive(finalref, "", "\\s+", "g");
-	if (hre.search(finalref, "^[A-G]$", "i")) {
-		return finalref;
-	} else {
-		return "";
-	}
+	m_durationQ = !getBoolean("no-duration");
+	m_classQ    =  getBoolean("pitch-class");
+	m_octaveQ   =  getBoolean("octave");
+	m_attacksQ  =  getBoolean("attacks-only");
+	m_rest      =  getString("rest");
+	m_restQ     = !getBoolean("no-rests");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::processFile --
+// Tool_pnum::processFile --
 //
 
-void Tool_pccount::processFile(HumdrumFile& infile) {
-	countPitches(infile);
-
-	string datavar;
-	string target;
-	string jsonvar;
+void Tool_pnum::processFile(HumdrumFile& infile) {
+	vector<HTp> kex;
 
-	if (m_attack) {
-		datavar = "data_" + m_id + "_count";
-		target = "id_" + m_id + "_count";
-		jsonvar = "vega_" + m_id + "_count";
-	} else {
-		datavar = "data_" + m_id + "_dur";
-		target = "id_" + m_id + "_dur";
-		jsonvar = "vega_" + m_id + "_dur";
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (*token == "**kern") {
+				kex.push_back(token);
+				continue;
+			}
+			if (!token->isData()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			convertTokenToBase(token);
+		}
 	}
 
-	if (m_template) {
-		printVegaLiteJsonTemplate(datavar, infile);
-	} else if (m_data) {
-		printVegaLiteJsonData();
-	} else if (m_script) {
-		printVegaLiteScript(jsonvar, target, datavar, infile);
-	} else if (m_html) {
-		printVegaLiteHtml(jsonvar, target, datavar, infile);
-	} else if (m_page) {
-		printVegaLitePage(jsonvar, target, datavar, infile);
-	} else {
-		printHumdrumTable();
+	string newex;
+	for (int i=0; i<(int)kex.size(); i++) {
+		if (m_midiQ) {
+			newex = "**pmid";
+		} else {
+			newex = "**b" + to_string(m_base);
+		}
+		kex[i]->setText(newex);
 	}
 }
 
@@ -113373,561 +117852,553 @@ void Tool_pccount::processFile(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_pccount::printVegaLitePage --
-//
-
-void Tool_pccount::printVegaLitePage(const string& jsonvar,
-		const string& target, const string& datavar, HumdrumFile& infile) {
-	stringstream& out = m_free_text;
-
-	out << "<!DOCTYPE html>\n";
-	out << "<html>\n";
-	out << "  <head>\n";
-	out << "    <title>Vega-Lite Bar Chart</title>\n";
-	out << "    <meta charset=\"utf-8\" />\n";
-	out << "\n";
-	out << "    <script src=\"https://cdn.jsdelivr.net/npm/vega@5.4.0\"></script>\n";
-	out << "    <script src=\"https://cdn.jsdelivr.net/npm/vega-lite@4.0.0-beta.1\"></script>\n";
-	out << "    <script src=\"https://cdn.jsdelivr.net/npm/vega-embed@5\"></script>\n";
-	out << "\n";
-	out << "    <style media=\"screen\">\n";
-	out << "      /* Add space between Vega-Embed links  */\n";
-	out << "      .vega-actions a {\n";
-	out << "        margin-right: 5px;\n";
-	out << "      }\n";
-	out << "    </style>\n";
-	out << "  </head>\n";
-	out << "  <body>\n";
-	out << "    <h1>Pitch-class histogram</h1>\n";
-	printVegaLiteHtml(jsonvar, target, datavar, infile);
-	out << "</body>\n";
-	out << "</html>\n";
-}
-
-
-
-//////////////////////////////
-//
-// Tool_pccount::printVegaLiteHtml --
+// Tool_pnum::convertTokenToBase --
 //
 
-void Tool_pccount::printVegaLiteHtml(const string& jsonvar,
-		const string& target, const string& datavar, HumdrumFile& infile) {
-	stringstream& out = m_free_text;
-
-	out << "<div class=\"vega-svg\" id=\"" << target << "\"></div>\n";
-	out << "\n";
-	out << "<script>\n";
-	printVegaLiteScript(jsonvar, target, datavar, infile);
-	out << "</script>\n";
+void Tool_pnum::convertTokenToBase(HTp token) {
+	string output;
+	int scount = token->getSubtokenCount();
+	for (int i=0; i<scount; i++) {
+		string subtok = token->getSubtoken(i);
+		output += convertSubtokenToBase(subtok);
+		if (i < scount - 1) {
+			output += " ";
+		}
+	}
+	token->setText(output);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::printVegaLiteScript --
+// Tool_pnum::convertSubtokenToBase --
 //
 
-void Tool_pccount::printVegaLiteScript(const string& jsonvar,
-		const string& target, const string& datavar, HumdrumFile& infile) {
-	stringstream& out = m_free_text;
-
-	out << "var " << datavar << " =\n";
-	printVegaLiteJsonData();
-	out << ";\n";
-	out << "\n";
-	out << "var " << jsonvar << " =\n";
-	printVegaLiteJsonTemplate(datavar, infile);
-	out << ";\n";
-	out << "vegaEmbed('#" << target << "', " << jsonvar << ");\n";
-}
+string Tool_pnum::convertSubtokenToBase(const string& text) {
+	int pitch = 0;
+	if (text.find("r") == string::npos) {
+		switch (m_base) {
+			case 7:
+				pitch = Convert::kernToBase7(text);
+				break;
+			case 40:
+				pitch = Convert::kernToBase40(text);
+				break;
+			default:
+				pitch = Convert::kernToBase12(text);
+		}
+	} else if (!m_restQ) {
+		return ".";
+	}
+	string recip;
+	if (m_durationQ) {
+		HumRegex hre;
+		if (hre.search(text, "(\\d+%?\\d*\\.*)")) {
+			recip = hre.getMatch(1);
+		}
+	}
 
+	string output;
 
+	int pc = pitch % m_base;
+	int oct = pitch / m_base;
 
-//////////////////////////////
-//
-// Tool_pccount::printVegaLiteJsonData --
-//
+	if (m_midiQ) {
+		// MIDI numbers use 5 for middle-C octave.
+		pitch += 12;
+	}
 
-void Tool_pccount::printVegaLiteJsonData(void) {
-	stringstream& out = m_free_text;
+	int tie = 1;
+	if (text.find("_") != string::npos) {
+		tie = -1;
+	}
+	if (text.find("]") != string::npos) {
+		tie = -1;
+	}
+	pitch *= tie;
+	if (m_attacksQ && pitch < 0) {
+		return ".";
+	}
 
-	m_maxpc = 0;
-	for (int i=0; i<(int)m_counts[0].size(); i++) {
-		if (m_counts[0][i] > m_maxpc) {
-			m_maxpc = m_counts[0][i];
-		}
+	if (m_durationQ) {
+		output += recip;
+		output += "/";
 	}
-	out << "[\n";
-	int commacounter = 0;
-	double percent = 100.0;
-	for (int i=1; i<(int)m_counts.size(); i++) {
-		for (int j=0; j<(int)m_counts[i].size(); j++) {
-			if (m_counts[i][j] == 0.0) {
-				continue;
+
+	if (text.find("r") != string::npos) {
+		output += m_rest;
+	} else {
+		if (!m_octaveQ && !m_classQ) {
+			output += to_string(pitch);
+		} else {
+			if (m_classQ) {
+				if (pitch < 0) {
+					output += "-";
+				}
+				output += to_string(pc);
 			}
-			if (commacounter > 0) {
-				out << ",\n\t";
-			} else {
-				out << "\t";
+			if (m_classQ && m_octaveQ) {
+				output += ":";
 			}
-			commacounter++;
-			if (m_attack) {
-				out << "{\"count\":" << m_counts[i][j] << ", ";
-			} else {
-				out << "{\"percent\":" << m_counts[i][j]/m_maxpc*percent << ", ";
+			if (m_octaveQ) {
+				output += to_string(oct);
 			}
-			out << "\"pitch class\":\"" << getPitchClassString(j) << "\", ";
-			out << "\"voice\":\"" << m_names[i] << "\"";
-			out << "}";
 		}
 	}
-	out << "\n]\n";
+
+	return output;
 }
 
 
 
-//////////////////////////////
-//
-// Tool_pccount::setFactorMaximum -- normalize by the maximum pitch-class value.
-//
 
-void Tool_pccount::setFactorMaximum(void) {
-	m_factor = 0.0;
-	for (int i=0; i<(int)m_counts[0].size(); i++) {
-		if (m_counts[0][i] > m_factor) {
-			m_factor = m_counts[0][i];
-		}
-	}
-}
 
+#define OBJTAB "\t\t\t\t\t\t"
+#define SVGTAG "_99%svg%";
+
+#define SVGTEXT(out, text) \
+	if (m_defineQ) { \
+		out << "SVG "; \
+	} else { \
+		out << "t 1 1\n"; \
+		out << SVGTAG; \
+	} \
+	printScoreEncodedText((out), (text)); \
+	out << "\n";
 
 
 //////////////////////////////
 //
-// Tool_pccount::setFactorNormalize -- normalize by the sum of all pitch class values.
+// _VoiceInfo::_VoiceInfo --
 //
 
-void Tool_pccount::setFactorNormalize(void) {
-	m_factor = 0.0;
-	for (int i=0; i<(int)m_counts[0].size(); i++) {
-		m_factor += m_counts[0][i];
-	}
+_VoiceInfo::_VoiceInfo(void) {
+	clear();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::printHumdrumTable --
+// _VoiceInfo::clear --
 //
 
-void Tool_pccount::printHumdrumTable(void) {
-
-	double factor = 0.0;
-
-	if (m_maximum) {
-		setFactorMaximum();
-		m_free_text << "!!!MAX: " << m_factor << endl;
-	} else if (m_normalize) {
-		setFactorNormalize();
-		m_free_text << "!!!TOTAL: " << factor << endl;
-	}
-
-	// exclusive interpretation
-	m_free_text << "**kern";
-	m_free_text << "\t**all";
-	for (int i=0; i<(int)m_counts.size() - 1; i++) {
-		m_free_text << "\t**part";
+void _VoiceInfo::clear(void) {
+	name = "";
+	abbr = "";
+	midibins.resize(128);
+	fill(midibins.begin(), midibins.end(), 0.0);
+	diatonic.resize(7 * 12);
+	for (int i=0; i<(int)diatonic.size(); i++) {
+		diatonic[i].resize(6);
+		fill(diatonic[i].begin(), diatonic[i].end(), 0.0);
 	}
-	m_free_text << endl;
+	track = -1;
+	kernQ = false;
+	diafinal.clear();
+	accfinal.clear();
+	namfinal.clear();
+	index = -1;
+}
 
-	// part names
-	m_free_text << "*";
-	for (int i=0; i<(int)m_counts.size(); i++) {
-		if (i < (int)m_names.size()) {
-			m_free_text << "\t*I\"" << m_names.at(i);
-		} else {
-			m_free_text << "\t*";
-		}
-	}
-	m_free_text << endl;
 
-	if (!m_abbreviations.empty()) {
+//////////////////////////////
+//
+// _VoiceInfo::print --
+//
 
-		// part abbreviation
-		m_free_text << "*";
-		for (int i=0; i<(int)m_counts.size(); i++) {
-			if (i < (int)m_abbreviations.size()) {
-				m_free_text << "\t*I\'" << m_abbreviations.at(i);
-			} else {
-				m_free_text << "\t*";
-			}
-		}
-		m_free_text << endl;
+ostream& _VoiceInfo::print(ostream& out) {
+	out << "==================================" << endl;
+	out << "track:  " << track << endl;
+	out << " name:  " << name << endl;
+	out << " abbr:  " << abbr << endl;
+	out << " kern:  " << kernQ << endl;
+	out << " final:";
+	for (int i=0; i<(int)diafinal.size(); i++) {
+		out << " " << diafinal.at(i) << "/" << accfinal.at(i);
 	}
-
-	for (int i=0; i<(int)m_counts[0].size(); i++) {
-		if (m_counts[0][i] == 0) {
-			continue;
-		}
-		if ((i == 5) || (i == 11) || (i == 22) || (i == 28) || (i == 34)) {
-			continue;
-		}
-		string pitch = Convert::base40ToKern(i + 4*40);
-		m_free_text << pitch;
-		for (int j=0; j<(int)m_counts.size(); j++) {
-			if (m_normalize) {
-				m_free_text << "\t" << m_counts[j][i] / m_factor;
-			} else if (m_maximum) {
-				m_free_text << "\t" << m_counts[j][i] / m_factor;
-			} else {
-				m_free_text << "\t" << m_counts[j][i];
-			}
+	out << endl;
+	out << " midi:  ";
+	for (int i=0; i<(int)midibins.size(); i++) {
+		if (midibins.at(i) > 0.0) {
+			out << " " << i << ":" << midibins.at(i);
 		}
-		m_free_text << endl;
 	}
-
-	int columns = (int)m_counts.size() + 1;
-	for (int i=0; i<columns; i++) {
-		m_free_text << "*-";
-		if (i < columns - 1) {
-			m_free_text << "\t";
+	out << endl;
+	out << " diat:  ";
+	for (int i=0; i<(int)diatonic.size(); i++) {
+		if (diatonic.at(i).at(0) > 0.0) {
+			out << " " << i << ":" << diatonic.at(i).at(0);
 		}
 	}
-	m_free_text << endl;
+	out << endl;
+	out << "==================================" << endl;
+	return out;
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_pccount::countPitches --
+// Tool_prange::Tool_prange -- Set the recognized options for the tool.
 //
 
-void Tool_pccount::countPitches(HumdrumFile& infile) {
-	if (m_parttracks.size() == 0) {
-		return;
-	}
-	m_counts.clear();
-	m_counts.resize(m_parttracks.size());
-	for (int i=0; i<(int)m_parttracks.size(); i++) {
-		m_counts[i].resize(40);
-		fill(m_counts[i].begin(), m_counts[i].end(), 0.0);
-	}
-	for (int i=0; i<infile.getStrandCount(); i++) {
-		HTp sstart = infile.getStrandStart(i);
-		HTp send = infile.getStrandEnd(i);
-		addCounts(sstart, send);
-	}
+Tool_prange::Tool_prange(void) {
 
-	// fill in sum for all parts
-	for (int i=0; i<(int)m_counts[0].size(); i++) {
-		for (int j=1; j<(int)m_counts.size(); j++) {
-			m_counts[0][i] += m_counts[j][i];
-		}
-	}
+	define("A|acc|color-accidentals=b", "add color to accidentals in histogram");
+	define("D|diatonic=b",              "diatonic counts ignore chormatic alteration");
+	define("K|no-key=b",                "do not display key signature");
+	define("N|norm=b",                  "normalize pitch counts");
+	define("S|score=b",                 "convert range info to SCORE");
+	define("T|no-title=b",              "do not display a title");
+	define("a|all=b",                   "generate all-voice analysis");
+	define("c|range|count=s:60-71",     "count notes in a particular MIDI note number range (inclusive)");
+	define("debug=b",                   "trace input parsing");
+	define("d|duration=b",              "weight pitches by duration");
+	define("e|embed=b",                 "embed SCORE data in input Humdrum data");
+	define("fill=b",                    "change color of fill only");
+	define("finalis|final|last=b",      "include finalis note by voice");
+	define("f|fraction=b",              "display histogram fractions");
+	define("h|hover=b",                 "include svg hover capabilities");
+	define("i|instrument=b",            "categorize multiple inputs by instrument");
+	define("j|jrp=b",                   "set options for JRP style");
+	define("l|local|local-maximum|local-maxima=b",  "use maximum values by voice rather than all voices");
+	define("no-define=b",               "do not use defines in output SCORE data");
+	define("pitch=b",                   "display pitch info in **pitch format");
+	define("print=b",                   "count printed notes rather than sounding");
+	define("p|percentile=d:0.0",        "display the xth percentile pitch");
+	define("q|quartile=b",              "display quartile notes");
+	define("r|reverse=b",               "reverse list of notes in analysis from high to low");
+	define("x|extrema=b",               "highlight extrema notes in each part");
+	define("sx|scorexml|score-xml|ScoreXML|scoreXML=b", "output ScoreXML format");
+	define("title=s:",                  "title for SCORE display");
 
 }
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_pccount::addCounts --
+// Tool_prange::run -- Do the main work of the tool.
 //
 
-void Tool_pccount::addCounts(HTp sstart, HTp send) {
-	if (!sstart) {
-		return;
+bool Tool_prange::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
-	if (!sstart->isKern()) {
-		return;
+	return status;
+}
+
+
+bool Tool_prange::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	int track = sstart->getTrack();
-	int kindex = m_rkern[track];
-	HTp current = sstart;
-	while (current && (current != send)) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull() || current->isRest()) {
-			current = current->getNextToken();
-			continue;
-		}
-		vector<string> subtokens = current->getSubtokens();
-		for (int i=0; i<(int)subtokens.size(); i++) {
-			if (m_attack) {
-				// ignore sustained parts of notes when counting attacks
-				if (subtokens[i].find("_") != string::npos) {
-					continue;
-				}
-				if (subtokens[i].find("]") != string::npos) {
-					continue;
-				}
-			}
-			int b40 = Convert::kernToBase40(subtokens[i]);
-			if (m_attack) {
-				m_counts[kindex][b40%40]++;
-			} else {
-				double duration = Convert::recipToDuration(subtokens[i]).getFloat();
-				m_counts[kindex][b40%40] += duration;
-			}
-		}
-		current = current->getNextToken();
+	return status;
+}
+
+
+bool Tool_prange::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
 }
 
 
+bool Tool_prange::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
+}
+
 
 
 //////////////////////////////
 //
-// Tool_pccount::initializePartInfo --
+// Tool_prange::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_pccount::initializePartInfo(HumdrumFile& infile) {
-	m_names.clear();
-	m_abbreviations.clear();
-	m_parttracks.clear();
-	m_rkern.clear();
-
-	m_rkern.resize(infile.getTrackCount() + 1);
-	fill(m_rkern.begin(), m_rkern.end(), -1);
-
-	m_parttracks.push_back(-1);
-	m_names.push_back("all");
-	m_abbreviations.push_back("all");
+void Tool_prange::initialize(void) {
+	m_accQ         = getBoolean("color-accidentals");
+	m_addFractionQ = getBoolean("fraction");
+	m_allQ         = getBoolean("all");
+	m_debugQ       = getBoolean("debug");
+	m_defineQ      = false;
+	m_diatonicQ    = getBoolean("diatonic");
+	m_durationQ    = getBoolean("duration");
+	m_fillOnlyQ    = getBoolean("fill");
+	m_finalisQ     = getBoolean("finalis");
+	m_hoverQ       = getBoolean("hover");
+	m_instrumentQ  = getBoolean("instrument");
+	m_keyQ         = !getBoolean("no-key");
+	m_listQ        = false;
+	m_localQ       = getBoolean("local-maximum");
+	m_normQ        = getBoolean("norm");
+	m_notitleQ     = getBoolean("no-title");
+	m_percentile   = getDouble("percentile");
+	m_percentileQ  = getBoolean("percentile");
+	m_pitchQ       = getBoolean("pitch");
+	m_printQ       = getBoolean("print");
+	m_quartileQ    = getBoolean("quartile");
+	m_rangeQ       = getBoolean("range");
+	m_reverseQ     = !getBoolean("reverse");
+	m_scoreQ       = getBoolean("score");
+	m_title        = getString("title");
+	m_titleQ       = getBoolean("title");
+	m_embedQ       = getBoolean("embed");
+	m_extremaQ     = getBoolean("extrema");
 
-	vector<HTp> starts = infile.getKernSpineStartList();
+	getRange(m_rangeL, m_rangeH, getString("range"));
 
-	int foundpart = false;
-	int foundabbr = false;
+	if (getBoolean("jrp")) {
+		// default style settings for JRP range displays:
+		m_scoreQ   = true;
+		m_allQ     = true;
+		m_hoverQ   = true;
+		m_accQ     = true;
+		m_finalisQ = true;
+		m_notitleQ = true;
+	}
 
-	int track = 0;
-	for (int i=0; i<(int)starts.size(); i++) {
-		track = starts[i]->getTrack();
-		m_rkern[track] = i+1;
-		m_parttracks.push_back(track);
-		HTp current = starts[i];
-		foundpart = false;
-		foundabbr = false;
-		if (!current->isKern()) {
-			continue;
-		}
-		while (current) {
-			if (current->isData()) {
-				break;
-			}
-			if ((!foundpart) && (current->compare(0, 3, "*I\"") == 0)) {
-				m_names.emplace_back(current->substr(3));
-				foundpart = true;
-			} else if ((!foundabbr) && (current->compare(0, 3, "*I\'") == 0)) {
-				m_abbreviations.emplace_back(current->substr(3));
-				foundabbr = true;
-			}
-			current = current->getNextToken();
-		}
-		//if (!foundpart) {
-		//		m_names.emplace_back("");
-		//}
-		//if (!foundabbr) {
-		//		m_names.emplace_back("");
-		//}
+	// The percentile is a fraction from 0.0 to 1.0.
+	// if the percentile is above 1.0, then it is assumed
+	// to be a percentage, in which case the value will be
+	// divided by 100 to get it in the range from 0 to 1.
+	if (m_percentile > 1) {
+		m_percentile = m_percentile / 100.0;
 	}
 
+	#ifdef __EMSCRIPTEN__
+		// Default styling for JavaScript version of program:
+		m_accQ     = !getBoolean("color-accidentals");
+		m_scoreQ   = !getBoolean("score");
+		m_embedQ   = !getBoolean("embed");
+		m_hoverQ   = !getBoolean("hover");
+		m_notitleQ = !getBoolean("no-title");
+	#endif
 }
 
 
+
 //////////////////////////////
 //
-// printVegaLiteJsonTemplate --
+// Tool_prange::processFile --
 //
 
-void Tool_pccount::printVegaLiteJsonTemplate(const string& datavariable, HumdrumFile& infile) {
-	stringstream& out = m_free_text;
+void Tool_prange::processFile(HumdrumFile& infile) {
+	prepareRefmap(infile);
+	vector<_VoiceInfo> voiceInfo;
+	infile.fillMidiInfo(m_trackMidi);
+	getVoiceInfo(voiceInfo, infile);
+	fillHistograms(voiceInfo, infile);
 
-	string idinfo;
-	if (m_id.empty() || m_id == "id") {
-		// do nothing
-	} else {
-		idinfo = "for " + m_id;
-	}
-	out << "{\n";
-	out << "	\"$schema\": \"https://vega.github.io/schema/vega-lite/v4.0.0-beta.1.json\",\n";
-	out << "	\"data\": {\"values\": " << datavariable << "},\n";
-	if (getBoolean("title")) {
-		out << "	\"title\": \"" << m_title << "\",\n";
-	} else {
-		if (m_attack) {
-			out << "	\"title\": \"Note-count pitch-class distribution " << idinfo <<" \",\n";
-		} else {
-			out << "	\"title\": \"Duration-weighted pitch-class distribution " << idinfo <<" \",\n";
+	if (m_debugQ) {
+		for (int i=0; i<(int)voiceInfo.size(); i++) {
+			voiceInfo[i].print(cerr);
 		}
 	}
-	out << "	\"width\": " << m_width << ",\n";
-	out << "	\"height\": " << int(m_width * m_ratio) << ",\n";
-	out << "	\"encoding\": {\n";
-	out << "		\"y\": {\n";
-	if (m_attack) {
-		out << "			\"field\": \"count\",\n";
-		out << "			\"title\": \"Number of note attacks\",\n";
-	} else {
-		out << "			\"field\": \"percent\",\n";
-		out << "			\"title\": \"Percent of maximum pitch class\",\n";
-	}
-	out << "			\"type\": \"quantitative\",\n";
-	if (m_attack) {
-		out << "			\"scale\": {\"domain\": [0, " << m_maxpc << "]},\n";
-	} else {
-		out << "			\"scale\": {\"domain\": [0, 100]},\n";
-	}
-	out << "			\"aggregate\": \"sum\"\n";
-	out << "		},\n";
-	out << "		\"x\": {\n";
-	out << "			\"field\": \"pitch class\",\n";
-	out << "			\"type\": \"nominal\",\n";
-	out << "			\"scale\": {\n";
-	out << "				\"domain\": [";
-		printPitchClassList();
-		out << "]\n";
-	out << "			},\n";
-	out << "			\"axis\": {\n";
-	out << "				\"labelAngle\": 0\n";
-	out << "			}\n";
-	out << "		},\n";
-	out << "		\"order\": {\"type\": \"quantitative\"},\n";
-	out << "		\"color\": {\n";
-	out << "			\"field\": \"voice\",\n";
-	out << "			\"type\": \"nominal\",\n";
-	if (m_counts.size() == 2) {
-		out << "			\"legend\": {\"title\": \"Voice\"},\n";
-	} else {
-		out << "			\"legend\": {\"title\": \"Voices\"},\n";
-	}
-	out << "			\"scale\": {\n";
-  	out << "				\"domain\": [";
-		printVoiceList();
-		out << "],\n";
- 	out << "				\"range\": [";
-		printColorList();
-		out << "],\n";
- 	out << "				}\n";
-	out << "		}\n";
-	out << "	},\n";
-
-	out << "	\"layer\": [\n";
-	out << "		{\"mark\": \"bar\"}";
 
-	string final = getFinal(infile);
-	if (m_key && !final.empty()) {
-		out << ",\n";
-		out << "		{\n";
-		out << "			\"mark\": {\"type\":\"text\", \"align\":\"center\", \"fill\":\"black\", \"baseline\":\"bottom\"},\n";
-		if (m_attack) {
-			int count = getCount(final);
-			out << "			\"data\": {\"values\": [ {\"pitch class\":\"" << final << "\", \"count\":" << count << "}]},\n";
+	if (m_scoreQ) {
+		stringstream scoreout;
+		printScoreFile(scoreout, voiceInfo, infile);
+		if (m_embedQ) {
+			if (m_extremaQ) {
+				doExtremaMarkup(infile);
+			}
+			m_humdrum_text << infile;
+			printEmbeddedScore(m_humdrum_text, scoreout, infile);
 		} else {
-			double percent = getPercent(final);
-			out << "			\"data\": {\"values\": [ {\"pitch class\":\"" << final << "\", \"percent\":" << percent << "}]},\n";
+			if (m_extremaQ) {
+				doExtremaMarkup(infile);
+			}
+			m_humdrum_text << scoreout.str();
 		}
-		out << "			\"encoding\": {\"text\": {\"value\":\"final\"}}\n";
-		out << "		}\n";
+	} else {
+		printAnalysis(m_humdrum_text, voiceInfo[0].midibins);
 	}
-
-	out << "	]\n";
-	out << "}\n";
-
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::getCount --
+// Tool_prange::doExtremaMarkup -- Mark highest and lowest note
+//     in each **kern spine.
+//
 //
 
-int Tool_pccount::getCount(const string& pitchclass) {
-	int b40 = Convert::kernToBase40(pitchclass);
-	int index = b40 % 40;
-	int output = (int)m_counts[0][index];
-	return output;
+void Tool_prange::doExtremaMarkup(HumdrumFile& infile) {
+	bool highQ = false;
+	bool lowQ = false;
+	for (int i=0; i<(int)m_trackMidi.size(); i++) {
+		int maxindex = -1;
+		int minindex = -1;
+
+		for (int j=(int)m_trackMidi[i].size()-1; j>=0; j--) {
+			if (m_trackMidi[i][j].empty()) {
+				continue;
+			}
+			if (maxindex < 0) {
+				maxindex = j;
+				break;
+			}
+		}
+
+		for (int j=1; j<(int)m_trackMidi[i].size(); j++) {
+			if (m_trackMidi[i][j].empty()) {
+				continue;
+			}
+			if (minindex < 0) {
+				minindex = j;
+				break;
+			}
+		}
+
+		if ((maxindex < 0) || (minindex < 0)) {
+			continue;
+		}
+		applyMarkup(m_trackMidi[i][maxindex], m_highMark);
+		applyMarkup(m_trackMidi[i][minindex], m_lowMark);
+		highQ = true;
+		lowQ  = true;
+	}
+	if (highQ) {
+		string highRdf = "!!!RDF**kern: " + m_highMark + " = marked note, color=\"hotpink\", highest note";
+		infile.appendLine(highRdf);
+	}
+	if (lowQ) {
+		string lowRdf = "!!!RDF**kern: " + m_lowMark + " = marked note, color=\"limegreen\", lowest note";
+		infile.appendLine(lowRdf);
+	}
+	if (highQ || lowQ) {
+		infile.createLinesFromTokens();
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::getPercent --
+// Tool_prange::applyMarkup --
 //
 
-double Tool_pccount::getPercent(const string& pitchclass) {
-	setFactorMaximum();
-	int b40 = Convert::kernToBase40(pitchclass);
-	int index = b40 % 40;
-	double output = m_counts[0][index] / m_factor * 100.0;
-	return output;
+void Tool_prange::applyMarkup(vector<pair<HTp, int>>& notelist, const string& mark) {
+	for (int i=0; i<(int)notelist.size(); i++) {
+		HTp token = notelist[i].first;
+		int subtoken = notelist[i].second;
+		int tokenCount = token->getSubtokenCount();
+		if (tokenCount == 1) {
+			string text = *token;
+			text += mark;
+			token->setText(text);
+		} else {
+			string stok = token->getSubtoken(subtoken);
+			stok = mark + stok;
+			token->replaceSubtoken(subtoken, stok);
+		}
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::printColorList --
+// Tool_prange::printEmbeddedScore --
 //
 
-void Tool_pccount::printColorList(void) {
-	stringstream& out = m_free_text;
-	for (int i=(int)m_names.size() - 1; i>0; i--) {
-		string color = m_vcolor[m_names[i]];
-		out << "\"";
-		if (color.empty()) {
-			out << "black";
-		} else {
-			out << color;
-		}
-		out << "\"";
-		if (i > 1) {
-			out << ", ";
-		}
+void Tool_prange::printEmbeddedScore(ostream& out, stringstream& scoredata, HumdrumFile& infile) {
+	int id = getPrangeId(infile);
+
+	out << "!!@@BEGIN: PREHTML\n";
+	out << "!!@CONTENT: <div class=\"score-svg\" ";
+	out <<    "style=\"margin-top:50px;text-align:center;\" ";
+	out <<    " data-score=\"prange-" << id << "\"></div>\n";
+	out << "!!@@END: PREHTML\n";
+	out << "!!@@BEGIN: SCORE\n";
+	out << "!!@ID: prange-" << id << "\n";
+	out << "!!@OUTPUTFORMAT: svg\n";
+	out << "!!@CROP: yes\n";
+	out << "!!@PADDING: 10\n";
+	out << "!!@SCALING: 1.5\n";
+	out << "!!@SVGFORMAT: yes\n";
+	out << "!!@TRANSPARENT: yes\n";
+	out << "!!@ANTIALIAS: no\n";
+	out << "!!@EMBEDPMX: yes\n";
+	out << "!!@ANNOTATE: no\n";
+	out << "!!@CONTENTS:\n";
+	string line;
+	while(getline(scoredata, line)) {
+		out << "!!" << line << endl;
 	}
+	out << "!!@@END: SCORE\n";
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::printVoiceList --
+// Tool_prange::getPrangeId -- Find a line in this form
+//          ^!!@ID: prange-(\d+)$
+//      and return $1+1.  Searching backwards since the HTML section
+//      will likely be at the bottom.  Assuming that the prange
+//      SVG images are stored in sequence, with the highest ID last
+//      in the file if there are more than one.
 //
 
-void Tool_pccount::printVoiceList(void) {
-	stringstream& out = m_free_text;
-	for (int i=(int)m_names.size() - 1; i>0; i--) {
-		out << "\"";
-		out << m_names[i];
-		out << "\"";
-		if (i > 1) {
-			out << ", ";
+int Tool_prange::getPrangeId(HumdrumFile& infile) {
+	string search = "!!@ID: prange-";
+	int length = (int)search.length();
+	for (int i=infile.getLineCount() - 1; i>=0; i--) {
+		HTp token = infile.token(i, 0);
+		if (token->compare(0, length, search) == 0) {
+			HumRegex hre;
+			if (hre.search(token, "prange-(\\d+)")) {
+				return hre.getMatchInt(1) + 1;
+			}
 		}
 	}
+	return 1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pccount::printReverseVoiceList --
+// Tool_prange::mergeAllVoiceInfo --
 //
 
-void Tool_pccount::printReverseVoiceList(void) {
-	stringstream& out = m_free_text;
-	for (int i=1; i<(int)m_names.size(); i++) {
-		out << "\"";
-		out << m_names[i];
-		out << "\"";
-		if (i < (int)m_names.size() - 1) {
-			out << ", ";
+void Tool_prange::mergeAllVoiceInfo(vector<_VoiceInfo>& voiceInfo) {
+	voiceInfo.at(0).diafinal.clear();
+	voiceInfo.at(0).accfinal.clear();
+
+	for (int i=1; i<(int)voiceInfo.size(); i++) {
+		if (!voiceInfo[i].kernQ) {
+			continue;
+		}
+		for (int j=0; j<(int)voiceInfo.at(i).diafinal.size(); j++) {
+			voiceInfo.at(0).diafinal.push_back(voiceInfo.at(i).diafinal.at(j));
+			voiceInfo.at(0).accfinal.push_back(voiceInfo.at(i).accfinal.at(j));
+			voiceInfo.at(0).namfinal.push_back(voiceInfo.at(i).name);
+		}
+
+		for (int j=0; j<(int)voiceInfo[i].midibins.size(); j++) {
+			voiceInfo[0].midibins[j] += voiceInfo[i].midibins[j];
+		}
+
+		for (int j=0; j<(int)voiceInfo.at(i).diatonic.size(); j++) {
+			for (int k=0; k<(int)voiceInfo.at(i).diatonic.at(k).size(); k++) {
+				voiceInfo[0].diatonic.at(j).at(k) += voiceInfo.at(i).diatonic.at(j).at(k);
+			}
 		}
 	}
 }
@@ -113936,234 +118407,308 @@ void Tool_pccount::printReverseVoiceList(void) {
 
 //////////////////////////////
 //
-// Tool_pccount::printPitchClassList --
+// Tool_prange::getVoiceInfo -- get names and track info for **kern spines.
 //
 
-void Tool_pccount::printPitchClassList(void) {
-	stringstream& out = m_free_text;
-
-	if (m_counts[0][0] > 0.0)  { out << "\"C♭♭\", "; }
-	if (m_counts[0][1] > 0.0)  { out << "\"C♭\", "; }
-	out << "\"C\"";
-	if (m_counts[0][3] > 0.0)  { out << ", \"C♯\""; }
-	if (m_counts[0][4] > 0.0)  { out << ", \"C♯♯\""; }
-	// 5 is empty
-
-	if (m_counts[0][6] > 0.0)  { out << ", \"D♭♭\""; }
-	if (m_counts[0][7] > 0.0)  { out << ", \"D♭\""; }
-	out << ", \"D\"";
-	if (m_counts[0][9] > 0.0)  { out << ", \"D♯\""; }
-	if (m_counts[0][10] > 0.0) { out << ", \"D♯♯\""; }
-	// 11 is empty
+void Tool_prange::getVoiceInfo(vector<_VoiceInfo>& voiceInfo, HumdrumFile& infile) {
+	voiceInfo.clear();
+	voiceInfo.resize(infile.getMaxTracks() + 1);
+	for (int i=0; i<(int)voiceInfo.size(); i++) {
+		voiceInfo.at(i).index = i;
+	}
 
-	if (m_counts[0][12] > 0.0) { out << ", \"E♭♭\""; }
-	if (m_counts[0][13] > 0.0) { out << ", \"E♭\""; }
-	out << ", \"E\"";
-	if (m_counts[0][15] > 0.0) { out << ", \"E♯\""; }
-	if (m_counts[0][16] > 0.0) { out << ", \"E♯♯\""; }
+	vector<HTp> kstarts = infile.getKernSpineStartList();
 
-	if (m_counts[0][17] > 0.0) { out << ", \"F♭♭\""; }
-	if (m_counts[0][18] > 0.0) { out << ", \"F♭\""; }
-	out << ", \"F\"";
-	if (m_counts[0][20] > 0.0) { out << ", \"F♯\""; }
-	if (m_counts[0][21] > 0.0) { out << ", \"F♯♯\""; }
-	// 22 is empty
+	if (kstarts.size() == 2) {
+		voiceInfo[0].name  = "both";
+		voiceInfo[0].abbr  = "both";
+		voiceInfo[0].track = 0;
+	} else {
+		voiceInfo[0].name  = "all";
+		voiceInfo[0].abbr  = "all";
+		voiceInfo[0].track = 0;
+	}
 
-	if (m_counts[0][23] > 0.0) { out << ", \"G♭♭\""; }
-	if (m_counts[0][24] > 0.0) { out << ", \"G♭\""; }
-	out << ", \"G\"";
-	if (m_counts[0][26] > 0.0) { out << ", \"G♯\""; }
-	if (m_counts[0][27] > 0.0) { out << ", \"G♯♯\""; }
-	// 28 is empty
 
-	if (m_counts[0][29] > 0.0) { out << ", \"A♭♭\""; }
-	if (m_counts[0][30] > 0.0) { out << ", \"A♭\""; }
-	out << ", \"A\"";
-	if (m_counts[0][32] > 0.0) { out << ", \"A♯\""; }
-	if (m_counts[0][33] > 0.0) { out << ", \"A♯♯\""; }
-	// 34 is empty
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			break;
+		}
+		if (!infile[i].hasSpines()) {
+			continue;
+		}
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			int track = token->getTrack();
+			voiceInfo[track].track = track;
+			if (token->isKern()) {
+				voiceInfo[track].kernQ = true;
+			}
+			if (!voiceInfo[track].kernQ) {
+				continue;
+			}
+			if (token->isInstrumentName()) {
+				voiceInfo[track].name = token->getInstrumentName();
+			}
+			if (token->isInstrumentAbbreviation()) {
+				voiceInfo[track].abbr = token->getInstrumentAbbreviation();
+			}
+		}
+	}
 
-	if (m_counts[0][35] > 0.0) { out << ", \"B♭♭\""; }
-	if (m_counts[0][36] > 0.0) { out << ", \"B♭\""; }
-	out << ", \"B\"";
-	if (m_counts[0][38] > 0.0) { out << ", \"B♯\""; }
-	if (m_counts[0][39] > 0.0) { out << ", \"B♯♯\""; }
 
+	// Check for piano/Grand Staff parts with LH/RH encoding.
+	if (kstarts.size() == 2) {
+		string bottomStaff = getHand(kstarts[0]);
+		string topStaff    = getHand(kstarts[1]);
+		if (!bottomStaff.empty() && !topStaff.empty()) {
+			int track = kstarts[0]->getTrack();
+			voiceInfo[track].name = "left hand";
+			track = kstarts[1]->getTrack();
+			voiceInfo[track].name = "right hand";
+		}
+	}
 }
 
 
+
 //////////////////////////////
 //
-// Tool_pccount::getPitchClassString --
+// Tool_prange::getHand --
 //
 
-string Tool_pccount::getPitchClassString(int b40) {
-	switch (b40%40) {
-		case 0: return "C♭♭";
-		case 1: return "C♭";
-		case 2: return "C";
-		case 3: return "C♯";
-		case 4: return "C♯♯";
-		// 5 is empty
-		case 6: return "D♭♭";
-		case 7: return "D♭";
-		case 8: return "D";
-		case 9: return "D♯";
-		case 10: return "D♯♯";
-		// 11 is empty
-		case 12: return "E♭♭";
-		case 13: return "E♭";
-		case 14: return "E";
-		case 15: return "E♯";
-		case 16: return "E♯♯";
-		case 17: return "F♭♭";
-		case 18: return "F♭";
-		case 19: return "F";
-		case 20: return "F♯";
-		case 21: return "F♯♯";
-		// 22 is empty
-		case 23: return "G♭♭";
-		case 24: return "G♭";
-		case 25: return "G";
-		case 26: return "G♯";
-		case 27: return "G♯♯";
-		// 28 is empty
-		case 29: return "A♭♭";
-		case 30: return "A♭";
-		case 31: return "A";
-		case 32: return "A♯";
-		case 33: return "A♯♯";
-		// 34 is empty
-		case 35: return "B♭♭";
-		case 36: return "B♭";
-		case 37: return "B";
-		case 38: return "B♯";
-		case 39: return "B♯♯";
+string Tool_prange::getHand(HTp sstart) {
+	HTp current = sstart->getNextToken();
+	HTp target = NULL;
+	while (current) {
+		if (current->isData()) {
+			break;
+		}
+		if (*current == "*LH") {
+			target = current;
+			break;
+		}
+		if (*current == "*RH") {
+			target = current;
+			break;
+		}
+		current = current->getNextToken();
 	}
 
-	return "?";
+	if (target) {
+		if (*current == "*LH") {
+			return "LH";
+		} else if (*current == "*RH") {
+			return "RH";
+		} else {
+			return "";
+		}
+	} else {
+		return "";
+	}
 }
 
 
 
-
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_periodicity::Tool_periodicity -- Set the recognized options for the tool.
+// Tool_prange::getInstrumentNames --  Find any instrument names which are listed
+//      before the first data line.  Instrument names are in the form:
+//
+//      *I"name
 //
 
-Tool_periodicity::Tool_periodicity(void) {
-	define("m|min=b",         "minimum time unit (other than grace notes)");
-	define("n|max-rows=i:-1", "maxumum number of rows in svg analysis display");
-	define("t|track=i:0",     "track to analyze");
-	define("attacks=b",       "extract attack grid)");
-	define("raw=b",           "show only raw period data");
-	define("s|svg=b",         "output svg image");
-	define("p|power=d:2.0",   "scaling power for visual display");
-	define("1|one=b",         "composite rhythms are not weighted by attack");
+void Tool_prange::getInstrumentNames(vector<string>& nameByTrack, vector<int>& kernSpines,
+		HumdrumFile& infile) {
+	HumRegex hre;
+
+	int track;
+	string name;
+	// nameByTrack.resize(kernSpines.size());
+	nameByTrack.resize(infile.getMaxTrack() + 1);
+	fill(nameByTrack.begin(), nameByTrack.end(), "");
+	vector<HTp> kspines = infile.getKernSpineStartList();
+	if (kspines.size() == 2) {
+		nameByTrack.at(0) = "both";
+	} else {
+		nameByTrack.at(0) = "all";
+	}
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (hre.search(token, "^\\*I\"(.*)\\s*")) {
+				name = hre.getMatch(1);
+				track = token->getTrack();
+				for (int k=0; k<(int)kernSpines.size(); k++) {
+					if (track == kernSpines[k]) {
+						nameByTrack[k] = name;
+					}
+				}
+			}
+		}
+	}
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_periodicity::run -- Primary interfaces to the tool.
+// Tool_prange::fillHistograms -- Store notes in score by MIDI note number.
 //
 
-bool Tool_periodicity::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
+void Tool_prange::fillHistograms(vector<_VoiceInfo>& voiceInfo, HumdrumFile& infile) {
+	// storage for finals info:
+	vector<vector<int>> diafinal;
+	vector<vector<int>> accfinal;
+	diafinal.resize(infile.getMaxTracks() + 1);
+	accfinal.resize(infile.getMaxTracks() + 1);
+
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			int track = token->getTrack();
 
+			diafinal.at(track).clear();
+			accfinal.at(track).clear();
 
-bool Tool_periodicity::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status;
-	status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
+			vector<string> tokens = token->getSubtokens();
+			for (int k=0; k<(int)tokens.size(); k++) {
+				if (tokens[k].find("r") != string::npos) {
+					continue;
+				}
+				if (tokens[k].find("R") != string::npos) {
+					// non-pitched note
+					continue;
+				}
+				bool hasPitch = false;
+				for (int m=0; m<(int)tokens[k].size(); m++) {
+					char test = tokens[k].at(m);
+					if (!isalpha(test)) {
+						continue;
+					}
+					test = tolower(test);
+					if ((test >= 'a') && (test <= 'g')) {
+						hasPitch = true;
+						break;
+					}
+				}
+				if (!hasPitch) {
+					continue;
+				}
+				int octave = Convert::kernToOctaveNumber(tokens[k]) + 3;
+				if (octave < 0) {
+					cerr << "Note too low: " << tokens[k] << endl;
+					continue;
+				}
+				if (octave >= 12) {
+					cerr << "Note too high: " << tokens[k] << endl;
+					continue;
+				}
+				int dpc    = Convert::kernToDiatonicPC(tokens[k]);
+				int acc    = Convert::kernToAccidentalCount(tokens[k]);
+				if (acc < -2) {
+					cerr << "Accidental too flat: " << tokens[k] << endl;
+					continue;
+				}
+				if (acc > +2) {
+					cerr << "Accidental too sharp: " << tokens[k] << endl;
+					continue;
+				}
+				int diatonic = dpc + 7 * octave;
+				int realdiatonic = dpc + 7 * (octave-3);
 
+				diafinal.at(track).push_back(realdiatonic);
+				accfinal.at(track).push_back(acc);
 
-bool Tool_periodicity::run(HumdrumFile& infile, ostream& out) {
-	bool status;
-	status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+				acc += 3;
+				int midi = Convert::kernToMidiNoteNumber(tokens[k]);
+				if (midi < 0) {
+					cerr << "MIDI pitch too low: " << tokens[k] << endl;
+				}
+				if (midi > 127) {
+					cerr << "MIDI pitch too high: " << tokens[k] << endl;
+				}
+				if (m_durationQ) {
+					double duration = Convert::kernToDuration(tokens[k]).getFloat();
+					voiceInfo[track].diatonic.at(diatonic).at(0) += duration;
+					voiceInfo[track].diatonic.at(diatonic).at(acc) += duration;
+					voiceInfo[track].midibins.at(midi) += duration;
+				} else {
+					if (tokens[k].find("]") != string::npos) {
+						continue;
+					}
+					if (tokens[k].find("_") != string::npos) {
+						continue;
+					}
+					voiceInfo[track].diatonic.at(diatonic).at(0)++;
+					voiceInfo[track].diatonic.at(diatonic).at(acc)++;
+					voiceInfo[track].midibins.at(midi)++;
+				}
+			}
+		}
 	}
-	return status;
-}
 
-//
-// In-place processing of file:
-//
+	mergeFinals(voiceInfo, diafinal, accfinal);
 
-bool Tool_periodicity::run(HumdrumFile& infile) {
-	processFile(infile);
-	return true;
+	// Sum all voices into midibins and diatonic arrays of vector position 0:
+	mergeAllVoiceInfo(voiceInfo);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_periodicity::processFile --
+// Tool_prange::mergeFinals --
 //
 
-void Tool_periodicity::processFile(HumdrumFile& infile) {
-	HumNum minrhy = infile.tpq() * 4;
-	if (getBoolean("min")) {
-		m_free_text << minrhy << endl;
-		return;
-	}
-
-	vector<vector<double>> attackgrids;
-	attackgrids.resize(infile.getTrackCount()+1);
-	fillAttackGrids(infile, attackgrids, minrhy);
-	if (getBoolean("attacks")) {
-		printAttackGrid(m_free_text, infile, attackgrids, minrhy);
-		return;
-	}
-
-	int atrack = getInteger("track");
-	vector<vector<double>> analysis;
-	doPeriodicityAnalysis(analysis, attackgrids[atrack], minrhy);
-
-	if (getBoolean("raw")) {
-		printPeriodicityAnalysis(m_free_text, analysis);
-		return;
+void Tool_prange::mergeFinals(vector<_VoiceInfo>& voiceInfo, vector<vector<int>>& diafinal,
+		vector<vector<int>>& accfinal) {
+	for (int i=0; i<(int)voiceInfo.size(); i++) {
+		voiceInfo.at(i).diafinal = diafinal.at(i);
+		voiceInfo.at(i).accfinal = accfinal.at(i);
 	}
-
-	printSvgAnalysis(m_free_text, analysis, minrhy);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_periodicity::printPeriodicityAnalysis --
+// Tool_prange::printFilenameBase --
 //
 
-void Tool_periodicity::printPeriodicityAnalysis(ostream& out, vector<vector<double>>& analysis) {
-	for (int i=0; i<(int)analysis.size(); i++) {
-		for (int j=0; j<(int)analysis[i].size(); j++) {
-			out << analysis[i][j];
-			if (j < (int)analysis[i].size() - 1) {
-				out << "\t";
+void Tool_prange::printFilenameBase(ostream& out, const string& filename) {
+	HumRegex hre;
+	if (hre.search(filename, "([^/]+)\\.([^.]*)", "")) {
+		if (hre.getMatch(1).size() <= 8) {
+			printXmlEncodedText(out, hre.getMatch(1));
+		} else {
+			// problem with too long a name (MS-DOS will have problems).
+			// optimize to chop off everything after the dash in the
+			// name (for Josquin catalog numbers).
+			string shortname = hre.getMatch(1);
+			if (hre.search(shortname, "-.*")) {
+			   hre.replaceDestructive(shortname, "", "-.*");
+				printXmlEncodedText(out, shortname);
+			} else {
+				printXmlEncodedText(out, shortname);
 			}
 		}
-		out << "\n";
 	}
 }
 
@@ -114171,13 +118716,19 @@ void Tool_periodicity::printPeriodicityAnalysis(ostream& out, vector<vector<doub
 
 //////////////////////////////
 //
-// Tool_periodicity::doPeriodicAnalysis --
+// Tool_prange::printReferenceRecords --
 //
 
-void Tool_periodicity::doPeriodicityAnalysis(vector<vector<double>> &analysis, vector<double>& grid, HumNum minrhy) {
-	analysis.resize(minrhy.getNumerator());
-	for (int i=0; i<(int)analysis.size(); i++) {
-		doAnalysis(analysis, i, grid);
+void Tool_prange::printReferenceRecords(ostream& out, HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReferenceRecord()) {
+			continue;
+		}
+		out <<  "\t\t\t\t\t\t<?Humdrum key=\"";
+		printXmlEncodedText(out, infile[i].getReferenceKey());
+		out << "\" value=\"";
+		printXmlEncodedText(out, infile[i].getReferenceValue());
+		out << "\"?>\n";
 	}
 }
 
@@ -114185,1272 +118736,1381 @@ void Tool_periodicity::doPeriodicityAnalysis(vector<vector<double>> &analysis, v
 
 //////////////////////////////
 //
-// Tool_periodicity::doAnalysis --
+// Tool_prange::printScoreEncodedText -- print SCORE text string
+//    See SCORE 3.1 manual additions (page 19) for more.
 //
 
-void Tool_periodicity::doAnalysis(vector<vector<double>>& analysis, int level, vector<double>& grid) {
-	int period = level + 1;
-	analysis[level].resize(period);
-	std::fill(analysis[level].begin(), analysis[level].end(), 0.0);
-	for (int i=0; i<period; i++) {
-		int j = i;
-		while (j < (int)grid.size()) {
-			analysis[level][i] += grid[j];
-			j += period;
-		}
-	}
-}
+void Tool_prange::printScoreEncodedText(ostream& out, const string& strang) {
+	string newstring = strang;
+	HumRegex hre;
 
+	hre.replaceDestructive(newstring, "<<$1", "&([aeiou])acute;", "gi");
+	hre.replaceDestructive(newstring, "<<$1", "([áéíóú])", "gi");
 
+	hre.replaceDestructive(newstring, ">>$1", "&([aeiou])grave;", "gi");
+	hre.replaceDestructive(newstring, ">>$1", "([àèìòù])", "gi");
 
-//////////////////////////////
-//
-// Tool_periodicity::printAttackGrid --
-//
+	hre.replaceDestructive(newstring, "%%$1", "&([aeiou])uml;", "gi");
+	hre.replaceDestructive(newstring, "%%$1", "([äëïöü])", "gi");
 
-void Tool_periodicity::printAttackGrid(ostream& out, HumdrumFile& infile, vector<vector<double>>& grids, HumNum minrhy) {
-	out << "!!!minrhy: " << minrhy << endl;
-	out << "**all";
-	for (int i=1; i<(int)grids.size(); i++) {
-		out << "\t**track";
-	}
-	out << "\n";
-	for (int j=0; j<(int)grids[0].size(); j++) {
-		for (int i=0; i<(int)grids.size(); i++) {
-			out << grids[i][j];
-			if (i < (int)grids.size() - 1) {
-				out << "\t";
-			}
-		}
-		out << "\n";
-	}
-	for (int i=0; i<(int)grids.size(); i++) {
-		out << "*-";
-		if (i < (int)grids.size() - 1) {
-			out << "\t";
-		}
-	}
-	out << "\n";
+	hre.replaceDestructive(newstring, "^^$1", "&([aeiou])circ;", "gi");
+	hre.replaceDestructive(newstring, "^^$1", "([âêîôû])", "gi");
+
+	hre.replaceDestructive(newstring, "##c", "&ccedil;",  "g");
+	hre.replaceDestructive(newstring, "##C", "&Ccedil;",  "g");
+	hre.replaceDestructive(newstring, "?\\|", "\\|",      "g");
+	hre.replaceDestructive(newstring, "?\\",  "\\\\",     "g");
+	hre.replaceDestructive(newstring, "?m",   "---",      "g");
+	hre.replaceDestructive(newstring, "?n",   "--",       "g");
+	hre.replaceDestructive(newstring, "?2",   "-sharp",   "g");
+	hre.replaceDestructive(newstring, "?1",   "-flat",    "g");
+	hre.replaceDestructive(newstring, "?3",   "-natural", "g");
+	hre.replaceDestructive(newstring, "\\",   "/",        "g");
+	hre.replaceDestructive(newstring, "?[",   "\\[",      "g");
+	hre.replaceDestructive(newstring, "?]",   "\\]",      "g");
 
+	out << newstring;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_periodicity::fillAttackGrids --
+// Tool_prange::printXmlEncodedText -- convert
+//    & to &amp;
+//    " to &quot;
+//    ' to &spos;
+//    < to &lt;
+//    > to &gt;
 //
 
-void Tool_periodicity::fillAttackGrids(HumdrumFile& infile, vector<vector<double>>& grids, HumNum minrhy) {
-	HumNum elements = minrhy * infile.getScoreDuration() / 4;
-
-	for (int t=0; t<(int)grids.size(); t++) {
-		grids[t].resize(elements.getNumerator());
-	}
+void Tool_prange::printXmlEncodedText(ostream& out, const string& strang) {
+	HumRegex hre;
+	string astring = strang;
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		HumNum position = infile[i].getDurationFromStart() / 4 * minrhy;
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
-				continue;
-			}
-			if (!token->isNoteAttack()) {
-				continue;
-			}
-			int track = token->getTrack();
-			grids.at(track).at(position.getNumerator()) += 1;
-		}
-	}
+	hre.replaceDestructive(astring, "&",  "&amp;",  "g");
+	hre.replaceDestructive(astring, "'",  "&apos;", "g");
+	hre.replaceDestructive(astring, "\"", "&quot;", "g");
+	hre.replaceDestructive(astring, "<",  "&lt;",   "g");
+	hre.replaceDestructive(astring, ">",  "&gt;",   "g");
 
-	bool oneQ = getBoolean("one");
-	for (int j=0; j<(int)grids.at(0).size(); j++) {
-		grids.at(0).at(j) = 0;
-		for (int i=0; i<(int)grids.size(); i++) {
-			if (!grids.at(i).at(j)) {
-				continue;
-			}
-			if (oneQ) {
-				grids.at(0).at(j) = 1;
-			} else {
-				grids.at(0).at(j) += grids.at(i).at(j);
-			}
-		}
-	}
+	out << astring;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_periodicity::printSvgAnalysis --
+// Tool_prange::printScoreFile --
 //
 
-void Tool_periodicity::printSvgAnalysis(ostream& out, vector<vector<double>>& analysis, HumNum minrhy) {
-	pugi::xml_document image;
-	auto declaration = image.prepend_child(pugi::node_declaration);
-	declaration.append_attribute("version") = "1.0";
-	declaration.append_attribute("encoding") = "UTF-8";
-	declaration.append_attribute("standalone") = "no";
-
-	auto svgnode = image.append_child("svg");
-	svgnode.append_attribute("version") = "1.1";
-	svgnode.append_attribute("xmlns") = "http://www.w3.org/2000/svg";
-	svgnode.append_attribute("xmlns:xlink") = "http://www.w3.org/1999/xlink";
-	svgnode.append_attribute("overflow") = "visible";
-	svgnode.append_attribute("viewBox") = "0 0 1000 1000";
-	svgnode.append_attribute("width") = "1000px";
-	svgnode.append_attribute("height") = "1000px";
-
-	auto style = svgnode.append_child("style");
-	style.text().set(".label { font: 14px sans-serif; alignment-baseline: middle; text-anchor: left; }");
+void Tool_prange::printScoreFile(ostream& out, vector<_VoiceInfo>& voiceInfo, HumdrumFile& infile) {
+	string titlestring = getTitle();
 
-	auto grid = svgnode.append_child("g");
-	grid.append_attribute("id") = "grid";
+	if (m_defineQ) {
+		out << "#define SVG t 1 1 \\n_99%svg%\n";
+	}
 
-	auto labels = svgnode.append_child("g");
+	string acctext = "g.bar.doubleflat path&#123;color:darkorange;stroke:darkorange;&#125;g.bar.flat path&#123;color:brown;stroke:brown;&#125;g.bar.sharp path&#123;color:royalblue;stroke:royalblue;&#125;g.bar.doublesharp path&#123;color:aquamarine;stroke:aquamarine;&#125;";
+	string hovertext = ".bar:hover path&#123;fill:red;color:red;stroke:red &#33;important&#125;";
+	string hoverfilltext = hovertext;
 
-	double hue = 0.0;
-	double saturation = 100;
-	double lightness = 75;
+	string text1 = "<style>";
+	text1 += hoverfilltext;
+	if (m_accQ) {
+		text1 += acctext;
+	}
+	text1 += "g.labeltext&#123;color:gray;&#125;";
+	text1 += "g.lastnote&#123;color:gray;&#125;";
+	if (m_extremaQ) {
+		text1 += "g.highest-pitch&#123;color:hotpink;&#125;";
+		text1 += "g.lowest-pitch&#123;color:limegreen;&#125;";
+	}
+	text1 += "</style>";
+	string text2 = text1;
 
-	pugi::xml_node crect;
-	double width;
-	double height;
 
-	stringstream ss;
-	stringstream ssl;
-	//stringstream css;
-	double x;
-	double y;
+	// print CSS style information if requested
+	if (m_hoverQ) {
+		SVGTEXT(out, text1);
+	}
 
-	double imagewidth = 1000.0;
-	double imageheight = 1000.0;
+	int maxStaffPosition = getMaxStaffPosition(voiceInfo);
 
-	int maxrow = getInteger("max-rows");
-	if (maxrow <= 0) {
-		maxrow = (int)analysis.back().size();
+	if (!titlestring.empty()) {
+		// print title
+		int vpos = 54;
+		if (maxStaffPosition > 12) {
+			vpos = maxStaffPosition + 3;
+		}
+		out << "t 2 10 ";
+		out << vpos;
+		out << " 1 1 0 0 0 0 -1.35\n";
+		// out << "_03";
+		printScoreEncodedText(out, titlestring);
+		out << "\n";
 	}
 
+	// print duration label if duration weighting is being used
+	SVGTEXT(out, "<g class=\"labeltext\">");
+	if (m_durationQ) {
+		out << "t 2 185.075 14 1 0.738 0 0 0 0 0\n";
+		out << "_00(durations)\n";
+	} else {
+		out << "t 2 185.075 14 1 0.738 0 0 0 0 0\n";
+		out << "_00(attacks)\n";
+	}
+	SVGTEXT(out, "</g>");
 
-	// double sdur = (double)analysis.back().size();
-	double sdur = (double)maxrow;
+	// print staff lines
+	out << "8 1 0 0 0 200\n";   // staff 1
+	out << "8 2 0 -6 0 200\n";   // staff 2
 
-	double maxscore = 0.0;
-	for (int i=0; i<maxrow; i++) {
-		for (int j=0; j<(int)analysis[i].size(); j++) {
-			if (maxscore < analysis[i][j]) {
-				maxscore = analysis[i][j];
-			}
-		}
+	int keysig = getKeySignature(infile);
+	// print key signature
+	if (keysig) {
+		out << "17 1 10 0 " << keysig << " 101.0";
+		printKeySigCompression(out, keysig, 0);
+		out << endl;
+		out << "17 2 10 0 " << keysig;
+		printKeySigCompression(out, keysig, 1);
+		out << endl;
 	}
 
-	double power = getDouble("power");
-	for (int i=0; i<maxrow; i++) {
-		for (int j=0; j<(int)analysis[i].size(); j++) {
-			width = 1 / sdur * imagewidth;
-			height = 1 / sdur * imageheight;
+	// print barlines
+	out << "14 1 0 2\n";         // starting barline
+	out << "14 1 200 2\n";       // ending barline
+	out << "14 1 0 2 8\n";       // curly brace at start
 
-			x = j/sdur * imageheight;
-			y = i/sdur * imagewidth;
+	// print clefs
+	out << "3 2 2\n";            // treble clef
+	out << "3 1 2 0 1\n";        // bass clef
 
-			double value = analysis[i][j]/maxscore;
-			value = pow(value, 1.0/power);
+	assignHorizontalPosition(voiceInfo, 25.0, 170.0);
 
-			getColorMapping(value, hue, saturation, lightness);
-			ss << "hsl(" << hue << "," << saturation << "%," << lightness << "%)";
-			crect = grid.append_child("rect");
-			crect.append_attribute("x") = to_string(x).c_str();
-			crect.append_attribute("y") = to_string(y).c_str();
-			crect.append_attribute("width") = to_string(width*0.99).c_str();
-			crect.append_attribute("height") = to_string(height*0.99).c_str();
-			crect.append_attribute("fill") = ss.str().c_str();
-			//css << "Xm" << getMeasure1(i) << " Ym" << getMeasure2(j);
-			//css << " X" << getQon1(i)     << " Y" << getQon2(j);
-			//css << " X" << getQoff1(i)    << " Y" << getQoff2(j);
-			//crect.append_attribute("class") = css.str().c_str();
-			ss.str("");
-			//css.str("");
+	double maxvalue = 0.0;
+	for (int i=1; i<(int)voiceInfo.size(); i++) {
+		double tempvalue = getMaxValue(voiceInfo.at(i).diatonic);
+		if (tempvalue > maxvalue) {
+			maxvalue = tempvalue;
 		}
+	}
+	for (int i=(int)voiceInfo.size()-1; i>0; i--) {
+		if (voiceInfo.at(i).kernQ) {
+			printScoreVoice(out, voiceInfo.at(i), maxvalue);
+		}
+	}
+	if (m_allQ) {
+		printScoreVoice(out, voiceInfo.at(0), maxvalue);
+	}
+}
 
-		pugi::xml_node label = labels.append_child("text");
-		label.append_attribute("class") = "label";
-
-		HumNum rval = (i+1);
-		rval /= minrhy;
-		rval *= 4;
 
-		std::string rhythm = Convert::durationToRecip(rval);
-		rhythm += " (" + to_string(i+1) + ")";
-		label.text().set(rhythm.c_str());
-		x = (i+1+0.5)/sdur * imageheight;
-		y = (i+0.5)/sdur * imagewidth;
-		label.append_attribute("x") = to_string(x).c_str();
-		label.append_attribute("y") = to_string(y).c_str();
-	}
+//////////////////////////////
+//
+// Tool_prange::getMaxStaffPosition(vector<_VoiceInfo>& voiceinfo) {
+//
 
-	image.save(out);
+int Tool_prange::getMaxStaffPosition(vector<_VoiceInfo>& voiceInfo) {
+	int maxi = getMaxDiatonicIndex(voiceInfo[0].diatonic);
+	int maxdiatonic = maxi - 3 * 7;
+	int staffline = maxdiatonic - 27;
+	return staffline;
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_periodicity::getColorMapping --
+// Tool_prange::printKeySigCompression --
 //
 
-void Tool_periodicity::getColorMapping(double input, double& hue,
-		double& saturation, double& lightness) {
-	double maxhue = 0.75 * 360.0;
-	hue = input;
-	if (hue < 0.0) {
-		hue = 0.0;
-	}
-	hue = hue * hue;
-	if (hue != 1.0) {
-		hue *= 0.95;
-	}
-
-	hue = (1.0 - hue) * 360.0;
-	if (hue == 0.0) {
-		// avoid -0.0;
-		hue = 0.0;
-	}
-
-	if (hue > maxhue) {
-		hue = maxhue;
+void Tool_prange::printKeySigCompression(ostream& out, int keysig, int extra) {
+	double compression = 0.0;
+	switch (abs(keysig)) {
+		case 0: compression = 0.0; break;
+		case 1: compression = 0.0; break;
+		case 2: compression = 0.0; break;
+		case 3: compression = 0.0; break;
+		case 4: compression = 0.9; break;
+		case 5: compression = 0.8; break;
+		case 6: compression = 0.7; break;
+		case 7: compression = 0.6; break;
 	}
-	if (hue < 0.0) {
-		hue = maxhue;
+	if (compression <= 0.0) {
+		return;
 	}
-
-	saturation = 100.0;
-	lightness = 50.0;
-
-	if (hue > 60) {
-		lightness = lightness - (hue-60) / (maxhue-60) * lightness / 1.5;
+	for (int i=0; i<extra; i++) {
+		out << " 0";
 	}
+	out << " " << compression;
 }
 
 
 
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_gridtest::Tool_phrase -- Set the recognized options for the tool.
+// Tool_prange::assignHorizontalPosition --
 //
 
-Tool_phrase::Tool_phrase(void) {
-	define("A|no-average=b", "do not do average phrase-length analysis");
-	define("R|remove2=b",    "remove phrase boundaries in data and do not do analysis");
-	define("m|mark=b",       "mark phrase boundaries based on rests");
-	define("r|remove=b",     "remove phrase boundaries in data");
-	define("c|color=s",      "display color of analysis data");
-}
-
+void Tool_prange::assignHorizontalPosition(vector<_VoiceInfo>& voiceInfo, int minval, int maxval) {
+	int count = 0;
+	for (int i=1; i<(int)voiceInfo.size(); i++) {
+		if (voiceInfo[i].kernQ) {
+			count++;
+		}
+	}
+	if (m_allQ) {
+		count++;
+	}
 
+	vector<double> hpos(count, 0);
+	hpos[0] = maxval;
+	hpos.back() = minval;
 
-///////////////////////////////
-//
-// Tool_phrase::run -- Primary interfaces to the tool.
-//
+	if (hpos.size() > 2) {
+		for (int i=1; i<(int)hpos.size()-1; i++) {
+			int ii = hpos.size() - i - 1;
+			hpos[i] = (double)ii / (hpos.size()-1) * (maxval - minval) + minval;
+		}
+	}
 
-bool Tool_phrase::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	int position = 0;
+	if (m_allQ) {
+		position = 1;
+		voiceInfo[0].hpos = hpos[0];
+	}
+	for (int i=0; i<(int)voiceInfo.size(); i++) {
+		if (voiceInfo.at(i).kernQ) {
+			voiceInfo.at(i).hpos = hpos.at(position++);
+		}
 	}
-	return status;
-}
-
-
-bool Tool_phrase::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	return run(infile, out);
 }
 
 
-bool Tool_phrase::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	return status;
-}
 
+//////////////////////////////
+//
+// Tool_prange::getKeySignature -- find first key signature in file.
+//
 
-bool Tool_phrase::run(HumdrumFile& infile) {
-	initialize(infile);
-	for (int i=0; i<(int)m_starts.size(); i++) {
-		if (m_removeQ) {
-			removePhraseMarks(m_starts[i]);
-		}
-		if (m_remove2Q) {
+int Tool_prange::getKeySignature(HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			if (infile[i].isData()) {
+				break;
+			}
 			continue;
 		}
-		if (hasPhraseMarks(m_starts[i])) {
-			analyzeSpineByPhrase(i);
-		} else {
-			analyzeSpineByRests(i);
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isKeySignature()) {
+				return Convert::kernKeyToNumber(*token);
+			}
 		}
 	}
-	if (!m_remove2Q) {
-		prepareAnalysis(infile);
-	}
-	infile.createLinesFromTokens();
-	return true;
+
+	return 0; // C major key signature
 }
 
 
 
 //////////////////////////////
 //
-// Tool_phrase::prepareAnalysis --
+// Tool_prange::printScoreVoice -- print the range information for a particular voice (in SCORE format).
 //
 
-void Tool_phrase::prepareAnalysis(HumdrumFile& infile) {
-	string exinterp = "**cdata";
-	infile.appendDataSpine(m_results.back(), "", exinterp);
-	for (int i = (int)m_results.size()-1; i>0; i--) {
-		int track = m_starts[i]->getTrack();
-		infile.insertDataSpineBefore(track, m_results[i-1], "", exinterp);
+void Tool_prange::printScoreVoice(ostream& out, _VoiceInfo& voiceInfo, double maxvalue) {
+	int mini = getMinDiatonicIndex(voiceInfo.diatonic);
+	int maxi = getMaxDiatonicIndex(voiceInfo.diatonic);
+
+	if ((mini < 0) || (maxi < 0)) {
+		// no data for voice so skip
+		return;
 	}
-	if (m_averageQ) {
-		addAverageLines(infile);
+
+	// int minacci = getMinDiatonicAcc(voiceInfo.diatonic, mini);
+	// int maxacci = getMaxDiatonicAcc(voiceInfo.diatonic, maxi);
+	int mindiatonic = mini - 3 * 7;
+	int maxdiatonic = maxi - 3 * 7;
+	// int minacc = minacci - 3;
+	// int maxacc = maxacci - 3;
+
+	int    staff;
+	double vpos;
+
+	int voicevpos = -3;
+	staff = getStaffBase7(mindiatonic);
+	int lowestvpos = getVpos(mindiatonic);
+	if ((staff == 1) && (lowestvpos <= 0)) {
+		voicevpos += lowestvpos - 2;
 	}
-	if (!m_color.empty()) {
-		int insertline = -1;
-		for (int i=0; i<infile.getLineCount(); i++) {
-			if (infile[i].isData() || infile[i].isBarline()) {
-				insertline = i;
-				break;
-			}
+
+	if (m_localQ || (voiceInfo.index == 0)) {
+		double localmaxvalue = getMaxValue(voiceInfo.diatonic);
+		maxvalue = localmaxvalue;
+	}
+	double width;
+	double hoffset = 2.3333;
+	double maxhist = 17.6;
+	int i;
+	int base7;
+
+	// print histogram bars
+	for (i=mini; i<=maxi; i++) {
+		if (voiceInfo.diatonic.at(i).at(0) <= 0.0) {
+			continue;
 		}
-		if (insertline > 0) {
-			stringstream ss;
-			int fsize = infile[insertline].getFieldCount();
-			for (int j=0; j<fsize; j++) {
-				ss << "*";
-				HTp token = infile.token(insertline, j);
-				string dt = token->getDataType();
-				if (dt.empty() || (dt == "**cdata")) {
-					ss << "color:" << m_color;
-				}
-				if (j < fsize  - 1) {
-					ss << "\t";
-				}
+		base7 = i - 3 * 7;
+		staff = getStaffBase7(base7);
+		vpos  = getVpos(base7);
+
+		// staring positions of accidentals:
+		vector<double> starthpos(6, 0.0);
+		for (int j=1; j<(int)starthpos.size(); j++) {
+			double width = maxhist * voiceInfo.diatonic.at(i).at(j)/maxvalue;
+			starthpos[j] = starthpos[j-1] + width;
+		}
+		for (int j=(int)starthpos.size() - 1; j>0; j--) {
+			starthpos[j] = starthpos[j-1];
+		}
+
+		// print chromatic alterations
+		for (int j=(int)voiceInfo.diatonic.at(i).size()-1; j>0; j--) {
+			if (voiceInfo.diatonic.at(i).at(j) <= 0.0) {
+				continue;
+			}
+			int acc = 0;
+			switch (j) {
+				case 1: acc = -2; break;
+				case 2: acc = -1; break;
+				case 3: acc =  0; break;
+				case 4: acc = +1; break;
+				case 5: acc = +2; break;
+			}
+
+			width = maxhist * voiceInfo.diatonic.at(i).at(j)/maxvalue + hoffset;
+			if (m_hoverQ) {
+				string title = getNoteTitle((int)voiceInfo.diatonic.at(i).at(j), base7, acc);
+				SVGTEXT(out, title);
+			}
+			out << "1 " << staff << " " << (voiceInfo.hpos + starthpos.at(j) + hoffset) << " " << vpos;
+			out << " 0 -1 4 0 0 0 99 0 0 ";
+			out << width << "\n";
+			if (m_hoverQ) {
+				SVGTEXT(out, "</g>");
 			}
-			string output = ss.str();
-			infile.insertLine(insertline, output);
 		}
 	}
-}
 
+	string voicestring = voiceInfo.name;
+	if (voicestring.empty()) {
+		voicestring = voiceInfo.abbr;
+	}
+	if (!voicestring.empty()) {
+		HumRegex hre;
+		hre.replaceDestructive(voicestring, "", "(\\\\n)+$");
+		vector<string> pieces;
+		hre.split(pieces, voicestring, "\\\\n");
 
+		if (pieces.size() > 1) {
+			voicestring = "";
+			for (int i=0; i<(int)pieces.size(); i++) {
+				voicestring += pieces[i];
+				if (i < (int)pieces.size() - 1) {
+					voicestring += "/";
+				}
+			}
+		}
 
-///////////////////////////////
-//
-// Tool_pharse::addAverageLines --
-//
+		double increment = 4.0;
+		for (int i=0; i<(int)pieces.size(); i++) {
+			// print voice name
+			double tvoffset = -4.0;
+			out << "t 1 " << voiceInfo.hpos << " "
+				<< (voicevpos - increment * i)
+			  	<< " 1 1 0 0 0 0 " << tvoffset;
+			out << "\n";
 
-void Tool_phrase::addAverageLines(HumdrumFile& infile) {
-	vector<string> averages;
-	averages.resize(m_starts.size()+1);
-	int tcount = 0;
-	HumNum tsum = 0;
-	double average;
-	stringstream ss;
-	for (int i=0; i<(int)m_starts.size(); i++) {
-		if (m_pcount[i] > 0) {
-			average = m_psum[i].getFloat() / m_pcount[i];
-		} else {
-			average = 0.0;
+			if (pieces[i] == "all") {
+				out << "_02";
+			} else if (pieces[i] == "both") {
+				out << "_02";
+			} else {
+				out << "_00";
+			}
+			printScoreEncodedText(out, pieces[i]);
+			out << "\n";
 		}
-		ss.str("");
-		ss.clear();
-		ss << "!!average-phrase-length-k" << i+1 << ":\t" << average;
-		averages[i+1] = ss.str();
-		tcount += m_pcount[i];
-		tsum += m_psum[i];
 	}
-	average = tsum.getFloat() / tcount;
-	ss.str("");
-	ss.clear();
-	ss << "!!average-phrase-length:\t" << average;
-	averages[0] = ss.str();
 
-	for (int i=0; i<(int)averages.size(); i++) {
-		infile.appendLine(averages[i]);
+	// print the lowest pitch in range
+	staff = getStaffBase7(mindiatonic);
+	vpos = getVpos(mindiatonic);
+	if (m_hoverQ) {
+		string content = "<g class=\"lowest-pitch\"><title>";
+		content += getDiatonicPitchName(mindiatonic, 0);
+		content += ": lowest note";
+		if (!voicestring.empty()) {
+			content += " of ";
+			content += voicestring;
+			content += "'s range";
+		}
+		content += "</title>";
+		SVGTEXT(out, content);
+	}
+	out << "1 " << staff << " " << voiceInfo.hpos << " " << vpos
+		  << " 0 0 4 0 0 -2\n";
+	if (m_hoverQ) {
+		SVGTEXT(out, "</g>");
+	}
+
+	// print the highest pitch in range
+	staff = getStaffBase7(maxdiatonic);
+	vpos = getVpos(maxdiatonic);
+	if (m_hoverQ) {
+		string content = "<g class=\"highest-pitch\"><title>";
+		content += getDiatonicPitchName(maxdiatonic, 0);
+		content += ": highest note";
+		if (!voicestring.empty()) {
+			content += " of ";
+			content += voicestring;
+			content += "'s range";
+		}
+		content += "</title>";
+		SVGTEXT(out, content);
+	}
+	out << "1 " << staff << " " << voiceInfo.hpos << " " << vpos
+		  << " 0 0 4 0 0 -2\n";
+	if (m_hoverQ) {
+		SVGTEXT(out, "</g>");
 	}
-}
 
+	double goffset  = -1.66;
+	double toffset  = 1.5;
+	double median12 = getMedian12(voiceInfo.midibins);
+	double median40 = Convert::base12ToBase40(median12);
+	double median7  = Convert::base40ToDiatonic(median40);
+	// int    acc      = Convert::base40ToAccidental(median40);
 
+	staff = getStaffBase7(median7);
+	vpos = getVpos(median7);
 
-///////////////////////////////
-//
-// Tool_phrase::initialize --
-//
+	// these offsets are useful when the quartile pitches are not shown...
+	int vvpos = maxdiatonic - median7 + 1;
+	int vvpos2 = median7 - mindiatonic + 1;
+	double offset = goffset;
+	if (vvpos <= 2) {
+		offset += toffset;
+	} else if (vvpos2 <= 2) {
+		offset -= toffset;
+	}
 
-void Tool_phrase::initialize(HumdrumFile& infile) {
-	m_starts = infile.getKernSpineStartList();
-	m_results.resize(m_starts.size());
-	int lines = infile.getLineCount();
-	for (int i=0; i<(int)m_results.size(); i++) {
-		m_results[i].resize(lines);
+	if (m_hoverQ) {
+		string content = "<g><title>";
+		content += getDiatonicPitchName(median7, 0);
+		content += ": median note";
+		if (!voicestring.empty()) {
+			content += " of ";
+			content += voicestring;
+			content += "'s range";
+		}
+		content += "</title>";
+		SVGTEXT(out, content);
 	}
-	m_pcount.resize(m_starts.size());
-	m_psum.resize(m_starts.size());
-	std::fill(m_pcount.begin(), m_pcount.end(), 0);
-	std::fill(m_psum.begin(), m_psum.end(), 0);
-	m_markQ = getBoolean("mark");
-	m_removeQ = getBoolean("remove");
-	m_averageQ = !getBoolean("no-average");
-	m_remove2Q = getBoolean("remove2");
-	if (getBoolean("color")) {
-		m_color = getString("color");
+	out << "1 " << staff << " " << voiceInfo.hpos << " ";
+	if (vpos > 0) {
+		out << vpos + 100;
+	} else {
+		out << vpos - 100;
+	}
+	out << " 0 1 4 0 0 " << offset << "\n";
+	if (m_hoverQ) {
+		SVGTEXT(out, "</g>");
 	}
-}
-
 
+	if (m_finalisQ) {
+		for (int f=0; f<(int)voiceInfo.diafinal.size(); f++) {
+			int diafinalis = voiceInfo.diafinal.at(f);
+			int accfinalis = voiceInfo.accfinal.at(f);
+			int staff = getStaffBase7(diafinalis);
+			int vpos = getVpos(diafinalis);
+			double goffset = -1.66;
+			double toffset = 3.5;
 
-///////////////////////////////
-//
-// Tool_phrase::analyzeSpineByRests --
-//
+			// these offsets are useful when the quartile pitches are not shown...
+			double offset = goffset;
+			offset += toffset;
 
-void Tool_phrase::analyzeSpineByRests(int index) {
-	HTp start    = m_starts[index];
-	HTp current  = start;
-	HTp lastnote = NULL;   // last note to be processed
-	HTp pstart   = NULL;   // phrase start;
-	HumNum dur;
-	stringstream ss;
-	while (current) {
-		if (current->isBarline()) {
-			if (current->find("||") != std::string::npos) {
-				if (pstart) {
-					dur = current->getDurationFromStart()
-							- pstart->getDurationFromStart();
-					ss.str("");
-					ss.clear();
-					ss << dur.getFloat();
-					m_psum[index] += dur;
-					m_pcount[index]++;
-					m_results[index][pstart->getLineIndex()] = ss.str();
-					pstart = NULL;
-					if (m_markQ && lastnote) {
-						lastnote->setText(lastnote->getText() + "}");
+			if (m_hoverQ) {
+				string content = "<g class=\"lastnote\"><title>";
+				content += getDiatonicPitchName(diafinalis, accfinalis);
+				content += ": last note";
+				if (!voicestring.empty()) {
+					content += " of ";
+					if (voiceInfo.index == 0) {
+						content += voiceInfo.namfinal.at(f);
+					} else {
+						content += voicestring;
 					}
 				}
+				content += "</title>";
+				SVGTEXT(out, content);
+			}
+			out << "1 " << staff << " " << voiceInfo.hpos << " ";
+			if (vpos > 0) {
+				out << vpos + 100;
+			} else {
+				out << vpos - 100;
+			}
+			out << " 0 0 4 0 0 " << offset << "\n";
+			if (m_hoverQ) {
+				SVGTEXT(out, "</g>");
 			}
 		}
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
+	}
+
+	/* Needs fixing
+	int topquartile;
+	if (m_quartileQ) {
+		// print top quartile
+		topquartile = getTopQuartile(voiceInfo.midibins);
+		if (m_diatonicQ) {
+			topquartile = Convert::base7ToBase12(topquartile);
 		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
+		staff = getStaffBase7(topquartile);
+		vpos = getVpos(topquartile);
+		vvpos = median7 - topquartile + 1;
+		if (vvpos <= 2) {
+			offset = goffset + toffset;
+		} else {
+			offset = goffset;
 		}
-		if (pstart && current->isRest()) {
-			if (lastnote) {
-				dur = current->getDurationFromStart()
-						- pstart->getDurationFromStart();
-				ss.str("");
-				ss.clear();
-				ss << dur.getFloat();
-				m_psum[index] += dur;
-				m_pcount[index]++;
-				m_results[index][pstart->getLineIndex()] = ss.str();
-				if (m_markQ) {
-					lastnote->setText(lastnote->getText() + "}");
-				}
+		vvpos = maxdiatonic - topquartile + 1;
+		if (vvpos <= 2) {
+			offset = goffset + toffset;
+		}
+
+		if (m_hoverQ) {
+			if (m_defineQ) {
+				out << "SVG ";
+			} else {
+				out << "t 1 1\n";
+				out << SVGTAG;
 			}
-			pstart = NULL;
-			lastnote = NULL;
-			current = current->getNextToken();
-			continue;
+			printScoreEncodedText(out, "<g><title>");
+			printDiatonicPitchName(out, topquartile, 0);
+			out << ": top quartile note";
+			if (voicestring.size() > 0) {
+				out <<  " of " << voicestring << "\'s range";
+			}
+			printScoreEncodedText(out, "</title>\n");
 		}
-		if (current->isRest()) {
-			current = current->getNextToken();
-			continue;
+		out << "1 " << staff << " " << voiceInfo.hpos << " ";
+		if (vpos > 0) {
+			out << vpos + 100;
+		} else {
+			out << vpos - 100;
 		}
-		if (current->isNote()) {
-			lastnote = current;
+		out << " 0 0 4 0 0 " << offset << "\n";
+		if (m_hoverQ) {
+			SVGTEXT(out, "</g>");
 		}
-		if (pstart && current->isNote() && (current->find(";") != std::string::npos)) {
-			// fermata at end of phrase.
-			dur = current->getDurationFromStart() + current->getDuration()
-					- pstart->getDurationFromStart();
-			ss.str("");
-			ss.clear();
-			ss << dur.getFloat();
-			m_psum[index] += dur;
-			m_pcount[index]++;
-			m_results[index][pstart->getLineIndex()] = ss.str();
-			if (m_markQ) {
-				current->setText(current->getText() + "}");
-			}
-			current = current->getNextToken();
-			pstart = NULL;
-			continue;
+	}
+
+	// print bottom quartile
+	if (m_quartileQ) {
+		int bottomquartile = getBottomQuartile(voiceInfo.midibins);
+		if (m_diatonicQ) {
+			bottomquartile = Convert::base7ToBase12(bottomquartile);
 		}
-		if (current->isNote() && pstart == NULL) {
-			pstart = current;
-			if (m_markQ) {
-				current->setText("{" + current->getText());
-			}
+		staff = getStaffBase7(bottomquartile);
+		vpos = getVpos(bottomquartile);
+		vvpos = median7 - bottomquartile + 1;
+		if (vvpos <= 2) {
+			offset = goffset + toffset;
+		} else {
+			offset = goffset;
 		}
-		current = current->getNextToken();
-	}
-	if (pstart) {
-		dur = start->getOwner()->getOwner()->getScoreDuration()
-				- pstart->getDurationFromStart();
-		ss.str("");
-		ss.clear();
-		ss << dur.getFloat();
-		m_psum[index] += dur;
-		m_pcount[index]++;
-		m_results[index][pstart->getLineIndex()] = ss.str();
-		if (m_markQ && lastnote) {
-			lastnote->setText(lastnote->getText() + "}");
+		vvpos = bottomquartile - mindiatonic + 1;
+		if (vvpos <= 2) {
+			offset = goffset - toffset;
+		}
+		if (m_hoverQ) {
+			if (m_defineQ) {
+				out << "SVG ";
+			} else {
+				out << "t 1 1\n";
+				out << SVGTAG;
+			}
+			printScoreEncodedText(out, "<g><title>");
+			printDiatonicPitchName(out, bottomquartile, 0);
+			out << ": bottom quartile note";
+			if (voicestring.size() > 0) {
+				out <<  " of " << voicestring << "\'s range";
+			}
+			printScoreEncodedText(out, "</title>\n");
+		}
+		out << "1.0 " << staff << ".0 " << voiceInfo.hpos << " ";
+		if (vpos > 0) {
+			out << vpos + 100;
+		} else {
+			out << vpos - 100;
+		}
+		out << " 0 0 4 0 0 " << offset << "\n";
+		if (m_hoverQ) {
+			SVGTEXT(out, "</g>");
 		}
 	}
+	*/
+
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_phrase::analyzeSpineByPhrase --
+// Tool_prange::printDiatonicPitchName --
 //
 
-void Tool_phrase::analyzeSpineByPhrase(int index) {
-	HTp start    = m_starts[index];
-	HTp current  = start;
-	HTp pstart   = NULL;   // phrase start;
-	HumNum dur;
-	stringstream ss;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->find("{") != std::string::npos) {
-			pstart = current;
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->find("}") != std::string::npos) {
-			if (pstart) {
-				dur = current->getDurationFromStart() + current->getDuration()
-						- pstart->getDurationFromStart();
-				ss.str("");
-				ss.clear();
-				ss << dur.getFloat();
-				m_psum[index] += dur;
-				m_pcount[index]++;
-				m_results[index][pstart->getLineIndex()] = ss.str();
-			}
-			current = current->getNextToken();
-			continue;
-		}
-		current = current->getNextToken();
+void Tool_prange::printDiatonicPitchName(ostream& out, int base7, int acc) {
+	out << getDiatonicPitchName(base7, acc);
+}
+
+
+
+//////////////////////////////
+//
+// Tool_prange::getDiatonicPitchName --
+//
+
+string Tool_prange::getDiatonicPitchName(int base7, int acc) {
+	string output;
+	int dpc = base7 % 7;
+	char letter = (dpc + 2) % 7 + 'A';
+	output += letter;
+	switch (acc) {
+		case -1: output += "&#9837;"; break;
+		case +1: output += "&#9839;"; break;
+		case -2: output += "&#119083;"; break;
+		case +2: output += "&#119082;"; break;
 	}
+	int octave = base7 / 7;
+	output += to_string(octave);
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_phrase::removePhraseMarks -- Remvoe { and } characters from **kern data.
+// Tool_prange::printHtmlStringEncodeSimple --
 //
 
-void Tool_phrase::removePhraseMarks(HTp start) {
-	HTp current = start;
+void Tool_prange::printHtmlStringEncodeSimple(ostream& out, const string& strang) {
+	string newstring = strang;
 	HumRegex hre;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->find("{") != std::string::npos) {
-			string data = *current;
-			hre.replaceDestructive(data, "", "\\{", "g");
-			current->setText(data);
-		}
-		if (current->find("}") != std::string::npos) {
-			string data = *current;
-			hre.replaceDestructive(data, "", "\\}", "g");
-			current->setText(data);
-		}
-		current = current->getNextToken();
-	}
+	hre.replaceDestructive(newstring, "&", "&amp;", "g");
+	hre.replaceDestructive(newstring, "<", "&lt;", "g");
+	hre.replaceDestructive(newstring, ">", "&lt;", "g");
+	out << newstring;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_phrase::hasPhraseMarks -- True if **kern data spine (primary layer), has
-//   "{" (or "}", but this is not checked) characters (phrase markers).
+// Tool_prange::getNoteTitle -- return the title of the histogram bar.
+//    value = duration or count of notes
+//    diatonic = base7 value for note
+//    acc = accidental for diatonic note.
 //
 
-bool Tool_phrase::hasPhraseMarks(HTp start) {
-	HTp current = start;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
+string Tool_prange::getNoteTitle(double value, int diatonic, int acc) {
+	stringstream output;
+	output << "<g class=\"bar";
+	switch (acc) {
+		case -2: output << " doubleflat";  break;
+		case -1: output << " flat";        break;
+		case  0: output << " natural";     break;
+		case +1: output << " sharp";       break;
+		case +2: output << " doublesharp"; break;
+	}
+	output << "\"";
+	output << "><title>";
+	if (m_durationQ) {
+		output << value / 8.0;
+		if (value/8.0 == 1.0) {
+			output << " long on ";
+		} else {
+			output << " longs on ";
 		}
-		if (current->find("{") != std::string::npos) {
-			return true;
+		output << getDiatonicPitchName(diatonic, acc);
+	} else {
+		output << value;
+		output << " ";
+		output << getDiatonicPitchName(diatonic, acc);
+		if (value != 1.0) {
+			output << "s";
 		}
-		current = current->getNextToken();
 	}
-	return false;
+	output << "</title>";
+	return output.str();
 }
 
 
 
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_pline::Tool_pline -- Set the recognized options for the tool.
+// Tool_prange::getDiatonicInterval --
 //
 
-Tool_pline::Tool_pline(void) {
-	define("c|color=b",   "color poetic lines (currently only by notes)");
-	define("o|overlap=b", "do overlap analysis/markup");
+int Tool_prange::getDiatonicInterval(int note1, int note2) {
+	int vpos1 = getVpos(note1);
+	int vpos2 = getVpos(note2);
+	return abs(vpos1 - vpos2) + 1;
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_pline::run -- Do the main work of the tool.
+// Tool_prange::getTopQuartile --
 //
 
-bool Tool_pline::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+int Tool_prange::getTopQuartile(vector<double>& midibins) {
+	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
+
+	double cumsum = 0.0;
+	int i;
+	for (i=midibins.size()-1; i>=0; i--) {
+		if (midibins[i] <= 0.0) {
+			continue;
+		}
+		cumsum += midibins[i]/sum;
+		if (cumsum >= 0.25) {
+			return i;
+		}
 	}
-	return status;
+
+	return -1;
 }
 
 
-bool Tool_pline::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
 
+//////////////////////////////
+//
+// Tool_prange::getBottomQuartile --
+//
 
-bool Tool_pline::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
+int Tool_prange::getBottomQuartile(vector<double>& midibins) {
+	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
 
+	double cumsum = 0.0;
+	int i;
+	for (i=0; i<(int)midibins.size(); i++) {
+		if (midibins[i] <= 0.0) {
+			continue;
+		}
+		cumsum += midibins[i]/sum;
+		if (cumsum >= 0.25) {
+			return i;
+		}
+	}
 
-bool Tool_pline::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
+	return -1;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_pline::initialize --
+// Tool_prange::getMaxValue --
 //
 
-void Tool_pline::initialize(void) {
-	m_colors.resize(14);
+double Tool_prange::getMaxValue(vector<vector<double>>& bins) {
+	double maxi = 0;
+	for (int i=1; i<(int)bins.size(); i++) {
+		if (bins.at(i).at(0) > bins.at(maxi).at(0)) {
+			maxi = i;
+		}
+	}
+	return bins.at(maxi).at(0);
+}
 
-	m_colors[0] = "red";        // red
-	m_colors[1] = "darkorange"; // orange
-	m_colors[2] = "gold";       // yellow
-	m_colors[3] = "limegreen";  // green
-	m_colors[4] = "skyblue";    // light blue
-	m_colors[5] = "mediumblue"; // dark blue
-	m_colors[6] = "purple";     // purple
 
-	m_colors[7]  = "darkred";        // red
-	m_colors[8]  = "lightsalmon";    // orange
-	m_colors[9]  = "darkgoldenrod";  // yellow
-	m_colors[10] = "olivedrab";      // green
-	m_colors[11] = "darkturquoise";  // light blue
-	m_colors[12] = "darkblue";       // dark blue
-	m_colors[13] = "indigo";         // purple
 
-	// lighter colors for staff highlighting:
-	//m_colors[0] = "#ffaaaa";  // red
-	//m_colors[1] = "#ffbb00";  // orange
-	//m_colors[2] = "#eeee00";  // yellow
-	//m_colors[3] = "#99cc01";  // green
-	//m_colors[4] = "#bbddff";  // light blue
-	//m_colors[5] = "#88aaff";  // dark blue
-	//m_colors[6] = "#cc88ff";  // purple
+//////////////////////////////
+//
+// Tool_prange::getVpos == return the position on the staff given the diatonic pitch.
+//     and the staff. 1=bass, 2=treble.
+//     3 = bottom line of clef, 0 = space below first ledger line.
+//
 
-	m_colorQ = getBoolean("color");
-	m_colorQ = true;  // default behavior for now.
+double Tool_prange::getVpos(double base7) {
+	double output = 0;
+	if (base7 < 4 * 7) {
+		// bass clef
+		output = base7 - (1 + 2*7);  // D2
+	} else {
+		// treble clef
+		output = base7 - (6 + 3*7);  // B3
+	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pline::processFile --
+// Tool_prange::getStaffBase7 -- return 1 if less than middle C; otherwise return 2.
 //
 
-void Tool_pline::processFile(HumdrumFile& infile) {
-	getPlineInterpretations(infile, m_ptokens);
-	fillLineInfo(infile, m_lineInfo);
-	if (m_colorQ) {
-		plineToColor(infile, m_ptokens);
-	}
-	infile.createLinesFromTokens();
-	m_humdrum_text << infile;
-	if (m_colorQ) {
-		m_humdrum_text << "!!!RDF**kern: 😀 = marked note, color=black" << endl;
+int Tool_prange::getStaffBase7(int base7) {
+	if (base7 < 4 * 7) {
+		return 1;
+	} else {
+		return 2;
 	}
 }
 
 
-
 //////////////////////////////
 //
-// Tool_pline::markRests --
+// Tool_prange::getMaxDiatonicIndex -- return the highest non-zero content.
 //
 
-void Tool_pline::markRests(HumdrumFile& infile) {
-	vector<HTp> spinestops;
-	infile.getSpineStopList(spinestops);
-	for (int i=0; i<(int)spinestops.size(); i++) {
-		if (!spinestops[i]->isKern()) {
-			continue;
+int Tool_prange::getMaxDiatonicIndex(vector<vector<double>>& diatonic) {
+	for (int i=diatonic.size()-1; i>=0; i--) {
+		if (diatonic.at(i).at(0) != 0.0) {
+			return i;
 		}
-		markSpineRests(spinestops[i]);
 	}
+	return -1;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_pline::markSpineRests --
+// Tool_prange::getMinDiatonicIndex -- return the lowest non-zero content.
 //
 
-void Tool_pline::markSpineRests(HTp spineStop) {
-	string marker = "😀";
-	int track = spineStop->getTrack();
-	int lastValue = -1;
-	HTp current = spineStop->getPreviousToken();
-	int  line;
-	int  cvalue;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getPreviousToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getPreviousToken();
-			continue;
-		}
-
-		line = current->getLineIndex();
-		cvalue = m_lineInfo.at(line).at(track);
-
-		if (current->isRest() && (cvalue != lastValue)) {
-			string text = *current;
-			text += marker;
-			current->setText(text);
-		} else {
-			lastValue = cvalue;
-			string text = *current;
-			text += "@" + to_string(cvalue);
-			current->setText(text);
+int Tool_prange::getMinDiatonicIndex(vector<vector<double>>& diatonic) {
+	for (int i=0; i<(int)diatonic.size(); i++) {
+		if (diatonic.at(i).at(0) != 0.0) {
+			return i;
 		}
-		current = current->getPreviousToken();
 	}
+	return -1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pline::fillLineInfo --
+// Tool_prange::getMinDiatonicAcc -- return the lowest accidental.
 //
 
-void Tool_pline::fillLineInfo(HumdrumFile& infile, vector<vector<int>>& lineinfo) {
-	lineinfo.clear();
-	lineinfo.resize(infile.getLineCount());
-	int maxtrack = infile.getMaxTrack();
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		lineinfo[i].resize(maxtrack + 1);
-		fill(lineinfo[i].begin(), lineinfo[i].end(), 0);
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (hre.search(token, "^\\*pline:\\s*(\\d+)")) {
-				int digit = hre.getMatchInt(1);
-				int track = token->getTrack();
-				lineinfo[i][track] = digit;
-			}
-		}
-	}
-
-	for (int i=1; i<(int)lineinfo.size() - 1; i++) {
-		for (int j=1; j<=maxtrack; j++) {
-			if (lineinfo.at(i).at(j)) {
-				continue;
-			} else {
-				lineinfo.at(i).at(j) = lineinfo.at(i-1).at(j);
-			}
+int Tool_prange::getMinDiatonicAcc(vector<vector<double>>& diatonic, int index) {
+	for (int i=1; i<(int)diatonic.at(index).size(); i++) {
+		if (diatonic.at(index).at(i) != 0.0) {
+			return i;
 		}
 	}
-
-	// for (int i=0; i<(int)lineinfo.size() - 1; i++) {
-	// 	for (int j=1; j<=maxtrack; j++) {
-	// 		cerr << lineinfo[i][j] << "\t";
-	// 	}
-	// 	cerr << endl;
-	// }
-
+	return -1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pline::plineToColor --
+// Tool_prange::getMaxDiatonicAcc -- return the highest accidental.
 //
 
-void Tool_pline::plineToColor(HumdrumFile& infile, vector<HTp>& tokens) {
-	HumRegex hre;
-	markRests(infile);
-	for (int i=0; i<(int)tokens.size(); i++) {
-		if (!hre.search(tokens[i], "^\\*pline:\\s*(\\d+)")) {
-			continue;
+int Tool_prange::getMaxDiatonicAcc(vector<vector<double>>& diatonic, int index) {
+	for (int i=(int)diatonic.at(index).size() - 1; i>0; i--) {
+		if (diatonic.at(index).at(i) != 0.0) {
+			return i;
 		}
-		int lineNum = hre.getMatchInt(1);
-		int colorIndex = (lineNum - 1) % m_colors.size();
-		string color = m_colors.at(colorIndex);
-		string text = "*color:";
-		text += color;
-		tokens[i]->setText(text);
 	}
+	return -1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pline::getPlineInterpretations --
+// Tool_prange::prepareRefmap --
 //
 
-void Tool_pline::getPlineInterpretations(HumdrumFile& infile, vector<HTp>& tokens) {
+void Tool_prange::prepareRefmap(HumdrumFile& infile) {
+	vector<HLp> refrecords = infile.getGlobalReferenceRecords();
+	m_refmap.clear();
 	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (hre.search(token, "^\\*pline:\\s*(\\d+)")) {
-				tokens.push_back(token);
+	for (int i = (int)refrecords.size()-1; i>=0; i--) {
+		string key = refrecords[i]->getReferenceKey();
+		string value = refrecords[i]->getReferenceValue();
+		m_refmap[key] = value;
+		if (key.find("@") != string::npos) {
+			// create default value
+			hre.replaceDestructive(key, "", "@.*");
+			if (m_refmap[key].empty()) {
+				m_refmap[key] = value;
 			}
 		}
 	}
-}
-
-
-
+	// fill in @{} templates (mostly for !!!title:)
+	int counter = 0; // prevent recursions
+	for (auto& entry : m_refmap) {
 
+		if (entry.second.find("@") != string::npos) {
+			while (hre.search(entry.second, "@\\{(.*?)\\}")) {
+				string key = hre.getMatch(1);
+				string value = m_refmap[key];
+				hre.replaceDestructive(entry.second, value, "@\\{" + key + "\\}", "g");
+				counter++;
+				if (counter > 1000) {
+					break;
+				}
+			}
+		}
 
-/////////////////////////////////
-//
-// Tool_gridtest::Tool_pnum -- Set the recognized options for the tool.
-//
+	}
 
-Tool_pnum::Tool_pnum(void) {
-	define("b|base=i:midi",    "numeric base of pitch to extract");
-	define("D|no-duration=b",  "do not include duration");
-	define("c|pitch-class=b",  "give numeric pitch-class rather than pitch");
-	define("o|octave=b",       "give octave rather than pitch");
-	define("r|rest=s:0",       "representation string for rests");
-	define("R|no-rests=b",     "do not include rests in conversion");
-	define("x|attacks-only=b", "only mark lines with note attacks");
+	// prepare title
+	if (m_refmap["title"].empty()) {
+		m_refmap["title"] = m_refmap["OTL"];
+	}
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_pnum::run -- Primary interfaces to the tool.
+// Tool_prange::getTitle --
 //
 
-bool Tool_pnum::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+string Tool_prange::getTitle(void) {
+	string titlestring = "_00";
+	HumRegex hre;
+	if (m_notitleQ) {
+		return "";
+	} else if (m_titleQ) {
+		titlestring = m_title;
+		int counter = 0;
+
+		if (titlestring.find("@") != string::npos) {
+			while (hre.search(titlestring, "@\\{(.*?)\\}")) {
+				string key = hre.getMatch(1);
+				string value = m_refmap[key];
+				hre.replaceDestructive(titlestring, value, "@\\{" + key + "\\}", "g");
+				counter++;
+				if (counter > 1000) {
+					break;
+				}
+			}
+		}
+		if (!titlestring.empty()) {
+			titlestring = "_00" + titlestring;
+		}
+	} else {
+		titlestring = m_refmap["title"];
+		if (!titlestring.empty()) {
+			titlestring = "_00" + titlestring;
+		}
 	}
-	return status;
+	return titlestring;
 }
 
 
-bool Tool_pnum::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	return run(infile, out);
-}
 
+//////////////////////////////
+//
+// Tool_prange::clearHistograms --
+//
 
-bool Tool_pnum::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	out << infile;
-	return status;
+void Tool_prange::clearHistograms(vector<vector<double> >& bins, int start) {
+	int i;
+	for (i=start; i<(int)bins.size(); i++) {
+		bins[i].resize(40*11);
+		fill(bins[i].begin(), bins[i].end(), 0.0);
+		// bins[i].allowGrowth(0);
+	}
+	for (int i=0; i<(int)bins.size(); i++) {
+		if (bins[i].size() == 0) {
+			bins[i].resize(40*11);
+			fill(bins[i].begin(), bins[i].end(), 0.0);
+		}
+	}
 }
 
 
-bool Tool_pnum::run(HumdrumFile& infile) {
-   initialize(infile);
-	processFile(infile);
-	infile.createLinesFromTokens();
-	return true;
-}
-
 
 
 //////////////////////////////
 //
-// Tool_pnum::initialize --
+// Tool_prange::printAnalysis --
 //
 
-void Tool_pnum::initialize(HumdrumFile& infile) {
-	m_midiQ = false;
-	if (getString("base") == "midi") {
-		m_base = 12;
-		m_midiQ = true;
-	} else {
-		// check base for valid numbers, but for now default to 12 if unknown
-		m_base = getInteger("base");
+void Tool_prange::printAnalysis(ostream& out, vector<double>& midibins) {
+	if (m_percentileQ) {
+		printPercentile(out, midibins, m_percentile);
+		return;
+	}  else if (m_rangeQ) {
+		double notesinrange = countNotesInRange(midibins, m_rangeL, m_rangeH);
+		out << notesinrange << endl;
+		return;
 	}
 
-	m_durationQ = !getBoolean("no-duration");
-	m_classQ    =  getBoolean("pitch-class");
-	m_octaveQ   =  getBoolean("octave");
-	m_attacksQ  =  getBoolean("attacks-only");
-	m_rest      =  getString("rest");
-	m_restQ     = !getBoolean("no-rests");
-}
+	int i;
+	double normval = 1.0;
+
+	// print the pitch histogram
+
+	double fracL = 0.0;
+	double fracH = 0.0;
+	double fracA = 0.0;
+	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
+	if (m_normQ) {
+		normval = sum;
+	}
+	double runningtotal = 0.0;
 
 
+	out << "**keyno\t";
+	if (m_pitchQ) {
+		out << "**pitch";
+	} else {
+		out << "**kern";
+	}
+	out << "\t**count";
+	if (m_addFractionQ) {
+		out << "\t**fracL";
+		out << "\t**fracA";
+		out << "\t**fracH";
+	}
+	out << "\n";
 
-//////////////////////////////
-//
-// Tool_pnum::processFile --
-//
 
-void Tool_pnum::processFile(HumdrumFile& infile) {
-	vector<HTp> kex;
+	int base12;
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
+	if (!m_reverseQ) {
+		for (i=0; i<(int)midibins.size(); i++) {
+			if (midibins[i] <= 0.0) {
 				continue;
 			}
-			if (*token == "**kern") {
-				kex.push_back(token);
-				continue;
+			if (m_diatonicQ) {
+				base12 = Convert::base7ToBase12(i);
+			} else {
+				base12 = i;
 			}
-			if (!token->isData()) {
-				continue;
+			out << base12 << "\t";
+			if (m_pitchQ) {
+				out << Convert::base12ToPitch(base12);
+			} else {
+				out << Convert::base12ToKern(base12);
 			}
-			if (token->isNull()) {
+			out << "\t";
+			out << midibins[i] / normval;
+			fracL = runningtotal/sum;
+			runningtotal += midibins[i];
+			fracH = runningtotal/sum;
+			fracA = (fracH + fracL)/2.0;
+			fracL = (int)(fracL * 10000.0 + 0.5)/10000.0;
+			fracH = (int)(fracH * 10000.0 + 0.5)/10000.0;
+			fracA = (int)(fracA * 10000.0 + 0.5)/10000.0;
+			if (m_addFractionQ) {
+				out << "\t" << fracL;
+				out << "\t" << fracA;
+				out << "\t" << fracH;
+			}
+			out << "\n";
+		}
+	} else {
+		for (i=(int)midibins.size()-1; i>=0; i--) {
+			if (midibins[i] <= 0.0) {
 				continue;
 			}
-			convertTokenToBase(token);
+			if (m_diatonicQ) {
+				base12 = Convert::base7ToBase12(i);
+			} else {
+				base12 = i;
+			}
+			out << base12 << "\t";
+			if (m_pitchQ) {
+				out << Convert::base12ToPitch(base12);
+			} else {
+				out << Convert::base12ToKern(base12);
+			}
+			out << "\t";
+			out << midibins[i] / normval;
+			fracL = runningtotal/sum;
+			runningtotal += midibins[i];
+			fracH = runningtotal/sum;
+			fracA = (fracH + fracL)/2.0;
+			fracL = (int)(fracL * 10000.0 + 0.5)/10000.0;
+			fracH = (int)(fracH * 10000.0 + 0.5)/10000.0;
+			fracA = (int)(fracA * 10000.0 + 0.5)/10000.0;
+			if (m_addFractionQ) {
+				out << "\t" << fracL;
+				out << "\t" << fracA;
+				out << "\t" << fracH;
+			}
+			out << "\n";
 		}
 	}
 
-	string newex;
-	for (int i=0; i<(int)kex.size(); i++) {
-		if (m_midiQ) {
-			newex = "**pmid";
-		} else {
-			newex = "**b" + to_string(m_base);
-		}
-		kex[i]->setText(newex);
+	out << "*-\t*-\t*-";
+	if (m_addFractionQ) {
+		out << "\t*-";
+		out << "\t*-";
+		out << "\t*-";
 	}
-}
-
+	out << "\n";
 
+	out << "!!tessitura:\t" << getTessitura(midibins) << " semitones\n";
 
-//////////////////////////////
-//
-// Tool_pnum::convertTokenToBase --
-//
+	double mean = getMean12(midibins);
+	if (m_diatonicQ && (mean > 0)) {
+		mean = Convert::base7ToBase12(mean);
+	}
+	out << "!!mean:\t\t" << mean;
+	out << " (";
+	if (mean < 0) {
+		out << "unpitched";
+	} else {
+		out << Convert::base12ToKern(int(mean+0.5));
+	}
+	out << ")" << "\n";
 
-void Tool_pnum::convertTokenToBase(HTp token) {
-	string output;
-	int scount = token->getSubtokenCount();
-	for (int i=0; i<scount; i++) {
-		string subtok = token->getSubtoken(i);
-		output += convertSubtokenToBase(subtok);
-		if (i < scount - 1) {
-			output += " ";
-		}
+	int median12 = getMedian12(midibins);
+	out << "!!median:\t" << median12;
+	out << " (";
+	if (median12 < 0) {
+		out << "unpitched";
+	} else {
+		out << Convert::base12ToKern(median12);
 	}
-	token->setText(output);
+	out << ")" << "\n";
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_pnum::convertSubtokenToBase --
+// Tool_prange::getMedian12 -- return the pitch on which half of pitches are above
+//     and half are below.
 //
 
-string Tool_pnum::convertSubtokenToBase(const string& text) {
-	int pitch = 0;
-	if (text.find("r") == string::npos) {
-		switch (m_base) {
-			case 7:
-				pitch = Convert::kernToBase7(text);
-				break;
-			case 40:
-				pitch = Convert::kernToBase40(text);
-				break;
-			default:
-				pitch = Convert::kernToBase12(text);
+int Tool_prange::getMedian12(vector<double>& midibins) {
+	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
+
+	double cumsum = 0.0;
+	int i;
+	for (i=0; i<(int)midibins.size(); i++) {
+		if (midibins[i] <= 0.0) {
+			continue;
 		}
-	} else if (!m_restQ) {
-		return ".";
-	}
-	string recip;
-	if (m_durationQ) {
-		HumRegex hre;
-		if (hre.search(text, "(\\d+%?\\d*\\.*)")) {
-			recip = hre.getMatch(1);
+		cumsum += midibins[i]/sum;
+		if (cumsum >= 0.50) {
+			return i;
 		}
 	}
 
-	string output;
+	return -1000;
+}
 
-	int pc = pitch % m_base;
-	int oct = pitch / m_base;
 
-	if (m_midiQ) {
-		// MIDI numbers use 5 for middle-C octave.
-		pitch += 12;
-	}
 
-	int tie = 1;
-	if (text.find("_") != string::npos) {
-		tie = -1;
-	}
-	if (text.find("]") != string::npos) {
-		tie = -1;
-	}
-	pitch *= tie;
-	if (m_attacksQ && pitch < 0) {
-		return ".";
-	}
+//////////////////////////////
+//
+// Tool_prange::getMean12 -- return the interval between the highest and lowest
+//     pitch in terms if semitones.
+//
 
-	if (m_durationQ) {
-		output += recip;
-		output += "/";
-	}
+double Tool_prange::getMean12(vector<double>& midibins) {
+	double top    = 0.0;
+	double bottom = 0.0;
 
-	if (text.find("r") != string::npos) {
-		output += m_rest;
-	} else {
-		if (!m_octaveQ && !m_classQ) {
-			output += to_string(pitch);
-		} else {
-			if (m_classQ) {
-				if (pitch < 0) {
-					output += "-";
-				}
-				output += to_string(pc);
-			}
-			if (m_classQ && m_octaveQ) {
-				output += ":";
-			}
-			if (m_octaveQ) {
-				output += to_string(oct);
-			}
+	int i;
+	for (i=0; i<(int)midibins.size(); i++) {
+		if (midibins[i] <= 0.0) {
+			continue;
 		}
+		top += midibins[i] * i;
+		bottom += midibins[i];
+
 	}
 
-	return output;
+	if (bottom == 0) {
+		return -1000;
+	}
+	return top / bottom;
 }
 
 
 
+//////////////////////////////
+//
+// Tool_prange::getTessitura -- return the interval between the highest and lowest
+//     pitch in terms if semitones.
+//
 
+int Tool_prange::getTessitura(vector<double>& midibins) {
+	int minn = -1000;
+	int maxx = -1000;
+	int i;
 
-#define OBJTAB "\t\t\t\t\t\t"
-#define SVGTAG "_99%svg%";
+	for (i=0; i<(int)midibins.size(); i++) {
+		if (midibins[i] <= 0.0) {
+			continue;
+		}
+		if (minn < 0) {
+			minn = i;
+		}
+		if (maxx < 0) {
+			maxx = i;
+		}
+		if (minn > i) {
+			minn = i;
+		}
+		if (maxx < i) {
+			maxx = i;
+		}
+	}
+	if (m_diatonicQ) {
+		maxx = Convert::base7ToBase12(maxx);
+		minn = Convert::base7ToBase12(minn);
+	}
+
+	return maxx - minn + 1;
+}
 
-#define SVGTEXT(out, text) \
-	if (m_defineQ) { \
-		out << "SVG "; \
-	} else { \
-		out << "t 1 1\n"; \
-		out << SVGTAG; \
-	} \
-	printScoreEncodedText((out), (text)); \
-	out << "\n";
 
 
 //////////////////////////////
 //
-// _VoiceInfo::_VoiceInfo --
+// Tool_prange::countNotesInRange --
 //
 
-_VoiceInfo::_VoiceInfo(void) {
-	clear();
+double Tool_prange::countNotesInRange(vector<double>& midibins, int low, int high) {
+	int i;
+	double sum = 0;
+	for (i=low; i<=high; i++) {
+		sum += midibins[i];
+	}
+	return sum;
 }
 
 
 
 //////////////////////////////
 //
-// _VoiceInfo::clear --
+// Tool_prange::printPercentile --
 //
 
-void _VoiceInfo::clear(void) {
-	name = "";
-	abbr = "";
-	midibins.resize(128);
-	fill(midibins.begin(), midibins.end(), 0.0);
-	diatonic.resize(7 * 12);
-	for (int i=0; i<(int)diatonic.size(); i++) {
-		diatonic[i].resize(6);
-		fill(diatonic[i].begin(), diatonic[i].end(), 0.0);
+void Tool_prange::printPercentile(ostream& out, vector<double>& midibins, double m_percentile) {
+	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
+	double runningtotal = 0.0;
+	int i;
+	for (i=0; i<(int)midibins.size(); i++) {
+		if (midibins[i] <= 0) {
+			continue;
+		}
+		runningtotal += midibins[i] / sum;
+		if (runningtotal >= m_percentile) {
+			out << i << endl;
+			return;
+		}
 	}
-	track = -1;
-	kernQ = false;
-	diafinal.clear();
-	accfinal.clear();
-	namfinal.clear();
-	index = -1;
+
+	out << "unknown" << endl;
 }
 
 
+
 //////////////////////////////
 //
-// _VoiceInfo::print --
+// Tool_prange::getRange --
 //
 
-ostream& _VoiceInfo::print(ostream& out) {
-	out << "==================================" << endl;
-	out << "track:  " << track << endl;
-	out << " name:  " << name << endl;
-	out << " abbr:  " << abbr << endl;
-	out << " kern:  " << kernQ << endl;
-	out << " final:";
-	for (int i=0; i<(int)diafinal.size(); i++) {
-		out << " " << diafinal.at(i) << "/" << accfinal.at(i);
+void Tool_prange::getRange(int& rangeL, int& rangeH, const string& rangestring) {
+	rangeL = -1; rangeH = -1;
+	if (rangestring.empty()) {
+		return;
 	}
-	out << endl;
-	out << " midi:  ";
-	for (int i=0; i<(int)midibins.size(); i++) {
-		if (midibins.at(i) > 0.0) {
-			out << " " << i << ":" << midibins.at(i);
+	int length = (int)rangestring.length();
+	char* buffer = new char[length+1];
+	strcpy(buffer, rangestring.c_str());
+	char* ptr;
+	if (std::isdigit(buffer[0])) {
+		ptr = strtok(buffer, " \t\n:-");
+		sscanf(ptr, "%d", &rangeL);
+		ptr = strtok(NULL, " \t\n:-");
+		if (ptr != NULL) {
+			sscanf(ptr, "%d", &rangeH);
 		}
-	}
-	out << endl;
-	out << " diat:  ";
-	for (int i=0; i<(int)diatonic.size(); i++) {
-		if (diatonic.at(i).at(0) > 0.0) {
-			out << " " << i << ":" << diatonic.at(i).at(0);
+	} else {
+		ptr = strtok(buffer, " :");
+		if (ptr != NULL) {
+			rangeL = Convert::kernToMidiNoteNumber(ptr);
+			ptr = strtok(NULL, " :");
+			if (ptr != NULL) {
+				rangeH = Convert::kernToMidiNoteNumber(ptr);
+			}
 		}
 	}
-	out << endl;
-	out << "==================================" << endl;
-	return out;
+
+	if (rangeH < 0) {
+		rangeH = rangeL;
+	}
+
+	if (rangeL <   0) { rangeL =   0; }
+	if (rangeH <   0) { rangeH =   0; }
+	if (rangeL > 127) { rangeL = 127; }
+	if (rangeH > 127) { rangeH = 127; }
+	if (rangeL > rangeH) {
+		int temp = rangeL;
+		rangeL = rangeH;
+		rangeH = temp;
+	}
+
 }
 
 
 
+
+
 /////////////////////////////////
 //
-// Tool_prange::Tool_prange -- Set the recognized options for the tool.
+// Tool_gridtest::Tool_recip -- Set the recognized options for the tool.
 //
 
-Tool_prange::Tool_prange(void) {
-
-	define("A|acc|color-accidentals=b", "add color to accidentals in histogram");
-	define("D|diatonic=b",              "diatonic counts ignore chormatic alteration");
-	define("K|no-key=b",                "do not display key signature");
-	define("N|norm=b",                  "normalize pitch counts");
-	define("S|score=b",                 "convert range info to SCORE");
-	define("T|no-title=b",              "do not display a title");
-	define("a|all=b",                   "generate all-voice analysis");
-	define("c|range|count=s:60-71",     "count notes in a particular MIDI note number range (inclusive)");
-	define("debug=b",                   "trace input parsing");
-	define("d|duration=b",              "weight pitches by duration");
-	define("e|embed=b",                 "embed SCORE data in input Humdrum data");
-	define("fill=b",                    "change color of fill only");
-	define("finalis|final|last=b",      "include finalis note by voice");
-	define("f|fraction=b",              "display histogram fractions");
-	define("h|hover=b",                 "include svg hover capabilities");
-	define("i|instrument=b",            "categorize multiple inputs by instrument");
-	define("j|jrp=b",                   "set options for JRP style");
-	define("l|local|local-maximum|local-maxima=b",  "use maximum values by voice rather than all voices");
-	define("no-define=b",               "do not use defines in output SCORE data");
-	define("pitch=b",                   "display pitch info in **pitch format");
-	define("print=b",                   "count printed notes rather than sounding");
-	define("p|percentile=d:0.0",        "display the xth percentile pitch");
-	define("q|quartile=b",              "display quartile notes");
-	define("r|reverse=b",               "reverse list of notes in analysis from high to low");
-	define("x|extrema=b",               "highlight extrema notes in each part");
-	define("sx|scorexml|score-xml|ScoreXML|scoreXML=b", "output ScoreXML format");
-	define("title=s:",                  "title for SCORE display");
-
+Tool_recip::Tool_recip(void) {
+	define("c|composite=b",          "do composite rhythm analysis");
+	define("a|append=b",             "append composite analysis to input");
+	define("p|prepend=b",            "prepend composite analysis to input");
+	define("r|replace=b",            "replace **kern data with **recip data");
+	define("x|attacks-only=b",       "only mark lines with note attacks");
+	define("G|ignore-grace-notes=b", "ignore grace notes");
+	define("k|kern-spine=i:1",       "analyze only given kern spine");
+	define("K|all-spines=b",         "analyze each kern spine separately");
+	define("e|exinterp=s:**recip",   "use the given exinterp for data output");
+	define("n|kern-pitch=s:e",       "note to add for '-e kern' option");
+	define("kern=b",                 "equivalent to '-e kern' option");
 }
 
 
-/////////////////////////////////
+
+///////////////////////////////
 //
-// Tool_prange::run -- Do the main work of the tool.
+// Tool_recip::run -- Primary interfaces to the tool.
 //
 
-bool Tool_prange::run(HumdrumFileSet& infiles) {
+bool Tool_recip::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -115459,32 +120119,44 @@ bool Tool_prange::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_prange::run(const string& indata, ostream& out) {
+bool Tool_recip::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
+	return run(infile, out);
 }
 
 
-bool Tool_prange::run(HumdrumFile& infile, ostream& out) {
+bool Tool_recip::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
+	out << infile;
 	return status;
 }
 
 
-bool Tool_prange::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
+bool Tool_recip::run(HumdrumFile& infile) {
+   initialize(infile);
+
+	int lineCount = infile.getLineCount();
+	if (lineCount == 0) {
+		m_error_text << "No input data";
+		return false;
+	}
+
+	if (getBoolean("composite") || getBoolean("append") || getBoolean("prepend")) {
+		doCompositeAnalysis(infile);
+		infile.createLinesFromTokens();
+		return true;
+	} else if (getBoolean("replace")) {
+		replaceKernWithRecip(infile);
+		infile.createLinesFromTokens();
+		return true;
+	}
+	HumdrumFile cfile = infile;
+	cfile.analyzeStructure();
+	replaceKernWithRecip(cfile);
+	cfile.createLinesFromTokens();
+	insertAnalysisSpines(infile, cfile);
+	// infile.adjustMergeSpineLines();
+	infile.createLinesFromTokens();
 	return true;
 }
 
@@ -115492,107 +120164,83 @@ bool Tool_prange::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_prange::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_recip::insertAnalysisSpines -- Could be more efficient than the
+//     k-index loop...
 //
 
-void Tool_prange::initialize(void) {
-	m_accQ         = getBoolean("color-accidentals");
-	m_addFractionQ = getBoolean("fraction");
-	m_allQ         = getBoolean("all");
-	m_debugQ       = getBoolean("debug");
-	m_defineQ      = false;
-	m_diatonicQ    = getBoolean("diatonic");
-	m_durationQ    = getBoolean("duration");
-	m_fillOnlyQ    = getBoolean("fill");
-	m_finalisQ     = getBoolean("finalis");
-	m_hoverQ       = getBoolean("hover");
-	m_instrumentQ  = getBoolean("instrument");
-	m_keyQ         = !getBoolean("no-key");
-	m_listQ        = false;
-	m_localQ       = getBoolean("local-maximum");
-	m_normQ        = getBoolean("norm");
-	m_notitleQ     = getBoolean("no-title");
-	m_percentile   = getDouble("percentile");
-	m_percentileQ  = getBoolean("percentile");
-	m_pitchQ       = getBoolean("pitch");
-	m_printQ       = getBoolean("print");
-	m_quartileQ    = getBoolean("quartile");
-	m_rangeQ       = getBoolean("range");
-	m_reverseQ     = !getBoolean("reverse");
-	m_scoreQ       = getBoolean("score");
-	m_title        = getString("title");
-	m_titleQ       = getBoolean("title");
-	m_embedQ       = getBoolean("embed");
-	m_extremaQ     = getBoolean("extrema");
-
-	getRange(m_rangeL, m_rangeH, getString("range"));
-
-	if (getBoolean("jrp")) {
-		// default style settings for JRP range displays:
-		m_scoreQ   = true;
-		m_allQ     = true;
-		m_hoverQ   = true;
-		m_accQ     = true;
-		m_finalisQ = true;
-		m_notitleQ = true;
-	}
-
-	// The percentile is a fraction from 0.0 to 1.0.
-	// if the percentile is above 1.0, then it is assumed
-	// to be a percentage, in which case the value will be
-	// divided by 100 to get it in the range from 0 to 1.
-	if (m_percentile > 1) {
-		m_percentile = m_percentile / 100.0;
+void Tool_recip::insertAnalysisSpines(HumdrumFile& infile, HumdrumFile& cfile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			continue;
+		}
+		for (int k=(int)m_kernspines.size()-1; k>=0; k--) {
+			int fcount = infile[i].getFieldCount();
+			int ktrack = m_kernspines[k]->getTrack();
+			int insertj = -1;
+			for (int j=fcount-1; j>=0; j--) {
+				if (!infile.token(i, j)->isKern()) {
+					continue;
+				}
+				int track = infile.token(i, j)->getTrack();
+				if (track != ktrack) {
+					continue;
+				}
+				if (insertj < 0) {
+					insertj = j;
+				}
+				infile[i].appendToken(insertj, cfile.token(i, j)->getText());
+				// infile.token(i, insertj+1)->setTrack(remapping[k]);
+			}
+		}
 	}
-
-	#ifdef __EMSCRIPTEN__
-		// Default styling for JavaScript version of program:
-		m_accQ     = !getBoolean("color-accidentals");
-		m_scoreQ   = !getBoolean("score");
-		m_embedQ   = !getBoolean("embed");
-		m_hoverQ   = !getBoolean("hover");
-		m_notitleQ = !getBoolean("no-title");
-	#endif
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::processFile --
+// Tool_recip::doCompositeAnalysis --
 //
 
-void Tool_prange::processFile(HumdrumFile& infile) {
-	prepareRefmap(infile);
-	vector<_VoiceInfo> voiceInfo;
-	infile.fillMidiInfo(m_trackMidi);
-	getVoiceInfo(voiceInfo, infile);
-	fillHistograms(voiceInfo, infile);
+void Tool_recip::doCompositeAnalysis(HumdrumFile& infile) {
 
-	if (m_debugQ) {
-		for (int i=0; i<(int)voiceInfo.size(); i++) {
-			voiceInfo[i].print(cerr);
-		}
+	// Calculate composite rhythm **recip spine:
+
+	vector<HumNum> composite(infile.getLineCount());
+	for (int i=0; i<(int)composite.size(); i++) {
+		composite[i] = infile[i].getDuration();
 	}
 
-	if (m_scoreQ) {
-		stringstream scoreout;
-		printScoreFile(scoreout, voiceInfo, infile);
-		if (m_embedQ) {
-			if (m_extremaQ) {
-				doExtremaMarkup(infile);
-			}
-			m_humdrum_text << infile;
-			printEmbeddedScore(m_humdrum_text, scoreout, infile);
-		} else {
-			if (m_extremaQ) {
-				doExtremaMarkup(infile);
-			}
-			m_humdrum_text << scoreout.str();
+	int kernQ = false;
+	if (m_exinterp.find("kern") != std::string::npos) {
+		kernQ = true;
+// cerr << "KERN ON" << endl;
+	}
+
+	// convert durations to **recip strings
+	vector<string> recips(composite.size());
+	for (int i=0; i<(int)recips.size(); i++) {
+		if ((!m_graceQ) && (composite[i] == 0)) {
+			continue;
+		}
+		recips[i] = Convert::durationToRecip(composite[i]);
+		if (kernQ) {
+			recips[i] += m_kernpitch;
+// cerr << "ADDING PITCH " << m_kernpitch << endl;
 		}
+	}
+
+	if (getBoolean("append")) {
+		infile.appendDataSpine(recips, "", m_exinterp);
+		return;
+	} else if (getBoolean("prepend")) {
+		infile.prependDataSpine(recips, "", m_exinterp);
+		return;
 	} else {
-		printAnalysis(m_humdrum_text, voiceInfo[0].midibins);
+		infile.prependDataSpine(recips, "", m_exinterp);
+		infile.printFieldIndex(0, m_humdrum_text);
+		infile.clear();
+		infile.readString(m_humdrum_text.str());
 	}
 }
 
@@ -115600,174 +120248,187 @@ void Tool_prange::processFile(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_prange::doExtremaMarkup -- Mark highest and lowest note
-//     in each **kern spine.
-//
+// Tool_recip::replaceKernWithRecip --
 //
 
-void Tool_prange::doExtremaMarkup(HumdrumFile& infile) {
-	bool highQ = false;
-	bool lowQ = false;
-	for (int i=0; i<(int)m_trackMidi.size(); i++) {
-		int maxindex = -1;
-		int minindex = -1;
-
-		for (int j=(int)m_trackMidi[i].size()-1; j>=0; j--) {
-			if (m_trackMidi[i][j].empty()) {
+void Tool_recip::replaceKernWithRecip(HumdrumFile& infile) {
+	vector<HTp> kspines = infile.getKernSpineStartList();
+	HumRegex hre;
+	string expression = "[^q\\d.%\\]\\[]+";
+	for (int i=0; i<infile.getStrandCount(); i++) {
+		HTp stok = infile.getStrandStart(i);
+		if (!stok->isKern()) {
+			continue;
+		}
+		HTp etok = infile.getStrandEnd(i);
+		HTp tok = stok;
+		while (tok && (tok != etok)) {
+			if (!tok->isData()) {
+				tok = tok->getNextToken();
 				continue;
 			}
-			if (maxindex < 0) {
-				maxindex = j;
-				break;
-			}
-		}
-
-		for (int j=1; j<(int)m_trackMidi[i].size(); j++) {
-			if (m_trackMidi[i][j].empty()) {
+			if (tok->isNull()) {
+				tok = tok->getNextToken();
 				continue;
 			}
-			if (minindex < 0) {
-				minindex = j;
-				break;
+			if (tok->find('q') != string::npos) {
+				if (m_graceQ) {
+					tok->setText("q");
+				} else {
+					tok->setText(".");
+				}
+			} else {
+				hre.replaceDestructive(*tok, "", expression, "g");
 			}
+			tok = tok->getNextToken();
 		}
+	}
 
-		if ((maxindex < 0) || (minindex < 0)) {
-			continue;
-		}
-		applyMarkup(m_trackMidi[i][maxindex], m_highMark);
-		applyMarkup(m_trackMidi[i][minindex], m_lowMark);
-		highQ = true;
-		lowQ  = true;
+	for (int i=0; i<(int)kspines.size(); i++) {
+		kspines[i]->setText(m_exinterp);
 	}
-	if (highQ) {
-		string highRdf = "!!!RDF**kern: " + m_highMark + " = marked note, color=\"hotpink\", highest note";
-		infile.appendLine(highRdf);
+
+}
+
+
+
+
+//////////////////////////////
+//
+// Tool_recip::initialize --
+//
+
+void Tool_recip::initialize(HumdrumFile& infile) {
+	m_kernspines = infile.getKernSpineStartList();
+	m_graceQ = !getBoolean("ignore-grace-notes");
+
+	m_exinterp = getString("exinterp");
+	if (m_exinterp.empty()) {
+		m_exinterp = "**recip";
+	} else if (m_exinterp[0] != '*') {
+		m_exinterp.insert(0, "*");
 	}
-	if (lowQ) {
-		string lowRdf = "!!!RDF**kern: " + m_lowMark + " = marked note, color=\"limegreen\", lowest note";
-		infile.appendLine(lowRdf);
+	if (m_exinterp[1] != '*') {
+		m_exinterp.insert(0, "*");
 	}
-	if (highQ || lowQ) {
-		infile.createLinesFromTokens();
+
+	m_kernpitch = getString("kern-pitch");
+
+	if (getBoolean("kern")) {
+		m_exinterp = "**kern";
 	}
+
 }
 
 
 
-//////////////////////////////
+
+
+/////////////////////////////////
 //
-// Tool_prange::applyMarkup --
+// Tool_restfill::Tool_restfill -- Set the recognized options for the tool.
 //
 
-void Tool_prange::applyMarkup(vector<pair<HTp, int>>& notelist, const string& mark) {
-	for (int i=0; i<(int)notelist.size(); i++) {
-		HTp token = notelist[i].first;
-		int subtoken = notelist[i].second;
-		int tokenCount = token->getSubtokenCount();
-		if (tokenCount == 1) {
-			string text = *token;
-			text += mark;
-			token->setText(text);
-		} else {
-			string stok = token->getSubtoken(subtoken);
-			stok = mark + stok;
-			token->replaceSubtoken(subtoken, stok);
-		}
-	}
+Tool_restfill::Tool_restfill(void) {
+	define("y|hidden-rests=b",  "hide inserted rests");
+	define("i|exinterp=s:kern", "type of spine to fill with rests");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_prange::printEmbeddedScore --
+// Tool_restfill::run -- Do the main work of the tool.
 //
 
-void Tool_prange::printEmbeddedScore(ostream& out, stringstream& scoredata, HumdrumFile& infile) {
-	int id = getPrangeId(infile);
+bool Tool_restfill::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
 
-	out << "!!@@BEGIN: PREHTML\n";
-	out << "!!@CONTENT: <div class=\"score-svg\" ";
-	out <<    "style=\"margin-top:50px;text-align:center;\" ";
-	out <<    " data-score=\"prange-" << id << "\"></div>\n";
-	out << "!!@@END: PREHTML\n";
-	out << "!!@@BEGIN: SCORE\n";
-	out << "!!@ID: prange-" << id << "\n";
-	out << "!!@OUTPUTFORMAT: svg\n";
-	out << "!!@CROP: yes\n";
-	out << "!!@PADDING: 10\n";
-	out << "!!@SCALING: 1.5\n";
-	out << "!!@SVGFORMAT: yes\n";
-	out << "!!@TRANSPARENT: yes\n";
-	out << "!!@ANTIALIAS: no\n";
-	out << "!!@EMBEDPMX: yes\n";
-	out << "!!@ANNOTATE: no\n";
-	out << "!!@CONTENTS:\n";
-	string line;
-	while(getline(scoredata, line)) {
-		out << "!!" << line << endl;
+
+bool Tool_restfill::run(const string& indata, ostream& out) {
+	HumdrumFile infile;
+	infile.readStringNoRhythm(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	out << "!!@@END: SCORE\n";
+	return status;
+}
+
+
+bool Tool_restfill::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_restfill::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	infile.createLinesFromTokens();
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getPrangeId -- Find a line in this form
-//          ^!!@ID: prange-(\d+)$
-//      and return $1+1.  Searching backwards since the HTML section
-//      will likely be at the bottom.  Assuming that the prange
-//      SVG images are stored in sequence, with the highest ID last
-//      in the file if there are more than one.
+// Tool_restfill::initialize --
 //
 
-int Tool_prange::getPrangeId(HumdrumFile& infile) {
-	string search = "!!@ID: prange-";
-	int length = (int)search.length();
-	for (int i=infile.getLineCount() - 1; i>=0; i--) {
-		HTp token = infile.token(i, 0);
-		if (token->compare(0, length, search) == 0) {
-			HumRegex hre;
-			if (hre.search(token, "prange-(\\d+)")) {
-				return hre.getMatchInt(1) + 1;
-			}
+void Tool_restfill::initialize(void) {
+	m_hiddenQ = getBoolean("hidden-rests");
+	m_exinterp = getString("exinterp");
+	if (m_exinterp.empty()) {
+		m_exinterp = "**kern";
+	}
+	if (m_exinterp.compare(0, 2, "**") != 0) {
+		if (m_exinterp.compare(0, 1, "*") != 0) {
+			m_exinterp = "**" + m_exinterp;
+		} else {
+			m_exinterp = "*" + m_exinterp;
 		}
 	}
-	return 1;
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::mergeAllVoiceInfo --
+// Tool_restfill::processFile --
 //
 
-void Tool_prange::mergeAllVoiceInfo(vector<_VoiceInfo>& voiceInfo) {
-	voiceInfo.at(0).diafinal.clear();
-	voiceInfo.at(0).accfinal.clear();
-
-	for (int i=1; i<(int)voiceInfo.size(); i++) {
-		if (!voiceInfo[i].kernQ) {
-			continue;
-		}
-		for (int j=0; j<(int)voiceInfo.at(i).diafinal.size(); j++) {
-			voiceInfo.at(0).diafinal.push_back(voiceInfo.at(i).diafinal.at(j));
-			voiceInfo.at(0).accfinal.push_back(voiceInfo.at(i).accfinal.at(j));
-			voiceInfo.at(0).namfinal.push_back(voiceInfo.at(i).name);
-		}
+void Tool_restfill::processFile(HumdrumFile& infile) {
 
-		for (int j=0; j<(int)voiceInfo[i].midibins.size(); j++) {
-			voiceInfo[0].midibins[j] += voiceInfo[i].midibins[j];
+	vector<HTp> starts;
+	infile.getSpineStartList(starts, m_exinterp);
+	vector<bool> process(starts.size(), false);
+	for (int i=0; i<(int)starts.size(); i++) {
+		process[i] = hasBlankMeasure(starts[i]);
+		if (process[i]) {
+			starts[i]->setText("**temp-kern");
 		}
-
-		for (int j=0; j<(int)voiceInfo.at(i).diatonic.size(); j++) {
-			for (int k=0; k<(int)voiceInfo.at(i).diatonic.at(k).size(); k++) {
-				voiceInfo[0].diatonic.at(j).at(k) += voiceInfo.at(i).diatonic.at(j).at(k);
-			}
+	}
+	infile.analyzeStructure();
+	for (int i=0; i<(int)starts.size(); i++) {
+		if (!process[i]) {
+			continue;
 		}
+		starts[i]->setText("**kern");
+		fillInRests(starts[i]);
 	}
 }
 
@@ -115775,107 +120436,88 @@ void Tool_prange::mergeAllVoiceInfo(vector<_VoiceInfo>& voiceInfo) {
 
 //////////////////////////////
 //
-// Tool_prange::getVoiceInfo -- get names and track info for **kern spines.
+// Tool_restfill::hasBlankMeasure --
 //
 
-void Tool_prange::getVoiceInfo(vector<_VoiceInfo>& voiceInfo, HumdrumFile& infile) {
-	voiceInfo.clear();
-	voiceInfo.resize(infile.getMaxTracks() + 1);
-	for (int i=0; i<(int)voiceInfo.size(); i++) {
-		voiceInfo.at(i).index = i;
-	}
-
-	vector<HTp> kstarts = infile.getKernSpineStartList();
-
-	if (kstarts.size() == 2) {
-		voiceInfo[0].name  = "both";
-		voiceInfo[0].abbr  = "both";
-		voiceInfo[0].track = 0;
-	} else {
-		voiceInfo[0].name  = "all";
-		voiceInfo[0].abbr  = "all";
-		voiceInfo[0].track = 0;
-	}
-
+bool Tool_restfill::hasBlankMeasure(HTp start) {
+	bool foundcontent = false;
+	HTp current = start;
+	int founddata = false;
+	while (current) {
 
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			break;
-		}
-		if (!infile[i].hasSpines()) {
-			continue;
-		}
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			int track = token->getTrack();
-			voiceInfo[track].track = track;
-			if (token->isKern()) {
-				voiceInfo[track].kernQ = true;
-			}
-			if (!voiceInfo[track].kernQ) {
-				continue;
-			}
-			if (token->isInstrumentName()) {
-				voiceInfo[track].name = token->getInstrumentName();
-			}
-			if (token->isInstrumentAbbreviation()) {
-				voiceInfo[track].abbr = token->getInstrumentAbbreviation();
+		if (current->isBarline()) {
+			if (founddata && !foundcontent) {
+				return true;
 			}
+			foundcontent = false;
+			founddata = false;
+			current = current->getNextToken();
+			continue;
 		}
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		founddata = true;
+		if (!current->isNull()) {
+			foundcontent = true;
+		}
+		current = current->getNextToken();
+
 	}
-
-
-	// Check for piano/Grand Staff parts with LH/RH encoding.
-	if (kstarts.size() == 2) {
-		string bottomStaff = getHand(kstarts[0]);
-		string topStaff    = getHand(kstarts[1]);
-		if (!bottomStaff.empty() && !topStaff.empty()) {
-			int track = kstarts[0]->getTrack();
-			voiceInfo[track].name = "left hand";
-			track = kstarts[1]->getTrack();
-			voiceInfo[track].name = "right hand";
-		}
-	}
+	return false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getHand --
+// Tool_restfill::fillInRests --
+//   Also deal with cases where the last measure does not end in a barline.
 //
 
-string Tool_prange::getHand(HTp sstart) {
-	HTp current = sstart->getNextToken();
-	HTp target = NULL;
+void Tool_restfill::fillInRests(HTp start) {
+	HTp current = start;
+	HTp firstcell = NULL;
+	int founddata = false;
+	bool foundcontent = false;
+	HumNum lasttime = 0;
+	HumNum currtime = 0;
+	HumNum duration = 0;
 	while (current) {
-		if (current->isData()) {
-			break;
+		if (current->isBarline()) {
+			if (firstcell) {
+				lasttime = firstcell->getDurationFromStart();
+			}
+			currtime = getNextTime(current);
+			if (firstcell && founddata && !foundcontent) {
+				duration = currtime - lasttime;
+				addRest(firstcell, duration);
+			}
+			firstcell = NULL;
+			founddata = false;
+			foundcontent = false;
+			current = current->getNextToken();
+			lasttime = currtime;
+			continue;
 		}
-		if (*current == "*LH") {
-			target = current;
-			break;
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		if (*current == "*RH") {
-			target = current;
-			break;
+		if (current->getDuration() == 0) {
+			// grace-note line, so ignore
+			current = current->getNextToken();
+			continue;
 		}
-		current = current->getNextToken();
-	}
-
-	if (target) {
-		if (*current == "*LH") {
-			return "LH";
-		} else if (*current == "*RH") {
-			return "RH";
-		} else {
-			return "";
+		founddata = true;
+		if (!current->isNull()) {
+			foundcontent = true;
 		}
-	} else {
-		return "";
+		if (!firstcell) {
+			firstcell = current;
+		}
+		current = current->getNextToken();
 	}
 }
 
@@ -115883,1173 +120525,1344 @@ string Tool_prange::getHand(HTp sstart) {
 
 //////////////////////////////
 //
-// Tool_prange::getInstrumentNames --  Find any instrument names which are listed
-//      before the first data line.  Instrument names are in the form:
-//
-//      *I"name
+// Tool_restfill::addRest --
 //
 
-void Tool_prange::getInstrumentNames(vector<string>& nameByTrack, vector<int>& kernSpines,
-		HumdrumFile& infile) {
-	HumRegex hre;
-
-	int track;
-	string name;
-	// nameByTrack.resize(kernSpines.size());
-	nameByTrack.resize(infile.getMaxTrack() + 1);
-	fill(nameByTrack.begin(), nameByTrack.end(), "");
-	vector<HTp> kspines = infile.getKernSpineStartList();
-	if (kspines.size() == 2) {
-		nameByTrack.at(0) = "both";
-	} else {
-		nameByTrack.at(0) = "all";
+void Tool_restfill::addRest(HTp cell, HumNum duration) {
+	if (!cell) {
+		return;
 	}
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (hre.search(token, "^\\*I\"(.*)\\s*")) {
-				name = hre.getMatch(1);
-				track = token->getTrack();
-				for (int k=0; k<(int)kernSpines.size(); k++) {
-					if (track == kernSpines[k]) {
-						nameByTrack[k] = name;
-					}
-				}
-			}
-		}
+	string text = Convert::durationToRecip(duration);
+	text += "r";
+	if (m_hiddenQ) {
+		text += "yy";
 	}
+	cell->setText(text);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::fillHistograms -- Store notes in score by MIDI note number.
+// Tool_restfill::getNextTime --
 //
 
-void Tool_prange::fillHistograms(vector<_VoiceInfo>& voiceInfo, HumdrumFile& infile) {
-	// storage for finals info:
-	vector<vector<int>> diafinal;
-	vector<vector<int>> accfinal;
-	diafinal.resize(infile.getMaxTracks() + 1);
-	accfinal.resize(infile.getMaxTracks() + 1);
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isNull()) {
-				continue;
-			}
-			int track = token->getTrack();
-
-			diafinal.at(track).clear();
-			accfinal.at(track).clear();
-
-			vector<string> tokens = token->getSubtokens();
-			for (int k=0; k<(int)tokens.size(); k++) {
-				if (tokens[k].find("r") != string::npos) {
-					continue;
-				}
-				if (tokens[k].find("R") != string::npos) {
-					// non-pitched note
-					continue;
-				}
-				bool hasPitch = false;
-				for (int m=0; m<(int)tokens[k].size(); m++) {
-					char test = tokens[k].at(m);
-					if (!isalpha(test)) {
-						continue;
-					}
-					test = tolower(test);
-					if ((test >= 'a') && (test <= 'g')) {
-						hasPitch = true;
-						break;
-					}
-				}
-				if (!hasPitch) {
-					continue;
-				}
-				int octave = Convert::kernToOctaveNumber(tokens[k]) + 3;
-				if (octave < 0) {
-					cerr << "Note too low: " << tokens[k] << endl;
-					continue;
-				}
-				if (octave >= 12) {
-					cerr << "Note too high: " << tokens[k] << endl;
-					continue;
-				}
-				int dpc    = Convert::kernToDiatonicPC(tokens[k]);
-				int acc    = Convert::kernToAccidentalCount(tokens[k]);
-				if (acc < -2) {
-					cerr << "Accidental too flat: " << tokens[k] << endl;
-					continue;
-				}
-				if (acc > +2) {
-					cerr << "Accidental too sharp: " << tokens[k] << endl;
-					continue;
-				}
-				int diatonic = dpc + 7 * octave;
-				int realdiatonic = dpc + 7 * (octave-3);
-
-				diafinal.at(track).push_back(realdiatonic);
-				accfinal.at(track).push_back(acc);
-
-				acc += 3;
-				int midi = Convert::kernToMidiNoteNumber(tokens[k]);
-				if (midi < 0) {
-					cerr << "MIDI pitch too low: " << tokens[k] << endl;
-				}
-				if (midi > 127) {
-					cerr << "MIDI pitch too high: " << tokens[k] << endl;
-				}
-				if (m_durationQ) {
-					double duration = Convert::kernToDuration(tokens[k]).getFloat();
-					voiceInfo[track].diatonic.at(diatonic).at(0) += duration;
-					voiceInfo[track].diatonic.at(diatonic).at(acc) += duration;
-					voiceInfo[track].midibins.at(midi) += duration;
-				} else {
-					if (tokens[k].find("]") != string::npos) {
-						continue;
-					}
-					if (tokens[k].find("_") != string::npos) {
-						continue;
-					}
-					voiceInfo[track].diatonic.at(diatonic).at(0)++;
-					voiceInfo[track].diatonic.at(diatonic).at(acc)++;
-					voiceInfo[track].midibins.at(midi)++;
-				}
-			}
+HumNum Tool_restfill::getNextTime(HTp token) {
+	HTp current = token;
+	while (current) {
+		if (current->isData()) {
+			return current->getDurationFromStart();
 		}
+		current = current->getNextToken();
 	}
+	return token->getOwner()->getOwner()->getScoreDuration();
+}
 
-	mergeFinals(voiceInfo, diafinal, accfinal);
 
-	// Sum all voices into midibins and diatonic arrays of vector position 0:
-	mergeAllVoiceInfo(voiceInfo);
-}
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_prange::mergeFinals --
+// Tool_rid::Tool_rid -- Set the recognized options for the tool.
 //
 
-void Tool_prange::mergeFinals(vector<_VoiceInfo>& voiceInfo, vector<vector<int>>& diafinal,
-		vector<vector<int>>& accfinal) {
-	for (int i=0; i<(int)voiceInfo.size(); i++) {
-		voiceInfo.at(i).diafinal = diafinal.at(i);
-		voiceInfo.at(i).accfinal = accfinal.at(i);
-	}
+Tool_rid::Tool_rid(void) {
+   // Humdrum Toolkit classic rid options:
+   define("D|all-data=b",                  "remove all data records");
+   define("d|null-data=b",                 "remove null data records");
+   define("G|all-global=b",                "remove all global comments");
+   define("g|null-global=b",               "remove null global comments");
+   define("I|all-interpretation=b",        "remove all interpretation records");
+   define("i|null-interpretation=b",       "remove null interpretation records");
+   define("L|all-local-comment=b",         "remove all local comments");
+   define("l|1|null-local-comment=b",      "remove null local comments");
+   define("T|all-tandem-interpretation=b", "remove all tandem interpretations");
+   define("U|u=b",                         "remove unnecessary (duplicate ex. interps.");
+   define("k|consider-kern-only=b",        "for -d, only consider **kern spines.");
+   define("V=b",                           "negate filtering effect of program.");
+   define("H|no-humdrum-syntax=b",         "equivalent to -GLIMd.");
+
+   // additional options
+   define("M|all-barlines=b",              "remove measure lines");
+   define("C|all-comments=b",              "remove all comment lines");
+   define("c=b",                           "remove global and local comment lines");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_prange::printFilenameBase --
+// Tool_rid::run -- Do the main work of the tool.
 //
 
-void Tool_prange::printFilenameBase(ostream& out, const string& filename) {
-	HumRegex hre;
-	if (hre.search(filename, "([^/]+)\\.([^.]*)", "")) {
-		if (hre.getMatch(1).size() <= 8) {
-			printXmlEncodedText(out, hre.getMatch(1));
-		} else {
-			// problem with too long a name (MS-DOS will have problems).
-			// optimize to chop off everything after the dash in the
-			// name (for Josquin catalog numbers).
-			string shortname = hre.getMatch(1);
-			if (hre.search(shortname, "-.*")) {
-			   hre.replaceDestructive(shortname, "", "-.*");
-				printXmlEncodedText(out, shortname);
-			} else {
-				printXmlEncodedText(out, shortname);
-			}
-		}
+bool Tool_rid::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
 }
 
 
+bool Tool_rid::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
 
-//////////////////////////////
-//
-// Tool_prange::printReferenceRecords --
-//
 
-void Tool_prange::printReferenceRecords(ostream& out, HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReferenceRecord()) {
-			continue;
-		}
-		out <<  "\t\t\t\t\t\t<?Humdrum key=\"";
-		printXmlEncodedText(out, infile[i].getReferenceKey());
-		out << "\" value=\"";
-		printXmlEncodedText(out, infile[i].getReferenceValue());
-		out << "\"?>\n";
+bool Tool_rid::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
+}
+
+
+bool Tool_rid::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::printScoreEncodedText -- print SCORE text string
-//    See SCORE 3.1 manual additions (page 19) for more.
+// Tool_rid::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_prange::printScoreEncodedText(ostream& out, const string& strang) {
-	string newstring = strang;
-	HumRegex hre;
-
-	hre.replaceDestructive(newstring, "<<$1", "&([aeiou])acute;", "gi");
-	hre.replaceDestructive(newstring, "<<$1", "([áéíóú])", "gi");
-
-	hre.replaceDestructive(newstring, ">>$1", "&([aeiou])grave;", "gi");
-	hre.replaceDestructive(newstring, ">>$1", "([àèìòù])", "gi");
-
-	hre.replaceDestructive(newstring, "%%$1", "&([aeiou])uml;", "gi");
-	hre.replaceDestructive(newstring, "%%$1", "([äëïöü])", "gi");
-
-	hre.replaceDestructive(newstring, "^^$1", "&([aeiou])circ;", "gi");
-	hre.replaceDestructive(newstring, "^^$1", "([âêîôû])", "gi");
-
-	hre.replaceDestructive(newstring, "##c", "&ccedil;",  "g");
-	hre.replaceDestructive(newstring, "##C", "&Ccedil;",  "g");
-	hre.replaceDestructive(newstring, "?\\|", "\\|",      "g");
-	hre.replaceDestructive(newstring, "?\\",  "\\\\",     "g");
-	hre.replaceDestructive(newstring, "?m",   "---",      "g");
-	hre.replaceDestructive(newstring, "?n",   "--",       "g");
-	hre.replaceDestructive(newstring, "?2",   "-sharp",   "g");
-	hre.replaceDestructive(newstring, "?1",   "-flat",    "g");
-	hre.replaceDestructive(newstring, "?3",   "-natural", "g");
-	hre.replaceDestructive(newstring, "\\",   "/",        "g");
-	hre.replaceDestructive(newstring, "?[",   "\\[",      "g");
-	hre.replaceDestructive(newstring, "?]",   "\\]",      "g");
+void Tool_rid::initialize(void) {
+   option_D = getBoolean("D");
+   option_d = getBoolean("d");
+   option_G = getBoolean("G");
+   option_g = getBoolean("g");
+   option_I = getBoolean("I");
+   option_i = getBoolean("i");
+   option_L = getBoolean("L");
+   option_l = getBoolean("l");
+   option_T = getBoolean("T");
+   option_U = getBoolean("U");
+   option_M = getBoolean("M");
+   option_C = getBoolean("C");
+   option_c = getBoolean("c");
+   option_k = getBoolean("k");
+   option_V = getBoolean("V");
 
-	out << newstring;
+   if (getBoolean("no-humdrum-syntax")) {
+      // remove all Humdrum file structure
+      option_G = option_L = option_I = option_M = option_d = 1;
+   }
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::printXmlEncodedText -- convert
-//    & to &amp;
-//    " to &quot;
-//    ' to &spos;
-//    < to &lt;
-//    > to &gt;
+// Tool_rid::processFile --
 //
 
-void Tool_prange::printXmlEncodedText(ostream& out, const string& strang) {
-	HumRegex hre;
-	string astring = strang;
+void Tool_rid::processFile(HumdrumFile& infile) {
+	int setcount = 1; // disabled for now.
 
-	hre.replaceDestructive(astring, "&",  "&amp;",  "g");
-	hre.replaceDestructive(astring, "'",  "&apos;", "g");
-	hre.replaceDestructive(astring, "\"", "&quot;", "g");
-	hre.replaceDestructive(astring, "<",  "&lt;",   "g");
-	hre.replaceDestructive(astring, ">",  "&gt;",   "g");
+   HumRegex hre;
+   int revQ = option_V;
+
+   // if bibliographic/reference records are not suppressed
+   // print the !!!!SEGMENT: marker if present.
+   if ((setcount > 1) && (!option_G)) {
+      infile.printNonemptySegmentLabel(m_humdrum_text);
+   }
+
+   for (int i=0; i<infile.getLineCount(); i++) {
+      if (option_D && (infile[i].isBarline() || infile[i].isData())) {
+         // remove data lines if -D is specified
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_d) {
+         // remove null data lines if -d is specified
+         if (option_k && infile[i].isData() &&
+               infile[i].equalFieldsQ("**kern", ".")) {
+            // remove if only all **kern spines are null.
+            if (revQ) {
+               m_humdrum_text << infile[i] << "\n";
+            }
+            continue;
+         } else if (!option_k && infile[i].isData() &&
+               infile[i].isAllNull()) {
+            // remove null data lines if all spines are null.
+            if (revQ) {
+               m_humdrum_text << infile[i] << "\n";
+            }
+            continue;
+         }
+      }
+      if (option_G && (infile[i].isGlobalComment() ||
+            infile[i].isReference())) {
+         // remove global comments if -G is specified
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_g && hre.search(infile.token(i, 0), "^!!+\\s*$")) {
+         // remove empty global comments if -g is specified
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_I && infile[i].isInterpretation()) {
+         // remove all interpretation records
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_i && infile[i].isInterpretation() &&
+            infile[i].isAllNull()) {
+         // remove null interpretation records
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_L && infile[i].isLocalComment()) {
+         // remove all local comments
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_l && infile[i].isLocalComment() &&
+            infile[i].isAllNull()) {
+         // remove null local comments
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_T && (infile[i].isInterpretation() && !infile[i].isManipulator())) {
+         // remove tandem (non-manipulator) interpretations
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_U) {
+         // remove unnecessary (duplicate exclusive) interpretations
+         // HumdrumFile class does not allow duplicate ex. interps.
+         // continue;
+      }
+
+      // non-classical options:
+
+      if (option_M && infile[i].isBarline()) {
+         // remove all measure lines
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_C && infile[i].isComment()) {
+         // remove all comments (local & global)
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
+      if (option_c && (infile[i].isLocalComment() ||
+            infile[i].isGlobalComment())) {
+         // remove all comments (local & global)
+         if (revQ) {
+            m_humdrum_text << infile[i] << "\n";
+         }
+         continue;
+      }
 
-	out << astring;
+      // got past all test, so print the current line:
+      if (!revQ) {
+         m_humdrum_text << infile[i] << "\n";
+      }
+   }
 }
 
 
 
-//////////////////////////////
+
+
+/////////////////////////////////
 //
-// Tool_prange::printScoreFile --
+// Tool_rphrase::Tool_rphrase -- Set the recognized options for the tool.
 //
 
-void Tool_prange::printScoreFile(ostream& out, vector<_VoiceInfo>& voiceInfo, HumdrumFile& infile) {
-	string titlestring = getTitle();
-
-	if (m_defineQ) {
-		out << "#define SVG t 1 1 \\n_99%svg%\n";
-	}
+Tool_rphrase::Tool_rphrase(void) {
+	define("a|average=b",            "calculate average length of rest-phrases by score");
+	define("A|all-average=b",        "calculate average length of rest-phrases for all scores");
+	define("B|no-breath=b",          "ignore breath interpretations");
+	define("c|composite|collapse=b", "collapse all voices into single part");
+	define("d|duration-unit=d:2.0",  "duration units, default: 2.0 (minims/half notes)");
+	define("f|filename=b",           "include filename in output analysis");
+	define("F|full-filename=b",      "include full filename location in output analysis");
+	define("I|no-info=b",            "do not display summary info");
+	define("l|longa=b",              "display minim length of longas");
+	define("m|b|measure|barline=b",  "include barline numbers in output analysis");
+	define("mark=b",                 "mark starts of phrases in score");
+	define("s|sort=b",               "sort phrases by short to long length");
+	define("S|reverse-sort=b",       "sort phrases by long to short length");
+	define("u|url-type=s",           "URL type (jrp, 1520s) for hyperlink");
+	define("z|squeeze=b",            "squeeze notation");
+	define("close=b",                "close details element initially");
+}
 
-	string acctext = "g.bar.doubleflat path&#123;color:darkorange;stroke:darkorange;&#125;g.bar.flat path&#123;color:brown;stroke:brown;&#125;g.bar.sharp path&#123;color:royalblue;stroke:royalblue;&#125;g.bar.doublesharp path&#123;color:aquamarine;stroke:aquamarine;&#125;";
-	string hovertext = ".bar:hover path&#123;fill:red;color:red;stroke:red &#33;important&#125;";
-	string hoverfilltext = hovertext;
 
-	string text1 = "<style>";
-	text1 += hoverfilltext;
-	if (m_accQ) {
-		text1 += acctext;
-	}
-	text1 += "g.labeltext&#123;color:gray;&#125;";
-	text1 += "g.lastnote&#123;color:gray;&#125;";
-	if (m_extremaQ) {
-		text1 += "g.highest-pitch&#123;color:hotpink;&#125;";
-		text1 += "g.lowest-pitch&#123;color:limegreen;&#125;";
-	}
-	text1 += "</style>";
-	string text2 = text1;
 
+/////////////////////////////////
+//
+// Tool_rphrase::run -- Do the main work of the tool.
+//
 
-	// print CSS style information if requested
-	if (m_hoverQ) {
-		SVGTEXT(out, text1);
+bool Tool_rphrase::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
+}
 
-	int maxStaffPosition = getMaxStaffPosition(voiceInfo);
-
-	if (!titlestring.empty()) {
-		// print title
-		int vpos = 54;
-		if (maxStaffPosition > 12) {
-			vpos = maxStaffPosition + 3;
-		}
-		out << "t 2 10 ";
-		out << vpos;
-		out << " 1 1 0 0 0 0 -1.35\n";
-		// out << "_03";
-		printScoreEncodedText(out, titlestring);
-		out << "\n";
-	}
 
-	// print duration label if duration weighting is being used
-	SVGTEXT(out, "<g class=\"labeltext\">");
-	if (m_durationQ) {
-		out << "t 2 185.075 14 1 0.738 0 0 0 0 0\n";
-		out << "_00(durations)\n";
+bool Tool_rphrase::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
 	} else {
-		out << "t 2 185.075 14 1 0.738 0 0 0 0 0\n";
-		out << "_00(attacks)\n";
+		out << infile;
 	}
-	SVGTEXT(out, "</g>");
+	return status;
+}
 
-	// print staff lines
-	out << "8 1 0 0 0 200\n";   // staff 1
-	out << "8 2 0 -6 0 200\n";   // staff 2
 
-	int keysig = getKeySignature(infile);
-	// print key signature
-	if (keysig) {
-		out << "17 1 10 0 " << keysig << " 101.0";
-		printKeySigCompression(out, keysig, 0);
-		out << endl;
-		out << "17 2 10 0 " << keysig;
-		printKeySigCompression(out, keysig, 1);
-		out << endl;
+bool Tool_rphrase::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
+	return status;
+}
 
-	// print barlines
-	out << "14 1 0 2\n";         // starting barline
-	out << "14 1 200 2\n";       // ending barline
-	out << "14 1 0 2 8\n";       // curly brace at start
-
-	// print clefs
-	out << "3 2 2\n";            // treble clef
-	out << "3 1 2 0 1\n";        // bass clef
-
-	assignHorizontalPosition(voiceInfo, 25.0, 170.0);
 
-	double maxvalue = 0.0;
-	for (int i=1; i<(int)voiceInfo.size(); i++) {
-		double tempvalue = getMaxValue(voiceInfo.at(i).diatonic);
-		if (tempvalue > maxvalue) {
-			maxvalue = tempvalue;
-		}
-	}
-	for (int i=(int)voiceInfo.size()-1; i>0; i--) {
-		if (voiceInfo.at(i).kernQ) {
-			printScoreVoice(out, voiceInfo.at(i), maxvalue);
-		}
-	}
-	if (m_allQ) {
-		printScoreVoice(out, voiceInfo.at(0), maxvalue);
-	}
+bool Tool_rphrase::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_prange::getMaxStaffPosition(vector<_VoiceInfo>& voiceinfo) {
+// Tool_rphrase::finally --
 //
 
-int Tool_prange::getMaxStaffPosition(vector<_VoiceInfo>& voiceInfo) {
-	int maxi = getMaxDiatonicIndex(voiceInfo[0].diatonic);
-	int maxdiatonic = maxi - 3 * 7;
-	int staffline = maxdiatonic - 27;
-	return staffline;
+void Tool_rphrase::finally(void) {
+	if (!m_markQ) {
+		if (m_allAverageQ) {
+			if (m_compositeQ) {
+				double average = m_sumComposite / m_pcountComposite;
+				m_free_text << "Composite average phrase length: " << average << " minims" << endl;
+			} else {
+				double average = m_sum / m_pcount;
+				m_free_text << "All average phrase length: " << average << " minims" << endl;
+			}
+		}
+	}
 }
 
 
 
-
 //////////////////////////////
 //
-// Tool_prange::printKeySigCompression --
+// Tool_rphrase::initialize --
 //
 
-void Tool_prange::printKeySigCompression(ostream& out, int keysig, int extra) {
-	double compression = 0.0;
-	switch (abs(keysig)) {
-		case 0: compression = 0.0; break;
-		case 1: compression = 0.0; break;
-		case 2: compression = 0.0; break;
-		case 3: compression = 0.0; break;
-		case 4: compression = 0.9; break;
-		case 5: compression = 0.8; break;
-		case 6: compression = 0.7; break;
-		case 7: compression = 0.6; break;
-	}
-	if (compression <= 0.0) {
-		return;
-	}
-	for (int i=0; i<extra; i++) {
-		out << " 0";
-	}
-	out << " " << compression;
+void Tool_rphrase::initialize(void) {
+	m_barlineQ      = getBoolean("measure");
+	m_allAverageQ   = getBoolean("all-average");
+	m_breathQ       = !getBoolean("no-breath");
+	m_compositeQ    = getBoolean("collapse");
+	m_filenameQ     = getBoolean("filename");
+	m_fullFilenameQ = getBoolean("full-filename");
+	m_urlType       = getString("url-type");
+	m_longaQ        = getBoolean("longa");
+	#ifndef __EMSCRIPTEN__
+		m_markQ         = getBoolean("mark");
+		m_averageQ      = getBoolean("average");
+	#else
+		m_markQ         = !getBoolean("mark");
+		m_averageQ      = !getBoolean("average");
+	#endif
+	m_sortQ         = getBoolean("sort");
+	m_reverseSortQ  = getBoolean("reverse-sort");
+	m_durUnit       = getDouble("duration-unit");
+	m_infoQ         = !getDouble("no-info");
+	m_squeezeQ      = getBoolean("squeeze");
+	m_closeQ        = getBoolean("close");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::assignHorizontalPosition --
+// Tool_rphrase::processFile --
 //
 
-void Tool_prange::assignHorizontalPosition(vector<_VoiceInfo>& voiceInfo, int minval, int maxval) {
-	int count = 0;
-	for (int i=1; i<(int)voiceInfo.size(); i++) {
-		if (voiceInfo[i].kernQ) {
-			count++;
-		}
+void Tool_rphrase::processFile(HumdrumFile& infile) {
+	if (m_filenameQ) {
+		m_filename = infile.getFilename();
+		HumRegex hre;
+		hre.replaceDestructive(m_filename, "", ".*\\/");
+		hre.replaceDestructive(m_filename, "", "\\.krn$");
+	} else if (m_fullFilenameQ) {
+		m_filename = infile.getFilename();
 	}
-	if (m_allQ) {
-		count++;
+	vector<HTp> kernStarts = infile.getKernSpineStartList();
+	vector<Tool_rphrase::VoiceInfo> voiceInfo(kernStarts.size());
+	Tool_rphrase::VoiceInfo compositeInfo;
+
+	if (m_compositeQ) {
+		fillCompositeInfo(compositeInfo, infile);
+	} else {
+		fillVoiceInfo(voiceInfo, kernStarts, infile);
 	}
 
-	vector<double> hpos(count, 0);
-	hpos[0] = maxval;
-	hpos.back() = minval;
+	if (m_longaQ) {
+		markLongaDurations(infile);
+	}
 
-	if (hpos.size() > 2) {
-		for (int i=1; i<(int)hpos.size()-1; i++) {
-			int ii = hpos.size() - i - 1;
-			hpos[i] = (double)ii / (hpos.size()-1) * (maxval - minval) + minval;
+	if ((!m_allAverageQ) && (!m_markQ)) {
+		if (m_line == 1) {
+			if (m_compositeQ) {
+				m_free_text << "Filename";
+				if (!m_urlType.empty()) {
+					m_free_text << "\tVHV";
+				}
+				m_free_text << "\tVoice";
+				m_free_text << "\tComp seg count";
+				if (m_averageQ) {
+					m_free_text << "\tAvg comp seg dur";
+				}
+				m_free_text << "\tComposite seg durs";
+				m_free_text << endl;
+			} else {
+				m_free_text << "Filename";
+				if (!m_urlType.empty()) {
+					m_free_text << "\tVHV";
+				}
+				m_free_text << "\tVoice";
+				m_free_text << "\tSounding dur";
+				m_free_text << "\tResting dur";
+				m_free_text << "\tTotal dur";
+				m_free_text << "\tSeg count";
+				if (m_averageQ) {
+					m_free_text << "\tSeg dur average";
+				}
+				m_free_text << "\tSegment durs";
+				m_free_text << endl;
+			}
+		}
+		if (m_compositeQ) {
+			if (m_compositeQ) {
+				m_line++;
+			}
+			printVoiceInfo(compositeInfo);
+		} else {
+			printVoiceInfo(voiceInfo);
 		}
 	}
 
-	int position = 0;
-	if (m_allQ) {
-		position = 1;
-		voiceInfo[0].hpos = hpos[0];
-	}
-	for (int i=0; i<(int)voiceInfo.size(); i++) {
-		if (voiceInfo.at(i).kernQ) {
-			voiceInfo.at(i).hpos = hpos.at(position++);
+	if (m_markQ) {
+		outputMarkedFile(infile, voiceInfo, compositeInfo);
+		if (m_squeezeQ) {
+			m_humdrum_text << "!!!verovio: evenNoteSpacing" << endl;
 		}
 	}
+
 }
 
 
 
-//////////////////////////////
+//////////////////////////
 //
-// Tool_prange::getKeySignature -- find first key signature in file.
+// Tool_rphrase::markLongaDuratios --
 //
 
-int Tool_prange::getKeySignature(HumdrumFile& infile) {
+void Tool_rphrase::markLongaDurations(HumdrumFile& infile) {
+	string longrdf;
 	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			if (infile[i].isData()) {
-				break;
-			}
+		if (infile[i].hasSpines()) {
 			continue;
 		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isKeySignature()) {
-				return Convert::kernKeyToNumber(*token);
-			}
+		if (!infile[i].isReferenceRecord()) {
+			continue;
+		}
+		string key = infile[i].getReferenceKey();
+		if (key != "RDF**kern") {
+			continue;
+		}
+		string value = infile[i].getReferenceValue();
+		HumRegex hre;
+		if (hre.search(value, "^\\s*([^\\s=]+)\\s*=.*long")) {
+			longrdf = hre.getMatch(1);
+			break;
 		}
 	}
 
-	return 0; // C major key signature
-}
-
-
-
-//////////////////////////////
-//
-// Tool_prange::printScoreVoice -- print the range information for a particular voice (in SCORE format).
-//
-
-void Tool_prange::printScoreVoice(ostream& out, _VoiceInfo& voiceInfo, double maxvalue) {
-	int mini = getMinDiatonicIndex(voiceInfo.diatonic);
-	int maxi = getMaxDiatonicIndex(voiceInfo.diatonic);
-
-	if ((mini < 0) || (maxi < 0)) {
-		// no data for voice so skip
+	if (longrdf.empty()) {
 		return;
 	}
 
-	// int minacci = getMinDiatonicAcc(voiceInfo.diatonic, mini);
-	// int maxacci = getMaxDiatonicAcc(voiceInfo.diatonic, maxi);
-	int mindiatonic = mini - 3 * 7;
-	int maxdiatonic = maxi - 3 * 7;
-	// int minacc = minacci - 3;
-	// int maxacc = maxacci - 3;
-
-	int    staff;
-	double vpos;
-
-	int voicevpos = -3;
-	staff = getStaffBase7(mindiatonic);
-	int lowestvpos = getVpos(mindiatonic);
-	if ((staff == 1) && (lowestvpos <= 0)) {
-		voicevpos += lowestvpos - 2;
-	}
-
-	if (m_localQ || (voiceInfo.index == 0)) {
-		double localmaxvalue = getMaxValue(voiceInfo.diatonic);
-		maxvalue = localmaxvalue;
-	}
-	double width;
-	double hoffset = 2.3333;
-	double maxhist = 17.6;
-	int i;
-	int base7;
-
-	// print histogram bars
-	for (i=mini; i<=maxi; i++) {
-		if (voiceInfo.diatonic.at(i).at(0) <= 0.0) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
 			continue;
 		}
-		base7 = i - 3 * 7;
-		staff = getStaffBase7(base7);
-		vpos  = getVpos(base7);
-
-		// staring positions of accidentals:
-		vector<double> starthpos(6, 0.0);
-		for (int j=1; j<(int)starthpos.size(); j++) {
-			double width = maxhist * voiceInfo.diatonic.at(i).at(j)/maxvalue;
-			starthpos[j] = starthpos[j-1] + width;
-		}
-		for (int j=(int)starthpos.size() - 1; j>0; j--) {
-			starthpos[j] = starthpos[j-1];
+		if (!infile[i].isData()) {
+			continue;
 		}
-
-		// print chromatic alterations
-		for (int j=(int)voiceInfo.diatonic.at(i).size()-1; j>0; j--) {
-			if (voiceInfo.diatonic.at(i).at(j) <= 0.0) {
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
 				continue;
 			}
-			int acc = 0;
-			switch (j) {
-				case 1: acc = -2; break;
-				case 2: acc = -1; break;
-				case 3: acc =  0; break;
-				case 4: acc = +1; break;
-				case 5: acc = +2; break;
-			}
-
-			width = maxhist * voiceInfo.diatonic.at(i).at(j)/maxvalue + hoffset;
-			if (m_hoverQ) {
-				string title = getNoteTitle((int)voiceInfo.diatonic.at(i).at(j), base7, acc);
-				SVGTEXT(out, title);
-			}
-			out << "1 " << staff << " " << (voiceInfo.hpos + starthpos.at(j) + hoffset) << " " << vpos;
-			out << " 0 -1 4 0 0 0 99 0 0 ";
-			out << width << "\n";
-			if (m_hoverQ) {
-				SVGTEXT(out, "</g>");
-			}
-		}
-	}
-
-	string voicestring = voiceInfo.name;
-	if (voicestring.empty()) {
-		voicestring = voiceInfo.abbr;
-	}
-	if (!voicestring.empty()) {
-		HumRegex hre;
-		hre.replaceDestructive(voicestring, "", "(\\\\n)+$");
-		vector<string> pieces;
-		hre.split(pieces, voicestring, "\\\\n");
-
-		if (pieces.size() > 1) {
-			voicestring = "";
-			for (int i=0; i<(int)pieces.size(); i++) {
-				voicestring += pieces[i];
-				if (i < (int)pieces.size() - 1) {
-					voicestring += "/";
-				}
-			}
-		}
-
-		double increment = 4.0;
-		for (int i=0; i<(int)pieces.size(); i++) {
-			// print voice name
-			double tvoffset = -4.0;
-			out << "t 1 " << voiceInfo.hpos << " "
-				<< (voicevpos - increment * i)
-			  	<< " 1 1 0 0 0 0 " << tvoffset;
-			out << "\n";
-
-			if (pieces[i] == "all") {
-				out << "_02";
-			} else if (pieces[i] == "both") {
-				out << "_02";
-			} else {
-				out << "_00";
+			if (token->find(longrdf) != string::npos) {
+				HumNum duration = token->getTiedDuration();
+				stringstream value;
+				value.str("");
+				value << duration.getFloat() / m_durUnit;
+				token->setValue("auto", "rphrase-longa", value.str());
 			}
-			printScoreEncodedText(out, pieces[i]);
-			out << "\n";
-		}
-	}
-
-	// print the lowest pitch in range
-	staff = getStaffBase7(mindiatonic);
-	vpos = getVpos(mindiatonic);
-	if (m_hoverQ) {
-		string content = "<g class=\"lowest-pitch\"><title>";
-		content += getDiatonicPitchName(mindiatonic, 0);
-		content += ": lowest note";
-		if (!voicestring.empty()) {
-			content += " of ";
-			content += voicestring;
-			content += "'s range";
-		}
-		content += "</title>";
-		SVGTEXT(out, content);
-	}
-	out << "1 " << staff << " " << voiceInfo.hpos << " " << vpos
-		  << " 0 0 4 0 0 -2\n";
-	if (m_hoverQ) {
-		SVGTEXT(out, "</g>");
-	}
-
-	// print the highest pitch in range
-	staff = getStaffBase7(maxdiatonic);
-	vpos = getVpos(maxdiatonic);
-	if (m_hoverQ) {
-		string content = "<g class=\"highest-pitch\"><title>";
-		content += getDiatonicPitchName(maxdiatonic, 0);
-		content += ": highest note";
-		if (!voicestring.empty()) {
-			content += " of ";
-			content += voicestring;
-			content += "'s range";
-		}
-		content += "</title>";
-		SVGTEXT(out, content);
-	}
-	out << "1 " << staff << " " << voiceInfo.hpos << " " << vpos
-		  << " 0 0 4 0 0 -2\n";
-	if (m_hoverQ) {
-		SVGTEXT(out, "</g>");
-	}
-
-	double goffset  = -1.66;
-	double toffset  = 1.5;
-	double median12 = getMedian12(voiceInfo.midibins);
-	double median40 = Convert::base12ToBase40(median12);
-	double median7  = Convert::base40ToDiatonic(median40);
-	// int    acc      = Convert::base40ToAccidental(median40);
+		}
+	}
+}
 
-	staff = getStaffBase7(median7);
-	vpos = getVpos(median7);
 
-	// these offsets are useful when the quartile pitches are not shown...
-	int vvpos = maxdiatonic - median7 + 1;
-	int vvpos2 = median7 - mindiatonic + 1;
-	double offset = goffset;
-	if (vvpos <= 2) {
-		offset += toffset;
-	} else if (vvpos2 <= 2) {
-		offset -= toffset;
-	}
 
-	if (m_hoverQ) {
-		string content = "<g><title>";
-		content += getDiatonicPitchName(median7, 0);
-		content += ": median note";
-		if (!voicestring.empty()) {
-			content += " of ";
-			content += voicestring;
-			content += "'s range";
+//////////////////////////////
+//
+// Tool_rphrase::outputMarkedFile --
+//
+
+void Tool_rphrase::outputMarkedFile(HumdrumFile& infile, vector<Tool_rphrase::VoiceInfo>& voiceInfo,
+		Tool_rphrase::VoiceInfo& compositeInfo) {
+	m_free_text.clear();
+	m_free_text.str("");
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			m_humdrum_text << infile[i] << endl;
+		} else {
+			printDataLine(infile, i);
 		}
-		content += "</title>";
-		SVGTEXT(out, content);
-	}
-	out << "1 " << staff << " " << voiceInfo.hpos << " ";
-	if (vpos > 0) {
-		out << vpos + 100;
-	} else {
-		out << vpos - 100;
 	}
-	out << " 0 1 4 0 0 " << offset << "\n";
-	if (m_hoverQ) {
-		SVGTEXT(out, "</g>");
+
+	if (m_infoQ) {
+		printEmbeddedVoiceInfo(voiceInfo, compositeInfo, infile);
 	}
+}
 
-	if (m_finalisQ) {
-		for (int f=0; f<(int)voiceInfo.diafinal.size(); f++) {
-			int diafinalis = voiceInfo.diafinal.at(f);
-			int accfinalis = voiceInfo.accfinal.at(f);
-			int staff = getStaffBase7(diafinalis);
-			int vpos = getVpos(diafinalis);
-			double goffset = -1.66;
-			double toffset = 3.5;
 
-			// these offsets are useful when the quartile pitches are not shown...
-			double offset = goffset;
-			offset += toffset;
 
-			if (m_hoverQ) {
-				string content = "<g class=\"lastnote\"><title>";
-				content += getDiatonicPitchName(diafinalis, accfinalis);
-				content += ": last note";
-				if (!voicestring.empty()) {
-					content += " of ";
-					if (voiceInfo.index == 0) {
-						content += voiceInfo.namfinal.at(f);
-					} else {
-						content += voicestring;
-					}
-				}
-				content += "</title>";
-				SVGTEXT(out, content);
-			}
-			out << "1 " << staff << " " << voiceInfo.hpos << " ";
-			if (vpos > 0) {
-				out << vpos + 100;
-			} else {
-				out << vpos - 100;
+//////////////////////////////
+//
+// Tool_rphrase::printDataLine --
+//
+
+void Tool_rphrase::printDataLine(HumdrumFile& infile, int index) {
+
+	bool hasLonga = false;
+	if (m_longaQ) {
+		for (int j=0; j<infile[index].getFieldCount(); j++) {
+			HTp token = infile.token(index, j);
+			if (!token->isKern()) {
+				continue;
 			}
-			out << " 0 0 4 0 0 " << offset << "\n";
-			if (m_hoverQ) {
-				SVGTEXT(out, "</g>");
+			string lotext = token->getValue("auto", "rphrase-longa");
+			if (!lotext.empty()) {
+				hasLonga = true;
+				break;
 			}
 		}
 	}
 
-	/* Needs fixing
-	int topquartile;
-	if (m_quartileQ) {
-		// print top quartile
-		topquartile = getTopQuartile(voiceInfo.midibins);
-		if (m_diatonicQ) {
-			topquartile = Convert::base7ToBase12(topquartile);
+
+	bool hasLo = false;
+	for (int j=0; j<infile[index].getFieldCount(); j++) {
+		HTp token = infile.token(index, j);
+		if (!token->isKern()) {
+			continue;
 		}
-		staff = getStaffBase7(topquartile);
-		vpos = getVpos(topquartile);
-		vvpos = median7 - topquartile + 1;
-		if (vvpos <= 2) {
-			offset = goffset + toffset;
-		} else {
-			offset = goffset;
+		string lotext = token->getValue("auto", "rphrase-start");
+		if (!lotext.empty()) {
+			hasLo = true;
+			break;
 		}
-		vvpos = maxdiatonic - topquartile + 1;
-		if (vvpos <= 2) {
-			offset = goffset + toffset;
+	}
+
+	// search for composite phrase info
+	bool hasGlo = false;
+	if (infile[index].isData()) {
+		string glotext = infile[index].getValue("auto", "rphrase-composite-start");
+		if (!glotext.empty()) {
+			hasGlo = true;
 		}
+	}
 
-		if (m_hoverQ) {
-			if (m_defineQ) {
-				out << "SVG ";
+	if (hasGlo) {
+		string glotext = infile[index].getValue("auto", "rphrase-composite-start");
+		m_humdrum_text << "!!LO:TX:b:B:color=" << m_compositeLengthColor << ":t=" << glotext << endl;
+	}
+
+	if (hasLonga) {
+		for (int j=0; j<infile[index].getFieldCount(); j++) {
+			HTp token = infile.token(index, j);
+			if (!token->isKern()) {
+				m_humdrum_text << "!";
 			} else {
-				out << "t 1 1\n";
-				out << SVGTAG;
+				string value = token->getValue("auto", "rphrase-longa");
+				if (value.empty()) {
+					m_humdrum_text << "!";
+				} else {
+					m_humdrum_text << "!LO:TX:a:B:color=silver:t=" << value;
+				}
 			}
-			printScoreEncodedText(out, "<g><title>");
-			printDiatonicPitchName(out, topquartile, 0);
-			out << ": top quartile note";
-			if (voicestring.size() > 0) {
-				out <<  " of " << voicestring << "\'s range";
+			if (j < infile[index].getFieldCount() - 1) {
+				m_humdrum_text << "\t";
 			}
-			printScoreEncodedText(out, "</title>\n");
-		}
-		out << "1 " << staff << " " << voiceInfo.hpos << " ";
-		if (vpos > 0) {
-			out << vpos + 100;
-		} else {
-			out << vpos - 100;
-		}
-		out << " 0 0 4 0 0 " << offset << "\n";
-		if (m_hoverQ) {
-			SVGTEXT(out, "</g>");
 		}
+		m_humdrum_text << endl;
 	}
 
-	// print bottom quartile
-	if (m_quartileQ) {
-		int bottomquartile = getBottomQuartile(voiceInfo.midibins);
-		if (m_diatonicQ) {
-			bottomquartile = Convert::base7ToBase12(bottomquartile);
-		}
-		staff = getStaffBase7(bottomquartile);
-		vpos = getVpos(bottomquartile);
-		vvpos = median7 - bottomquartile + 1;
-		if (vvpos <= 2) {
-			offset = goffset + toffset;
-		} else {
-			offset = goffset;
-		}
-		vvpos = bottomquartile - mindiatonic + 1;
-		if (vvpos <= 2) {
-			offset = goffset - toffset;
-		}
-		if (m_hoverQ) {
-			if (m_defineQ) {
-				out << "SVG ";
+	if (hasLo) {
+		for (int j=0; j<infile[index].getFieldCount(); j++) {
+			HTp token = infile.token(index, j);
+			if (!token->isKern()) {
+				m_humdrum_text << "!";
 			} else {
-				out << "t 1 1\n";
-				out << SVGTAG;
+				string value = token->getValue("auto", "rphrase-start");
+				if (value.empty()) {
+					m_humdrum_text << "!";
+				} else {
+					m_humdrum_text << "!LO:TX:a:B:color=" << m_voiceLengthColor << ":t=" << value;
+				}
 			}
-			printScoreEncodedText(out, "<g><title>");
-			printDiatonicPitchName(out, bottomquartile, 0);
-			out << ": bottom quartile note";
-			if (voicestring.size() > 0) {
-				out <<  " of " << voicestring << "\'s range";
+			if (j < infile[index].getFieldCount() - 1) {
+				m_humdrum_text << "\t";
 			}
-			printScoreEncodedText(out, "</title>\n");
-		}
-		out << "1.0 " << staff << ".0 " << voiceInfo.hpos << " ";
-		if (vpos > 0) {
-			out << vpos + 100;
-		} else {
-			out << vpos - 100;
-		}
-		out << " 0 0 4 0 0 " << offset << "\n";
-		if (m_hoverQ) {
-			SVGTEXT(out, "</g>");
 		}
+		m_humdrum_text << endl;
 	}
-	*/
 
+	m_humdrum_text << infile[index] << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::printDiatonicPitchName --
+// Tool_rphrase::getCompositeStates --
 //
 
-void Tool_prange::printDiatonicPitchName(ostream& out, int base7, int acc) {
-	out << getDiatonicPitchName(base7, acc);
+void Tool_rphrase::getCompositeStates(vector<int>& noteStates, HumdrumFile& infile) {
+	noteStates.resize(infile.getLineCount());
+	fill(noteStates.begin(), noteStates.end(), -1);
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
+		}
+		int value = 0;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isRest()) {
+				continue;
+			} else if (token->isNull()) {
+				HTp resolve = token->resolveNull();
+				if (!resolve) {
+					continue;
+				} else if (resolve->isRest()) {
+					continue;
+				} else {
+					value = 1;
+					break;
+				}
+			} else {
+				value = 1;
+				break;
+			}
+		}
+		noteStates[i] = value;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getDiatonicPitchName --
+// Tool_rphrase::printVoiceInfo --
 //
 
-string Tool_prange::getDiatonicPitchName(int base7, int acc) {
-	string output;
-	int dpc = base7 % 7;
-	char letter = (dpc + 2) % 7 + 'A';
-	output += letter;
-	switch (acc) {
-		case -1: output += "&#9837;"; break;
-		case +1: output += "&#9839;"; break;
-		case -2: output += "&#119083;"; break;
-		case +2: output += "&#119082;"; break;
+void Tool_rphrase::printVoiceInfo(vector<Tool_rphrase::VoiceInfo>& voiceInfo) {
+	for (int i=(int)voiceInfo.size() - 1; i>=0; i--) {
+		if (!m_compositeQ) {
+			m_line++;
+		}
+		printVoiceInfo(voiceInfo[i]);
+	}
+}
+
+
+//////////////////////////////
+//
+// Tool_rphrase::printHyperlink --
+//
+
+void Tool_rphrase::printHyperlink(const string& urlType) {
+	string command = "rphrase";
+	string options;
+	options += "l";
+	options+= "z";
+	if (m_compositeQ) {
+		options += "c";
+	}
+	if (m_sortQ) {
+		options += "s";
+	} else if (m_reverseSortQ) {
+		options += "S";
+	}
+	if (!options.empty()) {
+		command += "%20-";
+		command += options;
+	}
+
+	if (urlType == "jrp") {
+		m_free_text << "=HYPERLINK(\"https://verovio.humdrum.org/?file=jrp/\" & ";
+		m_free_text << "LEFT(A" << m_line << ", 3) & \"/\" & A" << m_line << " & ";
+		m_free_text << "\".krn&filter=" << command << "&k=ey\", LEFT(A" << m_line;
+		m_free_text << ", FIND(\"-\", A" << m_line << ") - 1))";
+	} else if (urlType == "1520s") {
+		m_free_text << "=HYPERLINK(\"https://verovio.humdrum.org/?file=1520s/\" & A";
+		m_free_text << m_line << " & \".krn&filter=" << command << "&k=ey\", LEFT(A";
+		m_free_text << m_line << ", FIND(\"-\", A" << m_line << ") - 1))";
 	}
-	int octave = base7 / 7;
-	output += to_string(octave);
-	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::printHtmlStringEncodeSimple --
+// Tool_rphrase::printVoiceInfo --
 //
 
-void Tool_prange::printHtmlStringEncodeSimple(ostream& out, const string& strang) {
-	string newstring = strang;
-	HumRegex hre;
-	hre.replaceDestructive(newstring, "&", "&amp;", "g");
-	hre.replaceDestructive(newstring, "<", "&lt;", "g");
-	hre.replaceDestructive(newstring, ">", "&lt;", "g");
-	out << newstring;
+void Tool_rphrase::printVoiceInfo(Tool_rphrase::VoiceInfo& voiceInfo) {
+	if (m_filenameQ) {
+		m_free_text << m_filename << "\t";
+	}
+	if (!m_urlType.empty()) {
+		printHyperlink(m_urlType);
+		m_free_text << "\t";
+	}
+	m_free_text << voiceInfo.name << "\t";
+
+	if (!m_compositeQ) {
+		double sounding = 0.0;
+		double resting = 0.0;
+		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+			if (voiceInfo.phraseDurs[i] > 0.0) {
+				sounding += voiceInfo.phraseDurs[i];
+			}
+			if (voiceInfo.restsBefore[i] > 0.0) {
+				resting += voiceInfo.restsBefore[i];
+			}
+		}
+		double total = sounding + resting;
+		// double sounding_percent = int (sounding/total * 100.0 + 0.5);
+		// double resting_percent = int (sounding/total * 100.0 + 0.5);
+
+		m_free_text << twoDigitRound(sounding) << "\t";
+		m_free_text << twoDigitRound(resting) << "\t";
+		m_free_text << twoDigitRound(total) << "\t";
+	}
+
+	m_free_text << voiceInfo.phraseDurs.size() << "\t";
+
+	if (m_averageQ) {
+		double sum = 0;
+		int count = 0;
+		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+			count++;
+			sum += voiceInfo.phraseDurs.at(i);
+		}
+		m_free_text << int(sum / count * 100.0 + 0.5)/100.0 << "\t";
+	}
+
+	if (m_sortQ || m_reverseSortQ) {
+		vector<pair<double, int>> sortList;
+		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+			sortList.emplace_back(voiceInfo.phraseDurs[i], i);
+		}
+		if (m_sortQ) {
+			sort(sortList.begin(), sortList.end(),
+				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
+					return a.first < b.first;
+			});
+		} else if (m_reverseSortQ) {
+			sort(sortList.begin(), sortList.end(),
+				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
+					return a.first > b.first;
+			});
+		}
+
+		for (int i=0; i<(int)sortList.size(); i++) {
+			int ii = sortList[i].second;
+			if (m_barlineQ) {
+				m_free_text << "m" << voiceInfo.barStarts.at(ii) << ":";
+			}
+			m_free_text << twoDigitRound(voiceInfo.phraseDurs.at(ii));
+			if (i < (int)sortList.size() - 1) {
+				m_free_text << " ";
+			}
+		}
+	} else {
+		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+			if (voiceInfo.restsBefore.at(i) > 0) {
+				m_free_text << "r:" << twoDigitRound(voiceInfo.restsBefore.at(i)) << " ";
+			} else if (i > 0) {
+				// force display r:0 for section boundaries.
+				m_free_text << "r:" << twoDigitRound(voiceInfo.restsBefore.at(i)) << " ";
+			}
+			if (m_barlineQ) {
+				m_free_text << "m" << voiceInfo.barStarts.at(i) << ":";
+			}
+			m_free_text << twoDigitRound(voiceInfo.phraseDurs.at(i));
+			if (i < (int)voiceInfo.phraseDurs.size() - 1) {
+				m_free_text << " ";
+			}
+		}
+	}
+
+	m_free_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getNoteTitle -- return the title of the histogram bar.
-//    value = duration or count of notes
-//    diatonic = base7 value for note
-//    acc = accidental for diatonic note.
+// Tool_rphrase::printEmbeddedVoiceInfo --
 //
 
-string Tool_prange::getNoteTitle(double value, int diatonic, int acc) {
-	stringstream output;
-	output << "<g class=\"bar";
-	switch (acc) {
-		case -2: output << " doubleflat";  break;
-		case -1: output << " flat";        break;
-		case  0: output << " natural";     break;
-		case +1: output << " sharp";       break;
-		case +2: output << " doublesharp"; break;
+void Tool_rphrase::printEmbeddedVoiceInfo(vector<Tool_rphrase::VoiceInfo>& voiceInfo, Tool_rphrase::VoiceInfo& compositeInfo, HumdrumFile& infile) {
+
+	m_humdrum_text << "!!@@BEGIN: PREHTML" << endl;;
+
+	m_humdrum_text << "!!@SCRIPT:" << endl;
+	m_humdrum_text << "!!   function rphraseGotoMeasure(measure) {" << endl;
+	m_humdrum_text << "!!      let target = `svg .measure.m-${measure}`;" << endl;
+	m_humdrum_text << "!!      let element = document.querySelector(target);" << endl;
+	m_humdrum_text << "!!      if (element) {" << endl;
+	m_humdrum_text << "!!         element.scrollIntoViewIfNeeded({ behavior: 'smooth' });" << endl;
+	m_humdrum_text << "!!     }" << endl;
+	m_humdrum_text << "!!   }" << endl;
+
+	m_humdrum_text << "!!@CONTENT:\n";
+
+	if (m_compositeQ) {
+		m_humdrum_text << "!!<style> .PREHTML .composite ul .rest { color: #ccc; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML .composite ul .measure { color: #ccc; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML .composite ul .length { cursor: pointer; font-weight: bold; color: " << m_compositeLengthColor << "; } </style>" << endl;
+	} else {
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase .rest { color: #ccc; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase .measure { color: #ccc; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase .length { cursor: pointer; font-weight: bold; color: " << m_voiceLengthColor << "; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase { border-collapse: collapse; </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase th, .PREHTML table.rphrase td { vertical-align: top; padding-right: 10px; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase tr { border-bottom: 1px solid #ccc; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase tr th:last-child, .PREHTML table.rphrase tr td:last-child { padding-right: 0; } </style>" << endl;
+
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.average { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.segments { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.sounding { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.resting { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.average { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.segments { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.sounding { text-align: right; } </style>" << endl;
+		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.resting { text-align: right; } </style>" << endl;
 	}
-	output << "\"";
-	output << "><title>";
-	if (m_durationQ) {
-		output << value / 8.0;
-		if (value/8.0 == 1.0) {
-			output << " long on ";
-		} else {
-			output << " longs on ";
+	m_humdrum_text << "!!<style> .PREHTML details { position: relative; padding-left: 20px; } </style>" << endl;
+	m_humdrum_text << "!!<style> .PREHTML summary { font-size: 1.5rem; cursor: pointer; list-style: none; } </style>" << endl;
+	m_humdrum_text << "!!<style> .PREHTML summary::before { content: '▶'; display: inline-block; width: 2em; margin-left: -1.5em; text-align: center; } </style>" << endl;
+	m_humdrum_text << "!!<style> .PREHTML details[open] summary::before { content: '▼'; } </style>" << endl;
+
+	if (m_compositeQ) {
+		m_humdrum_text << "!!<details";
+		if (!m_closeQ) {
+			m_humdrum_text << " open";
 		}
-		output << getDiatonicPitchName(diatonic, acc);
+		m_humdrum_text << "><summary>Composite rest phrasing</summary>\n";
 	} else {
-		output << value;
-		output << " ";
-		output << getDiatonicPitchName(diatonic, acc);
-		if (value != 1.0) {
-			output << "s";
+		m_humdrum_text << "!!<details";
+		if (!m_closeQ) {
+			m_humdrum_text << " open";
+		}
+		m_humdrum_text << "><summary>Voice rest phrasing</summary>\n";
+	}
+	if (m_compositeQ) {
+		printEmbeddedCompositeInfo(compositeInfo, infile);
+	} else {
+		if (voiceInfo.size() > 0) {
+			m_humdrum_text << "!!<table class='rphrase'>" << endl;
+			m_humdrum_text << "!!<tr><th class='voice'>Voice</th><th class='sounding'>Sounding</th><th class='resting'>Resting</th><th class='segments'>Segments</th><th class='average'>Average</th><th class='segment-durations'>Segment durations</th></tr>" << endl;
+			for (int i=(int)voiceInfo.size() - 1; i>=0; i--) {
+				printEmbeddedIndividualVoiceInfo(voiceInfo[i], infile);
+			}
+			m_humdrum_text << "!!</table>" << endl;
+			printEmbeddedVoiceInfoSummary(voiceInfo, infile);
 		}
 	}
-	output << "</title>";
-	return output.str();
+	m_humdrum_text << "!!</details>" << endl;
+	m_humdrum_text << "!!@@END: PREHTML" << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getDiatonicInterval --
+// Tool_rphrase::printEmbeddedCompositeInfo --
 //
 
-int Tool_prange::getDiatonicInterval(int note1, int note2) {
-	int vpos1 = getVpos(note1);
-	int vpos2 = getVpos(note2);
-	return abs(vpos1 - vpos2) + 1;
-}
-
+void Tool_rphrase::printEmbeddedCompositeInfo(Tool_rphrase::VoiceInfo& compositeInfo, HumdrumFile& infile) {
 
+	m_humdrum_text << "!!<div class='composite'>" << endl;
+	m_humdrum_text << "!!<ul>" << endl;
+	m_humdrum_text << "!!<li>Composite segment count: " << compositeInfo.phraseDurs.size() << "</li>" << endl;
 
-//////////////////////////////
-//
-// Tool_prange::getTopQuartile --
-//
+	if (!compositeInfo.phraseDurs.empty()) {
+		m_humdrum_text << "!!<li>Composite segment duration";
+		if (compositeInfo.phraseDurs.size() != 1) {
+			m_humdrum_text << "s";
+		}
+		m_humdrum_text << ": ";
+		if (m_sortQ || m_reverseSortQ) {
+			vector<pair<double, int>> sortList;
+			for (int i=0; i<(int)compositeInfo.phraseDurs.size(); i++) {
+				sortList.emplace_back(compositeInfo.phraseDurs[i], i);
+			}
+			if (m_sortQ) {
+				sort(sortList.begin(), sortList.end(),
+					[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
+						return a.first < b.first;
+				});
+			} else if (m_reverseSortQ) {
+				sort(sortList.begin(), sortList.end(),
+					[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
+						return a.first > b.first;
+				});
+			}
 
-int Tool_prange::getTopQuartile(vector<double>& midibins) {
-	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
+			for (int i=0; i<(int)sortList.size(); i++) {
+				int ii = sortList[i].second;
+				if (m_barlineQ) {
+					m_humdrum_text << "m" << compositeInfo.barStarts.at(ii) << ":";
+				}
+				m_humdrum_text << "<span class='length' title='measure " << compositeInfo.barStarts.at(ii)
+				               << "' onclick='rphraseGotoMeasure(" << compositeInfo.barStarts.at(ii)
+				               << ")' >" << twoDigitRound(compositeInfo.phraseDurs.at(ii)) << "</span>";
+				if (i < (int)sortList.size() - 1) {
+					m_humdrum_text << " ";
+				}
+			}
+		} else {
+			for (int i=0; i<(int)compositeInfo.phraseDurs.size(); i++) {
+				if (compositeInfo.restsBefore.at(i) > 0) {
+					m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(compositeInfo.restsBefore.at(i)) << "</span> ";
+				} else if (i > 0) {
+					// force display r:0 for section boundaries.
+					m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(compositeInfo.restsBefore.at(i)) << "</span> ";
+				}
+				if (m_barlineQ) {
+					m_humdrum_text << "<span class='measure'>m" << compositeInfo.barStarts.at(i) << ":</span>";
+				}
+				m_humdrum_text << "<span class='length' title='measure " << compositeInfo.barStarts.at(i)
+				               << "' onclick='rphraseGotoMeasure(" << compositeInfo.barStarts.at(i)
+				               << ")' >" << twoDigitRound(compositeInfo.phraseDurs.at(i)) << "</span>";
+				if (i < (int)compositeInfo.phraseDurs.size() - 1) {
+					m_humdrum_text << " ";
+				}
+			}
+		}
+		m_humdrum_text << "</li>" << endl;
 
-	double cumsum = 0.0;
-	int i;
-	for (i=midibins.size()-1; i>=0; i--) {
-		if (midibins[i] <= 0.0) {
-			continue;
+		if (m_averageQ && (compositeInfo.phraseDurs.size() > 1)) {
+			double sum = 0;
+			int count = 0;
+			for (int i=0; i<(int)compositeInfo.phraseDurs.size(); i++) {
+				count++;
+				sum += compositeInfo.phraseDurs.at(i);
+			}
+			double average = int(sum / count * 100.0 + 0.5)/100.0;
+			m_humdrum_text << "!!<li>Average composite segment durations: " << average << "</li>" << endl;
 		}
-		cumsum += midibins[i]/sum;
-		if (cumsum >= 0.25) {
-			return i;
+
+		m_humdrum_text << "!!<li>Voices: " << getVoiceInfo(infile) << "</li>" << endl;
+
+		if (m_durUnit != 2.0) {
+			m_humdrum_text << "!!<li>Duration unit: " << m_durUnit << "</li>" << endl;
 		}
 	}
 
-	return -1;
+	m_humdrum_text << "!!</ul>" << endl;
+	m_humdrum_text << "!!</div>" << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getBottomQuartile --
+// Tool_rphrase::getVoiceInfo --
 //
 
-int Tool_prange::getBottomQuartile(vector<double>& midibins) {
-	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
-
-	double cumsum = 0.0;
-	int i;
-	for (i=0; i<(int)midibins.size(); i++) {
-		if (midibins[i] <= 0.0) {
-			continue;
+string Tool_rphrase::getVoiceInfo(HumdrumFile& infile) {
+	vector<HTp> kspines = infile.getKernSpineStartList();
+	string vcount = to_string(kspines.size());
+	string ocount;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			break;
 		}
-		cumsum += midibins[i]/sum;
-		if (cumsum >= 0.25) {
-			return i;
+		if (infile[i].isReferenceRecord()) {
+			string key = infile[i].getReferenceKey();
+			if (key == "voices") {
+				ocount = infile[i].getReferenceValue();
+			}
 		}
 	}
 
-	return -1;
+	if (ocount.empty()) {
+		return vcount;
+	}
+
+	if (ocount != vcount) {
+		string output = ocount;
+		output += "(";
+		output += vcount;
+		output += ")";
+		return output;
+	} else {
+		return vcount;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getMaxValue --
+// Tool_rphrase::printEmbeddedVoiceInfoSummary --
 //
 
-double Tool_prange::getMaxValue(vector<vector<double>>& bins) {
-	double maxi = 0;
-	for (int i=1; i<(int)bins.size(); i++) {
-		if (bins.at(i).at(0) > bins.at(maxi).at(0)) {
-			maxi = i;
+void Tool_rphrase::printEmbeddedVoiceInfoSummary(vector<Tool_rphrase::VoiceInfo>& voiceInfo, HumdrumFile& infile) {
+	m_humdrum_text << "!!<ul>" << endl;
+
+	double total = 0.0;
+	for (int i=0; i<(int)voiceInfo[0].phraseDurs.size(); i++) {
+		if (voiceInfo[0].phraseDurs[i] > 0.0) {
+			total += voiceInfo[0].phraseDurs[i];
+		}
+		if (voiceInfo[0].restsBefore[i] > 0.0) {
+			total += voiceInfo[0].restsBefore[i];
 		}
 	}
-	return bins.at(maxi).at(0);
-}
-
+	m_humdrum_text << "!!<li>Score duration: " << twoDigitRound(total) << "</li>" << endl;
 
+	int countSum = 0;
+	for (int i=0; i<(int)voiceInfo.size(); i++) {
+		countSum += (int)voiceInfo[i].phraseDurs.size();
+	}
+	m_humdrum_text << "!!<li>Total segments: " << countSum << "</li>" << endl;
 
-//////////////////////////////
-//
-// Tool_prange::getVpos == return the position on the staff given the diatonic pitch.
-//     and the staff. 1=bass, 2=treble.
-//     3 = bottom line of clef, 0 = space below first ledger line.
-//
+	double averageCount = countSum / (double)voiceInfo.size();
+	averageCount = (int)(averageCount * 10 + 0.5) / 10.0;
+	m_humdrum_text << "!!<li>Average voice segments: " << averageCount << "</li>" << endl;
 
-double Tool_prange::getVpos(double base7) {
-	double output = 0;
-	if (base7 < 4 * 7) {
-		// bass clef
-		output = base7 - (1 + 2*7);  // D2
-	} else {
-		// treble clef
-		output = base7 - (6 + 3*7);  // B3
+	double durSum = 0.0;
+	for (int i=0; i<(int)voiceInfo.size(); i++) {
+		for (int j=0; j<(int)voiceInfo[i].phraseDurs.size(); j++) {
+			durSum += voiceInfo[i].phraseDurs[j];
+		}
 	}
-	return output;
+	double averageDur = durSum / countSum;
+	averageDur = (int)(averageDur * 10 + 0.5) / 10.0;
+	m_humdrum_text << "!!<li>Average segment duration: " << averageDur << "</li>" << endl;
+
+	m_humdrum_text << "!!<li>Voices: " << getVoiceInfo(infile) << "</li>" << endl;
+
+	m_humdrum_text << "!!</ul>" << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getStaffBase7 -- return 1 if less than middle C; otherwise return 2.
+// Tool_rphrase::printEmbeddedIndividualVoiceInfo --
 //
 
-int Tool_prange::getStaffBase7(int base7) {
-	if (base7 < 4 * 7) {
-		return 1;
-	} else {
-		return 2;
-	}
-}
-
+void Tool_rphrase::printEmbeddedIndividualVoiceInfo(Tool_rphrase::VoiceInfo& voiceInfo, HumdrumFile& infile) {
+	m_humdrum_text << "!!<tr>" << endl;
 
-//////////////////////////////
-//
-// Tool_prange::getMaxDiatonicIndex -- return the highest non-zero content.
-//
+	m_humdrum_text << "!!<td class='voice'>" << voiceInfo.name << "</td>" << endl;
 
-int Tool_prange::getMaxDiatonicIndex(vector<vector<double>>& diatonic) {
-	for (int i=diatonic.size()-1; i>=0; i--) {
-		if (diatonic.at(i).at(0) != 0.0) {
-			return i;
+	double sounding = 0.0;
+	double resting = 0.0;
+	for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+		if (voiceInfo.phraseDurs[i] > 0.0) {
+			sounding += voiceInfo.phraseDurs[i];
+		}
+		if (voiceInfo.restsBefore[i] > 0.0) {
+			resting += voiceInfo.restsBefore[i];
 		}
 	}
-	return -1;
-}
-
+	double total = sounding + resting;
+	double spercent = int(sounding/total * 100.0 + 0.5);
+	double rpercent = int(resting/total * 100.0 + 0.5);
+	m_humdrum_text << "!!<td class='sounding'>" << sounding << "(" << spercent << "%)</td>" << endl;
+	m_humdrum_text << "!!<td class='resting'>" << resting << "(" << rpercent << "%)</td>" << endl;
 
+	// Segment count
+	m_humdrum_text << "!!<td class='segments'>";
+	m_humdrum_text << voiceInfo.phraseDurs.size();
+	m_humdrum_text << "</td>" << endl;
 
-//////////////////////////////
-//
-// Tool_prange::getMinDiatonicIndex -- return the lowest non-zero content.
-//
+	// Segment duration average
+	m_humdrum_text << "!!<td class='average'>";
+	double sum = 0;
+	int count = 0;
+	for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+		count++;
+		sum += voiceInfo.phraseDurs.at(i);
+	}
+	double average = int(sum / count * 100.0 + 0.5)/100.0;
+	m_humdrum_text << average;
+	m_humdrum_text << "</td>" << endl;
 
-int Tool_prange::getMinDiatonicIndex(vector<vector<double>>& diatonic) {
-	for (int i=0; i<(int)diatonic.size(); i++) {
-		if (diatonic.at(i).at(0) != 0.0) {
-			return i;
+	// Segments
+	m_humdrum_text << "!!<td class='segment-durations'>";
+	if (m_sortQ || m_reverseSortQ) {
+		vector<pair<double, int>> sortList;
+		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+			sortList.emplace_back(voiceInfo.phraseDurs[i], i);
+		}
+		if (m_sortQ) {
+			sort(sortList.begin(), sortList.end(),
+				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
+					return a.first < b.first;
+			});
+		} else if (m_reverseSortQ) {
+			sort(sortList.begin(), sortList.end(),
+				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
+					return a.first > b.first;
+			});
+		}
+		for (int i=0; i<(int)sortList.size(); i++) {
+			int ii = sortList[i].second;
+			if (m_barlineQ) {
+				m_humdrum_text << "<span class='measure'>m" << voiceInfo.barStarts.at(ii) << ":</span>";
+			}
+			m_humdrum_text << "<span class='length' title='measure " << voiceInfo.barStarts.at(ii)
+			               << ")' >" << twoDigitRound(voiceInfo.phraseDurs.at(ii)) << "</span>";
+			if (i < (int)sortList.size() - 1) {
+				m_humdrum_text << " ";
+			}
+		}
+	} else {
+		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
+			if (voiceInfo.restsBefore.at(i) > 0) {
+				m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(voiceInfo.restsBefore.at(i)) << "</span> ";
+			} else if (i > 0) {
+				// force display r:0 for section boundaries.
+				m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(voiceInfo.restsBefore.at(i)) << "</span> ";
+			}
+			if (m_barlineQ) {
+				m_humdrum_text << "<span class='measure'>m" << voiceInfo.barStarts.at(i) << ":</span>";
+			}
+			m_humdrum_text << "<span class='length' title='measure " << voiceInfo.barStarts.at(i)
+			               << "' onclick='rphraseGotoMeasure(" << voiceInfo.barStarts.at(i)
+			               << ")' >" << twoDigitRound(voiceInfo.phraseDurs.at(i)) << "</span>";
+			if (i < (int)voiceInfo.phraseDurs.size() - 1) {
+				m_humdrum_text << " ";
+			}
 		}
 	}
-	return -1;
+
+	m_humdrum_text << "</td>" << endl;
+	m_humdrum_text << "!!</tr>" << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getMinDiatonicAcc -- return the lowest accidental.
+// Tool_rphrase::fillCompositeInfo --
 //
 
-int Tool_prange::getMinDiatonicAcc(vector<vector<double>>& diatonic, int index) {
-	for (int i=1; i<(int)diatonic.at(index).size(); i++) {
-		if (diatonic.at(index).at(i) != 0.0) {
-			return i;
-		}
-	}
-	return -1;
-}
+void Tool_rphrase::fillCompositeInfo(Tool_rphrase::VoiceInfo& compositeInfo, HumdrumFile& infile) {
+	compositeInfo.name = getCompositeLabel(infile);
+	vector<int> noteStates;
+	getCompositeStates(noteStates, infile);
 
+	bool inPhraseQ       = false;
+	int currentBarline   = 0;
+	int startBarline     = 1;
+	HumNum startTime     = 0;
+	HumNum restBefore    = 0;
+	HumNum startTimeRest = 0;
+	HumNum scoreDur      = infile.getScoreDuration();
+	HTp phraseStartTok   = NULL;
 
+	for (int i=0; i<infile.getLineCount(); i++) {
 
-//////////////////////////////
-//
-// Tool_prange::getMaxDiatonicAcc -- return the highest accidental.
-//
+		// Split phrases at double barlines (medial cadences):
+		if (infile[i].isBarline()) {
+			HTp token = infile.token(i, 0);
+			if (token->find("||") != string::npos) {
+				HumNum tdur = token->getDurationFromStart();
+				if (tdur != scoreDur) {
+					// Only process if double barline is not at the end of the score.
 
-int Tool_prange::getMaxDiatonicAcc(vector<vector<double>>& diatonic, int index) {
-	for (int i=(int)diatonic.at(index).size() - 1; i>0; i--) {
-		if (diatonic.at(index).at(i) != 0.0) {
-			return i;
-		}
-	}
-	return -1;
-}
+					if (inPhraseQ) {
+						// In phrase, so continue if still notes, otherwise
+						// if a rest, then record the currently active phrase
+						// that has ended.
 
+						// ending a phrase
+						HumNum endTime = infile[i].getDurationFromStart();
+						HumNum duration = endTime - startTime;
+						startTime = -1;
+						inPhraseQ = false;
+						double value = duration.getFloat() / m_durUnit;
+						compositeInfo.phraseDurs.push_back(value);
+						compositeInfo.barStarts.push_back(startBarline);
+						compositeInfo.phraseStartToks.push_back(phraseStartTok);
+						phraseStartTok = NULL;
+						m_sumComposite += duration.getFloat() / m_durUnit;
+						m_pcountComposite++;
+						double rvalue = restBefore.getFloat() / m_durUnit;
+						compositeInfo.restsBefore.push_back(rvalue);
 
+						// record rest start
+						startTimeRest = endTime;
+					} else {
+						// Not in phrase, so not splitting a rest region.
+						// This case should be rare (starting a medial cadence
+						// with rests and potentially starting new section with rests.
+					}
 
-//////////////////////////////
-//
-// Tool_prange::prepareRefmap --
-//
+				}
+			}
+		}
 
-void Tool_prange::prepareRefmap(HumdrumFile& infile) {
-	vector<HLp> refrecords = infile.getGlobalReferenceRecords();
-	m_refmap.clear();
-	HumRegex hre;
-	for (int i = (int)refrecords.size()-1; i>=0; i--) {
-		string key = refrecords[i]->getReferenceKey();
-		string value = refrecords[i]->getReferenceValue();
-		m_refmap[key] = value;
-		if (key.find("@") != string::npos) {
-			// create default value
-			hre.replaceDestructive(key, "", "@.*");
-			if (m_refmap[key].empty()) {
-				m_refmap[key] = value;
+		if (infile[i].isBarline()) {
+			HTp token = infile.token(i, 0);
+			HumRegex hre;
+			if (hre.search(token, "(\\d+)")) {
+				currentBarline = hre.getMatchInt(1);
+				continue;
 			}
 		}
-	}
-	// fill in @{} templates (mostly for !!!title:)
-	int counter = 0; // prevent recursions
-	for (auto& entry : m_refmap) {
 
-		if (entry.second.find("@") != string::npos) {
-			while (hre.search(entry.second, "@\\{(.*?)\\}")) {
-				string key = hre.getMatch(1);
-				string value = m_refmap[key];
-				hre.replaceDestructive(entry.second, value, "@\\{" + key + "\\}", "g");
-				counter++;
-				if (counter > 1000) {
-					break;
+
+		if (!infile[i].isData()) {
+			continue;
+		}
+
+		if (inPhraseQ) {
+			// In phrase, so continue if still notes, otherwise
+			// if a rest, then record the currently active phrase
+			// that has ended.
+			if (noteStates[i] == 0) {
+				// ending a phrase
+				HumNum endTime = infile[i].getDurationFromStart();
+				HumNum duration = endTime - startTime;
+				startTime = -1;
+				inPhraseQ = false;
+				double value = duration.getFloat() / m_durUnit;
+				compositeInfo.phraseDurs.push_back(value);
+				compositeInfo.barStarts.push_back(startBarline);
+				compositeInfo.phraseStartToks.push_back(phraseStartTok);
+				phraseStartTok = NULL;
+				m_sumComposite += duration.getFloat() / m_durUnit;
+				m_pcountComposite++;
+				double rvalue = restBefore.getFloat() / m_durUnit;
+				compositeInfo.restsBefore.push_back(rvalue);
+				// record rest start
+				startTimeRest = endTime;
+			} else {
+				// continuing a phrase, so do nothing
+			}
+		} else {
+			// Not in phrase, so continue if rest; otherwise,
+			// if a note, then record a phrase start.
+			if (noteStates[i] == 0) {
+				// continuing a non-phrase, so do nothing
+			} else {
+				// starting a phrase
+				startTime = infile[i].getDurationFromStart();
+				startBarline = currentBarline;
+				inPhraseQ = true;
+				// check if there are rests before the phrase
+				// The rest duration will be stored when the
+				// end of the next phrase is encountered.
+				if (startTimeRest >= 0) {
+					restBefore = startTime - startTimeRest;
+				} else {
+					restBefore = 0;
 				}
+				phraseStartTok = infile.token(i, 0);
 			}
 		}
 
 	}
 
-	// prepare title
-	if (m_refmap["title"].empty()) {
-		m_refmap["title"] = m_refmap["OTL"];
+	if (inPhraseQ) {
+		// process last phrase
+		HumNum endTime = infile.getScoreDuration();
+		HumNum duration = endTime - startTime;
+		double value = duration.getFloat() / m_durUnit;
+		compositeInfo.phraseDurs.push_back(value);
+		compositeInfo.barStarts.push_back(startBarline);
+		compositeInfo.phraseStartToks.push_back(phraseStartTok);
+		m_sumComposite += duration.getFloat() / m_durUnit;
+		m_pcountComposite++;
+		double rvalue = restBefore.getFloat() / m_durUnit;
+		compositeInfo.restsBefore.push_back(rvalue);
+	}
+
+	if (m_markQ) {
+		markCompositePhraseStartsInScore(infile, compositeInfo);
 	}
 }
 
@@ -117057,395 +121870,403 @@ void Tool_prange::prepareRefmap(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_prange::getTitle --
+// Tool_rphrase::getCompositeLabel --
 //
 
-string Tool_prange::getTitle(void) {
-	string titlestring = "_00";
-	HumRegex hre;
-	if (m_notitleQ) {
-		return "";
-	} else if (m_titleQ) {
-		titlestring = m_title;
-		int counter = 0;
-
-		if (titlestring.find("@") != string::npos) {
-			while (hre.search(titlestring, "@\\{(.*?)\\}")) {
-				string key = hre.getMatch(1);
-				string value = m_refmap[key];
-				hre.replaceDestructive(titlestring, value, "@\\{" + key + "\\}", "g");
-				counter++;
-				if (counter > 1000) {
-					break;
-				}
-			}
-		}
-		if (!titlestring.empty()) {
-			titlestring = "_00" + titlestring;
-		}
-	} else {
-		titlestring = m_refmap["title"];
-		if (!titlestring.empty()) {
-			titlestring = "_00" + titlestring;
+string Tool_rphrase::getCompositeLabel(HumdrumFile& infile) {
+	string voices;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReferenceRecord()) {
+			continue;
+		}
+		string key = infile[i].getReferenceKey();
+		if (key != "voices") {
+			continue;
 		}
+		voices = infile[i].getReferenceValue();
+		break;
 	}
-	return titlestring;
-}
 
+	if (voices.empty()) {
+		return "composite";
+	}
 
+	vector<HTp> kstarts = infile.getKernSpineStartList();
 
-//////////////////////////////
-//
-// Tool_prange::clearHistograms --
-//
+	string output = "composite ";
+	output += voices;
 
-void Tool_prange::clearHistograms(vector<vector<double> >& bins, int start) {
-	int i;
-	for (i=start; i<(int)bins.size(); i++) {
-		bins[i].resize(40*11);
-		fill(bins[i].begin(), bins[i].end(), 0.0);
-		// bins[i].allowGrowth(0);
-	}
-	for (int i=0; i<(int)bins.size(); i++) {
-		if (bins[i].size() == 0) {
-			bins[i].resize(40*11);
-			fill(bins[i].begin(), bins[i].end(), 0.0);
+
+	HumRegex hre;
+
+	if (hre.search(voices, "^\\d+$")) {
+		int vint = stoi(voices);
+		if (vint != (int)kstarts.size()) {
+			output += "(";
+			output += to_string(kstarts.size());
+			output += ")";
 		}
+	} else {
+		output += "(";
+		output += to_string(kstarts.size());
+		output += ")";
 	}
-}
 
+	return output;
+}
 
 
 
 //////////////////////////////
 //
-// Tool_prange::printAnalysis --
+// Tool_rphrase::fillVoiceInfo --
 //
 
-void Tool_prange::printAnalysis(ostream& out, vector<double>& midibins) {
-	if (m_percentileQ) {
-		printPercentile(out, midibins, m_percentile);
-		return;
-	}  else if (m_rangeQ) {
-		double notesinrange = countNotesInRange(midibins, m_rangeL, m_rangeH);
-		out << notesinrange << endl;
-		return;
+void Tool_rphrase::fillVoiceInfo(vector<Tool_rphrase::VoiceInfo>& voiceInfo,
+		vector<HTp>& kstarts, HumdrumFile& infile) {
+	for (int i=0; i<(int)kstarts.size(); i++) {
+		fillVoiceInfo(voiceInfo.at(i), kstarts.at(i), infile);
 	}
+}
 
-	int i;
-	double normval = 1.0;
 
-	// print the pitch histogram
+void Tool_rphrase::fillVoiceInfo(Tool_rphrase::VoiceInfo& voiceInfo, HTp& kstart, HumdrumFile& infile) {
+	HTp current = kstart;
 
-	double fracL = 0.0;
-	double fracH = 0.0;
-	double fracA = 0.0;
-	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
-	if (m_normQ) {
-		normval = sum;
-	}
-	double runningtotal = 0.0;
+	bool inPhraseQ       = false;
+	int currentBarline   = 0;
+	int startBarline     = 1;
+	HumNum startTime     = 0;
 
+	HumNum restBefore    = 0;
+	HumNum startTimeRest = 0;
 
-	out << "**keyno\t";
-	if (m_pitchQ) {
-		out << "**pitch";
-	} else {
-		out << "**kern";
-	}
-	out << "\t**count";
-	if (m_addFractionQ) {
-		out << "\t**fracL";
-		out << "\t**fracA";
-		out << "\t**fracH";
-	}
-	out << "\n";
+	HumNum scoreDur = infile.getScoreDuration();
+	HTp phraseStartTok = NULL;
 
+	while (current) {
 
-	int base12;
+		// Split phrases at double barlines (medial cadences):
+		if (infile[current->getLineIndex()].isBarline()) {
+			HTp token = infile.token(current->getLineIndex(), 0);
+			if (token->find("||") != string::npos) {
+				HumNum tdur = token->getDurationFromStart();
+				if (tdur != scoreDur) {
+					// Only process if double barline is not at the end of the score.
 
-	if (!m_reverseQ) {
-		for (i=0; i<(int)midibins.size(); i++) {
-			if (midibins[i] <= 0.0) {
-				continue;
-			}
-			if (m_diatonicQ) {
-				base12 = Convert::base7ToBase12(i);
-			} else {
-				base12 = i;
-			}
-			out << base12 << "\t";
-			if (m_pitchQ) {
-				out << Convert::base12ToPitch(base12);
-			} else {
-				out << Convert::base12ToKern(base12);
-			}
-			out << "\t";
-			out << midibins[i] / normval;
-			fracL = runningtotal/sum;
-			runningtotal += midibins[i];
-			fracH = runningtotal/sum;
-			fracA = (fracH + fracL)/2.0;
-			fracL = (int)(fracL * 10000.0 + 0.5)/10000.0;
-			fracH = (int)(fracH * 10000.0 + 0.5)/10000.0;
-			fracA = (int)(fracA * 10000.0 + 0.5)/10000.0;
-			if (m_addFractionQ) {
-				out << "\t" << fracL;
-				out << "\t" << fracA;
-				out << "\t" << fracH;
+					if (inPhraseQ) {
+						// In phrase, so continue if still notes, otherwise
+						// if a rest, then record the currently active phrase
+						// that has ended.
+
+						HumNum endTime = current->getDurationFromStart();
+						HumNum duration = endTime - startTime;
+						startTime = -1;
+						inPhraseQ = false;
+						double value = duration.getFloat() / m_durUnit;
+						voiceInfo.phraseDurs.push_back(value);
+						voiceInfo.barStarts.push_back(startBarline);
+						voiceInfo.phraseStartToks.push_back(phraseStartTok);
+						phraseStartTok = NULL;
+						m_sum += duration.getFloat() / m_durUnit;
+						m_pcount++;
+						double rvalue = restBefore.getFloat() / m_durUnit;
+						voiceInfo.restsBefore.push_back(rvalue);
+
+						// record rest start
+						startTimeRest = endTime;
+					} else {
+						// Not in phrase, so not splitting a rest region.
+						// This case should be rare (starting a medial cadence
+						// with rests and potentially starting new section with rests.
+					}
+
+				}
 			}
-			out << "\n";
 		}
-	} else {
-		for (i=(int)midibins.size()-1; i>=0; i--) {
-			if (midibins[i] <= 0.0) {
+
+		if (current->isBarline()) {
+			HumRegex hre;
+			if (hre.search(current, "(\\d+)")) {
+				currentBarline = hre.getMatchInt(1);
+				current = current->getNextToken();
 				continue;
 			}
-			if (m_diatonicQ) {
-				base12 = Convert::base7ToBase12(i);
+		}
+
+		if (current->isInstrumentName()) {
+			voiceInfo.name = current->substr(3);
+		}
+		if (!(current->isData() || (m_breathQ && (*current == "*breath")))) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+
+		if (inPhraseQ) {
+			// In phrase, so continue if still notes, otherwise
+			// if a rest, then record the currently active phrase
+			// that has ended.
+			if (current->isRest() || (*current == "*breath")) {
+				// ending a phrase
+				HumNum endTime = current->getDurationFromStart();
+				HumNum duration = endTime - startTime;
+				startTime = -1;
+				inPhraseQ = false;
+				double value = duration.getFloat() / m_durUnit;
+				voiceInfo.phraseDurs.push_back(value);
+				voiceInfo.barStarts.push_back(startBarline);
+				voiceInfo.phraseStartToks.push_back(phraseStartTok);
+				phraseStartTok = NULL;
+				m_sum += duration.getFloat() / m_durUnit;
+				m_pcount++;
+				double rvalue = restBefore.getFloat() / m_durUnit;
+				voiceInfo.restsBefore.push_back(rvalue);
+				// record rest start
+				startTimeRest = endTime;
 			} else {
-				base12 = i;
+				// continuing a phrase, so do nothing
 			}
-			out << base12 << "\t";
-			if (m_pitchQ) {
-				out << Convert::base12ToPitch(base12);
+		} else {
+			// Not in phrase, so continue if rest; otherwise,
+			// if a note, then record a phrase start.
+			if (current->isRest() || (*current == "*breath")) {
+				// continuing a non-phrase, so do nothing
 			} else {
-				out << Convert::base12ToKern(base12);
-			}
-			out << "\t";
-			out << midibins[i] / normval;
-			fracL = runningtotal/sum;
-			runningtotal += midibins[i];
-			fracH = runningtotal/sum;
-			fracA = (fracH + fracL)/2.0;
-			fracL = (int)(fracL * 10000.0 + 0.5)/10000.0;
-			fracH = (int)(fracH * 10000.0 + 0.5)/10000.0;
-			fracA = (int)(fracA * 10000.0 + 0.5)/10000.0;
-			if (m_addFractionQ) {
-				out << "\t" << fracL;
-				out << "\t" << fracA;
-				out << "\t" << fracH;
+				// starting a phrase
+				startTime = current->getDurationFromStart();
+				startBarline = currentBarline;
+				inPhraseQ = true;
+				// check if there are rests before the phrase
+				// The rest duration will be stored when the
+				// end of the next phrase is encountered.
+				if (startTimeRest >= 0) {
+					restBefore = startTime - startTimeRest;
+				} else {
+					restBefore = 0;
+				}
+				phraseStartTok = current;
 			}
-			out << "\n";
 		}
-	}
-
-	out << "*-\t*-\t*-";
-	if (m_addFractionQ) {
-		out << "\t*-";
-		out << "\t*-";
-		out << "\t*-";
-	}
-	out << "\n";
 
-	out << "!!tessitura:\t" << getTessitura(midibins) << " semitones\n";
-
-	double mean = getMean12(midibins);
-	if (m_diatonicQ && (mean > 0)) {
-		mean = Convert::base7ToBase12(mean);
+		current = current->getNextToken();
 	}
-	out << "!!mean:\t\t" << mean;
-	out << " (";
-	if (mean < 0) {
-		out << "unpitched";
-	} else {
-		out << Convert::base12ToKern(int(mean+0.5));
+	if (inPhraseQ) {
+		// process last phrase
+		HumNum endTime = kstart->getLine()->getOwner()->getScoreDuration();
+		HumNum duration = endTime - startTime;
+		double value = duration.getFloat() / m_durUnit;
+		voiceInfo.phraseDurs.push_back(value);
+		voiceInfo.barStarts.push_back(startBarline);
+		voiceInfo.phraseStartToks.push_back(phraseStartTok);
+		m_sum += duration.getFloat() / m_durUnit;
+		m_pcount++;
+		double rvalue = restBefore.getFloat() / m_durUnit;
+		voiceInfo.restsBefore.push_back(rvalue);
 	}
-	out << ")" << "\n";
 
-	int median12 = getMedian12(midibins);
-	out << "!!median:\t" << median12;
-	out << " (";
-	if (median12 < 0) {
-		out << "unpitched";
-	} else {
-		out << Convert::base12ToKern(median12);
+	if (m_markQ) {
+		markPhraseStartsInScore(infile, voiceInfo);
 	}
-	out << ")" << "\n";
-
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getMedian12 -- return the pitch on which half of pitches are above
-//     and half are below.
+// Tool_rphrase::markCompositePhraseStartsInScore --
 //
 
-int Tool_prange::getMedian12(vector<double>& midibins) {
-	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
-
-	double cumsum = 0.0;
-	int i;
-	for (i=0; i<(int)midibins.size(); i++) {
-		if (midibins[i] <= 0.0) {
-			continue;
+void Tool_rphrase::markCompositePhraseStartsInScore(HumdrumFile& infile, Tool_rphrase::VoiceInfo& compositeInfo) {
+	stringstream buffer;
+	for (int i=0; i<(int)compositeInfo.phraseStartToks.size(); i++) {
+		HTp tok = compositeInfo.phraseStartToks.at(i);
+		string measure = "";
+		if (m_barlineQ) {
+			measure = to_string(compositeInfo.barStarts.at(i));
 		}
-		cumsum += midibins[i]/sum;
-		if (cumsum >= 0.50) {
-			return i;
+		double duration = compositeInfo.phraseDurs.at(i);
+		buffer.str("");
+		if (!measure.empty()) {
+			buffer << "m" << measure << "&colon;";
 		}
+		buffer << twoDigitRound(duration);
+		int lineIndex = tok->getLineIndex();
+		infile[lineIndex].setValue("auto", "rphrase-composite-start", buffer.str());
 	}
-
-	return -1000;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getMean12 -- return the interval between the highest and lowest
-//     pitch in terms if semitones.
+// Tool_rphrase::twoDigitRound --
 //
 
-double Tool_prange::getMean12(vector<double>& midibins) {
-	double top    = 0.0;
-	double bottom = 0.0;
-
-	int i;
-	for (i=0; i<(int)midibins.size(); i++) {
-		if (midibins[i] <= 0.0) {
-			continue;
-		}
-		top += midibins[i] * i;
-		bottom += midibins[i];
-
-	}
-
-	if (bottom == 0) {
-		return -1000;
-	}
-	return top / bottom;
+double Tool_rphrase::twoDigitRound(double input) {
+	return int(input * 100.0 + 0.499999) / 100.0;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getTessitura -- return the interval between the highest and lowest
-//     pitch in terms if semitones.
+// Tool_rphrase::markPhraseStartsInScore --
 //
 
-int Tool_prange::getTessitura(vector<double>& midibins) {
-	int minn = -1000;
-	int maxx = -1000;
-	int i;
-
-	for (i=0; i<(int)midibins.size(); i++) {
-		if (midibins[i] <= 0.0) {
-			continue;
-		}
-		if (minn < 0) {
-			minn = i;
-		}
-		if (maxx < 0) {
-			maxx = i;
-		}
-		if (minn > i) {
-			minn = i;
+void Tool_rphrase::markPhraseStartsInScore(HumdrumFile& infile, Tool_rphrase::VoiceInfo& voiceInfo) {
+	stringstream buffer;
+	for (int i=0; i<(int)voiceInfo.phraseStartToks.size(); i++) {
+		HTp tok = voiceInfo.phraseStartToks.at(i);
+		string measure = "";
+		if (m_barlineQ) {
+			measure = to_string(voiceInfo.barStarts.at(i));
 		}
-		if (maxx < i) {
-			maxx = i;
+		double duration = voiceInfo.phraseDurs.at(i);
+		buffer.str("");
+		if (!measure.empty()) {
+			buffer << "m" << measure << "&colon;";
 		}
+		buffer << duration;
+		tok->setValue("auto", "rphrase-start", buffer.str());
 	}
-	if (m_diatonicQ) {
-		maxx = Convert::base7ToBase12(maxx);
-		minn = Convert::base7ToBase12(minn);
-	}
-
-	return maxx - minn + 1;
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_prange::countNotesInRange --
+// Tool_ruthfix::Tool_ruthfix -- Set the recognized options for the tool.
 //
 
-double Tool_prange::countNotesInRange(vector<double>& midibins, int low, int high) {
-	int i;
-	double sum = 0;
-	for (i=low; i<=high; i++) {
-		sum += midibins[i];
-	}
-	return sum;
+Tool_ruthfix::Tool_ruthfix(void) {
+	// add options here
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_prange::printPercentile --
+// Tool_ruthfix::run -- Do the main work of the tool.
 //
 
-void Tool_prange::printPercentile(ostream& out, vector<double>& midibins, double m_percentile) {
-	double sum = accumulate(midibins.begin(), midibins.end(), 0.0);
-	double runningtotal = 0.0;
-	int i;
-	for (i=0; i<(int)midibins.size(); i++) {
-		if (midibins[i] <= 0) {
-			continue;
-		}
-		runningtotal += midibins[i] / sum;
-		if (runningtotal >= m_percentile) {
-			out << i << endl;
-			return;
-		}
+bool Tool_ruthfix::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
+	return status;
+}
 
-	out << "unknown" << endl;
+
+bool Tool_ruthfix::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_ruthfix::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_ruthfix::run(HumdrumFile& infile) {
+	insertCrossBarTies(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_prange::getRange --
+// Tool_ruthfix::insertCrossBarTies --
 //
 
-void Tool_prange::getRange(int& rangeL, int& rangeH, const string& rangestring) {
-	rangeL = -1; rangeH = -1;
-	if (rangestring.empty()) {
-		return;
+void Tool_ruthfix::insertCrossBarTies(HumdrumFile& infile) {
+	int scount = infile.getStrandCount();
+	if (scount == 0) {
+		// The input file was not read from a file but was created
+		// dynamically.  The easiest thing to do is to reload to get the
+		// spine/strand information.
+		stringstream ss;
+		infile.createLinesFromTokens();
+		ss << infile;
+		infile.readString(ss.str());
 	}
-	int length = (int)rangestring.length();
-	char* buffer = new char[length+1];
-	strcpy(buffer, rangestring.c_str());
-	char* ptr;
-	if (std::isdigit(buffer[0])) {
-		ptr = strtok(buffer, " \t\n:-");
-		sscanf(ptr, "%d", &rangeL);
-		ptr = strtok(NULL, " \t\n:-");
-		if (ptr != NULL) {
-			sscanf(ptr, "%d", &rangeH);
+	scount = infile.getStrandCount();
+
+
+	HTp token;
+	for (int i=0; i<scount; i++) {
+		token = infile.getStrandStart(i);
+		if (!token->isKern()) {
+			continue;
 		}
-	} else {
-		ptr = strtok(buffer, " :");
-		if (ptr != NULL) {
-			rangeL = Convert::kernToMidiNoteNumber(ptr);
-			ptr = strtok(NULL, " :");
-			if (ptr != NULL) {
-				rangeH = Convert::kernToMidiNoteNumber(ptr);
+		insertCrossBarTies(infile, i);
+	}
+}
+
+
+void Tool_ruthfix::insertCrossBarTies(HumdrumFile& infile, int strand) {
+	HTp sstart = infile.getStrandStart(strand);
+	HTp send   = infile.getStrandEnd(strand);
+	HTp s = sstart;
+	HTp lastnote = NULL;
+	bool barstart = true;
+	while (s != send) {
+		if (s->isBarline()) {
+			barstart = true;
+		} else if (s->isNote()) {
+			if (lastnote && barstart && (s->find("yy") != string::npos)) {
+				createTiedNote(lastnote, s);
 			}
+			barstart = false;
+			lastnote = s;
+		} else if (s->isRest()) {
+			lastnote = NULL;
+			barstart = false;
+		}
+		s = s->getNextToken();
+		if (!s) {
+			break;
 		}
 	}
+}
 
-	if (rangeH < 0) {
-		rangeH = rangeL;
-	}
 
-	if (rangeL <   0) { rangeL =   0; }
-	if (rangeH <   0) { rangeH =   0; }
-	if (rangeL > 127) { rangeL = 127; }
-	if (rangeH > 127) { rangeH = 127; }
-	if (rangeL > rangeH) {
-		int temp = rangeL;
-		rangeL = rangeH;
-		rangeH = temp;
-	}
 
+//////////////////////////////
+//
+// Tool_ruthfix::createTiedNote -- Does not work for chords.
+//  change  1E-X TO 2E-Xyy
+//      to  [1E-X TO 2E-X]
+//
+
+void Tool_ruthfix::createTiedNote(HTp left, HTp right) {
+	if (left->isChord() || right->isChord()) {
+		return;
+	}
+	auto loc = right->find("yy");
+	if (loc != string::npos) {
+		left->insert(0, 1, '[');
+		right->replace(loc, 2, "]");
+	}
 }
 
 
@@ -117454,31 +122275,22 @@ void Tool_prange::getRange(int& rangeL, int& rangeH, const string& rangestring)
 
 /////////////////////////////////
 //
-// Tool_gridtest::Tool_recip -- Set the recognized options for the tool.
+// Tool_sab2gs::Tool_sab2gs -- Set the recognized options for the tool.
 //
 
-Tool_recip::Tool_recip(void) {
-	define("c|composite=b",          "do composite rhythm analysis");
-	define("a|append=b",             "append composite analysis to input");
-	define("p|prepend=b",            "prepend composite analysis to input");
-	define("r|replace=b",            "replace **kern data with **recip data");
-	define("x|attacks-only=b",       "only mark lines with note attacks");
-	define("G|ignore-grace-notes=b", "ignore grace notes");
-	define("k|kern-spine=i:1",       "analyze only given kern spine");
-	define("K|all-spines=b",         "analyze each kern spine separately");
-	define("e|exinterp=s:**recip",   "use the given exinterp for data output");
-	define("n|kern-pitch=s:e",       "note to add for '-e kern' option");
-	define("kern=b",                 "equivalent to '-e kern' option");
+Tool_sab2gs::Tool_sab2gs(void) {
+	define("b|below=s:<", "Marker for displaying on next staff below");
+	define("d|down=b",    "Use only *down/*Xdown interpretations");
 }
 
 
 
-///////////////////////////////
+/////////////////////////////////
 //
-// Tool_recip::run -- Primary interfaces to the tool.
+// Tool_sab2gs::run -- Do the main work of the tool.
 //
 
-bool Tool_recip::run(HumdrumFileSet& infiles) {
+bool Tool_sab2gs::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -117487,44 +122299,32 @@ bool Tool_recip::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_recip::run(const string& indata, ostream& out) {
+bool Tool_sab2gs::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
-	return run(infile, out);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
 }
 
 
-bool Tool_recip::run(HumdrumFile& infile, ostream& out) {
+bool Tool_sab2gs::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
-	out << infile;
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
 	return status;
 }
 
 
-bool Tool_recip::run(HumdrumFile& infile) {
-   initialize(infile);
-
-	int lineCount = infile.getLineCount();
-	if (lineCount == 0) {
-		m_error_text << "No input data";
-		return false;
-	}
-
-	if (getBoolean("composite") || getBoolean("append") || getBoolean("prepend")) {
-		doCompositeAnalysis(infile);
-		infile.createLinesFromTokens();
-		return true;
-	} else if (getBoolean("replace")) {
-		replaceKernWithRecip(infile);
-		infile.createLinesFromTokens();
-		return true;
-	}
-	HumdrumFile cfile = infile;
-	cfile.analyzeStructure();
-	replaceKernWithRecip(cfile);
-	cfile.createLinesFromTokens();
-	insertAnalysisSpines(infile, cfile);
-	// infile.adjustMergeSpineLines();
-	infile.createLinesFromTokens();
+bool Tool_sab2gs::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
 	return true;
 }
 
@@ -117532,679 +122332,660 @@ bool Tool_recip::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_recip::insertAnalysisSpines -- Could be more efficient than the
-//     k-index loop...
+// Tool_sab2gs::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_recip::insertAnalysisSpines(HumdrumFile& infile, HumdrumFile& cfile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			continue;
-		}
-		for (int k=(int)m_kernspines.size()-1; k>=0; k--) {
-			int fcount = infile[i].getFieldCount();
-			int ktrack = m_kernspines[k]->getTrack();
-			int insertj = -1;
-			for (int j=fcount-1; j>=0; j--) {
-				if (!infile.token(i, j)->isKern()) {
-					continue;
-				}
-				int track = infile.token(i, j)->getTrack();
-				if (track != ktrack) {
-					continue;
-				}
-				if (insertj < 0) {
-					insertj = j;
-				}
-				infile[i].appendToken(insertj, cfile.token(i, j)->getText());
-				// infile.token(i, insertj+1)->setTrack(remapping[k]);
-			}
-		}
-	}
+void Tool_sab2gs::initialize(void) {
+	m_belowMarker = getString("below");
+	m_downQ       = getBoolean("down");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_recip::doCompositeAnalysis --
+// Tool_sab2gs::processFile --
 //
 
-void Tool_recip::doCompositeAnalysis(HumdrumFile& infile) {
-
-	// Calculate composite rhythm **recip spine:
+void Tool_sab2gs::processFile(HumdrumFile& infile) {
 
-	vector<HumNum> composite(infile.getLineCount());
-	for (int i=0; i<(int)composite.size(); i++) {
-		composite[i] = infile[i].getDuration();
+	vector<HTp> spines;
+	infile.getSpineStartList(spines);
+	vector<HTp> kernSpines;
+	for (int i=0; i<(int)spines.size(); i++) {
+		if (spines[i]->isKern()) {
+			kernSpines.push_back(spines[i]);
+		}
 	}
-
-	int kernQ = false;
-	if (m_exinterp.find("kern") != std::string::npos) {
-		kernQ = true;
-// cerr << "KERN ON" << endl;
+	if (kernSpines.size() != 3) {
+		// Not valid for processing kern spines, so return original:
+		m_humdrum_text << infile;
+		return;
 	}
 
-	// convert durations to **recip strings
-	vector<string> recips(composite.size());
-	for (int i=0; i<(int)recips.size(); i++) {
-		if ((!m_graceQ) && (composite[i] == 0)) {
-			continue;
-		}
-		recips[i] = Convert::durationToRecip(composite[i]);
-		if (kernQ) {
-			recips[i] += m_kernpitch;
-// cerr << "ADDING PITCH " << m_kernpitch << endl;
-		}
+	string belowMarker = hasBelowMarker(infile);
+	if (!belowMarker.empty()) {
+		m_hasBelowMarker = true;
+		m_belowMarker = belowMarker;
 	}
 
-	if (getBoolean("append")) {
-		infile.appendDataSpine(recips, "", m_exinterp);
-		return;
-	} else if (getBoolean("prepend")) {
-		infile.prependDataSpine(recips, "", m_exinterp);
-		return;
-	} else {
-		infile.prependDataSpine(recips, "", m_exinterp);
-		infile.printFieldIndex(0, m_humdrum_text);
-		infile.clear();
-		infile.readString(m_humdrum_text.str());
-	}
+	adjustMiddleVoice(kernSpines[1]);
+	printGrandStaff(infile, kernSpines);
 }
 
 
 
-//////////////////////////////
+/////////////////////////////
 //
-// Tool_recip::replaceKernWithRecip --
+// Tool_sab2gs::hasBelowMarker -- Returns below marker if found; otherwise,
+//     returns empty string.
 //
 
-void Tool_recip::replaceKernWithRecip(HumdrumFile& infile) {
-	vector<HTp> kspines = infile.getKernSpineStartList();
+string Tool_sab2gs::hasBelowMarker(HumdrumFile& infile) {
+	string output;
 	HumRegex hre;
-	string expression = "[^q\\d.%\\]\\[]+";
-	for (int i=0; i<infile.getStrandCount(); i++) {
-		HTp stok = infile.getStrandStart(i);
-		if (!stok->isKern()) {
-			continue;
-		}
-		HTp etok = infile.getStrandEnd(i);
-		HTp tok = stok;
-		while (tok && (tok != etok)) {
-			if (!tok->isData()) {
-				tok = tok->getNextToken();
-				continue;
-			}
-			if (tok->isNull()) {
-				tok = tok->getNextToken();
+	if (m_hasCrossStaff) {
+		// Search backwards since if there is a below marker, it will be more
+		// likely found at the bottom of the score.
+		for (int i=infile.getLineCount()-1; i<=0; i--) {
+			if (infile[i].hasSpines()) {
 				continue;
 			}
-			if (tok->find('q') != string::npos) {
-				if (m_graceQ) {
-					tok->setText("q");
-				} else {
-					tok->setText(".");
-				}
-			} else {
-				hre.replaceDestructive(*tok, "", expression, "g");
+			if (hre.search(infile.token(i, 0), "^!!!RDF\\*\\*kern\\s*:\\s*([^\\s=]+)\\s*=\\s*below\\s*$")) {
+				output = hre.getMatch(1);
+				break;
 			}
-			tok = tok->getNextToken();
 		}
 	}
-
-	for (int i=0; i<(int)kspines.size(); i++) {
-		kspines[i]->setText(m_exinterp);
-	}
-
+	return output;
 }
 
 
 
-
-//////////////////////////////
+///////////////////////////////
 //
-// Tool_recip::initialize --
+// Tool_sab2gs::printGrandStaff --
 //
 
-void Tool_recip::initialize(HumdrumFile& infile) {
-	m_kernspines = infile.getKernSpineStartList();
-	m_graceQ = !getBoolean("ignore-grace-notes");
+void Tool_sab2gs::printGrandStaff(HumdrumFile& infile, vector<HTp>& starts) {
+	bool foundData = false;
 
-	m_exinterp = getString("exinterp");
-	if (m_exinterp.empty()) {
-		m_exinterp = "**recip";
-	} else if (m_exinterp[0] != '*') {
-		m_exinterp.insert(0, "*");
-	}
-	if (m_exinterp[1] != '*') {
-		m_exinterp.insert(0, "*");
+	vector<int> ktracks(starts.size());
+	for (int i=0; i<(int)starts.size(); i++) {
+		ktracks.at(i) = starts.at(i)->getTrack();
 	}
 
-	m_kernpitch = getString("kern-pitch");
-
-	if (getBoolean("kern")) {
-		m_exinterp = "**kern";
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << endl;
+			continue;
+		}
+		if (!foundData && (infile[i].isData() || infile[i].isBarline())) {
+			printSpineSplit(infile, i, ktracks);
+			foundData = true;
+		}
+		if (*infile.token(i, 0) == "*-") {
+			printSpineMerge(infile, i, ktracks);
+			foundData = false;
+			printReducedLine(infile, i, ktracks);
+			if (m_hasCrossStaff && !m_hasBelowMarker) {
+				m_humdrum_text << "!!!RDF**kern: " << m_belowMarker << " = below" << endl;
+			}
+			continue;
+		}
+		if (foundData) {
+			printSwappedLine(infile, i, ktracks);
+		} else {
+			printReducedLine(infile, i, ktracks);
+		}
 	}
-
-}
-
-
-
-
-
-/////////////////////////////////
-//
-// Tool_restfill::Tool_restfill -- Set the recognized options for the tool.
-//
-
-Tool_restfill::Tool_restfill(void) {
-	define("y|hidden-rests=b",  "hide inserted rests");
-	define("i|exinterp=s:kern", "type of spine to fill with rests");
 }
 
 
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_restfill::run -- Do the main work of the tool.
+// Tool_sab2gs::printSpineSplit -- Split second and third spines, moving non-kern spines
+//    after the second one to the end of the line (null interpretations);
 //
 
-bool Tool_restfill::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+void Tool_sab2gs::printSpineSplit(HumdrumFile& infile, int index, vector<int>& ktracks) {
+	// First print all non-kern spines at the start of the line:
+	int nextIndex = 0;
+	int fcount = 0;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		m_humdrum_text << "*";
+		nextIndex++;
 	}
-	return status;
-}
-
-
-bool Tool_restfill::run(const string& indata, ostream& out) {
-	HumdrumFile infile;
-	infile.readStringNoRhythm(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	// Must be on the first **kern spine:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
+		return;
 	}
-	return status;
-}
-
-
-bool Tool_restfill::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	// Print the first **kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
 	}
-	return status;
-}
-
-
-bool Tool_restfill::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	infile.createLinesFromTokens();
-	return true;
+	fcount++;
+	m_humdrum_text << "*";
+	nextIndex++;
+	// Next print all non-kern spines after first **kern spine:
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		m_humdrum_text << "*";
+		nextIndex++;
+	}
+	// Second **kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Ignore the second kern spine as it does not exist yet in the
+	// output data.
+	nextIndex++;
+	// Then store any non-kern spines between the second and third kern spines to
+	// append to the end of the data line later.
+	string postData;
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (!postData.empty()) {
+			postData += "\t";
+		}
+		nextIndex++;
+		postData += "*";
+	}
+	// Third kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Now print the third kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
+	}
+	fcount++;
+	nextIndex++;
+	m_humdrum_text << "*^";
+	// Now print the non-kern spines after the third **kern spine (or rather just
+	// all spines including any other **kern spines, although current requirement
+	// is that there are only three **kern spines.
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		// HTp token = infile.token(index, nextIndex);
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		nextIndex++;
+		m_humdrum_text << "*";
+	}
+	// Finally print any non-kern spines after the second **kern spine:
+	if (!postData.empty()) {
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		m_humdrum_text << postData;
+	}
+	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_restfill::initialize --
+// Tool_sab2gs::printSpineMerge -- Merge second and third spines, moving non-kern spines
+//    after the second one to the end of the line (null interpretations);
 //
 
-void Tool_restfill::initialize(void) {
-	m_hiddenQ = getBoolean("hidden-rests");
-	m_exinterp = getString("exinterp");
-	if (m_exinterp.empty()) {
-		m_exinterp = "**kern";
+void Tool_sab2gs::printSpineMerge(HumdrumFile& infile, int index, vector<int>& ktracks) {
+	// First print all non-kern spines at the start of the line:
+	int nextIndex = 0;
+	int fcount = 0;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		m_humdrum_text << "*";
+		nextIndex++;
+	}
+	// Must be on the first **kern spine:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Print the first **kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
+	}
+	fcount++;
+	m_humdrum_text << "*";
+	nextIndex++;
+	// Next print all non-kern spines after first **kern spine:
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		m_humdrum_text << "*";
+		nextIndex++;
+	}
+	// Second **kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Save the second kern spine as it does not exist yet in the
+	// output data.
+	// HTp savedKernToken = infile.token(index, nextIndex);
+	nextIndex++;
+	// Then store any non-kern spines between the second and third kern spines to
+	// append to the end of the data line later.
+	string postData;
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (!postData.empty()) {
+			postData += "\t";
+		}
+		nextIndex++;
+		postData += "*";
+	}
+	// Third kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Now print the third kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
 	}
-	if (m_exinterp.compare(0, 2, "**") != 0) {
-		if (m_exinterp.compare(0, 1, "*") != 0) {
-			m_exinterp = "**" + m_exinterp;
-		} else {
-			m_exinterp = "*" + m_exinterp;
-		}
+	fcount++;
+	m_humdrum_text << "*v";
+	nextIndex++;
+	// Now printed the saved second **kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
 	}
-
-}
-
-
-
-//////////////////////////////
-//
-// Tool_restfill::processFile --
-//
-
-void Tool_restfill::processFile(HumdrumFile& infile) {
-
-	vector<HTp> starts;
-	infile.getSpineStartList(starts, m_exinterp);
-	vector<bool> process(starts.size(), false);
-	for (int i=0; i<(int)starts.size(); i++) {
-		process[i] = hasBlankMeasure(starts[i]);
-		if (process[i]) {
-			starts[i]->setText("**temp-kern");
+	m_humdrum_text << "*v";
+	fcount++;
+	// Now print the non-kern spines after the third **kern spine (or rather just
+	// all spines including any other **kern spines, although current requirement
+	// is that there are only three **kern spines.
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		// HTp token = infile.token(index, nextIndex);
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
 		}
+		fcount++;
+		nextIndex++;
+		m_humdrum_text << "*";
 	}
-	infile.analyzeStructure();
-	for (int i=0; i<(int)starts.size(); i++) {
-		if (!process[i]) {
-			continue;
+	// Finally print any non-kern spines after the second **kern spine:
+	if (!postData.empty()) {
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
 		}
-		starts[i]->setText("**kern");
-		fillInRests(starts[i]);
+		fcount++;
+		m_humdrum_text << postData;
 	}
+	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_restfill::hasBlankMeasure --
+// Tool_sab2gs::printSwappedLine -- move the second **kern spine immediately after
+//    the third one, and move any non-kern spines after then end of the line.
 //
 
-bool Tool_restfill::hasBlankMeasure(HTp start) {
-	bool foundcontent = false;
-	HTp current = start;
-	int founddata = false;
-	while (current) {
-
-		if (current->isBarline()) {
-			if (founddata && !foundcontent) {
-				return true;
-			}
-			foundcontent = false;
-			founddata = false;
-			current = current->getNextToken();
-			continue;
-		}
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
+void Tool_sab2gs::printSwappedLine(HumdrumFile& infile, int index, vector<int>& ktracks) {
+	// First print all non-kern spines at the start of the line:
+	int nextIndex = 0;
+	int fcount = 0;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
 		}
-		founddata = true;
-		if (!current->isNull()) {
-			foundcontent = true;
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
 		}
-		current = current->getNextToken();
-
+		fcount++;
+		m_humdrum_text << token;
+		nextIndex++;
 	}
-	return false;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_restfill::fillInRests --
-//   Also deal with cases where the last measure does not end in a barline.
-//
-
-void Tool_restfill::fillInRests(HTp start) {
-	HTp current = start;
-	HTp firstcell = NULL;
-	int founddata = false;
-	bool foundcontent = false;
-	HumNum lasttime = 0;
-	HumNum currtime = 0;
-	HumNum duration = 0;
-	while (current) {
-		if (current->isBarline()) {
-			if (firstcell) {
-				lasttime = firstcell->getDurationFromStart();
-			}
-			currtime = getNextTime(current);
-			if (firstcell && founddata && !foundcontent) {
-				duration = currtime - lasttime;
-				addRest(firstcell, duration);
-			}
-			firstcell = NULL;
-			founddata = false;
-			foundcontent = false;
-			current = current->getNextToken();
-			lasttime = currtime;
-			continue;
-		}
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
+	// Must be on the first **kern spine:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Print the first **kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
+	}
+	fcount++;
+	m_humdrum_text << infile.token(index, nextIndex);
+	nextIndex++;
+	// Next print all non-kern spines after first **kern spine:
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
 		}
-		if (current->getDuration() == 0) {
-			// grace-note line, so ignore
-			current = current->getNextToken();
-			continue;
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
 		}
-		founddata = true;
-		if (!current->isNull()) {
-			foundcontent = true;
+		m_humdrum_text << token;
+		nextIndex++;
+	}
+	// Second **kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Save the second kern spine as it does not exist yet in the
+	// output data.
+	HTp savedKernToken = infile.token(index, nextIndex++);
+	// Then store any non-kern spines between the second and third kern spines to
+	// append to the end of the data line later.
+	string postData;
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
 		}
-		if (!firstcell) {
-			firstcell = current;
+		if (!postData.empty()) {
+			postData += "\t";
 		}
-		current = current->getNextToken();
+		nextIndex++;
+		postData += *token;
 	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_restfill::addRest --
-//
-
-void Tool_restfill::addRest(HTp cell, HumNum duration) {
-	if (!cell) {
+	// Third kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
 		return;
 	}
-	string text = Convert::durationToRecip(duration);
-	text += "r";
-	if (m_hiddenQ) {
-		text += "yy";
+	// Now print the third kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
 	}
-	cell->setText(text);
+	fcount++;
+	m_humdrum_text << infile.token(index, nextIndex++);
+	// Now printed the saved second **kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
+	}
+	m_humdrum_text << savedKernToken;
+	fcount++;
+	// Now print the non-kern spines after the third **kern spine (or rather just
+	// all spines including any other **kern spines, although current requirement
+	// is that there are only three **kern spines.
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, nextIndex);
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		nextIndex++;
+		m_humdrum_text << token;
+	}
+	// Finally print any non-kern spines after the second **kern spine:
+	if (!postData.empty()) {
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		m_humdrum_text << postData;
+	}
+	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_restfill::getNextTime --
+// Tool_sab2gs::printReducedLine -- remove the contents of the second **kern
+//    spine, and move any non-kernspines after it to become after the third **kern spine
 //
 
-HumNum Tool_restfill::getNextTime(HTp token) {
-	HTp current = token;
-	while (current) {
-		if (current->isData()) {
-			return current->getDurationFromStart();
+void Tool_sab2gs::printReducedLine(HumdrumFile& infile, int index, vector<int>& ktracks) {
+	// First print all non-kern spines at the start of the line:
+	int nextIndex = 0;
+	int fcount = 0;
+	for (int i=0; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
 		}
-		current = current->getNextToken();
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		m_humdrum_text << token;
+		nextIndex++;
 	}
-	return token->getOwner()->getOwner()->getScoreDuration();
-}
-
-
-
-
-
-
-/////////////////////////////////
-//
-// Tool_rid::Tool_rid -- Set the recognized options for the tool.
-//
-
-Tool_rid::Tool_rid(void) {
-   // Humdrum Toolkit classic rid options:
-   define("D|all-data=b",                  "remove all data records");
-   define("d|null-data=b",                 "remove null data records");
-   define("G|all-global=b",                "remove all global comments");
-   define("g|null-global=b",               "remove null global comments");
-   define("I|all-interpretation=b",        "remove all interpretation records");
-   define("i|null-interpretation=b",       "remove null interpretation records");
-   define("L|all-local-comment=b",         "remove all local comments");
-   define("l|1|null-local-comment=b",      "remove null local comments");
-   define("T|all-tandem-interpretation=b", "remove all tandem interpretations");
-   define("U|u=b",                         "remove unnecessary (duplicate ex. interps.");
-   define("k|consider-kern-only=b",        "for -d, only consider **kern spines.");
-   define("V=b",                           "negate filtering effect of program.");
-   define("H|no-humdrum-syntax=b",         "equivalent to -GLIMd.");
-
-   // additional options
-   define("M|all-barlines=b",              "remove measure lines");
-   define("C|all-comments=b",              "remove all comment lines");
-   define("c=b",                           "remove global and local comment lines");
-}
-
-
-
-/////////////////////////////////
-//
-// Tool_rid::run -- Do the main work of the tool.
-//
-
-bool Tool_rid::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+	// Must be on the first **kern spine:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
+		return;
 	}
-	return status;
-}
-
-
-bool Tool_rid::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	// Print the first **kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
 	}
-	return status;
-}
-
-
-bool Tool_rid::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	fcount++;
+	m_humdrum_text << infile.token(index, nextIndex++);
+	// Next print all non-kern spines after first **kern spine:
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		m_humdrum_text << token;
+		nextIndex++;
 	}
-	return status;
-}
-
-
-bool Tool_rid::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_rid::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
-//
-
-void Tool_rid::initialize(void) {
-   option_D = getBoolean("D");
-   option_d = getBoolean("d");
-   option_G = getBoolean("G");
-   option_g = getBoolean("g");
-   option_I = getBoolean("I");
-   option_i = getBoolean("i");
-   option_L = getBoolean("L");
-   option_l = getBoolean("l");
-   option_T = getBoolean("T");
-   option_U = getBoolean("U");
-   option_M = getBoolean("M");
-   option_C = getBoolean("C");
-   option_c = getBoolean("c");
-   option_k = getBoolean("k");
-   option_V = getBoolean("V");
-
-   if (getBoolean("no-humdrum-syntax")) {
-      // remove all Humdrum file structure
-      option_G = option_L = option_I = option_M = option_d = 1;
-   }
-}
-
-
-
-//////////////////////////////
-//
-// Tool_rid::processFile --
-//
-
-void Tool_rid::processFile(HumdrumFile& infile) {
-	int setcount = 1; // disabled for now.
-
-   HumRegex hre;
-   int revQ = option_V;
-
-   // if bibliographic/reference records are not suppressed
-   // print the !!!!SEGMENT: marker if present.
-   if ((setcount > 1) && (!option_G)) {
-      infile.printNonemptySegmentLabel(m_humdrum_text);
-   }
-
-   for (int i=0; i<infile.getLineCount(); i++) {
-      if (option_D && (infile[i].isBarline() || infile[i].isData())) {
-         // remove data lines if -D is specified
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_d) {
-         // remove null data lines if -d is specified
-         if (option_k && infile[i].isData() &&
-               infile[i].equalFieldsQ("**kern", ".")) {
-            // remove if only all **kern spines are null.
-            if (revQ) {
-               m_humdrum_text << infile[i] << "\n";
-            }
-            continue;
-         } else if (!option_k && infile[i].isData() &&
-               infile[i].isAllNull()) {
-            // remove null data lines if all spines are null.
-            if (revQ) {
-               m_humdrum_text << infile[i] << "\n";
-            }
-            continue;
-         }
-      }
-      if (option_G && (infile[i].isGlobalComment() ||
-            infile[i].isReference())) {
-         // remove global comments if -G is specified
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_g && hre.search(infile.token(i, 0), "^!!+\\s*$")) {
-         // remove empty global comments if -g is specified
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_I && infile[i].isInterpretation()) {
-         // remove all interpretation records
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_i && infile[i].isInterpretation() &&
-            infile[i].isAllNull()) {
-         // remove null interpretation records
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_L && infile[i].isLocalComment()) {
-         // remove all local comments
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_l && infile[i].isLocalComment() &&
-            infile[i].isAllNull()) {
-         // remove null local comments
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_T && (infile[i].isInterpretation() && !infile[i].isManipulator())) {
-         // remove tandem (non-manipulator) interpretations
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_U) {
-         // remove unnecessary (duplicate exclusive) interpretations
-         // HumdrumFile class does not allow duplicate ex. interps.
-         // continue;
-      }
+	// Second **kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Ignore the second kern spine as it does not exist yet in the
+	// output data.
+	nextIndex++;
+	// Then store any non-kern spines between the second and third kern spines to
+	// append to the end of the data line later.
+	string postData;
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, i);
+		if (token->isKern()) {
+			break;
+		}
+		if (!postData.empty()) {
+			postData += "\t";
+		}
+		nextIndex++;
+		postData += *token;
+	}
+	// Third kern spine must be **kern data:
+	if (!infile.token(index, nextIndex)->isKern()) {
+		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
+		return;
+	}
+	// Now print the third kern spine:
+	if (fcount > 0) {
+		m_humdrum_text << "\t";
+	}
+	fcount++;
+	m_humdrum_text << infile.token(index, nextIndex++);
+	// Now print the non-kern spines after the third **kern spine (or rather just
+	// all spines including any other **kern spines, although current requirement
+	// is that there are only three **kern spines.
+	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
+		HTp token = infile.token(index, nextIndex);
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		nextIndex++;
+		m_humdrum_text << token;
+	}
+	// Finally print any non-kern spines after the second **kern spine:
+	if (!postData.empty()) {
+		if (fcount > 0) {
+			m_humdrum_text << "\t";
+		}
+		fcount++;
+		m_humdrum_text << postData;
+	}
+	m_humdrum_text << endl;
+}
 
-      // non-classical options:
 
-      if (option_M && infile[i].isBarline()) {
-         // remove all measure lines
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_C && infile[i].isComment()) {
-         // remove all comments (local & global)
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
-      if (option_c && (infile[i].isLocalComment() ||
-            infile[i].isGlobalComment())) {
-         // remove all comments (local & global)
-         if (revQ) {
-            m_humdrum_text << infile[i] << "\n";
-         }
-         continue;
-      }
+//////////////////////////////
+//
+// Tool_sab2gs::adjustMiddleVoice --
+//
 
-      // got past all test, so print the current line:
-      if (!revQ) {
-         m_humdrum_text << infile[i] << "\n";
-      }
-   }
-}
+void Tool_sab2gs::adjustMiddleVoice(HTp spineStart) {
+	HTp current = spineStart;
+	// staff: +1 = top staff, -1 = bottom staff
+	// when on top staff, force stem down, or on bottom staff, force stem up
+	// when on bottom staff add "<" marker after pitch (or rest) to move to
+	// bottom staff.  Staff choice is selected by clef: clefG2 is for top staff
+	// and staffF4 is for bottom staff. Chords are not expected.
+	int staff = 0;
+	string replacement = "$1" + m_belowMarker;
+	HumRegex hre;
+	while (current) {
+		if (*current == "*-") {
+			break;
+		}
+		if (!m_downQ && current->isClef()) {
+			if (current->substr(0, 7) == "*clefG2") {
+				staff = 1;
+				// suppress clef:
+				string text = "*x" + current->substr(1);
+				current->setText(text);
+			} else if (current->substr(0, 7) == "*clefF4") {
+				staff = -1;
+				// suppress clef:
+				string text = "*x" + current->substr(1);
+				current->setText(text);
+			}
+		} else if (current->isInterpretation()) {
+			if (*current == "*down") {
+				staff = -1;
+			} else if (*current == "*Xdown") {
+				staff = 1;
+			}
+		} else if ((staff != 0) && current->isData()) {
+			if (current->isNull()) {
+				// nothing to do with token
+				current = current->getNextToken();
+				continue;
+			}
+			if (staff > 0) {
+				// force stems down or add stem down to non-rest notes
+				if (hre.search(current, "[/\\\\]")) {
+					string value = hre.replaceCopy(current, "\\", "/", "g");
+					if (value != *current) {
+						current->setText(value);
+					}
+					current = current->getNextToken();
+					continue;
+				} if (current->isRest()) {
+					current = current->getNextToken();
+					continue;
+				} else {
+					string value = *current;
+					value += "\\";
+					current->setText(value);
+					current = current->getNextToken();
+					continue;
+				}
 
+			} else if (staff < 0) {
+				// force stems up or add stem up to non-rest notes
+				if (hre.search(current, "[/\\\\]")) {
+					string value = hre.replaceCopy(current, "\\", "/", "g");
+					if (value != *current) {
+						current->setText(value);
+					}
+					current = current->getNextToken();
+					continue;
+				} if (current->isRest()) {
+					// Do not at stem direction to rests
+				} else {
+					// Force stem up (assuming not a chord, although it should not matter):
+					string value = hre.replaceCopy(current, "/", "$");
+					if (value != *current) {
+						current->setText(value);
+					}
+				}
+				// Add < after pitch (and accidental and qualifiers) to display
+				// on staff below.
+				m_hasCrossStaff = true;
+				string output = hre.replaceCopy(current, replacement, "([A-Ga-gr]+[-#nXYxy]*)", "g");
+				if (output != *current) {
+					current->setText(output);
+				}
+			}
+		}
+		current = current->getNextToken();
+	}
+}
 
 
 
 
 /////////////////////////////////
 //
-// Tool_rphrase::Tool_rphrase -- Set the recognized options for the tool.
+// Tool_satb2gs::Tool_satb2gs -- Set the recognized options for the tool.
 //
 
-Tool_rphrase::Tool_rphrase(void) {
-	define("a|average=b",            "calculate average length of rest-phrases by score");
-	define("A|all-average=b",        "calculate average length of rest-phrases for all scores");
-	define("B|no-breath=b",          "ignore breath interpretations");
-	define("c|composite|collapse=b", "collapse all voices into single part");
-	define("d|duration-unit=d:2.0",  "duration units, default: 2.0 (minims/half notes)");
-	define("f|filename=b",           "include filename in output analysis");
-	define("F|full-filename=b",      "include full filename location in output analysis");
-	define("I|no-info=b",            "do not display summary info");
-	define("l|longa=b",              "display minim length of longas");
-	define("m|b|measure|barline=b",  "include barline numbers in output analysis");
-	define("mark=b",                 "mark starts of phrases in score");
-	define("s|sort=b",               "sort phrases by short to long length");
-	define("S|reverse-sort=b",       "sort phrases by long to short length");
-	define("u|url-type=s",           "URL type (jrp, 1520s) for hyperlink");
-	define("z|squeeze=b",            "squeeze notation");
-	define("close=b",                "close details element initially");
+Tool_satb2gs::Tool_satb2gs(void) {
+	// no options
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_rphrase::run -- Do the main work of the tool.
+// Tool_satb2gs::run -- Do the main work of the tool.
 //
 
-bool Tool_rphrase::run(HumdrumFileSet& infiles) {
+bool Tool_satb2gs::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -118213,7 +122994,7 @@ bool Tool_rphrase::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_rphrase::run(const string& indata, ostream& out) {
+bool Tool_satb2gs::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -118225,7 +123006,7 @@ bool Tool_rphrase::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_rphrase::run(HumdrumFile& infile, ostream& out) {
+bool Tool_satb2gs::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -118236,7 +123017,7 @@ bool Tool_rphrase::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_rphrase::run(HumdrumFile& infile) {
+bool Tool_satb2gs::run(HumdrumFile& infile) {
 	initialize();
 	processFile(infile);
 	return true;
@@ -118246,210 +123027,59 @@ bool Tool_rphrase::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_rphrase::finally --
-//
-
-void Tool_rphrase::finally(void) {
-	if (!m_markQ) {
-		if (m_allAverageQ) {
-			if (m_compositeQ) {
-				double average = m_sumComposite / m_pcountComposite;
-				m_free_text << "Composite average phrase length: " << average << " minims" << endl;
-			} else {
-				double average = m_sum / m_pcount;
-				m_free_text << "All average phrase length: " << average << " minims" << endl;
-			}
-		}
-	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_rphrase::initialize --
+// Tool_satb2gs::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_rphrase::initialize(void) {
-	m_barlineQ      = getBoolean("measure");
-	m_allAverageQ   = getBoolean("all-average");
-	m_breathQ       = !getBoolean("no-breath");
-	m_compositeQ    = getBoolean("collapse");
-	m_filenameQ     = getBoolean("filename");
-	m_fullFilenameQ = getBoolean("full-filename");
-	m_urlType       = getString("url-type");
-	m_longaQ        = getBoolean("longa");
-	#ifndef __EMSCRIPTEN__
-		m_markQ         = getBoolean("mark");
-		m_averageQ      = getBoolean("average");
-	#else
-		m_markQ         = !getBoolean("mark");
-		m_averageQ      = !getBoolean("average");
-	#endif
-	m_sortQ         = getBoolean("sort");
-	m_reverseSortQ  = getBoolean("reverse-sort");
-	m_durUnit       = getDouble("duration-unit");
-	m_infoQ         = !getDouble("no-info");
-	m_squeezeQ      = getBoolean("squeeze");
-	m_closeQ        = getBoolean("close");
+void Tool_satb2gs::initialize(void) {
+	// do nothing
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::processFile --
+// Tool_satb2gs::processFile --
 //
 
-void Tool_rphrase::processFile(HumdrumFile& infile) {
-	if (m_filenameQ) {
-		m_filename = infile.getFilename();
-		HumRegex hre;
-		hre.replaceDestructive(m_filename, "", ".*\\/");
-		hre.replaceDestructive(m_filename, "", "\\.krn$");
-	} else if (m_fullFilenameQ) {
-		m_filename = infile.getFilename();
-	}
-	vector<HTp> kernStarts = infile.getKernSpineStartList();
-	vector<Tool_rphrase::VoiceInfo> voiceInfo(kernStarts.size());
-	Tool_rphrase::VoiceInfo compositeInfo;
-
-	if (m_compositeQ) {
-		fillCompositeInfo(compositeInfo, infile);
-	} else {
-		fillVoiceInfo(voiceInfo, kernStarts, infile);
-	}
-
-	if (m_longaQ) {
-		markLongaDurations(infile);
-	}
-
-	if ((!m_allAverageQ) && (!m_markQ)) {
-		if (m_line == 1) {
-			if (m_compositeQ) {
-				m_free_text << "Filename";
-				if (!m_urlType.empty()) {
-					m_free_text << "\tVHV";
-				}
-				m_free_text << "\tVoice";
-				m_free_text << "\tComp seg count";
-				if (m_averageQ) {
-					m_free_text << "\tAvg comp seg dur";
-				}
-				m_free_text << "\tComposite seg durs";
-				m_free_text << endl;
-			} else {
-				m_free_text << "Filename";
-				if (!m_urlType.empty()) {
-					m_free_text << "\tVHV";
-				}
-				m_free_text << "\tVoice";
-				m_free_text << "\tSounding dur";
-				m_free_text << "\tResting dur";
-				m_free_text << "\tTotal dur";
-				m_free_text << "\tSeg count";
-				if (m_averageQ) {
-					m_free_text << "\tSeg dur average";
-				}
-				m_free_text << "\tSegment durs";
-				m_free_text << endl;
-			}
-		}
-		if (m_compositeQ) {
-			if (m_compositeQ) {
-				m_line++;
-			}
-			printVoiceInfo(compositeInfo);
-		} else {
-			printVoiceInfo(voiceInfo);
-		}
-	}
-
-	if (m_markQ) {
-		outputMarkedFile(infile, voiceInfo, compositeInfo);
-		if (m_squeezeQ) {
-			m_humdrum_text << "!!!verovio: evenNoteSpacing" << endl;
-		}
-	}
-
-}
-
-
-
-//////////////////////////
-//
-// Tool_rphrase::markLongaDuratios --
-//
+void Tool_satb2gs::processFile(HumdrumFile& infile) {
+	vector<vector<int>> tracks;
+	getTrackInfo(tracks, infile);
 
-void Tool_rphrase::markLongaDurations(HumdrumFile& infile) {
-	string longrdf;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].hasSpines()) {
-			continue;
-		}
-		if (!infile[i].isReferenceRecord()) {
-			continue;
-		}
-		string key = infile[i].getReferenceKey();
-		if (key != "RDF**kern") {
-			continue;
-		}
-		string value = infile[i].getReferenceValue();
-		HumRegex hre;
-		if (hre.search(value, "^\\s*([^\\s=]+)\\s*=.*long")) {
-			longrdf = hre.getMatch(1);
-			break;
-		}
+	if ((tracks[1].size() != 2) || (tracks[3].size() != 2)) {
+		cerr << "Warning: not processing data since there must be at least four **kern spines" << endl;
+		return;
 	}
 
-	if (longrdf.empty()) {
+	bool goodHeader = validateHeader(infile);
+	if (!goodHeader) {
+		cerr << "Warning: no spine manipulations allows within header, not processing file" << endl;
 		return;
 	}
 
+	bool dataQ = false;
 	for (int i=0; i<infile.getLineCount(); i++) {
 		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << endl;
 			continue;
 		}
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->find(longrdf) != string::npos) {
-				HumNum duration = token->getTiedDuration();
-				stringstream value;
-				value.str("");
-				value << duration.getFloat() / m_durUnit;
-				token->setValue("auto", "rphrase-longa", value.str());
+		if (infile[i].isData()) {
+			if (!dataQ) {
+				printSpineSplitLine(tracks);
 			}
+			dataQ = true;
 		}
-	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_rphrase::outputMarkedFile --
-//
-
-void Tool_rphrase::outputMarkedFile(HumdrumFile& infile, vector<Tool_rphrase::VoiceInfo>& voiceInfo,
-		Tool_rphrase::VoiceInfo& compositeInfo) {
-	m_free_text.clear();
-	m_free_text.str("");
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			m_humdrum_text << infile[i] << endl;
-		} else {
-			printDataLine(infile, i);
+		if (!dataQ) {
+			printHeaderLine(infile, i, tracks);
+			continue;
 		}
-	}
-
-	if (m_infoQ) {
-		printEmbeddedVoiceInfo(voiceInfo, compositeInfo, infile);
+		HTp token = infile.token(i, 0);
+		if (*token == "*-") {
+			printSpineMergeLine(tracks);
+			printTerminatorLine(tracks);
+			continue;
+		}
+		printRegularLine(infile, i, tracks);
 	}
 }
 
@@ -118457,491 +123087,504 @@ void Tool_rphrase::outputMarkedFile(HumdrumFile& infile, vector<Tool_rphrase::Vo
 
 //////////////////////////////
 //
-// Tool_rphrase::printDataLine --
+// Tool_satb2gs::printRegularLine -- print a regular line
+//   (between first data line and before terminator line).
 //
 
-void Tool_rphrase::printDataLine(HumdrumFile& infile, int index) {
-
-	bool hasLonga = false;
-	if (m_longaQ) {
-		for (int j=0; j<infile[index].getFieldCount(); j++) {
-			HTp token = infile.token(index, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			string lotext = token->getValue("auto", "rphrase-longa");
-			if (!lotext.empty()) {
-				hasLonga = true;
-				break;
-			}
-		}
-	}
-
-
-	bool hasLo = false;
-	for (int j=0; j<infile[index].getFieldCount(); j++) {
-		HTp token = infile.token(index, j);
-		if (!token->isKern()) {
-			continue;
-		}
-		string lotext = token->getValue("auto", "rphrase-start");
-		if (!lotext.empty()) {
-			hasLo = true;
-			break;
-		}
-	}
-
-	// search for composite phrase info
-	bool hasGlo = false;
-	if (infile[index].isData()) {
-		string glotext = infile[index].getValue("auto", "rphrase-composite-start");
-		if (!glotext.empty()) {
-			hasGlo = true;
-		}
-	}
-
-	if (hasGlo) {
-		string glotext = infile[index].getValue("auto", "rphrase-composite-start");
-		m_humdrum_text << "!!LO:TX:b:B:color=" << m_compositeLengthColor << ":t=" << glotext << endl;
-	}
+void Tool_satb2gs::printRegularLine(HumdrumFile& infile, int line,
+		vector<vector<int>>& tracks) {
 
-	if (hasLonga) {
-		for (int j=0; j<infile[index].getFieldCount(); j++) {
-			HTp token = infile.token(index, j);
-			if (!token->isKern()) {
-				m_humdrum_text << "!";
-			} else {
-				string value = token->getValue("auto", "rphrase-longa");
-				if (value.empty()) {
-					m_humdrum_text << "!";
-				} else {
-					m_humdrum_text << "!LO:TX:a:B:color=silver:t=" << value;
-				}
-			}
-			if (j < infile[index].getFieldCount() - 1) {
-				m_humdrum_text << "\t";
-			}
-		}
-		m_humdrum_text << endl;
+	int spinecount = infile[line].getFieldCount();
+	int track;
+	HTp token;
+	vector<vector<vector<HTp>>> tokens;
+	tokens.resize(5);
+	for (int i=0; i<(int)tracks.size(); i++) {
+		tokens[i].resize(tracks[i].size());
 	}
 
-	if (hasLo) {
-		for (int j=0; j<infile[index].getFieldCount(); j++) {
-			HTp token = infile.token(index, j);
-			if (!token->isKern()) {
-				m_humdrum_text << "!";
-			} else {
-				string value = token->getValue("auto", "rphrase-start");
-				if (value.empty()) {
-					m_humdrum_text << "!";
-				} else {
-					m_humdrum_text << "!LO:TX:a:B:color=" << m_voiceLengthColor << ":t=" << value;
+	// store tokens in output order:
+	for (int i=0; i<(int)tracks.size(); i++) {
+		for (int j=0; j<(int)tracks[i].size(); j++) {
+			int target = tracks[i][j];
+			for (int k=0; k<spinecount; k++) {
+				token = infile.token(line, k);
+				track = token->getTrack();
+				if (track != target) {
+					continue;
 				}
-			}
-			if (j < infile[index].getFieldCount() - 1) {
-				m_humdrum_text << "\t";
+				tokens[i][j].push_back(token);
 			}
 		}
-		m_humdrum_text << endl;
 	}
 
-	m_humdrum_text << infile[index] << endl;
-}
-
+	int counter = 0;
+	HTp top;
+	HTp bot;
+	HTp inner;
+	HTp outer;
+	bool suppressQ;
 
+	// now print in output order, but hide fermatas
+	// in the alto and tenor parts if there are fermatas
+	// int the soprano and bass parts respectively.
+	for (int i=0; i<(int)tokens.size(); i++) {
+		for (int j=0; j<(int)tokens[i].size(); j++) {
+			switch (i) {
+				case 0:
+				case 2:
+				case 4:
+					// non-kern spines
+					for (int k=0; k<(int)tokens[i][j].size(); k++) {
+						m_humdrum_text << tokens[i][j][k];
+						counter++;
+						if (counter < spinecount) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
 
-//////////////////////////////
-//
-// Tool_rphrase::getCompositeStates --
-//
+				case 1:
+				case 3:
+					top = tokens[i][0][0];
+					bot = tokens[i][1][0];
+					if (i == 1) {
+						// tenor: top is inner
+						inner = top;
+						outer = bot;
+					} else {
+						// alto: bottom is inner
+						inner = bot;
+						outer = top;
+					}
+					if (inner->hasFermata() && outer->hasFermata()) {
+						suppressQ = true;
+					} else {
+						suppressQ = false;
+					}
 
-void Tool_rphrase::getCompositeStates(vector<int>& noteStates, HumdrumFile& infile) {
-	noteStates.resize(infile.getLineCount());
-	fill(noteStates.begin(), noteStates.end(), -1);
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		int value = 0;
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
-				continue;
-			}
-			if (token->isRest()) {
-				continue;
-			} else if (token->isNull()) {
-				HTp resolve = token->resolveNull();
-				if (!resolve) {
-					continue;
-				} else if (resolve->isRest()) {
-					continue;
-				} else {
-					value = 1;
+					for (int k=0; k<(int)tokens[i][j].size(); k++) {
+						token = tokens[i][j][k];
+						if (suppressQ && ((void*)token == (void*)inner)) {
+							string value = *token;
+							// Make fermata invisible by adding 'y' after it:
+							for (int m=0; m<(int)value.size(); m++) {
+								m_humdrum_text << value[m];
+								if (value[m] == ';') {
+									if (m < (int)value.size() - 1) {
+										if (value.at(m+1) != 'y') {
+											m_humdrum_text << 'y';
+										}
+									} else {
+											m_humdrum_text << 'y';
+									}
+								}
+							}
+						} else {
+							m_humdrum_text << token;
+						}
+						counter++;
+						if (counter < spinecount) {
+							m_humdrum_text << "\t";
+						}
+					}
 					break;
-				}
-			} else {
-				value = 1;
-				break;
 			}
 		}
-		noteStates[i] = value;
 	}
+
+	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::printVoiceInfo --
+// Tool_satb2gs::printTerminatorLine --  Print the terminator line in the
+//   output data.
 //
 
-void Tool_rphrase::printVoiceInfo(vector<Tool_rphrase::VoiceInfo>& voiceInfo) {
-	for (int i=(int)voiceInfo.size() - 1; i>=0; i--) {
-		if (!m_compositeQ) {
-			m_line++;
+void Tool_satb2gs::printTerminatorLine(vector<vector<int>>& tracks) {
+	int count = getNewTrackCount(tracks);
+	for (int i=0; i<count; i++) {
+		m_humdrum_text << "*-";
+		if (i < count - 1) {
+			m_humdrum_text << "\t";
 		}
-		printVoiceInfo(voiceInfo[i]);
 	}
+	m_humdrum_text << endl;
 }
 
 
+
 //////////////////////////////
 //
-// Tool_rphrase::printHyperlink --
+// Tool_satb2gs::printSpineSplitLine --
 //
 
-void Tool_rphrase::printHyperlink(const string& urlType) {
-	string command = "rphrase";
-	string options;
-	options += "l";
-	options+= "z";
-	if (m_compositeQ) {
-		options += "c";
-	}
-	if (m_sortQ) {
-		options += "s";
-	} else if (m_reverseSortQ) {
-		options += "S";
-	}
-	if (!options.empty()) {
-		command += "%20-";
-		command += options;
-	}
+void Tool_satb2gs::printSpineSplitLine(vector<vector<int>>& tracks) {
+	int count = getNewTrackCount(tracks);
+	int counter = 0;
 
-	if (urlType == "jrp") {
-		m_free_text << "=HYPERLINK(\"https://verovio.humdrum.org/?file=jrp/\" & ";
-		m_free_text << "LEFT(A" << m_line << ", 3) & \"/\" & A" << m_line << " & ";
-		m_free_text << "\".krn&filter=" << command << "&k=ey\", LEFT(A" << m_line;
-		m_free_text << ", FIND(\"-\", A" << m_line << ") - 1))";
-	} else if (urlType == "1520s") {
-		m_free_text << "=HYPERLINK(\"https://verovio.humdrum.org/?file=1520s/\" & A";
-		m_free_text << m_line << " & \".krn&filter=" << command << "&k=ey\", LEFT(A";
-		m_free_text << m_line << ", FIND(\"-\", A" << m_line << ") - 1))";
+	for (int i=0; i<(int)tracks.size(); i++) {
+		switch (i) {
+			case 0:
+			case 2:
+			case 4:
+				for (int j=0; j<(int)tracks[i].size(); j++) {
+					m_humdrum_text << "*";
+					counter++;
+					if (counter < count) {
+						m_humdrum_text << "\t";
+					}
+				}
+				break;
+			case 1:
+			case 3:
+				m_humdrum_text << "*^";
+				counter++;
+				if (counter < count) {
+					m_humdrum_text << "\t";
+				}
+				break;
+		}
 	}
+	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::printVoiceInfo --
+// Tool_satb2gs::printSpineMergeLine --
 //
 
-void Tool_rphrase::printVoiceInfo(Tool_rphrase::VoiceInfo& voiceInfo) {
-	if (m_filenameQ) {
-		m_free_text << m_filename << "\t";
-	}
-	if (!m_urlType.empty()) {
-		printHyperlink(m_urlType);
-		m_free_text << "\t";
-	}
-	m_free_text << voiceInfo.name << "\t";
-
-	if (!m_compositeQ) {
-		double sounding = 0.0;
-		double resting = 0.0;
-		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-			if (voiceInfo.phraseDurs[i] > 0.0) {
-				sounding += voiceInfo.phraseDurs[i];
-			}
-			if (voiceInfo.restsBefore[i] > 0.0) {
-				resting += voiceInfo.restsBefore[i];
-			}
-		}
-		double total = sounding + resting;
-		// double sounding_percent = int (sounding/total * 100.0 + 0.5);
-		// double resting_percent = int (sounding/total * 100.0 + 0.5);
-
-		m_free_text << twoDigitRound(sounding) << "\t";
-		m_free_text << twoDigitRound(resting) << "\t";
-		m_free_text << twoDigitRound(total) << "\t";
-	}
+void Tool_satb2gs::printSpineMergeLine(vector<vector<int>>& tracks) {
+	int count = getNewTrackCount(tracks);
+	count += 2;
+	int counter;
 
-	m_free_text << voiceInfo.phraseDurs.size() << "\t";
+	if (!tracks[2].empty()) {
+		// do not need to place merges on separate lines since they are
+		// separated by non-kern spine(s) between bass and soprano subspines.
 
-	if (m_averageQ) {
-		double sum = 0;
-		int count = 0;
-		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-			count++;
-			sum += voiceInfo.phraseDurs.at(i);
+		counter = 0;
+		for (int i=0; i<(int)tracks.size(); i++) {
+			switch (i) {
+				case 0:
+				case 2:
+				case 4:
+					for (int j=0; j<(int)tracks[i].size(); j++) {
+						m_humdrum_text << "*";
+						counter++;
+						if (counter < count) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
+				case 1:
+				case 3:
+					for (int j=0; j<(int)tracks[i].size(); j++) {
+						m_humdrum_text << "*v";
+						counter++;
+						if (counter < count) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
+			}
 		}
-		m_free_text << int(sum / count * 100.0 + 0.5)/100.0 << "\t";
-	}
+		m_humdrum_text << endl;
 
-	if (m_sortQ || m_reverseSortQ) {
-		vector<pair<double, int>> sortList;
-		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-			sortList.emplace_back(voiceInfo.phraseDurs[i], i);
-		}
-		if (m_sortQ) {
-			sort(sortList.begin(), sortList.end(),
-				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
-					return a.first < b.first;
-			});
-		} else if (m_reverseSortQ) {
-			sort(sortList.begin(), sortList.end(),
-				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
-					return a.first > b.first;
-			});
-		}
+	} else {
+		// Merges for tenor/bass and soprano/alto need to be placed
+		// on separate lines.
 
-		for (int i=0; i<(int)sortList.size(); i++) {
-			int ii = sortList[i].second;
-			if (m_barlineQ) {
-				m_free_text << "m" << voiceInfo.barStarts.at(ii) << ":";
-			}
-			m_free_text << twoDigitRound(voiceInfo.phraseDurs.at(ii));
-			if (i < (int)sortList.size() - 1) {
-				m_free_text << " ";
+		// First merge tenor/bass (tracks[1])
+		counter = 0;
+		for (int i=0; i<(int)tracks.size(); i++) {
+			switch (i) {
+				case 0:
+				case 2:
+				case 3:
+				case 4:
+					for (int j=0; j<(int)tracks[i].size(); j++) {
+						m_humdrum_text << "*";
+						counter++;
+						if (counter < count) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
+				case 1:
+					for (int j=0; j<(int)tracks[i].size(); j++) {
+						m_humdrum_text << "*v";
+						counter++;
+						if (counter < count) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
 			}
 		}
-	} else {
-		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-			if (voiceInfo.restsBefore.at(i) > 0) {
-				m_free_text << "r:" << twoDigitRound(voiceInfo.restsBefore.at(i)) << " ";
-			} else if (i > 0) {
-				// force display r:0 for section boundaries.
-				m_free_text << "r:" << twoDigitRound(voiceInfo.restsBefore.at(i)) << " ";
-			}
-			if (m_barlineQ) {
-				m_free_text << "m" << voiceInfo.barStarts.at(i) << ":";
-			}
-			m_free_text << twoDigitRound(voiceInfo.phraseDurs.at(i));
-			if (i < (int)voiceInfo.phraseDurs.size() - 1) {
-				m_free_text << " ";
+		m_humdrum_text << endl;
+
+		// Now merge soprano/alto (tracks[3])
+		count--;
+		counter = 0;
+		for (int i=0; i<(int)tracks.size(); i++) {
+			switch (i) {
+				case 0:
+				case 2:
+				case 4:
+					for (int j=0; j<(int)tracks[i].size(); j++) {
+						m_humdrum_text << "*";
+						counter++;
+						if (counter < count) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
+				case 1:
+					m_humdrum_text << "*";
+					m_humdrum_text << "\t";
+					counter++;
+					break;
+				case 3:
+					for (int j=0; j<(int)tracks[i].size(); j++) {
+						m_humdrum_text << "*v";
+						counter++;
+						if (counter < count) {
+							m_humdrum_text << "\t";
+						}
+					}
+					break;
 			}
 		}
+		m_humdrum_text << endl;
 	}
-
-	m_free_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::printEmbeddedVoiceInfo --
+// Tool_satb2gs::getNewTrackCount -- Return the number of tracks (spines)
+//   in the output data (not counting subspines).
 //
 
-void Tool_rphrase::printEmbeddedVoiceInfo(vector<Tool_rphrase::VoiceInfo>& voiceInfo, Tool_rphrase::VoiceInfo& compositeInfo, HumdrumFile& infile) {
-
-	m_humdrum_text << "!!@@BEGIN: PREHTML" << endl;;
-
-	m_humdrum_text << "!!@SCRIPT:" << endl;
-	m_humdrum_text << "!!   function rphraseGotoMeasure(measure) {" << endl;
-	m_humdrum_text << "!!      let target = `svg .measure.m-${measure}`;" << endl;
-	m_humdrum_text << "!!      let element = document.querySelector(target);" << endl;
-	m_humdrum_text << "!!      if (element) {" << endl;
-	m_humdrum_text << "!!         element.scrollIntoViewIfNeeded({ behavior: 'smooth' });" << endl;
-	m_humdrum_text << "!!     }" << endl;
-	m_humdrum_text << "!!   }" << endl;
-
-	m_humdrum_text << "!!@CONTENT:\n";
-	if (m_compositeQ) {
-		m_humdrum_text << "!!<style> .PREHTML .composite ul .rest { color: #ccc; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML .composite ul .measure { color: #ccc; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML .composite ul .length { cursor: pointer; font-weight: bold; color: " << m_compositeLengthColor << "; } </style>" << endl;
-	} else {
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase .rest { color: #ccc; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase .measure { color: #ccc; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase .length { cursor: pointer; font-weight: bold; color: " << m_voiceLengthColor << "; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase { border-collapse: collapse; </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase th, .PREHTML table.rphrase td { vertical-align: top; padding-right: 10px; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase tr { border-bottom: 1px solid #ccc; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase tr th:last-child, .PREHTML table.rphrase tr td:last-child { padding-right: 0; } </style>" << endl;
-
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.average { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.segments { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.sounding { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase th.resting { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.average { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.segments { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.sounding { text-align: right; } </style>" << endl;
-		m_humdrum_text << "!!<style> .PREHTML table.rphrase td.resting { text-align: right; } </style>" << endl;
-	}
-	m_humdrum_text << "!!<style> .PREHTML details { position: relative; padding-left: 20px; } </style>" << endl;
-	m_humdrum_text << "!!<style> .PREHTML summary { font-size: 1.5rem; cursor: pointer; list-style: none; } </style>" << endl;
-	m_humdrum_text << "!!<style> .PREHTML summary::before { content: '▶'; display: inline-block; width: 2em; margin-left: -1.5em; text-align: center; } </style>" << endl;
-	m_humdrum_text << "!!<style> .PREHTML details[open] summary::before { content: '▼'; } </style>" << endl;
-
-	if (m_compositeQ) {
-		m_humdrum_text << "!!<details";
-		if (!m_closeQ) {
-			m_humdrum_text << " open";
-		}
-		m_humdrum_text << "><summary>Composite rest phrasing</summary>\n";
-	} else {
-		m_humdrum_text << "!!<details";
-		if (!m_closeQ) {
-			m_humdrum_text << " open";
-		}
-		m_humdrum_text << "><summary>Voice rest phrasing</summary>\n";
-	}
-	if (m_compositeQ) {
-		printEmbeddedCompositeInfo(compositeInfo, infile);
-	} else {
-		if (voiceInfo.size() > 0) {
-			m_humdrum_text << "!!<table class='rphrase'>" << endl;
-			m_humdrum_text << "!!<tr><th class='voice'>Voice</th><th class='sounding'>Sounding</th><th class='resting'>Resting</th><th class='segments'>Segments</th><th class='average'>Average</th><th class='segment-durations'>Segment durations</th></tr>" << endl;
-			for (int i=(int)voiceInfo.size() - 1; i>=0; i--) {
-				printEmbeddedIndividualVoiceInfo(voiceInfo[i], infile);
-			}
-			m_humdrum_text << "!!</table>" << endl;
-			printEmbeddedVoiceInfoSummary(voiceInfo, infile);
+int Tool_satb2gs::getNewTrackCount(vector<vector<int>>& tracks) {
+	int sum = 0;
+	for (int i=0; i<(int)tracks.size(); i++) {
+		for (int j=0; j<(int)tracks[i].size(); j++) {
+			sum++;
 		}
 	}
-	m_humdrum_text << "!!</details>" << endl;
-	m_humdrum_text << "!!@@END: PREHTML" << endl;
+	// remove two spines that were merged into two others:
+	sum -= 2;
+	return sum;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::printEmbeddedCompositeInfo --
+// Tool_satb2gs::printHeaderLine --
 //
 
-void Tool_rphrase::printEmbeddedCompositeInfo(Tool_rphrase::VoiceInfo& compositeInfo, HumdrumFile& infile) {
-
-	m_humdrum_text << "!!<div class='composite'>" << endl;
-	m_humdrum_text << "!!<ul>" << endl;
-	m_humdrum_text << "!!<li>Composite segment count: " << compositeInfo.phraseDurs.size() << "</li>" << endl;
-
-	if (!compositeInfo.phraseDurs.empty()) {
-		m_humdrum_text << "!!<li>Composite segment duration";
-		if (compositeInfo.phraseDurs.size() != 1) {
-			m_humdrum_text << "s";
-		}
-		m_humdrum_text << ": ";
-		if (m_sortQ || m_reverseSortQ) {
-			vector<pair<double, int>> sortList;
-			for (int i=0; i<(int)compositeInfo.phraseDurs.size(); i++) {
-				sortList.emplace_back(compositeInfo.phraseDurs[i], i);
-			}
-			if (m_sortQ) {
-				sort(sortList.begin(), sortList.end(),
-					[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
-						return a.first < b.first;
-				});
-			} else if (m_reverseSortQ) {
-				sort(sortList.begin(), sortList.end(),
-					[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
-						return a.first > b.first;
-				});
-			}
+void Tool_satb2gs::printHeaderLine(HumdrumFile& infile, int line,
+		vector<vector<int>>& tracks) {
+	int count = infile.getMaxTrack() - 2;
 
-			for (int i=0; i<(int)sortList.size(); i++) {
-				int ii = sortList[i].second;
-				if (m_barlineQ) {
-					m_humdrum_text << "m" << compositeInfo.barStarts.at(ii) << ":";
-				}
-				m_humdrum_text << "<span class='length' title='measure " << compositeInfo.barStarts.at(ii)
-				               << "' onclick='rphraseGotoMeasure(" << compositeInfo.barStarts.at(ii)
-				               << ")' >" << twoDigitRound(compositeInfo.phraseDurs.at(ii)) << "</span>";
-				if (i < (int)sortList.size() - 1) {
-					m_humdrum_text << " ";
-				}
-			}
-		} else {
-			for (int i=0; i<(int)compositeInfo.phraseDurs.size(); i++) {
-				if (compositeInfo.restsBefore.at(i) > 0) {
-					m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(compositeInfo.restsBefore.at(i)) << "</span> ";
-				} else if (i > 0) {
-					// force display r:0 for section boundaries.
-					m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(compositeInfo.restsBefore.at(i)) << "</span> ";
-				}
-				if (m_barlineQ) {
-					m_humdrum_text << "<span class='measure'>m" << compositeInfo.barStarts.at(i) << ":</span>";
+	HTp token;
+	int counter = 0;
+	for (int i=0; i<(int)tracks.size(); i++) {
+		switch (i) {
+			case 0:
+			case 2:
+			case 4:
+				for (int j=0; j<(int)tracks[i].size(); j++) {
+					token = infile.token(line, tracks[i][j]-1);
+					m_humdrum_text << token;
+					counter++;
+					if (counter < count) {
+						m_humdrum_text << "\t";
+					}
 				}
-				m_humdrum_text << "<span class='length' title='measure " << compositeInfo.barStarts.at(i)
-				               << "' onclick='rphraseGotoMeasure(" << compositeInfo.barStarts.at(i)
-				               << ")' >" << twoDigitRound(compositeInfo.phraseDurs.at(i)) << "</span>";
-				if (i < (int)compositeInfo.phraseDurs.size() - 1) {
-					m_humdrum_text << " ";
+				break;
+
+			case 1:
+			case 3:
+				token = infile.token(line, tracks[i][0]-1);
+				if (token->isInstrumentName()) {
+					// suppress instrument names, but keep blank name
+					// to force indent.
+					m_humdrum_text << "*I\"";
+				} else if (token->isInstrumentAbbreviation()) {
+					// suppress instrument abbreviations
+					m_humdrum_text << "*";
+				} else if (token->isInstrumentDesignation()) {
+					// suppress instrument designations (such as *Itenor)
+					m_humdrum_text << "*";
+				} else if (token->isClef()) {
+					vector<HTp> clefs = getClefs(infile, line);
+					if (i == 1) {
+						if (clefs.size() == 4) {
+							m_humdrum_text << clefs[0];
+						} else {
+							m_humdrum_text << "*clefF4";
+						}
+					} else {
+						if (clefs.size() == 4) {
+							m_humdrum_text << clefs.back();
+						} else {
+							m_humdrum_text << "*clefG2";
+						}
+					}
+				} else {
+					m_humdrum_text << token;
 				}
-			}
+				counter++;
+				if (counter < count) {
+					m_humdrum_text << "\t";
+				}
+				break;
 		}
-		m_humdrum_text << "</li>" << endl;
+	}
+	m_humdrum_text << endl;
+}
 
-		if (m_averageQ && (compositeInfo.phraseDurs.size() > 1)) {
-			double sum = 0;
-			int count = 0;
-			for (int i=0; i<(int)compositeInfo.phraseDurs.size(); i++) {
-				count++;
-				sum += compositeInfo.phraseDurs.at(i);
-			}
-			double average = int(sum / count * 100.0 + 0.5)/100.0;
-			m_humdrum_text << "!!<li>Average composite segment durations: " << average << "</li>" << endl;
-		}
 
-		m_humdrum_text << "!!<li>Voices: " << getVoiceInfo(infile) << "</li>" << endl;
 
-		if (m_durUnit != 2.0) {
-			m_humdrum_text << "!!<li>Duration unit: " << m_durUnit << "</li>" << endl;
+//////////////////////////////
+//
+// Tool_satb2gs::getClefs -- get a list of the clefs on the current line.
+//
+
+vector<HTp> Tool_satb2gs::getClefs(HumdrumFile& infile, int line) {
+	vector<HTp> output;
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		HTp token = infile[line].token(i);
+		if (!token->isKern()) {
+			continue;
+		}
+		if (token->isClef()) {
+			output.push_back(token);
 		}
 	}
-
-	m_humdrum_text << "!!</ul>" << endl;
-	m_humdrum_text << "!!</div>" << endl;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::getVoiceInfo --
+// Tool_satb2gs::getTrackInfo --
+//     tracks 0 = list of spines before bass **kern spine
+//     tracks 1 = tenor and then bass **kern track numbers
+//     tracks 2 = aux. spines after after tenor and then after bass
+//     tracks 3 = soprano and then alto **kern track numbers
+//     tracks 4 = aux. spines after after soprano and then after alto
 //
 
-string Tool_rphrase::getVoiceInfo(HumdrumFile& infile) {
-	vector<HTp> kspines = infile.getKernSpineStartList();
-	string vcount = to_string(kspines.size());
-	string ocount;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
+void Tool_satb2gs::getTrackInfo(vector<vector<int>>& tracks, HumdrumFile& infile) {
+	tracks.resize(5);
+	for (int i=0; i<(int)tracks.size(); i++) {
+		tracks[i].clear();
+	}
+	vector<HTp> sstarts;
+	infile.getSpineStartList(sstarts);
+	int track;
+
+	// fill in tracks[0]: spines before first **kern spine
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (sstarts[i]->isKern()) {
 			break;
 		}
-		if (infile[i].isReferenceRecord()) {
-			string key = infile[i].getReferenceKey();
-			if (key == "voices") {
-				ocount = infile[i].getReferenceValue();
-			}
+		track = sstarts[i]->getTrack();
+		tracks[0].push_back(track);
+	}
+
+	int kcount = 0;
+
+	kcount = 0;
+	// Store tracks related to the tenor part:
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (sstarts[i]->isKern()) {
+			kcount++;
+		}
+		if (kcount > 2) {
+			break;
+		}
+		if (kcount < 2) {
+			continue;
+		}
+		track = sstarts[i]->getTrack();
+		if (sstarts[i]->isKern()) {
+			tracks[1].push_back(track);
+		} else {
+			tracks[2].push_back(track);
 		}
 	}
 
-	if (ocount.empty()) {
-		return vcount;
+	kcount = 0;
+	// Store tracks related to the bass part:
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (sstarts[i]->isKern()) {
+			kcount++;
+		}
+		if (kcount > 1) {
+			break;
+		}
+		if (kcount < 1) {
+			continue;
+		}
+		track = sstarts[i]->getTrack();
+		if (sstarts[i]->isKern()) {
+			tracks[1].push_back(track);
+		} else {
+			tracks[2].push_back(track);
+		}
 	}
 
-	if (ocount != vcount) {
-		string output = ocount;
-		output += "(";
-		output += vcount;
-		output += ")";
-		return output;
-	} else {
-		return vcount;
+	kcount = 0;
+	// Store tracks related to the soprano part:
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (sstarts[i]->isKern()) {
+			kcount++;
+		}
+		if (kcount > 4) {
+			break;
+		}
+		if (kcount < 4) {
+			continue;
+		}
+		track = sstarts[i]->getTrack();
+		if (sstarts[i]->isKern()) {
+			tracks[3].push_back(track);
+		} else {
+			tracks[4].push_back(track);
+		}
+	}
+
+	kcount = 0;
+	// Store tracks related to the alto part:
+	for (int i=0; i<(int)sstarts.size(); i++) {
+		if (sstarts[i]->isKern()) {
+			kcount++;
+		}
+		if (kcount > 3) {
+			break;
+		}
+		if (kcount < 3) {
+			continue;
+		}
+		track = sstarts[i]->getTrack();
+		if (sstarts[i]->isKern()) {
+			tracks[3].push_back(track);
+		} else {
+			tracks[4].push_back(track);
+		}
 	}
 }
 
@@ -118949,287 +123592,163 @@ string Tool_rphrase::getVoiceInfo(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_rphrase::printEmbeddedVoiceInfoSummary --
+// Tool_satb2gs::validateHeader -- Header cannot contain
+//   spine manipulators.
 //
 
-void Tool_rphrase::printEmbeddedVoiceInfoSummary(vector<Tool_rphrase::VoiceInfo>& voiceInfo, HumdrumFile& infile) {
-	m_humdrum_text << "!!<ul>" << endl;
-
-	double total = 0.0;
-	for (int i=0; i<(int)voiceInfo[0].phraseDurs.size(); i++) {
-		if (voiceInfo[0].phraseDurs[i] > 0.0) {
-			total += voiceInfo[0].phraseDurs[i];
+bool Tool_satb2gs::validateHeader(HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isData()) {
+			break;
 		}
-		if (voiceInfo[0].restsBefore[i] > 0.0) {
-			total += voiceInfo[0].restsBefore[i];
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		if (token->isExclusive()) {
+			continue;
+		}
+		if (infile[i].isManipulator()) {
+			return false;
 		}
 	}
-	m_humdrum_text << "!!<li>Score duration: " << twoDigitRound(total) << "</li>" << endl;
 
-	int countSum = 0;
-	for (int i=0; i<(int)voiceInfo.size(); i++) {
-		countSum += (int)voiceInfo[i].phraseDurs.size();
-	}
-	m_humdrum_text << "!!<li>Total segments: " << countSum << "</li>" << endl;
+	return true;
+}
 
-	double averageCount = countSum / (double)voiceInfo.size();
-	averageCount = (int)(averageCount * 10 + 0.5) / 10.0;
-	m_humdrum_text << "!!<li>Average voice segments: " << averageCount << "</li>" << endl;
 
-	double durSum = 0.0;
-	for (int i=0; i<(int)voiceInfo.size(); i++) {
-		for (int j=0; j<(int)voiceInfo[i].phraseDurs.size(); j++) {
-			durSum += voiceInfo[i].phraseDurs[j];
-		}
-	}
-	double averageDur = durSum / countSum;
-	averageDur = (int)(averageDur * 10 + 0.5) / 10.0;
-	m_humdrum_text << "!!<li>Average segment duration: " << averageDur << "</li>" << endl;
 
-	m_humdrum_text << "!!<li>Voices: " << getVoiceInfo(infile) << "</li>" << endl;
 
-	m_humdrum_text << "!!</ul>" << endl;
+
+/////////////////////////////////
+//
+// Tool_scordatura::Tool_scordatura -- Set the recognized options for the tool.
+//
+
+Tool_scordatura::Tool_scordatura(void) {
+	define("s|sounding=b",      "generate sounding score");
+	define("w|written=b",       "generate written score");
+	define("m|mark|marker=s:@", "marker to add to score");
+	define("p|pitch|pitches=s", "list of pitches to mark");
+	define("i|interval=s",      "musical interval of marked pitches");
+	define("I|is-sounding=s",   "musical score is in sounding format for marks");
+	define("c|chromatic=i:0",   "chromatic interval of marked pitches");
+	define("d|diatonic=i:0",    "diatonic interval of marked pitches");
+	define("color=s",           "color marked pitches");
+	define("string=s",          "string number");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_rphrase::printEmbeddedIndividualVoiceInfo --
+// Tool_scordatura::run -- Do the main work of the tool.
 //
 
-void Tool_rphrase::printEmbeddedIndividualVoiceInfo(Tool_rphrase::VoiceInfo& voiceInfo, HumdrumFile& infile) {
-	m_humdrum_text << "!!<tr>" << endl;
-
-	m_humdrum_text << "!!<td class='voice'>" << voiceInfo.name << "</td>" << endl;
-
-	double sounding = 0.0;
-	double resting = 0.0;
-	for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-		if (voiceInfo.phraseDurs[i] > 0.0) {
-			sounding += voiceInfo.phraseDurs[i];
-		}
-		if (voiceInfo.restsBefore[i] > 0.0) {
-			resting += voiceInfo.restsBefore[i];
-		}
+bool Tool_scordatura::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
-	double total = sounding + resting;
-	double spercent = int(sounding/total * 100.0 + 0.5);
-	double rpercent = int(resting/total * 100.0 + 0.5);
-	m_humdrum_text << "!!<td class='sounding'>" << sounding << "(" << spercent << "%)</td>" << endl;
-	m_humdrum_text << "!!<td class='resting'>" << resting << "(" << rpercent << "%)</td>" << endl;
+	return status;
+}
 
-	// Segment count
-	m_humdrum_text << "!!<td class='segments'>";
-	m_humdrum_text << voiceInfo.phraseDurs.size();
-	m_humdrum_text << "</td>" << endl;
 
-	// Segment duration average
-	m_humdrum_text << "!!<td class='average'>";
-	double sum = 0;
-	int count = 0;
-	for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-		count++;
-		sum += voiceInfo.phraseDurs.at(i);
+bool Tool_scordatura::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	double average = int(sum / count * 100.0 + 0.5)/100.0;
-	m_humdrum_text << average;
-	m_humdrum_text << "</td>" << endl;
+	return status;
+}
 
-	// Segments
-	m_humdrum_text << "!!<td class='segment-durations'>";
-	if (m_sortQ || m_reverseSortQ) {
-		vector<pair<double, int>> sortList;
-		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-			sortList.emplace_back(voiceInfo.phraseDurs[i], i);
-		}
-		if (m_sortQ) {
-			sort(sortList.begin(), sortList.end(),
-				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
-					return a.first < b.first;
-			});
-		} else if (m_reverseSortQ) {
-			sort(sortList.begin(), sortList.end(),
-				[](const std::pair<double, int>& a, const std::pair<double, int>& b) {
-					return a.first > b.first;
-			});
-		}
-		for (int i=0; i<(int)sortList.size(); i++) {
-			int ii = sortList[i].second;
-			if (m_barlineQ) {
-				m_humdrum_text << "<span class='measure'>m" << voiceInfo.barStarts.at(ii) << ":</span>";
-			}
-			m_humdrum_text << "<span class='length' title='measure " << voiceInfo.barStarts.at(ii)
-			               << ")' >" << twoDigitRound(voiceInfo.phraseDurs.at(ii)) << "</span>";
-			if (i < (int)sortList.size() - 1) {
-				m_humdrum_text << " ";
-			}
-		}
+
+bool Tool_scordatura::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
 	} else {
-		for (int i=0; i<(int)voiceInfo.phraseDurs.size(); i++) {
-			if (voiceInfo.restsBefore.at(i) > 0) {
-				m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(voiceInfo.restsBefore.at(i)) << "</span> ";
-			} else if (i > 0) {
-				// force display r:0 for section boundaries.
-				m_humdrum_text << "<span title='inter-phrase rest' class='rest'>" << twoDigitRound(voiceInfo.restsBefore.at(i)) << "</span> ";
-			}
-			if (m_barlineQ) {
-				m_humdrum_text << "<span class='measure'>m" << voiceInfo.barStarts.at(i) << ":</span>";
-			}
-			m_humdrum_text << "<span class='length' title='measure " << voiceInfo.barStarts.at(i)
-			               << "' onclick='rphraseGotoMeasure(" << voiceInfo.barStarts.at(i)
-			               << ")' >" << twoDigitRound(voiceInfo.phraseDurs.at(i)) << "</span>";
-			if (i < (int)voiceInfo.phraseDurs.size() - 1) {
-				m_humdrum_text << " ";
-			}
-		}
+		out << infile;
 	}
+	return status;
+}
 
-	m_humdrum_text << "</td>" << endl;
-	m_humdrum_text << "!!</tr>" << endl;
+
+bool Tool_scordatura::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::fillCompositeInfo --
+// Tool_scordatura::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_rphrase::fillCompositeInfo(Tool_rphrase::VoiceInfo& compositeInfo, HumdrumFile& infile) {
-	compositeInfo.name = getCompositeLabel(infile);
-	vector<int> noteStates;
-	getCompositeStates(noteStates, infile);
-
-	bool inPhraseQ       = false;
-	int currentBarline   = 0;
-	int startBarline     = 1;
-	HumNum startTime     = 0;
-	HumNum restBefore    = 0;
-	HumNum startTimeRest = 0;
-	HumNum scoreDur      = infile.getScoreDuration();
-	HTp phraseStartTok   = NULL;
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-
-		// Split phrases at double barlines (medial cadences):
-		if (infile[i].isBarline()) {
-			HTp token = infile.token(i, 0);
-			if (token->find("||") != string::npos) {
-				HumNum tdur = token->getDurationFromStart();
-				if (tdur != scoreDur) {
-					// Only process if double barline is not at the end of the score.
-
-					if (inPhraseQ) {
-						// In phrase, so continue if still notes, otherwise
-						// if a rest, then record the currently active phrase
-						// that has ended.
-
-						// ending a phrase
-						HumNum endTime = infile[i].getDurationFromStart();
-						HumNum duration = endTime - startTime;
-						startTime = -1;
-						inPhraseQ = false;
-						double value = duration.getFloat() / m_durUnit;
-						compositeInfo.phraseDurs.push_back(value);
-						compositeInfo.barStarts.push_back(startBarline);
-						compositeInfo.phraseStartToks.push_back(phraseStartTok);
-						phraseStartTok = NULL;
-						m_sumComposite += duration.getFloat() / m_durUnit;
-						m_pcountComposite++;
-						double rvalue = restBefore.getFloat() / m_durUnit;
-						compositeInfo.restsBefore.push_back(rvalue);
-
-						// record rest start
-						startTimeRest = endTime;
-					} else {
-						// Not in phrase, so not splitting a rest region.
-						// This case should be rare (starting a medial cadence
-						// with rests and potentially starting new section with rests.
-					}
-
-				}
-			}
+void Tool_scordatura::initialize(void) {
+	m_writtenQ  = getBoolean("written");
+	m_soundingQ = getBoolean("sounding");
+	m_pitches.clear();
+	m_marker = getString("mark");
+	m_IQ = getBoolean("I");
+	m_color = getString("color");
+	if (getBoolean("pitches")) {
+		m_pitches = parsePitches(getString("pitches"));
+	}
+	m_cd = getBoolean("diatonic") && getBoolean("chromatic");
+	m_interval.clear();
+	if (m_cd) {
+		m_diatonic = getInteger("diatonic");
+		m_chromatic = getInteger("chromatic");
+	} else {
+		if (getBoolean("interval")) {
+			m_interval = getString("interval");
 		}
+	}
+	if ((abs(m_diatonic) > 28) || (abs(m_chromatic) > 48)) {
+		m_diatonic = 0;
+		m_chromatic = 0;
+		m_cd = false;
+	}
+	if (!m_pitches.empty()) {
+		prepareTranspositionInterval();
+	}
+	m_string = getString("string");
+}
 
-		if (infile[i].isBarline()) {
-			HTp token = infile.token(i, 0);
-			HumRegex hre;
-			if (hre.search(token, "(\\d+)")) {
-				currentBarline = hre.getMatchInt(1);
-				continue;
-			}
-		}
 
 
-		if (!infile[i].isData()) {
-			continue;
-		}
+//////////////////////////////
+//
+// Tool_scordatura::processFile --
+//
 
-		if (inPhraseQ) {
-			// In phrase, so continue if still notes, otherwise
-			// if a rest, then record the currently active phrase
-			// that has ended.
-			if (noteStates[i] == 0) {
-				// ending a phrase
-				HumNum endTime = infile[i].getDurationFromStart();
-				HumNum duration = endTime - startTime;
-				startTime = -1;
-				inPhraseQ = false;
-				double value = duration.getFloat() / m_durUnit;
-				compositeInfo.phraseDurs.push_back(value);
-				compositeInfo.barStarts.push_back(startBarline);
-				compositeInfo.phraseStartToks.push_back(phraseStartTok);
-				phraseStartTok = NULL;
-				m_sumComposite += duration.getFloat() / m_durUnit;
-				m_pcountComposite++;
-				double rvalue = restBefore.getFloat() / m_durUnit;
-				compositeInfo.restsBefore.push_back(rvalue);
-				// record rest start
-				startTimeRest = endTime;
-			} else {
-				// continuing a phrase, so do nothing
-			}
-		} else {
-			// Not in phrase, so continue if rest; otherwise,
-			// if a note, then record a phrase start.
-			if (noteStates[i] == 0) {
-				// continuing a non-phrase, so do nothing
-			} else {
-				// starting a phrase
-				startTime = infile[i].getDurationFromStart();
-				startBarline = currentBarline;
-				inPhraseQ = true;
-				// check if there are rests before the phrase
-				// The rest duration will be stored when the
-				// end of the next phrase is encountered.
-				if (startTimeRest >= 0) {
-					restBefore = startTime - startTimeRest;
-				} else {
-					restBefore = 0;
-				}
-				phraseStartTok = infile.token(i, 0);
-			}
-		}
+void Tool_scordatura::processFile(HumdrumFile& infile) {
+	m_modifiedQ = false;
 
+	if (!m_pitches.empty()) {
+		markPitches(infile);
+		if (m_modifiedQ) {
+			addMarkerRdf(infile);
+		}
 	}
 
-	if (inPhraseQ) {
-		// process last phrase
-		HumNum endTime = infile.getScoreDuration();
-		HumNum duration = endTime - startTime;
-		double value = duration.getFloat() / m_durUnit;
-		compositeInfo.phraseDurs.push_back(value);
-		compositeInfo.barStarts.push_back(startBarline);
-		compositeInfo.phraseStartToks.push_back(phraseStartTok);
-		m_sumComposite += duration.getFloat() / m_durUnit;
-		m_pcountComposite++;
-		double rvalue = restBefore.getFloat() / m_durUnit;
-		compositeInfo.restsBefore.push_back(rvalue);
+	if (m_writtenQ || m_soundingQ) {
+		vector<HTp> rdfs;
+		getScordaturaRdfs(rdfs, infile);
+		if (!rdfs.empty()) {
+			processScordaturas(infile, rdfs);
+		}
 	}
 
-	if (m_markQ) {
-		markCompositePhraseStartsInScore(infile, compositeInfo);
+	if (m_modifiedQ) {
+		infile.createLinesFromTokens();
 	}
 }
 
@@ -119237,403 +123756,376 @@ void Tool_rphrase::fillCompositeInfo(Tool_rphrase::VoiceInfo& compositeInfo, Hum
 
 //////////////////////////////
 //
-// Tool_rphrase::getCompositeLabel --
+// Tool_scordatura::processScoredaturas --
 //
 
-string Tool_rphrase::getCompositeLabel(HumdrumFile& infile) {
-	string voices;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReferenceRecord()) {
-			continue;
-		}
-		string key = infile[i].getReferenceKey();
-		if (key != "voices") {
-			continue;
-		}
-		voices = infile[i].getReferenceValue();
-		break;
-	}
-
-	if (voices.empty()) {
-		return "composite";
+void Tool_scordatura::processScordaturas(HumdrumFile& infile, vector<HTp>& rdfs) {
+	for (int i=0; i<(int)rdfs.size(); i++) {
+		processScordatura(infile, rdfs[i]);
 	}
+}
 
-	vector<HTp> kstarts = infile.getKernSpineStartList();
 
-	string output = "composite ";
-	output += voices;
 
+//////////////////////////////
+//
+// Tool_scordatura::processScordatura --
+//
 
+void Tool_scordatura::processScordatura(HumdrumFile& infile, HTp reference) {
 	HumRegex hre;
 
-	if (hre.search(voices, "^\\d+$")) {
-		int vint = stoi(voices);
-		if (vint != (int)kstarts.size()) {
-			output += "(";
-			output += to_string(kstarts.size());
-			output += ")";
+	if (m_writtenQ) {
+		if (!hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*([^\\s]+)\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*ITrd(-?\\d+)c(-?\\d+)\\b")) {
+			return;
+		}
+	} else if (m_soundingQ) {
+		if (!hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*([^\\s]+)\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*Trd(-?\\d+)c(-?\\d+)\\b")) {
+			return;
 		}
-	} else {
-		output += "(";
-		output += to_string(kstarts.size());
-		output += ")";
 	}
 
-	return output;
+	string marker = hre.getMatch(1);
+	int diatonic = hre.getMatchInt(2);
+	int chromatic = hre.getMatchInt(3);
+
+	if (diatonic == 0 && chromatic == 0) {
+		// nothing to do
+		return;
+	}
+
+	flipScordaturaInfo(reference, diatonic, chromatic);
+	transposeMarker(infile, marker, diatonic, chromatic);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::fillVoiceInfo --
+// Tool_scordatura::transposeMarker --
 //
 
-void Tool_rphrase::fillVoiceInfo(vector<Tool_rphrase::VoiceInfo>& voiceInfo,
-		vector<HTp>& kstarts, HumdrumFile& infile) {
-	for (int i=0; i<(int)kstarts.size(); i++) {
-		fillVoiceInfo(voiceInfo.at(i), kstarts.at(i), infile);
+
+void Tool_scordatura::transposeMarker(HumdrumFile& infile, const string& marker, int diatonic, int chromatic) {
+	m_transposer.setTranspositionDC(diatonic, chromatic);
+	for (int i=0; i<infile.getStrandCount(); i++) {
+		HTp sstart = infile.getStrandBegin(i);
+		if (!sstart->isKern()) {
+			continue;
+		}
+		HTp sstop = infile.getStrandEnd(i);
+		transposeStrand(sstart, sstop, marker);
 	}
 }
 
 
-void Tool_rphrase::fillVoiceInfo(Tool_rphrase::VoiceInfo& voiceInfo, HTp& kstart, HumdrumFile& infile) {
-	HTp current = kstart;
-
-	bool inPhraseQ       = false;
-	int currentBarline   = 0;
-	int startBarline     = 1;
-	HumNum startTime     = 0;
-
-	HumNum restBefore    = 0;
-	HumNum startTimeRest = 0;
-
-	HumNum scoreDur = infile.getScoreDuration();
-	HTp phraseStartTok = NULL;
-
-	while (current) {
-
-		// Split phrases at double barlines (medial cadences):
-		if (infile[current->getLineIndex()].isBarline()) {
-			HTp token = infile.token(current->getLineIndex(), 0);
-			if (token->find("||") != string::npos) {
-				HumNum tdur = token->getDurationFromStart();
-				if (tdur != scoreDur) {
-					// Only process if double barline is not at the end of the score.
-
-					if (inPhraseQ) {
-						// In phrase, so continue if still notes, otherwise
-						// if a rest, then record the currently active phrase
-						// that has ended.
-
-						HumNum endTime = current->getDurationFromStart();
-						HumNum duration = endTime - startTime;
-						startTime = -1;
-						inPhraseQ = false;
-						double value = duration.getFloat() / m_durUnit;
-						voiceInfo.phraseDurs.push_back(value);
-						voiceInfo.barStarts.push_back(startBarline);
-						voiceInfo.phraseStartToks.push_back(phraseStartTok);
-						phraseStartTok = NULL;
-						m_sum += duration.getFloat() / m_durUnit;
-						m_pcount++;
-						double rvalue = restBefore.getFloat() / m_durUnit;
-						voiceInfo.restsBefore.push_back(rvalue);
-
-						// record rest start
-						startTimeRest = endTime;
-					} else {
-						// Not in phrase, so not splitting a rest region.
-						// This case should be rare (starting a medial cadence
-						// with rests and potentially starting new section with rests.
-					}
-
-				}
-			}
-		}
 
-		if (current->isBarline()) {
-			HumRegex hre;
-			if (hre.search(current, "(\\d+)")) {
-				currentBarline = hre.getMatchInt(1);
-				current = current->getNextToken();
-				continue;
-			}
-		}
+//////////////////////////////
+//
+// Tool_scordatura::transposeStrand --
+//
 
-		if (current->isInstrumentName()) {
-			voiceInfo.name = current->substr(3);
-		}
-		if (!(current->isData() || (m_breathQ && (*current == "*breath")))) {
+void Tool_scordatura::transposeStrand(HTp sstart, HTp sstop, const string& marker) {
+	HTp current = sstart;
+	while (current && current != sstop) {
+		if (!current->isData()) {
 			current = current->getNextToken();
 			continue;
 		}
-		if (current->isNull()) {
+		if (current->isNull() || current->isRest()) {
 			current = current->getNextToken();
 			continue;
 		}
-
-		if (inPhraseQ) {
-			// In phrase, so continue if still notes, otherwise
-			// if a rest, then record the currently active phrase
-			// that has ended.
-			if (current->isRest() || (*current == "*breath")) {
-				// ending a phrase
-				HumNum endTime = current->getDurationFromStart();
-				HumNum duration = endTime - startTime;
-				startTime = -1;
-				inPhraseQ = false;
-				double value = duration.getFloat() / m_durUnit;
-				voiceInfo.phraseDurs.push_back(value);
-				voiceInfo.barStarts.push_back(startBarline);
-				voiceInfo.phraseStartToks.push_back(phraseStartTok);
-				phraseStartTok = NULL;
-				m_sum += duration.getFloat() / m_durUnit;
-				m_pcount++;
-				double rvalue = restBefore.getFloat() / m_durUnit;
-				voiceInfo.restsBefore.push_back(rvalue);
-				// record rest start
-				startTimeRest = endTime;
-			} else {
-				// continuing a phrase, so do nothing
-			}
-		} else {
-			// Not in phrase, so continue if rest; otherwise,
-			// if a note, then record a phrase start.
-			if (current->isRest() || (*current == "*breath")) {
-				// continuing a non-phrase, so do nothing
-			} else {
-				// starting a phrase
-				startTime = current->getDurationFromStart();
-				startBarline = currentBarline;
-				inPhraseQ = true;
-				// check if there are rests before the phrase
-				// The rest duration will be stored when the
-				// end of the next phrase is encountered.
-				if (startTimeRest >= 0) {
-					restBefore = startTime - startTimeRest;
-				} else {
-					restBefore = 0;
-				}
-				phraseStartTok = current;
-			}
+		if (current->find(marker) != string::npos) {
+			transposeChord(current, marker);
 		}
-
 		current = current->getNextToken();
 	}
-	if (inPhraseQ) {
-		// process last phrase
-		HumNum endTime = kstart->getLine()->getOwner()->getScoreDuration();
-		HumNum duration = endTime - startTime;
-		double value = duration.getFloat() / m_durUnit;
-		voiceInfo.phraseDurs.push_back(value);
-		voiceInfo.barStarts.push_back(startBarline);
-		voiceInfo.phraseStartToks.push_back(phraseStartTok);
-		m_sum += duration.getFloat() / m_durUnit;
-		m_pcount++;
-		double rvalue = restBefore.getFloat() / m_durUnit;
-		voiceInfo.restsBefore.push_back(rvalue);
-	}
-
-	if (m_markQ) {
-		markPhraseStartsInScore(infile, voiceInfo);
-	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::markCompositePhraseStartsInScore --
+// Tool_scordatura::transposeChord --
 //
 
-void Tool_rphrase::markCompositePhraseStartsInScore(HumdrumFile& infile, Tool_rphrase::VoiceInfo& compositeInfo) {
-	stringstream buffer;
-	for (int i=0; i<(int)compositeInfo.phraseStartToks.size(); i++) {
-		HTp tok = compositeInfo.phraseStartToks.at(i);
-		string measure = "";
-		if (m_barlineQ) {
-			measure = to_string(compositeInfo.barStarts.at(i));
+void Tool_scordatura::transposeChord(HTp token, const string& marker) {
+	int scount = token->getSubtokenCount();
+	if (scount == 1) {
+		string inputnote = *token;
+		string newtoken;
+		newtoken = transposeNote(inputnote);
+		token->setText(newtoken);
+		return;
+	}
+	vector<string> subtokens;
+	subtokens = token->getSubtokens();
+	for (int i=0; i<(int)subtokens.size(); i++) {
+		if (subtokens[i].find(marker) == string::npos) {
+			continue;
 		}
-		double duration = compositeInfo.phraseDurs.at(i);
-		buffer.str("");
-		if (!measure.empty()) {
-			buffer << "m" << measure << "&colon;";
+		string newtoken = transposeNote(subtokens[i]);
+		subtokens[i] = newtoken;
+	}
+	string newchord;
+	for (int i=0; i<(int)subtokens.size(); i++) {
+		newchord += subtokens[i];
+		if (i<(int)subtokens.size() - 1) {
+			newchord += ' ';
 		}
-		buffer << twoDigitRound(duration);
-		int lineIndex = tok->getLineIndex();
-		infile[lineIndex].setValue("auto", "rphrase-composite-start", buffer.str());
 	}
+	token->setText(newchord);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::twoDigitRound --
+// Tool_scordatura::transposeNote --
 //
 
-double Tool_rphrase::twoDigitRound(double input) {
-	return int(input * 100.0 + 0.499999) / 100.0;
+string Tool_scordatura::transposeNote(const string& note) {
+	HumRegex hre;
+	if (!hre.search(note, "(.*?)([A-Ga-g]+[-#]*)(.*)")) {
+		return note;
+	}
+	string pre = hre.getMatch(1);
+	string pitch = hre.getMatch(2);
+	string post = hre.getMatch(3);
+	HumPitch hpitch;
+	hpitch.setKernPitch(pitch);
+	m_transposer.transpose(hpitch);
+	string output;
+	output = pre;
+	output += hpitch.getKernPitch();
+	output += post;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_rphrase::markPhraseStartsInScore --
+// Tool_scordatura::flipScordaturaInfo --
 //
 
-void Tool_rphrase::markPhraseStartsInScore(HumdrumFile& infile, Tool_rphrase::VoiceInfo& voiceInfo) {
-	stringstream buffer;
-	for (int i=0; i<(int)voiceInfo.phraseStartToks.size(); i++) {
-		HTp tok = voiceInfo.phraseStartToks.at(i);
-		string measure = "";
-		if (m_barlineQ) {
-			measure = to_string(voiceInfo.barStarts.at(i));
-		}
-		double duration = voiceInfo.phraseDurs.at(i);
-		buffer.str("");
-		if (!measure.empty()) {
-			buffer << "m" << measure << "&colon;";
-		}
-		buffer << duration;
-		tok->setValue("auto", "rphrase-start", buffer.str());
+void Tool_scordatura::flipScordaturaInfo(HTp reference, int diatonic, int chromatic) {
+	diatonic *= -1;
+	chromatic *= -1;
+	string output;
+	if (m_writtenQ) {
+		output = "Trd";
+		output += to_string(diatonic);
+		output += "c";
+		output += to_string(chromatic);
+	} else if (m_soundingQ) {
+		output = "ITrd";
+		output += to_string(diatonic);
+		output += "c";
+		output += to_string(chromatic);
+	} else {
+		return;
+	}
+	HumRegex hre;
+	string token = *reference;
+	hre.replaceDestructive(token, output, "I?Trd-?\\dc-?\\d");
+	if (token != *reference) {
+		m_modifiedQ = true;
+		reference->setText(token);
 	}
 }
 
 
 
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_ruthfix::Tool_ruthfix -- Set the recognized options for the tool.
+// Tool_scordatura::getScoredaturaRdfs --
 //
 
-Tool_ruthfix::Tool_ruthfix(void) {
-	// add options here
+void Tool_scordatura::getScordaturaRdfs(vector<HTp>& rdfs, HumdrumFile& infile) {
+	rdfs.clear();
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isReference()) {
+			continue;
+		}
+		HTp reference = infile.token(i, 0);
+		if (m_writtenQ) {
+			if (hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*[^\\s]+\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*ITrd-?\\d+c-?\\d+\\b")) {
+				rdfs.push_back(reference);
+			}
+		} else if (m_soundingQ) {
+			if (hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*[^\\s]+\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*Trd-?\\d+c-?\\d+\\b")) {
+				rdfs.push_back(reference);
+			}
+		}
+	}
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_ruthfix::run -- Do the main work of the tool.
+// Tool_scordatura::parsePitches --
 //
 
-bool Tool_ruthfix::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
-
-bool Tool_ruthfix::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
+set<int> Tool_scordatura::parsePitches(const string& input) {
+	HumRegex hre;
+	string value = input;
+	hre.replaceDestructive(value, "-", "\\s*-\\s*", "g");
 
+	vector<string> pieces;
+	hre.split(pieces, value, "[^A-Ga-g0-9-]+");
 
-bool Tool_ruthfix::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	HumPitch pitcher;
+	set<int> output;
+	string p1;
+	string p2;
+	int d1;
+	int d2;
+	for (int i=0; i<(int)pieces.size(); i++) {
+		if (hre.search(pieces[i], "(.*)-(.*)")) {
+			// pitch range
+			p1 = hre.getMatch(1);
+			p2 = hre.getMatch(2);
+			d1 = Convert::kernToBase7(p1);
+			d2 = Convert::kernToBase7(p2);
+			if ((d1 < 0) || (d2 < 0) || (d1 > d2) || (d1 > 127) || (d2 > 127)) {
+				continue;
+			}
+			for (int j=d1; j<=d2; j++) {
+				output.insert(j);
+			}
+		} else {
+			// single pitch
+			d1 = Convert::kernToBase7(pieces[i]);
+			if ((d1 < 0) || (d1 > 127)) {
+				continue;
+			}
+			output.insert(d1);
+		}
 	}
-	return status;
-}
-
-
-bool Tool_ruthfix::run(HumdrumFile& infile) {
-	insertCrossBarTies(infile);
-	return true;
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_ruthfix::insertCrossBarTies --
+// Tool_scordatura::markPitches --
 //
 
-void Tool_ruthfix::insertCrossBarTies(HumdrumFile& infile) {
-	int scount = infile.getStrandCount();
-	if (scount == 0) {
-		// The input file was not read from a file but was created
-		// dynamically.  The easiest thing to do is to reload to get the
-		// spine/strand information.
-		stringstream ss;
-		infile.createLinesFromTokens();
-		ss << infile;
-		infile.readString(ss.str());
+void Tool_scordatura::markPitches(HumdrumFile& infile) {
+	for (int i=0; i<infile.getStrandCount(); i++) {
+		HTp sstart = infile.getStrandStart(i);
+		if (!sstart->isKern()) {
+			continue;
+		}
+		HTp sstop = infile.getStrandStop(i);
+		markPitches(sstart, sstop);
 	}
-	scount = infile.getStrandCount();
+}
 
 
-	HTp token;
-	for (int i=0; i<scount; i++) {
-		token = infile.getStrandStart(i);
-		if (!token->isKern()) {
+void Tool_scordatura::markPitches(HTp sstart, HTp sstop) {
+	HTp current = sstart;
+	while (current && (current != sstop)) {
+		if (current->isNull() || current->isRest()) {
+			current = current->getNextToken();
 			continue;
 		}
-		insertCrossBarTies(infile, i);
+		markPitches(current);
+		current = current->getNextToken();
 	}
 }
 
 
-void Tool_ruthfix::insertCrossBarTies(HumdrumFile& infile, int strand) {
-	HTp sstart = infile.getStrandStart(strand);
-	HTp send   = infile.getStrandEnd(strand);
-	HTp s = sstart;
-	HTp lastnote = NULL;
-	bool barstart = true;
-	while (s != send) {
-		if (s->isBarline()) {
-			barstart = true;
-		} else if (s->isNote()) {
-			if (lastnote && barstart && (s->find("yy") != string::npos)) {
-				createTiedNote(lastnote, s);
-			}
-			barstart = false;
-			lastnote = s;
-		} else if (s->isRest()) {
-			lastnote = NULL;
-			barstart = false;
+void Tool_scordatura::markPitches(HTp token) {
+	vector<string> subtokens = token->getSubtokens();
+	int counter = 0;
+	for (int i=0; i<(int)subtokens.size(); i++) {
+		int dia = Convert::kernToBase7(subtokens[i]);
+		if (m_pitches.find(dia) != m_pitches.end()) {
+			counter++;
+			subtokens[i] += m_marker;
 		}
-		s = s->getNextToken();
-		if (!s) {
-			break;
+	}
+	if (counter == 0) {
+		return;
+	}
+	string newtoken;
+	for (int i=0; i<(int)subtokens.size(); i++) {
+		newtoken += subtokens[i];
+		if (i < (int)subtokens.size() - 1) {
+			newtoken += ' ';
 		}
 	}
+	token->setText(newtoken);
+	m_modifiedQ = true;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_scordatura::addMarkerRdf --
+//
+
+void Tool_scordatura::addMarkerRdf(HumdrumFile& infile) {
+	string line = "!!!RDF**kern: ";
+	line += m_marker;
+	line += " = ";
+	if (!m_string.empty()) {
+		line += "string=";
+		line += m_string;
+		line += " ";
+	}
+	line += "scordatura=";
+	if (m_IQ) {
+		line += "I";
+	}
+	line += "Tr";
+	if (m_transposition.empty()) {
+		line += "XXX";
+	} else {
+		line += m_transposition;
+	}
+	if (!m_color.empty()) {
+		line += ", color=";
+		line += m_color;
+	}
+	infile.appendLine(line);
+	m_modifiedQ = true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_ruthfix::createTiedNote -- Does not work for chords.
-//  change  1E-X TO 2E-Xyy
-//      to  [1E-X TO 2E-X]
+// Tool_scordatura::prepareTranspositionInterval --
 //
 
-void Tool_ruthfix::createTiedNote(HTp left, HTp right) {
-	if (left->isChord() || right->isChord()) {
+void Tool_scordatura::prepareTranspositionInterval(void) {
+	m_transposition.clear();
+	if (m_cd) {
+		m_transposition = "d";
+		m_transposition += to_string(m_diatonic);
+		m_transposition += "c";
+		m_transposition += to_string(m_chromatic);
 		return;
 	}
-	auto loc = right->find("yy");
-	if (loc != string::npos) {
-		left->insert(0, 1, '[');
-		right->replace(loc, 2, "]");
+
+	if (m_interval.empty()) {
+		return;
 	}
+
+	HumTransposer trans;
+	trans.intervalToDiatonicChromatic(m_diatonic, m_chromatic, m_interval);
+	m_transposition = "d";
+	m_transposition += to_string(m_diatonic);
+	m_transposition += "c";
+	m_transposition += to_string(m_chromatic);
 }
 
 
@@ -119642,22 +124134,41 @@ void Tool_ruthfix::createTiedNote(HTp left, HTp right) {
 
 /////////////////////////////////
 //
-// Tool_sab2gs::Tool_sab2gs -- Set the recognized options for the tool.
+// Tool_semitones::Tool_semitones -- Set the recognized options for the tool.
 //
 
-Tool_sab2gs::Tool_sab2gs(void) {
-	define("b|below=s:<", "Marker for displaying on next staff below");
-	define("d|down=b",    "Use only *down/*Xdown interpretations");
+Tool_semitones::Tool_semitones(void) {
+	define("1|first=b",                   "mark only the first note of intervals");
+	define("2|second=b",                  "mark only the second note of intervals");
+	define("A|O|no-analysis|no-output=b", "do not print analysis spines");
+	define("I|no-input=b",                "do not print input data spines");
+	define("M|no-mark|no-marks=b",        "do not mark notes");
+	define("R|no-rests=b",                "ignore rests");
+	define("T|no-ties=b",                 "do not mark ties");
+	define("X|include|only=s",            "include only **kern tokens with given pattern");
+	define("color=s:red",                 "mark color");
+	define("c|cdata=b",                   "store resulting data as **cdata (allowing display in VHV");
+	define("d|down=b",                    "highlight notes that that have a negative semitone interval");
+	define("j|jump=i:3",                  "starting interval defining leaps");
+	define("l|leap=b",                    "highlight notes that have leap motion");
+	define("mark=s:@",                    "mark character");
+	define("m|midi=b",                    "show MIDI note number for pitches");
+	define("n|count=b",                   "output count of intervals being marked");
+	define("p|pc=b",                      "output pitch classes from C=0 instead of MIDI notes for -m option");
+	define("r|same|repeat|repeated=b",    "highlight notes that are repeated ");
+	define("s|step=b",                    "highlight notes that have step-wise motion");
+	define("u|up=b",                      "highlight notes that that have a positive semitone interval");
+	define("x|exclude=s",                 "exclude **kern tokens with given pattern");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_sab2gs::run -- Do the main work of the tool.
+// Tool_semitones::run -- Do the main work of the tool.
 //
 
-bool Tool_sab2gs::run(HumdrumFileSet& infiles) {
+bool Tool_semitones::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -119666,7 +124177,7 @@ bool Tool_sab2gs::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_sab2gs::run(const string& indata, ostream& out) {
+bool Tool_semitones::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -119678,7 +124189,7 @@ bool Tool_sab2gs::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_sab2gs::run(HumdrumFile& infile, ostream& out) {
+bool Tool_semitones::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -119689,7 +124200,7 @@ bool Tool_sab2gs::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_sab2gs::run(HumdrumFile& infile) {
+bool Tool_semitones::run(HumdrumFile& infile) {
 	initialize();
 	processFile(infile);
 	return true;
@@ -119699,660 +124210,668 @@ bool Tool_sab2gs::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_sab2gs::initialize --  Initializations that only have to be done once
+// Tool_semitones::initialize --  Initializations that only have to be done once
 //    for all HumdrumFile segments.
 //
 
-void Tool_sab2gs::initialize(void) {
-	m_belowMarker = getString("below");
+void Tool_semitones::initialize(void) {
+	// processing of options goes here
+
+	m_cdataQ      = getBoolean("cdata");
+	m_count       = getBoolean("count");
 	m_downQ       = getBoolean("down");
+	m_firstQ      = getBoolean("first");
+	m_leapQ       = getBoolean("leap");
+	m_midiQ       = getBoolean("midi");
+	m_noanalysisQ = getBoolean("no-analysis");
+	m_noinputQ    = getBoolean("no-input");
+	m_nomarkQ     = getBoolean("no-marks");
+	m_notiesQ     = getBoolean("no-ties");
+	m_pcQ         = getBoolean("pc");
+	m_repeatQ     = getBoolean("repeat");
+	m_norestsQ    = getBoolean("no-rests");
+	m_secondQ     = getBoolean("second");
+	m_stepQ       = getBoolean("step");
+	m_upQ         = getBoolean("up");
+
+	m_leap        = getInteger("jump");
+
+	m_color       = getString("color");
+	m_exclude     = getString("exclude");
+	m_include     = getString("include");
+	m_marker      = getString("mark");
+
+	if (!m_firstQ && !m_secondQ) {
+		m_firstQ  = true;
+		m_secondQ = true;
+	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_sab2gs::processFile --
+// Tool_semitones::processFile --
 //
 
-void Tool_sab2gs::processFile(HumdrumFile& infile) {
-
-	vector<HTp> spines;
-	infile.getSpineStartList(spines);
-	vector<HTp> kernSpines;
-	for (int i=0; i<(int)spines.size(); i++) {
-		if (spines[i]->isKern()) {
-			kernSpines.push_back(spines[i]);
-		}
+void Tool_semitones::processFile(HumdrumFile& infile) {
+	m_markCount = 0;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		analyzeLine(infile, i);
 	}
-	if (kernSpines.size() != 3) {
-		// Not valid for processing kern spines, so return original:
-		m_humdrum_text << infile;
-		return;
+	if ((m_markCount > 0) && !m_nomarkQ) {
+		m_humdrum_text << "!!!RDF**kern: ";
+		m_humdrum_text << m_marker;
+		m_humdrum_text << " = marked note";
+		if (getBoolean("color")) {
+			m_humdrum_text << ", color=" << m_color;
+		}
+		m_humdrum_text << '\n';
 	}
-
-	string belowMarker = hasBelowMarker(infile);
-	if (!belowMarker.empty()) {
-		m_hasBelowMarker = true;
-		m_belowMarker = belowMarker;
+	if (m_count) {
+		showCount();
 	}
-
-	adjustMiddleVoice(kernSpines[1]);
-	printGrandStaff(infile, kernSpines);
 }
 
 
 
-/////////////////////////////
+//////////////////////////////
 //
-// Tool_sab2gs::hasBelowMarker -- Returns below marker if found; otherwise,
-//     returns empty string.
+// Tool_semitones::showCount -- Give a count for the number of
+//     intervals that were marked.
 //
 
-string Tool_sab2gs::hasBelowMarker(HumdrumFile& infile) {
-	string output;
-	HumRegex hre;
-	if (m_hasCrossStaff) {
-		// Search backwards since if there is a below marker, it will be more
-		// likely found at the bottom of the score.
-		for (int i=infile.getLineCount()-1; i<=0; i--) {
-			if (infile[i].hasSpines()) {
-				continue;
-			}
-			if (hre.search(infile.token(i, 0), "^!!!RDF\\*\\*kern\\s*:\\s*([^\\s=]+)\\s*=\\s*below\\s*$")) {
-				output = hre.getMatch(1);
-				break;
-			}
-		}
+void Tool_semitones::showCount(void) {
+	m_humdrum_text << "!!semitone_count: " << m_markCount;
+	if (m_repeatQ) {
+		m_humdrum_text << " REPEAT";
 	}
-	return output;
+	if (m_upQ) {
+		m_humdrum_text << " UP";
+	}
+	if (m_downQ) {
+		m_humdrum_text << " DOWN";
+	}
+	if (m_stepQ) {
+		m_humdrum_text << " STEP";
+	}
+	if (m_leapQ) {
+		m_humdrum_text << " LEAP";
+	}
+	if ((m_stepQ || m_leapQ) && (m_leap != 3)) {
+		m_humdrum_text << " JUMP:" << m_leap;
+	}
+	if (m_marker != "@") {
+		m_humdrum_text << " MARK:" << m_marker;
+	}
+	m_humdrum_text << '\n';
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_sab2gs::printGrandStaff --
+// Tool_semitones::analyzeLine --  Append analysis spines after every **kern
+//   spine.
 //
 
-void Tool_sab2gs::printGrandStaff(HumdrumFile& infile, vector<HTp>& starts) {
-	bool foundData = false;
-
-	vector<int> ktracks(starts.size());
-	for (int i=0; i<(int)starts.size(); i++) {
-		ktracks.at(i) = starts.at(i)->getTrack();
+void Tool_semitones::analyzeLine(HumdrumFile& infile, int line) {
+	int group = 0;
+	if (!infile[line].hasSpines()) {
+		m_humdrum_text << infile[line] << "\n";
+		return;
 	}
-
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
-		}
-		if (!foundData && (infile[i].isData() || infile[i].isBarline())) {
-			printSpineSplit(infile, i, ktracks);
-			foundData = true;
-		}
-		if (*infile.token(i, 0) == "*-") {
-			printSpineMerge(infile, i, ktracks);
-			foundData = false;
-			printReducedLine(infile, i, ktracks);
-			if (m_hasCrossStaff && !m_hasBelowMarker) {
-				m_humdrum_text << "!!!RDF**kern: " << m_belowMarker << " = below" << endl;
+	for (int i=0; i<infile[line].getFieldCount(); i++) {
+		HTp token = infile.token(line, i);
+		if (!m_noinputQ) {
+			if (!token->isKern()) {
+				m_humdrum_text << token;
+				if (i < infile[line].getFieldCount() - 1) {
+					m_humdrum_text << '\t';
+				}
+				continue;
 			}
-			continue;
 		}
-		if (foundData) {
-			printSwappedLine(infile, i, ktracks);
-		} else {
-			printReducedLine(infile, i, ktracks);
+		i = processKernSpines(infile, line, i, group++);
+		if (!m_noinputQ) {
+			if (i < infile[line].getFieldCount() - 1) {
+				m_humdrum_text << '\t';
+			}
 		}
 	}
+	m_humdrum_text << '\n';
 }
 
 
+
 //////////////////////////////
 //
-// Tool_sab2gs::printSpineSplit -- Split second and third spines, moving non-kern spines
-//    after the second one to the end of the line (null interpretations);
+// Tool_semitones::processKernSpine --
 //
 
-void Tool_sab2gs::printSpineSplit(HumdrumFile& infile, int index, vector<int>& ktracks) {
-	// First print all non-kern spines at the start of the line:
-	int nextIndex = 0;
-	int fcount = 0;
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
-		}
-		fcount++;
-		m_humdrum_text << "*";
-		nextIndex++;
-	}
-	// Must be on the first **kern spine:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
-		return;
-	}
-	// Print the first **kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
-	}
-	fcount++;
-	m_humdrum_text << "*";
-	nextIndex++;
-	// Next print all non-kern spines after first **kern spine:
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
-		}
-		m_humdrum_text << "*";
-		nextIndex++;
-	}
-	// Second **kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
-		return;
-	}
-	// Ignore the second kern spine as it does not exist yet in the
-	// output data.
-	nextIndex++;
-	// Then store any non-kern spines between the second and third kern spines to
-	// append to the end of the data line later.
-	string postData;
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (!postData.empty()) {
-			postData += "\t";
-		}
-		nextIndex++;
-		postData += "*";
-	}
-	// Third kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
-		return;
-	}
-	// Now print the third kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
-	}
-	fcount++;
-	nextIndex++;
-	m_humdrum_text << "*^";
-	// Now print the non-kern spines after the third **kern spine (or rather just
-	// all spines including any other **kern spines, although current requirement
-	// is that there are only three **kern spines.
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		// HTp token = infile.token(index, nextIndex);
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
-		}
-		fcount++;
-		nextIndex++;
-		m_humdrum_text << "*";
+int Tool_semitones::processKernSpines(HumdrumFile& infile, int line, int start, int kspine) {
+	HTp token = infile.token(line, start);
+	if (!token->isKern()) {
+		return start;
 	}
-	// Finally print any non-kern spines after the second **kern spine:
-	if (!postData.empty()) {
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+	int track = token->getTrack();
+	vector<HTp> toks;
+	toks.push_back(token);
+	for (int i=start+1; i<infile[line].getFieldCount(); i++) {
+		HTp newtok = infile.token(line, i);
+		int newtrack = newtok->getTrack();
+		if (newtrack == track) {
+			toks.push_back(newtok);
+			continue;
 		}
-		fcount++;
-		m_humdrum_text << postData;
+		break;
 	}
-	m_humdrum_text << endl;
-}
 
+	int toksize = (int)toks.size();
 
+	// calculate intervals/MIDI note numbers if appropriate
+	bool allQ = m_stepQ || m_leapQ || m_upQ || m_downQ || m_repeatQ;
+	bool dirQ = m_upQ || m_downQ;
+	bool typeQ = m_stepQ || m_leapQ;
+	vector<string> intervals(toksize);
+	if (infile[line].isData()) {
+		for (int i=0; i<toksize; i++) {
+			intervals[i] = getTwelveToneIntervalString(toks[i]);
+		}
+		if (allQ && !m_midiQ) {
+			for (int i=0; i<(int)intervals.size(); i++) {
+				if (intervals[i].empty()) {
+					continue;
+				}
+            if (!isdigit(intervals[i].back())) {
+					continue;
+				}
+				int value = stoi(intervals[i]);
+				if (m_upQ && m_stepQ && (value > 0) && (value < m_leap)) {
+					markInterval(toks[i]);
+				} else if (m_downQ && m_stepQ && (value < 0) && (value > -m_leap)) {
+					markInterval(toks[i]);
+				} else if (!dirQ && m_stepQ && (value != 0) && (abs(value) < m_leap)) {
+					markInterval(toks[i]);
 
-//////////////////////////////
-//
-// Tool_sab2gs::printSpineMerge -- Merge second and third spines, moving non-kern spines
-//    after the second one to the end of the line (null interpretations);
-//
+				} else if (m_upQ && m_leapQ && (value > 0) && (value >= m_leap)) {
+					markInterval(toks[i]);
+				} else if (m_downQ && m_leapQ && (value < 0) && (value <= -m_leap)) {
+					markInterval(toks[i]);
+				} else if (!dirQ && m_leapQ && (value != 0) && (abs(value) >= m_leap)) {
+					markInterval(toks[i]);
 
-void Tool_sab2gs::printSpineMerge(HumdrumFile& infile, int index, vector<int>& ktracks) {
-	// First print all non-kern spines at the start of the line:
-	int nextIndex = 0;
-	int fcount = 0;
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+				} else if (m_repeatQ && (value == 0)) {
+					markInterval(toks[i]);
+				} else if (!typeQ && m_upQ && (value > 0)) {
+					markInterval(toks[i]);
+				} else if (!typeQ && m_downQ && (value < 0)) {
+					markInterval(toks[i]);
+				}
+			}
 		}
-		fcount++;
-		m_humdrum_text << "*";
-		nextIndex++;
-	}
-	// Must be on the first **kern spine:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
-		return;
-	}
-	// Print the first **kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
 	}
-	fcount++;
-	m_humdrum_text << "*";
-	nextIndex++;
-	// Next print all non-kern spines after first **kern spine:
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+
+	// print the **kern fields
+	if (!m_noinputQ) {
+		for (int i=0; i<toksize; i++) {
+			m_humdrum_text << toks[i];
+			if (i < toksize - 1) {
+				m_humdrum_text << '\t';
+			}
 		}
-		m_humdrum_text << "*";
-		nextIndex++;
-	}
-	// Second **kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
-		return;
 	}
-	// Save the second kern spine as it does not exist yet in the
-	// output data.
-	// HTp savedKernToken = infile.token(index, nextIndex);
-	nextIndex++;
-	// Then store any non-kern spines between the second and third kern spines to
-	// append to the end of the data line later.
-	string postData;
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (!postData.empty()) {
-			postData += "\t";
+
+	// then print the parallel analysis fields
+
+	if (!m_noanalysisQ) {
+		if (!m_noinputQ) {
+			m_humdrum_text << '\t';
+		} else if (m_noinputQ && (kspine != 0)) {
+			m_humdrum_text << '\t';
 		}
-		nextIndex++;
-		postData += "*";
-	}
-	// Third kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
-		return;
-	}
-	// Now print the third kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
-	}
-	fcount++;
-	m_humdrum_text << "*v";
-	nextIndex++;
-	// Now printed the saved second **kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
-	}
-	m_humdrum_text << "*v";
-	fcount++;
-	// Now print the non-kern spines after the third **kern spine (or rather just
-	// all spines including any other **kern spines, although current requirement
-	// is that there are only three **kern spines.
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		// HTp token = infile.token(index, nextIndex);
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+		if (!infile[line].isData()) {
+			if (infile[line].isLocalComment()) {
+				printTokens("!", toksize);
+	 		} else if (infile[line].isInterpretation()) {
+				if (toks[0]->compare(0, 2, "**") == 0) {
+					if (m_cdataQ) {
+						printTokens("**cdata", toksize);
+					} else if (m_midiQ) {
+						if (m_pcQ) {
+							printTokens("**mpc", toksize);
+						} else {
+							printTokens("**mnn", toksize);
+						}
+					} else {
+						printTokens("**tti", toksize);
+					}
+				} else {
+					for (int i=0; i<toksize; i++) {
+						m_humdrum_text << toks[i];
+						if (i < toksize - 1) {
+							m_humdrum_text << '\t';
+						}
+					}
+				}
+	 		} else if (infile[line].isBarline()) {
+				printTokens(*toks[0], toksize);
+			} else {
+				cerr << "STRANGE ERROR " << toks[0] << endl;
+			}
+			return start + toksize - 1;
 		}
-		fcount++;
-		nextIndex++;
-		m_humdrum_text << "*";
-	}
-	// Finally print any non-kern spines after the second **kern spine:
-	if (!postData.empty()) {
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+		// print twelve-tone analyses.
+		string value;
+		for (int i=0; i<toksize; i++) {
+			value = getTwelveToneIntervalString(toks[i]);
+			m_humdrum_text << value;
+			if (i < toksize - 1) {
+				m_humdrum_text << '\t';
+			}
 		}
-		fcount++;
-		m_humdrum_text << postData;
 	}
-	m_humdrum_text << endl;
+
+	return start + toksize - 1;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_sab2gs::printSwappedLine -- move the second **kern spine immediately after
-//    the third one, and move any non-kern spines after then end of the line.
+// Tool_semitones::markInterval -- mark the current note, any notes tied
+//    after it, and then the next note and any tied notes attached to
+//    that note.
 //
 
-void Tool_sab2gs::printSwappedLine(HumdrumFile& infile, int index, vector<int>& ktracks) {
-	// First print all non-kern spines at the start of the line:
-	int nextIndex = 0;
-	int fcount = 0;
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
-		}
-		fcount++;
-		m_humdrum_text << token;
-		nextIndex++;
+void Tool_semitones::markInterval(HTp token) {
+	if (!token->isData()) {
+		return;
 	}
-	// Must be on the first **kern spine:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
+	if (!token->isKern()) {
 		return;
 	}
-	// Print the first **kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
+	if (token->isNull()) {
+		return;
 	}
-	fcount++;
-	m_humdrum_text << infile.token(index, nextIndex);
-	nextIndex++;
-	// Next print all non-kern spines after first **kern spine:
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
-		}
-		m_humdrum_text << token;
-		nextIndex++;
+	if (token->isRest()) {
+		return;
 	}
-	// Second **kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
+	if (token->isUnpitched()) {
 		return;
 	}
-	// Save the second kern spine as it does not exist yet in the
-	// output data.
-	HTp savedKernToken = infile.token(index, nextIndex++);
-	// Then store any non-kern spines between the second and third kern spines to
-	// append to the end of the data line later.
-	string postData;
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
+	m_markCount++;
+	token = markNote(token, m_firstQ);
+	if (m_firstQ && !m_secondQ) {
+		return;
+	}
+	// find next note
+	HTp current = token->getNextToken();
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		if (!postData.empty()) {
-			postData += "\t";
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
 		}
-		nextIndex++;
-		postData += *token;
+		markNote(current, m_secondQ);
+		break;
 	}
-	// Third kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
-		return;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_semitones::markNote -- make note and any tied notes after it.
+//      Return the last note of a tied note (or the note if no tied notes
+//      after it).
+//
+
+HTp Tool_semitones::markNote(HTp token, bool markQ) {
+	string subtok = token->getSubtoken(0);
+	bool hasTieEnd = false;
+	if (subtok.find('_') != string::npos) {
+		hasTieEnd = true;
+	} else if (subtok.find(']') != string::npos) {
+		hasTieEnd = true;
 	}
-	// Now print the third kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
+
+	if (!(hasTieEnd && m_notiesQ)) {
+		if (markQ) {
+			addMarker(token);
+		}
 	}
-	fcount++;
-	m_humdrum_text << infile.token(index, nextIndex++);
-	// Now printed the saved second **kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
+
+	bool hasTie = false;
+	if (subtok.find('[') != string::npos) {
+		hasTie = true;
+	} else if (subtok.find('_') != string::npos) {
+		hasTie = true;
 	}
-	m_humdrum_text << savedKernToken;
-	fcount++;
-	// Now print the non-kern spines after the third **kern spine (or rather just
-	// all spines including any other **kern spines, although current requirement
-	// is that there are only three **kern spines.
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, nextIndex);
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
-		}
-		fcount++;
-		nextIndex++;
-		m_humdrum_text << token;
+
+	if (!hasTie) {
+		return token;
 	}
-	// Finally print any non-kern spines after the second **kern spine:
-	if (!postData.empty()) {
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+	HTp current = token->getNextToken();
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		fcount++;
-		m_humdrum_text << postData;
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+		subtok = current->getSubtoken(0);
+		bool hasTie = false;
+		if (subtok.find('[') != string::npos) {
+			hasTie = true;
+		} else if (subtok.find('_') != string::npos) {
+			hasTie = true;
+		}
+		if (!hasTie) {
+			if (subtok.find(']') != string::npos) {
+				markNote(current, markQ);
+			}
+			return current;
+		} else {
+			return markNote(current, markQ);
+		}
+		break;
 	}
-	m_humdrum_text << endl;
+	return NULL;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_sab2gs::printReducedLine -- remove the contents of the second **kern
-//    spine, and move any non-kernspines after it to become after the third **kern spine
+// Tool_semitones::addMarker --
 //
 
-void Tool_sab2gs::printReducedLine(HumdrumFile& infile, int index, vector<int>& ktracks) {
-	// First print all non-kern spines at the start of the line:
-	int nextIndex = 0;
-	int fcount = 0;
-	for (int i=0; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+void Tool_semitones::addMarker(HTp token) {
+	if (!m_nomarkQ) {
+		string contents = m_marker;
+		contents += token->getText();
+		token->setText(contents);
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_semitones::printTokens --
+//
+
+void Tool_semitones::printTokens(const string& value, int count) {
+	for (int i=0; i<count; i++) {
+		m_humdrum_text << value;
+		if (i < count - 1) {
+			m_humdrum_text << '\t';
 		}
-		fcount++;
-		m_humdrum_text << token;
-		nextIndex++;
 	}
-	// Must be on the first **kern spine:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend on line " << index+1 << ": " << infile[index] << endl;
-		return;
+}
+
+
+
+///////////////////////////////
+//
+// Tool_semitones::getTwelveToneIntervalString --
+//
+
+string Tool_semitones::getTwelveToneIntervalString(HTp token) {
+	if (token->isNull()) {
+		return ".";
 	}
-	// Print the first **kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
+	if (token->isRest()) {
+		if (m_midiQ) {
+			return "r";
+		} else {
+			return ".";
+		}
 	}
-	fcount++;
-	m_humdrum_text << infile.token(index, nextIndex++);
-	// Next print all non-kern spines after first **kern spine:
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
+	if (token->isUnpitched()) {
+		if (m_midiQ) {
+			return "R";
+		} else {
+			return ".";
 		}
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+	}
+	if ((m_include.size() > 0) || (m_exclude.size() > 0)) {
+		int status = filterData(token);
+		if (status == 0) {
+			return ".";
+		} else if (status < 0) {
+			return "x"; // excluded note
 		}
-		m_humdrum_text << token;
-		nextIndex++;
 	}
-	// Second **kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend B on line " << index+1 << ": " << infile[index] << endl;
-		return;
+	string tok = token->getSubtoken(0);
+	if (tok.find(']') != string::npos) {
+		return ".";
 	}
-	// Ignore the second kern spine as it does not exist yet in the
-	// output data.
-	nextIndex++;
-	// Then store any non-kern spines between the second and third kern spines to
-	// append to the end of the data line later.
-	string postData;
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, i);
-		if (token->isKern()) {
-			break;
-		}
-		if (!postData.empty()) {
-			postData += "\t";
+	if (tok.find('_') != string::npos) {
+		return ".";
+	}
+	int value = Convert::kernToMidiNoteNumber(tok);
+
+	if (m_midiQ) {
+		string output;
+		if (m_pcQ) {
+			value = value % 12;
 		}
-		nextIndex++;
-		postData += *token;
+		output = to_string(value);
+		return output;
 	}
-	// Third kern spine must be **kern data:
-	if (!infile.token(index, nextIndex)->isKern()) {
-		cerr << "Something strange happend C on line " << index+1 << ": " << infile[index] << endl;
-		return;
+
+	string nexttok = getNextNoteAttack(token);
+	if (nexttok.empty()) {
+		return ".";
 	}
-	// Now print the third kern spine:
-	if (fcount > 0) {
-		m_humdrum_text << "\t";
+	if (nexttok.find('r') != string::npos) {
+		// no interval since next note is a rest
+		return "r";
 	}
-	fcount++;
-	m_humdrum_text << infile.token(index, nextIndex++);
-	// Now print the non-kern spines after the third **kern spine (or rather just
-	// all spines including any other **kern spines, although current requirement
-	// is that there are only three **kern spines.
-	for (int i=nextIndex; i<infile[index].getFieldCount(); i++) {
-		HTp token = infile.token(index, nextIndex);
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+	int value2 = Convert::kernToMidiNoteNumber(nexttok);
+	int interval =  value2 - value;
+	string output = to_string(interval);
+	return output;
+}
+
+
+
+///////////////////////////////
+//
+// Tool_semitones::getNextNoteAttack -- Or rest.
+//
+
+string Tool_semitones::getNextNoteAttack(HTp token) {
+	HTp current = token;
+	current = current->getNextToken();
+	string tok;
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		fcount++;
-		nextIndex++;
-		m_humdrum_text << token;
-	}
-	// Finally print any non-kern spines after the second **kern spine:
-	if (!postData.empty()) {
-		if (fcount > 0) {
-			m_humdrum_text << "\t";
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
 		}
-		fcount++;
-		m_humdrum_text << postData;
+		if (current->isRest()) {
+			if (!m_norestsQ) {
+				return "r";
+			} else {
+				current = current->getNextToken();
+				continue;
+			}
+		}
+		if (current->isUnpitched()) {
+			return "R";
+		}
+		string tok = current->getSubtoken(0);
+		if (tok.find(']') != string::npos) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (tok.find('_') != string::npos) {
+			current = current->getNextToken();
+			continue;
+		}
+		return tok;
 	}
-	m_humdrum_text << endl;
+
+	if (!current) {
+		return "";
+	}
+	if (!current->isData()) {
+		return "";
+	}
+	// Some other strange problem.
+	return ".";
 }
 
 
+
 //////////////////////////////
 //
-// Tool_sab2gs::adjustMiddleVoice --
+// Tool_semitones::filterData -- select or deselect an interval based
+//    on regular expression pattern.  Return true if the note should
+//    be kept; otherwise, return false.
 //
 
-void Tool_sab2gs::adjustMiddleVoice(HTp spineStart) {
-	HTp current = spineStart;
-	// staff: +1 = top staff, -1 = bottom staff
-	// when on top staff, force stem down, or on bottom staff, force stem up
-	// when on bottom staff add "<" marker after pitch (or rest) to move to
-	// bottom staff.  Staff choice is selected by clef: clefG2 is for top staff
-	// and staffF4 is for bottom staff. Chords are not expected.
-	int staff = 0;
-	string replacement = "$1" + m_belowMarker;
+int Tool_semitones::filterData(HTp token) {
+	vector<HTp> toks = getTieGroup(token);
 	HumRegex hre;
-	while (current) {
-		if (*current == "*-") {
-			break;
-		}
-		if (!m_downQ && current->isClef()) {
-			if (current->substr(0, 7) == "*clefG2") {
-				staff = 1;
-				// suppress clef:
-				string text = "*x" + current->substr(1);
-				current->setText(text);
-			} else if (current->substr(0, 7) == "*clefF4") {
-				staff = -1;
-				// suppress clef:
-				string text = "*x" + current->substr(1);
-				current->setText(text);
-			}
-		} else if (current->isInterpretation()) {
-			if (*current == "*down") {
-				staff = -1;
-			} else if (*current == "*Xdown") {
-				staff = 1;
+	if (!m_exclude.empty()) {
+		for (int i=0; i<(int)toks.size(); i++) {
+			if (hre.search(toks[i], m_exclude)) {
+				return -1;
 			}
-		} else if ((staff != 0) && current->isData()) {
-			if (current->isNull()) {
-				// nothing to do with token
-				current = current->getNextToken();
-				continue;
+		}
+		return 1;
+	} else if (!m_include.empty()) {
+		for (int i=0; i<(int)toks.size(); i++) {
+			if (hre.search(toks[i], m_include)) {
+				return 1;
 			}
-			if (staff > 0) {
-				// force stems down or add stem down to non-rest notes
-				if (hre.search(current, "[/\\\\]")) {
-					string value = hre.replaceCopy(current, "\\", "/", "g");
-					if (value != *current) {
-						current->setText(value);
-					}
-					current = current->getNextToken();
-					continue;
-				} if (current->isRest()) {
-					current = current->getNextToken();
-					continue;
-				} else {
-					string value = *current;
-					value += "\\";
-					current->setText(value);
-					current = current->getNextToken();
-					continue;
-				}
+		}
+		return 0;
+	}
+	return 0;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_semitones::getTieGroup --
+//
+
+vector<HTp> Tool_semitones::getTieGroup(HTp token) {
+	vector<HTp> output;
+	if (!token) {
+		return output;
+	}
+	if (token->isNull()) {
+		return output;
+	}
+	if (!token->isData()) {
+		return output;
+	}
+	output.push_back(token);
+	if (token->isRest()) {
+		return output;
+	}
+	string subtok = token->getSubtoken(0);
+	bool continues = hasTieContinue(subtok);
+	HTp current = token;
+	while (continues) {
+		current = getNextNote(current);
+		if (!current) {
+			break;
+		}
+		string subtok = current->getSubtoken(0);
+		if (subtok.find(']') != string::npos) {
+			output.push_back(current);
+			break;
+		}
+		continues = hasTieContinue(subtok);
+	}
+	return output;
+}
 
-			} else if (staff < 0) {
-				// force stems up or add stem up to non-rest notes
-				if (hre.search(current, "[/\\\\]")) {
-					string value = hre.replaceCopy(current, "\\", "/", "g");
-					if (value != *current) {
-						current->setText(value);
-					}
-					current = current->getNextToken();
-					continue;
-				} if (current->isRest()) {
-					// Do not at stem direction to rests
-				} else {
-					// Force stem up (assuming not a chord, although it should not matter):
-					string value = hre.replaceCopy(current, "/", "$");
-					if (value != *current) {
-						current->setText(value);
-					}
-				}
-				// Add < after pitch (and accidental and qualifiers) to display
-				// on staff below.
-				m_hasCrossStaff = true;
-				string output = hre.replaceCopy(current, replacement, "([A-Ga-gr]+[-#nXYxy]*)", "g");
-				if (output != *current) {
-					current->setText(output);
-				}
-			}
+
+
+//////////////////////////////
+//
+// Tool_semitones::hasTieContinue --
+//
+
+bool Tool_semitones::hasTieContinue(const string& value) {
+	if (value.find('_') != string::npos) {
+		return true;
+	}
+	if (value.find('[') != string::npos) {
+		return true;
+	}
+	return false;
+}
+
+
+
+//////////////////////////////
+//
+// getNextNote --
+//
+
+HTp Tool_semitones::getNextNote(HTp token) {
+	HTp current = token->getNextToken();
+	while (current) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
 		}
-		current = current->getNextToken();
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+		break;
 	}
+	return current;
 }
 
 
 
 
+
 /////////////////////////////////
 //
-// Tool_satb2gs::Tool_satb2gs -- Set the recognized options for the tool.
+// Tool_shed::Tool_shed -- Set the recognized options for the tool.
 //
 
-Tool_satb2gs::Tool_satb2gs(void) {
-	// no options
+Tool_shed::Tool_shed(void) {
+	define("s|spine|spines=s",              "list of spines to process");
+	define("e|expression=s",                "regular expression");
+	define("E|exclusion-expression=s",      "regular expression to skip");
+	define("x|exclusive-interpretations=s", "apply only to spine types in list");
+	define("k|kern=b",                      "apply only to **kern data");
+	define("X=s",                           "defineable exclusive interpretation x");
+	define("Y=s",                           "defineable exclusive interpretation y");
+	define("Z=s",                           "defineable exclusive interpretation z");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_satb2gs::run -- Do the main work of the tool.
+// Tool_shed::run -- Do the main work of the tool.
 //
 
-bool Tool_satb2gs::run(HumdrumFileSet& infiles) {
+bool Tool_shed::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -120361,7 +124880,7 @@ bool Tool_satb2gs::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_satb2gs::run(const string& indata, ostream& out) {
+bool Tool_shed::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -120373,7 +124892,7 @@ bool Tool_satb2gs::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_satb2gs::run(HumdrumFile& infile, ostream& out) {
+bool Tool_shed::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -120384,9 +124903,17 @@ bool Tool_satb2gs::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_satb2gs::run(HumdrumFile& infile) {
+bool Tool_shed::run(HumdrumFile& infile) {
 	initialize();
-	processFile(infile);
+	initializeSegment(infile);
+	if (m_options.empty()) {
+		cerr << "Error: -e option is required" << endl;
+		return false;
+	}
+	for (int i=0; i<(int)m_options.size(); i++) {
+		prepareSearch(i);
+		processFile(infile);
+	}
 	return true;
 }
 
@@ -120394,450 +124921,278 @@ bool Tool_satb2gs::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_satb2gs::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_shed::prepareSearch --
 //
 
-void Tool_satb2gs::initialize(void) {
-	// do nothing
-}
-
-
+void Tool_shed::prepareSearch(int index) {
+	// deal with command-line options (seprately for each search):
+	m_exinterps.clear();
 
-//////////////////////////////
-//
-// Tool_satb2gs::processFile --
-//
+	if (getBoolean("kern")) {
+		m_exinterps.push_back("**kern");
+	} else if (getBoolean("exclusive-interpretations")) {
+		vector<string> extra = addToExInterpList();
+		for (int i=0; i<(int)extra.size(); i++) {
+			m_exinterps.push_back(extra[i]);
+		}
+	}
 
-void Tool_satb2gs::processFile(HumdrumFile& infile) {
-	vector<vector<int>> tracks;
-	getTrackInfo(tracks, infile);
+	m_search  = m_searches.at(index);
+	m_replace = m_replaces.at(index);
+	m_option  = m_options.at(index);
 
-	if ((tracks[1].size() != 2) || (tracks[3].size() != 2)) {
-		cerr << "Warning: not processing data since there must be at least four **kern spines" << endl;
-		return;
+	m_grepoptions = "";
+	if (m_option.find("i") != std::string::npos) {
+		m_grepoptions += "i";
 	}
-
-	bool goodHeader = validateHeader(infile);
-	if (!goodHeader) {
-		cerr << "Warning: no spine manipulations allows within header, not processing file" << endl;
-		return;
+	if (m_option.find("g") != std::string::npos) {
+		m_grepoptions += "g";
 	}
 
-	bool dataQ = false;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
-		}
-		if (infile[i].isData()) {
-			if (!dataQ) {
-				printSpineSplitLine(tracks);
-			}
-			dataQ = true;
+	if (m_option.find("X") != std::string::npos) {
+		if (m_xInterp != "") {
+			m_exinterps.push_back(m_xInterp);
 		}
-		if (!dataQ) {
-			printHeaderLine(infile, i, tracks);
-			continue;
+	}
+	if (m_option.find("Y") != std::string::npos) {
+		if (m_yInterp != "") {
+			m_exinterps.push_back(m_yInterp);
 		}
-		HTp token = infile.token(i, 0);
-		if (*token == "*-") {
-			printSpineMergeLine(tracks);
-			printTerminatorLine(tracks);
-			continue;
+	}
+	if (m_option.find("Z") != std::string::npos) {
+		if (m_zInterp != "") {
+			m_exinterps.push_back(m_zInterp);
 		}
-		printRegularLine(infile, i, tracks);
 	}
-}
-
-
-
-//////////////////////////////
-//
-// Tool_satb2gs::printRegularLine -- print a regular line
-//   (between first data line and before terminator line).
-//
 
-void Tool_satb2gs::printRegularLine(HumdrumFile& infile, int line,
-		vector<vector<int>>& tracks) {
+	m_data = true;             // process data
+	m_barline = false;         // process barline
+	m_exinterp = false;        // process exclusive interpretations
+	m_interpretation = false;  // process interpretations (other than exinterp
+	                           //     and spine manipulators).
 
-	int spinecount = infile[line].getFieldCount();
-	int track;
-	HTp token;
-	vector<vector<vector<HTp>>> tokens;
-	tokens.resize(5);
-	for (int i=0; i<(int)tracks.size(); i++) {
-		tokens[i].resize(tracks[i].size());
+	if (m_option.find("I") != std::string::npos) {
+		m_interpretation = true;
+		m_data = false;
 	}
-
-	// store tokens in output order:
-	for (int i=0; i<(int)tracks.size(); i++) {
-		for (int j=0; j<(int)tracks[i].size(); j++) {
-			int target = tracks[i][j];
-			for (int k=0; k<spinecount; k++) {
-				token = infile.token(line, k);
-				track = token->getTrack();
-				if (track != target) {
-					continue;
-				}
-				tokens[i][j].push_back(token);
-			}
-		}
+	if (m_option.find("X") != std::string::npos) {
+		m_exinterp = true;
+		m_data = false;
 	}
-
-	int counter = 0;
-	HTp top;
-	HTp bot;
-	HTp inner;
-	HTp outer;
-	bool suppressQ;
-
-	// now print in output order, but hide fermatas
-	// in the alto and tenor parts if there are fermatas
-	// int the soprano and bass parts respectively.
-	for (int i=0; i<(int)tokens.size(); i++) {
-		for (int j=0; j<(int)tokens[i].size(); j++) {
-			switch (i) {
-				case 0:
-				case 2:
-				case 4:
-					// non-kern spines
-					for (int k=0; k<(int)tokens[i][j].size(); k++) {
-						m_humdrum_text << tokens[i][j][k];
-						counter++;
-						if (counter < spinecount) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-
-				case 1:
-				case 3:
-					top = tokens[i][0][0];
-					bot = tokens[i][1][0];
-					if (i == 1) {
-						// tenor: top is inner
-						inner = top;
-						outer = bot;
-					} else {
-						// alto: bottom is inner
-						inner = bot;
-						outer = top;
-					}
-					if (inner->hasFermata() && outer->hasFermata()) {
-						suppressQ = true;
-					} else {
-						suppressQ = false;
-					}
-
-					for (int k=0; k<(int)tokens[i][j].size(); k++) {
-						token = tokens[i][j][k];
-						if (suppressQ && ((void*)token == (void*)inner)) {
-							string value = *token;
-							// Make fermata invisible by adding 'y' after it:
-							for (int m=0; m<(int)value.size(); m++) {
-								m_humdrum_text << value[m];
-								if (value[m] == ';') {
-									if (m < (int)value.size() - 1) {
-										if (value.at(m+1) != 'y') {
-											m_humdrum_text << 'y';
-										}
-									} else {
-											m_humdrum_text << 'y';
-									}
-								}
-							}
-						} else {
-							m_humdrum_text << token;
-						}
-						counter++;
-						if (counter < spinecount) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-			}
-		}
+	if (m_option.find("B") != std::string::npos) {
+		m_barline = true;
+		m_data = false;
+	}
+	if (m_option.find("M") != std::string::npos) {
+		// measure is an alias for barline
+		m_barline = true;
+		m_data = false;
+	}
+	if (m_option.find("L") != std::string::npos) {
+		m_localcomment = true;
+		m_data = false;
+	}
+	if (m_option.find("G") != std::string::npos) {
+		m_globalcomment = true;
+		m_data = false;
+	}
+	if (m_option.find("K") != std::string::npos) {
+		m_referencekey = true;
+		m_data = false;
+	}
+	if (m_option.find("V") != std::string::npos) {
+		m_referencevalue = true;
+		m_data = false;
+	}
+	if (m_option.find("R") != std::string::npos) {
+		m_reference = true;
+		m_referencekey = false;
+		m_referencevalue = false;
+		m_data = false;
+	}
+	if (m_option.find("D") != std::string::npos) {
+		m_data = true;
 	}
 
-	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_satb2gs::printTerminatorLine --  Print the terminator line in the
-//   output data.
+// Tool_shed::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_satb2gs::printTerminatorLine(vector<vector<int>>& tracks) {
-	int count = getNewTrackCount(tracks);
-	for (int i=0; i<count; i++) {
-		m_humdrum_text << "*-";
-		if (i < count - 1) {
-			m_humdrum_text << "\t";
-		}
+void Tool_shed::initialize(void) {
+	if (getBoolean("expression")) {
+		string value = getString("expression");
+		parseExpression(value);
 	}
-	m_humdrum_text << endl;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_satb2gs::printSpineSplitLine --
-//
-
-void Tool_satb2gs::printSpineSplitLine(vector<vector<int>>& tracks) {
-	int count = getNewTrackCount(tracks);
-	int counter = 0;
+	m_exclusion = getString("exclusion-expression");
 
-	for (int i=0; i<(int)tracks.size(); i++) {
-		switch (i) {
-			case 0:
-			case 2:
-			case 4:
-				for (int j=0; j<(int)tracks[i].size(); j++) {
-					m_humdrum_text << "*";
-					counter++;
-					if (counter < count) {
-						m_humdrum_text << "\t";
-					}
-				}
-				break;
-			case 1:
-			case 3:
-				m_humdrum_text << "*^";
-				counter++;
-				if (counter < count) {
-					m_humdrum_text << "\t";
-				}
-				break;
-		}
+	if (getBoolean("X")) {
+		m_xInterp = getExInterp(getString("X"));
+	}
+	if (getBoolean("Y")) {
+		m_yInterp = getExInterp(getString("Y"));
+	}
+	if (getBoolean("Z")) {
+		m_zInterp = getExInterp(getString("Z"));
 	}
-	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_satb2gs::printSpineMergeLine --
+// Tool_shed::getExInterp --
 //
 
-void Tool_satb2gs::printSpineMergeLine(vector<vector<int>>& tracks) {
-	int count = getNewTrackCount(tracks);
-	count += 2;
-	int counter;
-
-	if (!tracks[2].empty()) {
-		// do not need to place merges on separate lines since they are
-		// separated by non-kern spine(s) between bass and soprano subspines.
-
-		counter = 0;
-		for (int i=0; i<(int)tracks.size(); i++) {
-			switch (i) {
-				case 0:
-				case 2:
-				case 4:
-					for (int j=0; j<(int)tracks[i].size(); j++) {
-						m_humdrum_text << "*";
-						counter++;
-						if (counter < count) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-				case 1:
-				case 3:
-					for (int j=0; j<(int)tracks[i].size(); j++) {
-						m_humdrum_text << "*v";
-						counter++;
-						if (counter < count) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-			}
-		}
-		m_humdrum_text << endl;
-
-	} else {
-		// Merges for tenor/bass and soprano/alto need to be placed
-		// on separate lines.
-
-		// First merge tenor/bass (tracks[1])
-		counter = 0;
-		for (int i=0; i<(int)tracks.size(); i++) {
-			switch (i) {
-				case 0:
-				case 2:
-				case 3:
-				case 4:
-					for (int j=0; j<(int)tracks[i].size(); j++) {
-						m_humdrum_text << "*";
-						counter++;
-						if (counter < count) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-				case 1:
-					for (int j=0; j<(int)tracks[i].size(); j++) {
-						m_humdrum_text << "*v";
-						counter++;
-						if (counter < count) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-			}
-		}
-		m_humdrum_text << endl;
-
-		// Now merge soprano/alto (tracks[3])
-		count--;
-		counter = 0;
-		for (int i=0; i<(int)tracks.size(); i++) {
-			switch (i) {
-				case 0:
-				case 2:
-				case 4:
-					for (int j=0; j<(int)tracks[i].size(); j++) {
-						m_humdrum_text << "*";
-						counter++;
-						if (counter < count) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-				case 1:
-					m_humdrum_text << "*";
-					m_humdrum_text << "\t";
-					counter++;
-					break;
-				case 3:
-					for (int j=0; j<(int)tracks[i].size(); j++) {
-						m_humdrum_text << "*v";
-						counter++;
-						if (counter < count) {
-							m_humdrum_text << "\t";
-						}
-					}
-					break;
-			}
-		}
-		m_humdrum_text << endl;
+string Tool_shed::getExInterp(const string& value) {
+	if (value == "") {
+		return "**";
+	}
+	if (value == "*") {
+		return "**";
+	}
+	if (value.compare(0, 2, "**") == 0) {
+		return value;
+	}
+	if (value.compare(0, 1, "*") == 0) {
+		return "*" + value;
 	}
+	return "**" + value;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_satb2gs::getNewTrackCount -- Return the number of tracks (spines)
-//   in the output data (not counting subspines).
+// Tool_shed::parseExpression --
+//     Form of string:
+//        s/search/replace/options; s/search2/replace2/options2
+//
 //
 
-int Tool_satb2gs::getNewTrackCount(vector<vector<int>>& tracks) {
-	int sum = 0;
-	for (int i=0; i<(int)tracks.size(); i++) {
-		for (int j=0; j<(int)tracks[i].size(); j++) {
-			sum++;
+void Tool_shed::parseExpression(const string& expression) {
+	int state = 0;
+
+	m_searches.clear();
+	m_replaces.clear();
+	m_options.clear();
+
+	char divchar = '/';
+
+	for (int i=0; i<(int)expression.size(); i++) {
+		if (state == 0) {  // start of expression
+			if (isspace(expression[i])) {
+				continue;
+			} else if (expression[i] == 's') {
+				if (i >= (int)expression.size() - 1) {
+					cerr << "Error: spurious s at end of expression: "
+					     << expression << endl;
+					return;
+				} else {
+					divchar = expression[i+1];
+					i++;
+					state++;
+					m_searches.push_back("");
+				}
+			} else {
+				cerr << "Error at position " << i
+				     << " in expression: " << expression << endl;
+				return;
+			}
+		} else if (state == 1) { // search string
+			if (expression[i] == divchar) {
+				state++;
+				m_replaces.push_back("");
+				continue;
+			} if (expression[i] == '\\') {
+				if (i >= (int)expression.size() - 1) {
+					cerr << "Error: expression ends too soon: "
+					     << expression << endl;
+					return;
+				} else {
+					m_searches.back() += '\\';
+					m_searches.back() += expression[i+1];
+					i++;
+				}
+			} else {
+				m_searches.back() += expression[i];
+			}
+		} else if (state == 2) { // replace string
+			if (expression[i] == divchar) {
+				state++;
+				m_options.push_back("");
+				continue;
+			} if (expression[i] == '\\') {
+				if (i >= (int)expression.size() - 1) {
+					cerr << "Error: expression ends too soon: "
+					     << expression << endl;
+					return;
+				} else {
+					m_replaces.back() += '\\';
+					m_replaces.back() += expression[i+1];
+					i++;
+				}
+			} else {
+				m_replaces.back() += expression[i];
+			}
+		} else if (state == 3) { // regular expression options
+			if (expression[i] == ';') {
+				state++;
+			} else if (isspace(expression[i])) {
+				state++;
+			} else {
+				m_options.back() += expression[i];
+			}
+		}
+		if (state == 4) {
+			state = 0;
 		}
 	}
-	// remove two spines that were merged into two others:
-	sum -= 2;
-	return sum;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_satb2gs::printHeaderLine --
+// Tool_shed::initializeSegment -- Recalculate variables for each Humdrum
+//      input segment.
 //
 
-void Tool_satb2gs::printHeaderLine(HumdrumFile& infile, int line,
-		vector<vector<int>>& tracks) {
-	int count = infile.getMaxTrack() - 2;
-
-	HTp token;
-	int counter = 0;
-	for (int i=0; i<(int)tracks.size(); i++) {
-		switch (i) {
-			case 0:
-			case 2:
-			case 4:
-				for (int j=0; j<(int)tracks[i].size(); j++) {
-					token = infile.token(line, tracks[i][j]-1);
-					m_humdrum_text << token;
-					counter++;
-					if (counter < count) {
-						m_humdrum_text << "\t";
-					}
-				}
-				break;
-
-			case 1:
-			case 3:
-				token = infile.token(line, tracks[i][0]-1);
-				if (token->isInstrumentName()) {
-					// suppress instrument names, but keep blank name
-					// to force indent.
-					m_humdrum_text << "*I\"";
-				} else if (token->isInstrumentAbbreviation()) {
-					// suppress instrument abbreviations
-					m_humdrum_text << "*";
-				} else if (token->isInstrumentDesignation()) {
-					// suppress instrument designations (such as *Itenor)
-					m_humdrum_text << "*";
-				} else if (token->isClef()) {
-					vector<HTp> clefs = getClefs(infile, line);
-					if (i == 1) {
-						if (clefs.size() == 4) {
-							m_humdrum_text << clefs[0];
-						} else {
-							m_humdrum_text << "*clefF4";
-						}
-					} else {
-						if (clefs.size() == 4) {
-							m_humdrum_text << clefs.back();
-						} else {
-							m_humdrum_text << "*clefG2";
-						}
-					}
-				} else {
-					m_humdrum_text << token;
-				}
-				counter++;
-				if (counter < count) {
-					m_humdrum_text << "\t";
-				}
-				break;
-		}
+void Tool_shed::initializeSegment(HumdrumFile& infile) {
+	m_spines.clear();
+	if (getBoolean("spines")) {
+		int maxtrack = infile.getMaxTrack();
+		Convert::makeBooleanTrackList(m_spines, getString("spines"), maxtrack);
 	}
-	m_humdrum_text << endl;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_satb2gs::getClefs -- get a list of the clefs on the current line.
+// Tool_shed::addToExInterpList --
 //
 
-vector<HTp> Tool_satb2gs::getClefs(HumdrumFile& infile, int line) {
-	vector<HTp> output;
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		HTp token = infile[line].token(i);
-		if (!token->isKern()) {
+vector<string> Tool_shed::addToExInterpList(void) {
+	string elist = getString("exclusive-interpretations");
+	elist = Convert::trimWhiteSpace(elist);
+	HumRegex hre;
+	hre.replaceDestructive(elist, "", "^[,;\\s*]+");
+	hre.replaceDestructive(elist, "", "[,;\\s*]+$");
+	vector<string> pieces;
+	hre.split(pieces, elist, "[,;\\s*]+");
+
+	vector<string> output;
+	for (int i=0; i<(int)pieces.size(); i++) {
+		if (pieces[i].empty()) {
 			continue;
 		}
-		if (token->isClef()) {
-			output.push_back(token);
-		}
+		output.push_back("**" + pieces[i]);
 	}
 	return output;
 }
@@ -120846,111 +125201,93 @@ vector<HTp> Tool_satb2gs::getClefs(HumdrumFile& infile, int line) {
 
 //////////////////////////////
 //
-// Tool_satb2gs::getTrackInfo --
-//     tracks 0 = list of spines before bass **kern spine
-//     tracks 1 = tenor and then bass **kern track numbers
-//     tracks 2 = aux. spines after after tenor and then after bass
-//     tracks 3 = soprano and then alto **kern track numbers
-//     tracks 4 = aux. spines after after soprano and then after alto
+// Tool_shed::processFile --
 //
 
-void Tool_satb2gs::getTrackInfo(vector<vector<int>>& tracks, HumdrumFile& infile) {
-	tracks.resize(5);
-	for (int i=0; i<(int)tracks.size(); i++) {
-		tracks[i].clear();
+void Tool_shed::processFile(HumdrumFile& infile) {
+	if (m_search == "") {
+		// nothing to do
+		return;
 	}
-	vector<HTp> sstarts;
-	infile.getSpineStartList(sstarts);
-	int track;
+	m_modified = false;
 
-	// fill in tracks[0]: spines before first **kern spine
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (sstarts[i]->isKern()) {
-			break;
-		}
-		track = sstarts[i]->getTrack();
-		tracks[0].push_back(track);
+	if (m_interpretation) {
+		searchAndReplaceInterpretation(infile);
 	}
 
-	int kcount = 0;
+	if (m_localcomment) {
+		searchAndReplaceLocalComment(infile);
+	}
 
-	kcount = 0;
-	// Store tracks related to the tenor part:
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (sstarts[i]->isKern()) {
-			kcount++;
-		}
-		if (kcount > 2) {
-			break;
-		}
-		if (kcount < 2) {
-			continue;
-		}
-		track = sstarts[i]->getTrack();
-		if (sstarts[i]->isKern()) {
-			tracks[1].push_back(track);
-		} else {
-			tracks[2].push_back(track);
-		}
+	if (m_globalcomment) {
+		searchAndReplaceGlobalComment(infile);
 	}
 
-	kcount = 0;
-	// Store tracks related to the bass part:
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (sstarts[i]->isKern()) {
-			kcount++;
-		}
-		if (kcount > 1) {
-			break;
-		}
-		if (kcount < 1) {
-			continue;
-		}
-		track = sstarts[i]->getTrack();
-		if (sstarts[i]->isKern()) {
-			tracks[1].push_back(track);
-		} else {
-			tracks[2].push_back(track);
-		}
+	if (m_reference) {
+		searchAndReplaceReferenceRecords(infile);
 	}
 
-	kcount = 0;
-	// Store tracks related to the soprano part:
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (sstarts[i]->isKern()) {
-			kcount++;
-		}
-		if (kcount > 4) {
-			break;
-		}
-		if (kcount < 4) {
-			continue;
-		}
-		track = sstarts[i]->getTrack();
-		if (sstarts[i]->isKern()) {
-			tracks[3].push_back(track);
-		} else {
-			tracks[4].push_back(track);
-		}
+	if (m_referencekey) {
+		searchAndReplaceReferenceKeys(infile);
 	}
 
-	kcount = 0;
-	// Store tracks related to the alto part:
-	for (int i=0; i<(int)sstarts.size(); i++) {
-		if (sstarts[i]->isKern()) {
-			kcount++;
-		}
-		if (kcount > 3) {
-			break;
-		}
-		if (kcount < 3) {
+	if (m_referencevalue) {
+		searchAndReplaceReferenceValues(infile);
+	}
+
+	if (m_exinterp) {
+		searchAndReplaceExinterp(infile);
+	}
+
+	if (m_barline) {
+		searchAndReplaceBarline(infile);
+	}
+
+	if (m_data) {
+		searchAndReplaceData(infile);
+	}
+
+	if (m_modified) {
+		infile.createLinesFromTokens();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_shed::searchAndReplaceBarline --
+//
+
+void Tool_shed::searchAndReplaceBarline(HumdrumFile& infile) {
+	string isearch;
+	if (m_search[0] == '^') {
+		isearch = "^=" + m_search.substr(1);
+	} else {
+		isearch = "^=.*" + m_search;
+	}
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isBarline()) {
 			continue;
 		}
-		track = sstarts[i]->getTrack();
-		if (sstarts[i]->isKern()) {
-			tracks[3].push_back(track);
-		} else {
-			tracks[4].push_back(track);
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isNull()) {
+				// Don't mess with null interpretations
+				continue;
+			}
+			if (!isValid(token)) {
+				continue;
+			}
+			if (hre.search(token, isearch, m_grepoptions)) {
+				string text = token->getText().substr(1);
+				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
+				hre.replaceDestructive(text, "", "^=+");
+				text = "=" + text;
+				token->setText(text);
+				m_modified = true;
+			}
 		}
 	}
 }
@@ -120959,176 +125296,248 @@ void Tool_satb2gs::getTrackInfo(vector<vector<int>>& tracks, HumdrumFile& infile
 
 //////////////////////////////
 //
-// Tool_satb2gs::validateHeader -- Header cannot contain
-//   spine manipulators.
+// Tool_shed::searchAndReplaceInterpretation --
 //
 
-bool Tool_satb2gs::validateHeader(HumdrumFile& infile) {
+void Tool_shed::searchAndReplaceInterpretation(HumdrumFile& infile) {
+	string isearch;
+	if (m_search[0] == '^') {
+		isearch = "^\\*" + m_search.substr(1);
+	} else {
+		isearch = "^\\*.*" + m_search;
+	}
+	HumRegex hre;
 	for (int i=0; i<infile.getLineCount(); i++) {
-		if (infile[i].isData()) {
-			break;
-		}
 		if (!infile[i].isInterpretation()) {
 			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (token->isExclusive()) {
+		} else if (infile[i].isExclusiveInterpretation()) {
+			continue;
+		} else if (infile[i].isManipulator()) {
 			continue;
 		}
-		if (infile[i].isManipulator()) {
-			return false;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isNull()) {
+				// Don't mess with null interpretations
+				continue;
+			}
+			if (!isValid(token)) {
+				continue;
+			}
+			if (hre.search(token, isearch, m_grepoptions)) {
+				string text = token->getText().substr(1);
+				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
+				hre.replaceDestructive(text, "", "^\\*+");
+				text = "*" + text;
+				token->setText(text);
+				m_modified = true;
+			}
 		}
 	}
-
-	return true;
 }
 
 
 
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_scordatura::Tool_scordatura -- Set the recognized options for the tool.
+// Tool_shed::searchAndReplaceLocalComment --
 //
 
-Tool_scordatura::Tool_scordatura(void) {
-	define("s|sounding=b",      "generate sounding score");
-	define("w|written=b",       "generate written score");
-	define("m|mark|marker=s:@", "marker to add to score");
-	define("p|pitch|pitches=s", "list of pitches to mark");
-	define("i|interval=s",      "musical interval of marked pitches");
-	define("I|is-sounding=s",   "musical score is in sounding format for marks");
-	define("c|chromatic=i:0",   "chromatic interval of marked pitches");
-	define("d|diatonic=i:0",    "diatonic interval of marked pitches");
-	define("color=s",           "color marked pitches");
-	define("string=s",          "string number");
+void Tool_shed::searchAndReplaceLocalComment(HumdrumFile& infile) {
+	string isearch;
+	if (m_search[0] == '^') {
+		isearch = "^!" + m_search.substr(1);
+	} else {
+		isearch = "^!.*" + m_search;
+	}
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isLocalComment()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isNull()) {
+				// Don't mess with null interpretations
+				continue;
+			}
+			if (!isValid(token)) {
+				continue;
+			}
+			if (hre.search(token, isearch, m_grepoptions)) {
+				string text = token->getText().substr(1);
+				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
+				hre.replaceDestructive(text, "", "^!+");
+				text = "!" + text;
+				token->setText(text);
+				m_modified = true;
+			}
+		}
+	}
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_scordatura::run -- Do the main work of the tool.
+// Tool_shed::searchAndReplaceGlobalComment --
 //
 
-bool Tool_scordatura::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+void Tool_shed::searchAndReplaceGlobalComment(HumdrumFile& infile) {
+	string isearch;
+	if (m_search[0] == '^') {
+		isearch = "^!!" + m_search.substr(1);
+	} else {
+		isearch = "^!!.*" + m_search;
+	}
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isGlobalComment()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		if (token->size() < 3) {
+			// Don't mess with null comments
+			continue;
+		}
+		if (hre.search(token, isearch, m_grepoptions)) {
+			string text = token->getText().substr(2);
+			hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
+			hre.replaceDestructive(text, "", "^!+");
+			text = "!!" + text;
+			token->setText(text);
+			m_modified = true;
+		}
 	}
-	return status;
 }
 
 
-bool Tool_scordatura::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
 
+//////////////////////////////
+//
+// Tool_shed::searchAndReplaceReferenceRecords --
+//
 
-bool Tool_scordatura::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
+void Tool_shed::searchAndReplaceReferenceRecords(HumdrumFile& infile) {
+	string isearch;
+	if (m_search[0] == '^') {
+		isearch = "^!!!" + m_search.substr(1);
 	} else {
-		out << infile;
+		isearch = "^!!!.*" + m_search;
+	}
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isGlobalReference()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		if (hre.search(token, isearch, m_grepoptions)) {
+			string text = token->getText().substr(1);
+			hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
+			hre.replaceDestructive(text, "", "^!+");
+			text = "!!!" + text;
+			token->setText(text);
+			m_modified = true;
+		}
 	}
-	return status;
-}
-
-
-bool Tool_scordatura::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_shed::searchAndReplaceReferenceKeys --
 //
 
-void Tool_scordatura::initialize(void) {
-	m_writtenQ  = getBoolean("written");
-	m_soundingQ = getBoolean("sounding");
-	m_pitches.clear();
-	m_marker = getString("mark");
-	m_IQ = getBoolean("I");
-	m_color = getString("color");
-	if (getBoolean("pitches")) {
-		m_pitches = parsePitches(getString("pitches"));
-	}
-	m_cd = getBoolean("diatonic") && getBoolean("chromatic");
-	m_interval.clear();
-	if (m_cd) {
-		m_diatonic = getInteger("diatonic");
-		m_chromatic = getInteger("chromatic");
-	} else {
-		if (getBoolean("interval")) {
-			m_interval = getString("interval");
+void Tool_shed::searchAndReplaceReferenceKeys(HumdrumFile& infile) {
+	string isearch = m_search;
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isGlobalReference()) {
+			continue;
+		}
+		HTp token = infile.token(i, 0);
+		string key = infile[i].getReferenceKey();
+		if (hre.search(key, isearch, m_grepoptions)) {
+			hre.replaceDestructive(key, m_replace, m_search, m_grepoptions);
+			hre.replaceDestructive(key, "", "^!+");
+			hre.replaceDestructive(key, "", ":+$");
+			string value = infile[i].getReferenceValue();
+			string text = "!!!" + key + ": " + value;
+			token->setText(text);
+			m_modified = true;
 		}
 	}
-	if ((abs(m_diatonic) > 28) || (abs(m_chromatic) > 48)) {
-		m_diatonic = 0;
-		m_chromatic = 0;
-		m_cd = false;
-	}
-	if (!m_pitches.empty()) {
-		prepareTranspositionInterval();
-	}
-	m_string = getString("string");
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::processFile --
+// Tool_shed::searchAndReplaceReferenceValues --
 //
 
-void Tool_scordatura::processFile(HumdrumFile& infile) {
-	m_modifiedQ = false;
-
-	if (!m_pitches.empty()) {
-		markPitches(infile);
-		if (m_modifiedQ) {
-			addMarkerRdf(infile);
+void Tool_shed::searchAndReplaceReferenceValues(HumdrumFile& infile) {
+	string isearch = m_search;
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isGlobalReference()) {
+			continue;
 		}
-	}
-
-	if (m_writtenQ || m_soundingQ) {
-		vector<HTp> rdfs;
-		getScordaturaRdfs(rdfs, infile);
-		if (!rdfs.empty()) {
-			processScordaturas(infile, rdfs);
+		HTp token = infile.token(i, 0);
+		string value = infile[i].getReferenceValue();
+		if (hre.search(value, isearch, m_grepoptions)) {
+			hre.replaceDestructive(value, m_replace, m_search, m_grepoptions);
+			hre.replaceDestructive(value, "", "^!+");
+			hre.replaceDestructive(value, "", ":+$");
+			string key = infile[i].getReferenceKey();
+			string text = "!!!" + key + ": " + value;
+			token->setText(text);
+			m_modified = true;
 		}
 	}
-
-	if (m_modifiedQ) {
-		infile.createLinesFromTokens();
-	}
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::processScoredaturas --
+// Tool_shed::searchAndReplaceExinterp --
 //
 
-void Tool_scordatura::processScordaturas(HumdrumFile& infile, vector<HTp>& rdfs) {
-	for (int i=0; i<(int)rdfs.size(); i++) {
-		processScordatura(infile, rdfs[i]);
+void Tool_shed::searchAndReplaceExinterp(HumdrumFile& infile) {
+	string isearch;
+	if (m_search[0] == '^') {
+		isearch = "^\\*\\*" + m_search.substr(1);
+	} else {
+		isearch = "^\\*\\*.*" + m_search;
+	}
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		} else if (!infile[i].isExclusiveInterpretation()) {
+			// assuming a single line for all exclusive interpretations
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isNull()) {
+				// Don't mess with null interpretations
+				continue;
+			}
+			if (!isValid(token)) {
+				continue;
+			}
+			if (hre.search(token, isearch, m_grepoptions)) {
+				string text = token->getText().substr(2);
+				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
+				hre.replaceDestructive(text, "", "^\\*+");
+				text = "**" + text;
+				token->setText(text);
+				m_modified = true;
+			}
+		}
 	}
 }
 
@@ -121136,333 +125545,320 @@ void Tool_scordatura::processScordaturas(HumdrumFile& infile, vector<HTp>& rdfs)
 
 //////////////////////////////
 //
-// Tool_scordatura::processScordatura --
+// Tool_shed::searchAndReplaceData --
 //
 
-void Tool_scordatura::processScordatura(HumdrumFile& infile, HTp reference) {
-	HumRegex hre;
+void Tool_shed::searchAndReplaceData(HumdrumFile& infile) {
+	string dsearch = m_search;
 
-	if (m_writtenQ) {
-		if (!hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*([^\\s]+)\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*ITrd(-?\\d+)c(-?\\d+)\\b")) {
-			return;
+	HumRegex hre;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isData()) {
+			continue;
 		}
-	} else if (m_soundingQ) {
-		if (!hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*([^\\s]+)\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*Trd(-?\\d+)c(-?\\d+)\\b")) {
-			return;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->isNull()) {
+				// Don't mess with null interpretations
+				continue;
+			}
+			if (!isValid(token)) {
+				continue;
+			}
+			if (hre.search(token, dsearch, m_grepoptions)) {
+				string text = token->getText();
+				hre.replaceDestructive(text, m_replace, dsearch, m_grepoptions);
+				if (text == "") {
+					text = ".";
+				}
+				token->setText(text);
+				m_modified = true;
+			}
 		}
 	}
-
-	string marker = hre.getMatch(1);
-	int diatonic = hre.getMatchInt(2);
-	int chromatic = hre.getMatchInt(3);
-
-	if (diatonic == 0 && chromatic == 0) {
-		// nothing to do
-		return;
-	}
-
-	flipScordaturaInfo(reference, diatonic, chromatic);
-	transposeMarker(infile, marker, diatonic, chromatic);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::transposeMarker --
+// Tool_shed::isValidDataType -- usar with -x and -k options.
 //
 
-
-void Tool_scordatura::transposeMarker(HumdrumFile& infile, const string& marker, int diatonic, int chromatic) {
-	m_transposer.setTranspositionDC(diatonic, chromatic);
-	for (int i=0; i<infile.getStrandCount(); i++) {
-		HTp sstart = infile.getStrandBegin(i);
-		if (!sstart->isKern()) {
-			continue;
+bool Tool_shed::isValidDataType(HTp token) {
+	if (m_exinterps.empty()) {
+		return true;
+	}
+	string datatype = token->getDataType();
+	for (int i=0; i<(int)m_exinterps.size(); i++) {
+		if (datatype == m_exinterps[i]) {
+			return true;
 		}
-		HTp sstop = infile.getStrandEnd(i);
-		transposeStrand(sstart, sstop, marker);
 	}
+	return false;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::transposeStrand --
+// Tool_shed::isValidSpine -- used with -s option.
 //
 
-void Tool_scordatura::transposeStrand(HTp sstart, HTp sstop, const string& marker) {
-	HTp current = sstart;
-	while (current && current != sstop) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull() || current->isRest()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->find(marker) != string::npos) {
-			transposeChord(current, marker);
-		}
-		current = current->getNextToken();
+bool Tool_shed::isValidSpine(HTp token) {
+	if (m_spines.empty()) {
+		return true;
 	}
+	int track = token->getTrack();
+	return m_spines.at(track);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::transposeChord --
+// Tool_shed::isValid --
 //
 
-void Tool_scordatura::transposeChord(HTp token, const string& marker) {
-	int scount = token->getSubtokenCount();
-	if (scount == 1) {
-		string inputnote = *token;
-		string newtoken;
-		newtoken = transposeNote(inputnote);
-		token->setText(newtoken);
-		return;
-	}
-	vector<string> subtokens;
-	subtokens = token->getSubtokens();
-	for (int i=0; i<(int)subtokens.size(); i++) {
-		if (subtokens[i].find(marker) == string::npos) {
-			continue;
+bool Tool_shed::isValid(HTp token) {
+	if (!m_exclusion.empty()) {
+		HumRegex hre;
+		if (hre.search(token, m_exclusion)) {
+			return false;
 		}
-		string newtoken = transposeNote(subtokens[i]);
-		subtokens[i] = newtoken;
 	}
-	string newchord;
-	for (int i=0; i<(int)subtokens.size(); i++) {
-		newchord += subtokens[i];
-		if (i<(int)subtokens.size() - 1) {
-			newchord += ' ';
-		}
+	if (isValidDataType(token) && isValidSpine(token)) {
+		return true;
 	}
-	token->setText(newchord);
+	return false;
 }
 
 
 
-//////////////////////////////
+
+
+/////////////////////////////////
 //
-// Tool_scordatura::transposeNote --
+// Tool_sic::Tool_sic -- Set the recognized options for the tool.
 //
 
-string Tool_scordatura::transposeNote(const string& note) {
-	HumRegex hre;
-	if (!hre.search(note, "(.*?)([A-Ga-g]+[-#]*)(.*)")) {
-		return note;
-	}
-	string pre = hre.getMatch(1);
-	string pitch = hre.getMatch(2);
-	string post = hre.getMatch(3);
-	HumPitch hpitch;
-	hpitch.setKernPitch(pitch);
-	m_transposer.transpose(hpitch);
-	string output;
-	output = pre;
-	output += hpitch.getKernPitch();
-	output += post;
-	return output;
+Tool_sic::Tool_sic(void) {
+	define("s|substitution=b", "insert substitutions into music");
+	define("o|original=b",     "insert originals into music");
+	define("r|remove=b",       "remove sic layout tokens");
+	define("v|verbose=b",      "add verbose parameter");
+	define("q|quiet=b",        "remove verbose parameter");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_scordatura::flipScordaturaInfo --
+// Tool_sic::run -- Do the main work of the tool.
 //
 
-void Tool_scordatura::flipScordaturaInfo(HTp reference, int diatonic, int chromatic) {
-	diatonic *= -1;
-	chromatic *= -1;
-	string output;
-	if (m_writtenQ) {
-		output = "Trd";
-		output += to_string(diatonic);
-		output += "c";
-		output += to_string(chromatic);
-	} else if (m_soundingQ) {
-		output = "ITrd";
-		output += to_string(diatonic);
-		output += "c";
-		output += to_string(chromatic);
+bool Tool_sic::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_sic::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
 	} else {
-		return;
+		out << infile;
 	}
-	HumRegex hre;
-	string token = *reference;
-	hre.replaceDestructive(token, output, "I?Trd-?\\dc-?\\d");
-	if (token != *reference) {
-		m_modifiedQ = true;
-		reference->setText(token);
+	return status;
+}
+
+
+bool Tool_sic::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
+}
+
+
+bool Tool_sic::run(HumdrumFile& infile) {
+	initialize();
+	if (!(m_substituteQ || m_originalQ || m_removeQ || m_verboseQ || m_quietQ)) {
+		return true;
 	}
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::getScoredaturaRdfs --
+// Tool_sic::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-void Tool_scordatura::getScordaturaRdfs(vector<HTp>& rdfs, HumdrumFile& infile) {
-	rdfs.clear();
-	HumRegex hre;
+void Tool_sic::initialize(void) {
+	m_substituteQ = getBoolean("substitution");
+	m_originalQ   = getBoolean("original");
+	m_removeQ     = getBoolean("remove");
+	m_verboseQ    = getBoolean("verbose");
+	m_quietQ      = getBoolean("quiet");
+}
+
+
+
+//////////////////////////////
+//
+// Tool_sic::processFile --
+//
+
+void Tool_sic::processFile(HumdrumFile& infile) {
 	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isReference()) {
+		if (!infile[i].isLocalComment()) {
 			continue;
 		}
-		HTp reference = infile.token(i, 0);
-		if (m_writtenQ) {
-			if (hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*[^\\s]+\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*ITrd-?\\d+c-?\\d+\\b")) {
-				rdfs.push_back(reference);
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile[i].token(j);
+			if (token->compare(0, 8, "!LO:SIC:") != 0) {
+				continue;
 			}
-		} else if (m_soundingQ) {
-			if (hre.search(reference, "^!!!RDF\\*\\*kern\\s*:\\s*[^\\s]+\\s*=.*\\bscordatura\\s*=\\s*[\"']?\\s*Trd-?\\d+c-?\\d+\\b")) {
-				rdfs.push_back(reference);
+			if (m_verboseQ) {
+				addVerboseParameter(token);
+			} else if (m_quietQ) {
+				removeVerboseParameter(token);
+			}
+			if (m_removeQ) {
+				token->setText("!");
+				m_modifiedQ = true;
+			} else if (m_substituteQ) {
+				insertSubstitutionToken(token);
+			} else if (m_originalQ) {
+				insertOriginalToken(token);
 			}
 		}
 	}
+	if (m_modifiedQ) {
+		infile.createLinesFromTokens();
+	}
+	m_humdrum_text << infile;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::parsePitches --
+// Tool_sic::addVerboseParameter --
 //
 
-set<int> Tool_scordatura::parsePitches(const string& input) {
+void Tool_sic::addVerboseParameter(HTp token) {
 	HumRegex hre;
-	string value = input;
-	hre.replaceDestructive(value, "-", "\\s*-\\s*", "g");
-
-	vector<string> pieces;
-	hre.split(pieces, value, "[^A-Ga-g0-9-]+");
-
-	HumPitch pitcher;
-	set<int> output;
-	string p1;
-	string p2;
-	int d1;
-	int d2;
-	for (int i=0; i<(int)pieces.size(); i++) {
-		if (hre.search(pieces[i], "(.*)-(.*)")) {
-			// pitch range
-			p1 = hre.getMatch(1);
-			p2 = hre.getMatch(2);
-			d1 = Convert::kernToBase7(p1);
-			d2 = Convert::kernToBase7(p2);
-			if ((d1 < 0) || (d2 < 0) || (d1 > d2) || (d1 > 127) || (d2 > 127)) {
-				continue;
-			}
-			for (int j=d1; j<=d2; j++) {
-				output.insert(j);
-			}
-		} else {
-			// single pitch
-			d1 = Convert::kernToBase7(pieces[i]);
-			if ((d1 < 0) || (d1 > 127)) {
-				continue;
-			}
-			output.insert(d1);
-		}
+	string value = token->getText();
+	if (hre.search(value, "(:v:)|(:v$)")) {
+		return;
 	}
-	return output;
+	string newvalue = value + ":v";
+	token->setText(newvalue);
+	m_modifiedQ = true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::markPitches --
+// Tool_sic::removeVerboseParameter --
 //
 
-void Tool_scordatura::markPitches(HumdrumFile& infile) {
-	for (int i=0; i<infile.getStrandCount(); i++) {
-		HTp sstart = infile.getStrandStart(i);
-		if (!sstart->isKern()) {
-			continue;
-		}
-		HTp sstop = infile.getStrandStop(i);
-		markPitches(sstart, sstop);
+void Tool_sic::removeVerboseParameter(HTp token) {
+	HumRegex hre;
+	string value = token->getText();
+	string newvalue = value;
+	hre.replaceDestructive(newvalue, ":", ":v:", "g");
+	hre.replaceDestructive(newvalue, "", ":v$", "");
+	if (value == newvalue) {
+		return;
 	}
+	token->setText(newvalue);
+	m_modifiedQ = true;
 }
 
 
-void Tool_scordatura::markPitches(HTp sstart, HTp sstop) {
-	HTp current = sstart;
-	while (current && (current != sstop)) {
-		if (current->isNull() || current->isRest()) {
+
+//////////////////////////////
+//
+// Tool_sic::getTargetToken -- Get the token that the layout command
+//    applies to.
+//
+
+HTp Tool_sic::getTargetToken(HTp stok) {
+	HTp current = stok->getNextToken();
+	while (current) {
+		if (current->isNull()) {
 			current = current->getNextToken();
 			continue;
 		}
-		markPitches(current);
-		current = current->getNextToken();
-	}
-}
-
-
-void Tool_scordatura::markPitches(HTp token) {
-	vector<string> subtokens = token->getSubtokens();
-	int counter = 0;
-	for (int i=0; i<(int)subtokens.size(); i++) {
-		int dia = Convert::kernToBase7(subtokens[i]);
-		if (m_pitches.find(dia) != m_pitches.end()) {
-			counter++;
-			subtokens[i] += m_marker;
+		if (current->isManipulator()) {
+			// Layout commands should not apply to manipulators nor be split
+			// from their associated token.
+			current = NULL;
+			break;
 		}
-	}
-	if (counter == 0) {
-		return;
-	}
-	string newtoken;
-	for (int i=0; i<(int)subtokens.size(); i++) {
-		newtoken += subtokens[i];
-		if (i < (int)subtokens.size() - 1) {
-			newtoken += ' ';
+		if (current->isCommentLocal()) {
+			current = current->getNextToken();
+			continue;
 		}
+		break;
 	}
-	token->setText(newtoken);
-	m_modifiedQ = true;
+	if (!current) {
+		return NULL;
+	}
+	return current;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_scordatura::addMarkerRdf --
-//
-
-void Tool_scordatura::addMarkerRdf(HumdrumFile& infile) {
-	string line = "!!!RDF**kern: ";
-	line += m_marker;
-	line += " = ";
-	if (!m_string.empty()) {
-		line += "string=";
-		line += m_string;
-		line += " ";
+// Tool_sic::insertSubstitutionToken --
+//
+
+void Tool_sic::insertSubstitutionToken(HTp sictok) {
+	HTp target = getTargetToken(sictok);
+	if (!target) {
+		return;
 	}
-	line += "scordatura=";
-	if (m_IQ) {
-		line += "I";
+	HumRegex hre;
+	vector<string> pieces;
+	hre.split(pieces, *sictok, ":");
+	string tstring = target->getText();
+	string sstring;
+	for (int i=2; i<(int)pieces.size(); i++) {
+		if (pieces[i].compare(0, 2, "s=") == 0) {
+			sstring = pieces[i].substr(2);
+		}
 	}
-	line += "Tr";
-	if (m_transposition.empty()) {
-		line += "XXX";
-	} else {
-		line += m_transposition;
+	if (sstring.empty()) {
+		return;
 	}
-	if (!m_color.empty()) {
-		line += ", color=";
-		line += m_color;
+	target->setText(sstring);
+	m_modifiedQ = true;
+	string newsic = "!LO:SIC";
+	for (int i=2; i<(int)pieces.size(); i++) {
+		if (pieces[i].compare(0, 2, "s=") == 0) {
+			newsic += ":o=" + tstring;
+		} else {
+			newsic += ":" + pieces[i];
+		}
 	}
-	infile.appendLine(line);
+	sictok->setText(newsic);
 	m_modifiedQ = true;
 }
 
@@ -121470,1534 +125866,1101 @@ void Tool_scordatura::addMarkerRdf(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_scordatura::prepareTranspositionInterval --
+// Tool_sic::insertOriginalToken --
 //
 
-void Tool_scordatura::prepareTranspositionInterval(void) {
-	m_transposition.clear();
-	if (m_cd) {
-		m_transposition = "d";
-		m_transposition += to_string(m_diatonic);
-		m_transposition += "c";
-		m_transposition += to_string(m_chromatic);
+void Tool_sic::insertOriginalToken(HTp sictok) {
+	HTp target = getTargetToken(sictok);
+	if (!target) {
 		return;
 	}
-
-	if (m_interval.empty()) {
+	HumRegex hre;
+	vector<string> pieces;
+	hre.split(pieces, *sictok, ":");
+	string tstring = target->getText();
+	string sstring;
+	for (int i=2; i<(int)pieces.size(); i++) {
+		if (pieces[i].compare(0, 2, "o=") == 0) {
+			sstring = pieces[i].substr(2);
+		}
+	}
+	if (sstring.empty()) {
 		return;
 	}
-
-	HumTransposer trans;
-	trans.intervalToDiatonicChromatic(m_diatonic, m_chromatic, m_interval);
-	m_transposition = "d";
-	m_transposition += to_string(m_diatonic);
-	m_transposition += "c";
-	m_transposition += to_string(m_chromatic);
+	target->setText(sstring);
+	m_modifiedQ = true;
+	string newsic = "!LO:SIC";
+	for (int i=2; i<(int)pieces.size(); i++) {
+		if (pieces[i].compare(0, 2, "o=") == 0) {
+			newsic += ":s=" + tstring;
+		} else {
+			newsic += ":" + pieces[i];
+		}
+	}
+	sictok->setText(newsic);
+	m_modifiedQ = true;
 }
 
 
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_semitones::Tool_semitones -- Set the recognized options for the tool.
+// MeasureData::MeasureData --
 //
 
-Tool_semitones::Tool_semitones(void) {
-	define("1|first=b",                   "mark only the first note of intervals");
-	define("2|second=b",                  "mark only the second note of intervals");
-	define("A|O|no-analysis|no-output=b", "do not print analysis spines");
-	define("I|no-input=b",                "do not print input data spines");
-	define("M|no-mark|no-marks=b",        "do not mark notes");
-	define("R|no-rests=b",                "ignore rests");
-	define("T|no-ties=b",                 "do not mark ties");
-	define("X|include|only=s",            "include only **kern tokens with given pattern");
-	define("color=s:red",                 "mark color");
-	define("c|cdata=b",                   "store resulting data as **cdata (allowing display in VHV");
-	define("d|down=b",                    "highlight notes that that have a negative semitone interval");
-	define("j|jump=i:3",                  "starting interval defining leaps");
-	define("l|leap=b",                    "highlight notes that have leap motion");
-	define("mark=s:@",                    "mark character");
-	define("m|midi=b",                    "show MIDI note number for pitches");
-	define("n|count=b",                   "output count of intervals being marked");
-	define("p|pc=b",                      "output pitch classes from C=0 instead of MIDI notes for -m option");
-	define("r|same|repeat|repeated=b",    "highlight notes that are repeated ");
-	define("s|step=b",                    "highlight notes that have step-wise motion");
-	define("u|up=b",                      "highlight notes that that have a positive semitone interval");
-	define("x|exclude=s",                 "exclude **kern tokens with given pattern");
+MeasureData::MeasureData(void) {
+	m_hist7pc.resize(7);
+	std::fill(m_hist7pc.begin(), m_hist7pc.end(), 0.0);
 }
 
 
-
-/////////////////////////////////
-//
-// Tool_semitones::run -- Do the main work of the tool.
-//
-
-bool Tool_semitones::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
+MeasureData::MeasureData(HumdrumFile& infile, int startline, int stopline) {
+	setStartLine(startline);
+	setStopLine(stopline);
+	setOwner(infile);
 }
 
 
-bool Tool_semitones::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
+MeasureData::MeasureData(HumdrumFile* infile, int startline, int stopline) {
+	setStartLine(startline);
+	setStopLine(stopline);
+	setOwner(infile);
 }
 
 
-bool Tool_semitones::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
 
+//////////////////////////////
+//
+// MeasureData::~MeasureData --
+//
 
-bool Tool_semitones::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
+MeasureData::~MeasureData() {
+	clear();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// MeasureData::setOwner --
 //
 
-void Tool_semitones::initialize(void) {
-	// processing of options goes here
-
-	m_cdataQ      = getBoolean("cdata");
-	m_count       = getBoolean("count");
-	m_downQ       = getBoolean("down");
-	m_firstQ      = getBoolean("first");
-	m_leapQ       = getBoolean("leap");
-	m_midiQ       = getBoolean("midi");
-	m_noanalysisQ = getBoolean("no-analysis");
-	m_noinputQ    = getBoolean("no-input");
-	m_nomarkQ     = getBoolean("no-marks");
-	m_notiesQ     = getBoolean("no-ties");
-	m_pcQ         = getBoolean("pc");
-	m_repeatQ     = getBoolean("repeat");
-	m_norestsQ    = getBoolean("no-rests");
-	m_secondQ     = getBoolean("second");
-	m_stepQ       = getBoolean("step");
-	m_upQ         = getBoolean("up");
-
-	m_leap        = getInteger("jump");
+void MeasureData::setOwner(HumdrumFile* infile) {
+	m_owner = infile;
+}
 
-	m_color       = getString("color");
-	m_exclude     = getString("exclude");
-	m_include     = getString("include");
-	m_marker      = getString("mark");
 
-	if (!m_firstQ && !m_secondQ) {
-		m_firstQ  = true;
-		m_secondQ = true;
-	}
+void MeasureData::setOwner(HumdrumFile& infile) {
+	m_owner = &infile;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::processFile --
+// MeasureData::setStartLine --
 //
 
-void Tool_semitones::processFile(HumdrumFile& infile) {
-	m_markCount = 0;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		analyzeLine(infile, i);
-	}
-	if ((m_markCount > 0) && !m_nomarkQ) {
-		m_humdrum_text << "!!!RDF**kern: ";
-		m_humdrum_text << m_marker;
-		m_humdrum_text << " = marked note";
-		if (getBoolean("color")) {
-			m_humdrum_text << ", color=" << m_color;
-		}
-		m_humdrum_text << '\n';
-	}
-	if (m_count) {
-		showCount();
-	}
+void MeasureData::setStartLine(int startline) {
+	m_startline = startline;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::showCount -- Give a count for the number of
-//     intervals that were marked.
+// MeasureData::setStopLine --
 //
 
-void Tool_semitones::showCount(void) {
-	m_humdrum_text << "!!semitone_count: " << m_markCount;
-	if (m_repeatQ) {
-		m_humdrum_text << " REPEAT";
-	}
-	if (m_upQ) {
-		m_humdrum_text << " UP";
-	}
-	if (m_downQ) {
-		m_humdrum_text << " DOWN";
-	}
-	if (m_stepQ) {
-		m_humdrum_text << " STEP";
-	}
-	if (m_leapQ) {
-		m_humdrum_text << " LEAP";
-	}
-	if ((m_stepQ || m_leapQ) && (m_leap != 3)) {
-		m_humdrum_text << " JUMP:" << m_leap;
-	}
-	if (m_marker != "@") {
-		m_humdrum_text << " MARK:" << m_marker;
-	}
-	m_humdrum_text << '\n';
+void MeasureData::setStopLine(int stopline) {
+	m_stopline = stopline;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::analyzeLine --  Append analysis spines after every **kern
-//   spine.
+// MeasureData::getStartLine --
 //
 
-void Tool_semitones::analyzeLine(HumdrumFile& infile, int line) {
-	int group = 0;
-	if (!infile[line].hasSpines()) {
-		m_humdrum_text << infile[line] << "\n";
-		return;
-	}
-	for (int i=0; i<infile[line].getFieldCount(); i++) {
-		HTp token = infile.token(line, i);
-		if (!m_noinputQ) {
-			if (!token->isKern()) {
-				m_humdrum_text << token;
-				if (i < infile[line].getFieldCount() - 1) {
-					m_humdrum_text << '\t';
-				}
-				continue;
-			}
-		}
-		i = processKernSpines(infile, line, i, group++);
-		if (!m_noinputQ) {
-			if (i < infile[line].getFieldCount() - 1) {
-				m_humdrum_text << '\t';
-			}
-		}
-	}
-	m_humdrum_text << '\n';
+int MeasureData::getStartLine(void) {
+	return m_startline;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::processKernSpine --
+// MeasureData::getStopLine --
 //
 
-int Tool_semitones::processKernSpines(HumdrumFile& infile, int line, int start, int kspine) {
-	HTp token = infile.token(line, start);
-	if (!token->isKern()) {
-		return start;
-	}
-	int track = token->getTrack();
-	vector<HTp> toks;
-	toks.push_back(token);
-	for (int i=start+1; i<infile[line].getFieldCount(); i++) {
-		HTp newtok = infile.token(line, i);
-		int newtrack = newtok->getTrack();
-		if (newtrack == track) {
-			toks.push_back(newtok);
-			continue;
-		}
-		break;
-	}
-
-	int toksize = (int)toks.size();
-
-	// calculate intervals/MIDI note numbers if appropriate
-	bool allQ = m_stepQ || m_leapQ || m_upQ || m_downQ || m_repeatQ;
-	bool dirQ = m_upQ || m_downQ;
-	bool typeQ = m_stepQ || m_leapQ;
-	vector<string> intervals(toksize);
-	if (infile[line].isData()) {
-		for (int i=0; i<toksize; i++) {
-			intervals[i] = getTwelveToneIntervalString(toks[i]);
-		}
-		if (allQ && !m_midiQ) {
-			for (int i=0; i<(int)intervals.size(); i++) {
-				if (intervals[i].empty()) {
-					continue;
-				}
-            if (!isdigit(intervals[i].back())) {
-					continue;
-				}
-				int value = stoi(intervals[i]);
-				if (m_upQ && m_stepQ && (value > 0) && (value < m_leap)) {
-					markInterval(toks[i]);
-				} else if (m_downQ && m_stepQ && (value < 0) && (value > -m_leap)) {
-					markInterval(toks[i]);
-				} else if (!dirQ && m_stepQ && (value != 0) && (abs(value) < m_leap)) {
-					markInterval(toks[i]);
-
-				} else if (m_upQ && m_leapQ && (value > 0) && (value >= m_leap)) {
-					markInterval(toks[i]);
-				} else if (m_downQ && m_leapQ && (value < 0) && (value <= -m_leap)) {
-					markInterval(toks[i]);
-				} else if (!dirQ && m_leapQ && (value != 0) && (abs(value) >= m_leap)) {
-					markInterval(toks[i]);
-
-				} else if (m_repeatQ && (value == 0)) {
-					markInterval(toks[i]);
-				} else if (!typeQ && m_upQ && (value > 0)) {
-					markInterval(toks[i]);
-				} else if (!typeQ && m_downQ && (value < 0)) {
-					markInterval(toks[i]);
-				}
-			}
-		}
-	}
-
-	// print the **kern fields
-	if (!m_noinputQ) {
-		for (int i=0; i<toksize; i++) {
-			m_humdrum_text << toks[i];
-			if (i < toksize - 1) {
-				m_humdrum_text << '\t';
-			}
-		}
-	}
-
-	// then print the parallel analysis fields
-
-	if (!m_noanalysisQ) {
-		if (!m_noinputQ) {
-			m_humdrum_text << '\t';
-		} else if (m_noinputQ && (kspine != 0)) {
-			m_humdrum_text << '\t';
-		}
-		if (!infile[line].isData()) {
-			if (infile[line].isLocalComment()) {
-				printTokens("!", toksize);
-	 		} else if (infile[line].isInterpretation()) {
-				if (toks[0]->compare(0, 2, "**") == 0) {
-					if (m_cdataQ) {
-						printTokens("**cdata", toksize);
-					} else if (m_midiQ) {
-						if (m_pcQ) {
-							printTokens("**mpc", toksize);
-						} else {
-							printTokens("**mnn", toksize);
-						}
-					} else {
-						printTokens("**tti", toksize);
-					}
-				} else {
-					for (int i=0; i<toksize; i++) {
-						m_humdrum_text << toks[i];
-						if (i < toksize - 1) {
-							m_humdrum_text << '\t';
-						}
-					}
-				}
-	 		} else if (infile[line].isBarline()) {
-				printTokens(*toks[0], toksize);
-			} else {
-				cerr << "STRANGE ERROR " << toks[0] << endl;
-			}
-			return start + toksize - 1;
-		}
-		// print twelve-tone analyses.
-		string value;
-		for (int i=0; i<toksize; i++) {
-			value = getTwelveToneIntervalString(toks[i]);
-			m_humdrum_text << value;
-			if (i < toksize - 1) {
-				m_humdrum_text << '\t';
-			}
-		}
-	}
-
-	return start + toksize - 1;
+int MeasureData::getStopLine(void) {
+	return m_stopline;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::markInterval -- mark the current note, any notes tied
-//    after it, and then the next note and any tied notes attached to
-//    that note.
+// MeasureData::getStartTime -- return the start time in
+//     quarter notes
 //
 
-void Tool_semitones::markInterval(HTp token) {
-	if (!token->isData()) {
-		return;
-	}
-	if (!token->isKern()) {
-		return;
-	}
-	if (token->isNull()) {
-		return;
-	}
-	if (token->isRest()) {
-		return;
-	}
-	if (token->isUnpitched()) {
-		return;
-	}
-	m_markCount++;
-	token = markNote(token, m_firstQ);
-	if (m_firstQ && !m_secondQ) {
-		return;
+double MeasureData::getStartTime(void) {
+	if (m_owner == NULL) {
+		return 0.0;
 	}
-	// find next note
-	HTp current = token->getNextToken();
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		markNote(current, m_secondQ);
-		break;
+	if (getStartLine() < 0) {
+		return 0.0;
 	}
+	return (*m_owner)[getStartLine()].getDurationFromStart().getFloat();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::markNote -- make note and any tied notes after it.
-//      Return the last note of a tied note (or the note if no tied notes
-//      after it).
+// MeasureData::getMeasure -- return the measure number of the measure.
+//   return -1 if no measure number.
 //
 
-HTp Tool_semitones::markNote(HTp token, bool markQ) {
-	string subtok = token->getSubtoken(0);
-	bool hasTieEnd = false;
-	if (subtok.find('_') != string::npos) {
-		hasTieEnd = true;
-	} else if (subtok.find(']') != string::npos) {
-		hasTieEnd = true;
-	}
-
-	if (!(hasTieEnd && m_notiesQ)) {
-		if (markQ) {
-			addMarker(token);
-		}
-	}
-
-	bool hasTie = false;
-	if (subtok.find('[') != string::npos) {
-		hasTie = true;
-	} else if (subtok.find('_') != string::npos) {
-		hasTie = true;
+int MeasureData::getMeasure(void) {
+	if (m_owner == NULL) {
+		return -1;
 	}
-
-	if (!hasTie) {
-		return token;
+	if (getStartLine() < 0) {
+		return -1;
 	}
-	HTp current = token->getNextToken();
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		subtok = current->getSubtoken(0);
-		bool hasTie = false;
-		if (subtok.find('[') != string::npos) {
-			hasTie = true;
-		} else if (subtok.find('_') != string::npos) {
-			hasTie = true;
-		}
-		if (!hasTie) {
-			if (subtok.find(']') != string::npos) {
-				markNote(current, markQ);
-			}
-			return current;
-		} else {
-			return markNote(current, markQ);
-		}
-		break;
+	HumdrumFile& infile = *m_owner;
+	if (!infile[getStartLine()].isBarline()) {
+		return -1;
+	}
+	HumRegex hre;
+	if (hre.search(infile.token(getStartLine(), 0), "(\\d+)")) {
+		return hre.getMatchInt(1);
+	} else {
+		return -1;
 	}
-	return NULL;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::addMarker --
+// MeasureData::getQon -- return the start time class id of the measure.
 //
 
-void Tool_semitones::addMarker(HTp token) {
-	if (!m_nomarkQ) {
-		string contents = m_marker;
-		contents += token->getText();
-		token->setText(contents);
+std::string MeasureData::getQon(void) {
+	if (m_owner == NULL) {
+		return "";
+	}
+	if (getStartLine() < 0) {
+		return "";
+	}
+	HumdrumFile& infile = *m_owner;
+	HumNum ts =  infile[getStartLine()].getDurationFromStart();
+	string output = "qon" + to_string(ts.getNumerator());
+	if (ts.getDenominator() != 1) {
+		output += "-" + to_string(ts.getDenominator());
 	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::printTokens --
+// MeasureData::getQoff -- return the end time class id of the measure.
 //
 
-void Tool_semitones::printTokens(const string& value, int count) {
-	for (int i=0; i<count; i++) {
-		m_humdrum_text << value;
-		if (i < count - 1) {
-			m_humdrum_text << '\t';
-		}
+std::string MeasureData::getQoff(void) {
+	if (m_owner == NULL) {
+		return "";
+	}
+	if (getStopLine() < 0) {
+		return "";
+	}
+	HumdrumFile& infile = *m_owner;
+	HumNum ts =  infile[getStopLine()].getDurationFromStart();
+	string output = "qoff" + to_string(ts.getNumerator());
+	if (ts.getDenominator() != 1) {
+		output += "-" + to_string(ts.getDenominator());
 	}
+	return output;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_semitones::getTwelveToneIntervalString --
+// MeasureData::getStopTime -- return the stop time in
+//     quarter notes
 //
 
-string Tool_semitones::getTwelveToneIntervalString(HTp token) {
-	if (token->isNull()) {
-		return ".";
-	}
-	if (token->isRest()) {
-		if (m_midiQ) {
-			return "r";
-		} else {
-			return ".";
-		}
-	}
-	if (token->isUnpitched()) {
-		if (m_midiQ) {
-			return "R";
-		} else {
-			return ".";
-		}
-	}
-	if ((m_include.size() > 0) || (m_exclude.size() > 0)) {
-		int status = filterData(token);
-		if (status == 0) {
-			return ".";
-		} else if (status < 0) {
-			return "x"; // excluded note
-		}
-	}
-	string tok = token->getSubtoken(0);
-	if (tok.find(']') != string::npos) {
-		return ".";
+double MeasureData::getStopTime(void) {
+	if (m_owner == NULL) {
+		return 0.0;
 	}
-	if (tok.find('_') != string::npos) {
-		return ".";
+	if (getStopLine() < 0) {
+		return 0.0;
 	}
-	int value = Convert::kernToMidiNoteNumber(tok);
+	return (*m_owner)[getStopLine()].getDurationFromStart().getFloat();
+}
 
-	if (m_midiQ) {
-		string output;
-		if (m_pcQ) {
-			value = value % 12;
-		}
-		output = to_string(value);
-		return output;
-	}
 
-	string nexttok = getNextNoteAttack(token);
-	if (nexttok.empty()) {
-		return ".";
-	}
-	if (nexttok.find('r') != string::npos) {
-		// no interval since next note is a rest
-		return "r";
-	}
-	int value2 = Convert::kernToMidiNoteNumber(nexttok);
-	int interval =  value2 - value;
-	string output = to_string(interval);
-	return output;
+
+//////////////////////////////
+//
+// MeasureData::getDuration -- return the duration of the measure
+//     int quarter notes
+//
+
+double MeasureData::getDuration(void) {
+	return getStopTime() - getStartTime();
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_semitones::getNextNoteAttack -- Or rest.
+// MeasureData::getScoreDuration --
 //
 
-string Tool_semitones::getNextNoteAttack(HTp token) {
-	HTp current = token;
-	current = current->getNextToken();
-	string tok;
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isRest()) {
-			if (!m_norestsQ) {
-				return "r";
-			} else {
-				current = current->getNextToken();
-				continue;
-			}
-		}
-		if (current->isUnpitched()) {
-			return "R";
-		}
-		string tok = current->getSubtoken(0);
-		if (tok.find(']') != string::npos) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (tok.find('_') != string::npos) {
-			current = current->getNextToken();
-			continue;
-		}
-		return tok;
+double MeasureData::getScoreDuration(void) {
+	if (m_owner == NULL) {
+		return 0.0;
 	}
+	return m_owner->getScoreDuration().getFloat();
+}
 
-	if (!current) {
-		return "";
-	}
-	if (!current->isData()) {
-		return "";
-	}
-	// Some other strange problem.
-	return ".";
+
+
+//////////////////////////////
+//
+// MeasureData::clear --
+//
+
+void MeasureData::clear(void) {
+	m_owner = NULL;
+	m_owner       = NULL;
+	m_startline   = -1;
+	m_startline   = -1;
+	m_hist7pc.resize(7);
+	std::fill(m_hist7pc.begin(), m_hist7pc.end(), 0.0);
+	m_sum7pc      = 0.0;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::filterData -- select or deselect an interval based
-//    on regular expression pattern.  Return true if the note should
-//    be kept; otherwise, return false.
+// MeasureData::getHistogram7pc --
 //
 
-int Tool_semitones::filterData(HTp token) {
-	vector<HTp> toks = getTieGroup(token);
-	HumRegex hre;
-	if (!m_exclude.empty()) {
-		for (int i=0; i<(int)toks.size(); i++) {
-			if (hre.search(toks[i], m_exclude)) {
-				return -1;
-			}
-		}
-		return 1;
-	} else if (!m_include.empty()) {
-		for (int i=0; i<(int)toks.size(); i++) {
-			if (hre.search(toks[i], m_include)) {
-				return 1;
-			}
-		}
-		return 0;
-	}
-	return 0;
+std::vector<double>& MeasureData::getHistogram7pc(void) {
+	return m_hist7pc;
+}
+
+
+//////////////////////////////
+//
+// MeasureData::getSum7pc --
+//
+
+double MeasureData::getSum7pc(void) {
+	return m_sum7pc;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_semitones::getTieGroup --
+// MeasureData::generateNoteHistogram --
 //
 
-vector<HTp> Tool_semitones::getTieGroup(HTp token) {
-	vector<HTp> output;
-	if (!token) {
-		return output;
-	}
-	if (token->isNull()) {
-		return output;
+void MeasureData::generateNoteHistogram(void) {
+	m_hist7pc.resize(7);
+	std::fill(m_hist7pc.begin(), m_hist7pc.end(), 0.0);
+	m_sum7pc = 0;
+	if (m_owner == NULL) {
+		return;
 	}
-	if (!token->isData()) {
-		return output;
+	if (m_startline < 0) {
+		return;
 	}
-	output.push_back(token);
-	if (token->isRest()) {
-		return output;
+	if (m_stopline < 0) {
+		return;
 	}
-	string subtok = token->getSubtoken(0);
-	bool continues = hasTieContinue(subtok);
-	HTp current = token;
-	while (continues) {
-		current = getNextNote(current);
-		if (!current) {
-			break;
+
+	HumdrumFile& infile = *m_owner;
+	for (int i=m_startline; i<m_stopline; i++) {
+		if (!infile[i].isData()) {
+			continue;
 		}
-		string subtok = current->getSubtoken(0);
-		if (subtok.find(']') != string::npos) {
-			output.push_back(current);
-			break;
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (!token->isKern()) {
+				continue;
+			}
+			if (token->isNull()) {
+				continue;
+			}
+			if (token->isRest()) {
+				continue;
+			}
+			double duration = token->getDuration().getFloat();
+			int subtokcount = token->getSubtokenCount();
+			for (int k=0; k<subtokcount; k++) {
+				string subtok = token->getSubtoken(k);
+				int pc = Convert::kernToBase7PC(subtok);
+				if (pc < 0) {
+					continue;
+				}
+				m_hist7pc.at(pc) += duration;
+			}
 		}
-		continues = hasTieContinue(subtok);
 	}
-	return output;
+	m_sum7pc = 0.0;
+	for (int i=0; i<(int)m_hist7pc.size(); i++) {
+		m_sum7pc += m_hist7pc[i];
+	}
 }
 
 
+///////////////////////////////////////////////////////////////////////////
+
 
 //////////////////////////////
 //
-// Tool_semitones::hasTieContinue --
+// MeasureDataSet::MeasureDataSet --
 //
 
-bool Tool_semitones::hasTieContinue(const string& value) {
-	if (value.find('_') != string::npos) {
-		return true;
-	}
-	if (value.find('[') != string::npos) {
-		return true;
-	}
-	return false;
+MeasureDataSet::MeasureDataSet(void) {
+	m_data.reserve(1000);
 }
 
 
 
 //////////////////////////////
 //
-// getNextNote --
+// MeasureDataSet::MeasureDataSet --
 //
 
-HTp Tool_semitones::getNextNote(HTp token) {
-	HTp current = token->getNextToken();
-	while (current) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		break;
-	}
-	return current;
+MeasureDataSet::MeasureDataSet(HumdrumFile& infile) {
+	parse(infile);
 }
 
 
 
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_shed::Tool_shed -- Set the recognized options for the tool.
+// MeasureDataSet::~MeasureDataSet --
 //
 
-Tool_shed::Tool_shed(void) {
-	define("s|spine|spines=s",              "list of spines to process");
-	define("e|expression=s",                "regular expression");
-	define("E|exclusion-expression=s",      "regular expression to skip");
-	define("x|exclusive-interpretations=s", "apply only to spine types in list");
-	define("k|kern=b",                      "apply only to **kern data");
-	define("X=s",                           "defineable exclusive interpretation x");
-	define("Y=s",                           "defineable exclusive interpretation y");
-	define("Z=s",                           "defineable exclusive interpretation z");
+MeasureDataSet::~MeasureDataSet() {
+	clear();
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_shed::run -- Do the main work of the tool.
+// MeasureDataSet::clear --
 //
 
-bool Tool_shed::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+void MeasureDataSet::clear(void) {
+	for (int i=0; i<(int)m_data.size(); i++) {
+		delete m_data[i];
 	}
-	return status;
+	m_data.clear();
 }
 
 
-bool Tool_shed::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+
+//////////////////////////////
+//
+// MeasureDataSet::parse --
+//
+
+int MeasureDataSet::parse(HumdrumFile& infile) {
+	int lastbar = 0;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isBarline()) {
+			continue;
+		}
+		MeasureData* info = new MeasureData(infile, lastbar, i);
+		info->generateNoteHistogram();
+		m_data.push_back(info);
+		lastbar = i;
 	}
-	return status;
+	MeasureData* info = new MeasureData(infile, lastbar, infile.getLineCount() - 1);
+	m_data.push_back(info);
+	return 1;
 }
 
 
-bool Tool_shed::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
+
+//////////////////////////////
+//
+// MeasureDataSet::operator[] --
+//
+
+MeasureData& MeasureDataSet::operator[](int index) {
+	return *m_data[index];
 }
 
 
-bool Tool_shed::run(HumdrumFile& infile) {
-	initialize();
-	initializeSegment(infile);
-	if (m_options.empty()) {
-		cerr << "Error: -e option is required" << endl;
-		return false;
-	}
-	for (int i=0; i<(int)m_options.size(); i++) {
-		prepareSearch(i);
-		processFile(infile);
+
+//////////////////////////////
+//
+// MeasureDataSet::getScoreDuration --
+//
+
+double MeasureDataSet::getScoreDuration(void) {
+	if (m_data.empty()) {
+		return 0.0;
 	}
-	return true;
+	return m_data[0]->getScoreDuration();
+
 }
 
 
 
+///////////////////////////////////////////////////////////////////////////
+
 //////////////////////////////
 //
-// Tool_shed::prepareSearch --
+// MeasureComparison::MeasureComparison --
 //
 
-void Tool_shed::prepareSearch(int index) {
-	// deal with command-line options (seprately for each search):
-	m_exinterps.clear();
+MeasureComparison::MeasureComparison() {
+	// do nothing
+}
 
-	if (getBoolean("kern")) {
-		m_exinterps.push_back("**kern");
-	} else if (getBoolean("exclusive-interpretations")) {
-		vector<string> extra = addToExInterpList();
-		for (int i=0; i<(int)extra.size(); i++) {
-			m_exinterps.push_back(extra[i]);
-		}
-	}
 
-	m_search  = m_searches.at(index);
-	m_replace = m_replaces.at(index);
-	m_option  = m_options.at(index);
+MeasureComparison::MeasureComparison(MeasureData& data1, MeasureData& data2) {
+	compare(data1, data2);
+}
 
-	m_grepoptions = "";
-	if (m_option.find("i") != std::string::npos) {
-		m_grepoptions += "i";
-	}
-	if (m_option.find("g") != std::string::npos) {
-		m_grepoptions += "g";
-	}
 
-	if (m_option.find("X") != std::string::npos) {
-		if (m_xInterp != "") {
-			m_exinterps.push_back(m_xInterp);
-		}
-	}
-	if (m_option.find("Y") != std::string::npos) {
-		if (m_yInterp != "") {
-			m_exinterps.push_back(m_yInterp);
-		}
-	}
-	if (m_option.find("Z") != std::string::npos) {
-		if (m_zInterp != "") {
-			m_exinterps.push_back(m_zInterp);
-		}
-	}
+MeasureComparison::MeasureComparison(MeasureData* data1, MeasureData* data2) {
+	compare(data1, data2);
+}
 
-	m_data = true;             // process data
-	m_barline = false;         // process barline
-	m_exinterp = false;        // process exclusive interpretations
-	m_interpretation = false;  // process interpretations (other than exinterp
-	                           //     and spine manipulators).
 
-	if (m_option.find("I") != std::string::npos) {
-		m_interpretation = true;
-		m_data = false;
-	}
-	if (m_option.find("X") != std::string::npos) {
-		m_exinterp = true;
-		m_data = false;
-	}
-	if (m_option.find("B") != std::string::npos) {
-		m_barline = true;
-		m_data = false;
-	}
-	if (m_option.find("M") != std::string::npos) {
-		// measure is an alias for barline
-		m_barline = true;
-		m_data = false;
-	}
-	if (m_option.find("L") != std::string::npos) {
-		m_localcomment = true;
-		m_data = false;
-	}
-	if (m_option.find("G") != std::string::npos) {
-		m_globalcomment = true;
-		m_data = false;
-	}
-	if (m_option.find("K") != std::string::npos) {
-		m_referencekey = true;
-		m_data = false;
-	}
-	if (m_option.find("V") != std::string::npos) {
-		m_referencevalue = true;
-		m_data = false;
-	}
-	if (m_option.find("R") != std::string::npos) {
-		m_reference = true;
-		m_referencekey = false;
-		m_referencevalue = false;
-		m_data = false;
-	}
-	if (m_option.find("D") != std::string::npos) {
-		m_data = true;
-	}
 
+//////////////////////////////
+//
+// MeasureComparison::~MeasureComparison --
+//
+
+MeasureComparison::~MeasureComparison() {
+	clear();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// MeasureComparison::clear --
 //
 
-void Tool_shed::initialize(void) {
-	if (getBoolean("expression")) {
-		string value = getString("expression");
-		parseExpression(value);
-	}
-	m_exclusion = getString("exclusion-expression");
-
-	if (getBoolean("X")) {
-		m_xInterp = getExInterp(getString("X"));
-	}
-	if (getBoolean("Y")) {
-		m_yInterp = getExInterp(getString("Y"));
-	}
-	if (getBoolean("Z")) {
-		m_zInterp = getExInterp(getString("Z"));
-	}
+void MeasureComparison::clear(void) {
+	correlation7pc = 0.0;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::getExInterp --
+// MeasureComparison::compare --
 //
 
-string Tool_shed::getExInterp(const string& value) {
-	if (value == "") {
-		return "**";
+void MeasureComparison::compare(MeasureData& data1, MeasureData& data2) {
+	compare(&data1, &data2);
+}
+
+
+void MeasureComparison::compare(MeasureData* data1, MeasureData* data2) {
+	double sum1 = data1->getSum7pc();
+	double sum2 = data2->getSum7pc();
+	if ((sum1 == sum2) && (sum1 == 0.0)) {
+		correlation7pc = 1.0;
+		return;
 	}
-	if (value == "*") {
-		return "**";
+	if (sum1 == 0.0) {
+		correlation7pc = 0.0;
+		return;
 	}
-	if (value.compare(0, 2, "**") == 0) {
-		return value;
+	if (sum2 == 0.0) {
+		correlation7pc = 0.0;
+		return;
 	}
-	if (value.compare(0, 1, "*") == 0) {
-		return "*" + value;
+	correlation7pc = Convert::pearsonCorrelation(data1->getHistogram7pc(), data2->getHistogram7pc());
+	if (fabs(correlation7pc - 1.0) < 0.00000001) {
+		correlation7pc = 1.0;
 	}
-	return "**" + value;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::parseExpression --
-//     Form of string:
-//        s/search/replace/options; s/search2/replace2/options2
+// MeasureComparison::getCorrelation7pc --
 //
+
+double MeasureComparison::getCorrelation7pc(void) {
+	return correlation7pc;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::MeasureComparisonGrid --
 //
 
-void Tool_shed::parseExpression(const string& expression) {
-	int state = 0;
+MeasureComparisonGrid::MeasureComparisonGrid(void) {
+	// do nothing
+}
 
-	m_searches.clear();
-	m_replaces.clear();
-	m_options.clear();
 
-	char divchar = '/';
+MeasureComparisonGrid::MeasureComparisonGrid(MeasureDataSet& set1, MeasureDataSet& set2) {
+	analyze(set1, set2);
+}
 
-	for (int i=0; i<(int)expression.size(); i++) {
-		if (state == 0) {  // start of expression
-			if (isspace(expression[i])) {
-				continue;
-			} else if (expression[i] == 's') {
-				if (i >= (int)expression.size() - 1) {
-					cerr << "Error: spurious s at end of expression: "
-					     << expression << endl;
-					return;
-				} else {
-					divchar = expression[i+1];
-					i++;
-					state++;
-					m_searches.push_back("");
-				}
-			} else {
-				cerr << "Error at position " << i
-				     << " in expression: " << expression << endl;
-				return;
-			}
-		} else if (state == 1) { // search string
-			if (expression[i] == divchar) {
-				state++;
-				m_replaces.push_back("");
-				continue;
-			} if (expression[i] == '\\') {
-				if (i >= (int)expression.size() - 1) {
-					cerr << "Error: expression ends too soon: "
-					     << expression << endl;
-					return;
-				} else {
-					m_searches.back() += '\\';
-					m_searches.back() += expression[i+1];
-					i++;
-				}
-			} else {
-				m_searches.back() += expression[i];
-			}
-		} else if (state == 2) { // replace string
-			if (expression[i] == divchar) {
-				state++;
-				m_options.push_back("");
-				continue;
-			} if (expression[i] == '\\') {
-				if (i >= (int)expression.size() - 1) {
-					cerr << "Error: expression ends too soon: "
-					     << expression << endl;
-					return;
-				} else {
-					m_replaces.back() += '\\';
-					m_replaces.back() += expression[i+1];
-					i++;
-				}
-			} else {
-				m_replaces.back() += expression[i];
-			}
-		} else if (state == 3) { // regular expression options
-			if (expression[i] == ';') {
-				state++;
-			} else if (isspace(expression[i])) {
-				state++;
-			} else {
-				m_options.back() += expression[i];
-			}
-		}
-		if (state == 4) {
-			state = 0;
-		}
-	}
+
+MeasureComparisonGrid::MeasureComparisonGrid(MeasureDataSet* set1, MeasureDataSet* set2) {
+	analyze(set1, set2);
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::initializeSegment -- Recalculate variables for each Humdrum
-//      input segment.
+// MeasureComparisonGrid::~MeasureComparisonGrid --
 //
 
-void Tool_shed::initializeSegment(HumdrumFile& infile) {
-	m_spines.clear();
-	if (getBoolean("spines")) {
-		int maxtrack = infile.getMaxTrack();
-		Convert::makeBooleanTrackList(m_spines, getString("spines"), maxtrack);
-	}
+MeasureComparisonGrid::~MeasureComparisonGrid() {
+	// do nothing
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::addToExInterpList --
+// MeasureComparisonGrid::clear --
 //
 
-vector<string> Tool_shed::addToExInterpList(void) {
-	string elist = getString("exclusive-interpretations");
-	elist = Convert::trimWhiteSpace(elist);
-	HumRegex hre;
-	hre.replaceDestructive(elist, "", "^[,;\\s*]+");
-	hre.replaceDestructive(elist, "", "[,;\\s*]+$");
-	vector<string> pieces;
-	hre.split(pieces, elist, "[,;\\s*]+");
+void MeasureComparisonGrid::clear(void) {
+	m_grid.clear();
+}
 
-	vector<string> output;
-	for (int i=0; i<(int)pieces.size(); i++) {
-		if (pieces[i].empty()) {
-			continue;
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::analyze --
+//
+
+void MeasureComparisonGrid::analyze(MeasureDataSet* set1, MeasureDataSet* set2) {
+	analyze(*set1, *set2);
+}
+
+void MeasureComparisonGrid::analyze(MeasureDataSet& set1, MeasureDataSet& set2) {
+	m_grid.resize(set1.size());
+	for (int i=0; i<(int)m_grid.size(); i++) {
+		m_grid[i].resize(set2.size());
+	}
+	for (int i=0; i<(int)m_grid.size(); i++) {
+		for (int j=0; j<(int)m_grid[i].size(); j++) {
+			m_grid[i][j].compare(set1[i], set2[j]);
 		}
-		output.push_back("**" + pieces[i]);
 	}
-	return output;
+	m_set1 = &set1;
+	m_set2 = &set2;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::processFile --
+// MeasureComparisonGrid::printCorrelationGrid --
+//    default value: out = std::cout
 //
 
-void Tool_shed::processFile(HumdrumFile& infile) {
-	if (m_search == "") {
-		// nothing to do
-		return;
+ostream& MeasureComparisonGrid::printCorrelationGrid(ostream& out) {
+	for (int i=0; i<(int)m_grid.size(); i++) {
+		for (int j=0; j<(int)m_grid[i].size(); j++) {
+			double correl = m_grid[i][j].getCorrelation7pc();
+			if (correl > 0.0) {
+				out << int(correl * 100.0 + 0.5)/100.0;
+			} else {
+				out << -int(-correl * 100.0 + 0.5)/100.0;
+			}
+			if (j < (int)m_grid[i].size() - 1) {
+				out << '\t';
+			}
+		}
+		out << endl;
 	}
-	m_modified = false;
+	return out;
+}
 
-	if (m_interpretation) {
-		searchAndReplaceInterpretation(infile);
-	}
 
-	if (m_localcomment) {
-		searchAndReplaceLocalComment(infile);
-	}
 
-	if (m_globalcomment) {
-		searchAndReplaceGlobalComment(infile);
-	}
+//////////////////////////////
+//
+// MeasureComparisonGrid::printCorrelationDiagonal -- Assuming a square grid for now.
+//    default value: out = std::cout
+//
 
-	if (m_reference) {
-		searchAndReplaceReferenceRecords(infile);
+ostream& MeasureComparisonGrid::printCorrelationDiagonal(ostream& out) {
+	for (int i=0; i<(int)m_grid.size(); i++) {
+		for (int j=0; j<(int)m_grid[i].size(); j++) {
+			if (i != j) {
+				continue;
+			}
+			double correl = m_grid[i][j].getCorrelation7pc();
+			if (correl > 0.0) {
+				out << int(correl * 100.0 + 0.5)/100.0;
+			} else {
+				out << -int(-correl * 100.0 + 0.5)/100.0;
+			}
+			if (j < (int)m_grid[i].size() - 1) {
+				out << '\t';
+			}
+		}
+		out << endl;
 	}
+	return out;
+}
 
-	if (m_referencekey) {
-		searchAndReplaceReferenceKeys(infile);
-	}
 
-	if (m_referencevalue) {
-		searchAndReplaceReferenceValues(infile);
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getColorMapping --
+//
+
+void MeasureComparisonGrid::getColorMapping(double input, double& hue,
+		double& saturation, double& lightness) {
+	double maxhue = 0.75 * 360.0;
+	hue = input;
+	if (hue < 0.0) {
+		hue = 0.0;
+	}
+	hue = hue * hue;
+	if (hue != 1.0) {
+		hue *= 0.95;
 	}
 
-	if (m_exinterp) {
-		searchAndReplaceExinterp(infile);
+	hue = (1.0 - hue) * 360.0;
+	if (hue == 0.0) {
+		// avoid -0.0;
+		hue = 0.0;
 	}
 
-	if (m_barline) {
-		searchAndReplaceBarline(infile);
+	if (hue > maxhue) {
+		hue = maxhue;
+	}
+	if (hue < 0.0) {
+		hue = maxhue;
 	}
 
-	if (m_data) {
-		searchAndReplaceData(infile);
+	saturation = 100.0;
+	lightness = 50.0;
+
+	if (hue > 60) {
+		lightness = lightness - (hue-60) / (maxhue-60) * lightness / 1.5;
 	}
+}
 
-	if (m_modified) {
-		infile.createLinesFromTokens();
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getQoff1 -- return the end time class ID of the
+//     current grid cell (for the first piece being compared).
+//
+
+std::string MeasureComparisonGrid::getQoff1(int index) {
+	if (m_set1 == NULL) {
+		return "";
 	}
+	return (*m_set1)[index].getQoff();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceBarline --
+// MeasureComparisonGrid::getQoff2 -- return the end time class ID of the
+//     current grid cell (for the first piece being compared).
 //
 
-void Tool_shed::searchAndReplaceBarline(HumdrumFile& infile) {
-	string isearch;
-	if (m_search[0] == '^') {
-		isearch = "^=" + m_search.substr(1);
-	} else {
-		isearch = "^=.*" + m_search;
+std::string MeasureComparisonGrid::getQoff2(int index) {
+	if (m_set2 == NULL) {
+		return "";
 	}
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isBarline()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isNull()) {
-				// Don't mess with null interpretations
-				continue;
-			}
-			if (!isValid(token)) {
-				continue;
-			}
-			if (hre.search(token, isearch, m_grepoptions)) {
-				string text = token->getText().substr(1);
-				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
-				hre.replaceDestructive(text, "", "^=+");
-				text = "=" + text;
-				token->setText(text);
-				m_modified = true;
-			}
-		}
+	return (*m_set2)[index].getQoff();
+}
+
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getQon1 -- return the start time class ID of the
+//     current grid cell (for the first piece being compared).
+//
+
+string MeasureComparisonGrid::getQon1(int index) {
+	if (m_set1 == NULL) {
+		return "";
 	}
+	return (*m_set1)[index].getQon();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceInterpretation --
+// MeasureComparisonGrid::getQon2 -- return the start time class ID of the
+//     current grid cell (for the second piece being compared).
 //
 
-void Tool_shed::searchAndReplaceInterpretation(HumdrumFile& infile) {
-	string isearch;
-	if (m_search[0] == '^') {
-		isearch = "^\\*" + m_search.substr(1);
-	} else {
-		isearch = "^\\*.*" + m_search;
+string MeasureComparisonGrid::getQon2(int index) {
+	if (m_set2 == NULL) {
+		return "";
 	}
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		} else if (infile[i].isExclusiveInterpretation()) {
-			continue;
-		} else if (infile[i].isManipulator()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isNull()) {
-				// Don't mess with null interpretations
-				continue;
-			}
-			if (!isValid(token)) {
-				continue;
-			}
-			if (hre.search(token, isearch, m_grepoptions)) {
-				string text = token->getText().substr(1);
-				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
-				hre.replaceDestructive(text, "", "^\\*+");
-				text = "*" + text;
-				token->setText(text);
-				m_modified = true;
-			}
-		}
+	return (*m_set2)[index].getQon();
+}
+
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getMeasure1 -- return the measure of the
+//     current grid cell (for the first piece being compared).
+//
+
+int MeasureComparisonGrid::getMeasure1(int index) {
+	if (m_set1 == NULL) {
+		return 0.0;
 	}
+	return (*m_set1)[index].getMeasure();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceLocalComment --
+// MeasureComparisonGrid::getMeasure2 -- return the measure of the
+//     current grid cell (for the second piece being compared).
 //
 
-void Tool_shed::searchAndReplaceLocalComment(HumdrumFile& infile) {
-	string isearch;
-	if (m_search[0] == '^') {
-		isearch = "^!" + m_search.substr(1);
-	} else {
-		isearch = "^!.*" + m_search;
+int MeasureComparisonGrid::getMeasure2(int index) {
+	if (m_set2 == NULL) {
+		return 0.0;
 	}
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isLocalComment()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isNull()) {
-				// Don't mess with null interpretations
-				continue;
-			}
-			if (!isValid(token)) {
-				continue;
-			}
-			if (hre.search(token, isearch, m_grepoptions)) {
-				string text = token->getText().substr(1);
-				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
-				hre.replaceDestructive(text, "", "^!+");
-				text = "!" + text;
-				token->setText(text);
-				m_modified = true;
-			}
-		}
+	return (*m_set2)[index].getMeasure();
+}
+
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getStartTime1 -- return the start time of the
+//     measure at index position in the first compared score.
+//
+
+double MeasureComparisonGrid::getStartTime1(int index) {
+	if (m_set1 == NULL) {
+		return 0.0;
 	}
+	return (*m_set1)[index].getStartTime();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceGlobalComment --
+// MeasureComparisonGrid::getScoreDuration1 --
 //
 
-void Tool_shed::searchAndReplaceGlobalComment(HumdrumFile& infile) {
-	string isearch;
-	if (m_search[0] == '^') {
-		isearch = "^!!" + m_search.substr(1);
-	} else {
-		isearch = "^!!.*" + m_search;
+double MeasureComparisonGrid::getScoreDuration1(void) {
+	if (m_set1 == NULL) {
+		return 0.0;
 	}
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isGlobalComment()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (token->size() < 3) {
-			// Don't mess with null comments
-			continue;
-		}
-		if (hre.search(token, isearch, m_grepoptions)) {
-			string text = token->getText().substr(2);
-			hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
-			hre.replaceDestructive(text, "", "^!+");
-			text = "!!" + text;
-			token->setText(text);
-			m_modified = true;
-		}
+	return m_set1->getScoreDuration();
+}
+
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getStartTime2 --
+//
+
+double MeasureComparisonGrid::getStartTime2(int index) {
+	if (m_set2 == NULL) {
+		return 0.0;
 	}
+	return (*m_set2)[index].getStartTime();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceReferenceRecords --
+// MeasureComparisonGrid::getStopTime1 --
 //
 
-void Tool_shed::searchAndReplaceReferenceRecords(HumdrumFile& infile) {
-	string isearch;
-	if (m_search[0] == '^') {
-		isearch = "^!!!" + m_search.substr(1);
-	} else {
-		isearch = "^!!!.*" + m_search;
-	}
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isGlobalReference()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		if (hre.search(token, isearch, m_grepoptions)) {
-			string text = token->getText().substr(1);
-			hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
-			hre.replaceDestructive(text, "", "^!+");
-			text = "!!!" + text;
-			token->setText(text);
-			m_modified = true;
-		}
+double MeasureComparisonGrid::getStopTime1(int index) {
+	if (m_set1 == NULL) {
+		return 0.0;
 	}
+	return (*m_set1)[index].getStopTime();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceReferenceKeys --
+// MeasureComparisonGrid::getStopTime2 --
 //
 
-void Tool_shed::searchAndReplaceReferenceKeys(HumdrumFile& infile) {
-	string isearch = m_search;
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isGlobalReference()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		string key = infile[i].getReferenceKey();
-		if (hre.search(key, isearch, m_grepoptions)) {
-			hre.replaceDestructive(key, m_replace, m_search, m_grepoptions);
-			hre.replaceDestructive(key, "", "^!+");
-			hre.replaceDestructive(key, "", ":+$");
-			string value = infile[i].getReferenceValue();
-			string text = "!!!" + key + ": " + value;
-			token->setText(text);
-			m_modified = true;
-		}
+double MeasureComparisonGrid::getStopTime2(int index) {
+	if (m_set2 == NULL) {
+		return 0.0;
 	}
+	return (*m_set2)[index].getStopTime();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceReferenceValues --
+// MeasureComparisonGrid::getDuration1 --
 //
 
-void Tool_shed::searchAndReplaceReferenceValues(HumdrumFile& infile) {
-	string isearch = m_search;
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isGlobalReference()) {
-			continue;
-		}
-		HTp token = infile.token(i, 0);
-		string value = infile[i].getReferenceValue();
-		if (hre.search(value, isearch, m_grepoptions)) {
-			hre.replaceDestructive(value, m_replace, m_search, m_grepoptions);
-			hre.replaceDestructive(value, "", "^!+");
-			hre.replaceDestructive(value, "", ":+$");
-			string key = infile[i].getReferenceKey();
-			string text = "!!!" + key + ": " + value;
-			token->setText(text);
-			m_modified = true;
-		}
+double MeasureComparisonGrid::getDuration1(int index) {
+	if (m_set1 == NULL) {
+		return 0.0;
 	}
+	return (*m_set1)[index].getDuration();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceExinterp --
+// MeasureComparisonGrid::getDuration2 --
 //
 
-void Tool_shed::searchAndReplaceExinterp(HumdrumFile& infile) {
-	string isearch;
-	if (m_search[0] == '^') {
-		isearch = "^\\*\\*" + m_search.substr(1);
-	} else {
-		isearch = "^\\*\\*.*" + m_search;
+double MeasureComparisonGrid::getDuration2(int index) {
+	if (m_set2 == NULL) {
+		return 0.0;
 	}
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		} else if (!infile[i].isExclusiveInterpretation()) {
-			// assuming a single line for all exclusive interpretations
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isNull()) {
-				// Don't mess with null interpretations
-				continue;
-			}
-			if (!isValid(token)) {
-				continue;
-			}
-			if (hre.search(token, isearch, m_grepoptions)) {
-				string text = token->getText().substr(2);
-				hre.replaceDestructive(text, m_replace, m_search, m_grepoptions);
-				hre.replaceDestructive(text, "", "^\\*+");
-				text = "**" + text;
-				token->setText(text);
-				m_modified = true;
-			}
-		}
+	return (*m_set2)[index].getDuration();
+}
+
+
+
+//////////////////////////////
+//
+// MeasureComparisonGrid::getScoreDuration2 --
+//
+
+double MeasureComparisonGrid::getScoreDuration2(void) {
+	if (m_set2 == NULL) {
+		return 0.0;
 	}
+	return m_set2->getScoreDuration();
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::searchAndReplaceData --
+// MeasureComparisonGrid::printSvgGrid --
+//    default value: out = std::cout
 //
 
-void Tool_shed::searchAndReplaceData(HumdrumFile& infile) {
-	string dsearch = m_search;
+ostream& MeasureComparisonGrid::printSvgGrid(ostream& out) {
+	pugi::xml_document image;
+	auto declaration = image.prepend_child(pugi::node_declaration);
+	declaration.append_attribute("version") = "1.0";
+	declaration.append_attribute("encoding") = "UTF-8";
+	declaration.append_attribute("standalone") = "no";
 
-	HumRegex hre;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->isNull()) {
-				// Don't mess with null interpretations
-				continue;
-			}
-			if (!isValid(token)) {
-				continue;
-			}
-			if (hre.search(token, dsearch, m_grepoptions)) {
-				string text = token->getText();
-				hre.replaceDestructive(text, m_replace, dsearch, m_grepoptions);
-				if (text == "") {
-					text = ".";
-				}
-				token->setText(text);
-				m_modified = true;
-			}
+	auto svgnode = image.append_child("svg");
+	svgnode.append_attribute("version") = "1.1";
+	svgnode.append_attribute("xmlns") = "http://www.w3.org/2000/svg";
+	svgnode.append_attribute("xmlns:xlink") = "http://www.w3.org/1999/xlink";
+	svgnode.append_attribute("overflow") = "visible";
+	svgnode.append_attribute("viewBox") = "0 0 1000 1000";
+	svgnode.append_attribute("width") = "1000px";
+	svgnode.append_attribute("height") = "1000px";
+
+	auto grid = svgnode.append_child("g");
+	grid.append_attribute("id") = "grid";
+
+	double hue = 0.0;
+	double saturation = 100;
+	double lightness = 75;
+
+	pugi::xml_node crect;
+	double width;
+	double height;
+
+	stringstream ss;
+	stringstream css;
+	double x;
+	double y;
+
+	double imagewidth = 1000.0;
+	double imageheight = 1000.0;
+
+	double sdur1 = getScoreDuration1();
+	double sdur2 = getScoreDuration2();
+
+	for (int i=0; i<(int)m_grid.size(); i++) {
+		for (int j=0; j<(int)m_grid[i].size(); j++) {
+			width = getDuration2(j) / sdur2 * imagewidth;
+			height = getDuration1(i) / sdur1 * imageheight;
+
+			x = getStartTime2(j)/sdur2 * imageheight;
+			y = getStartTime1(i)/sdur1 * imagewidth;
+
+			getColorMapping(m_grid[i][j].getCorrelation7pc(), hue, saturation, lightness);
+			ss << "hsl(" << hue << "," << saturation << "%," << lightness << "%)";
+			crect = grid.append_child("rect");
+			crect.append_attribute("x") = to_string(x).c_str();
+			crect.append_attribute("y") = to_string(y).c_str();
+			crect.append_attribute("width") = to_string(width*0.99).c_str();
+			crect.append_attribute("height") = to_string(height*0.99).c_str();
+			crect.append_attribute("fill") = ss.str().c_str();
+			css << "Xm" << getMeasure1(i) << " Ym" << getMeasure2(j);
+			css << " X" << getQon1(i)     << " Y" << getQon2(j);
+			css << " X" << getQoff1(i)    << " Y" << getQoff2(j);
+			crect.append_attribute("class") = css.str().c_str();
+			ss.str("");
+			css.str("");
 		}
 	}
+
+	image.save(out);
+	return out;
 }
 
 
+///////////////////////////////////////////////////////////////////////////
+
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_shed::isValidDataType -- usar with -x and -k options.
+// Tool_simat::Tool_simat -- Set the recognized options for the tool.
 //
 
-bool Tool_shed::isValidDataType(HTp token) {
-	if (m_exinterps.empty()) {
-		return true;
+Tool_simat::Tool_simat(void) {
+	define("r|raw=b",      "output raw correlation matrix");
+	define("d|diagonal=b", "output diagonal of correlation matrix");
+}
+
+
+
+/////////////////////////////////
+//
+// Tool_simat::run -- Primary interfaces to the tool.
+//
+
+bool Tool_simat::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	if (infiles.getCount() == 1) {
+		status = run(infiles[0], infiles[0]);
+	} else if (infiles.getCount() > 1) {
+		status = run(infiles[0], infiles[1]);
+	} else {
+		status = false;
 	}
-	string datatype = token->getDataType();
-	for (int i=0; i<(int)m_exinterps.size(); i++) {
-		if (datatype == m_exinterps[i]) {
-			return true;
-		}
+	return status;
+}
+
+
+bool Tool_simat::run(const string& indata1, const string& indata2, ostream& out) {
+	HumdrumFile infile1(indata1);
+	HumdrumFile infile2;
+	bool status;
+	if (indata2.empty()) {
+		infile2.read(indata2);
+		status = run(infile1, infile2);
+	} else {
+		status = run(infile1, infile1);
 	}
-	return false;
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile1;
+		out << infile2;
+	}
+	return status;
 }
 
 
+bool Tool_simat::run(HumdrumFile& infile1, HumdrumFile& infile2, ostream& out) {
+	bool status;
+	if (infile2.getLineCount() == 0) {
+		status = run(infile1, infile1);
+	} else {
+		status = run(infile1, infile2);
+	}
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile1;
+		out << infile2;
+	}
+	return status;
+}
 
-//////////////////////////////
 //
-// Tool_shed::isValidSpine -- used with -s option.
+// In-place processing of file:
 //
 
-bool Tool_shed::isValidSpine(HTp token) {
-	if (m_spines.empty()) {
-		return true;
+bool Tool_simat::run(HumdrumFile& infile1, HumdrumFile& infile2) {
+	if (infile2.getLineCount() == 0) {
+		processFile(infile1, infile1);
+	} else {
+		processFile(infile1, infile2);
 	}
-	int track = token->getTrack();
-	return m_spines.at(track);
+
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_shed::isValid --
+// Tool_simat::processFile --
 //
 
-bool Tool_shed::isValid(HTp token) {
-	if (!m_exclusion.empty()) {
-		HumRegex hre;
-		if (hre.search(token, m_exclusion)) {
-			return false;
-		}
-	}
-	if (isValidDataType(token) && isValidSpine(token)) {
-		return true;
+void Tool_simat::processFile(HumdrumFile& infile1, HumdrumFile& infile2) {
+	m_data1.parse(infile1);
+	m_data2.parse(infile2);
+	m_grid.analyze(m_data1, m_data2);
+	if (getBoolean("raw")) {
+		m_grid.printCorrelationGrid(m_free_text);
+		suppressHumdrumFileOutput();
+	} else if (getBoolean("diagonal")) {
+		m_grid.printCorrelationDiagonal(m_free_text);
+		suppressHumdrumFileOutput();
+	} else {
+		m_grid.printSvgGrid(m_free_text);
+		suppressHumdrumFileOutput();
 	}
-	return false;
 }
 
 
@@ -123006,25 +126969,25 @@ bool Tool_shed::isValid(HTp token) {
 
 /////////////////////////////////
 //
-// Tool_sic::Tool_sic -- Set the recognized options for the tool.
+// Tool_slurcheck::Tool_slurcheck -- Set the recognized options for the tool.
 //
 
-Tool_sic::Tool_sic(void) {
-	define("s|substitution=b", "insert substitutions into music");
-	define("o|original=b",     "insert originals into music");
-	define("r|remove=b",       "remove sic layout tokens");
-	define("v|verbose=b",      "add verbose parameter");
-	define("q|quiet=b",        "remove verbose parameter");
+Tool_slurcheck::Tool_slurcheck(void) {
+	// add options here
+	define("l|list=b",     "list locations of unclosed slur endings");
+	define("c|count=b",    "count unclosed slur endings");
+	define("Z|no-zeros=b", "do not list files that have zero unclosed slurs in counts");
+	define("f|filename=b", "print filename for list and count options");
 }
 
 
 
 /////////////////////////////////
 //
-// Tool_sic::run -- Do the main work of the tool.
+// Tool_slurcheck::run -- Do the main work of the tool.
 //
 
-bool Tool_sic::run(HumdrumFileSet& infiles) {
+bool Tool_slurcheck::run(HumdrumFileSet& infiles) {
 	bool status = true;
 	for (int i=0; i<infiles.getCount(); i++) {
 		status &= run(infiles[i]);
@@ -123033,7 +126996,7 @@ bool Tool_sic::run(HumdrumFileSet& infiles) {
 }
 
 
-bool Tool_sic::run(const string& indata, ostream& out) {
+bool Tool_slurcheck::run(const string& indata, ostream& out) {
 	HumdrumFile infile(indata);
 	bool status = run(infile);
 	if (hasAnyText()) {
@@ -123045,7 +127008,7 @@ bool Tool_sic::run(const string& indata, ostream& out) {
 }
 
 
-bool Tool_sic::run(HumdrumFile& infile, ostream& out) {
+bool Tool_slurcheck::run(HumdrumFile& infile, ostream& out) {
 	bool status = run(infile);
 	if (hasAnyText()) {
 		getAllText(out);
@@ -123056,12 +127019,10 @@ bool Tool_sic::run(HumdrumFile& infile, ostream& out) {
 }
 
 
-bool Tool_sic::run(HumdrumFile& infile) {
+bool Tool_slurcheck::run(HumdrumFile& infile) {
 	initialize();
-	if (!(m_substituteQ || m_originalQ || m_removeQ || m_verboseQ || m_quietQ)) {
-		return true;
-	}
 	processFile(infile);
+	infile.createLinesFromTokens();
 	return true;
 }
 
@@ -123069,2045 +127030,2967 @@ bool Tool_sic::run(HumdrumFile& infile) {
 
 //////////////////////////////
 //
-// Tool_sic::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_slurcheck::initialize --
 //
 
-void Tool_sic::initialize(void) {
-	m_substituteQ = getBoolean("substitution");
-	m_originalQ   = getBoolean("original");
-	m_removeQ     = getBoolean("remove");
-	m_verboseQ    = getBoolean("verbose");
-	m_quietQ      = getBoolean("quiet");
+void Tool_slurcheck::initialize(void) {
 }
 
 
 
 //////////////////////////////
 //
-// Tool_sic::processFile --
+// Tool_slurcheck::processFile --
 //
 
-void Tool_sic::processFile(HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isLocalComment()) {
+void Tool_slurcheck::processFile(HumdrumFile& infile) {
+	infile.analyzeSlurs();
+	int opencount = 0;
+	int closecount = 0;
+	int listQ  = getBoolean("list");
+	int countQ = getBoolean("count");
+	int zeroQ = !getBoolean("no-zeros");
+	int filenameQ  = getBoolean("filename");
+	if (listQ || countQ) {
+		suppressHumdrumFileOutput();
+	}
+	for (int i=0; i<infile.getStrandCount(); i++) {
+		HTp stok = infile.getStrandStart(i);
+		if (!stok->isKern()) {
 			continue;
 		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile[i].token(j);
-			if (token->compare(0, 8, "!LO:SIC:") != 0) {
+		HTp etok = infile.getStrandEnd(i);
+		HTp tok = stok;
+		while (tok && (tok != etok)) {
+			if (!tok->isData()) {
+				tok = tok->getNextToken();
 				continue;
 			}
-			if (m_verboseQ) {
-				addVerboseParameter(token);
-			} else if (m_quietQ) {
-				removeVerboseParameter(token);
+			if (tok->isNull()) {
+				tok = tok->getNextToken();
+				continue;
 			}
-			if (m_removeQ) {
-				token->setText("!");
-				m_modifiedQ = true;
-			} else if (m_substituteQ) {
-				insertSubstitutionToken(token);
-			} else if (m_originalQ) {
-				insertOriginalToken(token);
+			string value = tok->getValue("auto", "hangingSlur");
+			if (value == "true") {
+				string side = tok->getValue("auto", "slurSide");
+				if (side == "start") {
+					opencount++;
+					if (listQ) {
+						if (filenameQ) {
+							m_free_text << infile.getFilename() << ":\t";
+						}
+						m_free_text << "UNCLOSED SLUR\tline:" << tok->getLineIndex()+1
+								<< "\tfield:" << tok->getFieldIndex()+1 << "\ttoken:" << tok << endl;
+					} else if (!countQ) {
+						string data = *tok;
+						data += "i";
+						tok->setText(data);
+					}
+				} else if (side == "stop") {
+					closecount++;
+					if (listQ) {
+						if (filenameQ) {
+							m_free_text << infile.getFilename() << ":\t";
+						}
+						m_free_text << "UNOPENED SLUR\tline:" << tok->getLineIndex()+1
+								<< "\tfield:" << tok->getFieldIndex()+1 << "\ttoken:" << tok << endl;
+					} else if (!countQ) {
+						string data = *tok;
+						data += "j";
+						tok->setText(data);
+					}
+				}
 			}
+			tok = tok->getNextToken();
 		}
 	}
-	if (m_modifiedQ) {
-		infile.createLinesFromTokens();
+
+	if (countQ) {
+		int sum = opencount + closecount;
+		if ((!zeroQ) && (sum == 0)) {
+			return;
+		}
+		if (filenameQ) {
+			m_free_text << infile.getFilename() << ":\t";
+		}
+		m_free_text << (opencount + closecount) << "\t(:" << opencount << "\t):" << closecount << endl;
 	}
-	m_humdrum_text << infile;
+
+	if (countQ || listQ) {
+		return;
+	}
+
+	if (opencount + closecount == 0) {
+		return;
+	}
+
+	if (opencount) {
+		infile.appendLine("!!!RDF**kern: i = marked note, color=\"hotpink\", text=\"extra(\"");
+	}
+
+	if (closecount) {
+		infile.appendLine("!!!RDF**kern: j = marked note, color=\"magenta\", text=\"extra)\"");
+	}
+
+	infile.createLinesFromTokens();
 }
 
 
 
-//////////////////////////////
+
+
+/////////////////////////////////
 //
-// Tool_sic::addVerboseParameter --
+// Tool_gridtest::Tool_spinetrace -- Set the recognized options for the tool.
+//
+
+Tool_spinetrace::Tool_spinetrace(void) {
+	define("a|append=b",  "append analysis to input data lines");
+	define("p|prepend=b", "prepend analysis to input data lines");
+}
+
+
+
+///////////////////////////////
 //
+// Tool_spinetrace::run -- Primary interfaces to the tool.
+//
+
+bool Tool_spinetrace::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
+}
+
+
+bool Tool_spinetrace::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
+}
+
+
+bool Tool_spinetrace::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	return status;
+}
 
-void Tool_sic::addVerboseParameter(HTp token) {
-	HumRegex hre;
-	string value = token->getText();
-	if (hre.search(value, "(:v:)|(:v$)")) {
-		return;
-	}
-	string newvalue = value + ":v";
-	token->setText(newvalue);
-	m_modifiedQ = true;
+
+bool Tool_spinetrace::run(HumdrumFile& infile) {
+   initialize(infile);
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_sic::removeVerboseParameter --
+// Tool_spinetrace::initialize --
 //
 
-void Tool_sic::removeVerboseParameter(HTp token) {
-	HumRegex hre;
-	string value = token->getText();
-	string newvalue = value;
-	hre.replaceDestructive(newvalue, ":", ":v:", "g");
-	hre.replaceDestructive(newvalue, "", ":v$", "");
-	if (value == newvalue) {
-		return;
-	}
-	token->setText(newvalue);
-	m_modifiedQ = true;
+void Tool_spinetrace::initialize(HumdrumFile& infile) {
+	// do nothing for now
 }
 
 
 
 //////////////////////////////
 //
-// Tool_sic::getTargetToken -- Get the token that the layout command
-//    applies to.
+// Tool_spinetrace::processFile --
 //
 
-HTp Tool_sic::getTargetToken(HTp stok) {
-	HTp current = stok->getNextToken();
-	while (current) {
-		if (current->isNull()) {
-			current = current->getNextToken();
+void Tool_spinetrace::processFile(HumdrumFile& infile) {
+	bool appendQ = getBoolean("append");
+	bool prependQ = getBoolean("prepend");
+
+	int linecount = infile.getLineCount();
+	for (int i=0; i<linecount; i++) {
+		if (!infile[i].hasSpines()) {
+			m_humdrum_text << infile[i] << endl;
 			continue;
 		}
-		if (current->isManipulator()) {
-			// Layout commands should not apply to manipulators nor be split
-			// from their associated token.
-			current = NULL;
-			break;
+		if (appendQ) {
+			m_humdrum_text << infile[i] << "\t";
 		}
-		if (current->isCommentLocal()) {
-			current = current->getNextToken();
-			continue;
+
+		if (!infile[i].isData()) {
+			if (infile[i].isInterpretation()) {
+				int fieldcount = infile[i].getFieldCount();
+				for (int j=0; j<fieldcount; j++) {
+					HTp token = infile.token(i, j);
+					if (token->compare(0, 2, "**") == 0) {
+						m_humdrum_text << "**spine";
+					} else {
+						m_humdrum_text << token;
+					}
+					if (j < fieldcount - 1) {
+						m_humdrum_text << "\t";
+					}
+				}
+			} else {
+				m_humdrum_text << infile[i];
+			}
+		} else {
+			int fieldcount = infile[i].getFieldCount();
+			for (int j=0; j<fieldcount; j++) {
+				m_humdrum_text << infile[i].token(j)->getSpineInfo();
+				if (j < fieldcount - 1) {
+					m_humdrum_text << '\t';
+				}
+			}
 		}
-		break;
-	}
-	if (!current) {
-		return NULL;
+
+		if (prependQ) {
+			m_humdrum_text << "\t" << infile[i];
+		}
+		m_humdrum_text << "\n";
 	}
-	return current;
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// Tool_sic::insertSubstitutionToken --
+// Tool_strophe::Tool_strophe -- Set the recognized options for the tool.
 //
 
-void Tool_sic::insertSubstitutionToken(HTp sictok) {
-	HTp target = getTargetToken(sictok);
-	if (!target) {
-		return;
-	}
-	HumRegex hre;
-	vector<string> pieces;
-	hre.split(pieces, *sictok, ":");
-	string tstring = target->getText();
-	string sstring;
-	for (int i=2; i<(int)pieces.size(); i++) {
-		if (pieces[i].compare(0, 2, "s=") == 0) {
-			sstring = pieces[i].substr(2);
-		}
-	}
-	if (sstring.empty()) {
-		return;
-	}
-	target->setText(sstring);
-	m_modifiedQ = true;
-	string newsic = "!LO:SIC";
-	for (int i=2; i<(int)pieces.size(); i++) {
-		if (pieces[i].compare(0, 2, "s=") == 0) {
-			newsic += ":o=" + tstring;
-		} else {
-			newsic += ":" + pieces[i];
-		}
-	}
-	sictok->setText(newsic);
-	m_modifiedQ = true;
+Tool_strophe::Tool_strophe(void) {
+	define("l|list=b",         "list all possible variants");
+	define("m=b",              "mark strophe music");
+	define("mark|marker=s:@",  "character to mark with");
+	define("c|color=s:red",    "character to mark with");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// Tool_sic::insertOriginalToken --
+// Tool_strophe::run -- Do the main work of the tool.
 //
 
-void Tool_sic::insertOriginalToken(HTp sictok) {
-	HTp target = getTargetToken(sictok);
-	if (!target) {
-		return;
-	}
-	HumRegex hre;
-	vector<string> pieces;
-	hre.split(pieces, *sictok, ":");
-	string tstring = target->getText();
-	string sstring;
-	for (int i=2; i<(int)pieces.size(); i++) {
-		if (pieces[i].compare(0, 2, "o=") == 0) {
-			sstring = pieces[i].substr(2);
-		}
-	}
-	if (sstring.empty()) {
-		return;
+bool Tool_strophe::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
-	target->setText(sstring);
-	m_modifiedQ = true;
-	string newsic = "!LO:SIC";
-	for (int i=2; i<(int)pieces.size(); i++) {
-		if (pieces[i].compare(0, 2, "o=") == 0) {
-			newsic += ":s=" + tstring;
-		} else {
-			newsic += ":" + pieces[i];
-		}
+	for (auto it = m_variants.begin(); it != m_variants.end(); ++it) {
+		m_free_text << *it << endl;
 	}
-	sictok->setText(newsic);
-	m_modifiedQ = true;
+	return status;
 }
 
 
-
-
-
-//////////////////////////////
-//
-// MeasureData::MeasureData --
-//
-
-MeasureData::MeasureData(void) {
-	m_hist7pc.resize(7);
-	std::fill(m_hist7pc.begin(), m_hist7pc.end(), 0.0);
+bool Tool_strophe::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else if (!m_listQ) {
+		out << infile;
+	}
+	return status;
 }
 
 
-MeasureData::MeasureData(HumdrumFile& infile, int startline, int stopline) {
-	setStartLine(startline);
-	setStopLine(stopline);
-	setOwner(infile);
+bool Tool_strophe::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else if (!m_listQ) {
+		out << infile;
+	}
+	return status;
 }
 
 
-MeasureData::MeasureData(HumdrumFile* infile, int startline, int stopline) {
-	setStartLine(startline);
-	setStopLine(stopline);
-	setOwner(infile);
+bool Tool_strophe::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::~MeasureData --
+// Tool_strophe::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-MeasureData::~MeasureData() {
-	clear();
+void Tool_strophe::initialize(void) {
+	m_listQ     = getBoolean("list");
+	m_markQ     = getBoolean("m");
+	m_marker    = getString("marker");
+	m_color     = getString("color");
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::setOwner --
+// Tool_strophe::processFile --
 //
 
-void MeasureData::setOwner(HumdrumFile* infile) {
-	m_owner = infile;
-}
-
-
-void MeasureData::setOwner(HumdrumFile& infile) {
-	m_owner = &infile;
+void Tool_strophe::processFile(HumdrumFile& infile) {
+	infile.analyzeStrophes();
+	if (m_listQ) {
+		displayStropheVariants(infile);
+	} else {
+		markWithColor(infile);
+	}
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::setStartLine --
+// Tool_strophe::markWithColor --  Maybe give different colors
+//     to different variants.  Currently only marking the primary
+//     strophe.
 //
 
-void MeasureData::setStartLine(int startline) {
-	m_startline = startline;
+void Tool_strophe::markWithColor(HumdrumFile& infile) {
+	int counter = 0;
+	for (int i=0; i<infile.getStropheCount(); i++) {
+		HTp strophestart = infile.getStropheStart(i);
+		HTp stropheend = infile.getStropheEnd(i);
+		counter += markStrophe(strophestart, stropheend);
+	}
+	if (counter) {
+		string rdf = "!!!RDF**kern: ";
+		rdf += m_marker;
+		rdf += " = marked note, strophe";
+		if (m_color != "red") {
+			rdf += ", color=\"";
+			rdf += m_color;
+			rdf += "\"";
+		}
+		infile.appendLine(rdf);
+		infile.createLinesFromTokens();
+	}
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::setStopLine --
+// Tool_strophe::markStrophe -- Returns the number of marked notes/rests.
 //
 
-void MeasureData::setStopLine(int stopline) {
-	m_stopline = stopline;
+int Tool_strophe::markStrophe(HTp strophestart, HTp stropheend) {
+	HTp current = strophestart;
+	int output = 0;
+	while (current && current != stropheend) {
+		if (current->isData() && !current->isNull()) {
+			// Think about multiple marking for individual notes in chords.
+			string value = current->getText();
+			value += m_marker;
+			current->setText(value);
+			output++;
+		}
+		current = current->getNextToken();
+	}
+	return output;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::getStartLine --
+// displayStropheVariants --
 //
 
-int MeasureData::getStartLine(void) {
-	return m_startline;
+void Tool_strophe::displayStropheVariants(HumdrumFile& infile) {
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (token->compare(0, 3, "*S/") != 0) {
+				continue;
+			}
+			string variant = token->substr(3);
+			m_variants.insert(variant);
+		}
+	}
 }
 
 
 
-//////////////////////////////
-//
-// MeasureData::getStopLine --
-//
 
-int MeasureData::getStopLine(void) {
-	return m_stopline;
-}
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// MeasureData::getStartTime -- return the start time in
-//     quarter notes
+// Tool_synco::Tool_synco -- Set the recognized options for the tool.
 //
 
-double MeasureData::getStartTime(void) {
-	if (m_owner == NULL) {
-		return 0.0;
-	}
-	if (getStartLine() < 0) {
-		return 0.0;
-	}
-	return (*m_owner)[getStartLine()].getDurationFromStart().getFloat();
+Tool_synco::Tool_synco(void) {
+	define("c|color=s:skyblue", "SVG color to highlight syncopation notes");
+	define("i|info=b",          "display only statistics info");
+	define("f|filename=b",      "add filename to statistics info");
+	define("a|all=b",           "average all statistics info");
 }
 
 
 
-//////////////////////////////
+/////////////////////////////////
 //
-// MeasureData::getMeasure -- return the measure number of the measure.
-//   return -1 if no measure number.
+// Tool_synco::run -- Do the main work of the tool.
 //
 
-int MeasureData::getMeasure(void) {
-	if (m_owner == NULL) {
-		return -1;
-	}
-	if (getStartLine() < 0) {
-		return -1;
-	}
-	HumdrumFile& infile = *m_owner;
-	if (!infile[getStartLine()].isBarline()) {
-		return -1;
+bool Tool_synco::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
 	}
-	HumRegex hre;
-	if (hre.search(infile.token(getStartLine(), 0), "(\\d+)")) {
-		return hre.getMatchInt(1);
-	} else {
-		return -1;
+	if (m_allQ) {
+		m_free_text << m_scountTotal << "\t";
+		m_free_text << m_notecountTotal << "\t";
+		double percent =  (double)m_scountTotal / m_notecountTotal;
+		percent = int(percent * 10000.0 + 0.5) / 100.0;
+		m_free_text << percent << "\t";
+		m_free_text << m_fileCount;
+		if (m_fileCount == 1) {
+			m_free_text << " file";
+		} else {
+			m_free_text << " files";
+		}
+		m_free_text << endl;
 	}
+	return status;
 }
 
 
 
-//////////////////////////////
-//
-// MeasureData::getQon -- return the start time class id of the measure.
-//
-
-std::string MeasureData::getQon(void) {
-	if (m_owner == NULL) {
-		return "";
-	}
-	if (getStartLine() < 0) {
-		return "";
-	}
-	HumdrumFile& infile = *m_owner;
-	HumNum ts =  infile[getStartLine()].getDurationFromStart();
-	string output = "qon" + to_string(ts.getNumerator());
-	if (ts.getDenominator() != 1) {
-		output += "-" + to_string(ts.getDenominator());
+bool Tool_synco::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	return output;
+	return status;
 }
 
 
-
-//////////////////////////////
-//
-// MeasureData::getQoff -- return the end time class id of the measure.
-//
-
-std::string MeasureData::getQoff(void) {
-	if (m_owner == NULL) {
-		return "";
-	}
-	if (getStopLine() < 0) {
-		return "";
-	}
-	HumdrumFile& infile = *m_owner;
-	HumNum ts =  infile[getStopLine()].getDurationFromStart();
-	string output = "qoff" + to_string(ts.getNumerator());
-	if (ts.getDenominator() != 1) {
-		output += "-" + to_string(ts.getDenominator());
+bool Tool_synco::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
 	}
-	return output;
+	return status;
 }
 
 
-
-//////////////////////////////
-//
-// MeasureData::getStopTime -- return the stop time in
-//     quarter notes
-//
-
-double MeasureData::getStopTime(void) {
-	if (m_owner == NULL) {
-		return 0.0;
+bool Tool_synco::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	if (m_hasSyncoQ && !m_infoQ) {
+		infile.createLinesFromTokens();
+		m_humdrum_text << infile;
+		m_humdrum_text << "!!!RDF**kern: | = marked note, color=" << m_color << endl;
 	}
-	if (getStopLine() < 0) {
-		return 0.0;
+	double notecount = infile.getNoteCount();
+	double density = m_scount / (double)notecount;
+	double percent =  int(density * 10000.0 + 0.5) / 100.0;
+	if (m_infoQ) {
+		m_free_text << m_scount << "\t" << notecount << "\t" << percent << "%";
+		if (m_fileQ) {
+			m_free_text << "\t" << infile.getFilename();
+		}
+		m_free_text << endl;
+
+		m_scountTotal    += m_scount;
+		m_notecountTotal += notecount;
+		m_fileCount++;
+	} else {
+		m_humdrum_text << "!!!syncopated_notes: " << m_scount << endl;
+		m_humdrum_text << "!!!total_notes: " << notecount << endl;
+		m_humdrum_text << "!!!syncopated_density: " << percent << "%" << endl;
 	}
-	return (*m_owner)[getStopLine()].getDurationFromStart().getFloat();
+
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::getDuration -- return the duration of the measure
-//     int quarter notes
+// Tool_synco::initialize --  Initializations that only have to be done once
+//    for all HumdrumFile segments.
 //
 
-double MeasureData::getDuration(void) {
-	return getStopTime() - getStartTime();
+void Tool_synco::initialize(void) {
+	m_infoQ = getBoolean("info");
+	m_fileQ = getBoolean("filename");
+	m_allQ  = getBoolean("all");
+	m_color = getString("color");
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::getScoreDuration --
+// Tool_synco::processFile --
 //
 
-double MeasureData::getScoreDuration(void) {
-	if (m_owner == NULL) {
-		return 0.0;
+void Tool_synco::processFile(HumdrumFile& infile) {
+	int scount = infile.getStrandCount();
+	m_scount = 0;
+	for (int i=0; i<scount; i++) {
+		HTp stok = infile.getStrandStart(i);
+		if (!stok->isKern()) {
+			continue;
+		}
+		HTp etok = infile.getStrandEnd(i);
+		processStrand(stok, etok);
 	}
-	return m_owner->getScoreDuration().getFloat();
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::clear --
+// Tool_synco::processStrand --
 //
 
-void MeasureData::clear(void) {
-	m_owner = NULL;
-	m_owner       = NULL;
-	m_startline   = -1;
-	m_startline   = -1;
-	m_hist7pc.resize(7);
-	std::fill(m_hist7pc.begin(), m_hist7pc.end(), 0.0);
-	m_sum7pc      = 0.0;
+void Tool_synco::processStrand(HTp stok, HTp etok) {
+	HTp current = stok;
+	while (current && (current != etok)) {
+		if (!current->isData()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isNull()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isRest()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (current->isSecondaryTiedNote()) {
+			current = current->getNextToken();
+			continue;
+		}
+		if (isSyncopated(current)) {
+			m_hasSyncoQ = true;
+			m_scount++;
+			markNote(current);
+		}
+		current = current->getNextToken();
+	}
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::getHistogram7pc --
+// Tool_synco::isSyncopated --
 //
 
-std::vector<double>& MeasureData::getHistogram7pc(void) {
-	return m_hist7pc;
+bool Tool_synco::isSyncopated(HTp token) {
+	double metlev   = getMetricLevel(token);
+	HumNum duration = token->getTiedDuration();
+	double logDur   = log2(duration.getFloat());
+	if (metlev == 2) {
+		return false;
+	}
+	if (logDur > metlev) {
+		return true;
+	} else {
+		return false;
+	}
 }
 
 
+
 //////////////////////////////
 //
-// MeasureData::getSum7pc --
+// Tool_synco::getMetricLevel -- Assuming whole-note beats for now.
 //
 
-double MeasureData::getSum7pc(void) {
-	return m_sum7pc;
+double Tool_synco::getMetricLevel(HTp token) {
+	HumNum durbar = token->getDurationFromBarline();
+	if (!durbar.isInteger()) {
+		return -1.0;
+	}
+	if (durbar.getNumerator() % 4 == 0) {
+		return 2.0;
+	}
+	if (durbar.getNumerator() % 2 == 0) {
+		return 1.0;
+	}
+	return 0.0;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureData::generateNoteHistogram --
+// Tool_synco::markNote -- Currently ignoring chords.
 //
 
-void MeasureData::generateNoteHistogram(void) {
-	m_hist7pc.resize(7);
-	std::fill(m_hist7pc.begin(), m_hist7pc.end(), 0.0);
-	m_sum7pc = 0;
-	if (m_owner == NULL) {
-		return;
-	}
-	if (m_startline < 0) {
-		return;
-	}
-	if (m_stopline < 0) {
-		return;
-	}
-
-	HumdrumFile& infile = *m_owner;
-	for (int i=m_startline; i<m_stopline; i++) {
-		if (!infile[i].isData()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (!token->isKern()) {
+void Tool_synco::markNote(HTp token) {
+	token->setText(token->getText() + "|");
+	if ((token->find('[') != string::npos) || (token->find('_') != string::npos)) {
+		HTp current = token->getNextToken();
+		while (current) {
+			if (!current->isData()) {
+				current = current->getNextToken();
 				continue;
 			}
-			if (token->isNull()) {
+			if (current->isNull()) {
+				current = current->getNextToken();
 				continue;
 			}
-			if (token->isRest()) {
-				continue;
+			if (current->isRest()) {
+				break;
 			}
-			double duration = token->getDuration().getFloat();
-			int subtokcount = token->getSubtokenCount();
-			for (int k=0; k<subtokcount; k++) {
-				string subtok = token->getSubtoken(k);
-				int pc = Convert::kernToBase7PC(subtok);
-				if (pc < 0) {
-					continue;
-				}
-				m_hist7pc.at(pc) += duration;
+			if (current->find("_") != string::npos) {
+				current->setText(current->getText() + "|");
+			} else if (current->find("]") != string::npos) {
+				current->setText(current->getText() + "|");
+				break;
 			}
+			current = current->getNextToken();
 		}
 	}
-	m_sum7pc = 0.0;
-	for (int i=0; i<(int)m_hist7pc.size(); i++) {
-		m_sum7pc += m_hist7pc[i];
-	}
 }
 
 
-///////////////////////////////////////////////////////////////////////////
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// MeasureDataSet::MeasureDataSet --
+// Tool_gridtest::Tool_tabber -- Set the recognized options for the tool.
 //
 
-MeasureDataSet::MeasureDataSet(void) {
-	m_data.reserve(1000);
+Tool_tabber::Tool_tabber(void) {
+	// do nothing for now.
+	define("r|remove=b",    "remove any extra tabs");
 }
 
 
 
-//////////////////////////////
+///////////////////////////////
 //
-// MeasureDataSet::MeasureDataSet --
+// Tool_tabber::run -- Primary interfaces to the tool.
 //
 
-MeasureDataSet::MeasureDataSet(HumdrumFile& infile) {
-	parse(infile);
+bool Tool_tabber::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
 }
 
 
-
-//////////////////////////////
-//
-// MeasureDataSet::~MeasureDataSet --
-//
-
-MeasureDataSet::~MeasureDataSet() {
-	clear();
+bool Tool_tabber::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	return run(infile, out);
 }
 
 
+bool Tool_tabber::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	out << m_free_text.str();
+	return status;
+}
 
-//////////////////////////////
-//
-// MeasureDataSet::clear --
-//
 
-void MeasureDataSet::clear(void) {
-	for (int i=0; i<(int)m_data.size(); i++) {
-		delete m_data[i];
-	}
-	m_data.clear();
+bool Tool_tabber::run(HumdrumFile& infile) {
+   initialize(infile);
+	processFile(infile);
+	return true;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureDataSet::parse --
+// Tool_tabber::initialize --
 //
 
-int MeasureDataSet::parse(HumdrumFile& infile) {
-	int lastbar = 0;
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isBarline()) {
-			continue;
-		}
-		MeasureData* info = new MeasureData(infile, lastbar, i);
-		info->generateNoteHistogram();
-		m_data.push_back(info);
-		lastbar = i;
-	}
-	MeasureData* info = new MeasureData(infile, lastbar, infile.getLineCount() - 1);
-	m_data.push_back(info);
-	return 1;
+void Tool_tabber::initialize(HumdrumFile& infile) {
+	// do nothing for now
 }
 
 
 
 //////////////////////////////
 //
-// MeasureDataSet::operator[] --
+// Tool_tabber::processFile --
 //
 
-MeasureData& MeasureDataSet::operator[](int index) {
-	return *m_data[index];
+void Tool_tabber::processFile(HumdrumFile& infile) {
+	if (getBoolean("remove")) {
+		infile.removeExtraTabs();
+	} else {
+		infile.addExtraTabs();
+	}
+	infile.createLinesFromTokens();
 }
 
 
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// MeasureDataSet::getScoreDuration --
+// Tool_tandeminfo::Tool_tandeminfo -- Set the recognized options for the tool.
 //
 
-double MeasureDataSet::getScoreDuration(void) {
-	if (m_data.empty()) {
-		return 0.0;
-	}
-	return m_data[0]->getScoreDuration();
+Tool_tandeminfo::Tool_tandeminfo(void) {
 
-}
+	define("c|count=b",                               "show only unique list of interpretations with counts");
+	define("D|no-description|M|no-meaning=b",         "do not include descriptions of tandem interpretations in output");
+	define("f|filename=b",                            "show filename");
+	define("h|header-only=b",                         "only process interpretations before first data line");
+	define("H|body-only=b",                           "only process interpretations after first data line");
+	define("l|location=b",                            "show location of interpretation in file (row, column)");
+	define("n|sort-by-count=b",                       "sort entries by unique counts from low to high (when -c is used)");
+	define("N|sort-by-reverse-count=b",               "sort entries by unique counts from high to low (when -c is used)");
+	define("s|sort=b",                                "sort entries alphabetically by tandem interpretation");
+	define("t|table=b",                               "embed analysis withing input data");
+	define("u|unknown-tandem-interpretations-only=b", "only list unknown interpretations");
+	define("x|exclusive-interpretations=b",           "show exclusive interpretation context");
+	define("z|zero-indexed-locations=b",              "locations are 0-indexed");
 
+	define("close=b",                                 "close <details> by default in HTML output");
+	define("humdrum|hmd=b",                           "textual output formatted with Humdrum syntax");
 
+	m_entries.reserve(1000);
+}
 
-///////////////////////////////////////////////////////////////////////////
 
-//////////////////////////////
+
+/////////////////////////////////
 //
-// MeasureComparison::MeasureComparison --
+// Tool_tandeminfo::run -- Do the main work of the tool.
 //
 
-MeasureComparison::MeasureComparison() {
-	// do nothing
+bool Tool_tandeminfo::run(HumdrumFileSet& infiles) {
+	bool status = true;
+	for (int i=0; i<infiles.getCount(); i++) {
+		status &= run(infiles[i]);
+	}
+	return status;
 }
 
 
-MeasureComparison::MeasureComparison(MeasureData& data1, MeasureData& data2) {
-	compare(data1, data2);
+bool Tool_tandeminfo::run(const string& indata, ostream& out) {
+	HumdrumFile infile(indata);
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
 }
 
 
-MeasureComparison::MeasureComparison(MeasureData* data1, MeasureData* data2) {
-	compare(data1, data2);
+bool Tool_tandeminfo::run(HumdrumFile& infile, ostream& out) {
+	bool status = run(infile);
+	if (hasAnyText()) {
+		getAllText(out);
+	} else {
+		out << infile;
+	}
+	return status;
 }
 
 
+bool Tool_tandeminfo::run(HumdrumFile& infile) {
+	initialize();
+	processFile(infile);
+	return true;
+}
+
 
 //////////////////////////////
 //
-// MeasureComparison::~MeasureComparison --
+// Tool_tandeminfo::initialize --
 //
 
-MeasureComparison::~MeasureComparison() {
-	clear();
-}
-
+void Tool_tandeminfo::initialize(void) {
+	m_exclusiveQ   = getBoolean("exclusive-interpretations");
+	m_unknownQ     = getBoolean("unknown-tandem-interpretations-only");
+	m_filenameQ    = getBoolean("filename");
+	m_locationQ    = getBoolean("location");
+	m_countQ       = getBoolean("count");
+	m_tableQ       = getBoolean("table");
+	m_zeroQ        = getBoolean("zero-indexed-locations");
+	m_sortQ        = getBoolean("sort");
+	m_headerOnlyQ  = getBoolean("header-only");
+	m_bodyOnlyQ    = getBoolean("body-only");
+	m_closeQ       = getBoolean("close");
+	m_humdrumQ     = getBoolean("humdrum");
+	m_descriptionQ = !getBoolean("no-description");
+	m_sortByCountQ = getBoolean("sort-by-count");
+	m_sortByReverseCountQ = getBoolean("sort-by-reverse-count");
 
+	if (m_headerOnlyQ && m_bodyOnlyQ) {
+		m_headerOnlyQ = 0;
+		m_bodyOnlyQ   = 0;
+	}
 
-//////////////////////////////
-//
-// MeasureComparison::clear --
-//
+	if (m_countQ && m_locationQ) {
+		m_locationQ = false;
+	}
 
-void MeasureComparison::clear(void) {
-	correlation7pc = 0.0;
+	// table option turned on by default for web interfaces:
+	#ifndef __EMSCRIPTEN__
+		m_tableQ       = getBoolean("table");
+	#else
+		m_tableQ       = !getBoolean("table");
+	#endif
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparison::compare --
+// Tool_tandeminfo::processFile --
 //
 
-void MeasureComparison::compare(MeasureData& data1, MeasureData& data2) {
-	compare(&data1, &data2);
-}
-
+void Tool_tandeminfo::processFile(HumdrumFile& infile) {
+	m_entries.clear();
+	m_count.clear();
 
-void MeasureComparison::compare(MeasureData* data1, MeasureData* data2) {
-	double sum1 = data1->getSum7pc();
-	double sum2 = data2->getSum7pc();
-	if ((sum1 == sum2) && (sum1 == 0.0)) {
-		correlation7pc = 1.0;
-		return;
-	}
-	if (sum1 == 0.0) {
-		correlation7pc = 0.0;
-		return;
-	}
-	if (sum2 == 0.0) {
-		correlation7pc = 0.0;
-		return;
-	}
-	correlation7pc = Convert::pearsonCorrelation(data1->getHistogram7pc(), data2->getHistogram7pc());
-	if (fabs(correlation7pc - 1.0) < 0.00000001) {
-		correlation7pc = 1.0;
+	bool foundDataQ = false;
+	for (int i=0; i<infile.getLineCount(); i++) {
+		if (infile[i].isManipulator()) {
+			continue;
+		}
+		if (infile[i].isData()) {
+			foundDataQ = true;
+		}
+		if (!infile[i].isInterpretation()) {
+			continue;
+		}
+		if (foundDataQ && m_headerOnlyQ) {
+			break;
+		}
+		if (!foundDataQ && m_bodyOnlyQ) {
+			continue;
+		}
+		for (int j=0; j<infile[i].getFieldCount(); j++) {
+			HTp token = infile.token(i, j);
+			if (*token == "*") {
+				continue;
+			}
+			string description;
+			if (m_descriptionQ || m_unknownQ) {
+				description = getDescription(token);
+				if (m_unknownQ) {
+					HumRegex hre;
+					if (!hre.search(description, m_unknown)) {
+						continue;
+					}
+				}
+			}
+			m_entries.resize(m_entries.size() + 1);
+			m_entries.back().token = token;
+			m_entries.back().description = description;
+		}
 	}
+
+	printEntries(infile);
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparison::getCorrelation7pc --
+// Tool_tandeminfo::printEntries --
 //
 
-double MeasureComparison::getCorrelation7pc(void) {
-	return correlation7pc;
-}
-
-//////////////////////////////////////////////////////////////////////////
+void Tool_tandeminfo::printEntries(HumdrumFile& infile) {
+	if (m_sortQ) {
+		sort(m_entries.begin(), m_entries.end(), [](const Entry &a, const Entry &b) {
+			string aa = a.token->getText();
+			string bb = b.token->getText();
+			std::transform(aa.begin(), aa.end(), aa.begin(), ::tolower);
+			std::transform(bb.begin(), bb.end(), bb.begin(), ::tolower);
+			return (aa < bb);
+		});
+	}
+	if (m_countQ) {
+		doCountAnalysis();
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::MeasureComparisonGrid --
-//
+	if (m_sortByCountQ) {
 
-MeasureComparisonGrid::MeasureComparisonGrid(void) {
-	// do nothing
-}
+		sort(m_entries.begin(), m_entries.end(), [](const Entry &a, const Entry &b) {
+			int anum = a.count;
+			int bnum = b.count;
+			if (anum != bnum) {
+				return anum < bnum;
+			}
+			string aa = a.token->getText();
+			string bb = b.token->getText();
+			std::transform(aa.begin(), aa.end(), aa.begin(), ::tolower);
+			std::transform(bb.begin(), bb.end(), bb.begin(), ::tolower);
+			return (aa < bb);
+		});
 
+	} else if (m_sortByReverseCountQ) {
 
-MeasureComparisonGrid::MeasureComparisonGrid(MeasureDataSet& set1, MeasureDataSet& set2) {
-	analyze(set1, set2);
-}
+		sort(m_entries.begin(), m_entries.end(), [](const Entry &a, const Entry &b) {
+			int anum = a.count;
+			int bnum = b.count;
+			if (anum != bnum) {
+				return anum > bnum;
+			}
+			string aa = a.token->getText();
+			string bb = b.token->getText();
+			std::transform(aa.begin(), aa.end(), aa.begin(), ::tolower);
+			std::transform(bb.begin(), bb.end(), bb.begin(), ::tolower);
+			return (aa < bb);
+		});
 
+	}
 
-MeasureComparisonGrid::MeasureComparisonGrid(MeasureDataSet* set1, MeasureDataSet* set2) {
-	analyze(set1, set2);
+	if (m_tableQ) {
+		printEntriesHtml(infile);
+	} else {
+		printEntriesText(infile);
+	}
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::~MeasureComparisonGrid --
+// Tool_tandeminfo::doCountAnalysis --
 //
 
-MeasureComparisonGrid::~MeasureComparisonGrid() {
-	// do nothing
+void Tool_tandeminfo::doCountAnalysis(void) {
+	m_count.clear();
+	for (int i=0; i<(int)m_entries.size(); i++) {
+		m_count[m_entries[i].token->getText()] += 1;
+	}
+
+	// store counts in entries:
+	for (int i=0; i<(int)m_entries.size(); i++) {
+		m_entries[i].count = m_count[m_entries[i].token->getText()];
+	}
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::clear --
+// Tool_tandeminfo::printEntiesHtml -- Print as embedded HTML code at end of
+//    input score.
 //
 
-void MeasureComparisonGrid::clear(void) {
-	m_grid.clear();
-}
+void Tool_tandeminfo::printEntriesHtml(HumdrumFile& infile) {
+	map<string, bool> processed;  // used for -c option
 
+	m_humdrum_text << infile;
 
+	m_humdrum_text << "!!@@BEGIN: PREHTML" << endl;
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::analyze --
-//
+	m_humdrum_text << "!!@SCRIPT:" << endl;
+	m_humdrum_text << "!!function gotoEditorCoordinate(row, col) {" << endl;
+	m_humdrum_text << "!!   if ((typeof EDITOR == 'undefined') || !EDITOR) {" << endl;
+	m_humdrum_text << "!!      return;" << endl;
+	m_humdrum_text << "!!   }" << endl;
+	m_humdrum_text << "!!   gotoLineFieldInEditor(row, col);" << endl;
+	m_humdrum_text << "!!}" << endl;
+	m_humdrum_text << "!!@CONTENT:" << endl;
+
+	m_humdrum_text << "!!<style>" << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo { border: 1px solid black; border-collapse: collapse; max-width: 98%; width:98%; }" << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo th, .PREHTML table.tandeminfo td { vertical-align: top; padding-right: 10px; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo tr:hover td { background-color: #eee; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo th.tandem, .PREHTML table.tandeminfo td.tandem { white-space: nowrap; padding-right: 30px; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo .tandem { font-family:\"Courier New\", Courier, monospace; }" << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo td.exclusive { font-family:\"Courier New\", Courier, monospace; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo th.location, .PREHTML table.tandeminfo td.location { white-space: nowrap; padding-right: 30px; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo th.count { padding-left: 20px; padding-right: 10px; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo td.count { text-align: right; padding-right: 30px; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo th.exclusive, .PREHTML table.tandeminfo td.exclusive { padding-right: 30px; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo tr th:last-child, .PREHTML table.tandeminfo tr td:last-child { width:100%; padding-right: 0; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo tr th:last-child, .PREHTML table.tandeminfo tr td:last-child { width:100%; padding-right: 0; } " << endl;
+	m_humdrum_text << "!!.PREHTML table.tandeminfo tr th, .PREHTML table.tandeminfo tr td { padding:1px; padding-left:3px; } " << endl;
+	m_humdrum_text << "!!.PREHTML span.unknown { color:crimson; font-weight:bold; }" << endl;
+	m_humdrum_text << "!!.PREHTML td.squeeze { letter-spacing:-0.5px; }" << endl;
+
+	m_humdrum_text << "!!.PREHTML details { position: relative; padding-left: 20px; }" << endl;
+	m_humdrum_text << "!!.PREHTML summary { font-size: 1.5rem; cursor: pointer; list-style: none; }" << endl;
+	m_humdrum_text << "!!.PREHTML summary::before { content: '▶'; display: inline-block; width: 2em; margin-left: -1.5em; text-align: center; }" << endl;
+	m_humdrum_text << "!!.PREHTML details[open] summary::before { content: '▼'; }" << endl;
+
+	m_humdrum_text << "!!</style>" << endl;
+
+	m_humdrum_text << "!!<details class='tandeminfo' ";
+	if (!m_closeQ) {
+		m_humdrum_text << "open";
+	}
+	m_humdrum_text << ">" << endl;;
+	m_humdrum_text << "!!<summary class='tandeminfo'>Tandem interpretation information</summary>" << endl;;
+	if (!m_entries.empty()) {
+		m_humdrum_text << "!!<table class='tandeminfo'>" << endl;
+
+		// print table header
+		m_humdrum_text << "!!<tr>" << endl;
+		if (m_locationQ) {
+			m_humdrum_text << "!!<th class='location'>Location</th>" << endl;
+		} else if (m_countQ) {
+			m_humdrum_text << "!!<th class='count'>Count</th>" << endl;
+		}
+		if (m_exclusiveQ) {
+			m_humdrum_text << "!!<th class='exclusive'>Exclusive</th>" << endl;
+		}
+		m_humdrum_text << "!!<th class='tandem' >Tandem</th>" << endl;
+		m_humdrum_text << "!!<th class='description'>Description</th>" << endl;
+		m_humdrum_text << "!!</tr>" << endl;
+
+		// print table entries
+		for (int i=0; i<(int)m_entries.size(); i++) {
+			HTp token = m_entries[i].token;
+			if (m_countQ && processed[token->getText()]) {
+				continue;
+			}
+			processed[token->getText()] = true;
+			m_humdrum_text << "!!<tr" << " onclick='gotoEditorCoordinate(" << token->getLineNumber() << ", " << token->getFieldNumber() << ")'>" << endl;
+
+			if (m_locationQ) {
+				m_humdrum_text << "!!<td class='location'>" << endl;
+				m_humdrum_text << "!!(" << token->getLineNumber() << ", " << token->getFieldNumber() << ")" << endl;
+				m_humdrum_text << "!!</td>" << endl;
+			} else if (m_countQ) {
+				m_humdrum_text << "!!<td class='count'>";
+				m_humdrum_text << m_count[token->getText()];
+				m_humdrum_text << "</td>" << endl;
+			}
+
+			if (m_exclusiveQ) {
+				m_humdrum_text << "!!<td class='exclusive'>" << endl;
+				m_humdrum_text << "!!" << token->getDataType() << endl;
+				m_humdrum_text << "!!</td>" << endl;
+			}
+
+			m_humdrum_text << "!!<td class='tandem";
+			if (m_entries[i].token->size() > 15) {
+				m_humdrum_text << " squeeze";
+			}
+			m_humdrum_text << "'>" << endl;
+			m_humdrum_text << "!!" << m_entries[i].token << endl;
+			m_humdrum_text << "!!</td>" << endl;
 
-void MeasureComparisonGrid::analyze(MeasureDataSet* set1, MeasureDataSet* set2) {
-	analyze(*set1, *set2);
-}
+			HumRegex hre;
+			m_humdrum_text << "!!<td class='description'>" << endl;
+			string description = m_entries[i].description;
+			// hre.replaceDestructive(description, "&lt;", "<", "g");
+			// hre.replaceDestructive(description, "&gt;", ">", "g");
+			hre.replaceDestructive(description, "<span class='tandeminfo unknown'>unknown</span>", "unknown");
+			hre.replaceDestructive(description, "<span class='tandeminfo unknown'>non-standard</span>", "non-standard");
+			m_humdrum_text << "!!" << description << endl;
+			m_humdrum_text << "!!</td>" << endl;
 
-void MeasureComparisonGrid::analyze(MeasureDataSet& set1, MeasureDataSet& set2) {
-	m_grid.resize(set1.size());
-	for (int i=0; i<(int)m_grid.size(); i++) {
-		m_grid[i].resize(set2.size());
+			m_humdrum_text << "!!</tr>" << endl;
+		}
+
+		m_humdrum_text << "!!</table>" << endl;
 	}
-	for (int i=0; i<(int)m_grid.size(); i++) {
-		for (int j=0; j<(int)m_grid[i].size(); j++) {
-			m_grid[i][j].compare(set1[i], set2[j]);
+
+	// print relevant settings
+	vector<string> settings;
+	if (m_headerOnlyQ) {
+		settings.push_back("Only processing header interpretations");
+	}
+	if (m_bodyOnlyQ) {
+		settings.push_back("Only processing body interpretations");
+	}
+	if (m_unknownQ) {
+		settings.push_back("Only processing unknown interpretations");
+	}
+	if (m_entries.empty()) {
+		settings.push_back("No interpretations found");
+	}
+	if (!m_entries.empty()) {
+		if ((!m_countQ) && m_sortQ && !m_entries.empty()) {
+			settings.push_back("List sorted alphabetically by interpretation");
+		} else if (m_countQ && m_sortByCountQ) {
+			settings.push_back("List sorted low to high by count");
+		} else if (m_countQ && m_sortByReverseCountQ) {
+			settings.push_back("List sorted high to low by count");
 		}
 	}
-	m_set1 = &set1;
-	m_set2 = &set2;
+	if (!settings.empty()) {
+		m_humdrum_text << "!!<ul>" << endl;
+		for (int i=0; i<(int)settings.size(); i++) {
+			m_humdrum_text << "!!<li>";
+			m_humdrum_text << settings[i];
+			m_humdrum_text << "</li>" << endl;
+		}
+		m_humdrum_text << "!!</ul>" << endl;
+	}
+
+	m_humdrum_text << "!!</details>" << endl;
+	m_humdrum_text << "!!@@END: PREHTML" << endl;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::printCorrelationGrid --
-//    default value: out = std::cout
+// Tool_tandeminfo::printEntiesText --
 //
 
-ostream& MeasureComparisonGrid::printCorrelationGrid(ostream& out) {
-	for (int i=0; i<(int)m_grid.size(); i++) {
-		for (int j=0; j<(int)m_grid[i].size(); j++) {
-			double correl = m_grid[i][j].getCorrelation7pc();
-			if (correl > 0.0) {
-				out << int(correl * 100.0 + 0.5)/100.0;
-			} else {
-				out << -int(-correl * 100.0 + 0.5)/100.0;
-			}
-			if (j < (int)m_grid[i].size() - 1) {
-				out << '\t';
-			}
+void Tool_tandeminfo::printEntriesText(HumdrumFile& infile) {
+	map<string, bool> processed;  // used for -c option
+
+	if (m_humdrumQ) {
+		if (m_locationQ) {
+			m_free_text << "**loc" << "\t";
+		} else if (m_countQ) {
+			m_free_text << "**count" << "\t";
 		}
-		out << endl;
+		if (m_filenameQ) {
+			m_free_text << "**file" << "\t";
+		}
+		if (m_exclusiveQ) {
+			m_free_text << "**exinterp" << "\t";
+		}
+		m_free_text << "**tandem";
+		if (m_descriptionQ) {
+			m_free_text << "\t" << "**info";
+		}
+		m_free_text << endl;
 	}
-	return out;
-}
-
 
+	for (int i=0; i<(int)m_entries.size(); i++) {
+		HTp token          = m_entries[i].token;
+		if (m_countQ && processed[token->getText()]) {
+			continue;
+		}
+		processed[token->getText()] = true;
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::printCorrelationDiagonal -- Assuming a square grid for now.
-//    default value: out = std::cout
-//
-
-ostream& MeasureComparisonGrid::printCorrelationDiagonal(ostream& out) {
-	for (int i=0; i<(int)m_grid.size(); i++) {
-		for (int j=0; j<(int)m_grid[i].size(); j++) {
-			if (i != j) {
-				continue;
-			}
-			double correl = m_grid[i][j].getCorrelation7pc();
-			if (correl > 0.0) {
-				out << int(correl * 100.0 + 0.5)/100.0;
+		string description = m_entries[i].description;
+		HumRegex hre;
+		hre.replaceDestructive(description, "", "</?span.*?>", "g");
+		if (m_filenameQ) {
+			m_free_text << infile.getFilename() << "\t";
+		}
+		if (m_locationQ) {
+			if (m_zeroQ) {
+				int row = token->getLineIndex();
+				int col = token->getFieldIndex();
+				m_free_text << "(" << row << ", " << col << ")" << "\t";
 			} else {
-				out << -int(-correl * 100.0 + 0.5)/100.0;
+				int row = token->getLineNumber();
+				int col = token->getFieldNumber();
+				m_free_text << "(" << row << ", " << col << ")" << "\t";
 			}
-			if (j < (int)m_grid[i].size() - 1) {
-				out << '\t';
+		} else if (m_countQ) {
+			m_free_text << m_count[token->getText()] << "\t";
+		}
+		if (m_exclusiveQ) {
+			string exinterp = token->getDataType();
+			if (m_humdrumQ) {
+				exinterp = exinterp.substr(2);
 			}
+			m_free_text << exinterp << "\t";
 		}
-		out << endl;
+		if (m_humdrumQ) {
+			string text = token->getText();
+			text = text.substr(1);
+			m_free_text << text;
+		} else {
+			m_free_text << token;
+		}
+		if (m_descriptionQ) {
+			m_free_text << "\t" << description;
+		}
+		m_free_text << endl;
+	}
+
+	if (m_humdrumQ) {
+		if (m_locationQ) {
+			m_free_text << "*-" << "\t";
+		} else if (m_countQ) {
+			m_free_text << "*-" << "\t";
+		}
+		if (m_filenameQ) {
+			m_free_text << "*-" << "\t";
+		}
+		if (m_exclusiveQ) {
+			m_free_text << "*-" << "\t";
+		}
+		m_free_text << "*-";
+		if (m_descriptionQ) {
+			m_free_text << "\t" << "*-";
+		}
+		m_free_text << endl;
 	}
-	return out;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getColorMapping --
+// Tool_tandeminfo::getDescription -- Return description of the input token; otherwise, return m_unknown.
 //
 
-void MeasureComparisonGrid::getColorMapping(double input, double& hue,
-		double& saturation, double& lightness) {
-	double maxhue = 0.75 * 360.0;
-	hue = input;
-	if (hue < 0.0) {
-		hue = 0.0;
+string Tool_tandeminfo::getDescription(HTp token) {
+	string tok = token->substr(1);
+	string description;
+
+	description = checkForKeySignature(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	hue = hue * hue;
-	if (hue != 1.0) {
-		hue *= 0.95;
+
+	description = checkForKeyDesignation(tok);
+	if (description != m_unknown) {
+		return description;
 	}
 
-	hue = (1.0 - hue) * 360.0;
-	if (hue == 0.0) {
-		// avoid -0.0;
-		hue = 0.0;
+	description = checkForInstrumentInfo(tok);
+	if (description != m_unknown) {
+		return description;
 	}
 
-	if (hue > maxhue) {
-		hue = maxhue;
+	description = checkForLabelInfo(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	if (hue < 0.0) {
-		hue = maxhue;
+
+	description = checkForTimeSignature(tok);
+	if (description != m_unknown) {
+		return description;
 	}
 
-	saturation = 100.0;
-	lightness = 50.0;
+	description = checkForMeter(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-	if (hue > 60) {
-		lightness = lightness - (hue-60) / (maxhue-60) * lightness / 1.5;
+	description = checkForTempoMarking(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-}
 
+	description = checkForClef(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForStaffPartGroup(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getQoff1 -- return the end time class ID of the
-//     current grid cell (for the first piece being compared).
-//
+	description = checkForTuplet(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-std::string MeasureComparisonGrid::getQoff1(int index) {
-	if (m_set1 == NULL) {
-		return "";
+	description = checkForHands(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	return (*m_set1)[index].getQoff();
-}
 
+	description = checkForPosition(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForCue(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getQoff2 -- return the end time class ID of the
-//     current grid cell (for the first piece being compared).
-//
+	description = checkForFlip(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-std::string MeasureComparisonGrid::getQoff2(int index) {
-	if (m_set2 == NULL) {
-		return "";
+	description = checkForTremolo(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	return (*m_set2)[index].getQoff();
-}
 
+	description = checkForOttava(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForPedal(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getQon1 -- return the start time class ID of the
-//     current grid cell (for the first piece being compared).
-//
+	description = checkForBracket(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-string MeasureComparisonGrid::getQon1(int index) {
-	if (m_set1 == NULL) {
-		return "";
+	description = checkForRscale(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	return (*m_set1)[index].getQon();
-}
 
+	description = checkForTimebase(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForTransposition(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getQon2 -- return the start time class ID of the
-//     current grid cell (for the second piece being compared).
-//
+	description = checkForGrp(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-string MeasureComparisonGrid::getQon2(int index) {
-	if (m_set2 == NULL) {
-		return "";
+	description = checkForStria(tok);
+	if (description != m_unknown) {
+		return description;
+	}
+
+	description = checkForFont(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	return (*m_set2)[index].getQon();
-}
 
+	description = checkForVerseLabels(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForLanguage(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getMeasure1 -- return the measure of the
-//     current grid cell (for the first piece being compared).
-//
+	description = checkForStemInfo(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-int MeasureComparisonGrid::getMeasure1(int index) {
-	if (m_set1 == NULL) {
-		return 0.0;
+	description = checkForXywh(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	return (*m_set1)[index].getMeasure();
-}
 
+	description = checkForCustos(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForTextInterps(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getMeasure2 -- return the measure of the
-//     current grid cell (for the second piece being compared).
-//
+	description = checkForRep(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-int MeasureComparisonGrid::getMeasure2(int index) {
-	if (m_set2 == NULL) {
-		return 0.0;
+	description = checkForPline(tok);
+	if (description != m_unknown) {
+		return description;
 	}
-	return (*m_set2)[index].getMeasure();
-}
 
+	description = checkForTacet(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
+	description = checkForFb(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-//////////////////////////////
-//
-// MeasureComparisonGrid::getStartTime1 -- return the start time of the
-//     measure at index position in the first compared score.
-//
+	description = checkForColor(tok);
+	if (description != m_unknown) {
+		return description;
+	}
 
-double MeasureComparisonGrid::getStartTime1(int index) {
-	if (m_set1 == NULL) {
-		return 0.0;
+	description = checkForThru(tok);
+	if (description != m_unknown) {
+		return description;
+	}
+
+	HumRegex hre;
+	if (hre.search(token, "\\s+$")) {
+		return "unknown (space at end of interpretation may be the problem)";
+	} else {
+		return m_unknown;
 	}
-	return (*m_set1)[index].getStartTime();
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getScoreDuration1 --
+// Tool_tandeminfo::checkForThru -- Humdrum Toolkit interpretations related to thru command.
 //
 
-double MeasureComparisonGrid::getScoreDuration1(void) {
-	if (m_set1 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForThru(const string& tok) {
+	if (tok == "thru") {
+		return "data processed by thru command (expansion lists processed)";
 	}
-	return m_set1->getScoreDuration();
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getStartTime2 --
+// Tool_tandeminfo::checkForColor -- Extended interprerations for coloring notes in **kern data.
+//     Used in verovio.
 //
 
-double MeasureComparisonGrid::getStartTime2(int index) {
-	if (m_set2 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForColor(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^color:(.*)")) {
+		string color = hre.getMatch(1);
+		string output;
+		if (hre.search(tok, "^#[0-9A-Fa-f]{3}$")) {
+			output = "3-digit hex ";
+		} else if (hre.search(tok, "^#[0-9A-Fa-f]{6}$")) {
+			output = "6-digit hex ";
+		} else if (hre.search(tok, "^#[0-9A-Fa-f]{8}$")) {
+			output = "8-digit hex  (RGB + transparency)";
+		} else if (hre.search(tok, "^rgb(\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+)$")) {
+			output = "RGB integer";
+		} else if (hre.search(tok, "^rgb(\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*,[\\d.]+)$")) {
+			output = "RGB integer with alpha";
+		} else if (hre.search(tok, "^hsl(\\d+\\s*,\\s*\\d+%\\s*,\\s*\\d+%)$")) {
+			output = "HSL";
+		} else if (hre.search(tok, "^hsl(\\d+\\s*,\\s*\\d+%\\s*,\\s*\\d+%,\\s*[\\d.]+)$")) {
+			output = "HSL with alpha";
+		} else if (hre.search(tok, "^[a-z]+$")) {
+			output = "named ";
+		}
+		output += " color";
+		return output;
 	}
-	return (*m_set2)[index].getStartTime();
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getStopTime1 --
+// Tool_tandeminfo::checkForFb -- Extended interprerations especially for **fb (**fa) exclusive
+//     interpretations.
 //
 
-double MeasureComparisonGrid::getStopTime1(int index) {
-	if (m_set1 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForFb(const string& tok) {
+	if (tok == "reverse") {
+		return "reverse order of accidental and number in figured bass";
 	}
-	return (*m_set1)[index].getStopTime();
+	if (tok == "Xreverse") {
+		return "stop reversing order of accidental and number in figured bass";
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getStopTime2 --
+// Tool_tandeminfo::checkForTacet -- Extended interprerations for marking parts that are not
+//     playing (rests only) in a movement/movement subsection.
 //
 
-double MeasureComparisonGrid::getStopTime2(int index) {
-	if (m_set2 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForTacet(const string& tok) {
+	if (tok == "tacet") {
+		return "part is tacet in movement/section";
 	}
-	return (*m_set2)[index].getStopTime();
+	if (tok == "Xtacet") {
+		return "end of part tacet";
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getDuration1 --
+// Tool_tandeminfo::checkForRep -- Extended interprerations for poetic line analysis related to pline tool.
 //
 
-double MeasureComparisonGrid::getDuration1(int index) {
-	if (m_set1 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForPline(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^pline:(\\d+)([abcr]*)$")) {
+		string number = hre.getMatch(1);
+		string info = hre.getMatch(2);
+		string output = "poetic line markup: " + number + info;
+		return output;
 	}
-	return (*m_set1)[index].getDuration();
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getDuration2 --
+// Tool_tandeminfo::checkForRep -- Extended interprerations for adding repeat sign shorthand for
+//     repeated music.
 //
 
-double MeasureComparisonGrid::getDuration2(int index) {
-	if (m_set2 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForRep(const string& tok) {
+	if (tok == "rep") {
+		return "start of repeat sign replacing notes/rests";
 	}
-	return (*m_set2)[index].getDuration();
+	if (tok == "Xrep") {
+		return "end of repeat sign replacing notes/rests";
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::getScoreDuration2 --
+// Tool_tandeminfo::checkForTextInterps -- Extended interprerations for **text and **silbe
 //
 
-double MeasureComparisonGrid::getScoreDuration2(void) {
-	if (m_set2 == NULL) {
-		return 0.0;
+string Tool_tandeminfo::checkForTextInterps(const string& tok) {
+	if (tok == "ij") {
+		return "start of text repeat region";
 	}
-	return m_set2->getScoreDuration();
+	if (tok == "Xij") {
+		return "end of text repeat region";
+	}
+	if (tok == "edit") {
+		return "start of editorial text region";
+	}
+	if (tok == "Xedit") {
+		return "end of editorial text region";
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// MeasureComparisonGrid::printSvgGrid --
-//    default value: out = std::cout
+// Tool_tandeminfo::checkForCustos -- Extended interprerations for marker
+//     at end of system for next note in part.
 //
 
-ostream& MeasureComparisonGrid::printSvgGrid(ostream& out) {
-	pugi::xml_document image;
-	auto declaration = image.prepend_child(pugi::node_declaration);
-	declaration.append_attribute("version") = "1.0";
-	declaration.append_attribute("encoding") = "UTF-8";
-	declaration.append_attribute("standalone") = "no";
-
-	auto svgnode = image.append_child("svg");
-	svgnode.append_attribute("version") = "1.1";
-	svgnode.append_attribute("xmlns") = "http://www.w3.org/2000/svg";
-	svgnode.append_attribute("xmlns:xlink") = "http://www.w3.org/1999/xlink";
-	svgnode.append_attribute("overflow") = "visible";
-	svgnode.append_attribute("viewBox") = "0 0 1000 1000";
-	svgnode.append_attribute("width") = "1000px";
-	svgnode.append_attribute("height") = "1000px";
-
-	auto grid = svgnode.append_child("g");
-	grid.append_attribute("id") = "grid";
-
-	double hue = 0.0;
-	double saturation = 100;
-	double lightness = 75;
-
-	pugi::xml_node crect;
-	double width;
-	double height;
-
-	stringstream ss;
-	stringstream css;
-	double x;
-	double y;
-
-	double imagewidth = 1000.0;
-	double imageheight = 1000.0;
-
-	double sdur1 = getScoreDuration1();
-	double sdur2 = getScoreDuration2();
+string Tool_tandeminfo::checkForCustos(const string& tok) {
+	HumRegex hre;
 
-	for (int i=0; i<(int)m_grid.size(); i++) {
-		for (int j=0; j<(int)m_grid[i].size(); j++) {
-			width = getDuration2(j) / sdur2 * imagewidth;
-			height = getDuration1(i) / sdur1 * imageheight;
+	if (tok == "custos") {
+		return "custos, pitch unspecified";
+	}
 
-			x = getStartTime2(j)/sdur2 * imageheight;
-			y = getStartTime1(i)/sdur1 * imagewidth;
+	if (tok == "custos:") {
+		return "custos, pitch unspecified";
+	}
 
-			getColorMapping(m_grid[i][j].getCorrelation7pc(), hue, saturation, lightness);
-			ss << "hsl(" << hue << "," << saturation << "%," << lightness << "%)";
-			crect = grid.append_child("rect");
-			crect.append_attribute("x") = to_string(x).c_str();
-			crect.append_attribute("y") = to_string(y).c_str();
-			crect.append_attribute("width") = to_string(width*0.99).c_str();
-			crect.append_attribute("height") = to_string(height*0.99).c_str();
-			crect.append_attribute("fill") = ss.str().c_str();
-			css << "Xm" << getMeasure1(i) << " Ym" << getMeasure2(j);
-			css << " X" << getQon1(i)     << " Y" << getQon2(j);
-			css << " X" << getQoff1(i)    << " Y" << getQoff2(j);
-			crect.append_attribute("class") = css.str().c_str();
-			ss.str("");
-			css.str("");
-		}
+	if (hre.search(tok, "^custos:([A-G]+|[a-g]+)(#+|-+|n)?$")) {
+		// also deal with chord custos
+		string pitch = hre.getMatch(1);
+		string accid = hre.getMatch(2);
+		string output = "custos on pitch " + pitch + accid;
+		return output;
 	}
 
-	image.save(out);
-	return out;
+	return m_unknown;
 }
 
 
-///////////////////////////////////////////////////////////////////////////
-
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_simat::Tool_simat -- Set the recognized options for the tool.
+// Tool_tandeminfo::checkForStemInfo -- Extended interprerations
+//      for visual display of stems (on left or right side of notes).
 //
 
-Tool_simat::Tool_simat(void) {
-	define("r|raw=b",      "output raw correlation matrix");
-	define("d|diagonal=b", "output diagonal of correlation matrix");
+string Tool_tandeminfo::checkForXywh(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^xywh-([^:\\s]+):(\\d+),(\\d+),(\\d+),(\\d+)$")) {
+		string page = hre.getMatch(1);
+		string x = hre.getMatch(2);
+		string y = hre.getMatch(3);
+		string w = hre.getMatch(4);
+		string h = hre.getMatch(5);
+		string output = "IIIF bounding box, page=";
+		output += page;
+		output += ", x=" + x;
+		output += ", y=" + y;
+		output += ", w=" + w;
+		output += ", h=" + h;
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_simat::run -- Primary interfaces to the tool.
+// Tool_tandeminfo::checkForStemInfo -- Extended interprerations
+//      for visual display of stems (on left or right side of notes).
 //
 
-bool Tool_simat::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	if (infiles.getCount() == 1) {
-		status = run(infiles[0], infiles[0]);
-	} else if (infiles.getCount() > 1) {
-		status = run(infiles[0], infiles[1]);
-	} else {
-		status = false;
-	}
-	return status;
-}
-
+string Tool_tandeminfo::checkForStemInfo(const string& tok) {
+	HumRegex hre;
 
-bool Tool_simat::run(const string& indata1, const string& indata2, ostream& out) {
-	HumdrumFile infile1(indata1);
-	HumdrumFile infile2;
-	bool status;
-	if (indata2.empty()) {
-		infile2.read(indata2);
-		status = run(infile1, infile2);
-	} else {
-		status = run(infile1, infile1);
-	}
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile1;
-		out << infile2;
+	if (hre.search(tok, "^(\\d+)/left$")) {
+		string rhythm = hre.getMatch(1);
+		string output = rhythm + "-rhythm notes always have stem up on the left";
+		return output;
 	}
-	return status;
-}
-
 
-bool Tool_simat::run(HumdrumFile& infile1, HumdrumFile& infile2, ostream& out) {
-	bool status;
-	if (infile2.getLineCount() == 0) {
-		status = run(infile1, infile1);
-	} else {
-		status = run(infile1, infile2);
-	}
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile1;
-		out << infile2;
+	if (hre.search(tok, "^(\\d+)\\\\left$")) {
+		string rhythm = hre.getMatch(1);
+		string output = rhythm + "-rhythm notes always have stem down on the left";
+		return output;
 	}
-	return status;
-}
-
-//
-// In-place processing of file:
-//
 
-bool Tool_simat::run(HumdrumFile& infile1, HumdrumFile& infile2) {
-	if (infile2.getLineCount() == 0) {
-		processFile(infile1, infile1);
-	} else {
-		processFile(infile1, infile2);
+	if (hre.search(tok, "^(\\d+)/right$")) {
+		string rhythm = hre.getMatch(1);
+		string output = rhythm + "-rhythm notes always have stem up on the right";
+		return output;
 	}
 
-	return true;
-}
-
-
-
-//////////////////////////////
-//
-// Tool_simat::processFile --
-//
+	if (hre.search(tok, "^(\\d+)\\\\right$")) {
+		string rhythm = hre.getMatch(1);
+		string output = rhythm + "-rhythm notes always have stem down on the right";
+		return output;
+	}
 
-void Tool_simat::processFile(HumdrumFile& infile1, HumdrumFile& infile2) {
-	m_data1.parse(infile1);
-	m_data2.parse(infile2);
-	m_grid.analyze(m_data1, m_data2);
-	if (getBoolean("raw")) {
-		m_grid.printCorrelationGrid(m_free_text);
-		suppressHumdrumFileOutput();
-	} else if (getBoolean("diagonal")) {
-		m_grid.printCorrelationDiagonal(m_free_text);
-		suppressHumdrumFileOutput();
-	} else {
-		m_grid.printSvgGrid(m_free_text);
-		suppressHumdrumFileOutput();
+	if (tok == "all/right") {
+		string output = "all notes always have stem up on the right";
+		return output;
 	}
-}
 
+	if (tok == "all\\right") {
+		string output = "all notes always have stem down on the right";
+		return output;
+	}
 
+	if (tok == "all/left") {
+		string output = "all notes always have stem up on the left";
+		return output;
+	}
 
+	if (tok == "all\\left") {
+		string output = "all notes always have stem down on the left";
+		return output;
+	}
 
+	if (tok == "all/center") {
+		string output = "all notes always have stem up on notehead center";
+		return output;
+	}
 
-/////////////////////////////////
-//
-// Tool_slurcheck::Tool_slurcheck -- Set the recognized options for the tool.
-//
+	if (tok == "all\\center") {
+		string output = "all notes always have stem down on notehead center";
+		return output;
+	}
+	// there is also "middle" which is the same as "center";
 
-Tool_slurcheck::Tool_slurcheck(void) {
-	// add options here
-	define("l|list=b",     "list locations of unclosed slur endings");
-	define("c|count=b",    "count unclosed slur endings");
-	define("Z|no-zeros=b", "do not list files that have zero unclosed slurs in counts");
-	define("f|filename=b", "print filename for list and count options");
+	return m_unknown;
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_slurcheck::run -- Do the main work of the tool.
+// Tool_tandeminfo::checkForLanguage -- Humdrum Toolkit and extended interprerations
+//      for langauages (for **text and **silbe).
 //
 
-bool Tool_slurcheck::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
-	}
-	return status;
-}
-
+string Tool_tandeminfo::checkForLanguage(const string& tok) {
+	HumRegex hre;
 
-bool Tool_slurcheck::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (hre.search(tok, "^L([A-Z][^\\s]+)$")) {
+		string language = hre.getMatch(1);
+		string output = "Language, old style: " + language;
+		return output;
 	}
-	return status;
-}
-
 
-bool Tool_slurcheck::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (hre.search(tok, "^lang:([A-Z]{2,3})$")) {
+		string code = hre.getMatch(1);
+		string name = Convert::getLanguageName(code);
+		if (name.empty()) {
+			string output = "language code <span class='tandem'>" + code +  "</span>=unknown";
+			return output;
+		}
+		string output = "language code";
+		if (code.size() == 2) {
+			output = "ISO 639-3 two-letter language code: ";
+		} else if (code.size() == 3) {
+			output = "ISO 639-3 three-letter language code: ";
+		}
+		output += "<span class='tandem'>";
+		output += code;
+		output += "</span>=\"";
+		output += name;
+		output += "\"";
+		return output;
 	}
-	return status;
-}
 
-
-bool Tool_slurcheck::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	infile.createLinesFromTokens();
-	return true;
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_slurcheck::initialize --
+// Tool_tandeminfo::checkForVerseLabels -- Extended tandem interpretations (used by verovio
+//      for visual rendeing of notation).
 //
 
-void Tool_slurcheck::initialize(void) {
+string Tool_tandeminfo::checkForVerseLabels(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^v:(.*)$")) {
+		string output = "verse label \"" + hre.getMatch(1) + "\"";
+		return output;
+	}
+	if (hre.search(tok, "^vv:(.*)$")) {
+		string output = "verse label \"" + hre.getMatch(1) + "\", repeated after each system break";
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_slurcheck::processFile --
+// Tool_tandeminfo::checkForFont -- Extended interprtations for styling **text and **silbe.
 //
 
-void Tool_slurcheck::processFile(HumdrumFile& infile) {
-	infile.analyzeSlurs();
-	int opencount = 0;
-	int closecount = 0;
-	int listQ  = getBoolean("list");
-	int countQ = getBoolean("count");
-	int zeroQ = !getBoolean("no-zeros");
-	int filenameQ  = getBoolean("filename");
-	if (listQ || countQ) {
-		suppressHumdrumFileOutput();
-	}
-	for (int i=0; i<infile.getStrandCount(); i++) {
-		HTp stok = infile.getStrandStart(i);
-		if (!stok->isKern()) {
-			continue;
-		}
-		HTp etok = infile.getStrandEnd(i);
-		HTp tok = stok;
-		while (tok && (tok != etok)) {
-			if (!tok->isData()) {
-				tok = tok->getNextToken();
-				continue;
-			}
-			if (tok->isNull()) {
-				tok = tok->getNextToken();
-				continue;
-			}
-			string value = tok->getValue("auto", "hangingSlur");
-			if (value == "true") {
-				string side = tok->getValue("auto", "slurSide");
-				if (side == "start") {
-					opencount++;
-					if (listQ) {
-						if (filenameQ) {
-							m_free_text << infile.getFilename() << ":\t";
-						}
-						m_free_text << "UNCLOSED SLUR\tline:" << tok->getLineIndex()+1
-								<< "\tfield:" << tok->getFieldIndex()+1 << "\ttoken:" << tok << endl;
-					} else if (!countQ) {
-						string data = *tok;
-						data += "i";
-						tok->setText(data);
-					}
-				} else if (side == "stop") {
-					closecount++;
-					if (listQ) {
-						if (filenameQ) {
-							m_free_text << infile.getFilename() << ":\t";
-						}
-						m_free_text << "UNOPENED SLUR\tline:" << tok->getLineIndex()+1
-								<< "\tfield:" << tok->getFieldIndex()+1 << "\ttoken:" << tok << endl;
-					} else if (!countQ) {
-						string data = *tok;
-						data += "j";
-						tok->setText(data);
-					}
-				}
-			}
-			tok = tok->getNextToken();
-		}
-	}
-
-	if (countQ) {
-		int sum = opencount + closecount;
-		if ((!zeroQ) && (sum == 0)) {
-			return;
-		}
-		if (filenameQ) {
-			m_free_text << infile.getFilename() << ":\t";
-		}
-		m_free_text << (opencount + closecount) << "\t(:" << opencount << "\t):" << closecount << endl;
-	}
-
-	if (countQ || listQ) {
-		return;
+string Tool_tandeminfo::checkForFont(const string& tok) {
+	if (tok == "italic") {
+		return "use italic font style";
 	}
-
-	if (opencount + closecount == 0) {
-		return;
+	if (tok == "Xitalic") {
+		return "stop using italic font style";
 	}
-
-	if (opencount) {
-		infile.appendLine("!!!RDF**kern: i = marked note, color=\"hotpink\", text=\"extra(\"");
+	if (tok == "bold") {
+		return "use bold font style";
 	}
-
-	if (closecount) {
-		infile.appendLine("!!!RDF**kern: j = marked note, color=\"magenta\", text=\"extra)\"");
+	if (tok == "Xbold") {
+		return "stop using bold font style";
 	}
 
-	infile.createLinesFromTokens();
+	return m_unknown;
 }
 
 
 
-
-
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_gridtest::Tool_spinetrace -- Set the recognized options for the tool.
+// Tool_tandeminfo::checkForStria -- Humdrum Toolkit interpretation.
 //
 
-Tool_spinetrace::Tool_spinetrace(void) {
-	define("a|append=b",  "append analysis to input data lines");
-	define("p|prepend=b", "prepend analysis to input data lines");
+string Tool_tandeminfo::checkForStria(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^stria(\\d+)$")) {
+		string output = "number of staff lines:" + hre.getMatch(1);
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_spinetrace::run -- Primary interfaces to the tool.
+// Tool_tandeminfo::checkForGrp -- Polyrhythm project interpretations for
+//      polyrhythm group assignments.  Related to humlib composite tool.
 //
 
-bool Tool_spinetrace::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+string Tool_tandeminfo::checkForGrp(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^grp:([AB])$")) {
+		string output = "composite rhythm grouping label " + hre.getMatch(1);
+		return output;
 	}
-	return status;
-}
-
-
-bool Tool_spinetrace::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	return run(infile, out);
-}
-
 
-bool Tool_spinetrace::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	return status;
-}
-
-
-bool Tool_spinetrace::run(HumdrumFile& infile) {
-   initialize(infile);
-	processFile(infile);
-	return true;
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_spinetrace::initialize --
+// Tool_tandeminfo::checkForTransposition -- Humdrum Toolkit interpretations related
+//      to pitch transposition.
 //
 
-void Tool_spinetrace::initialize(HumdrumFile& infile) {
-	// do nothing for now
+string Tool_tandeminfo::checkForTransposition(const string& tok) {
+	HumRegex hre;
+
+	if (hre.search(tok, "ITrd(-?\\d+)c(-?\\d+)$")) {
+		string diatonic = hre.getMatch(1);
+		string chromatic = hre.getMatch(2);
+		string output = "transposition for written part, diatonic: ";
+		output += diatonic;
+		output += ", chromatic: ";
+		output += chromatic;
+		return output;
+	}
+
+	if (hre.search(tok, "Trd(-?\\d+)c(-?\\d+)$")) {
+		string diatonic = hre.getMatch(1);
+		string chromatic = hre.getMatch(2);
+		string output = "transposed by diatonic: ";
+		output += diatonic;
+		output += ", chromatic: ";
+		output += chromatic;
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_spinetrace::processFile --
+// Tool_tandeminfo::checkForTimebase -- Humdrum Toolkit interpretations related
+//      to the timebase tool.
 //
 
-void Tool_spinetrace::processFile(HumdrumFile& infile) {
-	bool appendQ = getBoolean("append");
-	bool prependQ = getBoolean("prepend");
-
-	int linecount = infile.getLineCount();
-	for (int i=0; i<linecount; i++) {
-		if (!infile[i].hasSpines()) {
-			m_humdrum_text << infile[i] << endl;
-			continue;
-		}
-		if (appendQ) {
-			m_humdrum_text << infile[i] << "\t";
-		}
-
-		if (!infile[i].isData()) {
-			if (infile[i].isInterpretation()) {
-				int fieldcount = infile[i].getFieldCount();
-				for (int j=0; j<fieldcount; j++) {
-					HTp token = infile.token(i, j);
-					if (token->compare(0, 2, "**") == 0) {
-						m_humdrum_text << "**spine";
-					} else {
-						m_humdrum_text << token;
-					}
-					if (j < fieldcount - 1) {
-						m_humdrum_text << "\t";
-					}
-				}
-			} else {
-				m_humdrum_text << infile[i];
-			}
-		} else {
-			int fieldcount = infile[i].getFieldCount();
-			for (int j=0; j<fieldcount; j++) {
-				m_humdrum_text << infile[i].token(j)->getSpineInfo();
-				if (j < fieldcount - 1) {
-					m_humdrum_text << '\t';
-				}
-			}
-		}
-
-		if (prependQ) {
-			m_humdrum_text << "\t" << infile[i];
-		}
-		m_humdrum_text << "\n";
+string Tool_tandeminfo::checkForTimebase(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^tb(\\d+)$")) {
+		string number = hre.getMatch(1);
+		string output = "timebase: all data lines (should) have a duration of " + number;
+		return output;
 	}
-}
 
+	return m_unknown;
+}
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_strophe::Tool_strophe -- Set the recognized options for the tool.
+// Tool_tandeminfo::checkForRscale -- Extended interpretation for adjusting the visual
+//     display of note durations when they do not match the logical
+//     note durations (such as show a quarter note as if it were a
+//     half note, which would be indicated by "*rscale:2". Or a
+//     half note as if it were a quarter note with "*rscale:1/2".
+//     Also related to the rscale tool from Humdrum Extras and humlib.
+//     Used in verovio.
 //
 
-Tool_strophe::Tool_strophe(void) {
-	define("l|list=b",         "list all possible variants");
-	define("m=b",              "mark strophe music");
-	define("mark|marker=s:@",  "character to mark with");
-	define("c|color=s:red",    "character to mark with");
+string Tool_tandeminfo::checkForRscale(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^rscale:(\\d+)(/\\d+)?$")) {
+		string fraction = hre.getMatch(1) + hre.getMatch(2);
+		string output = "visual rhythmic scaling factor " + fraction;
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_strophe::run -- Do the main work of the tool.
+// Tool_tandeminfo::checkForBracket -- Extended interpretations for displaying
+//     various bracket lines in visual music notation.
 //
 
-bool Tool_strophe::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+string Tool_tandeminfo::checkForBracket(const string& tok) {
+	// Coloration
+	if (tok == "col") {
+		return "start of coloration bracket";
 	}
-	for (auto it = m_variants.begin(); it != m_variants.end(); ++it) {
-		m_free_text << *it << endl;
+	if (tok == "Xcol") {
+		return "end of coloration bracket";
 	}
-	return status;
-}
 
-
-bool Tool_strophe::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else if (!m_listQ) {
-		out << infile;
+	// Ligatures
+	if (tok == "lig") {
+		return "start of ligature bracket";
 	}
-	return status;
-}
-
-
-bool Tool_strophe::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else if (!m_listQ) {
-		out << infile;
+	if (tok == "Xlig") {
+		return "end of ligature bracket";
 	}
-	return status;
-}
 
+	// Schoenberg
+	if (tok == "haupt") {
+		return "start of Hauptstimme bracket";
+	}
+	if (tok == "Xhaupt") {
+		return "end of Hauptstimme bracket";
+	}
+	if (tok == "neben") {
+		return "start of Nebenstimme bracket";
+	}
+	if (tok == "Xneben") {
+		return "end of Nebenstimme bracket";
+	}
+	if (tok == "rhaupt") {
+		return "start of Hauptrhythm bracket";
+	}
+	if (tok == "Xrhaupt") {
+		return "end of Hauptrhythm bracket";
+	}
 
-bool Tool_strophe::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	return true;
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_strophe::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_tandeminfo::checkForPedal -- Extended interpretations for displaying
+//     ottava lines in music notation.
 //
 
-void Tool_strophe::initialize(void) {
-	m_listQ     = getBoolean("list");
-	m_markQ     = getBoolean("m");
-	m_marker    = getString("marker");
-	m_color     = getString("color");
+string Tool_tandeminfo::checkForPedal(const string& tok) {
+	if (tok == "ped") {
+		return "sustain pedal down";
+	}
+	if (tok == "Xped") {
+		return "sustain pedal up";
+	}
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_strophe::processFile --
+// Tool_tandeminfo::checkForOttava -- Extended interpretations for displaying
+//     ottava lines in music notation.
 //
 
-void Tool_strophe::processFile(HumdrumFile& infile) {
-	infile.analyzeStrophes();
-	if (m_listQ) {
-		displayStropheVariants(infile);
-	} else {
-		markWithColor(infile);
+string Tool_tandeminfo::checkForOttava(const string& tok) {
+	if (tok == "8va") {
+		return "start of 8va line";
+	}
+	if (tok == "X8va") {
+		return "end of 8va line";
+	}
+	if (tok == "8ba") {
+		return "start of 8ba (ottava basso) line";
 	}
+	if (tok == "X8ba") {
+		return "end of 8ba (ottava basso) line";
+	}
+	if (tok == "15ma") {
+		return "start of 15ma line";
+	}
+	if (tok == "X15ma") {
+		return "end of 15ma line";
+	}
+	if (tok == "coll8ba") {
+		return "coll ottava basso start";
+	}
+	if (tok == "Xcoll8ba") {
+		return "coll ottava basso end";
+	}
+	return m_unknown;
 }
 
 
 
+
 //////////////////////////////
 //
-// Tool_strophe::markWithColor --  Maybe give different colors
-//     to different variants.  Currently only marking the primary
-//     strophe.
+// Tool_tandeminfo::checkForTremolo -- Extended interpretations for collapsing
+//     repeated notes into tremolos in music notation rendering.
+//     Used specifically by verovio.
 //
 
-void Tool_strophe::markWithColor(HumdrumFile& infile) {
-	int counter = 0;
-	for (int i=0; i<infile.getStropheCount(); i++) {
-		HTp strophestart = infile.getStropheStart(i);
-		HTp stropheend = infile.getStropheEnd(i);
-		counter += markStrophe(strophestart, stropheend);
+string Tool_tandeminfo::checkForTremolo(const string& tok) {
+	if (tok == "tremolo") {
+		return "start of tremolo rendering of repeated notes";
 	}
-	if (counter) {
-		string rdf = "!!!RDF**kern: ";
-		rdf += m_marker;
-		rdf += " = marked note, strophe";
-		if (m_color != "red") {
-			rdf += ", color=\"";
-			rdf += m_color;
-			rdf += "\"";
-		}
-		infile.appendLine(rdf);
-		infile.createLinesFromTokens();
+	if (tok == "Xtremolo") {
+		return "end of tremolo rendering of repeated notes";
 	}
+	return m_unknown;
 }
 
 
-
 //////////////////////////////
 //
-// Tool_strophe::markStrophe -- Returns the number of marked notes/rests.
+// Tool_tandeminfo::checkForFlip -- Extended interpretations for use with the
+//     flipper humlib command.
 //
 
-int Tool_strophe::markStrophe(HTp strophestart, HTp stropheend) {
-	HTp current = strophestart;
-	int output = 0;
-	while (current && current != stropheend) {
-		if (current->isData() && !current->isNull()) {
-			// Think about multiple marking for individual notes in chords.
-			string value = current->getText();
-			value += m_marker;
-			current->setText(value);
-			output++;
-		}
-		current = current->getNextToken();
+string Tool_tandeminfo::checkForFlip(const string& tok) {
+	if (tok == "flip") {
+		return "switch order of subspines, specific to flipper tool";
 	}
-	return output;
+	if (tok == "Xflip") {
+		return "cancel flipping of subspine, specific to flipper tool";
+	}
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// displayStropheVariants --
+// Tool_tandeminfo::checkForCue -- Extended interpretations for visual rendering
+//     *cue means display as cue-sized notes.  Probably change
+//     this so that *cue means following notes are cue notes
+//     and add *cuesz for cue-sized notes (that are not cues
+//     from other instruments).
 //
 
-void Tool_strophe::displayStropheVariants(HumdrumFile& infile) {
-	for (int i=0; i<infile.getLineCount(); i++) {
-		if (!infile[i].isInterpretation()) {
-			continue;
-		}
-		for (int j=0; j<infile[i].getFieldCount(); j++) {
-			HTp token = infile.token(i, j);
-			if (token->compare(0, 3, "*S/") != 0) {
-				continue;
-			}
-			string variant = token->substr(3);
-			m_variants.insert(variant);
-		}
+string Tool_tandeminfo::checkForCue(const string& tok) {
+	if (tok == "cue") {
+		return "cue-sized notation follows";
 	}
+	if (tok == "Xcue") {
+		return "cancel cue-sized notation";
+	}
+	return m_unknown;
 }
 
 
 
+//////////////////////////////
+//
+// Tool_tandeminfo::checkForPosition -- Extended interpretations for visual rendering
+//     data above/below staff.  Useful in particular for **dynam.
+//     Staff number in part (relative to top staff) can be given
+//     as a number following a colon after the placement.
+//
 
+string Tool_tandeminfo::checkForPosition(const string& tok) {
+	if (tok == "above") {
+		return "place items above staff";
+	}
+	if (tok == "above:1") {
+		return "place items above first staff of part";
+	}
+	if (tok == "above:2") {
+		return "place items above second staff of part";
+	}
+	if (tok == "below") {
+		return "place items below staff";
+	}
+	if (tok == "below:1") {
+		return "place items below first staff of part";
+	}
+	if (tok == "below:2") {
+		return "place items below second staff of part";
+	}
+	if (tok == "center") {
+		return "centered items between two staves";
+	}
+	return m_unknown;
+}
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_synco::Tool_synco -- Set the recognized options for the tool.
+// Tool_tandeminfo::checkForHands -- Extended interpretations to indicate which
+//     hand is playing the notes (for grand-staff keyboard in particular).
 //
 
-Tool_synco::Tool_synco(void) {
-	define("c|color=s:skyblue", "SVG color to highlight syncopation notes");
-	define("i|info=b",          "display only statistics info");
-	define("f|filename=b",      "add filename to statistics info");
-	define("a|all=b",           "average all statistics info");
+string Tool_tandeminfo::checkForHands(const string& tok) {
+	if (tok == "LH") {
+		return "notes played by left hand";
+	}
+	if (tok == "RH") {
+		return "notes played by right hand";
+	}
+	return m_unknown;
 }
 
 
 
-/////////////////////////////////
+//////////////////////////////
 //
-// Tool_synco::run -- Do the main work of the tool.
+// Tool_tandeminfo::checkForTuplet -- Extended interpretations for **kern data to control
+//     visual stylings of tuplet numbers and brackets.
 //
 
-bool Tool_synco::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+string Tool_tandeminfo::checkForTuplet(const string& tok) {
+	if (tok == "Xbrackettup") {
+		return "suppress brackets for tuplets";
 	}
-	if (m_allQ) {
-		m_free_text << m_scountTotal << "\t";
-		m_free_text << m_notecountTotal << "\t";
-		double percent =  (double)m_scountTotal / m_notecountTotal;
-		percent = int(percent * 10000.0 + 0.5) / 100.0;
-		m_free_text << percent << "\t";
-		m_free_text << m_fileCount;
-		if (m_fileCount == 1) {
-			m_free_text << " file";
-		} else {
-			m_free_text << " files";
-		}
-		m_free_text << endl;
+	if (tok == "brackettup") {
+		return "do not suppress brackets for tuplets (default)";
 	}
-	return status;
+	if (tok == "tuplet") {
+		return "show tuplet numbers (default)";
+	}
+	if (tok == "Xtuplet") {
+		return "do not show tuplet numbers";
+	}
+	if (tok == "tupbreak") {
+		return "break tuplet at this point";
+	}
+	
+
+	return m_unknown;
 }
 
 
 
-bool Tool_synco::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
-	}
-	return status;
-}
+//////////////////////////////
+//
+// Tool_tandeminfo::checkForStaffPartGroup -- Humdrum Toolkit interpretation (*staff), and
+//    extensions to *part to group multiple staves into a single part as
+//    well as *group for grouping staves/parts into instrument class
+//    groups (useful for controlling connecting barlines across multiple
+//    staves).
+//
 
+string Tool_tandeminfo::checkForStaffPartGroup (const string& tok) {
+	HumRegex hre;
 
-bool Tool_synco::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	if (hasAnyText()) {
-		getAllText(out);
-	} else {
-		out << infile;
+	if (hre.search(tok, "^staff(\\d+)(/\\d+)*$")) {
+		string number = hre.getMatch(1);
+		string second = hre.getMatch(2);
+		string output;
+		if (second.empty()) {
+			output = "staff " + number;
+			return output;
+		}
+		output = "staves " + tok.substr(5);
+		return output;
 	}
-	return status;
-}
 
-
-bool Tool_synco::run(HumdrumFile& infile) {
-	initialize();
-	processFile(infile);
-	if (m_hasSyncoQ && !m_infoQ) {
-		infile.createLinesFromTokens();
-		m_humdrum_text << infile;
-		m_humdrum_text << "!!!RDF**kern: | = marked note, color=" << m_color << endl;
-	}
-	double notecount = infile.getNoteCount();
-	double density = m_scount / (double)notecount;
-	double percent =  int(density * 10000.0 + 0.5) / 100.0;
-	if (m_infoQ) {
-		m_free_text << m_scount << "\t" << notecount << "\t" << percent << "%";
-		if (m_fileQ) {
-			m_free_text << "\t" << infile.getFilename();
+	if (hre.search(tok, "^part(\\d+)(/\\d+)*$")) {
+		string number = hre.getMatch(1);
+		string second = hre.getMatch(2);
+		string output;
+		if (second.empty()) {
+			output = "part " + number;
+			return output;
 		}
-		m_free_text << endl;
+		output = "parts " + tok.substr(5);
+		return output;
+	}
 
-		m_scountTotal    += m_scount;
-		m_notecountTotal += notecount;
-		m_fileCount++;
-	} else {
-		m_humdrum_text << "!!!syncopated_notes: " << m_scount << endl;
-		m_humdrum_text << "!!!total_notes: " << notecount << endl;
-		m_humdrum_text << "!!!syncopated_density: " << percent << "%" << endl;
+	if (hre.search(tok, "^group(\\d+)(/\\d+)*$")) {
+		string number = hre.getMatch(1);
+		string second = hre.getMatch(2);
+		string output;
+		if (second.empty()) {
+			output = "group " + number;
+			return output;
+		}
+		output = "groups " + tok.substr(5);
+		return output;
 	}
 
-	return true;
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_synco::initialize --  Initializations that only have to be done once
-//    for all HumdrumFile segments.
+// Tool_tandeminfo::checkForClef -- Humdrum Toolkit interpretations.  Extension
+//     is "*clefX" for percussion clef (checked for below),
+//     and *clefG2yy for an invisible clef (not visually rendered).
 //
 
-void Tool_synco::initialize(void) {
-	m_infoQ = getBoolean("info");
-	m_fileQ = getBoolean("filename");
-	m_allQ  = getBoolean("all");
-	m_color = getString("color");
+string Tool_tandeminfo::checkForClef(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^(m|o)?clef([GFCX])(.*?)([12345])?(yy)?$")) {
+		string modori = hre.getMatch(1);
+		string ctype = hre.getMatch(2);
+		string octave = hre.getMatch(3);
+		string line = hre.getMatch(4);
+		string invisible = hre.getMatch(5);
+		string output = "clef: ";
+		if (ctype == "X") {
+			output += "percussion";
+			if (!line.empty()) {
+				output += ", line=" + line;
+			}
+			if (!octave.empty()) {
+				return m_unknown;
+			}
+		} else {
+			output += ctype;
+			if (line.empty()) {
+				return m_unknown;
+			}
+			output += ", line=" + line;
+			if (!octave.empty()) {
+				if (hre.search(octave, "^v+$")) {
+					output += ", octave displacement -" + to_string(octave.size());
+				} else if (hre.search(octave, "^\\^+$")) {
+					output += ", octave displacement +" + to_string(octave.size());
+				}
+			}
+		}
+		if (!invisible.empty()) {
+			output += ", invisible (not displayed in music rendering)";
+		}
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_synco::processFile --
+// Tool_tandeminfo::checkForTimeSignature -- Humdrum Toolkit interpretations.
+//      Extended for use with rare non-notatable rhythm bases, such as
+//      *M3/3%2 for three triplet whole notes to the measure (this
+//      is equivalent in duration to *M2/1 but gives a more refined
+//      version of what the beat is.  Maybe also allow "*M2/4." which
+//      would be equivalent to an explicit compound *M6/8 time signature.
+//      Other extensions could also be done such as *M4/4yy for an invisible
+//      time signature.  And another extension could be *M2/8+3/8 for *M5/8
+//      split into 2 + 3 beat groupings.
 //
 
-void Tool_synco::processFile(HumdrumFile& infile) {
-	int scount = infile.getStrandCount();
-	m_scount = 0;
-	for (int i=0; i<scount; i++) {
-		HTp stok = infile.getStrandStart(i);
-		if (!stok->isKern()) {
-			continue;
+string Tool_tandeminfo::checkForTimeSignature(const string& tok) {
+	HumRegex hre;
+	if (tok == "MX") {
+		return "unmeasured music time signature";
+	}
+	if (hre.search(tok, "^MX/(\\d+)(%\\d+)?(yy)?")) {
+		string output = "unmeasured music with beat " + hre.getMatch(1) + hre.getMatch(2);
+		if (hre.getMatch(3) == "yy") {
+			output += ", invisible";
+			return output;
 		}
-		HTp etok = infile.getStrandEnd(i);
-		processStrand(stok, etok);
 	}
+	if (hre.search(tok, "^M(\\d+)/(\\d+)(%\\d+)?(yy)?$")) {
+		string top = hre.getMatch(1);
+		string bot = hre.getMatch(2) + hre.getMatch(3);
+		string invisible = hre.getMatch(4);
+		string output = "time signature: top=";
+		output += "<span class='tandem'>";
+		output += top;
+		output += "</span>";
+		output += ", bottom=";
+		output += "<span class='tandem'>";
+		output += bot;
+		output += "</span>";
+		if (bot == "3%2") {
+			output += " (triplet semibreve)";
+		} else if (bot == "3%4") {
+			output += " (triplet breve)";
+		}
+		if (invisible == "yy") {
+			output += ", invisible";
+		}
+		return output;
+	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_synco::processStrand --
+// Tool_tandeminfo::checkForMeter -- Humdrum Toolkit interpretations. Extended for use
+//    with mensural signs.
 //
 
-void Tool_synco::processStrand(HTp stok, HTp etok) {
-	HTp current = stok;
-	while (current && (current != etok)) {
-		if (!current->isData()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isNull()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isRest()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (current->isSecondaryTiedNote()) {
-			current = current->getNextToken();
-			continue;
-		}
-		if (isSyncopated(current)) {
-			m_hasSyncoQ = true;
-			m_scount++;
-			markNote(current);
+string Tool_tandeminfo::checkForMeter(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^(m|o)?met\\((.*?)\\)$")) {
+		string modori = hre.getMatch(1);
+		string meter = hre.getMatch(2);
+		if (meter == "c") {
+			return "meter: common time";
+		}
+		if (meter == "c|") {
+			return "meter: cut time";
+		}
+		if (meter == "") {
+			return "meter: empty";
+		}
+		string output = "mensuration sign: ";
+		if (meter == "O") {
+			output += "circle";
+		} else if (meter == "O|") {
+			output += "cut-circle";
+		} else if (meter == "C") {
+			output += "c";
+		} else if (meter == "C|") {
+			output += "cut-c";
+		} else if (meter == "Cr") {
+			output += "reverse-c";
+		} else if (meter == "C.") {
+			output += "c-dot";
+		} else if (meter == "O.") {
+			output += "circle-dot";
+		} else {
+			output += meter;
 		}
-		current = current->getNextToken();
+		return output;
 	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_synco::isSyncopated --
+// Tool_tandeminfo::checkForTempoMarking -- Humdrum Toolit interpretations.
 //
 
-bool Tool_synco::isSyncopated(HTp token) {
-	double metlev   = getMetricLevel(token);
-	HumNum duration = token->getTiedDuration();
-	double logDur   = log2(duration.getFloat());
-	if (metlev == 2) {
-		return false;
+string Tool_tandeminfo::checkForTempoMarking(const string& tok) {
+	HumRegex hre;
+	if (hre.search(tok, "^MM(\\d+)(\\.\\d*)?$")) {
+		string tempo = hre.getMatch(1) + hre.getMatch(2);
+		string output = "tempo: " + tempo + " quarter notes per minute";
+		return output;
 	}
-	if (logDur > metlev) {
-		return true;
-	} else {
-		return false;
+
+	if (hre.search(tok, "^MM\\[(.*?)\\]$")) {
+		string text = hre.getMatch(1);
+		string output = "text-based tempo: " + text;
+		return output;
 	}
+
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_synco::getMetricLevel -- Assuming whole-note beats for now.
+// Tool_tandeminfo::checkForLabelInfo -- Humdrum Toolkit interpretations.
+//     Used by the thru command.
 //
 
-double Tool_synco::getMetricLevel(HTp token) {
-	HumNum durbar = token->getDurationFromBarline();
-	if (!durbar.isInteger()) {
-		return -1.0;
+string Tool_tandeminfo::checkForLabelInfo(const string& tok) {
+	HumRegex hre;
+	if (!hre.search(tok, "^>")) {
+		return m_unknown;
 	}
-	if (durbar.getNumerator() % 4 == 0) {
-		return 2.0;
+
+	if (hre.search(tok, "^>(\\[.*\\]$)")) {
+		string list = hre.getMatch(1);
+		string output = "default expansion list: ";
+		output += "<span class='tandem'>";
+		output += list;
+		output += "</span>";
+		return output;
 	}
-	if (durbar.getNumerator() % 2 == 0) {
-		return 1.0;
+
+	if (hre.search(tok, "^>([^[\\[\\]]+)(\\[.*\\]$)")) {
+		string expansionName = hre.getMatch(1);
+		string list = hre.getMatch(2);
+		string output = "alternate expansion list: label=";
+		output += "<span class='tandem'>" + expansionName + "</span>";
+		if (expansionName == "norep") {
+			output += " (meaning: no repeats, i.e., take only second endings)";
+		}
+		output += ", expansion list: " + list;
+		return output;
 	}
-	return 0.0;
+
+	if (hre.search(tok, "^>([^\\[\\]]+)$")) {
+		string label = hre.getMatch(1);
+		string output = "expansion label: ";
+		output += "<span class='tandem'>";
+		output += label;
+		output += "</span>";
+		return output;
+	}
+
+	return m_unknown;
+
 }
 
 
 
 //////////////////////////////
 //
-// Tool_synco::markNote -- Currently ignoring chords.
+// Tool_tandeminfo::checkForInstrumentInfo -- Humdrum Toolkit and extended interpretations.
+//     Humdrum Tookit:
+//         instrument group	 *IG
+//         instrument class	*IC
+//         instrument code	*I
+//     Extended:
+//         instrument name *I"
+//         instrument number *I#
+//         instrument abbreviation *I'
+//
+//     modori tool extensions:
+//         *mI == modernized
+//         *oI == original
 //
 
-void Tool_synco::markNote(HTp token) {
-	token->setText(token->getText() + "|");
-	if ((token->find('[') != string::npos) || (token->find('_') != string::npos)) {
-		HTp current = token->getNextToken();
-		while (current) {
-			if (!current->isData()) {
-				current = current->getNextToken();
-				continue;
-			}
-			if (current->isNull()) {
-				current = current->getNextToken();
-				continue;
-			}
-			if (current->isRest()) {
-				break;
-			}
-			if (current->find("_") != string::npos) {
-				current->setText(current->getText() + "|");
-			} else if (current->find("]") != string::npos) {
-				current->setText(current->getText() + "|");
-				break;
-			}
-			current = current->getNextToken();
+string Tool_tandeminfo::checkForInstrumentInfo(const string& tok) {
+	HumRegex hre;
+
+	if (hre.search(tok, "^(m|o)?I\"(.*)$")) {
+		string modori = hre.getMatch(1);
+		string name = hre.getMatch(2);
+		string output = "text to display in fromt of staff on first system (usually instrument name): \"";
+		output += name;
+		output += "\"";
+		if (modori == "o") {
+			output += " (original)";
+		} else if (modori == "m") {
+			output += " (modern)";
+		}
+		if (hre.search(tok, "\\\\n")) {
+			output += ", \"\\n\" means a line break";
+		}
+		return output;
+	}
+
+	if (hre.search(tok, "^(m|o)?I'(.*)$")) {
+		string modori = hre.getMatch(1);
+		string abbr = hre.getMatch(2);
+		string output = "text to display in front of staff on secondary systems (usually instrument abbreviation): \"";
+		output += abbr;
+		output += "\"";
+		if (modori == "o") {
+			output += " (original)";
+		} else if (modori == "m") {
+			output += " (modern)";
 		}
+		if (hre.search(tok, "\\\\n")) {
+			output += ", \"\\n\" means a line break";
+		}
+		return output;
+	}
+
+
+	if (hre.search(tok, "^(m|o)?IC([^\\s]*)$")) {
+		string modori = hre.getMatch(1);
+		string iclass = hre.getMatch(2);
+		bool andy = false;
+		bool ory  = false;
+		vector<string> iclasses;
+		string tok2 = tok;
+		hre.replaceDestructive(tok2, "", "IC", "g");
+		if (hre.search(tok2, "&")) {
+			hre.split(iclasses, tok2, "&+");
+			andy = true;
+		} else if (hre.search(tok2, "\\|")) {
+			hre.split(iclasses, tok2, "\\++");
+			ory = true;
+		} else {
+			iclasses.push_back(tok2);
+		}
+		string output;
+		if (modori == "o") {
+			output += "(original) ";
+		} else if (modori == "m") {
+			output += "(modern) ";
+		}
+		output += "instrument class";
+		if (iclasses.size() != 1) {
+			output += "es";
+		}
+		output += ":";
+		for (int i=0; i<(int)iclasses.size(); i++) {
+			output += " ";
+			output += "<span class='tandem'>";
+			output += iclasses[i];
+			output += "</span>";
+			HumInstrument inst;
+			inst.setHumdrum(iclasses[i]);
+			string name;
+			if (iclasses[i] == "bras") {
+				name = "brass";
+			} else if (iclasses[i] == "idio") {
+				name = "percussion";
+			} else if (iclasses[i] == "klav") {
+				name = "keyboards";
+			} else if (iclasses[i] == "str") {
+				name = "strings";
+			} else if (iclasses[i] == "vox") {
+				name = "voices";
+			} else if (iclasses[i] == "ww") {
+				name = "woodwinds";
+			} else if (iclasses[i] != "") {
+				name = "unknown";
+			}
+			if (!name.empty()) {
+				output += "=\"" + name + "\"";
+			}
+			if (i < (int)iclasses.size() - 1) {
+				if (andy) {
+					output += " and ";
+				} else if (ory) {
+					output += " or ";
+				}
+			}
+		}
+		if (modori == "o") {
+			output += " (original)";
+		} else if (modori == "m") {
+			output += " (modern)";
+		}
+		return output;
 	}
-}
 
 
+	if (hre.search(tok, "^(m|o)?IG([^\\s]*)$")) {
+		string modori = hre.getMatch(1);
+		string group = hre.getMatch(2);
+		bool andy = false;
+		bool ory  = false;
+		vector<string> groups;
+		string tok2 = tok;
+		hre.replaceDestructive(tok2, "", "IG", "g");
+		if (hre.search(tok2, "&")) {
+			hre.split(groups, tok2, "&+");
+			andy = true;
+		} else if (hre.search(tok2, "\\|")) {
+			hre.split(groups, tok2, "\\++");
+			ory = true;
+		} else {
+			groups.push_back(tok2);
+		}
+		string output;
+		if (modori == "o") {
+			output += "(original) ";
+		} else if (modori == "m") {
+			output += "(modern) ";
+		}
+		output += "instrument group";
+		if (groups.size() != 1) {
+			output += "s";
+		}
+		output += ":";
+		for (int i=0; i<(int)groups.size(); i++) {
+			output += " ";
+			output += groups[i];
+			HumInstrument inst;
+			inst.setHumdrum(groups[i]);
+			string name;
+			if (groups[i] == "acmp") {
+				name = "=accompaniment";
+			} else if (groups[i] == "solo") {
+				name = "=solo";
+			} else if (groups[i] == "cont") {
+				name = "=basso-continuo";
+			} else if (groups[i] == "ripn") {
+				name = "=ripieno";
+			} else if (groups[i] == "conc") {
+				name = "=concertino";
+			} else if (groups[i] != "") {
+				name = "=unknown";
+			}
+			if (!name.empty()) {
+				output += "=\"" + name + "\"";
+			}
+			if (i < (int)groups.size() - 1) {
+				if (andy) {
+					output += " and ";
+				} else if (ory) {
+					output += " or ";
+				}
+			}
+		}
+		if (modori == "o") {
+			output += " (original)";
+		} else if (modori == "m") {
+			output += " (modern)";
+		}
+		return output;
+	}
 
+	if (hre.search(tok, "^(m|o)?I#(\\d+)$")) {
+		string modori = hre.getMatch(1);
+		string number = hre.getMatch(2);
+		string output = "sub-instrument number: ";
+		output += number;
+		if (modori == "o") {
+			output += " (original)";
+		} else if (modori == "m") {
+			output += " (modern)";
+		}
+		return output;
+	}
 
+	if (hre.search(tok, "^(m|o)?I([a-z][a-zA-Z0-9_|&-]+)$")) {
+		string modori = hre.getMatch(1);
+		string code = hre.getMatch(2);
+		bool andy = false;
+		bool ory  = false;
+		vector<string> codes;
+		string tok2 = tok;
+		hre.replaceDestructive(tok2, "", "I", "g");
+		if (hre.search(tok2, "&")) {
+			hre.split(codes, tok2, "&+");
+			andy = true;
+		} else if (hre.search(tok2, "\\|")) {
+			hre.split(codes, tok2, "\\++");
+			ory = true;
+		} else {
+			codes.push_back(tok2);
+		}
+		string output;
+		if (modori == "o") {
+			output += "(original) ";
+		} else if (modori == "m") {
+			output += "(modern) ";
+		}
+		output += "instrument code";
+		if (codes.size() != 1) {
+			output += "s";
+		}
+		output += ":";
+		for (int i=0; i<(int)codes.size(); i++) {
+			output += " <span class='tandem'>";
+			output += codes[i];
+			output += "</span>";
+			HumInstrument inst;
+			inst.setHumdrum(codes[i]);
+			string text = inst.getName();
+			if (!text.empty()) {
+				output += "= \"" + text + "\"";
+			} else {
+				output += "= unknown code";
+			}
+			output += "";
+			if (i < (int)codes.size() - 1) {
+				if (andy) {
+					output += " and ";
+				} else if (ory) {
+					output += " or ";
+				}
+			}
+		}
 
-/////////////////////////////////
-//
-// Tool_gridtest::Tool_tabber -- Set the recognized options for the tool.
-//
+		return output;
+	}
 
-Tool_tabber::Tool_tabber(void) {
-	// do nothing for now.
-	define("r|remove=b",    "remove any extra tabs");
+	return m_unknown;
 }
 
 
 
-///////////////////////////////
+//////////////////////////////
 //
-// Tool_tabber::run -- Primary interfaces to the tool.
+// Tool_tandeminfo::checkForKeySignature -- Standard Humdrum Toolkit interpretations.
+//     Extended key signatures are possible (and detected by this function),
+//     but typically the standard ones are in circle-of-fifths orderings.
+//     This function also allows double sharps/flats in the key signature
+//     which are very uncommon in real music.  Standard key signatures:
+//
+//     *k[f#c#g#d#a#e#b#]
+//     *k[f#c#g#d#a#e#]
+//     *k[f#c#g#d#a#]
+//     *k[f#c#g#d#]
+//     *k[f#c#g#]
+//     *k[f#c#]
+//     *k[f#]
+//     *k[]
+//     *k[b-]
+//     *k[b-e-]
+//     *k[b-e-a-]
+//     *k[b-e-a-d-]
+//     *k[b-e-a-d-g-]
+//     *k[b-e-a-d-g-c-]
+//     *k[b-e-a-d-g-c-f-]
 //
 
-bool Tool_tabber::run(HumdrumFileSet& infiles) {
-	bool status = true;
-	for (int i=0; i<infiles.getCount(); i++) {
-		status &= run(infiles[i]);
+string Tool_tandeminfo::checkForKeySignature(const string& tok) {
+
+	// visual styling interpretations for key signatures:
+	if (tok == "kcancel") {
+		return "show cancellation naturals when changing key signatures";
+	}
+	if (tok == "Xkcancel") {
+		return "do not show cancellation naturals when changing key signatures (default)";
 	}
-	return status;
-}
 
+	if (tok == "k[]") {
+		return "key signature: no sharps or flats";
+	}
+	if (tok == "ok[]") {
+		return "original key signature: no sharps or flats";
+	}
+	if (tok == "mk[]") {
+		return "modern key signature: no sharps or flats";
+	}
 
-bool Tool_tabber::run(const string& indata, ostream& out) {
-	HumdrumFile infile(indata);
-	return run(infile, out);
-}
+	HumRegex hre;
+	string modori;
+	if (hre.search(tok, "^([m|o])k\\[")) {
+		modori = hre.getMatch(1);
+	}
+
+	if (hre.search(tok, "^(?:m|o)?k\\[(([a-gA-G]+[n#-]{1,2})+)\\]$")) {
+		string modori;
+		string pcs = hre.getMatch(1);
+		bool standardQ = false;
+		if (pcs == "f#") {
+			standardQ = true;
+		} else if (pcs == "b-") {
+			standardQ = true;
+		} else if (pcs == "f#c#") {
+			standardQ = true;
+		} else if (pcs == "b-e-") {
+			standardQ = true;
+		} else if (pcs == "f#c#g#") {
+			standardQ = true;
+		} else if (pcs == "b-e-a-") {
+			standardQ = true;
+		} else if (pcs == "f#c#g#d#") {
+			standardQ = true;
+		} else if (pcs == "b-e-a-d-") {
+			standardQ = true;
+		} else if (pcs == "f#c#g#d#a#") {
+			standardQ = true;
+		} else if (pcs == "b-e-a-d-g-") {
+			standardQ = true;
+		} else if (pcs == "f#c#g#d#a#e#") {
+			standardQ = true;
+		} else if (pcs == "b-e-a-d-g-c-") {
+			standardQ = true;
+		} else if (pcs == "f#c#g#d#a#e#b#") {
+			standardQ = true;
+		} else if (pcs == "b-e-a-d-g-c-f-") {
+			standardQ = true;
+		}
 
+		string output;
+		if (modori == "m") {
+			output = "modern ";
+		} else if (modori == "o") {
+			output = "original ";
+		}
+		output += "key signature";
+		if (!standardQ) {
+			output += " (non-standard)";
+		}
+		output += ": ";
+		int flats = 0;
+		int sharps = 0;
+		int naturals = 0;
+		int doubleflats = 0;
+		int doublesharps = 0;
+		vector<string> accidentals;
+		hre.split(accidentals, pcs, "[a-gA-G]");
+		for (int i=0; i<(int)accidentals.size(); i++) {
+			if (accidentals[i] == "##") {
+				doublesharps++;
+			} else if (accidentals[i] == "--") {
+				doubleflats++;
+			} else if (accidentals[i] == "#") {
+				sharps++;
+			} else if (accidentals[i] == "-") {
+				flats++;
+			} else if (accidentals[i] == "n") {
+				naturals++;
+			}
+		}
 
-bool Tool_tabber::run(HumdrumFile& infile, ostream& out) {
-	bool status = run(infile);
-	out << m_free_text.str();
-	return status;
-}
+		bool foundQ = false;
+		if (sharps) {
+			if (foundQ) {
+				output += ", ";
+			}
+			foundQ = true;
+			if (sharps == 1) {
+				output += "1 sharp";
+			} else {
+				output += to_string(sharps) + " sharps";
+			}
+		}
 
+		if (flats) {
+			if (foundQ) {
+				output += ", ";
+			}
+			foundQ = true;
+			if (flats == 1) {
+				output += "1 flat";
+			} else {
+				output += to_string(flats) + " flats";
+			}
+		}
 
-bool Tool_tabber::run(HumdrumFile& infile) {
-   initialize(infile);
-	processFile(infile);
-	return true;
+		if (naturals) {
+			if (foundQ) {
+				output += ", ";
+			}
+			foundQ = true;
+			if (naturals == 1) {
+				output += "1 natural";
+			} else {
+				output += to_string(naturals) + " naturals";
+			}
+		}
+
+		if (doublesharps) {
+			if (foundQ) {
+				output += ", ";
+			}
+			foundQ = true;
+			if (doublesharps == 1) {
+				output += "1 double sharp";
+			} else {
+				output += to_string(doublesharps) + " double sharps";
+			}
+		}
+
+		if (doubleflats) {
+			if (foundQ) {
+				output += ", ";
+			}
+			foundQ = true;
+			if (doubleflats == 1) {
+				output += "1 double flat";
+			} else {
+				output += to_string(doubleflats) + " double flats";
+			}
+		}
+
+		return output;
+	}
+	return m_unknown;
 }
 
 
 
 //////////////////////////////
 //
-// Tool_tabber::initialize --
+// Tool_tandeminfo::checkForKeyDesignation -- Standard Humdrum Toolkit interpretations, plus
+//     modal extensions by Brett Arden.  Typically only used in **kern data.
 //
 
-void Tool_tabber::initialize(HumdrumFile& infile) {
-	// do nothing for now
-}
+string Tool_tandeminfo::checkForKeyDesignation(const string& tok) {
+	HumRegex hre;
+	if (tok == "?:") {
+		return "key designation, unknown/unassigned key";
+	}
+	if (hre.search(tok, "^([a-gA-G])([-#]*):(ion|dor|phr|lyd|mix|aeo|loc)?(-hypo|-auth)?$")) {
+		string tonic = hre.getMatch(1);
+		string accid = hre.getMatch(2);
+		string mode  = hre.getMatch(3);
+		string older = hre.getMatch(4);
+		bool isUpper = isupper(tonic[0]);
+		string output = "key designation: ";
+		if (mode.empty()) {
+			output += toupper(tonic[0]);
+
+			if (accid == "") {
+				// do nothing
+			} else if (accid == "#") {
+				output += "-sharp";
+			} else if (accid == "-") {
+				output += "-flat";
+			} else if (accid == "##") {
+				output += "-double-sharp";
+			} else if (accid == "--") {
+				output += "-double-flat";
+			} else if (accid == "###") {
+				output += "-triple-sharp";
+			} else if (accid == "---") {
+				output += "-triple-flat";
+			} else {
+				return m_unknown;
+			}
 
+			if (isUpper) {
+				output += " major";
+			} else {
+				output += " minor";
+			}
+			return output;
+		} else {
+			// Modal key
+			if (isUpper && ((mode == "dor") || (mode == "phr") || (mode == "aeo") || (mode == "loc"))) {
+				// need a lower-case letter for these modes (minor third above tonic)
+				return m_unknown;
+			}
+			if ((!isUpper) && ((mode == "ion") || (mode == "lyd") || (mode == "mix"))) {
+				// need an upper-case letter for these modes (major third above tonic)
+				return m_unknown;
+			}
+			output += toupper(tonic[0]);
 
+			if (accid == "") {
+				// do nothing
+			} else if (accid == "#") {
+				output += "-sharp";
+			} else if (accid == "-") {
+				output += "-flat";
+			} else if (accid == "##") {
+				output += "-double-sharp";
+			} else if (accid == "--") {
+				output += "-double-flat";
+			} else if (accid == "###") {
+				output += "-triple-sharp";
+			} else if (accid == "---") {
+				output += "-triple-flat";
+			} else {
+				return m_unknown;
+			}
+
+			if (mode == "ion") {
+				output += " ionian";
+			} else if (mode == "dor") {
+				output += " dorian";
+			} else if (mode == "phr") {
+				output += " phrygian";
+			} else if (mode == "lyd") {
+				output += " lydian";
+			} else if (mode == "mix") {
+				output += " mixolydian";
+			} else if (mode == "aeo") {
+				output += " aeolian";
+			} else if (mode == "loc") {
+				output += " locrian";
+			} else {
+				return m_unknown;
+			}
 
-//////////////////////////////
-//
-// Tool_tabber::processFile --
-//
+			if (!older.empty()) {
+				if (older == "-plag") {
+					output += " (plagal)";
+				} else if (older == "-auth") {
+					output += " (authentic)";
+				} else {
+					output += " (unknown mode type: should be -auth (authentic), or -plag (plagal) for hypo modes)";
+				}
+			}
 
-void Tool_tabber::processFile(HumdrumFile& infile) {
-	if (getBoolean("remove")) {
-		infile.removeExtraTabs();
-	} else {
-		infile.addExtraTabs();
+			return output;
+		}
 	}
-	infile.createLinesFromTokens();
+
+	return m_unknown;
 }
 
 

From d22b1aef4fbbe395225d38f462f01bbfe00f6820 Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Sat, 7 Sep 2024 08:33:17 -0700
Subject: [PATCH 04/11] Fix for issue
 https://github.com/rism-digital/verovio/issues/3766

---
 src/iohumdrum.cpp | 332 ++++++++++++++++++++++++++++++----------------
 1 file changed, 216 insertions(+), 116 deletions(-)

diff --git a/src/iohumdrum.cpp b/src/iohumdrum.cpp
index 7eb039bc283..5feedb4b297 100644
--- a/src/iohumdrum.cpp
+++ b/src/iohumdrum.cpp
@@ -499,46 +499,46 @@ namespace humaux {
 
     ostream &StaffStateVariables::print(ostream &out, const std::string &prefix)
     {
-        out << prefix << "ADDRESS ==================  " << (long long)this << endl;
-        out << prefix << "verse                    =  " << verse << endl;
-        out << prefix << "suppress_tuplet_number   =  " << suppress_tuplet_number << endl;
-        out << prefix << "suppress_tuplet_bracket  =  " << suppress_tuplet_bracket << endl;
-        out << prefix << "suppress_articulations   =  " << suppress_articulations << endl;
-        out << prefix << "tremolo                  =  " << tremolo << endl;
+        out << prefix << "ADDRESS ==================  " << (long long)this << std::endl;
+        out << prefix << "verse                    =  " << verse << std::endl;
+        out << prefix << "suppress_tuplet_number   =  " << suppress_tuplet_number << std::endl;
+        out << prefix << "suppress_tuplet_bracket  =  " << suppress_tuplet_bracket << std::endl;
+        out << prefix << "suppress_articulations   =  " << suppress_articulations << std::endl;
+        out << prefix << "tremolo                  =  " << tremolo << std::endl;
         // std::vector<bool> cue_size;
         // std::vector<char> stem_type;
         // std::vector<char> stem_visible;
-        out << prefix << "ligature_recta           =  " << ligature_recta << endl;
-        out << prefix << "ligature_obliqua         =  " << ligature_obliqua << endl;
-        out << prefix << "last_clef                =  " << last_clef << endl;
-        out << prefix << "acclev                   =  " << acclev << endl;
-        out << prefix << "righthalfstem            =  " << righthalfstem << endl;
+        out << prefix << "ligature_recta           =  " << ligature_recta << std::endl;
+        out << prefix << "ligature_obliqua         =  " << ligature_obliqua << std::endl;
+        out << prefix << "last_clef                =  " << last_clef << std::endl;
+        out << prefix << "acclev                   =  " << acclev << std::endl;
+        out << prefix << "righthalfstem            =  " << righthalfstem << std::endl;
         // Note *ottavanotestart;
         // Note *ottavanoteend;
-        out << prefix << "ottavaendtimestamp       =  " << ottavaendtimestamp << endl;
+        out << prefix << "ottavaendtimestamp       =  " << ottavaendtimestamp << std::endl;
         // Measure *ottavameasure;
         // Note *ottavadownnotestart;
         // Note *ottavadownnoteend;
-        out << prefix << "ottavadownendtimestamp   =  " << ottavadownendtimestamp << endl;
+        out << prefix << "ottavadownendtimestamp   =  " << ottavadownendtimestamp << std::endl;
         // Measure *ottavadownmeasure;
         // Note *ottava2notestart;
         // Note *ottava2noteend;
-        out << prefix << "ottava2endtimestamp      =  " << ottava2endtimestamp << endl;
+        out << prefix << "ottava2endtimestamp      =  " << ottava2endtimestamp << std::endl;
         // Measure *ottava2measure;
         // Note *ottava2downnotestart;
         // Note *ottava2downnoteend;
-        out << prefix << "ottava2downendtimestamp  =  " << ottava2downendtimestamp << endl;
+        out << prefix << "ottava2downendtimestamp  =  " << ottava2downendtimestamp << std::endl;
         // Measure *ottava2downmeasure;
-        out << prefix << "meter_top                =  " << meter_top << endl;
-        out << prefix << "meter_bottom             =  " << meter_bottom << endl;
+        out << prefix << "meter_top                =  " << meter_top << std::endl;
+        out << prefix << "meter_bottom             =  " << meter_bottom << std::endl;
         // std::list<humaux::HumdrumTie> ties;
-        out << prefix << "m_dynampos               =  " << m_dynampos << endl;
-        out << prefix << "m_dynamstaffadj          =  " << m_dynamstaffadj << endl;
-        out << prefix << "m_dynamposdefined        =  " << m_dynamposdefined << endl;
-        out << prefix << "auto_custos              =  " << auto_custos << endl;
-        out << prefix << "suppress_manual_custos   =  " << suppress_manual_custos << endl;
-        out << prefix << "mensuration_type         =  " << mensuration_type << endl;
-        out << prefix << "join                     =  " << join << endl;
+        out << prefix << "m_dynampos               =  " << m_dynampos << std::endl;
+        out << prefix << "m_dynamstaffadj          =  " << m_dynamstaffadj << std::endl;
+        out << prefix << "m_dynamposdefined        =  " << m_dynamposdefined << std::endl;
+        out << prefix << "auto_custos              =  " << auto_custos << std::endl;
+        out << prefix << "suppress_manual_custos   =  " << suppress_manual_custos << std::endl;
+        out << prefix << "mensuration_type         =  " << mensuration_type << std::endl;
+        out << prefix << "join                     =  " << join << std::endl;
 
         return out;
     }
@@ -982,7 +982,7 @@ bool HumdrumInput::convertHumdrum()
     processMeiOptions(infile);
 
     if (m_debug) {
-        cout << GetMeiString();
+        std::cout << GetMeiString();
     }
 
     // If the document has <pb/> and <sb/> elements you can call:
@@ -2011,7 +2011,7 @@ void HumdrumInput::processHangingTieStart(humaux::HumdrumTie &tieinfo)
     int subindex = tieinfo.getStartSubindex();
     Measure *measure = tieinfo.getStartMeasure();
     if (measure == NULL) {
-        cerr << "Problem with start measure being NULL" << endl;
+        LogWarning("In HumdrumInput::processHangingTieStart: Start measure is NULL");
         return;
     }
     // int metercount = tieinfo.getMeterTop();
@@ -2668,7 +2668,9 @@ void HumdrumInput::parseEmbeddedOptions(Doc *doc)
                 continue;
             }
             if (value.empty()) {
-                cerr << "Warning: value is empty for parameter " << key << endl;
+                std::stringstream warning;
+                warning << "In HumdrumInput::parseEmbeddedOptions: value is empty for parameter " << key;
+                LogWarning(warning.str().c_str());
                 continue;
             }
             inputparameters[pkey] = pvalue;
@@ -2687,7 +2689,9 @@ void HumdrumInput::parseEmbeddedOptions(Doc *doc)
                 std::string pkey = hre.getMatch(1);
                 std::string pvalue = hre.getMatch(2);
                 if (value.empty()) {
-                    cerr << "Warning: value is empty for parameter " << key << endl;
+                    std::stringstream warning;
+                    warning << "In HumdrumInput::parseEmbeddedOptions: Value is empty for parameter " << key;
+                    LogWarning(warning.str().c_str());
                     continue;
                 }
                 inputparameters[pkey] = pvalue;
@@ -2700,7 +2704,10 @@ void HumdrumInput::parseEmbeddedOptions(Doc *doc)
     for (auto inputoption : inputparameters) {
         auto entry = optionlist->find(inputoption.first);
         if (entry == optionlist->end()) {
-            cerr << "Warning: option " << inputoption.first << " is not recognized" << endl;
+            std::stringstream warning;
+            warning << "In HumdrumInput::parseEmbeddedOptions: option ";
+            warning << inputoption.first << " is not recognized";
+            LogWarning(warning.str().c_str());
             continue;
         }
 
@@ -4954,8 +4961,13 @@ void HumdrumInput::createHumdrumVerbatimExtMeta(pugi::xml_node meiHead)
     pugi::xml_parse_result result = tmpdoc.load_string(xmldata.str().c_str());
     if (!result) {
         // some sort of error, so give up;
-        cerr << "ExtMeta parse error: " << result.description() << endl;
-        cerr << xmldata.str();
+        std::stringstream warning;
+        warning << "In HumdrumInput::createHumdrumVerbatimExtMeta: ExtMeta parse error: ";
+        warning << result.description();
+        LogWarning(warning.str().c_str());
+        std::stringstream warning2;
+        warning2 << "   xmldata string is: " << xmldata.str();
+        LogWarning(warning2.str().c_str());
         return;
     }
 
@@ -5879,8 +5891,8 @@ bool HumdrumInput::processStaffDecoration(const std::string &decoration)
         return false;
     }
     if ((0)) {
-        cerr << "INPUT DECORATION: " << decoration << endl;
-        cerr << "     PROCESSED:   " << d << endl;
+        std::cerr << "INPUT DECORATION: " << decoration << std::endl;
+        std::cerr << "     PROCESSED:   " << d << std::endl;
     }
 
     // Remove any staff numbers that are no longer present (or invalid):
@@ -6010,7 +6022,7 @@ bool HumdrumInput::processStaffDecoration(const std::string &decoration)
     if ((0)) {
         // print analysis:
         for (int i = 0; i < (int)d.size(); ++i) {
-            cerr << "D[" << i << "] =\t" << d[i] << " pairing: " << pairing[i] << endl;
+            std::cerr << "D[" << i << "] =\t" << d[i] << " pairing: " << pairing[i] << std::endl;
         }
     }
 
@@ -6238,13 +6250,13 @@ bool HumdrumInput::processStaffDecoration(const std::string &decoration)
     }
 
     if ((0)) {
-        cerr << "BAR GROUPS" << endl;
+        std::cerr << "BAR GROUPS" << std::endl;
         for (int i = 0; i < (int)bargroups.size(); ++i) {
-            cerr << "\tgroup_style=" << groupstyle[i] << "\tgroup = " << i << ":\t";
+            std::cerr << "\tgroup_style=" << groupstyle[i] << "\tgroup = " << i << ":\t";
             for (int j = 0; j < (int)bargroups[i].size(); j++) {
-                cerr << " " << bargroups[i][j];
+                std::cerr << " " << bargroups[i][j];
             }
-            cerr << endl;
+            std::cerr << std::endl;
         }
     }
 
@@ -6275,7 +6287,7 @@ bool HumdrumInput::processStaffDecoration(const std::string &decoration)
 
         for (int i = 0; i < (int)found.size(); ++i) {
             if (found[i] != 1) {
-                cerr << "I:" << i << "\t=\t" << found[i] << endl;
+                std::cerr << "I:" << i << "\t=\t" << found[i] << std::endl;
                 validQ = false;
                 break;
             }
@@ -6283,9 +6295,13 @@ bool HumdrumInput::processStaffDecoration(const std::string &decoration)
     }
 
     if (!validQ) {
-        cerr << "DECORATION IS INVALID " << decoration << endl;
+        std::stringstream warning;
+        warning << "In HumdrumInput::processStaffDecoration: Decoration is invalid: " << decoration;
+        LogWarning(warning.str().c_str());
         if (d != decoration) {
-            cerr << "\tSTAFF VERSION: " << d << endl;
+            std::stringstream warning;
+            warning << "In HumdrumInput::processStaffDecoration: Staff version: " << d;
+            LogWarning(warning.str().c_str());
         }
         StaffGrp *sg = new StaffGrp();
         setGroupSymbol(sg, staffGroupingSym_SYMBOL_bracket);
@@ -6890,7 +6906,7 @@ bool HumdrumInput::prepareFooter(
     }
     Object *detached = pgfoot->GetParent()->DetachChild(index);
     if (detached != pgfoot) {
-        std::cerr << "Detached element is not the pgHead" << std::endl;
+        LogWarning("In HumdrumInput::prepareFooter: Detached element is not the pgHead.");
         if (detached) {
             delete detached;
         }
@@ -6911,7 +6927,7 @@ bool HumdrumInput::prepareFooter(
     }
     detached = pgfoot2->GetParent()->DetachChild(index);
     if (detached != pgfoot2) {
-        std::cerr << "Detached element is not a pgFoot element" << std::endl;
+        LogWarning("In HumdrumInput::prepareFooter: Detached element is not a pgFoot element");
         if (detached) {
             delete detached;
         }
@@ -7065,7 +7081,7 @@ bool HumdrumInput::prepareHeader(
     }
     Object *detached = pghead->GetParent()->DetachChild(index);
     if (detached != pghead) {
-        std::cerr << "Detached element is not the pgHead" << std::endl;
+        LogWarning("In HumdrumInput::prepareHeader: Detached element is not the pgHead");
         if (detached) {
             delete detached;
         }
@@ -8874,7 +8890,10 @@ void HumdrumInput::setMensurationSymbol(
         }
     }
     else {
-        std::cerr << "Warning: do not understand mensuration " << metdata << std::endl;
+        std::stringstream warning;
+        warning << "In HumdrumInput::setMensurationSymbol: Problem parsing mensuration: ";
+        warning << metdata;
+        LogWarning(warning.str().c_str());
         return;
     }
 
@@ -8929,41 +8948,63 @@ void HumdrumInput::setMensurationSymbol(
             prolatio = stoi(num4);
         }
 
+        std::stringstream warning;
         switch (prolatio) {
             case 2: vrvmensur->SetProlatio(PROLATIO_2); break;
             case 3: vrvmensur->SetProlatio(PROLATIO_3); break;
             case 0: break;
-            default: cerr << "Warning: unknown prolation " << prolatio << " in " << mensurtok << endl;
+            default:
+                warning.str("");
+                warning << "In HumdrumInput::setMensurationSymbol: Unknown prolation ";
+                warning << prolatio << " in " << mensurtok;
+                LogWarning(warning.str().c_str());
         }
         switch (tempus) {
             case 2: vrvmensur->SetTempus(TEMPUS_2); break;
             case 3: vrvmensur->SetTempus(TEMPUS_3); break;
             case 0: break;
-            default: cerr << "Warning: unknown tempus " << tempus << " in " << mensurtok << endl;
+            default:
+                warning.str("");
+                warning << "In HumdrumInput::setMensurationSymbol: Unknown tempus ";
+                warning << tempus << " in " << mensurtok;
+                LogWarning(warning.str().c_str());
         }
         switch (modus) {
             case 2: vrvmensur->SetModusminor(MODUSMINOR_2); break;
             case 3: vrvmensur->SetModusminor(MODUSMINOR_3); break;
             case 0: break;
-            default: cerr << "Warning: unknown modus " << modus << " in " << mensurtok << endl;
+            default:
+                warning.str("");
+                warning << "In HumdrumInput::setMensurationSymbol: Unknown modus ";
+                warning << modus << " in " << mensurtok;
+                LogWarning(warning.str().c_str());
         }
         switch (maximodus) {
             case 2: vrvmensur->SetModusmaior(MODUSMAIOR_2); break;
             case 3: vrvmensur->SetModusmaior(MODUSMAIOR_3); break;
             case 0: break;
-            default: cerr << "Warning: unknown maximodus " << maximodus << " in " << mensurtok << endl;
+            default:
+                warning.str("");
+                warning << "In HumdrumInput::setMensurationSymbol: Unknown maximodus ";
+                warning << maximodus << " in " << mensurtok;
+                LogWarning(warning.str().c_str());
         }
     }
 
     std::vector<humaux::StaffStateVariables> &ss = m_staffstates;
 
     if (staffindex < 0) {
-        cerr << "Initialization problem, not setting mensuration information" << endl;
-        cerr << "STAFF INDEX = " << staffindex << endl;
+        std::stringstream warning;
+        warning << "In HumdrumInput::setMensrationSymbol: ";
+        warning << "Initialization problem, not setting mensuration information";
+        LogWarning(warning.str().c_str());
+        std::stringstream warning2;
+        warning2 << "   Staff index = " << staffindex;
+        LogWarning(warning2.str().c_str());
         return;
     }
     if (staffindex >= (int)ss.size()) {
-        cerr << "Problem with staff indexing in mensuration processing" << endl;
+        LogWarning("InHumdrumInput::setMensurationSymbol: Problem with staff indexing in mensuration processing");
         return;
     }
 
@@ -10147,7 +10188,10 @@ void HumdrumInput::storeStaffLayerTokensForMeasure(int startline, int endline)
             }
             staffindex = rkern[track];
             if (staffindex < 0) {
-                cerr << "STAFF INDEX PROBLEM FOR TRACK " << track << endl;
+                std::stringstream warning;
+                warning << "In HumdrumInput::storeStaffLayerTokensForMeasure:";
+                warning << "Staff inex problem for track " << track;
+                LogWarning(warning.str().c_str());
             }
             if ((int)lt[staffindex].size() < layerindex + 1) {
                 lt[staffindex].resize(lt[staffindex].size() + 1);
@@ -11153,7 +11197,10 @@ void HumdrumInput::addHarmFloatsForMeasure(int startline, int endline)
                 text->SetText(UTF8to32(*token));
             }
             else {
-                cerr << "Unknown type of harm data: " << datatype << endl;
+                std::stringstream warning;
+                warning << "In HumdrumInput::addHarmFloatsForMeasure: ";
+                warning << "Unknown type of harm data " << datatype;
+                LogWarning(warning.str().c_str());
                 continue;
             }
         }
@@ -12531,7 +12578,10 @@ void HumdrumInput::setMxHarmContent(Rend *rend, const std::string &content)
             }
         }
         else {
-            cerr << "should not get here with correct input " << content << endl;
+            std::stringstream warning;
+            warning << "In HumdrumInput::setMxHarmContent: ";
+            warning << "Should not get here if correct input: " << content;
+            LogWarning(warning.str().c_str());
         }
     }
 
@@ -12883,29 +12933,29 @@ void HumdrumInput::fixLargeTuplets(std::vector<humaux::HumdrumBeamAndTuplet> &tg
 
 void HumdrumInput::printGroupInfo(const std::vector<humaux::HumdrumBeamAndTuplet> &tg)
 {
-    cerr << "TOK\t\tGRP\tBRAK\tNUM\tNBASE\tNSCAL\tBSTART\tBEND";
-    cerr << "\tGBST\tGBEND\tTSTART\tTEND\tFORCE\tPRIORITY\n";
+    std::cerr << "TOK\t\tGRP\tBRAK\tNUM\tNBASE\tNSCAL\tBSTART\tBEND";
+    std::cerr << "\tGBST\tGBEND\tTSTART\tTEND\tFORCE\tPRIORITY\n";
     for (int i = 0; i < (int)tg.size(); ++i) {
-        cerr << tg.at(i).token << "\t";
+        std::cerr << tg.at(i).token << "\t";
         if (tg.at(i).token && (tg.at(i).token->size() < 8)) {
-            cerr << "\t";
+            std::cerr << "\t";
         }
-        cerr << tg.at(i).group << "\t";
-        cerr << tg.at(i).bracket << "\t";
-        cerr << tg.at(i).num << "\t";
-        cerr << tg.at(i).numbase << "\t";
-        cerr << tg.at(i).numscale << "\t";
-        cerr << tg.at(i).beamstart << "\t";
-        cerr << tg.at(i).beamend << "\t";
-        cerr << tg.at(i).gbeamstart << "\t";
-        cerr << tg.at(i).gbeamend << "\t";
-        cerr << "TS:" << tg.at(i).tupletstart << "\t";
-        cerr << "TE:" << tg.at(i).tupletend << "\t";
-        cerr << tg.at(i).force << "\t";
-        cerr << tg.at(i).priority;
-        cerr << endl;
+        std::cerr << tg.at(i).group << "\t";
+        std::cerr << tg.at(i).bracket << "\t";
+        std::cerr << tg.at(i).num << "\t";
+        std::cerr << tg.at(i).numbase << "\t";
+        std::cerr << tg.at(i).numscale << "\t";
+        std::cerr << tg.at(i).beamstart << "\t";
+        std::cerr << tg.at(i).beamend << "\t";
+        std::cerr << tg.at(i).gbeamstart << "\t";
+        std::cerr << tg.at(i).gbeamend << "\t";
+        std::cerr << "TS:" << tg.at(i).tupletstart << "\t";
+        std::cerr << "TE:" << tg.at(i).tupletend << "\t";
+        std::cerr << tg.at(i).force << "\t";
+        std::cerr << tg.at(i).priority;
+        std::cerr << std::endl;
     }
-    cerr << "============================================" << endl;
+    std::cerr << "============================================" << std::endl;
 }
 
 //////////////////////////////
@@ -13197,7 +13247,10 @@ bool HumdrumInput::checkForTremolo(
     int beams = -log(duration.getFloat()) / log(2.0);
     if (beams <= 0) {
         // something went wrong calculating durations.
-        cerr << "PROBLEM WITH TREMOLO2 CALCULATION: " << beams << endl;
+        std::stringstream warning;
+        warning << "In HumdrumInput::checkForTremolo: ";
+        warning << "Problem with tremolo2 calculation, beam count: " << beams;
+        LogWarning(warning.str().c_str());
         return false;
     }
 
@@ -13238,7 +13291,7 @@ bool HumdrumInput::checkForInvisibleBeam(
     int beamnum = tgs.at(layerindex).beamstart;
     for (int i = layerindex; i < (int)tgs.size(); ++i) {
         if (!tgs.at(i).token) {
-            cerr << "WARNING in checkForInvisibleBeam: NULL token\n";
+            LogWarning("In HumdrumInput::checkForInvisibleBeam: Encountered NULL token");
             return false;
         }
         int len = (int)tgs.at(i).token->size();
@@ -13966,9 +14019,13 @@ bool HumdrumInput::fillContentsOfLayer(int track, int startline, int endline, in
                     }
                 }
                 else {
-                    std::cerr << "Strange error for adding rest " << trest << std::endl;
-                    std::cerr << "LINE: " << trest->getLineNumber() << ", FIELD: " << trest->getFieldNumber()
-                              << std::endl;
+                    std::stringstream warning;
+                    warning << "In HumdrumInput::fillContentsOfLayer: ";
+                    warning << "Strange error when adding rest " << trest;
+                    LogWarning(warning.str().c_str());
+                    std::stringstream warning2;
+                    warning2 << "   Line: " << trest->getLineNumber() << ", Field: " << trest->getFieldNumber();
+                    LogWarning(warning2.str().c_str());
                 }
             }
 
@@ -16035,7 +16092,7 @@ void HumdrumInput::convertMensuralToken(
             }
         }
         else {
-            std::cerr << "WARNING: unmatched ligature ending" << std::endl;
+            LogWarning("In HumdrumInput::convertMensuralToken: Unmatched ligature ending");
         }
     }
     if (roff) {
@@ -17899,7 +17956,10 @@ void HumdrumInput::processLinkedDirection(int index, hum::HTp token, int staffin
             setLocationId(tempo, dirtok);
         }
         else {
-            cerr << "DIRTOK FOR " << token << " IS EMPTY " << endl;
+            std::stringstream warning;
+            warning << "In HumdrumInput::processLinkedDirection: dirtok for ";
+            warning << token << " is empty";
+            LogWarning(warning.str().c_str());
         }
         hum::HumNum tstamp = getMeasureTstamp(token, staffindex);
         if (token->isMensLike()) {
@@ -17944,7 +18004,10 @@ void HumdrumInput::processLinkedDirection(int index, hum::HTp token, int staffin
             setLocationId(dir, dirtok);
         }
         else {
-            cerr << "DIRTOK FOR " << token << " IS EMPTY " << endl;
+            std::stringstream warning;
+            warning << "In HumdrumInput::processLinkedDirection: (2) dirtok for ";
+            warning << token << " is empty";
+            LogWarning(warning.str().c_str());
         }
 
         if (token->isMensLike()) {
@@ -19884,7 +19947,7 @@ template <class ELEMENT> void HumdrumInput::setAttachmentType(ELEMENT *element,
 template <class ELEMENT> void HumdrumInput::attachToToken(ELEMENT *element, hum::HTp token)
 {
     if (token->isNull()) {
-        cerr << "ERROR: Cannot input null tokens into HumdrumInput::attachToToken() function." << endl;
+        LogWarning("In HumdrumInput::attachToToken: Cannot input null tokens into this function");
         return;
     }
     if (token->isChord()) {
@@ -22709,9 +22772,9 @@ void HumdrumInput::analyzeLayerBeams(
 
     if (m_debug) {
         for (int i = 0; i < (int)beamstate.size(); ++i) {
-            cerr << layerdata[i] << "(" << beamstate[i] << ")  ";
+            std::cerr << layerdata[i] << "(" << beamstate[i] << ")  ";
         }
-        cerr << endl;
+        std::cerr << std::endl;
     }
 
     // int beamstartindex = -1;
@@ -23152,10 +23215,14 @@ Beam *HumdrumInput::insertGBeam(
 void HumdrumInput::removeBeam(std::vector<std::string> &elements, std::vector<void *> &pointers)
 {
     if (elements.back() != "beam") {
-        cerr << "ERROR REMOVING BEAM" << endl;
-        cerr << "ELEMENT STACK:" << endl;
+        LogWarning("In HumdrumInput::removeBeam: Error removing beam");
+        std::stringstream warning;
+        LogWarning("   Element stack: ");
         for (int i = (int)elements.size() - 1; i >= 0; i--) {
-            cerr << i << ":\t" << elements[i] << endl;
+            std::stringstream warning;
+            warning.str("");
+            warning << "      " << i << ":\t" << elements[i];
+            LogWarning(warning.str().c_str());
         }
         return;
     }
@@ -23170,10 +23237,13 @@ void HumdrumInput::removeBeam(std::vector<std::string> &elements, std::vector<vo
 void HumdrumInput::removeGBeam(std::vector<std::string> &elements, std::vector<void *> &pointers)
 {
     if (elements.back() != "gbeam") {
-        cerr << "ERROR REMOVING GBEAM" << endl;
-        cerr << "ELEMENT STACK:" << endl;
+        LogWarning("In HumdrumInput::removeGBeam: Error removing gbeam");
+        LogWarning("   Element stack: ");
         for (int i = (int)elements.size() - 1; i >= 0; i--) {
-            cerr << i << ":\t" << elements[i] << endl;
+            std::stringstream warning;
+            warning.str("");
+            warning << "      " << i << ":\t" << elements[i];
+            LogWarning(warning.str().c_str());
         }
         return;
     }
@@ -23188,11 +23258,16 @@ void HumdrumInput::removeGBeam(std::vector<std::string> &elements, std::vector<v
 void HumdrumInput::removeTuplet(std::vector<std::string> &elements, std::vector<void *> &pointers)
 {
     if (elements.back() != "tuplet") {
-        cerr << "ERROR REMOVING TUPLET" << endl;
-        cerr << "ELEMENT BACK IS " << elements.back() << endl;
-        cerr << "ELEMENT STACK:" << endl;
+        LogWarning("In HumdrumInput::removeTuplet: Error removing tuplet");
+        std::stringstream warning;
+        warning << "   Last element is: " << elements.back();
+        LogWarning(warning.str().c_str());
+        LogWarning("   Element stack: ");
         for (int i = (int)elements.size() - 1; i >= 0; i--) {
-            cerr << i << ":\t" << elements[i] << endl;
+            std::stringstream warning;
+            warning.str("");
+            warning << "      " << i << ":\t" << elements[i];
+            LogWarning(warning.str().c_str());
         }
         return;
     }
@@ -24065,7 +24140,7 @@ void HumdrumInput::mergeTupletsCuttingBeam(std::vector<humaux::HumdrumBeamAndTup
                 scaleadj.at(j) = 2;
                 break;
             }
-            cerr << "SOMETHING STRANGE HAPPENED HERE" << endl;
+            LogWarning("In HumdrumInput::mergeTupletsCuttingBeam: Something strange happened");
         }
         target = newtg.at(i + 1)->tupletstart;
         scaleadj.at(i) = 2;
@@ -24082,7 +24157,7 @@ void HumdrumInput::mergeTupletsCuttingBeam(std::vector<humaux::HumdrumBeamAndTup
                 scaleadj.at(j) = 2;
                 break;
             }
-            cerr << "SOMETHING STRANGE HAPPENED HERE2" << endl;
+            LogWarning("In HumdrumInput::mergeTupletsCuttingBeam: Something strange happened (2)");
         }
 
         newtg.at(i)->tupletend = 0;
@@ -24110,11 +24185,11 @@ void HumdrumInput::mergeTupletsCuttingBeam(std::vector<humaux::HumdrumBeamAndTup
     }
 
     if (m_debug) {
-        cerr << "INDEX\tBEAM\tTSTART\tTEND\tNUM\tNUMBASE\n";
+        std::cerr << "INDEX\tBEAM\tTSTART\tTEND\tNUM\tNUMBASE\n";
         for (int i = 0; i < (int)newtg.size(); ++i) {
-            cerr << "I " << i << ":\t" << inbeam.at(i) << "\t" << newtg.at(i)->tupletstart << "\t"
-                 << newtg.at(i)->tupletend << "\t" << newtg.at(i)->num << "\t" << newtg.at(i)->numbase
-                 << "\tSA=" << scaleadj.at(i) << endl;
+            std::cerr << "I " << i << ":\t" << inbeam.at(i) << "\t" << newtg.at(i)->tupletstart << "\t"
+                      << newtg.at(i)->tupletend << "\t" << newtg.at(i)->num << "\t" << newtg.at(i)->numbase
+                      << "\tSA=" << scaleadj.at(i) << std::endl;
         }
     }
 
@@ -25521,6 +25596,7 @@ void HumdrumInput::adjustChordNoteDuration(Note *note, hum::HumNum hdur, int mei
 
 void HumdrumInput::setNoteMeiDur(Note *note, int meidur)
 {
+    std::stringstream warning;
     switch (meidur) {
         case -1: note->SetDur(DURATION_maxima); break;
         case 0: note->SetDur(DURATION_long); break;
@@ -25536,7 +25612,9 @@ void HumdrumInput::setNoteMeiDur(Note *note, int meidur)
         case 10: note->SetDur(DURATION_256); break;
         case 11: note->SetDur(DURATION_512); break;
         case 12: note->SetDur(DURATION_1024); break;
-        default: cerr << "UNKNOWN MEI DUR: " << meidur << endl;
+        default:
+            warning << "In HumdrumInput::setNoteMeiDur: Unknown MEI @dur: " << meidur;
+            LogWarning(warning.str().c_str());
     }
 }
 
@@ -25718,7 +25796,10 @@ void HumdrumInput::appendElement(const std::vector<std::string> &name, const std
         appendElement((Ligature *)pointers.back(), child);
     }
     else {
-        std::cerr << "WARNING: Cannot append to unknown element: " << name.back() << std::endl;
+        std::stringstream warning;
+        warning << "In HumdrumInput::appendElement: ";
+        warning << "Cannot append to unknown element: " << name.back();
+        LogWarning(warning.str().c_str());
     }
 }
 
@@ -26515,7 +26596,12 @@ void HumdrumInput::convertNote(Note *note, hum::HTp token, int staffadj, int sta
                     case -1: myaccid->SetAccid(ACCIDENTAL_WRITTEN_f); break;
                     case -2: myaccid->SetAccid(ACCIDENTAL_WRITTEN_ff); break;
                     case -3: myaccid->SetAccid(ACCIDENTAL_WRITTEN_tf); break;
-                    default: std::cerr << "Do not know how to convert accidental: " << accidCount << endl;
+                    default: {
+                        std::stringstream warning;
+                        warning << "In HumdrumInput::convertNote: ";
+                        warning << "Do not know how to convert accidental: " << accidCount;
+                        LogWarning(warning.str().c_str());
+                    }
                 }
 
                 if (accidlevel != 0) {
@@ -26563,7 +26649,12 @@ void HumdrumInput::convertNote(Note *note, hum::HTp token, int staffadj, int sta
                     case -1: accid->SetAccid(ACCIDENTAL_WRITTEN_f); break;
                     case -2: accid->SetAccid(ACCIDENTAL_WRITTEN_ff); break;
                     case -3: accid->SetAccid(ACCIDENTAL_WRITTEN_tf); break;
-                    default: std::cerr << "Do not know how to convert accidental: " << accidCount << endl;
+                    default: {
+                        std::stringstream warning;
+                        warning << "In HumdrumInput::convertNote: ";
+                        warning << "Do not know how to convert accidental: " << accidCount;
+                        LogWarning(warning.str().c_str());
+                    }
                 }
             }
             else if (!loaccid.empty()) {
@@ -26611,7 +26702,10 @@ void HumdrumInput::convertNote(Note *note, hum::HTp token, int staffadj, int sta
                     showInAccidGes = true;
                 }
                 else {
-                    std::cerr << "Warning: unknown accidental type " << std::endl;
+                    std::stringstream warning;
+                    warning << "In HumdrumInput::convertNote: ";
+                    warning << "Unknown accidental type: " << loaccid;
+                    LogWarning(warning.str().c_str());
                 }
                 // add more accidentals here as necessary.  Mostly left are quarter tones
                 // which are not dealt with directly in **kern data: su, sd, fu, fd, nu,
@@ -28615,8 +28709,11 @@ void HumdrumInput::addTurn(hum::HTp token, const string &tok, int noteIndex)
     }
     bool singleQ = false;
     if (turnstart == turnend) {
-        LogWarning(
-            "Humdrum: Single turn character on line %d, field, %d\n", token->getLineNumber(), token->getFieldNumber());
+        std::stringstream warning;
+        warning << "In HumdrumInput::addTurn: Single turn character ";
+        warning << "on line " << token->getLineNumber() << ", ";
+        warning << "field, " << token->getFieldNumber() << ".";
+        LogWarning(warning.str().c_str());
         singleQ = true;
     }
 
@@ -29651,15 +29748,15 @@ void HumdrumInput::printMeasureTokens()
 {
     std::vector<std::vector<std::vector<hum::HTp>>> &lt = m_layertokens;
     int i, j, k;
-    cerr << endl;
+    std::cerr << std::endl;
     for (i = 0; i < (int)lt.size(); ++i) {
-        cerr << "STAFF " << i + 1 << "\t";
+        std::cerr << "STAFF " << i + 1 << "\t";
         for (j = 0; j < (int)lt[i].size(); ++j) {
-            cerr << "LAYER " << j + 1 << ":\t";
+            std::cerr << "LAYER " << j + 1 << ":\t";
             for (k = 0; k < (int)lt[i][j].size(); ++k) {
-                cout << " " << *lt[i][j][k];
+                std::cout << " " << *lt[i][j][k];
             }
-            cerr << endl;
+            std::cerr << std::endl;
         }
     }
 }
@@ -29739,7 +29836,10 @@ template <class ELEMENT> hum::HumNum HumdrumInput::setDuration(ELEMENT element,
     }
     // Don't know what to do, so return duration
     // There will be an error in the data.
-    cerr << "Unprintable rhythm: " << duration << endl;
+    std::stringstream warning;
+    warning << "In HumdrumInput::setDuration: ";
+    warning << "Unprintable duration" << duration << " quarter notes";
+    LogWarning(warning.str().c_str());
     return duration;
 }
 
@@ -29807,7 +29907,7 @@ Tie *HumdrumInput::tieToPreviousItem(hum::HTp token, int subindex, hum::HumNum m
         tstamp += 1;
     }
     else {
-        cerr << "STRANGE CASE IN TIE INSERTION" << endl;
+        LogWarning("In HumdrumInput::tieToPreviousItem: Strange case for tie insertion.");
     }
 
     tie->SetTstamp(tstamp.getFloat()); // attach start to beginning of measure
@@ -31665,7 +31765,7 @@ std::vector<int> HumdrumInput::analyzeMultiRest(hum::HumdrumFile &infile)
     }
 
     // for (int i = 0; i < infile.getLineCount(); ++i) {
-    //    cout << infile[i] << "\t" << output[i] << "\n";
+    //   std:: cout << infile[i] << "\t" << output[i] << "\n";
     //}
     // Example analysis, with measure 4 staring a rest with num="6".
     // Measures 5-9 marked as whole-measure rests which will be merged into

From 744c1da4de88d044a07be818f77b14a405a275e5 Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Sat, 7 Sep 2024 08:45:19 -0700
Subject: [PATCH 05/11] Fix for issue
 https://github.com/rism-digital/verovio/issues/3766

---
 src/toolkit.cpp | 55 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/src/toolkit.cpp b/src/toolkit.cpp
index cfb0720caaf..cf2c81513db 100644
--- a/src/toolkit.cpp
+++ b/src/toolkit.cpp
@@ -609,7 +609,18 @@ bool Toolkit::LoadData(const std::string &data)
         pugi::xml_document xmlfile;
         xmlfile.load_string(data.c_str());
         stringstream conversion;
+
+        // Temporarily redirect cerr:
+        std::stringstream captured_cerr;
+        std::streambuf *cerr_buf = std::cerr.rdbuf();
+        std::cerr.rdbuf(captured_cerr.rdbuf());
+
         bool status = converter.convert(conversion, xmlfile);
+        LogWarning(captured_cerr.str().c_str());
+
+        // Restore cerr:
+        std::cerr.rdbuf(cerr_buf);
+
         if (!status) {
             LogError("Error converting MusicXML data");
             return false;
@@ -657,7 +668,18 @@ bool Toolkit::LoadData(const std::string &data)
         // This is the indirect converter from MuseData to MEI using iohumdrum:
         hum::Tool_musedata2hum converter;
         stringstream conversion;
+
+        // Temporarily redirect cerr:
+        std::stringstream captured_cerr;
+        std::streambuf *cerr_buf = std::cerr.rdbuf();
+        std::cerr.rdbuf(captured_cerr.rdbuf());
+
         bool status = converter.convertString(conversion, data);
+        LogWarning(captured_cerr.str().c_str());
+
+        // Restore cerr:
+        std::cerr.rdbuf(cerr_buf);
+
         if (!status) {
             LogError("Error converting MuseData data");
             return false;
@@ -684,8 +706,19 @@ bool Toolkit::LoadData(const std::string &data)
     else if (inputFormat == ESAC) {
         // This is the indirect converter from EsAC to MEI using iohumdrum:
         hum::Tool_esac2hum converter;
+
+        // Temporarily redirect cerr:
+        std::stringstream captured_cerr;
+        std::streambuf *cerr_buf = std::cerr.rdbuf();
+        std::cerr.rdbuf(captured_cerr.rdbuf());
         stringstream conversion;
+
         bool status = converter.convert(conversion, data);
+        LogWarning(captured_cerr.str().c_str());
+
+        // Restore cerr:
+        std::cerr.rdbuf(cerr_buf);
+
         if (!status) {
             LogError("Error converting EsAC data");
             return false;
@@ -2040,7 +2073,18 @@ const char *Toolkit::GetHumdrumBuffer()
         infile.load_string(meidata.c_str());
         stringstream out;
         hum::Tool_mei2hum converter;
+
+        // Temporarily redirect cerr:
+        std::stringstream captured_cerr;
+        std::streambuf *cerr_buf = std::cerr.rdbuf();
+        std::cerr.rdbuf(captured_cerr.rdbuf());
+
         converter.convert(out, infile);
+        LogWarning(captured_cerr.str().c_str());
+
+        // Restore cerr:
+        std::cerr.rdbuf(cerr_buf);
+
         this->SetHumdrumBuffer(out.str().c_str());
 #endif
         if (m_humdrumBuffer) {
@@ -2101,7 +2145,18 @@ std::string Toolkit::ConvertMEIToHumdrum(const std::string &meiData)
     pugi::xml_document xmlfile;
     xmlfile.load_string(meiData.c_str());
     std::stringstream conversion;
+
+    // Temporarily redirect cerr:
+    std::stringstream captured_cerr;
+    std::streambuf *cerr_buf = std::cerr.rdbuf();
+    std::cerr.rdbuf(captured_cerr.rdbuf());
+
     bool status = converter.convert(conversion, xmlfile);
+    LogWarning(captured_cerr.str().c_str());
+
+    // Restore cerr:
+    std::cerr.rdbuf(cerr_buf);
+
     if (!status) {
         LogError("Error converting MEI data to Humdrum: %s", conversion.str().c_str());
     }

From 4eb98d659155378905b711fa3764c52ba31bd014 Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Sat, 7 Sep 2024 14:12:48 -0700
Subject: [PATCH 06/11] Remove exit() from MusicXML-to-Humdrum converter.

---
 include/hum/humlib.h |  2 +-
 src/hum/humlib.cpp   | 20 ++++++++++----------
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/include/hum/humlib.h b/include/hum/humlib.h
index 374437551e4..2a722232a2c 100644
--- a/include/hum/humlib.h
+++ b/include/hum/humlib.h
@@ -1,7 +1,7 @@
 //
 // Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
 // Creation Date: Sat Aug  8 12:24:49 PDT 2015
-// Last Modified: Thu Sep  5 14:41:50 PDT 2024
+// Last Modified: Sat Sep  7 13:38:03 PDT 2024
 // Filename:      min/humlib.h
 // URL:           https://github.com/craigsapp/humlib/blob/master/min/humlib.h
 // Syntax:        C++11
diff --git a/src/hum/humlib.cpp b/src/hum/humlib.cpp
index 0e37f29e2d9..74a9708af41 100644
--- a/src/hum/humlib.cpp
+++ b/src/hum/humlib.cpp
@@ -1,7 +1,7 @@
 //
 // Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
 // Creation Date: Sat Aug  8 12:24:49 PDT 2015
-// Last Modified: Thu Sep  5 14:41:50 PDT 2024
+// Last Modified: Sat Sep  7 13:38:03 PDT 2024
 // Filename:      min/humlib.cpp
 // URL:           https://github.com/craigsapp/humlib/blob/master/min/humlib.cpp
 // Syntax:        C++11
@@ -106421,10 +106421,10 @@ bool Tool_musicxml2hum::convertFile(ostream& out, const char* filename) {
 	xml_document doc;
 	auto result = doc.load_file(filename);
 	if (!result) {
-		cerr << "\nXML file [" << filename << "] has syntax errors\n";
-		cerr << "Error description:\t" << result.description() << "\n";
-		cerr << "Error offset:\t" << result.offset << "\n\n";
-		exit(1);
+		cerr << "\nXML file [" << filename << "] has syntax errors ";
+		cerr << "Error description:\t" << result.description() << endl;
+		cerr << "Error offset:\t" << result.offset << "\n";
+		return false;
 	}
 
 	return convert(out, doc);
@@ -106441,10 +106441,10 @@ bool Tool_musicxml2hum::convert(ostream& out, const char* input) {
 	xml_document doc;
 	auto result = doc.load_string(input);
 	if (!result) {
-		cout << "\nXML content has syntax errors\n";
-		cout << "Error description:\t" << result.description() << "\n";
+		cout << "\nXML content has syntax errors";
+		cout << " Error description:\t" << result.description() << "\n";
 		cout << "Error offset:\t" << result.offset << "\n\n";
-		exit(1);
+		return false;
 	}
 
 	return convert(out, doc);
@@ -107389,10 +107389,10 @@ bool Tool_musicxml2hum::stitchParts(HumGrid& outdata,
 	// i used to start at 1 for some strange reason.
 	for (i=0; i<(int)partdata.size(); i++) {
 		if (measurecount != partdata[i].getMeasureCount()) {
-			cerr << "ERROR: cannot handle parts with different measure\n";
+			cerr << "ERROR: cannot handle parts with different measure ";
 			cerr << "counts yet. Compare MM" << measurecount << " to MM";
 			cerr << partdata[i].getMeasureCount() << endl;
-			exit(1);
+			return false;
 		}
 	}
 

From 0ede4fa0e7f77fd4973c08fd9172cceb009032ef Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Sat, 7 Sep 2024 14:59:44 -0700
Subject: [PATCH 07/11] Add "esac" as in input-from option.

---
 src/options.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/options.cpp b/src/options.cpp
index 94b640dc35b..ba0265ef74a 100644
--- a/src/options.cpp
+++ b/src/options.cpp
@@ -914,8 +914,8 @@ Options::Options()
     m_baseOptions.AddOption(&m_allPages);
 
     m_inputFrom.SetInfo("Input from",
-        "Select input format from: \"abc\", \"darms\", \"humdrum\", \"mei\", \"pae\", \"volpiano\", \"xml\" "
-        "(musicxml)");
+        "Select input format from: \"abc\", \"darms\", \"esac\", \"humdrum\", \"mei\", \"pae\", \"volpiano\", \"xml\" "
+        "(musicxml), \"musicxml-hum\" (musicxml via humdrum)");
     m_inputFrom.Init("mei");
     m_inputFrom.SetKey("inputFrom");
     m_inputFrom.SetShortOption('f', false);

From dc2c72a437ac9902a9fdf083aea8d0dd0b8be5d2 Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Mon, 9 Sep 2024 11:12:13 -0700
Subject: [PATCH 08/11] Humlib updates.

---
 include/hum/humlib.h |  54 +++-----
 src/hum/humlib.cpp   | 304 ++++++++++++++++++++++++++++++++++++-------
 2 files changed, 280 insertions(+), 78 deletions(-)

diff --git a/include/hum/humlib.h b/include/hum/humlib.h
index 2a722232a2c..d9b39732d30 100644
--- a/include/hum/humlib.h
+++ b/include/hum/humlib.h
@@ -1,7 +1,7 @@
 //
 // Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
 // Creation Date: Sat Aug  8 12:24:49 PDT 2015
-// Last Modified: Sat Sep  7 13:38:03 PDT 2024
+// Last Modified: Sun Sep  8 23:07:16 PDT 2024
 // Filename:      min/humlib.h
 // URL:           https://github.com/craigsapp/humlib/blob/master/min/humlib.h
 // Syntax:        C++11
@@ -7502,6 +7502,9 @@ class Tool_esac2hum : public HumTool {
 		void        processSong         (void);
 		void        printScoreContents  (std::ostream& output);
 		void        embedAnalyses       (std::ostream& output);
+		void        printPdfUrl         (std::ostream& output);
+		std::string getKolbergUrl       (int volume);
+      void        printKolbergPdfUrl  (std::ostream& output);
 
 	private:
 		bool        m_debugQ     = false;  // used with --debug option
@@ -7524,43 +7527,28 @@ class Tool_esac2hum : public HumTool {
 		std::string m_cutline;
 		std::vector<std::string> m_globalComments;
 
+		bool m_initialized = false;
 		int m_minrhy = 0;
 
 		Tool_esac2hum::Score m_score;
 
-		std::map<std::string, std::string> m_bem_translation = {
-			{"Czwarta zwrotka pieśni Niech będzie Jezus Chrystus pochwalony", "Fourth verse of the song \"Let Jesus Christ be praised\""},
-			{"Do oczepin, zakodować w A?", "For the unveiling, encode in A?"},
-			{"Do oczepin", "For the unveiling"},
-			{"Druga zwrotka poprzedniej pieśni", "Second verse of the previous song"},
-			{"Gdy zdejmują wianek", "When they remove the wreath"},
-			{"Jak do ślubu odjeżdżają (na wozie)", "As they depart for the wedding (on a wagon)"},
-			{"Krakowiak", "Krakowiak"},
-			{"Marsz (konfederatów Barskich) Przygrywka na trąbie", "March (of the Bar Confederates) Prelude on trumpet"},
-			{"Marsz konfederatów Barskich", "March of the Bar Confederates"},
-			{"Mazur", "Mazur"},
-			{"Na przodziek", "At the front"},
-			{"Owczarek gładki, szybko tańczony", "Smooth shepherd's dance, danced quickly"},
-			{"Owczarek", "Shepherd's dance"},
-			{"Piosenka żniwiarska", "Harvest song"},
-			{"Polonez (pokutującego wojaka)", "Polonaise (of the penitent soldier)"},
-			{"Polonez", "Polonaise"},
-			{"Polski chodzony", "Polish walking dance"},
-			{"Prawdopodobnie ośmiomiar 2+3+3", "Probably an eight-measure 2+3+3"},
-			{"Prawdopodobnie rytm jambiczny", "Probably iambic rhythm"},
-			{"Przy przenosinach", "During the moving"},
-			{"Tańczy na przodziek (na weselu)", "Dances at the front (at the wedding)"},
-			{"W sobotę gdy wianki wiją", "On Saturday when they weave the wreaths"},
-			{"Wesele do ślubu", "Wedding to the church"},
-			{"Wesele", "Wedding"},
-			{"Weselna gdy zbierają składkę", "Wedding song when collecting contributions"},
-			{"Weselna", "Wedding song"},
-			{"Wielkanoc", "Easter"},
-			{"Wieniec", "Wreath"},
-			{"Zakodować w C?", "Encode in C?"},
-			{"w t. 10 wpisany tryl dziadowski 43b21", "in measure 10, a beggar's trill written 43b21"},
-			{"żniwiarska", "Harvest song"}
+		class KolbergInfo {
+			public:
+				std::string titlePL;
+				std::string titleEN;
+				int firstPrintPage;
+				int firstScanPage;
+				std::vector<int> plates;
+
+				KolbergInfo(void) { firstPrintPage = 0; firstScanPage = 0; }
+				KolbergInfo(
+					const std::string& pl, const std::string& en, int fpp, int fsp, const std::vector<int>& plts)
+        				: titlePL(pl), titleEN(en), firstPrintPage(fpp), firstScanPage(fsp), plates(plts) {}
 		};
+		std::map<int, KolbergInfo> m_kinfo;
+		KolbergInfo getKolbergInfo(int volume);
+		std::string getKolbergUrl(int volume, int printPage);
+		int calculateScanPage(int inputPrintPage, int printPage, int scanPage, const std::vector<int>& platePages);
 
 
 };
diff --git a/src/hum/humlib.cpp b/src/hum/humlib.cpp
index 74a9708af41..ed7780c0419 100644
--- a/src/hum/humlib.cpp
+++ b/src/hum/humlib.cpp
@@ -1,7 +1,7 @@
 //
 // Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
 // Creation Date: Sat Aug  8 12:24:49 PDT 2015
-// Last Modified: Sat Sep  7 13:38:03 PDT 2024
+// Last Modified: Sun Sep  8 23:07:16 PDT 2024
 // Filename:      min/humlib.cpp
 // URL:           https://github.com/craigsapp/humlib/blob/master/min/humlib.cpp
 // Syntax:        C++11
@@ -55582,6 +55582,7 @@ bool Tool_autobeam::run(HumdrumFile& infile) {
 	}
 	// Re-load the text for each line from their tokens.
 	infile.createLinesFromTokens();
+	m_humdrum_text << infile;
 	return true;
 }
 
@@ -56410,9 +56411,9 @@ void Tool_autobeam::processMeasure(vector<HTp>& measure) {
 			if ((current.first % 3 == 0) && (current.first != 3)) {
 				// compound meter, so shift the beat to 3x the demoniator
 				beatdur *= 3;
-			} else if (current.first == 3 && (current.second > 4)) {
+			} else if (current.first == 3 && (current.second < 4)) {
 				// time signatures such as 3/8 and 3/16 which should
-				// beam together at the measure level (3/4 not included).
+				// beam together at the measure level (3/4 and 3/2 not included).
 				beatdur *= 3;
 			}
 		}
@@ -78526,7 +78527,7 @@ void Tool_esac2hum::cleanText(std::string& buffer) {
 
 	// Ż: c4 b9 c5 a5 -> c5 bb
 	hre.replaceDestructive(buffer, "\xc5\xbb", "\xc4\xb9\xc5\xa5", "g");
-	
+
 	// ż:  c4 b9 c5 ba -> c5 bc
 	hre.replaceDestructive(buffer, "\xc5\xbc", "\xc4\xb9\xc5\xba", "g");
 
@@ -79014,7 +79015,9 @@ void Tool_esac2hum::Score::calculateTimeSignatures(void) {
 	vector<string> timesigs;
 	hre.split(timesigs, ts, "\\s+");
 	if (timesigs.size() < 2) {
-		m_errors.push_back("ERROR: strange format for time signatures.");
+		string error = "ERROR: Cannot find time signature(s) in KEY[] field: ";
+		error += m_params["KEY"];
+		m_errors.push_back(error);
 		return;
 	}
 
@@ -79073,7 +79076,6 @@ void Tool_esac2hum::Score::prepareMultipleTimeSignatures(const string& ts) {
 					measure.setComplete();
 				}
 			}
-			
 		}
 	}
 
@@ -79347,15 +79349,15 @@ void Tool_esac2hum::Score::calculateKeyInformation(void) {
 		} else if (m_keydesignation == "*b:") {
 			m_keysignature = "*k[f#c#]";
 		} else if (m_keydesignation == "*f#:") {
-			m_keysignature = "*k[f#c#g$]";
+			m_keysignature = "*k[f#c#g#]";
 		} else if (m_keydesignation == "*c#:") {
-			m_keysignature = "*k[f#c#g$d#]";
+			m_keysignature = "*k[f#c#g#d#]";
 		} else if (m_keydesignation == "*g#:") {
-			m_keysignature = "*k[f#c#g$d#a#]";
+			m_keysignature = "*k[f#c#g#d#a#]";
 		} else if (m_keydesignation == "*d#:") {
-			m_keysignature = "*k[f#c#g$d#a#e#]";
+			m_keysignature = "*k[f#c#g#d#a#e#]";
 		} else if (m_keydesignation == "*a#:") {
-			m_keysignature = "*k[f#c#g$d#a#e#b#]";
+			m_keysignature = "*k[f#c#g#d#a#e#b#]";
 		} else if (m_keydesignation == "*d:") {
 			m_keysignature = "*k[b-]";
 		} else if (m_keydesignation == "*g:") {
@@ -79369,7 +79371,7 @@ void Tool_esac2hum::Score::calculateKeyInformation(void) {
 		} else if (m_keydesignation == "*e-:") {
 			m_keysignature = "*k[b-e-a-d-g-c-]";
 		} else if (m_keydesignation == "*a-:") {
-			m_keysignature = "*k[b-e-a-d-g-f-]";
+			m_keysignature = "*k[b-e-a-d-g-c-f-]";
 		} else {
 			m_errors.push_back("ERROR: invalid/exotic key signature required.");
 		}
@@ -79435,7 +79437,9 @@ void Tool_esac2hum::Score::generateHumdrumNotes(void) {
 
 	string tonic = m_params["_tonic"];
 	if (tonic.empty()) {
-		m_errors.push_back("Error: cannot find KEY[] tonic pitch");
+		string error = "Error: cannot find tonic pitch in KEY[] field: ";
+		error += m_params["KEY"];
+		m_errors.push_back(error);
 		return;
 	}
 	char letter = std::tolower(tonic[0]);
@@ -80162,7 +80166,7 @@ void Tool_esac2hum::getParameters(vector<string>& infile) {
 	}
 
 	string key = m_score.m_params["KEY"];
-	if (hre.search(key, "^\\s*([^\\s]+)\\s+(\\d+)\\s+([A-Gacdefg][bs]*)\\s+(.*?)\\s*$")) {
+	if (hre.search(key, "^\\s*([^\\s]+)\\s+(\\d+)\\s+([A-Gacdefg][b#]*)\\s+(.*?)\\s*$")) {
 		string id     = hre.getMatch(1);
 		string minrhy = hre.getMatch(2);
 		string tonic  = hre.getMatch(3);
@@ -80186,13 +80190,16 @@ void Tool_esac2hum::getParameters(vector<string>& infile) {
 		cerr << "Problem parsing KEY parameter: " << key << endl;
 	}
 
-	string trd;
+	string trd = m_score.m_params["TRD"];
 	if (hre.search(trd, "^\\s*(.*)\\ss\\.")) {
 		m_score.m_params["_source_trd"] = hre.getMatch(1);
 	}
-	if (hre.search(trd, "s\\.\\s*(\\d+-?\\d*)")) {
-		// Could be text aftewards about the origin of the song.
+	if (hre.search(trd, "\\bs\\.\\s*(\\d+)\\s*-\\s*(\\d+)?")) {
+		m_score.m_params["_page"] = hre.getMatch(1) + "-" + hre.getMatch(2);
+	} else if (hre.search(trd, "\\bs\\.\\s*(\\d+)")) {
 		m_score.m_params["_page"] = hre.getMatch(1);
+	} else {
+		cerr << "CANNOT FIND PAGE NUMBER IN " << trd << endl;
 	}
 
 	if (m_debugQ) {
@@ -80236,13 +80243,7 @@ void Tool_esac2hum::printBemComment(ostream& output) {
 	if (bem.empty()) {
 		return;
 	}
-	string english = m_bem_translation[bem];
-	if (english.empty()) {
-		output << "!!!ONB: " << bem << endl;
-	} else {
-		output << "!!!ONB@@PL: " << bem << endl;
-		output << "!!!ONB@@EN: " << english << endl;
-	}
+	output << "!!!ONB: " << bem << endl;
 }
 
 
@@ -80290,9 +80291,9 @@ void Tool_esac2hum::printFooter(ostream& output, vector<string>& infile) {
 void Tool_esac2hum::printPageNumbers(ostream& output) {
 	HumRegex hre;
 	string trd = m_score.m_params["TRD"];
-	if (hre.search(trd, "\\bs\\.\\s*(\\d+)\\s*-\\s*(\\d+)", "i")) {
+	if (hre.search(trd, "\\bs\\.\\s*(\\d+)\\s*-\\s*(\\d+)", "im")) {
 		output << "!!!page: " << hre.getMatch(1) << "-" << hre.getMatch(2) << endl;
-	} else if (hre.search(trd, "\\bs\\.\\s*(\\d+)", "i")) {
+	} else if (hre.search(trd, "\\bs\\.\\s*(\\d+)", "im")) {
 		output << "!!!page: " << hre.getMatch(1) << endl;
 	}
 }
@@ -80362,25 +80363,7 @@ void Tool_esac2hum::printPdfLinks(ostream& output) {
 
 	output << "!!!URL: https::kolberg.ispan.pl/dwok/tomy Oskar Kolberg: Complete Works digital edition" << endl;
 
-	string source = m_score.m_params["_source"];
-	HumRegex hre;
-	if (!hre.search(source, "^DWOK(\\d+)")) {
-		return;
-	}
-	string volume = hre.getMatch(1);
-	if (volume.size() == 1) {
-		volume = "0" + volume;
-	}
-	if (volume.size() == 2) {
-		volume = "0" + volume;
-	}
-	if (volume.size() > 3) {
-		return;
-	}
-	string nozero = volume;
-	hre.replaceDestructive(nozero, "" , "^0+");
-	// need http:// not https:// for the following PDF link:
-	output << "!!!URL-pdf: http://oskarkolberg.pl/MediaFiles/" << volume << "dwok.pdf" << " Oskar Kolberg: Complete Works, volume " << nozero << endl;
+	printKolbergPdfUrl(output);
 
 }
 
@@ -80815,6 +80798,237 @@ void Tool_esac2hum::Score::analyzeACC(void) {
 
 
 
+//////////////////////////////
+//
+// Tool_esac2hum::getKolbergInfo --
+//
+
+Tool_esac2hum::KolbergInfo Tool_esac2hum::getKolbergInfo(int volume) {
+
+	if (!m_initialized) {
+		m_initialized = true;
+		// Parameters:          Polish volume title,                                                          English translation,                                    print start page, Equivalent start scan (pdf page), Plate scan page vector
+		m_kinfo.emplace( 1,     KolbergInfo("Pieśni ludu polskego",                                          "Polish folk songs",                                                    3,  99, {149, 150, 167, 168, 233, 234, 251, 252, 317, 318, 335, 336, 401, 402, 419, 420, 485, 486, 503, 504}));
+		m_kinfo.emplace( 2,     KolbergInfo("Sandomierskie",                                                 "Sandomierz",                                                          23,  34, {}));
+		m_kinfo.emplace( 3,     KolbergInfo("Kujawy I",                                                      "Kuyavia I (north central Poland)",                                   209, 221, {}));
+		m_kinfo.emplace( 4,     KolbergInfo("Kujawy II",                                                     "Kuyavia II (north central Poland)",                                   69,  83, {}));
+		m_kinfo.emplace( 5,     KolbergInfo("Krakowskie I",                                                  "Crakow I",                                                           194, 222, {}));
+		m_kinfo.emplace( 6,     KolbergInfo("Krakowskie II",                                                 "Crakow II",                                                            5,  29, {49, 50}));
+		//               7:     Krakowskie III/Crakow III: no music
+		m_kinfo.emplace( 8,     KolbergInfo("Krakowskie IV",                                                 "Crakow IV",                                                          162, 182, {}));
+		m_kinfo.emplace( 9,     KolbergInfo("W. Ks. Poznańskie I",                                           "Grand Duchy of Poznań I",                                            117, 141, {}));
+		m_kinfo.emplace(10,     KolbergInfo("W. Ks. Poznańskie II",                                          "Grand Duchy of Poznań II",                                            60,  76, {}));
+		m_kinfo.emplace(11,     KolbergInfo("W. Ks. Poznańskie III",                                         "Grand Duchy of Poznań III",                                           39,  57, {}));
+		m_kinfo.emplace(12,     KolbergInfo("W. Ks. Poznańskie IV",                                          "Grand Duchy of Poznań IV",                                             3,  19, {}));
+		m_kinfo.emplace(13,     KolbergInfo("W. Ks. Poznańskie V",                                           "Grand Duchy of Poznań V",                                              3,  27, {}));
+		m_kinfo.emplace(14,     KolbergInfo("W. Ks. Poznańskie VI",                                          "Grand Duchy of Poznań VI",                                           157, 165, {}));
+		m_kinfo.emplace(15,     KolbergInfo("W. Ks. Poznańskie VII",                                         "Grand Duchy of Poznań VII",                                          317, 327, {}));
+		m_kinfo.emplace(16,     KolbergInfo("Lubelskie I",                                                   "Lublin Voivodeship I",                                               105, 125, {}));
+		m_kinfo.emplace(17,     KolbergInfo("Lubelskie II",                                                  "Lublin Voivodeship II",                                                1,  23, {}));
+		m_kinfo.emplace(18,     KolbergInfo("Kieleckie I",                                                   "Kielce Voivodeship I",                                                49,  65, {}));
+		m_kinfo.emplace(19,     KolbergInfo("Kieleckie II",                                                  "Kielce Voivodeship II",                                                1,  15, {}));
+		m_kinfo.emplace(20,     KolbergInfo("Radomskie I",                                                   "Radom Voivodeship I",                                                 75,  95, {}));
+		m_kinfo.emplace(21,     KolbergInfo("Radomskie II",                                                  "Radom Voivodeship II",                                                 1,  19, {}));
+		m_kinfo.emplace(22,     KolbergInfo("Łęczyckie",                                                     "Łęczyca Voivodeship",                                                 18,  36, {}));
+		m_kinfo.emplace(23,     KolbergInfo("Kaliskie",                                                      "Kalisz Region",                                                       54,  68, {}));
+		m_kinfo.emplace(24,     KolbergInfo("Mazowsze I",                                                    "Mazovia I",                                                           79, 103, {}));
+		m_kinfo.emplace(25,     KolbergInfo("Mazowsze II",                                                   "Mazovia II",                                                           1,  26, {}));
+		//              26:     Mazowsze III/Mazovia III: no music
+		m_kinfo.emplace(27,     KolbergInfo("Mazowsze IV",                                                   "Mazovia IV",                                                         115, 134, {}));
+		m_kinfo.emplace(28,     KolbergInfo("Mazowsze V",                                                    "Mazovia V",                                                           64,  83, {}));
+		m_kinfo.emplace(29,     KolbergInfo("Pokucie I",                                                     "Pokuttia I",                                                          90, 122, {}));
+		m_kinfo.emplace(30,     KolbergInfo("Pokucie II",                                                    "Pokuttia II",                                                          1,  14, {}));
+		m_kinfo.emplace(31,     KolbergInfo("Pokucie III",                                                   "Pokuttia III",                                                        10,  31, {}));
+		//              32:     Pokucie IV/Pokuttia IV: no music
+		m_kinfo.emplace(33,     KolbergInfo("Chełmskie I",                                                   "Chełm Voivodeship I",                                                114, 150, {163, 164, 175, 176, 177, 178}));
+		m_kinfo.emplace(34,     KolbergInfo("Chełmskie II",                                                  "Chełm Voivodeship II",                                                 4,  21, {}));
+		m_kinfo.emplace(35,     KolbergInfo("Przemyskie",                                                    "Przemyśl Voivodeship",                                                11,  47, {93, 94, 115, 116, 167, 168}));
+		//              36:     Wołyń/Volhynia: complications
+		//              37:     Miscellanea I/Miscellanea I: no music
+		//              38,     Miscellanea II/Miscellanea II: no music
+		m_kinfo.emplace(39,     KolbergInfo("Pomorze",                                                       "Pomerania",                                                           67, 115, {129, 130, 147, 148}));
+		m_kinfo.emplace(40,     KolbergInfo("Mazury Pruskie",                                                "Prussian Masuria",                                                    96, 155, {356, 357, 358, 359, 464, 465, 482, 483}));
+
+		m_kinfo.emplace(41,     KolbergInfo("Mazowsze VI",                                                   "Mazovia VI",                                                          20, 95,  {108, 109, 126, 127, 288, 289, 306, 307, 388, 389, 406, 407}));
+		m_kinfo.emplace(42,     KolbergInfo("Mazowsze VII",                                                  "Mazovia VII",                                                          6,  15, {}));
+		m_kinfo.emplace(43,     KolbergInfo("Śląsk",                                                         "Silesia",                                                             21,  62, {74, 75}));
+		m_kinfo.emplace(44,     KolbergInfo("Góry i Podgórze I",                                             "Mountains and Foothills I",                                           64, 110, {111, 112, 129, 130, 195, 196, 213, 214, 343, 344, 361, 362}));
+		m_kinfo.emplace(45,     KolbergInfo("Góry i Podgórze II",                                            "Mountains and Foothills II",                                           1,  11, {91, 92, 109, 110, 335, 336, 353, 354, 499, 500}));
+		m_kinfo.emplace(46,     KolbergInfo("Kaliskie i Sieradzkie",                                         "Kalisz Region and Sieradz Voivodeship",                                3,  29, {43, 44, 61, 62, 175, 176, 193, 194}));
+		m_kinfo.emplace(47,     KolbergInfo("Podole",                                                        "Podolia",                                                             59, 105, {151, 152, 153, 154, 155, 156, 157, 158}));
+		m_kinfo.emplace(48,     KolbergInfo("Tarnowskie-Rzeszowskie",                                        "Tarnów-Rzeszów Voivodeship",                                          65, 103, {119, 120, 233, 234, 251, 252}));
+		m_kinfo.emplace(49,     KolbergInfo("Sanockie-Krośnieńskie I",                                       "Sanok-Krosno Voivodeship I",                                         109, 185, {189, 190, 239, 240, 257, 258, 387, 388, 405, 406, 455, 456, 473, 474}));
+		m_kinfo.emplace(50,     KolbergInfo("Sanockie-Krośnieńskie II",                                      "Sanok-Krosno Voivodeship II",                                          1,  14, {30, 31, 48, 49, 114, 115, 132, 133, 198, 199, 216 ,217, 282, 283, 300, 301, 366, 367, 384, 385}));
+		//              51:     Sanockie-Krośnieńskie III/Sanok-Krosno Voivodeship III: no music
+		m_kinfo.emplace(52,     KolbergInfo("Białoruś-Polesie",                                              "Belarus-Polesia",                                                    116, 169, {182, 183, 200, 201, 266, 267, 284, 285, 382, 383, 400, 401, 530, 531, 548, 549}));
+		m_kinfo.emplace(53,     KolbergInfo("Litwa",                                                         "Lithuania",                                                          142, 176, {195, 196, 325, 326, 359, 360, 441, 442, 459, 460}));
+		m_kinfo.emplace(54,     KolbergInfo("Ruś karpacka I",                                                "Carpathian Ruthenia I",                                              267, 365, {371, 372}));
+		m_kinfo.emplace(55,     KolbergInfo("Ruś karpacka II",                                               "Carpathian Ruthenia II",                                              22,  37, {48, 49, 146, 147, 164, 165, 214, 215, 232, 233, 426, 427, 444, 445}));
+		m_kinfo.emplace(56,     KolbergInfo("Ruś czerwona I",                                                "Red Ruthenia I",                                                      61, 157, {173, 174, 191, 192, 209, 210, 259, 260, 27, 278, 311, 312, 329, 330, 443, 444, 461, 462}));
+		//              57:     Ruś czerwona II/Red Ruthenia II, 14, 19, {70, 71, 88, 89}: complications
+		//              58:     Materiały do etnografii Słowian wschodnich/Materials for the ethnography of the Eastern Slavs
+		//              59/I,   Materiały do etnografii Słowian zachodnich i południowych. Cz. I Łużyce/Materials for the ethnography of western and southern Slavs. Part I Lusatia
+		//              59/II,  Materiały do etnografii Słowian zachodnich i południowych. Cz. II Czechy, Słowacja/Materials for the ethnography of western and southern Slavs. Part II Czech Republic, Slovakia
+		//              59/III, Materiały do etnografii Słowian zachodnich i południowych. Cz. III Słowiańszczyzna południowa/Materials for the ethnography of western and southern Slavs. Part III Southern Slavs
+		//              60:     Przysłowia/Proverbs: no music
+		//              61,     Pisma muzyczne, cz. I/Musical writings, part I: no music
+		//              62,     Pisma muzyczne, cz. II/"Musical writings, part II, 25, 33: not in EsAC
+		//              63,     Studia, rozprawy i artykuły/Studies, dissertations and articles", 55, 113: not in EsAC
+		m_kinfo.emplace(64,     KolbergInfo("Korespondencja Oskara Kolberga, cz. I (1837-1876)",             "Correspondence of Oskar Kolberg, Part I (1837-1876)",                216, 261, {}));
+		//              65:     Korespondencja Oskara Kolberga, cz. II (1877-1882)/Correspondence of Oskar Kolberg, Part II (1877-1882): no music
+		//              66:     Korespondencja Oskara Kolberga, cz. III (1883-1890)/Correspondence of Oskar Kolberg, Part III (1883-1890): no music
+		///             67:     Pieśni i melodie ludowe w opracowaniu fortepianowym, cz. I-II/Folk songs and melodies in piano arrangement, part I-II, 3, 22, not in EsAC
+		//              68:     Kompozycje wokalno-instrumentalne/Vocal and instrumental compositions, 3, 49, not in EsAC
+		//              69:     Kompozycje fortepianowe/Piano compositions: 3, 39, not in EsAC
+		//              70:     Pieśni ludu polskiego. Supl. do t.1/Polish folk songs: Supplement to Volume 1: no music
+		m_kinfo.emplace(71,     KolbergInfo("Sandomierskie. Suplement do t. 2 Dzieła Wszystkie",             "Sandomierz Voivodeship. Supplement to vol. 2 of The Complete Works",   3,  40, {116, 117, 134, 135}));
+		m_kinfo.emplace(72.1,   KolbergInfo("Kujawy. Suplement do t. 3 i 4, cz. I, Kuyavia",                 "Supplement to vol. 3 and 4, part I",                                   3,  30, {292, 293, 310, 311}));
+		//              72.2,   Kujawy. Suplement do t. 3 i 4, cz. II, Kuyavia/Supplement to vol. 3 and 4, part II,: missing information about pages?
+		m_kinfo.emplace(73,     KolbergInfo("Krakowsike. Suplement do T. 5-8",                               "Cracow: Supplement to Volumes 5-8",                                   39, 163, {297, 298, 407, 408}));
+		//              74:     Wielkie Księstwo Poznańskie. Suplement do t. 9-15/The Grand Duchy of Poznan. Supplement to vol. 9-15: no music
+		m_kinfo.emplace(75,     KolbergInfo("Lubelskie. Supplement do tomów 16-17",                          "Lublin: Supplement to volumes 16-17",                                  4,  60, {281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324}));
+		m_kinfo.emplace(76,     KolbergInfo("Kieleckie. Supplement do T. 18-19",                             "Kielce: Supplement to Volumes 18-19",                                  5,  41, {}));
+		//              77:     Radomskie. Suplement do t. 20 i 21. I/Radomskie: Supplement to Volumes 20-21. I: complications
+		m_kinfo.emplace(78,     KolbergInfo("Łęczyckie. Suplement do t. 22",                                 "Łęczyca Voivodeship: Supplement to Volume 22",                         3,   1, {}));
+		m_kinfo.emplace(79,     KolbergInfo("Kaliskie. Suplement do t. 23",                                  "Kalisz Region. Supplement to vol. 23",                                 3,  38, {}));
+		m_kinfo.emplace(80,     KolbergInfo("Mazowsze. Suplement do t. 24-28, cz. I",                        "Mazovia. Supplement to vol. 24-28, part I",                            7,  89, {}));
+		m_kinfo.emplace(81,     KolbergInfo("Pokucie. Suplement do tomów 29-32",                             "Corrections: Supplement to Volumes 29-32",                             3,  73, {121, 122, 139, 140}));
+		m_kinfo.emplace(82,     KolbergInfo("Chełmskie. Suplement do T. 33 i 34",                            "Chełm supplement to Volumes 33 and 34",                                7, 105, {}));
+		m_kinfo.emplace(83,     KolbergInfo("Przemyskie. Suplement do tomu 35 DWOK",                         "Przemyśl Voivodeship: Supplement to Volume 35 DWOK",                   9, 112, {380, 381, 382, 383, 384, 385, 386, 387}));
+		m_kinfo.emplace(84,     KolbergInfo("Wołyń. Suplement do t. 36., Volhynia",                          "Supplement to Volume 36",                                             35,  97, {313, 314, 315, 316, 317, 318, 319, 320}));
+
+
+	}
+
+	auto it = m_kinfo.find(volume);
+	if (it != m_kinfo.end()) {
+		return it->second;
+	} else {
+		return KolbergInfo();
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac::getKolbergUrl --
+//
+
+string Tool_esac2hum::getKolbergUrl(int volume) {
+	if ((volume < 1) || (volume > 84)) {
+		// Such a volume does not exist, return empty string.
+		return "";
+	}
+
+	stringstream ss;
+	ss << std::setw(3) << std::setfill('0') << volume;
+	// not https://
+	string url = "http://www.oskarkolberg.pl/MediaFiles/";
+	url += ss.str();
+	url += "dwok.pdf";
+
+	KolbergInfo kinfo = getKolbergInfo(volume);
+	if (kinfo.titlePL.empty()) {
+		// Do not have the page number info for volume, so just give URL for the volume.
+		return url;
+	}
+
+	string pageinfo = m_score.m_params["_page"];
+	int printPage = 0;
+	if (!pageinfo.empty()) {
+		HumRegex hre;
+		if (hre.search(pageinfo, "(\\d+)")) {
+			printPage = hre.getMatchInt(1);
+		} else {
+		     cerr << "XX PRINT PAGE: " << printPage << "\t_page: " << pageinfo << endl;
+		}
+	} else {
+		cerr << "YY PRINT PAGE: " << printPage << "\t_page: IS EMPTY: " << pageinfo << endl;
+	}
+
+	// Calculate the scan page that matches with the print page:
+	int startPrintPage = kinfo.firstPrintPage;
+	int startScanPage  = kinfo.firstScanPage;
+	int scanPage = calculateScanPage(printPage, startPrintPage, startScanPage, kinfo.plates);
+
+	url += "#page=" + to_string(scanPage);
+
+	if (!kinfo.titlePL.empty()) {
+		url += " @PL{Oskar Kolberg Dzieła Wszystkie " + to_string(volume) + ": " + kinfo.titlePL;
+		url += ", s. " + pageinfo;
+		url += "}";
+	}
+
+	if (!kinfo.titleEN.empty()) {
+		url += " @EN{Oskar Kolberg Complete Works " + to_string(volume) + ": " + kinfo.titleEN;
+		url += ", p";
+		if (pageinfo.find("-") != string::npos) {
+			url += "p";
+		}
+		url += "." + pageinfo;
+		url += "}";
+	}
+
+	if (kinfo.titlePL.empty() && kinfo.titleEN.empty()) {
+		url += " @PL{Oskar Kolberg Dzieła Wszystike " + to_string(volume);
+		url += " @PL{Oskar Kolberg Complete Works " + to_string(volume);
+	}
+
+	return url;
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::printKolbergPdfUrl --
+//
+
+void Tool_esac2hum::printKolbergPdfUrl(ostream& output) {
+	string source = m_score.m_params["_source"];
+	HumRegex hre;
+	if (!hre.search(source, "^DWOK(\\d+)")) {
+		return;
+	}
+	int volume = hre.getMatchInt(1);
+	string url = getKolbergUrl(volume);
+	if (!url.empty()) {
+		output << "!!!URL-pdf: " << url << endl;
+	}
+}
+
+
+
+//////////////////////////////
+//
+// Tool_esac2hum::calculateScanPage --
+//
+
+int Tool_esac2hum::calculateScanPage(int inputPrintPage, int printPage, int scanPage, const std::vector<int>& platePages) {
+	int currentPrintPage = printPage;
+	int currentScanPage = scanPage;
+	size_t plateIndex = 0;
+
+	// Iterate until we reach the input print page
+	while (currentPrintPage < inputPrintPage) {
+		++currentScanPage;  // Increment the scan page
+
+		// Check if the current scan page matches the current plate page in the vector
+		if (plateIndex < platePages.size() && currentScanPage == platePages[plateIndex]) {
+			// Skip the plate page (increment scanPage but not printPage)
+			++plateIndex;
+		} else {
+			// If not a plate page, increment the print page
+			++currentPrintPage;
+		}
+	}
+
+	return currentScanPage;
+}
+
+
+
 
 /////////////////////////////////
 //

From f79e85010724d42791bf4197257a056b79dbd7ce Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Mon, 9 Sep 2024 12:45:32 -0700
Subject: [PATCH 09/11] Implement LogRedirectStart/LogRedirectEnd in Toolkit.

---
 include/vrv/toolkit.h |  23 ++++++++++
 src/toolkit.cpp       | 100 ++++++++++++++++++++++--------------------
 2 files changed, 76 insertions(+), 47 deletions(-)

diff --git a/include/vrv/toolkit.h b/include/vrv/toolkit.h
index 8ec0376e71d..3e53fa61352 100644
--- a/include/vrv/toolkit.h
+++ b/include/vrv/toolkit.h
@@ -770,6 +770,17 @@ class Toolkit {
      */
     void ResetLogBuffer();
 
+    /**
+     * Start capturing std::cerr from an external codebase for redirection to vrv::logBuffer.
+     * Only one capture should be active at a given time.  Finish by calling LogRedirectEnd.
+     */
+    void LogRedirectStart();
+
+    /**
+     * End capturing std::cerr from an external codebase for redirection to vrv::logBuffer.
+     */
+    void LogRedirectEnd();
+
 private:
     bool SetFont(const std::string &fontName);
     bool IsUTF16(const std::string &filename);
@@ -805,6 +816,18 @@ class Toolkit {
      */
     char *m_cString;
 
+    /**
+     * Temporary capture buffer for redirecting std::cerr to vrv::LogWarning.
+     * Used to coordinate between LogRedirectStart()/LogRedirectEnd().
+     */
+    std::stringstream m_captured_cerr;
+
+    /**
+     * Temporary storage of the std::cerr read buffer during LogCapture. NULL when not in use.
+     * Used to coordinate between LogRedirectStart()/LogRedirectEnd().
+     */
+    std::streambuf *m_original_cerr_buf = NULL;
+
     EditorToolkit *m_editorToolkit;
 
 #ifndef NO_RUNTIME
diff --git a/src/toolkit.cpp b/src/toolkit.cpp
index cf2c81513db..1387c611599 100644
--- a/src/toolkit.cpp
+++ b/src/toolkit.cpp
@@ -610,16 +610,12 @@ bool Toolkit::LoadData(const std::string &data)
         xmlfile.load_string(data.c_str());
         stringstream conversion;
 
-        // Temporarily redirect cerr:
-        std::stringstream captured_cerr;
-        std::streambuf *cerr_buf = std::cerr.rdbuf();
-        std::cerr.rdbuf(captured_cerr.rdbuf());
-
+        LogRedirectStart();
         bool status = converter.convert(conversion, xmlfile);
-        LogWarning(captured_cerr.str().c_str());
-
-        // Restore cerr:
-        std::cerr.rdbuf(cerr_buf);
+        LogRedirectEnd();
+        if (!status) {
+            LogWarning("Problem converting MusicXML to Humdrum (see warning above this line for possible reasons");
+        }
 
         if (!status) {
             LogError("Error converting MusicXML data");
@@ -669,16 +665,12 @@ bool Toolkit::LoadData(const std::string &data)
         hum::Tool_musedata2hum converter;
         stringstream conversion;
 
-        // Temporarily redirect cerr:
-        std::stringstream captured_cerr;
-        std::streambuf *cerr_buf = std::cerr.rdbuf();
-        std::cerr.rdbuf(captured_cerr.rdbuf());
-
+        LogRedirectStart();
         bool status = converter.convertString(conversion, data);
-        LogWarning(captured_cerr.str().c_str());
-
-        // Restore cerr:
-        std::cerr.rdbuf(cerr_buf);
+        LogRedirectEnd();
+        if (!status) {
+            LogWarning("Problem converting MuseData to Humdrum (see warning above this line for possible reasons");
+        }
 
         if (!status) {
             LogError("Error converting MuseData data");
@@ -706,18 +698,14 @@ bool Toolkit::LoadData(const std::string &data)
     else if (inputFormat == ESAC) {
         // This is the indirect converter from EsAC to MEI using iohumdrum:
         hum::Tool_esac2hum converter;
+        std::stringstream conversion;
 
-        // Temporarily redirect cerr:
-        std::stringstream captured_cerr;
-        std::streambuf *cerr_buf = std::cerr.rdbuf();
-        std::cerr.rdbuf(captured_cerr.rdbuf());
-        stringstream conversion;
-
+        LogRedirectStart();
         bool status = converter.convert(conversion, data);
-        LogWarning(captured_cerr.str().c_str());
-
-        // Restore cerr:
-        std::cerr.rdbuf(cerr_buf);
+        LogRedirectEnd();
+        if (!status) {
+            LogWarning("Problem converting EsAC to Humdrum (see warning above this line for possible reasons");
+        }
 
         if (!status) {
             LogError("Error converting EsAC data");
@@ -1481,6 +1469,35 @@ void Toolkit::ResetLogBuffer()
     logBuffer.clear();
 }
 
+void Toolkit::LogRedirectStart()
+{
+    if (m_original_cerr_buf) {
+        vrv::LogError("In Toolkit::LogRedirectStart: Only one log redirect can be active at a time.");
+        return;
+    }
+    if (!m_captured_cerr.str().empty()) {
+        vrv::LogWarning("In Toolkit::LogRedirectStart: Log capture buffer not empty, sending current contents to "
+                        "LogWarning and resetting.");
+        vrv::LogWarning(m_captured_cerr.str().c_str());
+        m_captured_cerr.str("");
+    }
+    m_original_cerr_buf = std::cerr.rdbuf();
+    std::cerr.rdbuf(m_captured_cerr.rdbuf());
+}
+
+void Toolkit::LogRedirectEnd()
+{
+    if (!m_captured_cerr.str().empty()) {
+        vrv::LogWarning(m_captured_cerr.str().c_str());
+        m_captured_cerr.str("");
+    }
+
+    if (m_original_cerr_buf) {
+        std::cerr.rdbuf(m_original_cerr_buf);
+        m_original_cerr_buf = NULL;
+    }
+}
+
 void Toolkit::RedoLayout(const std::string &jsonOptions)
 {
     bool resetCache = true;
@@ -2074,16 +2091,12 @@ const char *Toolkit::GetHumdrumBuffer()
         stringstream out;
         hum::Tool_mei2hum converter;
 
-        // Temporarily redirect cerr:
-        std::stringstream captured_cerr;
-        std::streambuf *cerr_buf = std::cerr.rdbuf();
-        std::cerr.rdbuf(captured_cerr.rdbuf());
-
-        converter.convert(out, infile);
-        LogWarning(captured_cerr.str().c_str());
-
-        // Restore cerr:
-        std::cerr.rdbuf(cerr_buf);
+        LogRedirectStart();
+        bool status = converter.convert(out, infile);
+        LogRedirectEnd();
+        if (!status) {
+            LogWarning("Problem converting MEI to Humdrum (see warning above this line for possible reasons");
+        }
 
         this->SetHumdrumBuffer(out.str().c_str());
 #endif
@@ -2146,16 +2159,9 @@ std::string Toolkit::ConvertMEIToHumdrum(const std::string &meiData)
     xmlfile.load_string(meiData.c_str());
     std::stringstream conversion;
 
-    // Temporarily redirect cerr:
-    std::stringstream captured_cerr;
-    std::streambuf *cerr_buf = std::cerr.rdbuf();
-    std::cerr.rdbuf(captured_cerr.rdbuf());
-
+    LogRedirectStart();
     bool status = converter.convert(conversion, xmlfile);
-    LogWarning(captured_cerr.str().c_str());
-
-    // Restore cerr:
-    std::cerr.rdbuf(cerr_buf);
+    LogRedirectEnd();
 
     if (!status) {
         LogError("Error converting MEI data to Humdrum: %s", conversion.str().c_str());

From 73f8773025a82f0ba9adb4aa96cd118223c9337e Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Mon, 9 Sep 2024 23:38:04 -0700
Subject: [PATCH 10/11] Rename LogRedirectEnd() to LogRedirectStop().

---
 include/vrv/toolkit.h |  8 ++++----
 src/toolkit.cpp       | 12 ++++++------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/include/vrv/toolkit.h b/include/vrv/toolkit.h
index 3e53fa61352..3fb40f7c703 100644
--- a/include/vrv/toolkit.h
+++ b/include/vrv/toolkit.h
@@ -772,14 +772,14 @@ class Toolkit {
 
     /**
      * Start capturing std::cerr from an external codebase for redirection to vrv::logBuffer.
-     * Only one capture should be active at a given time.  Finish by calling LogRedirectEnd.
+     * Only one capture should be active at a given time.  Finish by calling LogRedirectStop.
      */
     void LogRedirectStart();
 
     /**
      * End capturing std::cerr from an external codebase for redirection to vrv::logBuffer.
      */
-    void LogRedirectEnd();
+    void LogRedirectStop();
 
 private:
     bool SetFont(const std::string &fontName);
@@ -818,13 +818,13 @@ class Toolkit {
 
     /**
      * Temporary capture buffer for redirecting std::cerr to vrv::LogWarning.
-     * Used to coordinate between LogRedirectStart()/LogRedirectEnd().
+     * Used to coordinate between LogRedirectStart()/LogRedirectStop().
      */
     std::stringstream m_captured_cerr;
 
     /**
      * Temporary storage of the std::cerr read buffer during LogCapture. NULL when not in use.
-     * Used to coordinate between LogRedirectStart()/LogRedirectEnd().
+     * Used to coordinate between LogRedirectStart()/LogRedirectStop().
      */
     std::streambuf *m_original_cerr_buf = NULL;
 
diff --git a/src/toolkit.cpp b/src/toolkit.cpp
index 1387c611599..3deffe9607b 100644
--- a/src/toolkit.cpp
+++ b/src/toolkit.cpp
@@ -612,7 +612,7 @@ bool Toolkit::LoadData(const std::string &data)
 
         LogRedirectStart();
         bool status = converter.convert(conversion, xmlfile);
-        LogRedirectEnd();
+        LogRedirectStop();
         if (!status) {
             LogWarning("Problem converting MusicXML to Humdrum (see warning above this line for possible reasons");
         }
@@ -667,7 +667,7 @@ bool Toolkit::LoadData(const std::string &data)
 
         LogRedirectStart();
         bool status = converter.convertString(conversion, data);
-        LogRedirectEnd();
+        LogRedirectStop();
         if (!status) {
             LogWarning("Problem converting MuseData to Humdrum (see warning above this line for possible reasons");
         }
@@ -702,7 +702,7 @@ bool Toolkit::LoadData(const std::string &data)
 
         LogRedirectStart();
         bool status = converter.convert(conversion, data);
-        LogRedirectEnd();
+        LogRedirectStop();
         if (!status) {
             LogWarning("Problem converting EsAC to Humdrum (see warning above this line for possible reasons");
         }
@@ -1485,7 +1485,7 @@ void Toolkit::LogRedirectStart()
     std::cerr.rdbuf(m_captured_cerr.rdbuf());
 }
 
-void Toolkit::LogRedirectEnd()
+void Toolkit::LogRedirectStop()
 {
     if (!m_captured_cerr.str().empty()) {
         vrv::LogWarning(m_captured_cerr.str().c_str());
@@ -2093,7 +2093,7 @@ const char *Toolkit::GetHumdrumBuffer()
 
         LogRedirectStart();
         bool status = converter.convert(out, infile);
-        LogRedirectEnd();
+        LogRedirectStop();
         if (!status) {
             LogWarning("Problem converting MEI to Humdrum (see warning above this line for possible reasons");
         }
@@ -2161,7 +2161,7 @@ std::string Toolkit::ConvertMEIToHumdrum(const std::string &meiData)
 
     LogRedirectStart();
     bool status = converter.convert(conversion, xmlfile);
-    LogRedirectEnd();
+    LogRedirectStop();
 
     if (!status) {
         LogError("Error converting MEI data to Humdrum: %s", conversion.str().c_str());

From bf6b69710c6f6022faefd4e60a52be1f690371eb Mon Sep 17 00:00:00 2001
From: Craig Stuart Sapp <craigsapp@gmail.com>
Date: Tue, 10 Sep 2024 00:21:18 -0700
Subject: [PATCH 11/11] Initialize member variable in contructor.

---
 include/vrv/toolkit.h | 2 +-
 src/toolkit.cpp       | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/include/vrv/toolkit.h b/include/vrv/toolkit.h
index 836d999febf..a537a3c2b0d 100644
--- a/include/vrv/toolkit.h
+++ b/include/vrv/toolkit.h
@@ -831,7 +831,7 @@ class Toolkit {
      * Temporary storage of the std::cerr read buffer during LogCapture. NULL when not in use.
      * Used to coordinate between LogRedirectStart()/LogRedirectStop().
      */
-    std::streambuf *m_original_cerr_buf = NULL;
+    std::streambuf *m_original_cerr_buf;
 
     EditorToolkit *m_editorToolkit;
 
diff --git a/src/toolkit.cpp b/src/toolkit.cpp
index 427b849f752..6a1df8d030f 100644
--- a/src/toolkit.cpp
+++ b/src/toolkit.cpp
@@ -70,6 +70,8 @@ Toolkit::Toolkit(bool initFont)
     m_humdrumBuffer = NULL;
     m_cString = NULL;
 
+    m_original_cerr_buf = NULL;
+
     if (initFont) {
         Resources &resources = m_doc.GetResourcesForModification();
         resources.InitFonts();