/* TextGrid_Sound.cpp
 *
 * Copyright (C) 1992-2020,2022,2024,2025 Paul Boersma
 *
 * This code is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or (at
 * your option) any later version.
 *
 * This code is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this work. If not, see <http://www.gnu.org/licenses/>.
 */

#include "TextGrid_Sound.h"
#include "Pitch_to_PitchTier.h"
#include "SpeechSynthesizer_and_TextGrid.h"
#include "SpeechRecognizer.h"
#include "LongSound.h"

static bool IntervalTier_check (IntervalTier me) {
	for (integer iinterval = 1; iinterval <= my intervals.size; iinterval ++) {
		TextInterval interval = my intervals.at [iinterval];
		if (interval -> xmin >= interval -> xmax) {
			Melder_casual (U"Interval ", iinterval, U" starts at ", interval -> xmin,
				U" but ends at ", interval -> xmax, U" seconds.");
			return false;
		}
	}
	if (my intervals.size < 2)
		return true;
	for (integer iinterval = 1; iinterval < my intervals.size; iinterval ++) {
		TextInterval thisInterval = my intervals.at [iinterval];
		TextInterval nextInterval = my intervals.at [iinterval + 1];
		if (thisInterval -> xmax != nextInterval -> xmin) {
			Melder_casual (U"Interval ", iinterval, U" ends at ", thisInterval -> xmax,
				U" but the next interval starts at ", nextInterval -> xmin, U" seconds.");
			return false;
		}
	}
	return true;
}

static void IntervalTier_insertIntervalDestructively (IntervalTier me, double tmin, double tmax) {
	Melder_assert (tmin < tmax);
	Melder_assert (tmin >= my xmin);
	Melder_assert (tmax <= my xmax);
	/*
		Make sure a boundary exists at tmin.
	*/
	if (! IntervalTier_hasTime (me, tmin)) {   // tmin is not a border but in the middle of some interval
		integer intervalNumber = IntervalTier_timeToIndex (me, tmin);   // this is the interval we will split in two
		Melder_require (intervalNumber > 0,
				U"Cannot add a boundary at ", Melder_fixed (tmin, 6),
				U" seconds, because this is outside the time domain of the intervals.");
		TextInterval leftPart = my intervals.at [intervalNumber];
		/*
			Move the text to the left of the boundary.
		*/
		autoTextInterval rightPart = TextInterval_create (tmin, leftPart -> xmax, U"");
		leftPart -> xmax = tmin;
		my intervals. addItem_move (rightPart.move());
	}

	/*
		Make sure a boundary exists at tmax.
	*/
	if (! IntervalTier_hasTime (me, tmax)) {   // tmin is not a border but in the middle of some interval
		integer intervalNumber = IntervalTier_timeToIndex (me, tmax);   // this is the interval we will split in two
		Melder_require (intervalNumber > 0,
				U"Cannot add a boundary at ", Melder_fixed (tmax, 6),
				U" seconds, because this is outside the time domain of the intervals.");
		TextInterval rightPart = my intervals.at [intervalNumber];
		/*
			Move the text to the right of the boundary.
		*/
		autoTextInterval leftPart = TextInterval_create (rightPart -> xmin, tmax, U"");
		rightPart -> xmin = tmax;
		my intervals. addItem_move (leftPart.move());
	}

	/*
		Collapse all intervals between tmin and tmax into one empty interval.
	*/
	integer firstIntervalNumber = IntervalTier_timeToIndex (me, tmin);
	integer lastIntervalNumber = IntervalTier_hasTime (me, tmax);   // either last or one after last
	Melder_assert (my intervals.at [firstIntervalNumber] -> xmin == tmin);
	Melder_assert (firstIntervalNumber >= 1 && firstIntervalNumber <= my intervals.size);
	Melder_assert (lastIntervalNumber >= 1 && lastIntervalNumber <= my intervals.size);

	trace (U"Remove intervals from ", lastIntervalNumber, U" down to (but not including) ", firstIntervalNumber);
	for (integer iinterval = lastIntervalNumber; iinterval > firstIntervalNumber; iinterval --) {
		TextInterval interval = my intervals.at [iinterval];
		if (interval -> xmin < tmax)   // excluding the last one if it is in fact after last
			my intervals. removeItem (iinterval);
	}
	trace (U"Extend interval ", firstIntervalNumber, U" (starting at ", tmin, U") to ", tmax);
	TextInterval newEmptyInterval = my intervals.at [firstIntervalNumber];
	newEmptyInterval -> xmax = tmax;
	TextInterval_setText (newEmptyInterval, U"");
}

static double IntervalTier_boundaryTimeClosestTo (IntervalTier me, double tmin, double tmax) {
	integer intervalNumber = IntervalTier_timeToLowIndex (me, tmax);
	if (intervalNumber != 0) {
		TextInterval interval = my intervals.at [intervalNumber];
		if (interval -> xmin > tmin && interval -> xmin < tmax)
			return interval -> xmin;
	}
	return 0.5 * (tmin + tmax);
}

static void IntervalTier_removeEmptyIntervals (IntervalTier me, IntervalTier boss) {
	IntervalTier_removeBoundariesBetweenIdenticallyLabeledIntervals (me, U"");
	if (my intervals.size < 2)
		return;
	TextInterval firstInterval = my intervals.at [1];
	if (Melder_equ (firstInterval -> text.get(), U""))
		IntervalTier_removeLeftBoundary (me, 2);
	if (my intervals.size < 2)
		return;
	TextInterval lastInterval = my intervals.at [my intervals.size];
	if (Melder_equ (lastInterval -> text.get(), U""))
		IntervalTier_removeLeftBoundary (me, my intervals.size);
	if (my intervals.size < 3)
		return;
	for (integer iinterval = my intervals.size - 1; iinterval >= 2; iinterval --) {
		TextInterval interval = my intervals.at [iinterval];
		if (Melder_equ (interval -> text.get(), U"")) {
			/*
				Distribute the empty interval between its neigbours.
			*/
			const double newBoundaryTime =
				boss ?
				IntervalTier_boundaryTimeClosestTo (boss, interval -> xmin, interval -> xmax) :
				0.5 * (interval -> xmin + interval -> xmax);
			TextInterval previous = my intervals.at [iinterval - 1];
			TextInterval next = my intervals.at [iinterval + 1];
			previous -> xmax = newBoundaryTime;
			next -> xmin = newBoundaryTime;
			my intervals. removeItem (iinterval);
		}
	}
}

