/* FunctionEditor_SoundAnalysis.c
 *
 * Copyright (C) 1992-2003 Paul Boersma
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program 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 program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * pb 2002/05/28
 * pb 2002/07/16 GPL
 * pb 2002/10/11 added a screen text for unavailable pitch
 * pb 2002/11/19 default log file names include "~" on Unix and Mach
 * pb 2002/11/19 added pulses and five separate analysis menus
 * pb 2003/02/19 clearer wording
 * pb 2003/02/24 spectral slices
 * pb 2003/03/03 Extract visible spectrogram: higher degree of oversampling than in editor
 * pb 2003/03/10 undid previous change because our PostScript code now does image interpolation
 * pb 2003/03/12 queriable
 * pb 2003/04/03 Get power at cursor cross
 * pb 2003/04/11 smaller log settings dialog
 * pb 2003/05/18 more shimmer measurements
 * pb 2003/05/20 longestAnalysis replaces the pitch/formant/intensity time steps,
 *               pitch.speckle, and formant.maximumDuration
 * pb 2003/05/21 pitch floor and ceiling replace the separate ranges for viewing and analysis
 * pb 2003/05/27 spectrogram maximum and autoscaling
 * pb 2003/07/20 moved voice report to VoiceAnalysis.c
 * pb 2003/08/23 reintroduced formant.numberOfTimeSteps
 * pb 2003/09/11 make sure that analyses objects can be extracted even before first drawing
 * pb 2003/09/16 advanced pitch settings: pitch.timeStep, pitch.viewFrom, pitch.viewTo, pitch.timeStepsPerView
 * pb 2003/10/01 time step settings: timeStepStrategy, fixedTimeStep, numberOfTimeStepsPerView
 */

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

#define SPECKLE_STEPS  100

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

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

#if defined (macintosh)
	#if defined __MACH__
		#define LOG_1_FILE_NAME "~/Desktop/Pitch Log"
		#define LOG_2_FILE_NAME "~/Desktop/Formant Log"
		#define LOG_3_FILE_NAME "~/Desktop/Log script 3"
		#define LOG_4_FILE_NAME "~/Desktop/Log script 4"
	#else
		#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"
	#endif
