/* FunctionEditor_SoundAnalysis.c */
/* Paul Boersma, May 28, 2002 */

#include "FunctionEditor_SoundAnalysis.h"
#include "Sound_and_Spectrogram.h"
#include "Sound_to_Pitch.h"
#include "Sound_to_Intensity.h"
#include "Sound_to_Formant.h"
#include "Resources.h"
#include "EditorM.h"

static const char *pitchUnits_strings [] = { 0, "Hz", "Hz", "st", "mel", "erb" };

struct logInfo {
	int toInfoWindow, toLogFile;
	char fileName [256], format [300];
};

#if defined (macintosh)
	#define LOG_1_FILE_NAME "Macintosh HD:Desktop Folder:Pitch Log"
	#define LOG_2_FILE_NAME "Macintosh HD:Desktop Folder:Formant Log"
	#define LOG_3_FILE_NAME "Macintosh HD:Desktop Folder:Log script 3"
	#define LOG_4_FILE_NAME "Macintosh HD:Desktop Folder:Log script 4"
#elif defined (WIN32)
	#define LOG_1_FILE_NAME "C:\\WINDOWS\\DESKTOP\\Pitch Log.txt"
	#define LOG_2_FILE_NAME "C:\\WINDOWS\\DESKTOP\\Formant Log.txt"
	#define LOG_3_FILE_NAME "C:\\WINDOWS\\DESKTOP\\Log script 3.praat"
	#define LOG_4_FILE_NAME "C:\\WINDOWS\\DESKTOP\\Log script 4.praat"
#else
	#define LOG_1_FILE_NAME "/home/mary/pitch_log"
	#define LOG_2_FILE_NAME "/home/boonserm/formant_log"
	#define LOG_3_FILE_NAME "/home/mietta/log_script3"
	#define LOG_4_FILE_NAME "/home/paola/log_script4"
#endif
#define LOG_1_FORMAT  "Time 'time:6' seconds, pitch 'f0:2' Hertz"
#define LOG_2_FORMAT  "'t1:4''tab$''t2:4''tab$''f1:0''tab$''f2:0''tab$''f3:0'"
static struct {
	struct FunctionEditor_spectrogram spectrogram;
	struct FunctionEditor_pitch pitch;
	struct FunctionEditor_intensity intensity;
	struct FunctionEditor_formant formant;
	struct logInfo log [2];
	char logScript3 [256], logScript4 [256];
} preferences = {
	{
		NULL, TRUE,
		1000, 250, 0.0, 5000.0, 0.0,
		1, 0.005, 5,
		70.0, 6.0, 0.0
	}, {
		NULL, TRUE,
		100, 0.0, 500.0, Pitch_yscale_LINEAR, Pitch_speckle_NO,
		75.0, 500.0, 1, FALSE,
		15, 0.03, 0.45,
		0.01, 0.35, 0.14
	}, {
		NULL, FALSE,
		100, 50.0, 100.0
	}, {
		NULL, TRUE,
		100, 2.0, 1.0, 30.0,
		10, 5500.0, 0.025, 50.0,
		1
	}, {
		TRUE, TRUE, LOG_1_FILE_NAME, LOG_1_FORMAT,
		TRUE, TRUE, LOG_2_FILE_NAME, LOG_2_FORMAT,
	},
		LOG_3_FILE_NAME, LOG_4_FILE_NAME
};

void FunctionEditor_SoundAnalysis_prefs (void) {
	Resources_addInt ("FunctionEditor.spectrogram.show", & preferences.spectrogram.show);
	Resources_addLong ("FunctionEditor.spectrogram.timeSteps", & preferences.spectrogram.timeSteps);
	Resources_addLong ("FunctionEditor.spectrogram.frequencySteps", & preferences.spectrogram.frequencySteps);
	Resources_addDouble ("FunctionEditor.spectrogram.viewFrom", & preferences.spectrogram.viewFrom);
	Resources_addDouble ("FunctionEditor.spectrogram.viewTo", & preferences.spectrogram.viewTo);
	Resources_addInt ("FunctionEditor.spectrogram.method", & preferences.spectrogram.method);
	Resources_addDouble ("FunctionEditor.spectrogram.windowLength", & preferences.spectrogram.windowLength);
	Resources_addInt ("FunctionEditor.spectrogram.windowShape", & preferences.spectrogram.windowShape);
	Resources_addDouble ("FunctionEditor.spectrogram.dynamicRange", & preferences.spectrogram.dynamicRange);
	Resources_addDouble ("FunctionEditor.spectrogram.preemphasis", & preferences.spectrogram.preemphasis);
	Resources_addDouble ("FunctionEditor.spectrogram.dynamicCompression", & preferences.spectrogram.dynamicCompression);
	Resources_addInt ("FunctionEditor.pitch.show", & preferences.pitch.show);
	Resources_addLong ("FunctionEditor.pitch.timeSteps", & preferences.pitch.timeSteps);
	Resources_addDouble ("FunctionEditor.pitch.viewFrom", & preferences.pitch.viewFrom);
	Resources_addDouble ("FunctionEditor.pitch.viewTo", & preferences.pitch.viewTo);
	Resources_addInt ("FunctionEditor.pitch.units", & preferences.pitch.units);
	Resources_addInt ("FunctionEditor.pitch.speckle", & preferences.pitch.speckle);
	Resources_addDouble ("FunctionEditor.pitch.minimumPitch", & preferences.pitch.minimumPitch);
	Resources_addDouble ("FunctionEditor.pitch.maximumPitch", & preferences.pitch.maximumPitch);
	Resources_addInt ("FunctionEditor.pitch.method", & preferences.pitch.method);
	Resources_addInt ("FunctionEditor.pitch.veryAccurate", & preferences.pitch.veryAccurate);
	Resources_addLong ("FunctionEditor.pitch.maximumNumberOfCandidates", & preferences.pitch.maximumNumberOfCandidates);
	Resources_addDouble ("FunctionEditor.pitch.silenceThreshold", & preferences.pitch.silenceThreshold);
	Resources_addDouble ("FunctionEditor.pitch.voicingThreshold", & preferences.pitch.voicingThreshold);
	Resources_addDouble ("FunctionEditor.pitch.octaveCost", & preferences.pitch.octaveCost);
	Resources_addDouble ("FunctionEditor.pitch.octaveJumpCost", & preferences.pitch.octaveJumpCost);
	Resources_addDouble ("FunctionEditor.pitch.voicedUnvoicedCost", & preferences.pitch.voicedUnvoicedCost);
	Resources_addInt ("FunctionEditor.intensity.show", & preferences.intensity.show);
	Resources_addLong ("FunctionEditor.intensity.timeSteps", & preferences.intensity.timeSteps);
	Resources_addDouble ("FunctionEditor.intensity.viewFrom", & preferences.intensity.viewFrom);
	Resources_addDouble ("FunctionEditor.intensity.viewTo", & preferences.intensity.viewTo);
	Resources_addInt ("FunctionEditor.formant.show", & preferences.formant.show);
	Resources_addLong ("FunctionEditor.formant.timeSteps", & preferences.formant.timeSteps);
	Resources_addDouble ("FunctionEditor.formant.maximumDuration", & preferences.formant.maximumDuration);
	Resources_addDouble ("FunctionEditor.formant.dotSize", & preferences.formant.dotSize);
	Resources_addDouble ("FunctionEditor.formant.dynamicRange", & preferences.formant.dynamicRange);
	Resources_addLong ("FunctionEditor.formant.numberOfPoles", & preferences.formant.numberOfPoles);
	Resources_addDouble ("FunctionEditor.formant.maximumFormant", & preferences.formant.maximumFormant);
	Resources_addDouble ("FunctionEditor.formant.windowLength", & preferences.formant.windowLength);
	Resources_addDouble ("FunctionEditor.formant.preemphasisFrom", & preferences.formant.preemphasisFrom);
	Resources_addInt ("FunctionEditor.formant.method", & preferences.formant.method);
	Resources_addInt ("FunctionEditor.log1.toInfoWindow", & preferences.log[0].toInfoWindow);
	Resources_addInt ("FunctionEditor.log1.toLogFile", & preferences.log[0].toLogFile);
	Resources_addString ("FunctionEditor.log1.fileName", & preferences.log[0].fileName [0]);
	Resources_addString ("FunctionEditor.log1.format", & preferences.log[0].format [0]);
	Resources_addInt ("FunctionEditor.log2.toInfoWindow", & preferences.log[1].toInfoWindow);
	Resources_addInt ("FunctionEditor.log2.toLogFile", & preferences.log[1].toLogFile);
	Resources_addString ("FunctionEditor.log2.fileName", & preferences.log[1].fileName [0]);
	Resources_addString ("FunctionEditor.log2.format", & preferences.log[1].format [0]);
	Resources_addString ("FunctionEditor.logScript3", & preferences.logScript3 [0]);
	Resources_addString ("FunctionEditor.logScript4", & preferences.logScript4 [0]);
}