void TextGrid_anySound_alignInterval (
	const TextGrid me, const Function anySound,
	const integer tierNumber, const integer intervalNumber,
	const conststring32 languageName,
	const bool includeWords, const bool includePhonemes
) {
	try {
		//TRACE
		IntervalTier headTier = TextGrid_checkSpecifiedTierIsIntervalTier (me, tierNumber);
		if (intervalNumber < 1 || intervalNumber > headTier -> intervals.size)
			Melder_throw (U"Interval ", intervalNumber, U" does not exist.");
		TextInterval interval = headTier -> intervals.at [intervalNumber];
		if (! includeWords && ! includePhonemes)
			Melder_throw (U"Nothing to be done, because you asked neither for word alignment nor for phoneme alignment.");
		if (Melder_stringMatchesCriterion (headTier -> name.get(), kMelder_string::CONTAINS, U"/", true))
			Melder_throw (U"The current tier already has a slash (\"/\") in its name. Cannot create a word or phoneme tier from it.");
		trace (U"tier ", tierNumber, U" interval ", intervalNumber,
				U" (", interval -> xmin, U" .. ", interval -> xmax, U" “", interval -> text.get(), U"”)");
		autoSound part =
			anySound -> classInfo == classLongSound ? 
				LongSound_extractPart (static_cast <LongSound> (anySound), interval -> xmin, interval -> xmax, true) :
				Sound_extractPart (static_cast <Sound> (anySound), interval -> xmin, interval -> xmax, kSound_windowShape::RECTANGULAR, 1.0, true);
		autoSpeechSynthesizer synthesizer = SpeechSynthesizer_create (languageName, U"Female1");
		synthesizer -> d_samplingFrequency = round (
			anySound -> classInfo == classLongSound ?
				static_cast <LongSound> (anySound) -> sampleRate :
				1.0 / static_cast <Sound> (anySound) -> dx
		);
		double silenceThreshold = -30.0, minSilenceDuration = 0.1, minSoundingDuration = 0.1;
		autoTextGrid analysis;
int tries = 0;
constexpr int maxTries = 5;
again:
		if (! Melder_equ (interval -> text.get(), U""))
			try {
				analysis = SpeechSynthesizer_Sound_TextInterval_align
						(synthesizer.get(), part.get(), interval, silenceThreshold, minSilenceDuration, minSoundingDuration);
			} catch (MelderError) {
				if (++ tries < maxTries) {
					Melder_casual (U"TRY ", tries, U" FAILED (SpeechSynthesizer & Sound & TextInterval: align):");
					Melder_casual (U"    Tier ", tierNumber);
					Melder_casual (U"    Interval ", intervalNumber, U": ", interval -> xmin, U" .. ", interval -> xmax, U" “", interval -> text.get(), U"”");
					Melder_casual (U"REASON OF THIS TEMPORARY FAILURE (now comes a suppressed Praat error message):\n", Melder_getError (), U"(GOING TO RETRY...)");
					Melder_clearError ();
					goto again;
				} else
					Melder_throw (U"All ", maxTries, U" tries failed.");
			}
		if (analysis) {
			/*
				Clean up the analysis.
			*/
			Melder_assert (fabs (analysis -> xmin - interval -> xmin) < 1e-12);
			if (analysis -> xmax != interval -> xmax) {
				Melder_casual (U"Analysis ends at ", analysis -> xmax, U" but interval at ", interval -> xmax, U" seconds.");
				analysis -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (1) -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (2) -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (3) -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (4) -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (1) -> intervals.at [analysis -> intervalTier_cast (1) -> intervals.size] -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (2) -> intervals.at [analysis -> intervalTier_cast (2) -> intervals.size] -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (3) -> intervals.at [analysis -> intervalTier_cast (3) -> intervals.size] -> xmax = interval -> xmax;
				analysis -> intervalTier_cast (4) -> intervals.at [analysis -> intervalTier_cast (4) -> intervals.size] -> xmax = interval -> xmax;
			}
			Melder_assert (analysis -> tiers->size == 4);
			IntervalTier analysisWordTier = analysis -> intervalTier_cast (3);
			tries += 1;
			if (! IntervalTier_check (analysisWordTier)) {
				if (tries < maxTries) {
					Melder_casual (U"TRY ", tries, U" FAILED (SpeechSynthesizer & Sound & TextInterval: align):");
					Melder_casual (U"    Tier ", tierNumber);
					Melder_casual (U"    Interval ", intervalNumber, U": ", interval -> xmin, U" .. ", interval -> xmax, U" “", interval -> text.get(), U"”");
					Melder_casual (U"REASON OF THIS TEMPORARY FAILURE:\n");
					Melder_casual (U"Analysis word tier out of order.\n(GOING TO RETRY...)");
					goto again;
				} else
					Melder_throw (U"All ", maxTries, U" tries failed.");
			} else if (tries > 1)
				Melder_casual (U"(TRY ", tries, U" SUCCEEDED)");
			IntervalTier_removeEmptyIntervals (analysisWordTier, nullptr);
			Melder_assert (analysisWordTier -> xmax == analysis -> xmax);
			Melder_assert (analysisWordTier -> intervals.size >= 1);
			TextInterval firstInterval = analysisWordTier -> intervals.at [1];
			TextInterval lastInterval = analysisWordTier -> intervals.at [analysisWordTier -> intervals.size];
			firstInterval -> xmin = analysis -> xmin;
			lastInterval  -> xmax = analysis -> xmax;
			if (lastInterval -> xmax != analysis -> xmax)
				Melder_crash (U"analysis ends at ", analysis -> xmax, U", but last interval at ", lastInterval -> xmax, U" seconds");
			if (! IntervalTier_check (analysisWordTier))
				Melder_throw (U"Analysis word tier out of order (2).");
			IntervalTier analysisPhonemeTier = analysis -> intervalTier_cast (4);
			if (! IntervalTier_check (analysisPhonemeTier))
				Melder_throw (U"Analysis phoneme tier out of order.");
			IntervalTier_removeEmptyIntervals (analysisPhonemeTier, analysisWordTier);
			Melder_assert (analysisPhonemeTier -> xmax == analysis -> xmax);
			Melder_assert (analysisPhonemeTier -> intervals.size >= 1);
			firstInterval = analysisPhonemeTier -> intervals.at [1];
			lastInterval  = analysisPhonemeTier -> intervals.at [analysisPhonemeTier -> intervals.size];
			firstInterval -> xmin = analysis -> xmin;
			lastInterval  -> xmax = analysis -> xmax;
			Melder_assert (lastInterval -> xmax == analysis -> xmax);
			if (! IntervalTier_check (analysisPhonemeTier))
				Melder_throw (U"Analysis phoneme tier out of order (2).");
		}
		integer wordTierNumber = 0, phonemeTierNumber = 0;
		IntervalTier wordTier = nullptr, phonemeTier = nullptr;
		/*
			Include a word tier.
		*/
		if (includeWords) {
			/*
			 * Make sure that the word tier exists.
			 */
			autoMelderString newWordTierName;
			MelderString_copy (& newWordTierName, headTier -> name.get(), U"/word");
			for (integer itier = 1; itier <= my tiers->size; itier ++) {
				Function tier = my tiers->at [itier];
				if (Melder_equ (newWordTierName.string, tier -> name.get())) {
					if (tier -> classInfo != classIntervalTier)
						Melder_throw (U"A tier with the prospective word tier name (", tier -> name.get(), U") already exists, but it is not an interval tier."
							U"\nPlease change its name or remove it.");
					wordTierNumber = itier;
					break;
				}
			}
			if (! wordTierNumber) {
				autoIntervalTier newWordTier = IntervalTier_create (my xmin, my xmax);
				Thing_setName (newWordTier.get(), newWordTierName.string);
				my tiers -> addItemAtPosition_move (newWordTier.move(), wordTierNumber = tierNumber + 1);
			}
			Melder_assert (wordTierNumber >= 1 && wordTierNumber <= my tiers->size);
			wordTier = static_cast <IntervalTier> (my tiers->at [wordTierNumber]);
			/*
				Make sure that the word tier has boundaries at the edges of the interval.
			*/
			IntervalTier_insertIntervalDestructively (wordTier, interval -> xmin, interval -> xmax);
			/*
				Copy the contents of the word analysis into the interval in the word tier.
			*/
			/* mutable */ integer wordIntervalNumber = IntervalTier_hasTime (wordTier, interval -> xmin);
			Melder_assert (wordIntervalNumber != 0);
			if (analysis) {
				IntervalTier analysisWordTier = analysis -> intervalTier_cast (3);
				if (! IntervalTier_check (analysisWordTier))
					Melder_throw (U"Analysis word tier out of order (3).");
				if (! IntervalTier_check (wordTier))
					Melder_throw (U"Word tier out of order (3).");
				for (integer ianalysisInterval = 1; ianalysisInterval <= analysisWordTier -> intervals.size; ianalysisInterval ++) {
					TextInterval analysisInterval = analysisWordTier -> intervals.at [ianalysisInterval];
					TextInterval wordInterval = nullptr;
					const double tmin = analysisInterval -> xmin, tmax = analysisInterval -> xmax;
					if (tmax == analysis -> xmax) {
						wordInterval = wordTier -> intervals.at [wordIntervalNumber];
						TextInterval_setText (wordInterval, analysisInterval -> text.get());
					} else {
						wordInterval = wordTier -> intervals.at [wordIntervalNumber];
						autoTextInterval newInterval = TextInterval_create (tmin, tmax, analysisInterval -> text.get());
						wordInterval -> xmin = tmax;
						wordTier -> intervals. addItem_move (newInterval.move());
						wordIntervalNumber ++;
					}
				}
				if (! IntervalTier_check (analysisWordTier))
					Melder_throw (U"Analysis word tier out of order (4).");
				if (! IntervalTier_check (wordTier))
					Melder_throw (U"Word tier out of order (4).");
			}
		}
		/*
			Include a phoneme tier.
		*/
		if (includePhonemes) {
			/*
				Make sure that the phoneme tier exists.
			*/
			autoMelderString newPhonemeTierName;
			MelderString_copy (& newPhonemeTierName, headTier -> name.get(), U"/phon");
			for (integer itier = 1; itier <= my tiers->size; itier ++) {
				Function tier = my tiers->at [itier];
				if (Melder_equ (newPhonemeTierName.string, tier -> name.get())) {
					if (tier -> classInfo != classIntervalTier)
						Melder_throw (U"A tier with the prospective phoneme tier name (", tier -> name.get(), U") already exists, but it is not an interval tier."
							U"\nPlease change its name or remove it.");
					phonemeTierNumber = itier;
					break;
				}
			}
			if (! phonemeTierNumber) {
				autoIntervalTier newPhonemeTier = IntervalTier_create (my xmin, my xmax);
				Thing_setName (newPhonemeTier.get(), newPhonemeTierName.string);
				my tiers -> addItemAtPosition_move (newPhonemeTier.move(),
						phonemeTierNumber = wordTierNumber ? wordTierNumber + 1 : tierNumber + 1);
			}
			Melder_assert (phonemeTierNumber >= 1 && phonemeTierNumber <= my tiers->size);
			phonemeTier = my intervalTier_cast (phonemeTierNumber);
			/*
				Make sure that the phoneme tier has boundaries at the edges of the interval.
			*/
			IntervalTier_insertIntervalDestructively (phonemeTier, interval -> xmin, interval -> xmax);
			/*
				Copy the contents of the phoneme analysis into the interval in the phoneme tier.
			*/
			integer phonemeIntervalNumber = IntervalTier_hasTime (phonemeTier, interval -> xmin);
			Melder_assert (phonemeIntervalNumber != 0);
			if (analysis) {
				IntervalTier analysisPhonemeTier = analysis -> intervalTier_cast (4);
				for (integer ianalysisInterval = 1; ianalysisInterval <= analysisPhonemeTier -> intervals.size; ianalysisInterval ++) {
					TextInterval analysisInterval = analysisPhonemeTier -> intervals.at [ianalysisInterval];
					TextInterval phonemeInterval = nullptr;
					double tmin = analysisInterval -> xmin, tmax = analysisInterval -> xmax;
					if (tmax == analysis -> xmax) {
						phonemeInterval = phonemeTier -> intervals.at [phonemeIntervalNumber];
						TextInterval_setText (phonemeInterval, analysisInterval -> text.get());
					} else {
						phonemeInterval = phonemeTier -> intervals.at [phonemeIntervalNumber];
						autoTextInterval newInterval = TextInterval_create (tmin, tmax, analysisInterval -> text.get());
						phonemeInterval -> xmin = tmax;
						phonemeTier -> intervals. addItem_move (newInterval.move());
						phonemeIntervalNumber ++;
					}
				}
			}
			if (includeWords) {
				/*
					Synchronize the boundaries between the word tier and the phoneme tier.
				*/
				//for (integer iinterval = 1; iinterval <=
			}
		}
	} catch (MelderError) {
		Melder_throw (me, U" & ", anySound, U": interval not aligned.");
	}
}