#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 "~/pitch_log"
	#define LOG_2_FILE_NAME "~/formant_log"
	#define LOG_3_FILE_NAME "~/log_script3"
	#define LOG_4_FILE_NAME "~/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 {
	double longestAnalysis;
	int timeStepStrategy; double fixedTimeStep; long numberOfTimeStepsPerView;
	struct FunctionEditor_spectrogram spectrogram;
	struct FunctionEditor_pitch pitch;
	struct FunctionEditor_intensity intensity;
	struct FunctionEditor_formant formant;
	struct FunctionEditor_pulse pulse;
	struct logInfo log [2];
	char logScript3 [256], logScript4 [256];
} preferences = {
	10.0,
	1 /* automatic time step */, 0.01 /* in case of a fixed step */, 100 /* in case of a fixed number per view */,
	{
		NULL, TRUE,
		1000, 250, 0.0, 5000.0, 0.0,
		1, 0.005, 5,
		100.0, TRUE,
		70.0, 6.0, 0.0
	}, {
		NULL, TRUE,
		75.0, 500.0, Pitch_yscale_LINEAR,
		0.0 /* auto view from */, 0.0 /* auto view to */,
		1, FALSE,
		15, 0.03, 0.45,
		0.01, 0.35, 0.14
	}, {
		NULL, FALSE,
		50.0, 100.0
	}, {
		NULL, TRUE,
		5500.0, 10,
		0.025,
		30.0, 1.0,
		1, 50.0,
	}, {
		NULL, TRUE
	}, {
		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_addDouble ("FunctionEditor.longestAnalysis", & preferences.longestAnalysis);
	Resources_addInt ("FunctionEditor.timeStepStrategy", & preferences.timeStepStrategy);
	Resources_addDouble ("FunctionEditor.fixedTimeStep", & preferences.fixedTimeStep);
	Resources_addLong ("FunctionEditor.numberOfTimeStepsPerView", & preferences.numberOfTimeStepsPerView);
	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.maximum", & preferences.spectrogram.maximum);
	Resources_addInt ("FunctionEditor.spectrogram.autoscaling", & preferences.spectrogram.autoscaling);
	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_addDouble ("FunctionEditor.pitch.floor", & preferences.pitch.floor);
	Resources_addDouble ("FunctionEditor.pitch.ceiling", & preferences.pitch.ceiling);
	Resources_addInt ("FunctionEditor.pitch.units", & preferences.pitch.units);
	Resources_addDouble ("FunctionEditor.pitch.viewFrom", & preferences.pitch.viewFrom);
	Resources_addDouble ("FunctionEditor.pitch.viewTo", & preferences.pitch.viewTo);
	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_addDouble ("FunctionEditor.intensity.viewFrom", & preferences.intensity.viewFrom);
	Resources_addDouble ("FunctionEditor.intensity.viewTo", & preferences.intensity.viewTo);
	Resources_addInt ("FunctionEditor.formant.show", & preferences.formant.show);
	Resources_addDouble ("FunctionEditor.formant.maximumFormant", & preferences.formant.maximumFormant);
	Resources_addLong ("FunctionEditor.formant.numberOfPoles", & preferences.formant.numberOfPoles);
	Resources_addDouble ("FunctionEditor.formant.windowLength", & preferences.formant.windowLength);
	Resources_addDouble ("FunctionEditor.formant.dynamicRange", & preferences.formant.dynamicRange);
	Resources_addDouble ("FunctionEditor.formant.dotSize", & preferences.formant.dotSize);
	Resources_addInt ("FunctionEditor.formant.method", & preferences.formant.method);
	Resources_addDouble ("FunctionEditor.formant.preemphasisFrom", & preferences.formant.preemphasisFrom);
	Resources_addInt ("FunctionEditor.pulse.show", & preferences.pulse.show);
	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);
	forget (my pulse.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 endWindow - my startWindow <= my longestAnalysis &&
		(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_inside (FunctionEditor me) {
	Sound sound = NULL;
	double margin = my pitch.veryAccurate ? 3.0 / my pitch.floor : 1.5 / my pitch.floor;
	forget (my pitch.data);
	sound = extractSound (me, my startWindow - margin, my endWindow + margin);
	if (sound != NULL) {
		double pitchTimeStep =
			my timeStepStrategy == 2 ? my fixedTimeStep :
			my timeStepStrategy == 3 ? (my endWindow - my startWindow) / my numberOfTimeStepsPerView :
			0.0;   /* The default: determined by pitch floor. */
		my pitch.data = Sound_to_Pitch_any (sound, pitchTimeStep,
			my pitch.floor, 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.ceiling);
		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 ();
}

static void computePitch (FunctionEditor me) {
	Melder_progressOff ();
	if (my pitch.show && my endWindow - my startWindow <= my longestAnalysis &&
		(my pitch.data == NULL || my pitch.data -> xmin != my startWindow || my pitch.data -> xmax != my endWindow))
	{
		computePitch_inside (me);
	}
	Melder_progressOn ();
}

static void computeIntensity (FunctionEditor me) {
	Melder_progressOff ();
	if (my intensity.show && my endWindow - my startWindow <= my longestAnalysis &&
		(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.floor;
		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.floor,
				my endWindow - my startWindow > my longestAnalysis ? (my endWindow - my startWindow) / 100 : 0.0);
			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 endWindow - my startWindow <= my longestAnalysis &&
		(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 longestAnalysis)
			sound = extractSound (me,
				0.5 * (my startWindow + my endWindow - my longestAnalysis) - margin,
				0.5 * (my startWindow + my endWindow + my longestAnalysis) + margin);
		else
			sound = extractSound (me, my startWindow - margin, my endWindow + margin);
		if (sound != NULL) {
			double formantTimeStep =
				my timeStepStrategy == 2 ? my fixedTimeStep :
				my timeStepStrategy == 3 ? (my endWindow - my startWindow) / my numberOfTimeStepsPerView :
				0.0;   /* The default: determined by analysis window length. */
			my formant.data = Sound_to_Formant_any (sound, formantTimeStep,
				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 ();
}

static void computePulses (FunctionEditor me) {
	Melder_progressOff ();
	if (my pulse.show && my endWindow - my startWindow <= my longestAnalysis &&
		(my pulse.data == NULL || my pulse.data -> xmin != my startWindow || my pulse.data -> xmax != my endWindow))
	{
		if (my pitch.data == NULL || my pitch.data -> xmin != my startWindow || my pitch.data -> xmax != my endWindow) {
			computePitch_inside (me);
		}
		if (my pitch.data != NULL) {
			Sound sound = NULL;
			forget (my pulse.data);
			sound = extractSound (me, my startWindow, my endWindow);
			if (sound != NULL) {
				my pulse.data = Sound_Pitch_to_PointProcess_cc (sound, my pitch.data);
				if (my pulse.data == NULL) Melder_clearError ();
				forget (sound);
			} else Melder_clearError ();
		} else Melder_clearError ();
	}
	Melder_progressOn ();
}

static int queriable (FunctionEditor me) {
	if (my endWindow - my startWindow > my longestAnalysis) {
		return Melder_error ("Window too long to show analyses. Zoom in to at most %s seconds or set the \"longest analysis\" "
			"to at least %s seconds.", Melder_half (my longestAnalysis), Melder_half (my endWindow - my startWindow));
	}
	if (my startSelection < my startWindow || my endSelection > my endWindow) {
		if (my startSelection == my endSelection) {
			Melder_error ("The cursor is outside the window.");
		} else if (my startSelection > my startWindow && my startSelection < my endWindow ||
		         my endSelection > my startWindow && my endSelection < my endWindow) {
			Melder_error ("Part of the selection is outside the window.");
		} else {
			Melder_error ("The selection is outside the window.");
		}
		return Melder_error ("Put the cursor inside the window, or make a selection inside the window.");
	}
	return TRUE;
}

/***** 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)
	BOOLEAN ("Show pulses", 0)
	POSITIVE ("Longest analysis (s)", "5.0")
	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)
SET_INTEGER ("Show pulses", my pulse.show)
SET_REAL ("Longest analysis", my longestAnalysis)
DO
	XmToggleButtonGadgetSetState (my spectrogramToggle, preferences.spectrogram.show = my spectrogram.show = GET_INTEGER ("Show spectrogram"), False);
	XmToggleButtonGadgetSetState (my pitchToggle, preferences.pitch.show = my pitch.show = GET_INTEGER ("Show pitch"), False);
	XmToggleButtonGadgetSetState (my intensityToggle, preferences.intensity.show = my intensity.show = GET_INTEGER ("Show intensity"), False);
	XmToggleButtonGadgetSetState (my formantToggle, preferences.formant.show = my formant.show = GET_INTEGER ("Show formants"), False);
	XmToggleButtonGadgetSetState (my pulseToggle, preferences.pulse.show = my pulse.show = GET_INTEGER ("Show pulses"), False);
	preferences.longestAnalysis = my longestAnalysis = GET_REAL ("Longest analysis");
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_timeStepSettings, "Time step settings", "Time step settings...")
	OPTIONMENU ("Time step strategy", 1)
		OPTION ("Automatic")
		OPTION ("Fixed")
		OPTION ("View-dependent")
	LABEL ("", "")
	LABEL ("", "If the time step strategy is \"fixed\":")
	POSITIVE ("Fixed time step (s)", "0.01")
	LABEL ("", "")
	LABEL ("", "If the time step strategy is \"view-dependent\":")
	NATURAL ("Number of time steps per view", "100")
	OK
SET_INTEGER ("Time step strategy", my timeStepStrategy)
SET_REAL ("Fixed time step", my fixedTimeStep)
SET_INTEGER ("Number of time steps per view", my numberOfTimeStepsPerView)
DO
	preferences.timeStepStrategy = my timeStepStrategy = GET_INTEGER ("Time step strategy");
	preferences.fixedTimeStep = my fixedTimeStep = GET_REAL ("Fixed time step");
	preferences.numberOfTimeStepsPerView = my numberOfTimeStepsPerView = GET_INTEGER ("Number of time steps per view");
	forget (my pitch.data);
	forget (my formant.data);
	forget (my intensity.data);
	forget (my pulse.data);
	FunctionEditor_redraw (me);
END

/***** SPECTROGRAM MENU *****/

DIRECT (FunctionEditor, cb_showSpectrogram)
	preferences.spectrogram.show = my spectrogram.show = ! my spectrogram.show;
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_spectrogramSettings, "Spectrogram settings", "Intro 3.2. Configuring the spectrogram")
	REAL ("left View range (Hz)", "0.0")
	POSITIVE ("right View range (Hz)", "5000.0")
	POSITIVE ("Window length (s)", "0.005")
	POSITIVE ("Dynamic range (dB)", "50.0")
	LABEL ("note", "")
	OK
SET_REAL ("left View range", my spectrogram.viewFrom)
SET_REAL ("right View range", my spectrogram.viewTo)
SET_REAL ("Window length", my spectrogram.windowLength)
SET_REAL ("Dynamic range", my spectrogram.dynamicRange)
if (my spectrogram.timeSteps != 1000 || my spectrogram.frequencySteps != 250 || my spectrogram.method != 1 ||
    my spectrogram.windowShape != 5 || my spectrogram.maximum != 100.0 || my spectrogram.autoscaling != TRUE ||
    my spectrogram.preemphasis != 6.0 || my spectrogram.dynamicCompression != 0.0)
{
	SET_STRING ("note", "Warning: you have non-standard \"advanced settings\".")
} else {
	SET_STRING ("note", "Note: all \"advanced settings\" have their standard values.")
}
DO
	preferences.spectrogram.viewFrom = my spectrogram.viewFrom = GET_REAL ("left View range");
	preferences.spectrogram.viewTo = my spectrogram.viewTo = GET_REAL ("right View range");
	preferences.spectrogram.windowLength = my spectrogram.windowLength = GET_REAL ("Window length");
	preferences.spectrogram.dynamicRange = my spectrogram.dynamicRange = GET_REAL ("Dynamic range");
	forget (my spectrogram.data);
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_advancedSpectrogramSettings, "Advanced spectrogram settings", "Advanced spectrogram settings...")
	LABEL ("", "Time and frequency resolutions:")
	NATURAL ("Number of time steps", "1000")
	NATURAL ("Number of frequency steps", "250")
	LABEL ("", "Spectrogram analysis settings:")
	OPTIONMENU ("Method", 1)
		OPTION ("Fourier")
	OPTIONMENU ("Window shape", 6)
		OPTION ("Square (rectangular)")
		OPTION ("Hamming (raised sine-squared)")
		OPTION ("Bartlett (triangular)")
		OPTION ("Welch (parabolic)")
		OPTION ("Hanning (sine-squared)")
		OPTION ("Gaussian")
	LABEL ("", "Spectrogram view settings:")
	BOOLEAN ("Autoscaling", 1)
	REAL ("Maximum (dB/Hz)", "100.0")
	REAL ("Pre-emphasis (dB/oct)", "6.0")
	REAL ("Dynamic compression (0-1)", "0.0")
	OK
SET_INTEGER ("Number of time steps", my spectrogram.timeSteps)
SET_INTEGER ("Number of frequency steps", my spectrogram.frequencySteps)
SET_INTEGER ("Method", my spectrogram.method)
SET_INTEGER ("Window shape", my spectrogram.windowShape + 1)
SET_REAL ("Maximum", my spectrogram.maximum)
SET_INTEGER ("Autoscaling", my spectrogram.autoscaling)
SET_REAL ("Pre-emphasis", my spectrogram.preemphasis)
SET_REAL ("Dynamic compression", my spectrogram.dynamicCompression)
DO
	preferences.spectrogram.timeSteps = my spectrogram.timeSteps = GET_INTEGER ("Number of time steps");
	preferences.spectrogram.frequencySteps = my spectrogram.frequencySteps = GET_INTEGER ("Number of frequency steps");
	preferences.spectrogram.method = my spectrogram.method = GET_INTEGER ("Method");
	preferences.spectrogram.windowShape = my spectrogram.windowShape = GET_INTEGER ("Window shape") - 1;
	preferences.spectrogram.maximum = my spectrogram.maximum = GET_REAL ("Maximum");
	preferences.spectrogram.autoscaling = my spectrogram.autoscaling = GET_INTEGER ("Autoscaling");
	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

DIRECT (FunctionEditor, cb_extractVisibleSpectrogram)
	Spectrogram publish;
	if (! my spectrogram.show)
		return Melder_error ("No spectrogram is visible.\nFirst choose \"Show spectrogram\" from the Spectrogram menu.");
	if (! my spectrogram.data) {
		computeSpectrogram (me);
		if (! my spectrogram.data) return Melder_error ("No spectrogram available (out of memory?).");
	}
	publish = Data_copy (my spectrogram.data);
	if (publish == NULL) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

DIRECT (FunctionEditor, cb_viewSpectralSlice)
	double start = my startSelection == my endSelection ?
		my spectrogram.windowShape == 5 ? my startSelection - my spectrogram.windowLength :
		my startSelection - my spectrogram.windowLength / 2 : my startSelection;
	double finish = my startSelection == my endSelection ?
		my spectrogram.windowShape == 5 ? my endSelection + my spectrogram.windowLength :
		my endSelection + my spectrogram.windowLength / 2 : my endSelection;
	Sound sound = extractSound (me, start, finish);
	Spectrum publish;
	if (sound == NULL) return 0;
	Sound_multiplyByWindow (sound,
		my spectrogram.windowShape == 0 ? enumi (Sound_WINDOW, Rectangular) :
		my spectrogram.windowShape == 1 ? enumi (Sound_WINDOW, Hamming) :
		my spectrogram.windowShape == 2 ? enumi (Sound_WINDOW, Triangular) :
		my spectrogram.windowShape == 3 ? enumi (Sound_WINDOW, Parabolic) :
		my spectrogram.windowShape == 4 ? enumi (Sound_WINDOW, Hanning) :
		my spectrogram.windowShape == 5 ? enumi (Sound_WINDOW, Gaussian2) : 0);
	publish = Sound_to_Spectrum_fft (sound);
	forget (sound);
	if (! publish) return 0;
	Thing_setName (publish, "slice");
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

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

DIRECT (FunctionEditor, cb_getSpectralPowerAtCursorCross)
	if (! my spectrogram.show)
		return Melder_error ("No spectrogram is visible.\nFirst choose \"Show spectrogram\" from the Spec. menu.");
	if (! my spectrogram.data) {
		computeSpectrogram (me);
		if (! my spectrogram.data) return Melder_error ("No spectrogram available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	if (my startSelection != my endSelection) return Melder_error ("Click inside the spectrogram first.");
	MelderInfo_open ();
	MelderInfo_write1 (Melder_double (Matrix_getValueAtXY (my spectrogram.data, my startSelection, my spectrogram.cursor)));
	MelderInfo_write5 (" Pa2/Hz (at time = ", Melder_double (my startSelection), " seconds and frequency = ",
		Melder_double (my spectrogram.cursor), " Hz)");
	MelderInfo_close ();
END

/***** PITCH MENU *****/

DIRECT (FunctionEditor, cb_showPitch)
	preferences.pitch.show = my pitch.show = ! my pitch.show;
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_pitchSettings, "Pitch settings", "Intro 4.2. Configuring the pitch contour")
	POSITIVE ("left Pitch range (Hz)", "75.0")
	POSITIVE ("right Pitch range (Hz)", "500.0")
	OPTIONMENU ("Units", 1)
		OPTION ("Hertz")
		OPTION ("Hertz logarithmic")
		OPTION ("Semitones re 100 Hz")
		OPTION ("Mel")
		OPTION ("Erb")
	LABEL ("note", "")
	OK
SET_REAL ("left Pitch range", my pitch.floor)
SET_REAL ("right Pitch range", my pitch.ceiling)
SET_INTEGER ("Units", my pitch.units)
if (my pitch.viewFrom != 0.0 || my pitch.viewTo != 0.0 ||
    my pitch.method != 1 || my pitch.veryAccurate != FALSE || my pitch.maximumNumberOfCandidates != 15 ||
    my pitch.silenceThreshold != 0.03 || my pitch.voicingThreshold != 0.45 || my pitch.octaveCost != 0.01 ||
    my pitch.octaveJumpCost != 0.35 || my pitch.voicedUnvoicedCost != 0.14)
{
	SET_STRING ("note", "Warning: you have some non-standard \"advanced settings\".")
} else {
	SET_STRING ("note", "Note: all of your \"advanced settings\" have their standard values.")
}
DO
	preferences.pitch.floor = my pitch.floor = GET_REAL ("left Pitch range");
	preferences.pitch.ceiling = my pitch.ceiling = GET_REAL ("right Pitch range");
	preferences.pitch.units = my pitch.units = GET_INTEGER ("Units");
	forget (my pitch.data);
	forget (my intensity.data);
	forget (my pulse.data);
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_advancedPitchSettings, "Advanced pitch settings", "Advanced pitch settings...")
	LABEL ("", "Make view range different from analysis range:")
	REAL ("left View range (units)", "0.0 (= auto)")
	REAL ("right View range (units)", "0.0 (= auto)")
	LABEL ("", "Analysis settings:")
	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_REAL ("left View range", my pitch.viewFrom)
SET_REAL ("right View range", my pitch.viewTo)
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.viewFrom = my pitch.viewFrom = GET_REAL ("left View range");
	preferences.pitch.viewTo = my pitch.viewTo = GET_REAL ("right View 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);
	forget (my intensity.data);
	forget (my pulse.data);
	FunctionEditor_redraw (me);
END

DIRECT (FunctionEditor, cb_getPitch)
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the Pitch menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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 Pitch menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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 Pitch menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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_extractVisiblePitchContour)
	Pitch publish;
	if (! my pitch.show)
		return Melder_error ("No pitch contour is visible.\nFirst choose \"Show pitch\" from the Pitch menu.");
	if (! my pitch.data) {
		computePitch (me);
		if (! my pitch.data) return Melder_error ("No pitch contour available (out of memory?).");
	}
	publish = Data_copy (my pitch.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

/***** INTENSITY MENU *****/

DIRECT (FunctionEditor, cb_showIntensity)
	preferences.intensity.show = my intensity.show = ! my intensity.show;
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_intensitySettings, "Intensity settings", "Intro 6.2. Configuring the intensity contour")
	REAL ("left View range (dB)", "50.0")
	REAL ("right View range (dB)", "100.0")
	LABEL ("", "Note: the pitch floor is taken from the pitch settings")
	OK
SET_REAL ("left View range", my intensity.viewFrom)
SET_REAL ("right View range", my intensity.viewTo)
DO
	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

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

DIRECT (FunctionEditor, cb_getIntensity)
	if (! my intensity.show)
		return Melder_error ("No intensity contour is visible.\nFirst choose \"Show intensity\" from the Intensity menu.");
	if (! my intensity.data) {
		computeIntensity (me);
		if (! my intensity.data) return Melder_error ("No intensity contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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

/***** FORMANT MENU *****/

DIRECT (FunctionEditor, cb_showFormants)
	preferences.formant.show = my formant.show = ! my formant.show;
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_formantSettings, "Formant settings", "Intro 5.2. Configuring the formant contours")
	POSITIVE ("Maximum formant (Hz)", "5500.0")
	POSITIVE ("Number of formants", "5.0")
	POSITIVE ("Window length (s)", "0.025")
	REAL ("Dynamic range (dB)", "30.0")
	POSITIVE ("Dot size (mm)", "1.0")
	LABEL ("note", "")
	OK
SET_REAL ("Maximum formant", my formant.maximumFormant)
SET_REAL ("Number of formants", 0.5 * my formant.numberOfPoles)
SET_REAL ("Window length", my formant.windowLength)
SET_REAL ("Dynamic range", my formant.dynamicRange)
SET_REAL ("Dot size", my formant.dotSize)
if (my formant.method != 1 || my formant.preemphasisFrom != 50.0) {
	SET_STRING ("note", "Warning: you have non-standard \"advanced settings\".")
} else {
	SET_STRING ("note", "Note: all \"advanced settings\" have their standard values.")
}
DO
	preferences.formant.maximumFormant = my formant.maximumFormant = GET_REAL ("Maximum formant");
	preferences.formant.numberOfPoles = my formant.numberOfPoles = 2.0 * GET_REAL ("Number of formants");
	preferences.formant.windowLength = my formant.windowLength = GET_REAL ("Window length");
	preferences.formant.dynamicRange = my formant.dynamicRange = GET_REAL ("Dynamic range");
	preferences.formant.dotSize = my formant.dotSize = GET_REAL ("Dot size");
	forget (my formant.data);
	FunctionEditor_redraw (me);
END

FORM (FunctionEditor, cb_advancedFormantSettings, "Advanced formant settings", "Advanced formant settings...")
	RADIO ("Method", 1)
		RADIOBUTTON ("Burg")
	POSITIVE ("Pre-emphasis from (Hz)", "50.0")
	OK
SET_INTEGER ("Method", my formant.method)
SET_REAL ("Pre-emphasis from", my formant.preemphasisFrom)
DO
	preferences.formant.method = my formant.method = GET_INTEGER ("Method");
	preferences.formant.preemphasisFrom = my formant.preemphasisFrom = GET_REAL ("Pre-emphasis from");
	forget (my formant.data);
	FunctionEditor_redraw (me);
END

DIRECT (FunctionEditor, cb_extractVisibleFormantContour)
	Formant publish;
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formants\" from the Formant menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	publish = Data_copy (my formant.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

DIRECT (FunctionEditor, cb_formantReport)
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formants\" from the Formant menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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);
		MelderInfo_open ();
		MelderInfo_writeLine1 ("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);
			MelderInfo_write5 (Melder_fixed (t, 6), "   ", Melder_fixed (f1, 6), "   ", Melder_fixed (f2, 6));
			MelderInfo_writeLine4 ("   ", Melder_fixed (f3, 6), "   ", Melder_fixed (f4, 6));
		}
		MelderInfo_close ();
	}
	return 1;
END

static int getFormant (FunctionEditor me, int iformant) {
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formants\" from the Formant menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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 getBandwidth (FunctionEditor me, int iformant) {
	if (! my formant.show)
		return Melder_error ("No formant contour is visible.\nFirst choose \"Show formants\" from the Formant menu.");
	if (! my formant.data) {
		computeFormants (me);
		if (! my formant.data) return Melder_error ("No formant contour available (out of memory?).");
	}
	if (! queriable (me)) return 0;
	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 (! getFormant (me, 1)) return 0; END
DIRECT (FunctionEditor, cb_getFirstBandwidth) if (! getBandwidth (me, 1)) return 0; END
DIRECT (FunctionEditor, cb_getSecondFormant) if (! getFormant (me, 2)) return 0; END
DIRECT (FunctionEditor, cb_getSecondBandwidth) if (! getBandwidth (me, 2)) return 0; END
DIRECT (FunctionEditor, cb_getThirdFormant) if (! getFormant (me, 3)) return 0; END
DIRECT (FunctionEditor, cb_getThirdBandwidth) if (! getBandwidth (me, 3)) return 0; END
DIRECT (FunctionEditor, cb_getFourthFormant) if (! getFormant (me, 4)) return 0; END
DIRECT (FunctionEditor, cb_getFourthBandwidth) if (! getBandwidth (me, 4)) return 0; END
FORM (FunctionEditor, cb_getFormant, "Get formant", 0)
	NATURAL ("Formant number", "5")
OK DO if (! getFormant (me, GET_INTEGER ("Formant number"))) return 0; END
FORM (FunctionEditor, cb_getBandwidth, "Get bandwidth", 0)
	NATURAL ("Formant number", "5")
OK DO if (! getBandwidth (me, GET_INTEGER ("Formant number"))) return 0; END

/***** PULSE MENU *****/

DIRECT (FunctionEditor, cb_showPulses)
	preferences.pulse.show = my pulse.show = ! my pulse.show;
	FunctionEditor_redraw (me);
END

DIRECT (FunctionEditor, cb_extractVisiblePulses)
	Pitch publish;
	if (! my pulse.show)
		return Melder_error ("No pulses are visible.\nFirst choose \"Show pulses\" from the Pulse menu.");
	if (! my pulse.data) {
		computePulses (me);
		if (! my pulse.data) return Melder_error ("No pulses available (out of memory?).");
	}
	publish = Data_copy (my pulse.data);
	if (! publish) return 0;
	if (my publishCallback)
		my publishCallback (me, my publishClosure, publish);
END

DIRECT (FunctionEditor, cb_voiceReport)
	Sound sound = NULL;
	double tmin = my startSelection, tmax = my endSelection;
	if (! my pulse.show)
		return Melder_error ("No pulses are visible.\nFirst choose \"Show pulses\" from the Pulse menu.");
	if (! my pulse.data) {
		computePulses (me);
		if (! my pulse.data) return Melder_error ("No pulses available (out of memory?).");
	}
	if (tmin == tmax) return Melder_error ("Make a selection first.");
	if (! queriable (me)) return 0;
	sound = extractSound (me, tmin, tmax);
	if (! sound) return Melder_error ("Selection too small (or out of memory).");
	Sound_Pitch_PointProcess_voiceReport (sound, my pitch.data, my pulse.data, tmin, tmax,
		my pitch.floor, my pitch.ceiling, my pitch.silenceThreshold, my pitch.voicingThreshold);
	forget (sound);
END

static int cb_getJitter_xx (FunctionEditor me, double (*PointProcess_getJitter_xx) (PointProcess, double, double, double, double)) {
	double pmin = 0.8 / my pitch.ceiling, pmax = 1.25 / my pitch.floor;
	if (! my pulse.show)
		return Melder_error ("No pulses are visible.\nFirst choose \"Show pulses\" from the Pulse menu.");
	if (! my pulse.data) {
		computePulses (me);
		if (! my pulse.data) return Melder_error ("No pulses available (out of memory?).");
	}
	if (my startSelection == my endSelection)
		return Melder_error ("Make a selection first.");
	if (! queriable (me)) return 0;
	Melder_informationReal (PointProcess_getJitter_xx (my pulse.data, my startSelection, my endSelection, pmin, pmax), NULL);
	return 1;
}
DIRECT (FunctionEditor, cb_getJitter_local) if (! cb_getJitter_xx (me, PointProcess_getJitter_local)) return 0; END
DIRECT (FunctionEditor, cb_getJitter_local_absolute) if (! cb_getJitter_xx (me, PointProcess_getJitter_local_absolute)) return 0; END
DIRECT (FunctionEditor, cb_getJitter_rap) if (! cb_getJitter_xx (me, PointProcess_getJitter_rap)) return 0; END
DIRECT (FunctionEditor, cb_getJitter_ppq5) if (! cb_getJitter_xx (me, PointProcess_getJitter_ppq5)) return 0; END
DIRECT (FunctionEditor, cb_getJitter_ddp) if (! cb_getJitter_xx (me, PointProcess_getJitter_ddp)) return 0; END

static int cb_getShimmer_xx (FunctionEditor me, double (*PointProcess_Sound_getShimmer_xx) (PointProcess, Sound, double, double, double, double)) {
	Sound sound = NULL;
	double pmin = 0.8 / my pitch.ceiling, pmax = 1.25 / my pitch.floor;
	if (! my pulse.show)
		return Melder_error ("No pulses are visible.\nFirst choose \"Show pulses\" from the Pulse menu.");
	if (! my pulse.data) {
		computePulses (me);
		if (! my pulse.data) return Melder_error ("No pulses available (out of memory?).");
	}
	if (my startSelection == my endSelection)
		return Melder_error ("Make a selection first.");
	if (! queriable (me)) return 0;
	sound = extractSound (me, my startSelection, my endSelection);
	if (! sound) return Melder_error ("Selection too small (or out of memory).");
	Melder_informationReal (PointProcess_Sound_getShimmer_xx (my pulse.data, sound, my startSelection, my endSelection, pmin, pmax), NULL);
	forget (sound);
	return 1;
}
DIRECT (FunctionEditor, cb_getShimmer_local) if (! cb_getShimmer_xx (me, PointProcess_Sound_getShimmer_local)) return 0; END
DIRECT (FunctionEditor, cb_getShimmer_local_dB) if (! cb_getShimmer_xx (me, PointProcess_Sound_getShimmer_local_dB)) return 0; END
DIRECT (FunctionEditor, cb_getShimmer_apq3) if (! cb_getShimmer_xx (me, PointProcess_Sound_getShimmer_apq3)) return 0; END
DIRECT (FunctionEditor, cb_getShimmer_apq5) if (! cb_getShimmer_xx (me, PointProcess_Sound_getShimmer_apq5)) return 0; END
DIRECT (FunctionEditor, cb_getShimmer_apq11) if (! cb_getShimmer_xx (me, PointProcess_Sound_getShimmer_apq11)) return 0; END
DIRECT (FunctionEditor, cb_getShimmer_dda) if (! cb_getShimmer_xx (me, PointProcess_Sound_getShimmer_dda)) return 0; 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);
	double pitchViewFrom = my pitch.viewFrom < my pitch.viewTo ? my pitch.viewFrom :
		my pitch.units == Pitch_yscale_LOGARITHMIC ? my pitch.floor : Pitch_convertFrequency (my pitch.floor, my pitch.units);
	double pitchViewTo = my pitch.viewFrom < my pitch.viewTo ? my pitch.viewTo :
		my pitch.units == Pitch_yscale_LOGARITHMIC ? my pitch.ceiling : Pitch_convertFrequency (my pitch.ceiling, my pitch.units);
	double yPitchViewFrom = my pitch.units == Pitch_yscale_LOGARITHMIC ? log10 (pitchViewFrom) : pitchViewFrom;
	double yPitchViewTo = my pitch.units == Pitch_yscale_LOGARITHMIC ? log10 (pitchViewTo) : pitchViewTo;
	if (my endWindow - my startWindow > my longestAnalysis) {
		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_printf (my graphics, 0.5, 0.67, "To see the analyses, zoom in to at most %s seconds,", Melder_half (my longestAnalysis));
		Graphics_text (my graphics, 0.5, 0.33, "or raise the \"longest analysis\" setting in the View menu.");
		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.maximum, my spectrogram.autoscaling,
			my spectrogram.dynamicRange, my spectrogram.preemphasis, my spectrogram.dynamicCompression);
	computePitch (me);
	if (my pitch.show && my pitch.data != NULL) {
		double periodsPerAnalysisWindow = my pitch.method == 1 ? 3.0 : 1.0;
		double greatestNonUndersamplingTimeStep = periodsPerAnalysisWindow / my pitch.floor / 2.0;
		int speckle = my timeStepStrategy == 1 ? my fixedTimeStep > greatestNonUndersamplingTimeStep :
			my timeStepStrategy == 2 ? (my endWindow - my startWindow) / my numberOfTimeStepsPerView > greatestNonUndersamplingTimeStep :
			FALSE;
		Graphics_setColour (my graphics, Graphics_CYAN);
		Graphics_setLineWidth (my graphics, 3.0);
		Pitch_drawInside (my pitch.data, my graphics, my startWindow, my endWindow, pitchViewFrom, pitchViewTo, 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, pitchViewFrom, pitchViewTo, 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, yPitchCursor = NUMundefined;
		Graphics_setWindow (my graphics, my startWindow, my endWindow, yPitchViewFrom, yPitchViewTo);
		Graphics_setColour (my graphics, Graphics_NAVY);
		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;
			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 - yPitchViewFrom) > 5.0) {
				Graphics_setTextAlignment (my graphics, Graphics_LEFT, Graphics_BOTTOM);
				Graphics_printf (my graphics, my endWindow, yPitchViewFrom - Graphics_dyMMtoWC (my graphics, 0.5), "%.4g %s",
					pitchViewFrom, Pitch_shortUnitText (my pitch.units));
			}
			if (! NUMdefined (pitchCursor) || Graphics_dyWCtoMM (my graphics, pitchViewTo - yPitchCursor) > 5.0) {
				Graphics_setTextAlignment (my graphics, Graphics_LEFT, Graphics_TOP);
				Graphics_printf (my graphics, my endWindow, yPitchViewTo, "%.4g %s",
					pitchViewTo, Pitch_shortUnitText (my pitch.units));
			}
		} else {
			Graphics_setTextAlignment (my graphics, Graphics_CENTRE, Graphics_HALF);
			Graphics_text (my graphics, 0.5 * (my startWindow + my endWindow), 0.5 * (yPitchViewFrom + yPitchViewTo),
				"(Cannot show pitch contour. Change pitch analysis settings, probably floor of pitch range.)");
		}
		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);
	}
}

void FunctionEditor_SoundAnalysis_drawPulses (I) {
	iam (FunctionEditor);
	computePulses (me);
	if (my pulse.show && my endWindow - my startWindow <= my longestAnalysis && my pulse.data != NULL) {
		long i;
		PointProcess point = my pulse.data;
		Graphics_setWindow (my graphics, my startWindow, my endWindow, -1.0, 1.0);
		Graphics_setColour (my graphics, Graphics_BLUE);
		if (point -> nt < 2000) for (i = 1; i <= point -> nt; i ++) {
			double t = point -> t [i];
			if (t >= my startWindow && t <= my endWindow)
				Graphics_line (my graphics, t, -0.9, t, 0.9);
		}
		Graphics_setColour (my graphics, Graphics_BLACK);
	}
}

FORM (FunctionEditor, cb_logSettings, "Log settings", "Log files")
	OPTIONMENU ("Write log 1 to", 3)
		OPTION ("Log file only")
		OPTION ("Info window only")
		OPTION ("Log file and Info window")
	LABEL ("", "Log file 1:")
	TEXTFIELD ("Log file 1", LOG_1_FILE_NAME)
	LABEL ("", "Log 1 format:")
	TEXTFIELD ("Log 1 format", LOG_1_FORMAT)
	OPTIONMENU ("Write log 2 to", 3)
		OPTION ("Log file only")
		OPTION ("Info window only")
		OPTION ("Log file and Info window")
	LABEL ("", "Log file 2:")
	TEXTFIELD ("Log file 2", LOG_2_FILE_NAME)
	LABEL ("", "Log 2 format:")
	TEXTFIELD ("Log 2 format", LOG_2_FORMAT)
	LABEL ("", "Log script 3:")
	TEXTFIELD ("Log script 3", LOG_3_FILE_NAME)
	LABEL ("", "Log script 4:")
	TEXTFIELD ("Log script 4", LOG_4_FILE_NAME)
	OK
SET_INTEGER ("Write log 1 to", preferences.log[0].toLogFile + 2 * preferences.log[0].toInfoWindow)
SET_STRING ("Log file 1", preferences.log[0].fileName)
SET_STRING ("Log 1 format", preferences.log[0].format)
SET_INTEGER ("Write log 2 to", preferences.log[1].toLogFile + 2 * preferences.log[1].toInfoWindow)
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].toLogFile = (GET_INTEGER ("Write log 1 to") & 1) != 0;
	preferences.log[0].toInfoWindow = (GET_INTEGER ("Write log 1 to") & 2) != 0;
	strcpy (preferences.log[0].fileName, GET_STRING ("Log file 1"));
	strcpy (preferences.log[0].format, GET_STRING ("Log 1 format"));
	preferences.log[1].toLogFile = (GET_INTEGER ("Write log 2 to") & 1) != 0;
	preferences.log[1].toInfoWindow = (GET_INTEGER ("Write log 2 to") & 2) != 0;
	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;
	if (! queriable (me)) return 0;
	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_relativePathToFile (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_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", "Time step settings...", 0, cb_timeStepSettings);
	Editor_addCommand (me, "View", "-- sound analysis --", 0, 0);
}