void FunctionEditor_SoundAnalysis_forget (I) {
	iam (FunctionEditor);
	forget (my spectrogram.data);
	forget (my pitch.data);
	forget (my intensity.data);
	forget (my formant.data);
}

static Sound extractSound (FunctionEditor me, double tmin, double tmax) {
	Sound sound = NULL;
	if (my longSound.data) {
		if (tmin < my longSound.data -> xmin) tmin = my longSound.data -> xmin;
		if (tmax > my longSound.data -> xmax) tmax = my longSound.data -> xmax;
		sound = LongSound_extractPart (my longSound.data, tmin, tmax, TRUE);
	} else if (my sound.data) {
		if (tmin < my sound.data -> xmin) tmin = my sound.data -> xmin;
		if (tmax > my sound.data -> xmax) tmax = my sound.data -> xmax;
		sound = Sound_extractPart (my sound.data, tmin, tmax, enumi (Sound_WINDOW, Rectangular), 1.0, TRUE);
	}
	return sound;
}

static void computeSpectrogram (FunctionEditor me) {
	Melder_progressOff ();
	if (my spectrogram.show && (my spectrogram.data == NULL || my spectrogram.data -> xmin != my startWindow || my spectrogram.data -> xmax != my endWindow)) {
		Sound sound = NULL;
		double margin = my spectrogram.windowShape == 5 ? my spectrogram.windowLength :  0.5 * my spectrogram.windowLength;
		forget (my spectrogram.data);
		sound = extractSound (me, my startWindow - margin, my endWindow + margin);
		if (sound != NULL) {
			my spectrogram.data = Sound_to_Spectrogram (sound, my spectrogram.windowLength,
				my spectrogram.viewTo, (my endWindow - my startWindow) / my spectrogram.timeSteps,
				my spectrogram.viewTo / my spectrogram.frequencySteps, my spectrogram.windowShape, 8.0, 8.0);
			if (my spectrogram.data != NULL) my spectrogram.data -> xmin = my startWindow, my spectrogram.data -> xmax = my endWindow;
			else Melder_clearError ();
			forget (sound);
		} else Melder_clearError ();
	}
	Melder_progressOn ();
}

static void computePitch (FunctionEditor me) {
	Melder_progressOff ();
	if (my pitch.show && (my pitch.data == NULL || my pitch.data -> xmin != my startWindow || my pitch.data -> xmax != my endWindow)) {
		Sound sound = NULL;
		double margin = my pitch.veryAccurate ? 3.0 / my pitch.minimumPitch : 1.5 / my pitch.minimumPitch;
		forget (my pitch.data);
		sound = extractSound (me, my startWindow - margin, my endWindow + margin);
		if (sound != NULL) {
			my pitch.data = Sound_to_Pitch_any (sound, (my endWindow - my startWindow) / my pitch.timeSteps,
				my pitch.minimumPitch, my pitch.method == 1 ? 3.0 : 1.0, my pitch.maximumNumberOfCandidates,
				(my pitch.method - 1) * 2 + my pitch.veryAccurate,
				my pitch.silenceThreshold, my pitch.voicingThreshold,
				my pitch.octaveCost, my pitch.octaveJumpCost, my pitch.voicedUnvoicedCost, my pitch.maximumPitch);
			if (my pitch.data != NULL) my pitch.data -> xmin = my startWindow, my pitch.data -> xmax = my endWindow;
			else Melder_clearError ();
			forget (sound);
		} else Melder_clearError ();
	}
	Melder_progressOn ();
}

static void computeIntensity (FunctionEditor me) {
	Melder_progressOff ();
	if (my intensity.show && (my intensity.data == NULL || my intensity.data -> xmin != my startWindow || my intensity.data -> xmax != my endWindow)) {
		Sound sound = NULL;
		double margin = 3.2 / my pitch.minimumPitch;
		forget (my intensity.data);
		sound = extractSound (me, my startWindow - margin, my endWindow + margin);
		if (sound != NULL) {
			my intensity.data = Sound_to_Intensity (sound, my pitch.minimumPitch, (my endWindow - my startWindow) / my intensity.timeSteps);
			if (my intensity.data != NULL) my intensity.data -> xmin = my startWindow, my intensity.data -> xmax = my endWindow;
			else Melder_clearError ();
			forget (sound);
		} else Melder_clearError ();
	}
	Melder_progressOn ();
}

static void computeFormants (FunctionEditor me) {
	Melder_progressOff ();
	if (my formant.show && (my formant.data == NULL || my formant.data -> xmin != my startWindow || my formant.data -> xmax != my endWindow)) {
		Sound sound = NULL;
		double margin = my formant.windowLength;
		forget (my formant.data);
		if (my endWindow - my startWindow > my formant.maximumDuration)
			sound = extractSound (me,
				0.5 * (my startWindow + my endWindow - my formant.maximumDuration) - margin,
				0.5 * (my startWindow + my endWindow + my formant.maximumDuration) + margin);
		else
			sound = extractSound (me, my startWindow - margin, my endWindow + margin);
		if (sound != NULL) {
			my formant.data = Sound_to_Formant_any (sound, (my endWindow - my startWindow) / my formant.timeSteps,
				my formant.numberOfPoles, my formant.maximumFormant,
				my formant.windowLength, my formant.method, my formant.preemphasisFrom, 50.0);
			if (my formant.data != NULL) my formant.data -> xmin = my startWindow, my formant.data -> xmax = my endWindow;
			else Melder_clearError ();
			forget (sound);
		} else Melder_clearError ();
	}
	Melder_progressOn ();
}

/***** QUERY MENU *****/