void splitIntervalIntoWhisperSegments (IntervalTier tier, const integer tierNumber,
	const double originalTmin, const double originalTmax,
	autovector <SpeechSegment> const& segments
) {
	for (integer i = 1; i <= segments.size; i ++) {
		SpeechSegment& segment = segments [i];

		const double currentTmin = originalTmin + segment. tmin;
		const double currentTmax = ( i == segments.size ? originalTmax : originalTmin + segment. tmax );

		if (i == 1) {
			const integer originalIntervalNumber = IntervalTier_hasTime (tier, originalTmin);
			TextInterval originalInterval = tier -> intervals.at [originalIntervalNumber];
			originalInterval -> xmax = currentTmax;
			TextInterval_setText (originalInterval, segment. text.get());
		} else {
			autoTextInterval newInterval = TextInterval_create (currentTmin, currentTmax, segment. text.get());
			tier -> intervals. addItem_move (newInterval.move ());
		}
	}

	if (! IntervalTier_check (tier))
		Melder_throw (U"Tier ", tierNumber, U" is out of order.");
}

void TextGrid_Sound_transcribeInterval (
	const TextGrid me, const Sound sound,
	const integer tierNumber, const integer intervalNumber,
	const conststring32 modelName, const conststring32 languageName,
	const bool includeWords, const bool useVad, const double speechProbabilityThreshold,
	const double minNonSpeechDuration, const double minSpeechDuration, const double speechPad,
	const bool includeDiarization, const integer maxNumSpeakers, const bool allowSpeakersOverlap,
	const double clusterThreshold, const double segmentationStep
) {
	/*
		Lambda function to create and return a new tier after a specified tier.
		If overwrite == true and the tier with the given name already exists, return this tier.
	*/
	auto getIntervalTier = [& me] (autoMelderString const& tierName, const integer prevTierNumber, const bool overwrite) {
		/* mutable search */ integer newTierNumber = 0;
		if (overwrite) {
			for (integer i = 1; i <= my tiers->size; i ++) {
				Function tier = my tiers->at [i];
				if (Melder_equ (tierName.string, tier -> name.get())) {
					if (tier -> classInfo != classIntervalTier)
						Melder_throw (U"A tier with the prospective tier name (", tier -> name.get(),
							U") already exists, but it is not an interval tier."
							U"\nPlease change its name or remove it."
						);
					newTierNumber = i;
					break;
				}
			}
		}
		if (! newTierNumber) {
			autoIntervalTier newTier = IntervalTier_create (my xmin, my xmax);
			Thing_setName (newTier.get(), tierName.string);
			newTierNumber = prevTierNumber + 1;
			my tiers -> addItemAtPosition_move (newTier.move(), newTierNumber);
			Melder_assert (newTierNumber >= 1 && newTierNumber <= my tiers->size);
		}
		return newTierNumber;
	};

	/*
		Lambda function to compute an overlap of a given word with the given speaker activity (encoded in a speakerTier).
	*/
	auto computeSpeakerOverlap = [] (SpeechSegment const& wordSegment, IntervalTier speakerTier) {
		const integer intervalStart = IntervalTier_timeToLowIndex (speakerTier, wordSegment. tmin);
		constTextInterval speakerInterval = speakerTier -> intervals.at [intervalStart];
		const conststring32 speakerIntervalText = speakerInterval -> text.get();

		/*
			Note: the assumption here is that one word spans maximum two speaker intervals (silence->speech or speech->silence)
		*/
		/* mutable conditional init */ double overlap;
		if (! speakerIntervalText || speakerIntervalText [0] == U'\0') {   // this word starts during this speaker's silence
			if (speakerInterval -> xmax > wordSegment. tmax)   // the whole word is during this speaker's silence
				overlap = 0.0;
			else
				overlap = wordSegment. tmax - speakerInterval -> xmax;   // second part of the word is during this speaker's speech
		} else {   // this word starts during this speaker's speech
			if (speakerInterval -> xmax > wordSegment. tmax)   // the whole word is during this speaker's speech
				overlap = wordSegment. tmax - wordSegment. tmin;
			else
				overlap = speakerInterval -> xmax - wordSegment. tmin;   // second part of the word is during this speaker's speech
		}
		return overlap;
	};

	try {
		//TRACE
		const integer headTierNumber = tierNumber;
		IntervalTier headTier = TextGrid_checkSpecifiedTierIsIntervalTier (me, headTierNumber);
		autostring32 headTierName = Melder_dup (headTier -> name.get());

		Melder_require (intervalNumber <= headTier -> intervals.size, U"Interval ", intervalNumber, U" does not exist.");
		Melder_require (speechProbabilityThreshold <= 1.0,
				U"The speech probability threshold should be in the interval [0, 1].");
		Melder_require (maxNumSpeakers >= 2, U"The maximum number of speakers should be at least 2.");
		Melder_require (clusterThreshold <= 2.0, U"The clustering threshold should not be greater than 2.0.");
		Melder_require (segmentationStep <= 1.0, U"The segmentation step should not be greater than 1.0.");

		constTextInterval originalInterval = headTier -> intervals.at [intervalNumber];
		const double originalTmin = originalInterval -> xmin;
		const double originalTmax = originalInterval -> xmax;

		if (Melder_stringMatchesCriterion (headTierName.get(), kMelder_string::CONTAINS, U"/", true))
			Melder_throw (U"The current tier already has a slash (\"/\") in its name. Cannot create a word tier from it.");

		trace (U"tier ", headTierNumber, U" interval ", intervalNumber,	U" (", originalTmin, U" .. ", originalTmax, U")");
		autoSound soundPart = Sound_extractPart (sound, originalTmin, originalTmax,
				kSound_windowShape::RECTANGULAR, 1.0, false);
		autoSpeechRecognizer speechRecognizer = SpeechRecognizer_create (modelName, languageName);

		WhisperTranscription whisperTranscription = SpeechRecognizer_recognize (speechRecognizer.get(), soundPart.get(),
				useVad, speechProbabilityThreshold, minNonSpeechDuration, minSpeechDuration, speechPad);
		autovector <autovector <SpeechSegment>> pyannoteDiarization;
		if (includeDiarization)
			pyannoteDiarization = doDiarization (soundPart.get(), maxNumSpeakers,
					allowSpeakersOverlap, clusterThreshold, segmentationStep, U"", U"s");

		autovector <SpeechSegment> wordSegments = whisperTranscription. words.move();
		autovector <SpeechSegment> sentenceSegments = whisperTranscription. sentences.move();
		autovector <autovector <SpeechSegment>> speakerSegments = pyannoteDiarization.move();

		integer numberOfSpeakers = speakerSegments.size;
		/* mutable conditional init */ bool doDiarize = includeDiarization;
		if (doDiarize) {
			if (numberOfSpeakers == 0) {
				Melder_warning (U"Diarization detected 0 speakers. Diarization tiers are not created.");
				doDiarize = false;   // falling back to just transcription without diarization
			} else if (numberOfSpeakers == 1) {
				Melder_warning (U"Diarization detected 1 speaker. Diarization tiers are not created.");
				doDiarize = false;   // falling back to just transcription without diarization
			}
		}

		/*
			----- Sentence transcription. -----
		*/
		if (! includeWords && ! doDiarize)
			splitIntervalIntoWhisperSegments (headTier, headTierNumber, originalTmin, originalTmax, sentenceSegments);

		/*
			----- Sentence transcription + word transcription. -----
		*/
		else if (includeWords && ! doDiarize) {
			splitIntervalIntoWhisperSegments (headTier, headTierNumber, originalTmin, originalTmax, sentenceSegments);

			/*
				Make sure that the word tier exists.
			*/
			autoMelderString wordTierName;
			MelderString_copy (& wordTierName, headTier -> name.get(), U"/word");
			const integer wordTierNumber = getIntervalTier(wordTierName, headTierNumber, true);
			const auto wordTier = static_cast <IntervalTier> (my tiers->at [wordTierNumber]);

			/*
				Insert the interval, and split this big interval into the set of intervals, one interval per word.
			*/
			IntervalTier_insertIntervalDestructively (wordTier, originalTmin, originalTmax);
			splitIntervalIntoWhisperSegments (wordTier, wordTierNumber, originalTmin, originalTmax, wordSegments);
		}

		/*
			----- Diarization: transcription assigned to speakers. -----
		*/
		else if (doDiarize) {
			autovector <IntervalTier> speakerSentenceTiers = newvectorzero <IntervalTier> (numberOfSpeakers);
			autovector <IntervalTier> speakerWordTiers = newvectorzero <IntervalTier> (numberOfSpeakers);
			autovector <autoIntervalTier> virtualSpeakerTiers = newvectorzero <autoIntervalTier> (numberOfSpeakers);

			struct WordWithContext {
				SpeechSegment *whisperSegment;
				autovector <double> overlaps;
				integer resolvedSpeaker;
			};
			autovector <WordWithContext> wordsWithContext = newvectorzero <WordWithContext> (0);

			/*
				Create new sentence tiers for all speakers. Reuse the head tier for speaker 1.
			*/
			autoMelderString speakerSentenceTierName;
			MelderString_copy (& speakerSentenceTierName, headTierName.get(), U"/sp1");
			Thing_setName (headTier, speakerSentenceTierName.string);   // rename the head tier to make it "diarized tier" to prevent running diarization on it in the future
			speakerSentenceTiers [1] = static_cast <IntervalTier> (my tiers->at [headTierNumber]);
			for (integer i = 2; i <= numberOfSpeakers; i ++) {
				MelderString_copy (& speakerSentenceTierName, headTierName.get(), U"/sp", i);
				const integer speakerSentenceTierNumber = getIntervalTier (
						speakerSentenceTierName, headTierNumber + i - 2, false);
				speakerSentenceTiers [i] = static_cast <IntervalTier> (my tiers->at [speakerSentenceTierNumber]);
			}

			/*
				Create virtual diarization tiers , one per speaker, with speech and non-speech intervals.
			*/
			for (integer i = 1; i <= numberOfSpeakers; i ++) {
				autoIntervalTier virtualSpeakerTier = IntervalTier_create (my xmin, my xmax);
				splitIntervalIntoWhisperSegments (virtualSpeakerTier.get(), 0, originalTmin, originalTmax, speakerSegments [i]);
				virtualSpeakerTiers [i] = virtualSpeakerTier.move();
			}

			/*
				Fill in the wordsWithContext vector: words (in a chronological order) and overlaps for each word
			*/
			for (integer s = 1; s <= wordSegments.size; s ++) {
				conststring32 wordSegmentText = wordSegments [s] .text.get();
				if (! wordSegmentText || wordSegmentText [0] == U'\0')
					continue;

				WordWithContext *wordWithContext = wordsWithContext. append();
				wordWithContext -> whisperSegment = & wordSegments [s];
				wordWithContext -> overlaps = newvectorzero <double> (numberOfSpeakers);
				wordWithContext -> resolvedSpeaker = 0;

				for (integer i = 1; i <= numberOfSpeakers; i ++)
					wordWithContext -> overlaps [i] = computeSpeakerOverlap (wordSegments [s], virtualSpeakerTiers [i].get());
			}

			/*
				Find a dominant speaker for each word.
			*/
			for (integer s = 1; s <= wordsWithContext.size; s ++) {
				const constvector <double> overlaps = wordsWithContext [s]. overlaps.get();
				const double tmin = wordsWithContext [s]. whisperSegment -> tmin;
				const double tmax = wordsWithContext [s]. whisperSegment -> tmax;
				const double prevTmax = ( s > 1 ? wordsWithContext [s - 1]. whisperSegment -> tmax : wordsWithContext [1]. whisperSegment -> tmin );
				const integer prevResolvedSpeaker = ( s > 1 ? wordsWithContext [s - 1]. resolvedSpeaker : 0 );

				/* mutable search */ integer resolvedSpeaker = 1;
				/* mutable search */ double longestOverlap = overlaps [1];
				for (integer i = 2; i <= numberOfSpeakers; i ++) {
					if (overlaps [i] > longestOverlap) {
						resolvedSpeaker = i;
						longestOverlap = overlaps [i];
					} else if (overlaps [i] == longestOverlap && prevResolvedSpeaker == i) {   // fallback to the last speaker
						resolvedSpeaker = i;
					}
				}

				if (s > 1 &&  longestOverlap < (tmax - tmin) / 2 && prevTmax == tmin)   // if overlap is less than half, fallback to the last speaker
					resolvedSpeaker = prevResolvedSpeaker;
				wordsWithContext [s]. resolvedSpeaker = resolvedSpeaker;
			}

			/*
				Lambda function to insert a subsentence to a speaker tier.
			*/
			auto insertSubsentenceToSpeakerTier = [&] (const integer subsentenceSpeaker, autoMelderString const& subsentenceText,
				const integer firstWordInSubsentence, const integer lastWordInSubsentence,
				const integer firstWordInSentence, const integer lastWordInSentence)
			{
				const double subsentenceTmin = originalTmin + wordsWithContext [firstWordInSubsentence]. whisperSegment -> tmin;
				const double subsentenceTmax = originalTmin + wordsWithContext [lastWordInSubsentence]. whisperSegment -> tmax;
				IntervalTier speakerSubsentenceTier = speakerSentenceTiers [subsentenceSpeaker];
				Melder_assert (subsentenceTmin < subsentenceTmax);
				IntervalTier_insertIntervalDestructively (speakerSubsentenceTier, subsentenceTmin, subsentenceTmax);

				autoMelderString fullText;
				if (firstWordInSubsentence != firstWordInSentence)   // before
					MelderString_append (& fullText, U"... ");
				MelderString_append (& fullText, subsentenceText.string);   // text
				if (lastWordInSubsentence == lastWordInSentence)   // after
					MelderString_append (& fullText, U".");
				else   // also after
					MelderString_append (& fullText, U"...");

				const integer subsentenceIntervalNumber = IntervalTier_hasTime (speakerSubsentenceTier, subsentenceTmin);
				TextInterval_setText (speakerSubsentenceTier -> intervals.at [subsentenceIntervalNumber], fullText.string);
			};

			/*
				Create sub-sentences, with first-word-speaker correction.
			*/
			/* mutable increment */ integer currentWord = 1;
			for (integer sentence = 1; sentence <= sentenceSegments.size; sentence ++) {
				const double sentenceTmax = sentenceSegments [sentence]. tmax;
				const conststring32 sentenceText = sentenceSegments [sentence]. text.get();
				if (! sentenceText || sentenceText [0] == U'\0')
					continue;

				/*
					Find the first and the last words in the current sentence.
				*/
				const integer firstWordInSentence = currentWord;
				while (currentWord <= wordsWithContext.size && wordsWithContext [currentWord]. whisperSegment -> tmin < sentenceTmax)
					currentWord ++;
				const integer lastWordInSentence = currentWord - 1;

				/*
					If the first word speaker is different from the second word speaker, reassign the first word to the second's word speaker.
				*/
				if (firstWordInSentence < lastWordInSentence) {
					const integer firstSpeaker = wordsWithContext [firstWordInSentence]. resolvedSpeaker;
					const integer secondSpeaker = wordsWithContext [firstWordInSentence + 1]. resolvedSpeaker;
					if (firstSpeaker != secondSpeaker)
						wordsWithContext [firstWordInSentence]. resolvedSpeaker = secondSpeaker;
				}

				/*
					Initialise the first subsentence.
				*/
				/* mutable search */ integer subsentenceSpeaker = wordsWithContext [firstWordInSentence]. resolvedSpeaker;
				/* mutable search */ integer firstWordInSubsentence = firstWordInSentence;
				autoMelderString subsentenceText;
				MelderString_copy (& subsentenceText, wordsWithContext [firstWordInSubsentence]. whisperSegment -> text.get());

				/*
					Iterate over all the words in the current sentence, inserting all the subsentence intervals except the last one.
				*/
				for (integer i = firstWordInSentence + 1; i <= lastWordInSentence; i ++) {
					const integer currentSpeaker = wordsWithContext [i]. resolvedSpeaker;
					if (currentSpeaker != subsentenceSpeaker) {
						insertSubsentenceToSpeakerTier (subsentenceSpeaker, subsentenceText,
								firstWordInSubsentence, i - 1, firstWordInSentence, lastWordInSentence);
						subsentenceSpeaker = currentSpeaker;
						firstWordInSubsentence = i;
						MelderString_copy (& subsentenceText, wordsWithContext [i]. whisperSegment -> text.get());
					} else {
						MelderString_append (& subsentenceText, U" ", wordsWithContext [i]. whisperSegment -> text.get());
					}
				}

				/*
					Insert the last interval subsentence.
				*/
				insertSubsentenceToSpeakerTier (subsentenceSpeaker, subsentenceText,
						firstWordInSubsentence, lastWordInSentence, firstWordInSentence, lastWordInSentence);
			}

			/*
				----- Word transcription assigned to speakers. -----
			*/
			if (includeWords)  {
				/*
					Create new word tiers for all speakers.
				*/
				for (integer i = 1; i <= numberOfSpeakers; i ++) {
					autoMelderString speakerWordTierName;
					MelderString_copy (& speakerWordTierName, headTierName.get(), U"/sp", i, U"/w");
					const integer speakerWordTierNumber = getIntervalTier(   // headTierNumber headTierNumber+(1+1) headTierNumber+(1+1)+(1+1)
							speakerWordTierName, headTierNumber + 2 * (i - 1), false);
					speakerWordTiers [i] = static_cast <IntervalTier> (my tiers->at [speakerWordTierNumber]);
				}

				/*
					Insert words into speaker word tiers.
				*/
				for (integer s = 1; s <= wordsWithContext.size; s ++) {
					const integer resolvedSpeaker = wordsWithContext [s]. resolvedSpeaker;
					const double tmin = originalTmin + wordsWithContext [s]. whisperSegment -> tmin;
					const double tmax = originalTmin + wordsWithContext [s]. whisperSegment -> tmax;
					const conststring32 text = wordsWithContext [s]. whisperSegment -> text.get();

					Melder_assert (tmin < tmax);
					IntervalTier_insertIntervalDestructively (speakerWordTiers [resolvedSpeaker], tmin, tmax);
					const integer wordIntervalNumber = IntervalTier_hasTime (speakerWordTiers [resolvedSpeaker], tmin);
					TextInterval_setText (speakerWordTiers [resolvedSpeaker] -> intervals.at [wordIntervalNumber], text);
				}
			}
		}
	} catch (MelderError) {
		Melder_throw (me, U" & ", sound, U": interval not transcribed.");
	}
}