void FunctionEditor_SoundAnalysis_selectionQueries (I) {
	iam (FunctionEditor);
	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_addMenus (I) {
	iam (FunctionEditor);
	EditorMenu menu;

	menu = Editor_addMenu (me, "Spectrum", 0);
	my spectrogramToggle = EditorMenu_addCommand (menu, "Show spectrogram",
		motif_CHECKABLE | (preferences.spectrogram.show ? motif_CHECKED : 0), cb_showSpectrogram);
	EditorMenu_addCommand (menu, "Spectrogram settings...", 0, cb_spectrogramSettings);
	EditorMenu_addCommand (menu, "Advanced spectrogram settings...", 0, cb_advancedSpectrogramSettings);
	EditorMenu_addCommand (menu, "Extract visible spectrogram", 0, cb_extractVisibleSpectrogram);
	EditorMenu_addCommand (menu, "View spectral slice", 'S', cb_viewSpectralSlice);
	EditorMenu_addCommand (menu, "-- query spectrogram --", 0, NULL);
	EditorMenu_addCommand (menu, "Query:", motif_INSENSITIVE, cb_getFrequency /* dummy */);
	EditorMenu_addCommand (menu, "Get frequency", 0, cb_getFrequency);
	EditorMenu_addCommand (menu, "Get spectral power at cursor cross", motif_F9, cb_getSpectralPowerAtCursorCross);

	menu = Editor_addMenu (me, "Pitch", 0);
	my pitchToggle = EditorMenu_addCommand (menu, "Show pitch",
		motif_CHECKABLE | (preferences.pitch.show ? motif_CHECKED : 0), cb_showPitch);
	EditorMenu_addCommand (menu, "Pitch settings...", 0, cb_pitchSettings);
	EditorMenu_addCommand (menu, "Advanced pitch settings...", 0, cb_advancedPitchSettings);
	EditorMenu_addCommand (menu, "Extract visible pitch contour", 0, cb_extractVisiblePitchContour);
	EditorMenu_addCommand (menu, "-- query pitch --", 0, NULL);
	EditorMenu_addCommand (menu, "Query:", motif_INSENSITIVE, cb_getFrequency /* dummy */);
	EditorMenu_addCommand (menu, "Get pitch", motif_F10, cb_getPitch);
	EditorMenu_addCommand (menu, "Get minimum pitch", motif_F10 + motif_COMMAND, cb_getMinimumPitch);
	EditorMenu_addCommand (menu, "Get maximum pitch", motif_F10 + motif_SHIFT, cb_getMaximumPitch);
	EditorMenu_addCommand (menu, "-- select pitch --", 0, NULL);
	EditorMenu_addCommand (menu, "Select:", motif_INSENSITIVE, cb_moveCursorToMinimumPitch /* dummy */);
	EditorMenu_addCommand (menu, "Move cursor to minimum pitch", 'L', cb_moveCursorToMinimumPitch);
	EditorMenu_addCommand (menu, "Move cursor to maximum pitch", 'H', cb_moveCursorToMaximumPitch);

	menu = Editor_addMenu (me, "Intensity", 0);
	my intensityToggle = EditorMenu_addCommand (menu, "Show intensity",
		motif_CHECKABLE | (preferences.intensity.show ? motif_CHECKED : 0), cb_showIntensity);
	EditorMenu_addCommand (menu, "Intensity settings...", 0, cb_intensitySettings);
	EditorMenu_addCommand (menu, "Extract visible intensity contour", 0, cb_extractVisibleIntensityContour);
	EditorMenu_addCommand (menu, "-- query intensity --", 0, NULL);
	EditorMenu_addCommand (menu, "Query:", motif_INSENSITIVE, cb_getFrequency /* dummy */);
	EditorMenu_addCommand (menu, "Get intensity", motif_F11, cb_getIntensity);

	menu = Editor_addMenu (me, "Formant", 0);
	my formantToggle = EditorMenu_addCommand (menu, "Show formants",
		motif_CHECKABLE | (preferences.formant.show ? motif_CHECKED : 0), cb_showFormants);
	EditorMenu_addCommand (menu, "Formant settings...", 0, cb_formantSettings);
	EditorMenu_addCommand (menu, "Advanced formant settings...", 0, cb_advancedFormantSettings);
	EditorMenu_addCommand (menu, "Extract visible formant contour", 0, cb_extractVisibleFormantContour);
	EditorMenu_addCommand (menu, "-- query formants --", 0, NULL);
	EditorMenu_addCommand (menu, "Query:", motif_INSENSITIVE, cb_getFrequency /* dummy */);
	EditorMenu_addCommand (menu, "Formant report", 0, cb_formantReport);
	EditorMenu_addCommand (menu, "Get first formant", motif_F1, cb_getFirstFormant);
	EditorMenu_addCommand (menu, "Get first bandwidth", 0, cb_getFirstBandwidth);
	EditorMenu_addCommand (menu, "Get second formant", motif_F2, cb_getSecondFormant);
	EditorMenu_addCommand (menu, "Get second bandwidth", 0, cb_getSecondBandwidth);
	EditorMenu_addCommand (menu, "Get third formant", motif_F3, cb_getThirdFormant);
	EditorMenu_addCommand (menu, "Get third bandwidth", 0, cb_getThirdBandwidth);
	EditorMenu_addCommand (menu, "Get fourth formant", motif_F4, cb_getFourthFormant);
	EditorMenu_addCommand (menu, "Get fourth bandwidth", 0, cb_getFourthBandwidth);
	EditorMenu_addCommand (menu, "Get formant...", 0, cb_getFormant);
	EditorMenu_addCommand (menu, "Get bandwidth...", 0, cb_getBandwidth);

	menu = Editor_addMenu (me, "Pulses", 0);
	my pulseToggle = EditorMenu_addCommand (menu, "Show pulses",
		motif_CHECKABLE | (preferences.pulse.show ? motif_CHECKED : 0), cb_showPulses);
	EditorMenu_addCommand (menu, "Extract visible pulses", 0, cb_extractVisiblePulses);
	EditorMenu_addCommand (menu, "-- query pulses --", 0, NULL);
	EditorMenu_addCommand (menu, "Query:", motif_INSENSITIVE, cb_getFrequency /* dummy */);
	EditorMenu_addCommand (menu, "Voice report", 0, cb_voiceReport);
	EditorMenu_addCommand (menu, "Get jitter (local)", 0, cb_getJitter_local);
	EditorMenu_addCommand (menu, "Get jitter (local, absolute)", 0, cb_getJitter_local_absolute);
	EditorMenu_addCommand (menu, "Get jitter (rap)", 0, cb_getJitter_rap);
	EditorMenu_addCommand (menu, "Get jitter (ppq5)", 0, cb_getJitter_ppq5);
	EditorMenu_addCommand (menu, "Get jitter (ddp)", 0, cb_getJitter_ddp);
	EditorMenu_addCommand (menu, "Get shimmer (local)", 0, cb_getShimmer_local);
	EditorMenu_addCommand (menu, "Get shimmer (local_dB)", 0, cb_getShimmer_local_dB);
	EditorMenu_addCommand (menu, "Get shimmer (apq3)", 0, cb_getShimmer_apq3);
	EditorMenu_addCommand (menu, "Get shimmer (apq5)", 0, cb_getShimmer_apq5);
	EditorMenu_addCommand (menu, "Get shimmer (apq11)", 0, cb_getShimmer_apq11);
	EditorMenu_addCommand (menu, "Get shimmer (dda)", 0, cb_getShimmer_dda);
}

void FunctionEditor_SoundAnalysis_init (I) {
	iam (FunctionEditor);
	my longestAnalysis = preferences.longestAnalysis;
	if (preferences.timeStepStrategy < 1 || preferences.timeStepStrategy > 3)
		preferences.timeStepStrategy = 1;
	my timeStepStrategy = preferences.timeStepStrategy;
	my fixedTimeStep = preferences.fixedTimeStep;
	my numberOfTimeStepsPerView = preferences.numberOfTimeStepsPerView;
	my spectrogram = preferences.spectrogram;
	my pitch = preferences.pitch;
	my intensity = preferences.intensity;
	my formant = preferences.formant;
	my pulse = preferences.pulse;
}

/* End of file FunctionEditor_SoundAnalysis.c */