DIRECT (FunctionEditor, cb_getFrequency)
	Melder_informationReal (my spectrogram.cursor, "Hertz");
END

DIRECT (FunctionEditor, cb_getPitch)
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (my startSelection == my endSelection)
		Melder_informationReal (
			Pitch_getValueAtTime (my pitch.data, my startSelection, Pitch_yscaleToUnits (my pitch.units), 1),
			Pitch_longUnitText (my pitch.units));
	else
		Melder_informationReal (
			Pitch_getMean (my pitch.data, my startSelection, my endSelection, Pitch_yscaleToUnits (my pitch.units)),
			Pitch_longUnitText (my pitch.units));
END

DIRECT (FunctionEditor, cb_getMinimumPitch)
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (my startSelection == my endSelection) {
		return Melder_error ("Empty selection.");
	} else {
		double value;
		Pitch_getMinimumAndTime (my pitch.data, my startSelection, my endSelection,
			Pitch_yscaleToUnits (my pitch.units), 1, & value, NULL);
		Melder_informationReal (value, Pitch_longUnitText (my pitch.units));
	}
END

DIRECT (FunctionEditor, cb_getMaximumPitch)
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (my startSelection == my endSelection) {
		return Melder_error ("Empty selection.");
	} else {
		double value;
		Pitch_getMaximumAndTime (my pitch.data, my startSelection, my endSelection,
			Pitch_yscaleToUnits (my pitch.units), 1, & value, NULL);
		Melder_informationReal (value, Pitch_longUnitText (my pitch.units));
	}
END

DIRECT (FunctionEditor, cb_getIntensity)
	if (! my intensity.show)
		return Melder_error ("No intensity contour is visible.\nFirst choose \"Show intensity\" from the View menu.");
	if (! my intensity.data) {
		computeIntensity (me);
		if (! my intensity.data) return Melder_error ("No intensity contour available (out of memory?).");
	}
	if (my startSelection == my endSelection)
		Melder_informationReal (Vector_getValueAtX (my intensity.data, my startSelection, 1), "dB");
	else
		Melder_informationReal (Vector_getMean (my intensity.data, my startSelection, my endSelection), "dB");
END

DIRECT (FunctionEditor, cb_formantReport)
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formant\" from the View menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	if (my startSelection == my endSelection) {
		double f1 = Formant_getValueAtTime (my formant.data, 1, my startSelection, 0);
		double f2 = Formant_getValueAtTime (my formant.data, 2, my startSelection, 0);
		double f3 = Formant_getValueAtTime (my formant.data, 3, my startSelection, 0);
		double f4 = Formant_getValueAtTime (my formant.data, 4, my startSelection, 0);
		Melder_information ("Time   F1   F2   F3   F4\n%f   %f   %f   %f   %f", my startSelection, f1, f2, f3, f4);
	} else {
		long i, i1, i2;
		Sampled_getWindowSamples (my formant.data, my startSelection, my endSelection, & i1, & i2);
		Melder_information ("Time   F1   F2   F3   F4");
		for (i = i1; i <= i2; i ++) {
			double t = Sampled_indexToX (my formant.data, i);
			double f1 = Formant_getValueAtTime (my formant.data, 1, t, 0);
			double f2 = Formant_getValueAtTime (my formant.data, 2, t, 0);
			double f3 = Formant_getValueAtTime (my formant.data, 3, t, 0);
			double f4 = Formant_getValueAtTime (my formant.data, 4, t, 0);
			Melder_info ("%f   %f   %f   %f   %f", t, f1, f2, f3, f4);
		}
	}
	return 1;
END

static int cb_getFormant (FunctionEditor me, int iformant) {
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formant\" from the View menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	if (my startSelection == my endSelection)
		Melder_informationReal (Formant_getValueAtTime (my formant.data, iformant, my startSelection, 0), "Hertz");
	else
		Melder_informationReal (Formant_getMean (my formant.data, iformant, my startSelection, my endSelection, 0), "Hertz");
	return 1;
}
static int cb_getBandwidth (FunctionEditor me, int iformant) {
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formant\" from the View menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	if (my startSelection == my endSelection)
		Melder_informationReal (Formant_getBandwidthAtTime (my formant.data, iformant, my startSelection, 0), "Hertz");
	else
		Melder_informationReal (Formant_getBandwidthAtTime (my formant.data, iformant, 0.5 * (my startSelection + my endSelection), 0), "Hertz");
	return 1;
}
DIRECT (FunctionEditor, cb_getFirstFormant) if (! cb_getFormant (me, 1)) return 0; END
DIRECT (FunctionEditor, cb_getFirstBandwidth) if (! cb_getBandwidth (me, 1)) return 0; END
DIRECT (FunctionEditor, cb_getSecondFormant) if (! cb_getFormant (me, 2)) return 0; END
DIRECT (FunctionEditor, cb_getSecondBandwidth) if (! cb_getBandwidth (me, 2)) return 0; END
DIRECT (FunctionEditor, cb_getThirdFormant) if (! cb_getFormant (me, 3)) return 0; END
DIRECT (FunctionEditor, cb_getThirdBandwidth) if (! cb_getBandwidth (me, 3)) return 0; END
DIRECT (FunctionEditor, cb_getFourthFormant) if (! cb_getFormant (me, 4)) return 0; END
DIRECT (FunctionEditor, cb_getFourthBandwidth) if (! cb_getBandwidth (me, 4)) return 0; END
DIRECT (FunctionEditor, cb_getFifthFormant) if (! cb_getFormant (me, 5)) return 0; END
DIRECT (FunctionEditor, cb_getFifthBandwidth) if (! cb_getBandwidth (me, 5)) return 0; END

/***** VIEW MENU *****/

FORM (FunctionEditor, cb_showAnalyses, "Show analyses", 0)
	BOOLEAN ("Show spectrogram", 1)
	BOOLEAN ("Show pitch", 1)
	BOOLEAN ("Show intensity", 0)
	BOOLEAN ("Show formants", 1)
	OK
SET_INTEGER ("Show spectrogram", my spectrogram.show)
SET_INTEGER ("Show pitch", my pitch.show)
SET_INTEGER ("Show intensity", my intensity.show)
SET_INTEGER ("Show formants", my formant.show)
DO
	preferences.spectrogram.show = my spectrogram.show = GET_INTEGER ("Show spectrogram");
	preferences.pitch.show = my pitch.show = GET_INTEGER ("Show pitch");
	preferences.intensity.show = my intensity.show = GET_INTEGER ("Show intensity");
	preferences.formant.show = my formant.show = GET_INTEGER ("Show formants");
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_spectrogramSettings, "Spectrogram settings", "Spectrogram analysis...")
	BOOLEAN ("Show", 1)
	NATURAL ("Time steps", "1000")
	NATURAL ("Frequency steps", "250")
	REAL ("left View range (Hz)", "0.0")
	POSITIVE ("right View range (Hz)", "5000.0")
	OPTIONMENU ("Method", 1)
		OPTION ("Fourier")
	POSITIVE ("Window length (s)", "0.005")
	OPTIONMENU ("Window shape", 6)
		OPTION ("Square (rectangular)")
		OPTION ("Hamming (raised sine-squared)")
		OPTION ("Bartlett (triangular)")
		OPTION ("Welch (parabolic)")
		OPTION ("Hanning (sine-squared)")
		OPTION ("Gaussian")
	POSITIVE ("Dynamic range (dB)", "50.0")
	REAL ("Pre-emphasis (dB/oct)", "6.0")
	REAL ("Dynamic compression (0-1)", "0.0")
	OK