void TextGrid_Sound_diarizeInterval (
	const TextGrid me, const Sound sound,
	const integer tierNumber, const integer intervalNumber,
	const integer maxNumSpeakers, const bool allowSpeakersOverlap,
	const conststring32 nonSpeechLabel, const conststring32 speechLabel,
	const double clusterThreshold, const double segmentationStep
) {
	/*
		Lambda function to create and return a new tier after a specified tier.
		If overwrite == true and the tier with the given name already exists, return this tier.
	*/
	auto getIntervalTier = [& me] (autoMelderString const& tierName, const integer prevTierNumber, const bool overwrite) {
		/* mutable search */ integer newTierNumber = 0;
		if (overwrite) {
			for (integer i = 1; i <= my tiers->size; i ++) {
				Function tier = my tiers->at [i];
				if (Melder_equ (tierName.string, tier -> name.get())) {
					if (tier -> classInfo != classIntervalTier)
						Melder_throw (U"A tier with the prospective tier name (", tier -> name.get(),
							U") already exists, but it is not an interval tier."
							U"\nPlease change its name or remove it."
						);
					newTierNumber = i;
					break;
				}
			}
		}
		if (! newTierNumber) {
			autoIntervalTier newTier = IntervalTier_create (my xmin, my xmax);
			Thing_setName (newTier.get(), tierName.string);
			newTierNumber = prevTierNumber + 1;
			my tiers -> addItemAtPosition_move (newTier.move(), newTierNumber);
			Melder_assert (newTierNumber >= 1 && newTierNumber <= my tiers->size);
		}
		return newTierNumber;
	};

	try {
		//TRACE
		const integer headTierNumber = tierNumber;
		IntervalTier headTier = TextGrid_checkSpecifiedTierIsIntervalTier (me, headTierNumber);
		autostring32 headTierName = Melder_dup (headTier -> name.get());

		Melder_require (intervalNumber <= headTier -> intervals.size, U"Interval ", intervalNumber, U" does not exist.");
		Melder_require (maxNumSpeakers >= 2, U"The maximum number of speakers should be at least 2");
		Melder_require (clusterThreshold <= 2.0, U"The clustering threshold should not be greater than 2.0.");
		Melder_require (segmentationStep <= 1.0, U"The segmentation step should not be greater than 1.0.");

		constTextInterval originalInterval = headTier -> intervals.at [intervalNumber];
		const double originalTmin = originalInterval -> xmin;
		const double originalTmax = originalInterval -> xmax;

		if (Melder_stringMatchesCriterion (headTierName.get(), kMelder_string::CONTAINS, U"/", true))
			Melder_throw (U"The current tier already has a slash (\"/\") in its name. Cannot create a speaker tier from it.");

		trace (U"tier ", headTierNumber, U" interval ", intervalNumber,	U" (", originalTmin, U" .. ", originalTmax, U")");
		autoSound soundPart = Sound_extractPart (sound, originalTmin, originalTmax,
				kSound_windowShape::RECTANGULAR, 1.0, false);

		autovector <autovector <SpeechSegment>> speakerSegments = doDiarization (soundPart.get(),
			maxNumSpeakers, allowSpeakersOverlap, clusterThreshold, segmentationStep,
			nonSpeechLabel, speechLabel
		);

		integer numberOfSpeakers = speakerSegments.size;
		if (numberOfSpeakers < 1)
			Melder_throw (U"Diarization detected 0 speakers. Diarization tiers are not created.");

		autovector <IntervalTier> speakerTiers = newvectorzero <IntervalTier> (numberOfSpeakers);

		/*
			Create new speaker tiers for all speakers. Reuse the head tier for speaker 1.
		*/
		autoMelderString speakerTierName;
		MelderString_copy (& speakerTierName, headTierName.get(), U"/sp1");
		Thing_setName (headTier, speakerTierName.string);   // rename the head tier to make it "diarized tier" to prevent running diarization on it in the future
		speakerTiers [1] = static_cast <IntervalTier> (my tiers->at [headTierNumber]);
		splitIntervalIntoWhisperSegments (speakerTiers [1], headTierNumber, originalTmin, originalTmax, speakerSegments [1]);

		for (integer i = 2; i <= numberOfSpeakers; i ++) {
			MelderString_copy (& speakerTierName, headTierName.get(), U"/sp", i);
			const integer speakerTierNumber = getIntervalTier(
					speakerTierName, headTierNumber + i - 2, false);
			speakerTiers [i] = static_cast <IntervalTier> (my tiers->at [speakerTierNumber]);
			splitIntervalIntoWhisperSegments (speakerTiers [i], speakerTierNumber, originalTmin, originalTmax, speakerSegments [i]);
		}
	} catch (MelderError) {
		Melder_throw (me, U" & ", sound, U": interval not diarized.");
	}
}

