/* 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
 */

#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 "PointProcess_and_Sound.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;
	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 = {
	5.0,
	{
		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,
		1, FALSE,
		15, 0.03, 0.45,
		0.01, 0.35, 0.14
	}, {
		NULL, FALSE,
		50.0, 100.0
	}, {
		NULL, TRUE,
		1.0, 30.0,
		10, 5500.0, 0.025, 50.0,
		1
	}, {
		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.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_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.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.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) {
		my pitch.data = Sound_to_Pitch_any (sound,
			my endWindow - my startWindow > my longestAnalysis ? (my endWindow - my startWindow) / 100 : 0.0,
			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) {
			my formant.data = Sound_to_Formant_any (sound, (my endWindow - my startWindow) / 100,
				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

/***** 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")
	NATURAL ("Number of time steps", "1000")
	NATURAL ("Number of 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")
	REAL ("Maximum (dB/Hz)", "100.0")
	BOOLEAN ("Autoscaling", 1)
	POSITIVE ("Dynamic range (dB)", "50.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_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 ("Maximum", my spectrogram.maximum)
SET_INTEGER ("Autoscaling", my spectrogram.autoscaling)
SET_REAL ("Dynamic range", my spectrogram.dynamicRange)
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.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.maximum = my spectrogram.maximum = GET_REAL ("Maximum");
	preferences.spectrogram.autoscaling = my spectrogram.autoscaling = GET_INTEGER ("Autoscaling");
	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

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.");
	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 ("", "")
	LABEL ("", "UNIMPORTANT 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 Pitch range", my pitch.floor)
SET_REAL ("right Pitch range", my pitch.ceiling)
SET_INTEGER ("Units", my pitch.units)
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.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");
	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.");
	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.");
	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 ("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_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.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

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.");
	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.");
	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;
	double pmin = 0.8 / my pitch.ceiling, pmax = 1.25 / my pitch.floor;
	double shimmerLocal, shimmerLocal_dB, apq3, apq5, apq11, dda;
	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).");
	MelderInfo_open ();
	MelderInfo_writeLine3 ("Analysis start: ", Melder_double (tmin), " seconds");
	MelderInfo_writeLine3 ("Analysis end: ", Melder_double (tmax), " seconds");
	MelderInfo_writeLine3 ("Analysis duration: ", Melder_double (tmax - tmin), " seconds");
	{
		long imin, imax, n = Sampled_getWindowSamples (my pitch.data, tmin, tmax, & imin, & imax), i, nunvoiced = n;
		for (i = imin; i <= imax; i ++) {
			Pitch_Frame frame = & my pitch.data -> frame [i];
			if (frame -> intensity >= my pitch.silenceThreshold) {
				long icand;
				for (icand = 1; icand <= frame -> nCandidates; icand ++) {
					Pitch_Candidate cand = & frame -> candidate [icand];
					if (cand -> frequency > 0.0 && cand -> frequency < my pitch.ceiling && cand -> strength >= my pitch.voicingThreshold) {
						nunvoiced --;
						break;   /* next frame */
					}
				}
			}
		}
		MelderInfo_write2 ("Fraction of locally unvoiced frames: ", Melder_percent (n <= 0 ? NUMundefined : (double) nunvoiced / n, 3));
		MelderInfo_writeLine5 (" (", Melder_integer (nunvoiced), "/", Melder_integer (n), ")");
	}
	{
		long imin, imax, n = PointProcess_getWindowPoints (my pulse.data, tmin, tmax, & imin, & imax), i;
		long numberOfVoiceBreaks = 0;
		double durationOfVoiceBreaks = 0.0;
		if (n > 1) {
			int previousPeriodVoiced = TRUE;
			for (i = imin + 1; i < imax; i ++) {
				double period = my pulse.data -> t [i] - my pulse.data -> t [i - 1];
				if (period > pmax) {
					durationOfVoiceBreaks += period;
					if (previousPeriodVoiced) {
						numberOfVoiceBreaks ++;
						previousPeriodVoiced = FALSE;
					}
				} else {
					previousPeriodVoiced = TRUE;
				}
			}
		}
		MelderInfo_writeLine2 ("Number of voice breaks: ", Melder_integer (numberOfVoiceBreaks));
		MelderInfo_write2 ("Degree of voice breaks: ", Melder_percent (durationOfVoiceBreaks / (tmax - tmin), 3));
		MelderInfo_writeLine5 (" (", Melder_fixed (durationOfVoiceBreaks, 6), " s / ", Melder_fixed (tmax - tmin, 6), " s)");
	}
	MelderInfo_writeLine2 ("Jitter (local): ", Melder_percent (PointProcess_getJitter_local (my pulse.data, tmin, tmax, pmin, pmax), 3));
	MelderInfo_writeLine3 ("Jitter (local, absolute): ",
		Melder_fixedExponent (PointProcess_getJitter_local_absolute (my pulse.data, tmin, tmax, pmin, pmax), -6, 3), " seconds");
	MelderInfo_writeLine2 ("Jitter (rap): ", Melder_percent (PointProcess_getJitter_rap (my pulse.data, tmin, tmax, pmin, pmax), 3));
	MelderInfo_writeLine2 ("Jitter (ppq5): ", Melder_percent (PointProcess_getJitter_ppq5 (my pulse.data, tmin, tmax, pmin, pmax), 3));
	MelderInfo_writeLine2 ("Jitter (ddp): ", Melder_percent (PointProcess_getJitter_ddp (my pulse.data, tmin, tmax, pmin, pmax), 3));
	PointProcess_Sound_getShimmer_multi (my pulse.data, sound, tmin, tmax, pmin, pmax,
		& shimmerLocal, & shimmerLocal_dB, & apq3, & apq5, & apq11, & dda);
	MelderInfo_writeLine2 ("Shimmer (local): ", Melder_percent (shimmerLocal, 3));
	MelderInfo_writeLine3 ("Shimmer (local, dB): ", Melder_fixed (shimmerLocal_dB, 3), " dB");
	MelderInfo_writeLine2 ("Shimmer (apq3): ", Melder_percent (apq3, 3));
	MelderInfo_writeLine2 ("Shimmer (apq5): ", Melder_percent (apq5, 3));
	MelderInfo_writeLine2 ("Shimmer (apq11): ", Melder_percent (apq11, 3));
	MelderInfo_writeLine2 ("Shimmer (dda): ", Melder_percent (dda, 3));
	MelderInfo_close ();
	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);
	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 y1 = Pitch_convertFrequency (my pitch.floor, my pitch.units);
		double y2 = Pitch_convertFrequency (my pitch.ceiling, my pitch.units);
		int speckle = my endWindow - my startWindow > my longestAnalysis;
		Graphics_setColour (my graphics, Graphics_CYAN);
		Graphics_setLineWidth (my graphics, 3.0);
		Pitch_drawInside (my pitch.data, my graphics, my startWindow, my endWindow, y1, y2, 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, y1, y2, 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 = Pitch_convertFrequency (my pitch.floor, my pitch.units);
		y2 = Pitch_convertFrequency (my pitch.ceiling, my pitch.units);
		Graphics_setWindow (my graphics, my startWindow, my endWindow, y1, y2);
		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 - 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.units == Pitch_yscale_LOGARITHMIC ? my pitch.floor : y1, 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.units == Pitch_yscale_LOGARITHMIC ? my pitch.ceiling : y2, 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 * (y1 + y2),
				"(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", "-- 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, "Extract visible spectrogram", 0, cb_extractVisibleSpectrogram);
	EditorMenu_addCommand (menu, "View spectral slice", 0, 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, "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, "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;
	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 */