SET_INTEGER ("Show", my spectrogram.show)
SET_INTEGER ("Time steps", my spectrogram.timeSteps)
SET_INTEGER ("Frequency steps", my spectrogram.frequencySteps)
SET_REAL ("left View range", my spectrogram.viewFrom)
SET_REAL ("right View range", my spectrogram.viewTo)
SET_INTEGER ("Method", my spectrogram.method)
SET_REAL ("Window length", my spectrogram.windowLength)
SET_INTEGER ("Window shape", my spectrogram.windowShape + 1)
SET_REAL ("Dynamic range", my spectrogram.dynamicRange)
SET_REAL ("Pre-emphasis", my spectrogram.preemphasis)
SET_REAL ("Dynamic compression", my spectrogram.dynamicCompression)
DO
	preferences.spectrogram.show = my spectrogram.show = GET_INTEGER ("Show");
	preferences.spectrogram.timeSteps = my spectrogram.timeSteps = GET_INTEGER ("Time steps");
	preferences.spectrogram.frequencySteps = my spectrogram.frequencySteps = GET_INTEGER ("Frequency steps");
	preferences.spectrogram.viewFrom = my spectrogram.viewFrom = GET_REAL ("left View range");
	preferences.spectrogram.viewTo = my spectrogram.viewTo = GET_REAL ("right View range");
	preferences.spectrogram.method = my spectrogram.method = GET_INTEGER ("Method");
	preferences.spectrogram.windowLength = my spectrogram.windowLength = GET_REAL ("Window length");
	preferences.spectrogram.windowShape = my spectrogram.windowShape = GET_INTEGER ("Window shape") - 1;
	preferences.spectrogram.dynamicRange = my spectrogram.dynamicRange = GET_REAL ("Dynamic range");
	preferences.spectrogram.preemphasis = my spectrogram.preemphasis = GET_REAL ("Pre-emphasis");
	preferences.spectrogram.dynamicCompression = my spectrogram.dynamicCompression = GET_REAL ("Dynamic compression");
	forget (my spectrogram.data);
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_pitchSettings, "Pitch settings", "Pitch analysis...")
	BOOLEAN ("Show", 1)
	NATURAL ("Time steps", "100")
	REAL ("left View range", "0.0")
	REAL ("right View range", "500.0")
	OPTIONMENU ("Units", 1)
		OPTION ("Hertz")
		OPTION ("Hertz logarithmic")
		OPTION ("Semitones re 100 Hz")
		OPTION ("Mel")
		OPTION ("Erb")
	BOOLEAN ("Speckle", 0)
	REAL ("left Analysis range (Hz)", "75.0")
	POSITIVE ("right Analysis range (Hz)", "500.0")
	OPTIONMENU ("Method", 1)
		OPTION ("Autocorrelation")
		OPTION ("Forward cross-correlation")
	BOOLEAN ("Very accurate", 0)
	NATURAL ("Max. number of candidates", "15")
	REAL ("Silence threshold", "0.03")
	REAL ("Voicing threshold", "0.45")
	REAL ("Octave cost", "0.01")
	REAL ("Octave-jump cost", "0.35")
	REAL ("Voiced / unvoiced cost", "0.14")
	OK
SET_INTEGER ("Show", my pitch.show)
SET_INTEGER ("Time steps", my pitch.timeSteps)
SET_REAL ("left View range", my pitch.viewFrom)
SET_REAL ("right View range", my pitch.viewTo)
SET_INTEGER ("Units", my pitch.units)
SET_INTEGER ("Speckle", my pitch.speckle)
SET_REAL ("left Analysis range", my pitch.minimumPitch)
SET_REAL ("right Analysis range", my pitch.maximumPitch)
SET_INTEGER ("Method", my pitch.method)
SET_INTEGER ("Very accurate", my pitch.veryAccurate)
SET_INTEGER ("Max. number of candidates", my pitch.maximumNumberOfCandidates)
SET_REAL ("Silence threshold", my pitch.silenceThreshold)
SET_REAL ("Voicing threshold", my pitch.voicingThreshold)
SET_REAL ("Octave cost", my pitch.octaveCost)
SET_REAL ("Octave-jump cost", my pitch.octaveJumpCost)
SET_REAL ("Voiced / unvoiced cost", my pitch.voicedUnvoicedCost)
DO
	long maxnCandidates = GET_INTEGER ("Max. number of candidates");
	if (maxnCandidates < 2) return Melder_error ("Maximum number of candidates must be greater than 1.");
	preferences.pitch.show = my pitch.show = GET_INTEGER ("Show");
	preferences.pitch.timeSteps = my pitch.timeSteps = GET_INTEGER ("Time steps");
	preferences.pitch.viewFrom = my pitch.viewFrom = GET_REAL ("left View range");
	preferences.pitch.viewTo = my pitch.viewTo = GET_REAL ("right View range");
	preferences.pitch.units = my pitch.units = GET_INTEGER ("Units");
	if (my pitch.units == Pitch_yscale_LOGARITHMIC && my pitch.viewFrom <= 0.0)
		my pitch.viewFrom = my pitch.viewTo / 1000.0;
	preferences.pitch.speckle = my pitch.speckle = GET_INTEGER ("Speckle");
	preferences.pitch.minimumPitch = my pitch.minimumPitch = GET_REAL ("left Analysis range");
	preferences.pitch.maximumPitch = my pitch.maximumPitch = GET_REAL ("right Analysis range");
	preferences.pitch.method = my pitch.method = GET_INTEGER ("Method");
	preferences.pitch.veryAccurate = my pitch.veryAccurate = GET_INTEGER ("Very accurate");
	preferences.pitch.maximumNumberOfCandidates = my pitch.maximumNumberOfCandidates = GET_INTEGER ("Max. number of candidates");
	preferences.pitch.silenceThreshold = my pitch.silenceThreshold = GET_REAL ("Silence threshold");
	preferences.pitch.voicingThreshold = my pitch.voicingThreshold = GET_REAL ("Voicing threshold");
	preferences.pitch.octaveCost = my pitch.octaveCost = GET_REAL ("Octave cost");
	preferences.pitch.octaveJumpCost = my pitch.octaveJumpCost = GET_REAL ("Octave-jump cost");
	preferences.pitch.voicedUnvoicedCost = my pitch.voicedUnvoicedCost = GET_REAL ("Voiced / unvoiced cost");
	forget (my pitch.data);
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_intensitySettings, "Intensity settings", 0)
	BOOLEAN ("Show", 0)
	NATURAL ("Time steps", "100")
	REAL ("left View range (dB)", "50.0")
	REAL ("right View range (dB)", "100.0")
	LABEL ("analysis", "Analysis: minimum F0 taken from pitch analysis")
	OK