void TextGrid_Sound_draw (
	const TextGrid me, const Sound sound, const Graphics g,
	/* mutable autowindow */ double tmin, /* mutable autowindow */ double tmax,
	const bool showBoundaries, const bool useTextStyles, const bool garnish
) {
	const integer numberOfTiers = my tiers ->size;

	Function_unidirectionalAutowindow (me, & tmin, & tmax);

	Graphics_setInner (g);
	Graphics_setWindow (g, tmin, tmax, -1.0 - 0.5 * numberOfTiers, 1.0);

	/*
		Draw sound in upper part.
	*/
	integer first, last;
	if (sound && Sampled_getWindowSamples (sound, tmin, tmax, & first, & last) > 1) {
		Graphics_setLineType (g, Graphics_DOTTED);
		Graphics_line (g, tmin, 0.0, tmax, 0.0);
		Graphics_setLineType (g, Graphics_DRAWN);
		Graphics_function (g, & sound -> z [1] [0], first, last,
				Sampled_indexToX (sound, first), Sampled_indexToX (sound, last));
	}

	/*
		Draw labels in lower part.
	*/
	Graphics_setTextAlignment (g, Graphics_CENTRE, Graphics_HALF);
	Graphics_setPercentSignIsItalic (g, useTextStyles);
	Graphics_setNumberSignIsBold (g, useTextStyles);
	Graphics_setCircumflexIsSuperscript (g, useTextStyles);
	Graphics_setUnderscoreIsSubscript (g, useTextStyles);
	for (integer itier = 1; itier <= numberOfTiers; itier ++) {
		Function anyTier = my tiers->at [itier];
		const double ymin = -1.0 - 0.5 * itier, ymax = ymin + 0.5;
		Graphics_rectangle (g, tmin, tmax, ymin, ymax);
		if (anyTier -> classInfo == classIntervalTier) {
			IntervalTier tier = static_cast <IntervalTier> (anyTier);
			const integer ninterval = tier -> intervals.size;
			for (integer iinterval = 1; iinterval <= ninterval; iinterval ++) {
				TextInterval interval = tier -> intervals.at [iinterval];
				const double intmin = Melder_clippedLeft (tmin, interval -> xmin);
				const double intmax = Melder_clippedRight (interval -> xmax, tmax);
				if (intmin >= intmax)
					continue;
				if (showBoundaries && intmin > tmin && intmin < tmax) {
					Graphics_setLineType (g, Graphics_DOTTED);
					Graphics_line (g, intmin, -1.0, intmin, 1.0);   // in sound part
					Graphics_setLineType (g, Graphics_DRAWN);
				}
				/*
					Draw left boundary.
				*/
				if (intmin > tmin && intmin < tmax)
					Graphics_line (g, intmin, ymin, intmin, ymax);
				/*
					Draw label text.
				*/
				if (interval -> text && intmax >= tmin && intmin <= tmax) {
					const double t1 = tmin > intmin ? tmin : intmin;
					const double t2 = tmax < intmax ? tmax : intmax;
					Graphics_text (g, 0.5 * (t1 + t2), 0.5 * (ymin + ymax), interval -> text.get());
				}
			}
		} else {
			TextTier tier = static_cast <TextTier> (anyTier);
			const integer numberOfPoints = tier -> points.size;
			for (integer ipoint = 1; ipoint <= numberOfPoints; ipoint ++) {
				TextPoint point = tier -> points.at [ipoint];
				const double t = point -> number;
				if (t > tmin && t < tmax) {
					if (showBoundaries) {
						Graphics_setLineType (g, Graphics_DOTTED);
						Graphics_line (g, t, -1.0, t, 1.0);   // in sound part
						Graphics_setLineType (g, Graphics_DRAWN);
					}
					Graphics_line (g, t, ymin, t, 0.8 * ymin + 0.2 * ymax);
					Graphics_line (g, t, 0.2 * ymin + 0.8 * ymax, t, ymax);
					if (point -> mark)
						Graphics_text (g, t, 0.5 * (ymin + ymax), point -> mark.get());
				}
			}
		}
	}
	Graphics_setPercentSignIsItalic (g, true);
	Graphics_setNumberSignIsBold (g, true);
	Graphics_setCircumflexIsSuperscript (g, true);
	Graphics_setUnderscoreIsSubscript (g, true);
	Graphics_unsetInner (g);
	if (garnish) {
		Graphics_drawInnerBox (g);
		Graphics_textBottom (g, true, U"Time (s)");
		Graphics_marksBottom (g, 2, true, true, false);
	}
}

autoSoundList TextGrid_Sound_extractAllIntervals (TextGrid me, Sound sound, integer tierNumber, bool preserveTimes) {
	try {
		IntervalTier tier = TextGrid_checkSpecifiedTierIsIntervalTier (me, tierNumber);
		autoSoundList list = SoundList_create ();
		for (integer iseg = 1; iseg <= tier -> intervals.size; iseg ++) {
			TextInterval segment = tier -> intervals.at [iseg];
			autoSound interval = Sound_extractPart (sound, segment -> xmin, segment -> xmax, kSound_windowShape::RECTANGULAR, 1.0, preserveTimes);
			Thing_setName (interval.get(), segment -> text ? segment -> text.get() : U"untitled");
			list -> addItem_move (interval.move());
		}
		return list;
	} catch (MelderError) {
		Melder_throw (me, U" & ", sound, U": intervals not extracted.");
	}
}

autoSoundList TextGrid_Sound_extractNonemptyIntervals (TextGrid me, Sound sound, integer tierNumber, bool preserveTimes) {
	try {
		IntervalTier tier = TextGrid_checkSpecifiedTierIsIntervalTier (me, tierNumber);
		autoSoundList list = SoundList_create ();
		for (integer iseg = 1; iseg <= tier -> intervals.size; iseg ++) {
			TextInterval segment = tier -> intervals.at [iseg];
			if (segment -> text && segment -> text [0] != U'\0') {
				autoSound interval = Sound_extractPart (sound, segment -> xmin, segment -> xmax, kSound_windowShape::RECTANGULAR, 1.0, preserveTimes);
				Thing_setName (interval.get(), segment -> text ? segment -> text.get() : U"untitled");
				list -> addItem_move (interval.move());
			}
		}
		if (list->size == 0)
			Melder_warning (U"No non-empty intervals were found.");
		return list;
	} catch (MelderError) {
		Melder_throw (me, U" & ", sound, U": non-empty intervals not extracted.");
	}
}

autoSoundList TextGrid_Sound_extractIntervalsWhere (TextGrid me, Sound sound, integer tierNumber,
	kMelder_string which, conststring32 text, bool preserveTimes)
{
	try {
		IntervalTier tier = TextGrid_checkSpecifiedTierIsIntervalTier (me, tierNumber);
		autoSoundList list = SoundList_create ();
		/* mutable counter */ integer count = 0;
		for (integer iseg = 1; iseg <= tier -> intervals.size; iseg ++) {
			const TextInterval segment = tier -> intervals.at [iseg];
			if (Melder_stringMatchesCriterion (segment -> text.get(), which, text, true)) {
				autoSound interval = Sound_extractPart (sound, segment -> xmin, segment -> xmax, kSound_windowShape::RECTANGULAR, 1.0, preserveTimes);
				Thing_setName (interval.get(), Melder_cat (sound -> name ? sound -> name.get() : U"", U"_", text, U"_", ++ count));
				list -> addItem_move (interval.move());
			}
		}
		if (list->size == 0)
			Melder_warning (U"No label that ", kMelder_string_getText (which), U" the text \"", text, U"\" was found.");
		return list;
	} catch (MelderError) {
		Melder_throw (me, U" & ", sound, U": intervals not extracted.");
	}
}

static void autoMarks (Graphics g, double ymin, double ymax, bool haveDottedLines) {
	const double dy = ymax - ymin;
	if (dy < 26.0) {
		const integer imin = Melder_iroundUp ((ymin + 2.0) / 5.0), imax = Melder_ifloor ((ymax - 2.0) / 5.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 5.0, true, true, haveDottedLines, nullptr);
	} else if (dy < 110.0) {
		const integer imin = Melder_iroundUp ((ymin + 8.0) / 20.0), imax = Melder_ifloor ((ymax - 8.0) / 20.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 20.0, true, true, haveDottedLines, nullptr);
	} else if (dy < 260.0) {
		const integer imin = Melder_iroundUp ((ymin + 20.0) / 50.0), imax = Melder_ifloor ((ymax - 20.0) / 50.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 50.0, true, true, haveDottedLines, nullptr);
	} else if (dy < 510.0) {
		const integer imin = Melder_iroundUp ((ymin + 40.0) / 100.0), imax = Melder_ifloor ((ymax - 40.0) / 100.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 100.0, true, true, haveDottedLines, nullptr);
	}
}

static void autoMarks_logarithmic (Graphics g, double ymin, double ymax, bool haveDottedLines) {
	const double fy = ymax / ymin;
	for (int i = -12; i <= 12; i ++) {
		const double power = pow (10, i);
		/* mutable */ double y = power;
		if (y > ymin * 1.2 && y < ymax / 1.2)
			Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
		if (fy > 2100) {
			;   // enough
		} else if (fy > 210) {
			y = 3.0 * power;
			if (y > ymin * 1.2 && y < ymax / 1.2)
				Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
		} else {
			y = 2.0 * power;
			if (y > ymin * 1.2 && y < ymax / 1.2)
				Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
			y = 5.0 * power;
			if (y > ymin * 1.2 && y < ymax / 1.2)
				Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
			if (fy < 21) {
				y = 3.0 * power;
				if (y > ymin * 1.2 && y < ymax / 1.2)
					Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
				y = 7.0 * power;
				if (y > ymin * 1.2 && y < ymax / 1.2)
					Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
			}
			if (fy < 4.1) {
				y = 1.5 * power;
				if (y > ymin * 1.2 && y < ymax / 1.2)
					Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
				y = 4.0 * power;
				if (y > ymin * 1.2 && y < ymax / 1.2)
					Graphics_markLeftLogarithmic (g, y, true, true, haveDottedLines, nullptr);
			}
		}
	}
}

static void autoMarks_semitones (Graphics g, double ymin, double ymax, bool haveDottedLines) {
	const double dy = ymax - ymin;
	if (dy < 16.0) {
		const integer imin = Melder_iroundUp ((ymin + 1.2) / 3.0), imax = Melder_ifloor ((ymax - 1.2) / 3.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 3.0, true, true, haveDottedLines, nullptr);
	} else if (dy < 32.0) {
		const integer imin = Melder_iroundUp ((ymin + 2.4) / 6.0), imax = Melder_ifloor ((ymax - 2.4) / 6.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 6.0, true, true, haveDottedLines, nullptr);
	} else if (dy < 64.0) {
		const integer imin = Melder_iroundUp ((ymin + 4.8) / 12.0), imax = Melder_ifloor ((ymax - 4.8) / 12.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 12.0, true, true, haveDottedLines, nullptr);
	} else if (dy < 128.0) {
		const integer imin = Melder_iroundUp ((ymin + 9.6) / 24.0), imax = Melder_ifloor ((ymax - 9.6) / 24.0);
		for (integer i = imin; i <= imax; i ++)
			Graphics_markLeft (g, i * 24.0, true, true, haveDottedLines, nullptr);
	}
}