SET_INTEGER ("Show", my intensity.show)
SET_INTEGER ("Time steps", my intensity.timeSteps)
SET_REAL ("left View range", my intensity.viewFrom)
SET_REAL ("right View range", my intensity.viewTo)
DO
	preferences.intensity.show = my intensity.show = GET_INTEGER ("Show");
	preferences.intensity.timeSteps = my intensity.timeSteps = GET_INTEGER ("Time steps");
	preferences.intensity.viewFrom = my intensity.viewFrom = GET_REAL ("left View range");
	preferences.intensity.viewTo = my intensity.viewTo = GET_REAL ("right View range");
	forget (my intensity.data);
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_formantSettings, "Formant settings", "Formant analysis...")
	BOOLEAN ("Show", 1)
	NATURAL ("Time steps", "100")
	POSITIVE ("Maximum duration (s)", "2.0")
	POSITIVE ("Dot size (mm)", "1.0")
	REAL ("Dynamic range (dB)", "30.0")
	NATURAL ("Number of poles", "10")
	POSITIVE ("Maximum formant (Hz)", "5500.0")
	POSITIVE ("Window length (s)", "0.025")
	POSITIVE ("Pre-emphasis from (Hz)", "50.0")
	RADIO ("Method", 1)
		RADIOBUTTON ("Burg")
	OK
SET_INTEGER ("Show", my formant.show)
SET_INTEGER ("Time steps", my formant.timeSteps)
SET_REAL ("Maximum duration", my formant.maximumDuration)
SET_REAL ("Dot size", my formant.dotSize)
SET_REAL ("Dynamic range", my formant.dynamicRange)
SET_INTEGER ("Number of poles", my formant.numberOfPoles)
SET_REAL ("Maximum formant", my formant.maximumFormant)
SET_REAL ("Window length", my formant.windowLength)
SET_REAL ("Pre-emphasis from", my formant.preemphasisFrom)
SET_INTEGER ("Method", my formant.method)
DO
	preferences.formant.show = my formant.show = GET_INTEGER ("Show");
	preferences.formant.timeSteps = my formant.timeSteps = GET_INTEGER ("Time steps");
	preferences.formant.maximumDuration = my formant.maximumDuration = GET_REAL ("Maximum duration");
	preferences.formant.dotSize = my formant.dotSize = GET_REAL ("Dot size");
	preferences.formant.dynamicRange = my formant.dynamicRange = GET_REAL ("Dynamic range");
	preferences.formant.numberOfPoles = my formant.numberOfPoles = GET_INTEGER ("Number of poles");
	preferences.formant.maximumFormant = my formant.maximumFormant = GET_REAL ("Maximum formant");
	preferences.formant.windowLength = my formant.windowLength = GET_REAL ("Window length");
	preferences.formant.preemphasisFrom = my formant.preemphasisFrom = GET_REAL ("Pre-emphasis from");
	preferences.formant.method = my formant.method = GET_INTEGER ("Method");
	forget (my formant.data);
	FunctionEditor_redraw (me);
END

/***** SELECT MENU *****/

DIRECT (FunctionEditor, cb_moveCursorToMinimumPitch)
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (my startSelection == my endSelection) {
		return Melder_error ("Empty selection.");
	} else {
		double time;
		Pitch_getMinimumAndTime (my pitch.data, my startSelection, my endSelection,
			Pitch_yscaleToUnits (my pitch.units), 1, NULL, & time);
		if (! NUMdefined (time))
			return Melder_error ("Selection is voiceless.");
		my startSelection = my endSelection = time;
		FunctionEditor_marksChanged (me);
	}
END

DIRECT (FunctionEditor, cb_moveCursorToMaximumPitch)
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (my startSelection == my endSelection) {
		return Melder_error ("Empty selection.");
	} else {
		double time;
		Pitch_getMaximumAndTime (my pitch.data, my startSelection, my endSelection,
			Pitch_yscaleToUnits (my pitch.units), 1, NULL, & time);
		if (! NUMdefined (time))
			return Melder_error ("Selection is voiceless.");
		my startSelection = my endSelection = time;
		FunctionEditor_marksChanged (me);
	}
END