void TextGrid_Pitch_drawSeparately (
	const TextGrid grid, const Pitch pitch, const Graphics g,
	/* mutable autowindow */ double tmin, /* mutable autowindow */ double tmax,
	/* mutable conversion */ double fmin, /* mutable conversion */ double fmax,
	const bool showBoundaries, const bool useTextStyles, const bool garnish, const bool speckle, const kPitch_unit unit
) {
	const integer numberOfTiers = grid -> tiers->size;
	Function_unidirectionalAutowindow (grid, & tmin, & tmax);
	if (Function_isUnitLogarithmic (pitch, Pitch_LEVEL_FREQUENCY, (int) unit)) {
		fmin = Function_convertStandardToSpecialUnit (pitch, fmin, Pitch_LEVEL_FREQUENCY, (int) unit);
		fmax = Function_convertStandardToSpecialUnit (pitch, fmax, Pitch_LEVEL_FREQUENCY, (int) unit);
	}
	if (unit == kPitch_unit::HERTZ_LOGARITHMIC)
		Pitch_draw (pitch, g, tmin, tmax, pow (10.0, fmin - 0.25 * (fmax - fmin) * numberOfTiers), pow (10.0, fmax), false, speckle, unit);
	else
		Pitch_draw (pitch, g, tmin, tmax, fmin - 0.25 * (fmax - fmin) * numberOfTiers, fmax, false, speckle, unit);
	TextGrid_Sound_draw (grid, nullptr, g, tmin, tmax, showBoundaries, useTextStyles, false);
	/*
		Restore window for the sake of margin drawing.
	*/
	Graphics_setWindow (g, tmin, tmax, fmin - 0.25 * (fmax - fmin) * numberOfTiers, fmax);
	if (unit == kPitch_unit::HERTZ_LOGARITHMIC)
		fmin = pow (10, fmin), fmax = pow (10.0, fmax);
	if (garnish) {
		Graphics_drawInnerBox (g);
		if (unit == kPitch_unit::HERTZ_LOGARITHMIC) {
			Graphics_markLeftLogarithmic (g, fmin, true, true, false, nullptr);
			Graphics_markLeftLogarithmic (g, fmax, true, true, false, nullptr);
			autoMarks_logarithmic (g, fmin, fmax, false);
		} else if (unit == kPitch_unit::SEMITONES_100) {
			Graphics_markLeft (g, fmin, true, true, false, nullptr);
			Graphics_markLeft (g, fmax, true, true, false, nullptr);
			autoMarks_semitones (g, fmin, fmax, false);
		} else {
			Graphics_markLeft (g, fmin, true, true, false, nullptr);
			Graphics_markLeft (g, fmax, true, true, false, nullptr);
			autoMarks (g, fmin, fmax, false);
		}
		Graphics_textLeft (g, true, Melder_cat (U"Pitch (", Function_getUnitText (pitch, Pitch_LEVEL_FREQUENCY, (int) unit, Function_UNIT_TEXT_GRAPHICAL), U")"));
		Graphics_textBottom (g, true, U"Time (s)");
		Graphics_marksBottom (g, 2, true, true, false);
	}
}

void TextGrid_Pitch_draw (
	const TextGrid grid, const Pitch pitch, const Graphics g,
	const integer tierNumber,
	/* mutable autowindow */ double tmin, /* mutable autowindow */ double tmax,
	/* mutable conversion */ double fmin, /* mutable conversion */ double fmax,
	const double fontSize, const bool useTextStyles, const int horizontalAlignment, const bool garnish, const bool speckle, const kPitch_unit unit
) {
	try {
		Function anyTier = TextGrid_checkSpecifiedTierNumberWithinRange (grid, tierNumber);
		const double oldFontSize = Graphics_inqFontSize (g);
		Pitch_draw (pitch, g, tmin, tmax, fmin, fmax, garnish, speckle, unit);
		Function_unidirectionalAutowindow (grid, & tmin, & tmax);
		autoPitchTier pitchTier = Pitch_to_PitchTier (pitch);
		if (Function_isUnitLogarithmic (pitch, Pitch_LEVEL_FREQUENCY, (int) unit)) {
			fmin = Function_convertStandardToSpecialUnit (pitch, fmin, Pitch_LEVEL_FREQUENCY, (int) unit);
			fmax = Function_convertStandardToSpecialUnit (pitch, fmax, Pitch_LEVEL_FREQUENCY, (int) unit);
		}
		Graphics_setTextAlignment (g, (kGraphics_horizontalAlignment) horizontalAlignment, Graphics_BOTTOM);
		Graphics_setInner (g);
		Graphics_setFontSize (g, fontSize);
		Graphics_setPercentSignIsItalic (g, useTextStyles);
		Graphics_setNumberSignIsBold (g, useTextStyles);
		Graphics_setCircumflexIsSuperscript (g, useTextStyles);
		Graphics_setUnderscoreIsSubscript (g, useTextStyles);
		if (anyTier -> classInfo == classIntervalTier) {
			const IntervalTier tier = static_cast <IntervalTier> (anyTier);
			for (integer i = 1; i <= tier -> intervals.size; i ++) {
				TextInterval interval = tier -> intervals.at [i];
				if (! interval -> text || ! interval -> text [0])
					continue;
				const double tleft = Melder_clippedLeft (pitch -> xmin, interval -> xmin);
				const double tright = Melder_clippedRight (interval -> xmax, pitch -> xmax);
				const double tmid = 0.5 * (tleft + tright);
				if (tmid < tmin || tmid > tmax)
					continue;
				const double f0 = Function_convertStandardToSpecialUnit (pitch, RealTier_getValueAtTime (pitchTier.get(), tmid), Pitch_LEVEL_FREQUENCY, (int) unit);
				if (f0 < fmin || f0 > fmax)
					continue;
				Graphics_text (g,
					horizontalAlignment == (int) Graphics_LEFT ? tleft : horizontalAlignment == (int) Graphics_RIGHT ? tright : tmid,
					f0, interval -> text.get()
				);
			}
		} else {
			const TextTier tier = static_cast <TextTier> (anyTier);
			for (integer i = 1; i <= tier -> points.size; i ++) {
				TextPoint point = tier -> points.at [i];
				const double t = point -> number;
				if (! point -> mark || ! point -> mark [0])
					continue;
				if (t < tmin || t > tmax)
					continue;
				const double f0 = Function_convertStandardToSpecialUnit (pitch, RealTier_getValueAtTime (pitchTier.get(), t), Pitch_LEVEL_FREQUENCY, (int) unit);
				if (f0 < fmin || f0 > fmax)
					continue;
				Graphics_text (g, t, f0, point -> mark.get());
			}
		}
		Graphics_setPercentSignIsItalic (g, true);
		Graphics_setNumberSignIsBold (g, true);
		Graphics_setCircumflexIsSuperscript (g, true);
		Graphics_setUnderscoreIsSubscript (g, true);
		Graphics_setFontSize (g, oldFontSize);
		Graphics_unsetInner (g);
	} catch (MelderError) {
		Melder_throw (grid, U" & ", pitch, U": not drawn.");
	}
}

autoSound Sound_readWithAdjacentAnnotationFiles_buckeye (conststring32 soundFileName, autoTextGrid *out_textgrid) {
	try {
		structMelderFile file { };
		Melder_pathToFile (soundFileName, & file);
		char32 *lastPeriod = str32rchr (file.path, U'.');
		Melder_require (lastPeriod,
			U"Sound file name should have an extension, but is ", & file, U".");
		Melder_require (Melder_equ (lastPeriod, U".wav"),
			U"Sound file name should end in “.wav”, not in “", lastPeriod, U"”.");
		autoSound sound = Sound_readFromSoundFile (& file);
		OrderedOf <structTextGrid> textgrids;

		/*
			Read the .phones file.
		*/
		lastPeriod [1] = U'p';
		lastPeriod [2] = U'h';
		lastPeriod [3] = U'o';
		lastPeriod [4] = U'n';
		lastPeriod [5] = U'e';
		lastPeriod [6] = U's';
		lastPeriod [7] = U'\0';
		autoTextGrid phones = TextGrid_readFromEspsLabelFile (& file, false, 2);
		Melder_assert (phones -> tiers->size == 2);
		Thing_setName (phones -> tiers->at [1], U"phon");
		Thing_setName (phones -> tiers->at [2], U"phon*");
		textgrids. addItem_ref (phones.get());

		/*
			Read the .words file.
		*/
		lastPeriod [1] = U'w';
		lastPeriod [2] = U'o';
		lastPeriod [3] = U'r';
		lastPeriod [4] = U'd';
		lastPeriod [5] = U's';
		lastPeriod [6] = U'\0';
		autoTextGrid words = TextGrid_readFromEspsLabelFile (& file, false, 4);
		Melder_assert (words -> tiers->size == 4);
		Thing_setName (words -> tiers->at [1], U"words");
		Thing_setName (words -> tiers->at [2], U"dict");
		Thing_setName (words -> tiers->at [3], U"trans");
		Thing_setName (words -> tiers->at [4], U"pos");
		textgrids. addItem_ref (words.get());

		/*
			Read the .log file.
		*/
		lastPeriod [1] = U'l';
		lastPeriod [2] = U'o';
		lastPeriod [3] = U'g';
		lastPeriod [4] = U'\0';
		autoTextGrid log = TextGrid_readFromEspsLabelFile (& file, true, 1);
		Thing_setName (log -> tiers->at [1], U"log");
		textgrids. addItem_ref (log.get());

		*out_textgrid = TextGrids_merge (& textgrids, true);
		Melder_assert ((*out_textgrid) -> tiers->size == 7);
		lastPeriod [0] = U'\0';
		Thing_setName (sound.get(), MelderFile_name (& file));
		Thing_setName ((*out_textgrid).get(), MelderFile_name (& file));

		return sound;
	} catch (MelderError) {
		Melder_throw (U"Sound “", soundFileName, U"” not read with adjacent Buckeye annotation files.");
	}
}