void FunctionEditor_SoundAnalysis_draw (I) {
	iam (FunctionEditor);
	if (my longSound.data && my endWindow - my startWindow > 60.0) {
		Graphics_setWindow (my graphics, 0.0, 1.0, 0.0, 1.0);
		Graphics_setColour (my graphics, Graphics_BLACK);
		Graphics_setFont (my graphics, Graphics_HELVETICA);
		Graphics_setFontSize (my graphics, 9);
		Graphics_setTextAlignment (my graphics, Graphics_CENTRE, Graphics_HALF);
		Graphics_text (my graphics, 0.5, 0.5, "(window too large to draw analyses)");
		Graphics_rectangle (my graphics, 0.0, 1.0, 0.0, 1.0);
		return;
	}
	computeSpectrogram (me);
	if (my spectrogram.show && my spectrogram.data != NULL)
		Spectrogram_paintInside (my spectrogram.data, my graphics, my startWindow, my endWindow, 
			my spectrogram.viewFrom, my spectrogram.viewTo,
			my spectrogram.dynamicRange, my spectrogram.preemphasis, my spectrogram.dynamicCompression);
	computePitch (me);
	if (my pitch.show && my pitch.data != NULL) {
		Graphics_setColour (my graphics, Graphics_CYAN);
		Graphics_setLineWidth (my graphics, 3.0);
		Pitch_drawInside (my pitch.data, my graphics, my startWindow, my endWindow,
			my pitch.viewFrom, my pitch.viewTo, my pitch.speckle, my pitch.units);
		Graphics_setColour (my graphics, Graphics_BLUE);
		Graphics_setLineWidth (my graphics, 1.0);
		Pitch_drawInside (my pitch.data, my graphics, my startWindow, my endWindow,
			my pitch.viewFrom, my pitch.viewTo, my pitch.speckle, my pitch.units);
		Graphics_setColour (my graphics, Graphics_BLACK);
	}
	computeIntensity (me);
	if (my intensity.show && my intensity.data != NULL) {
		Graphics_setColour (my graphics, my spectrogram.show ? Graphics_YELLOW : Graphics_LIME);
		Graphics_setLineWidth (my graphics, my spectrogram.show ? 1.0 : 3.0);
		Intensity_drawInside (my intensity.data, my graphics, my startWindow, my endWindow,
			my intensity.viewFrom, my intensity.viewTo);
		Graphics_setLineWidth (my graphics, 1.0);
		Graphics_setColour (my graphics, Graphics_BLACK);
	}
	computeFormants (me);
	if (my formant.show && my formant.data != NULL) {
		Graphics_setColour (my graphics, Graphics_RED);
		Formant_drawSpeckles_inside (my formant.data, my graphics, my startWindow, my endWindow, 
			my spectrogram.viewFrom, my spectrogram.viewTo, my formant.dynamicRange, my formant.dotSize);
		Graphics_setColour (my graphics, Graphics_BLACK);
	}
	/*
	 * Draw vertical scales.
	 */
	if (my pitch.show) {
		double pitchCursor = NUMundefined, y1, y2, yPitchCursor = NUMundefined;
		y1 = my pitch.units == Pitch_yscale_LOGARITHMIC ? log10 (my pitch.viewFrom) : my pitch.viewFrom;
		y2 = my pitch.units == Pitch_yscale_LOGARITHMIC ? log10 (my pitch.viewTo) : my pitch.viewTo;
		Graphics_setWindow (my graphics, my startWindow, my endWindow, y1, y2);
		if (my pitch.data) {
			if (my startSelection == my endSelection)
				pitchCursor = Pitch_getValueAtTime (my pitch.data, my startSelection, Pitch_yscaleToUnits (my pitch.units), 1);
			else
				pitchCursor = Pitch_getMean (my pitch.data, my startSelection, my endSelection, Pitch_yscaleToUnits (my pitch.units));
			yPitchCursor = my pitch.units == Pitch_yscale_LOGARITHMIC ? log10 (pitchCursor) : pitchCursor;
		}
		Graphics_setColour (my graphics, Graphics_NAVY);
		if (NUMdefined (pitchCursor)) {
			Graphics_setTextAlignment (my graphics, Graphics_LEFT, Graphics_HALF);
			Graphics_printf (my graphics, my endWindow, yPitchCursor, "%.5g %s", pitchCursor, Pitch_shortUnitText (my pitch.units));
		}
		if (! NUMdefined (pitchCursor) || Graphics_dyWCtoMM (my graphics, yPitchCursor - y1) > 5.0) {
			Graphics_setTextAlignment (my graphics, Graphics_LEFT, Graphics_BOTTOM);
			Graphics_printf (my graphics, my endWindow, y1 - Graphics_dyMMtoWC (my graphics, 0.5), "%.4g %s",
				my pitch.viewFrom, Pitch_shortUnitText (my pitch.units));
		}
		if (! NUMdefined (pitchCursor) || Graphics_dyWCtoMM (my graphics, y2 - yPitchCursor) > 5.0) {
			Graphics_setTextAlignment (my graphics, Graphics_LEFT, Graphics_TOP);
			Graphics_printf (my graphics, my endWindow, y2, "%.4g %s",
				my pitch.viewTo, Pitch_shortUnitText (my pitch.units));
		}
		Graphics_setColour (my graphics, Graphics_BLACK);
	}
	if (my intensity.show) {
		double intensityCursor = NUMundefined;
		int intensityCursorVisible;
		int textColour, alignment;
		double y;
		if (! my pitch.show) textColour = Graphics_GREEN, alignment = Graphics_LEFT, y = my endWindow;
		else if (! my spectrogram.show && ! my formant.show) textColour = Graphics_GREEN, alignment = Graphics_RIGHT, y = my startWindow;
		else textColour = my spectrogram.show ? Graphics_LIME : Graphics_GREEN, alignment = Graphics_RIGHT, y = my endWindow;
		Graphics_setWindow (my graphics, my startWindow, my endWindow, my intensity.viewFrom, my intensity.viewTo);
		if (my intensity.data) {
			if (my startSelection == my endSelection)
				intensityCursor = Vector_getValueAtX (my intensity.data, my startSelection, 1);
			else
				intensityCursor = Vector_getMean (my intensity.data, my startSelection, my endSelection);
		}
		Graphics_setColour (my graphics, textColour);
		intensityCursorVisible = NUMdefined (intensityCursor) && intensityCursor > my intensity.viewFrom && intensityCursor < my intensity.viewTo;
		if (intensityCursorVisible) {
			Graphics_setTextAlignment (my graphics, alignment, Graphics_HALF);
			Graphics_printf (my graphics, y, intensityCursor, "%.4g dB", intensityCursor);
		}
		if (! intensityCursorVisible || Graphics_dyWCtoMM (my graphics, intensityCursor - my intensity.viewFrom) > 5.0) {
			Graphics_setTextAlignment (my graphics, alignment, Graphics_BOTTOM);
			Graphics_printf (my graphics, y, my intensity.viewFrom - Graphics_dyMMtoWC (my graphics, 0.5),
				"%.4g dB", my intensity.viewFrom);
		}
		if (! intensityCursorVisible || Graphics_dyWCtoMM (my graphics, my intensity.viewTo - intensityCursor) > 5.0) {
			Graphics_setTextAlignment (my graphics, alignment, Graphics_TOP);
			Graphics_printf (my graphics, y, my intensity.viewTo, "%.4g dB", my intensity.viewTo);
		}
		Graphics_setColour (my graphics, Graphics_BLACK);
	}
	if (my spectrogram.show || my formant.show) {
		char text [100];
		double textWidth;
		int frequencyCursorVisible = my spectrogram.cursor > my spectrogram.viewFrom && my spectrogram.cursor < my spectrogram.viewTo;
		Graphics_setWindow (my graphics, my startWindow, my endWindow, my spectrogram.viewFrom, my spectrogram.viewTo);
		/*
		 * Range marks.
		 */
		Graphics_setLineType (my graphics, Graphics_DRAWN);
		Graphics_setColour (my graphics, Graphics_BLACK);
		if (! frequencyCursorVisible || Graphics_dyWCtoMM (my graphics, my spectrogram.cursor - my spectrogram.viewFrom) > 5.0) {
			sprintf (text, "%.4g Hz", my spectrogram.viewFrom);
			textWidth = Graphics_textWidth (my graphics, text) + Graphics_dxMMtoWC (my graphics, 0.5);
			/*Graphics_line (my graphics, my startWindow - textWidth, my spectrogram.viewFrom, my startWindow, my spectrogram.viewFrom);*/
			Graphics_setTextAlignment (my graphics, Graphics_RIGHT, Graphics_BOTTOM);
			Graphics_text (my graphics, my startWindow, my spectrogram.viewFrom - Graphics_dyMMtoWC (my graphics, 0.5), text);
		}
		if (! frequencyCursorVisible || Graphics_dyWCtoMM (my graphics, my spectrogram.viewTo - my spectrogram.cursor) > 5.0) {
			sprintf (text, "%.4g Hz", my spectrogram.viewTo);
			textWidth = Graphics_textWidth (my graphics, text) + Graphics_dxMMtoWC (my graphics, 0.5);
			/*Graphics_line (my graphics, my startWindow - textWidth, my spectrogram.viewTo, my startWindow, my spectrogram.viewTo);*/
			Graphics_setTextAlignment (my graphics, Graphics_RIGHT, Graphics_TOP);
			Graphics_text (my graphics, my startWindow, my spectrogram.viewTo, text);
		}
		/*
		 * Cursor lines.
		 */
		Graphics_setLineType (my graphics, Graphics_DOTTED);
		Graphics_setColour (my graphics, Graphics_RED);
		if (frequencyCursorVisible) {
			double x = my startWindow, y = my spectrogram.cursor;
			Graphics_setTextAlignment (my graphics, Graphics_RIGHT, Graphics_HALF);
			Graphics_printf (my graphics, x, y, "%.5g Hz", y);
			Graphics_line (my graphics, x, y, my endWindow, y);
		}
		if (my startSelection == my endSelection && my startSelection > my startWindow && my startSelection < my endWindow)
			Graphics_line (my graphics, my startSelection, my spectrogram.viewFrom, my startSelection, my spectrogram.viewTo);
		/*
		 * Cadre.
		 */
		Graphics_setLineType (my graphics, Graphics_DRAWN);
		Graphics_setColour (my graphics, Graphics_BLACK);
		Graphics_rectangle (my graphics, my startWindow, my endWindow, my spectrogram.viewFrom, my spectrogram.viewTo);
	}
}

DIRECT (FunctionEditor, cb_extractVisibleSpectrogram)
	Spectrogram publish;
	if (! my spectrogram.show)
		return Melder_error ("No spectrogram is visible.\nFirst choose \"Show spectrogram\" from the View menu.");
	publish = Data_copy (my spectrogram.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

DIRECT (FunctionEditor, cb_extractVisiblePitchContour)
	Pitch publish;
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
	publish = Data_copy (my pitch.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

DIRECT (FunctionEditor, cb_extractVisibleIntensityContour)
	Intensity publish;
	if (! my intensity.show)
		return Melder_error ("No intensity contour is visible.\nFirst choose \"Show intensity\" from the View menu.");
	publish = Data_copy (my intensity.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

DIRECT (FunctionEditor, cb_extractVisibleFormantContour)
	Formant publish;
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formant\" from the View menu.");
	publish = Data_copy (my formant.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

FORM (FunctionEditor, cb_logSettings, "Log settings", "Log files")
	BOOLEAN ("Log 1 to Info window", 1)
	BOOLEAN ("Log 1 to log file", 1)
	LABEL ("", "Log file 1:")
	TEXTFIELD ("Log file 1", LOG_1_FILE_NAME)
	LABEL ("", "Log 1 format:")
	TEXTFIELD ("Log 1 format", LOG_1_FORMAT)
	LABEL ("", "")
	BOOLEAN ("Log 2 to Info window", 1)
	BOOLEAN ("Log 2 to log file", 1)
	LABEL ("", "Log file 2:")
	TEXTFIELD ("Log file 2", LOG_2_FILE_NAME)
	LABEL ("", "Log 2 format:")
	TEXTFIELD ("Log 2 format", LOG_2_FORMAT)
	LABEL ("", "")
	LABEL ("", "Log script 3:")
	TEXTFIELD ("Log script 3", LOG_3_FILE_NAME)
	LABEL ("", "")
	LABEL ("", "Log script 4:")
	TEXTFIELD ("Log script 4", LOG_4_FILE_NAME)
	OK
SET_INTEGER ("Log 1 to Info window", preferences.log[0].toInfoWindow)
SET_INTEGER ("Log 1 to log file", preferences.log[0].toLogFile)
SET_STRING ("Log file 1", preferences.log[0].fileName)
SET_STRING ("Log 1 format", preferences.log[0].format)
SET_INTEGER ("Log 2 to Info window", preferences.log[1].toInfoWindow)
SET_INTEGER ("Log 2 to log file", preferences.log[1].toLogFile)
SET_STRING ("Log file 2", preferences.log[1].fileName)
SET_STRING ("Log 2 format", preferences.log[1].format)
SET_STRING ("Log script 3", preferences.logScript3)
SET_STRING ("Log script 4", preferences.logScript4)
DO
	preferences.log[0].toInfoWindow = GET_INTEGER ("Log 1 to Info window");
	preferences.log[0].toLogFile = GET_INTEGER ("Log 1 to log file");
	strcpy (preferences.log[0].fileName, GET_STRING ("Log file 1"));
	strcpy (preferences.log[0].format, GET_STRING ("Log 1 format"));
	preferences.log[1].toInfoWindow = GET_INTEGER ("Log 2 to Info window");
	preferences.log[1].toLogFile = GET_INTEGER ("Log 2 to log file");
	strcpy (preferences.log[1].fileName, GET_STRING ("Log file 2"));
	strcpy (preferences.log[1].format, GET_STRING ("Log 2 format"));
	strcpy (preferences.logScript3, GET_STRING ("Log script 3"));
	strcpy (preferences.logScript4, GET_STRING ("Log script 4"));
END

static int cb_deleteLogFile (FunctionEditor me, int which) {
	struct MelderFile file;
	(void) me;
	if (! Melder_pathToFile (preferences.log[which].fileName, & file)) return 0;
	MelderFile_delete (& file);
	return 1;
}
DIRECT (FunctionEditor, cb_deleteLogFile1) if (! cb_deleteLogFile (me, 0)) return 0; END
DIRECT (FunctionEditor, cb_deleteLogFile2) if (! cb_deleteLogFile (me, 0)) return 0; END

static int cb_log (FunctionEditor me, int which) {
	char format [1000], *p;
	strcpy (format, preferences.log[which].format);
	for (p = format; *p !='\0'; p ++) if (*p == '\'') {
		/*
		 * Found a left quote. Search for a matching right quote.
		 */
		char *q = p + 1, varName [300], *r, *s, *colon;
		int precision = -1;
		double value = NUMundefined;
		while (*q != '\0' && *q != '\'') q ++;
		if (*q == '\0') break;   /* No matching right quote: done with this line. */
		if (q - p == 1) continue;   /* Ignore empty variable names. */
		/*
		 * Found a right quote. Get potential variable name.
		 */
		for (r = p + 1, s = varName; q - r > 0; r ++, s ++) *s = *r;
		*s = '\0';   /* Trailing null byte. */
		colon = strchr (varName, ':');
		if (colon) {
			precision = atoi (colon + 1);
			*colon = '\0';
		}
		if (strequ (varName, "time")) {
			value = 0.5 * (my startSelection + my endSelection);
		} else if (strequ (varName, "t1")) {
			value = my startSelection;
		} else if (strequ (varName, "t2")) {
			value = my endSelection;
		} else if (strequ (varName, "dur")) {
			value = my endSelection - my startSelection;
		} else if (strequ (varName, "freq")) {
			value = my spectrogram.cursor;
		} else if (strequ (varName, "f0")) {
			if (! my pitch.show)
				return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the View menu.");
			if (! my pitch.data) {
				return Melder_error ("No pitch contour available (out of memory?).");
			}
			if (my startSelection == my endSelection)
				value = Pitch_getValueAtTime (my pitch.data, my startSelection, Pitch_yscaleToUnits (my pitch.units), 1);
			else
				value = Pitch_getMean (my pitch.data, my startSelection, my endSelection, Pitch_yscaleToUnits (my pitch.units));
		} else if (varName [0] == 'f' && varName [1] >= '1' && varName [1] <= '5' && varName [2] == '\0') {
			if (! my formant.show)
				return Melder_error ("No formant contour is visible.\nFirst choose \"Show formant\" from the View menu.");
			if (! my formant.data) {
				return Melder_error ("No formant contour available (out of memory?).");
			}
			if (my startSelection == my endSelection)
				value = Formant_getValueAtTime (my formant.data, varName [1] - '0', my startSelection, 0);
			else
				value = Formant_getMean (my formant.data, varName [1] - '0', my startSelection, my endSelection, 0);
		} else if (varName [0] == 'b' && varName [1] >= '1' && varName [1] <= '5' && varName [2] == '\0') {
			if (! my formant.show)
				return Melder_error ("No formant contour is visible.\nFirst choose \"Show formant\" from the View menu.");
			if (! my formant.data) {
				return Melder_error ("No formant contour available (out of memory?).");
			}
			value = Formant_getBandwidthAtTime (my formant.data, varName [1] - '0', 0.5 * (my startSelection + my endSelection), 0);
		} else if (strequ (varName, "intensity")) {
			if (! my intensity.show)
				return Melder_error ("No intensity contour is visible.\nFirst choose \"Show intensity\" from the View menu.");
			if (! my intensity.data) {
				return Melder_error ("No intensity contour available (out of memory?).");
			}
			if (my startSelection == my endSelection)
				value = Vector_getValueAtX (my intensity.data, my startSelection, 1);
			else
				value = Vector_getMean (my intensity.data, my startSelection, my endSelection);
		}
		if (NUMdefined (value)) {
			int varlen = (q - p) - 1, headlen = p - format;
			char formattedNumber [400];
			int arglen;
			if (precision >= 0) {
				sprintf (formattedNumber, "%.*f", precision, value);
			} else {
				sprintf (formattedNumber, "%.17g", value);
			}
			arglen = strlen (formattedNumber);
			strncpy (Melder_buffer1, format, headlen);
			strcpy (Melder_buffer1 + headlen, formattedNumber);
			strcpy (Melder_buffer1 + headlen + arglen, p + varlen + 2);
			strcpy (format, Melder_buffer1);
			p += arglen - 1;
		} else if (strequ (varName, "tab$")) {
			int headlen = p - format;
			strncpy (Melder_buffer1, format, headlen);
			strcpy (Melder_buffer1 + headlen, "\t");
			strcpy (Melder_buffer1 + headlen + 1, p + 4 + 2);
			strcpy (format, Melder_buffer1);
			p += 1 - 1;
		} else {
			p = q - 1;   /* Go to before next quote. */
		}
	}
	if (preferences.log[which].toInfoWindow) {
		Melder_info ("%s", format);
	}
	if (preferences.log[which].toLogFile) {
		struct MelderFile file;
		strcat (format, "\n");
		if (! Melder_pathToFile (preferences.log[which].fileName, & file)) return 0;
		if (! MelderFile_appendText (& file, format)) return 0;
	}
	return 1;
}

DIRECT (FunctionEditor, cb_log1) if (! cb_log (me, 0)) return 0; END
DIRECT (FunctionEditor, cb_log2) if (! cb_log (me, 1)) return 0; END

extern int DO_Exec_editor (Any editor, const char *script);
DIRECT (FunctionEditor, cb_logScript3) if (! DO_Exec_editor (me, preferences.logScript3)) return 0; END
DIRECT (FunctionEditor, cb_logScript4) if (! DO_Exec_editor (me, preferences.logScript4)) return 0; END

void FunctionEditor_SoundAnalysis_extractMenus (I) {
	iam (FunctionEditor);
	Editor_addCommand (me, "File", "Extract visible spectrogram", 0, cb_extractVisibleSpectrogram);
	Editor_addCommand (me, "File", "Extract visible pitch contour", 0, cb_extractVisiblePitchContour);
	Editor_addCommand (me, "File", "Extract visible intensity contour", 0, cb_extractVisibleIntensityContour);
	Editor_addCommand (me, "File", "Extract visible formant contour", 0, cb_extractVisibleFormantContour);
}

void FunctionEditor_SoundAnalysis_viewMenus (I) {
	iam (FunctionEditor);
	Editor_addCommand (me, "View", "Analysis window:", motif_INSENSITIVE, cb_showAnalyses);
	Editor_addCommand (me, "View", "Show analyses...", 0, cb_showAnalyses);
	Editor_addCommand (me, "View", "Spectrogram settings...", 0, cb_spectrogramSettings);
	Editor_addCommand (me, "View", "Pitch settings...", 0, cb_pitchSettings);
	Editor_addCommand (me, "View", "Intensity settings...", 0, cb_intensitySettings);
	Editor_addCommand (me, "View", "Formant settings...", 0, cb_formantSettings);
	Editor_addCommand (me, "View", "-- sound analysis --", 0, 0);
}

void FunctionEditor_SoundAnalysis_selectionQueries (I) {
	iam (FunctionEditor);
	Editor_addCommand (me, "Query", "-- query analyses --", 0, NULL);
	Editor_addCommand (me, "Query", "Get frequency", motif_F9, cb_getFrequency);
	Editor_addCommand (me, "Query", "Get pitch", motif_F10, cb_getPitch);
	Editor_addCommand (me, "Query", "Get minimum pitch", motif_F10 + motif_COMMAND, cb_getMinimumPitch);
	Editor_addCommand (me, "Query", "Get maximum pitch", motif_F10 + motif_SHIFT, cb_getMaximumPitch);
	Editor_addCommand (me, "Query", "Get intensity", motif_F11, cb_getIntensity);
	Editor_addCommand (me, "Query", "-- query formants --", 0, NULL);
	Editor_addCommand (me, "Query", "Formant report", 0, cb_formantReport);
	Editor_addCommand (me, "Query", "Get first formant", motif_F1, cb_getFirstFormant);
	Editor_addCommand (me, "Query", "Get first bandwidth", 0, cb_getFirstBandwidth);
	Editor_addCommand (me, "Query", "Get second formant", motif_F2, cb_getSecondFormant);
	Editor_addCommand (me, "Query", "Get second bandwidth", 0, cb_getSecondBandwidth);
	Editor_addCommand (me, "Query", "Get third formant", motif_F3, cb_getThirdFormant);
	Editor_addCommand (me, "Query", "Get third bandwidth", 0, cb_getThirdBandwidth);
	Editor_addCommand (me, "Query", "Get fourth formant", motif_F4, cb_getFourthFormant);
	Editor_addCommand (me, "Query", "Get fourth bandwidth", 0, cb_getFourthBandwidth);
	Editor_addCommand (me, "Query", "Get fifth formant", 0, cb_getFifthFormant);
	Editor_addCommand (me, "Query", "Get fifth bandwidth", 0, cb_getFifthBandwidth);
	Editor_addCommand (me, "Query", "-- query log --", 0, NULL);
	Editor_addCommand (me, "Query", "Log settings...", 0, cb_logSettings);
	Editor_addCommand (me, "Query", "Delete log file 1", 0, cb_deleteLogFile1);
	Editor_addCommand (me, "Query", "Delete log file 2", 0, cb_deleteLogFile2);
	Editor_addCommand (me, "Query", "Log 1", motif_F12, cb_log1);
	Editor_addCommand (me, "Query", "Log 2", motif_F12 + motif_SHIFT, cb_log2);
	Editor_addCommand (me, "Query", "Log script 3 (...)", motif_F12 + motif_OPTION, cb_logScript3);
	Editor_addCommand (me, "Query", "Log script 4 (...)", motif_F12 + motif_COMMAND, cb_logScript4);
}

void FunctionEditor_SoundAnalysis_selectMenus (I) {
	iam (FunctionEditor);
	Editor_addCommand (me, "Select", "-- pitch selections --", 0, NULL);
	Editor_addCommand (me, "Select", "Move cursor to minimum pitch", 'L', cb_moveCursorToMinimumPitch);
	Editor_addCommand (me, "Select", "Move cursor to maximum pitch", 'H', cb_moveCursorToMaximumPitch);
}

void FunctionEditor_SoundAnalysis_init (I) {
	iam (FunctionEditor);
	my spectrogram = preferences.spectrogram;
	my pitch = preferences.pitch;
	if (my pitch.units == Pitch_yscale_LOGARITHMIC && my pitch.viewFrom <= 0.0)
		my pitch.viewFrom = my pitch.viewTo / 1000.0;
	my intensity = preferences.intensity;
	my formant = preferences.formant;
}

/* End of file FunctionEditor_SoundAnalysis.c */