autoSound Sound_readWithAdjacentAnnotationFiles_timit (conststring32 soundFileName, autoTextGrid *out_textgrid) {
	try {
		structMelderFile file { };
		Melder_pathToFile (soundFileName, & file);
		char32 *lastPeriod = str32rchr (file.path, U'.');
		Melder_require (lastPeriod,
			U"Sound file name should have an extension, but is ", & file, U".");
		Melder_require (Melder_equ (lastPeriod, U".wav"),
			U"Sound file name should end in “.wav”, not in “", lastPeriod, U"”.");
		autoSound sound = Sound_readFromSoundFile (& file);
		OrderedOf <structTextGrid> textgrids;

		/*
			Read the .phn file.
		*/
		lastPeriod [1] = U'p';
		lastPeriod [2] = U'h';
		lastPeriod [3] = U'n';
		autoTextGrid phones = TextGrid_readFromTimitLabelFile (& file, true);
		textgrids. addItem_ref (phones.get());

		/*
			Read the .wrd file.
		*/
		lastPeriod [1] = U'w';
		lastPeriod [2] = U'r';
		lastPeriod [3] = U'd';
		autoTextGrid words = TextGrid_readFromTimitLabelFile (& file, false);
		textgrids. addItem_ref (words.get());

		/*
			Read the .txt file.
		*/
		lastPeriod [1] = U't';
		lastPeriod [2] = U'x';
		lastPeriod [3] = U't';
		autoTextGrid text = TextGrid_readFromTimitLabelFile (& file, false);
		textgrids. addItem_ref (text.get());

		*out_textgrid = TextGrids_merge (& textgrids, true);
		Thing_setName ((*out_textgrid) -> tiers->at [1], U"phon-ipa");
		Thing_setName ((*out_textgrid) -> tiers->at [2], U"phon-arpa");
		Thing_setName ((*out_textgrid) -> tiers->at [3], U"words");
		Thing_setName ((*out_textgrid) -> tiers->at [4], U"text");
		return sound;
	} catch (MelderError) {
		Melder_throw (U"Sound “", soundFileName, U"” not read with adjacent TIMIT annotation files.");
	}
}

autoTextGrid TextGrid_Sound_readFromCorpusGesprokenNederlands (conststring32 soundFileName, autoSound *out_sound) {
	try {
		structMelderFile file { };
		Melder_pathToFile (soundFileName, & file);
		if (out_sound)
			*out_sound = Sound_readFromSoundFile (& file);
		OrderedOf <structTextGrid> textgrids;

		/*
			The sound file resides in a folder like /Volumes/CGN/comp-a/nl/.
			We extract the name of the sound file and of the enclosing folders.
		*/

		conststring32 soundName = MelderFile_name (& file);
		const integer soundNamelength = Melder_length (soundName);
		Melder_require (soundNamelength == 12,
			U"Sound file name should be 12 characters long, but “", soundName, U"” is ", soundNamelength, U" characters long.");
		Melder_require (Melder_stringMatchesCriterion (soundName, kMelder_string::ENDS_WITH, U".wav", true),
			U"Sound file name should end in “.wav”, but it is “", soundName, U"”.");

		autostring32 baseName = Melder_dup (soundName);
		baseName [soundNamelength - 4] = U'\0';   // remove extension ".wav"

		structMelderFolder regionFolder { };
		MelderFile_getParentFolder (& file, & regionFolder);
		conststring32 regionName = MelderFolder_name (& regionFolder);
		Melder_require (Melder_equ (regionName, U"vl") || Melder_equ (regionName, U"nl"),
			U"Folder name should be “vl” or “nl”, not “", regionName, U"”.");

		structMelderFolder compFolder { };
		MelderFolder_getParentFolder (& regionFolder, & compFolder);
		conststring32 compName = MelderFolder_name (& compFolder);
		Melder_require (Melder_length (compName) == 6 && Melder_stringMatchesCriterion (compName, kMelder_string::STARTS_WITH, U"comp-", true),
			U"Folder name should be “comp-” plus another letter, not “", compName, U"”.");

		/*
			Travel up to the CGN folder.
		*/
		structMelderFolder cgnFolder { };
		MelderFolder_getParentFolder (& compFolder, & cgnFolder);

		/*
			Travel down to the annotation text folder.
		*/
		structMelderFolder dataFolder { };
		MelderFolder_getSubfolder (& cgnFolder, U"data", & dataFolder);
		structMelderFolder annotFolder { };
		MelderFolder_getSubfolder (& dataFolder, U"annot", & annotFolder);
		structMelderFolder annotTextFolder { };
		MelderFolder_getSubfolder (& annotFolder, U"text", & annotTextFolder);

		/*
			Read the .ort file.
		*/
		structMelderFolder ortFolder { };
		MelderFolder_getSubfolder (& annotTextFolder, U"ort", & ortFolder);
		structMelderFolder ortCompFolder { };
		MelderFolder_getSubfolder (& ortFolder, compName, & ortCompFolder);
		structMelderFolder ortRegionFolder { };
		MelderFolder_getSubfolder (& ortCompFolder, regionName, & ortRegionFolder);
		structMelderFile ortFile { };
		MelderFolder_getFile (& ortRegionFolder, Melder_cat (baseName.get(), U".ort.gz"), & ortFile);
		autoDaata ortData = Data_readFromFile (& ortFile);
		Melder_require (Thing_isa (ortData.get(), classTextGrid),
			U"The file ", MelderFile_messageName (& ortFile), U" contains a ", Thing_className (ortData.get()), U" instead of a TextGrid.");
		autoTextGrid ort = ortData.static_cast_move <structTextGrid>();
		for (integer itier = 1; itier <= ort -> tiers->size; itier ++)
			TextGrid_setTierName (ort.get(), itier, Melder_dup (Melder_cat (U"ort/", ort -> tiers->at [itier] -> name.get())).get());
		textgrids. addItem_ref (ort.get());

		/*
			Read the .awd file.
		*/
		structMelderFolder awdFolder { };
		MelderFolder_getSubfolder (& annotTextFolder, U"awd", & awdFolder);
		structMelderFolder awdCompFolder { };
		MelderFolder_getSubfolder (& awdFolder, compName, & awdCompFolder);
		structMelderFolder awdRegionFolder { };
		MelderFolder_getSubfolder (& awdCompFolder, regionName, & awdRegionFolder);
		structMelderFile awdFile { };
		MelderFolder_getFile (& awdRegionFolder, Melder_cat (baseName.get(), U".awd.gz"), & awdFile);
		autoDaata awdData = Data_readFromFile (& awdFile);
		Melder_require (Thing_isa (awdData.get(), classTextGrid),
			U"The file ", MelderFile_messageName (& awdFile), U" contains a ", Thing_className (awdData.get()), U" instead of a TextGrid.");
		autoTextGrid awd = awdData.static_cast_move <structTextGrid>();
		for (integer itier = 1; itier <= awd -> tiers->size; itier ++)
			TextGrid_setTierName (awd.get(), itier, Melder_dup (Melder_cat (U"awd/", awd -> tiers->at [itier] -> name.get())).get());
		textgrids. addItem_ref (awd.get());

		/*
			Read the .fon file.
		*/
		structMelderFolder fonFolder { };
		MelderFolder_getSubfolder (& annotTextFolder, U"fon", & fonFolder);
		structMelderFolder fonCompFolder { };
		MelderFolder_getSubfolder (& fonFolder, compName, & fonCompFolder);
		structMelderFolder fonRegionFolder { };
		MelderFolder_getSubfolder (& fonCompFolder, regionName, & fonRegionFolder);
		structMelderFile fonFile { };
		MelderFolder_getFile (& fonRegionFolder, Melder_cat (baseName.get(), U".fon.gz"), & fonFile);
		autoDaata fonData;
		if (MelderFile_readable (& fonFile)) {
			fonData = Data_readFromFile (& fonFile);
			Melder_require (Thing_isa (fonData.get(), classTextGrid),
				U"The file ", MelderFile_messageName (& fonFile), U" contains a ", Thing_className (fonData.get()), U" instead of a TextGrid.");
		}
		autoTextGrid fon;
		if (fonData) {
			fon = fonData.static_cast_move <structTextGrid>();
			for (integer itier = 1; itier <= fon -> tiers->size; itier ++)
				TextGrid_setTierName (fon.get(), itier, Melder_dup (Melder_cat (U"fon/", fon -> tiers->at [itier] -> name.get())).get());
			textgrids. addItem_ref (fon.get());
		}

		autoTextGrid me = TextGrids_merge (& textgrids, true);
		for (integer itier = 1; itier <= my tiers->size; itier ++)
			TextGrid_setTierName (me.get(), itier, replace_STR (my tiers->at [itier] -> name.get(), U"_", U"/", 0).get());

		if (out_sound)
			Thing_setName (out_sound->get(), baseName.get());
		Thing_setName (me.get(), baseName.get());
		return me;
	} catch (MelderError) {
		Melder_throw (U"Sound “", soundFileName, U"” not read with adjacent CGN annotation files.");
	}
}

/* End of file TextGrid_Sound.cpp */
