1 /* TimeSoundEditor.cpp
2  *
3  * Copyright (C) 1992-2021 Paul Boersma
4  *
5  * This code is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or (at
8  * your option) any later version.
9  *
10  * This code is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13  * See the GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this work. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "NUM2.h"
20 #include "TimeSoundEditor.h"
21 #include "EditorM.h"
22 #include "../kar/UnicodeData.h"
23 
24 #include "enums_getText.h"
25 #include "TimeSoundEditor_enums.h"
26 #include "enums_getValue.h"
27 #include "TimeSoundEditor_enums.h"
28 
29 Thing_implement (TimeSoundEditor, FunctionEditor, 0);
30 
31 #include "prefs_define.h"
32 #include "TimeSoundEditor_prefs.h"
33 #include "prefs_install.h"
34 #include "TimeSoundEditor_prefs.h"
35 #include "prefs_copyToInstance.h"
36 #include "TimeSoundEditor_prefs.h"
37 
38 /********** Thing methods **********/
39 
v_destroy()40 void structTimeSoundEditor :: v_destroy () noexcept {
41 	if (our d_ownSound)
42 		forget (our d_sound.data);
43 	TimeSoundEditor_Parent :: v_destroy ();
44 }
45 
v_info()46 void structTimeSoundEditor :: v_info () {
47 	TimeSoundEditor_Parent :: v_info ();
48 	/* Sound flags: */
49 	MelderInfo_writeLine (U"Sound scaling strategy: ", kTimeSoundEditor_scalingStrategy_getText (our p_sound_scalingStrategy));
50 }
51 
52 enum {
53 	TimeSoundEditor_PART_CURSOR = 1,
54 	TimeSoundEditor_PART_SELECTION = 2
55 };
56 
makeQueriable(TimeSoundEditor me,bool allowCursor,double * tmin,double * tmax)57 static int makeQueriable (TimeSoundEditor me, bool allowCursor, double *tmin, double *tmax) {
58 	if (my startSelection == my endSelection) {
59 		if (allowCursor) {
60 			*tmin = *tmax = my startSelection;
61 			return TimeSoundEditor_PART_CURSOR;
62 		} else {
63 			Melder_throw (U"Make a selection first.");
64 		}
65 	} else if (my startSelection < my startWindow || my endSelection > my endWindow) {
66 		Melder_throw (U"Command ambiguous: a part of the selection (", my startSelection, U", ", my endSelection, U") "
67 			U"is outside of the window (", my startWindow, U", ", my endWindow, U"). "
68 			U"Either zoom or re-select.");
69 	}
70 	*tmin = my startSelection;
71 	*tmax = my endSelection;
72 	return TimeSoundEditor_PART_SELECTION;
73 }
74 
75 /***** FILE MENU *****/
76 
menu_cb_DrawVisibleSound(TimeSoundEditor me,EDITOR_ARGS_FORM)77 static void menu_cb_DrawVisibleSound (TimeSoundEditor me, EDITOR_ARGS_FORM) {
78 	EDITOR_FORM (U"Draw visible sound", nullptr)
79 		my v_form_pictureWindow (cmd);
80 		LABEL (U"Sound:")
81 		BOOLEAN (preserveTimes, U"Preserve times", my default_picture_preserveTimes ());
82 		REAL (bottom, U"left Vertical range", my default_picture_bottom ())
83 		REAL (top, U"right Vertical range", my default_picture_top ())
84 		my v_form_pictureMargins (cmd);
85 		my v_form_pictureSelection (cmd);
86 		BOOLEAN (garnish, U"Garnish", my default_picture_garnish ());
87 	EDITOR_OK
88 		my v_ok_pictureWindow (cmd);
89 		SET_BOOLEAN (preserveTimes, my pref_picture_preserveTimes ())
90 		SET_REAL (bottom,  my pref_picture_bottom ())
91 		SET_REAL (top,     my pref_picture_top ())
92 		my v_ok_pictureMargins (cmd);
93 		my v_ok_pictureSelection (cmd);
94 		SET_BOOLEAN (garnish, my pref_picture_garnish ())
95 	EDITOR_DO
96 		my v_do_pictureWindow (cmd);
97 		my pref_picture_preserveTimes () = preserveTimes;
98 		my pref_picture_bottom () = bottom;
99 		my pref_picture_top () = top;
100 		my v_do_pictureMargins (cmd);
101 		my v_do_pictureSelection (cmd);
102 		my pref_picture_garnish () = garnish;
103 		if (! my d_longSound.data && ! my d_sound.data)
104 			Melder_throw (U"There is no sound to draw.");
105 		autoSound publish = my d_longSound.data ?
106 			LongSound_extractPart (my d_longSound.data, my startWindow, my endWindow, my pref_picture_preserveTimes ()) :
107 			Sound_extractPart (my d_sound.data, my startWindow, my endWindow, kSound_windowShape::RECTANGULAR, 1.0, my pref_picture_preserveTimes ());
108 		Editor_openPraatPicture (me);
109 		Sound_draw (publish.get(), my pictureGraphics, 0.0, 0.0, my pref_picture_bottom (), my pref_picture_top (),
110 			my pref_picture_garnish (), U"Curve");
111 		FunctionEditor_garnish (me);
112 		Editor_closePraatPicture (me);
113 	EDITOR_END
114 }
115 
menu_cb_DrawSelectedSound(TimeSoundEditor me,EDITOR_ARGS_FORM)116 static void menu_cb_DrawSelectedSound (TimeSoundEditor me, EDITOR_ARGS_FORM) {
117 	EDITOR_FORM (U"Draw selected sound", nullptr)
118 		my v_form_pictureWindow (cmd);
119 		LABEL (U"Sound:")
120 		BOOLEAN (preserveTimes, U"Preserve times",       my default_picture_preserveTimes ());
121 		REAL    (bottom,        U"left Vertical range",  my default_picture_bottom ());
122 		REAL    (top,           U"right Vertical range", my default_picture_top ());
123 		my v_form_pictureMargins (cmd);
124 		BOOLEAN (garnish, U"Garnish", my default_picture_garnish ());
125 	EDITOR_OK
126 		my v_ok_pictureWindow (cmd);
127 		SET_BOOLEAN (preserveTimes, my pref_picture_preserveTimes ());
128 		SET_REAL (bottom, my pref_picture_bottom ());
129 		SET_REAL (top,    my pref_picture_top ());
130 		my v_ok_pictureMargins (cmd);
131 		SET_BOOLEAN (garnish, my pref_picture_garnish ());
132 	EDITOR_DO
133 		my v_do_pictureWindow (cmd);
134 		my pref_picture_preserveTimes () = preserveTimes;
135 		my pref_picture_bottom () = bottom;
136 		my pref_picture_top () = top;
137 		my v_do_pictureMargins (cmd);
138 		my pref_picture_garnish () = garnish;
139 		if (! my d_longSound.data && ! my d_sound.data)
140 			Melder_throw (U"There is no sound to draw.");
141 		autoSound publish = my d_longSound.data ?
142 			LongSound_extractPart (my d_longSound.data, my startSelection, my endSelection, my pref_picture_preserveTimes ()) :
143 			Sound_extractPart (my d_sound.data, my startSelection, my endSelection,
144 				kSound_windowShape::RECTANGULAR, 1.0, my pref_picture_preserveTimes ());
145 		Editor_openPraatPicture (me);
146 		Sound_draw (publish.get(), my pictureGraphics, 0.0, 0.0, my pref_picture_bottom (), my pref_picture_top (),
147 			my pref_picture_garnish (), U"Curve");
148 		Editor_closePraatPicture (me);
149 	EDITOR_END
150 }
151 
do_ExtractSelectedSound(TimeSoundEditor me,bool preserveTimes)152 static autoSound do_ExtractSelectedSound (TimeSoundEditor me, bool preserveTimes) {
153 	if (my endSelection <= my startSelection)
154 		Melder_throw (U"No selection.");
155 	if (my d_longSound.data)
156 		return LongSound_extractPart (my d_longSound.data, my startSelection, my endSelection, preserveTimes);
157 	else if (my d_sound.data)
158 		return Sound_extractPart (my d_sound.data, my startSelection, my endSelection, kSound_windowShape::RECTANGULAR, 1.0, preserveTimes);
159 	Melder_fatal (U"No Sound or LongSound available.");
160 	return autoSound();   // never reached
161 }
162 
CONVERT_DATA_TO_ONE__ExtractSelectedSound_timeFromZero(TimeSoundEditor me,EDITOR_ARGS_DIRECT_WITH_OUTPUT)163 static void CONVERT_DATA_TO_ONE__ExtractSelectedSound_timeFromZero (TimeSoundEditor me, EDITOR_ARGS_DIRECT_WITH_OUTPUT) {
164 	CONVERT_DATA_TO_ONE
165 		autoSound result = do_ExtractSelectedSound (me, false);
166 	CONVERT_DATA_TO_ONE_END (U"untitled")
167 }
168 
CONVERT_DATA_TO_ONE__ExtractSelectedSound_preserveTimes(TimeSoundEditor me,EDITOR_ARGS_DIRECT_WITH_OUTPUT)169 static void CONVERT_DATA_TO_ONE__ExtractSelectedSound_preserveTimes (TimeSoundEditor me, EDITOR_ARGS_DIRECT_WITH_OUTPUT) {
170 	CONVERT_DATA_TO_ONE
171 		autoSound result = do_ExtractSelectedSound (me, true);
172 	CONVERT_DATA_TO_ONE_END (U"untitled")
173 }
174 
CONVERT_DATA_TO_ONE__ExtractSelectedSound_windowed(TimeSoundEditor me,EDITOR_ARGS_FORM)175 static void CONVERT_DATA_TO_ONE__ExtractSelectedSound_windowed (TimeSoundEditor me, EDITOR_ARGS_FORM) {
176 	EDITOR_FORM (U"Extract selected sound (windowed)", nullptr)
177 		WORD (name, U"Name", U"slice")
178 		OPTIONMENU_ENUM (kSound_windowShape, windowShape, U"Window shape", my default_extract_windowShape ())
179 		POSITIVE (relativeWidth, U"Relative width", my default_extract_relativeWidth ())
180 		BOOLEAN (preserveTimes, U"Preserve times", my default_extract_preserveTimes ())
181 	EDITOR_OK
182 		SET_ENUM (windowShape, kSound_windowShape, my pref_extract_windowShape ())
183 		SET_REAL (relativeWidth, my pref_extract_relativeWidth ())
184 		SET_BOOLEAN (preserveTimes, my pref_extract_preserveTimes ())
185 	EDITOR_DO
186 		Sound sound = my d_sound.data;
187 		Melder_assert (sound);
188 		CONVERT_DATA_TO_ONE
189 			my pref_extract_windowShape () = windowShape;
190 			my pref_extract_relativeWidth () = relativeWidth;
191 			my pref_extract_preserveTimes () = preserveTimes;
192 			autoSound result = Sound_extractPart (sound, my startSelection, my endSelection, my pref_extract_windowShape (),
193 					my pref_extract_relativeWidth (), my pref_extract_preserveTimes ());
194 		CONVERT_DATA_TO_ONE_END (name)
195 	EDITOR_END
196 }
197 
CONVERT_DATA_TO_ONE__ExtractSelectedSoundForOverlap(TimeSoundEditor me,EDITOR_ARGS_FORM)198 static void CONVERT_DATA_TO_ONE__ExtractSelectedSoundForOverlap (TimeSoundEditor me, EDITOR_ARGS_FORM) {
199 	EDITOR_FORM (U"Extract selected sound for overlap)", nullptr)
200 		WORD (name, U"Name", U"slice")
201 		POSITIVE (overlap, U"Overlap (s)", my default_extract_overlap ())
202 	EDITOR_OK
203 		SET_REAL (overlap, my pref_extract_overlap ())
204 	EDITOR_DO
205 		Sound sound = my d_sound.data;
206 		Melder_assert (sound);
207 		CONVERT_DATA_TO_ONE
208 			my pref_extract_overlap () = overlap;
209 			autoSound result = Sound_extractPartForOverlap (sound, my startSelection, my endSelection,
210 				my pref_extract_overlap ());
211 		CONVERT_DATA_TO_ONE_END (name)
212 	EDITOR_END
213 }
214 
do_write(TimeSoundEditor me,MelderFile file,int format,int numberOfBitsPerSamplePoint)215 static void do_write (TimeSoundEditor me, MelderFile file, int format, int numberOfBitsPerSamplePoint) {
216 	if (my startSelection >= my endSelection)
217 		Melder_throw (U"No samples selected.");
218 	if (my d_longSound.data) {
219 		LongSound_savePartAsAudioFile (my d_longSound.data, format, my startSelection, my endSelection, file, numberOfBitsPerSamplePoint);
220 	} else if (my d_sound.data) {
221 		Sound sound = my d_sound.data;
222 		double margin = 0.0;
223 		integer nmargin = Melder_ifloor (margin / sound -> dx);
224 		integer first, last, numberOfSamples = Sampled_getWindowSamples (sound,
225 			my startSelection, my endSelection, & first, & last) + nmargin * 2;
226 		first -= nmargin;
227 		last += nmargin;
228 		if (numberOfSamples) {
229 			autoSound save = Sound_create (sound -> ny, 0.0, numberOfSamples * sound -> dx, numberOfSamples, sound -> dx, 0.5 * sound -> dx);
230 			integer offset = first - 1;
231 			if (first < 1)
232 				first = 1;
233 			if (last > sound -> nx)
234 				last = sound -> nx;
235 			for (integer channel = 1; channel <= sound -> ny; channel ++) {
236 				for (integer i = first; i <= last; i ++)
237 					save -> z [channel] [i - offset] = sound -> z [channel] [i];
238 			}
239 			Sound_saveAsAudioFile (save.get(), file, format, numberOfBitsPerSamplePoint);
240 		}
241 	}
242 }
243 
menu_cb_WriteWav(TimeSoundEditor me,EDITOR_ARGS_FORM)244 static void menu_cb_WriteWav (TimeSoundEditor me, EDITOR_ARGS_FORM) {
245 	EDITOR_FORM_SAVE (U"Save selected sound as WAV file", nullptr)
246 		Melder_sprint (defaultName,300, my d_longSound.data ? my d_longSound.data -> name.get() : my d_sound.data -> name.get(), U".wav");
247 	EDITOR_DO_SAVE
248 		do_write (me, file, Melder_WAV, 16);
249 	EDITOR_END
250 }
251 
menu_cb_SaveAs24BitWav(TimeSoundEditor me,EDITOR_ARGS_FORM)252 static void menu_cb_SaveAs24BitWav (TimeSoundEditor me, EDITOR_ARGS_FORM) {
253 	EDITOR_FORM_SAVE (U"Save selected sound as 24-bit WAV file", nullptr)
254 		Melder_assert (! my d_longSound.data && my d_sound.data);
255 		Melder_sprint (defaultName,300, my d_sound.data -> name.get(), U".wav");
256 	EDITOR_DO_SAVE
257 		do_write (me, file, Melder_WAV, 24);
258 	EDITOR_END
259 }
260 
menu_cb_SaveAs32BitWav(TimeSoundEditor me,EDITOR_ARGS_FORM)261 static void menu_cb_SaveAs32BitWav (TimeSoundEditor me, EDITOR_ARGS_FORM) {
262 	EDITOR_FORM_SAVE (U"Save selected sound as 32-bit WAV file", nullptr)
263 		Melder_assert (! my d_longSound.data && my d_sound.data);
264 		Melder_sprint (defaultName,300, my d_sound.data -> name.get(), U".wav");
265 	EDITOR_DO_SAVE
266 		do_write (me, file, Melder_WAV, 32);
267 	EDITOR_END
268 }
269 
menu_cb_WriteAiff(TimeSoundEditor me,EDITOR_ARGS_FORM)270 static void menu_cb_WriteAiff (TimeSoundEditor me, EDITOR_ARGS_FORM) {
271 	EDITOR_FORM_SAVE (U"Save selected sound as AIFF file", nullptr)
272 		Melder_sprint (defaultName,300, my d_longSound.data ? my d_longSound.data -> name.get() : my d_sound.data -> name.get(), U".aiff");
273 	EDITOR_DO_SAVE
274 		do_write (me, file, Melder_AIFF, 16);
275 	EDITOR_END
276 }
277 
menu_cb_WriteAifc(TimeSoundEditor me,EDITOR_ARGS_FORM)278 static void menu_cb_WriteAifc (TimeSoundEditor me, EDITOR_ARGS_FORM) {
279 	EDITOR_FORM_SAVE (U"Save selected sound as AIFC file", nullptr)
280 		Melder_sprint (defaultName,300, my d_longSound.data ? my d_longSound.data -> name.get() : my d_sound.data -> name.get(), U".aifc");
281 	EDITOR_DO_SAVE
282 		do_write (me, file, Melder_AIFC, 16);
283 	EDITOR_END
284 }
285 
menu_cb_WriteNextSun(TimeSoundEditor me,EDITOR_ARGS_FORM)286 static void menu_cb_WriteNextSun (TimeSoundEditor me, EDITOR_ARGS_FORM) {
287 	EDITOR_FORM_SAVE (U"Save selected sound as NeXT/Sun file", nullptr)
288 		Melder_sprint (defaultName,300, my d_longSound.data ? my d_longSound.data -> name.get() : my d_sound.data -> name.get(), U".au");
289 	EDITOR_DO_SAVE
290 		do_write (me, file, Melder_NEXT_SUN, 16);
291 	EDITOR_END
292 }
293 
menu_cb_WriteNist(TimeSoundEditor me,EDITOR_ARGS_FORM)294 static void menu_cb_WriteNist (TimeSoundEditor me, EDITOR_ARGS_FORM) {
295 	EDITOR_FORM_SAVE (U"Save selected sound as NIST file", nullptr)
296 		Melder_sprint (defaultName,300, my d_longSound.data ? my d_longSound.data -> name.get() : my d_sound.data -> name.get(), U".nist");
297 	EDITOR_DO_SAVE
298 		do_write (me, file, Melder_NIST, 16);
299 	EDITOR_END
300 }
301 
menu_cb_WriteFlac(TimeSoundEditor me,EDITOR_ARGS_FORM)302 static void menu_cb_WriteFlac (TimeSoundEditor me, EDITOR_ARGS_FORM) {
303 	EDITOR_FORM_SAVE (U"Save selected sound as FLAC file", nullptr)
304 		Melder_sprint (defaultName,300, my d_longSound.data ? my d_longSound.data -> name.get() : my d_sound.data -> name.get(), U".flac");
305 	EDITOR_DO_SAVE
306 		do_write (me, file, Melder_FLAC, 16);
307 	EDITOR_END
308 }
309 
v_createMenuItems_file_draw(EditorMenu menu)310 void structTimeSoundEditor :: v_createMenuItems_file_draw (EditorMenu menu) {
311 	EditorMenu_addCommand (menu, U"Draw to picture window:", GuiMenu_INSENSITIVE, menu_cb_DrawVisibleSound /* dummy */);
312 	if (our d_sound.data || our d_longSound.data) {
313 		EditorMenu_addCommand (menu, U"Draw visible sound...", 0, menu_cb_DrawVisibleSound);
314 		our drawButton = EditorMenu_addCommand (menu, U"Draw selected sound...", 0, menu_cb_DrawSelectedSound);
315 	}
316 }
317 
v_createMenuItems_file_extract(EditorMenu menu)318 void structTimeSoundEditor :: v_createMenuItems_file_extract (EditorMenu menu) {
319 	EditorMenu_addCommand (menu, U"Extract to objects window:", GuiMenu_INSENSITIVE,
320 			CONVERT_DATA_TO_ONE__ExtractSelectedSound_preserveTimes /* dummy */);
321 	if (our d_sound.data || our d_longSound.data) {
322 		our publishPreserveButton = EditorMenu_addCommand (menu, U"Extract selected sound (preserve times)", 0,
323 				CONVERT_DATA_TO_ONE__ExtractSelectedSound_preserveTimes);
324 			EditorMenu_addCommand (menu, U"Extract sound selection (preserve times)", Editor_HIDDEN,
325 					CONVERT_DATA_TO_ONE__ExtractSelectedSound_preserveTimes);
326 			EditorMenu_addCommand (menu, U"Extract selection (preserve times)", Editor_HIDDEN,
327 					CONVERT_DATA_TO_ONE__ExtractSelectedSound_preserveTimes);
328 		our publishButton = EditorMenu_addCommand (menu, U"Extract selected sound (time from 0)", 0,
329 				CONVERT_DATA_TO_ONE__ExtractSelectedSound_timeFromZero);
330 			EditorMenu_addCommand (menu, U"Extract sound selection (time from 0)", Editor_HIDDEN,
331 					CONVERT_DATA_TO_ONE__ExtractSelectedSound_timeFromZero);
332 			EditorMenu_addCommand (menu, U"Extract selection (time from 0)", Editor_HIDDEN,
333 					CONVERT_DATA_TO_ONE__ExtractSelectedSound_timeFromZero);
334 			EditorMenu_addCommand (menu, U"Extract selection", Editor_HIDDEN,
335 					CONVERT_DATA_TO_ONE__ExtractSelectedSound_timeFromZero);
336 		if (our d_sound.data) {
337 			our publishWindowButton = EditorMenu_addCommand (menu, U"Extract selected sound (windowed)...", 0,
338 					CONVERT_DATA_TO_ONE__ExtractSelectedSound_windowed);
339 				EditorMenu_addCommand (menu, U"Extract windowed sound selection...", Editor_HIDDEN,
340 						CONVERT_DATA_TO_ONE__ExtractSelectedSound_windowed);
341 				EditorMenu_addCommand (menu, U"Extract windowed selection...", Editor_HIDDEN,
342 						CONVERT_DATA_TO_ONE__ExtractSelectedSound_windowed);
343 			our publishOverlapButton = EditorMenu_addCommand (menu, U"Extract selected sound for overlap...", 0,
344 					CONVERT_DATA_TO_ONE__ExtractSelectedSoundForOverlap);
345 		}
346 	}
347 }
348 
v_createMenuItems_file_write(EditorMenu menu)349 void structTimeSoundEditor :: v_createMenuItems_file_write (EditorMenu menu) {
350 	EditorMenu_addCommand (menu, U"Save to disk:", GuiMenu_INSENSITIVE, menu_cb_WriteWav /* dummy */);
351 	if (our d_sound.data || our d_longSound.data) {
352 		our writeWavButton = EditorMenu_addCommand (menu, U"Save selected sound as WAV file...", 0, menu_cb_WriteWav);
353 			EditorMenu_addCommand (menu, U"Write selected sound to WAV file...", Editor_HIDDEN, menu_cb_WriteWav);
354 			EditorMenu_addCommand (menu, U"Write sound selection to WAV file...", Editor_HIDDEN, menu_cb_WriteWav);
355 			EditorMenu_addCommand (menu, U"Write selection to WAV file...", Editor_HIDDEN, menu_cb_WriteWav);
356 		if (our d_sound.data) {
357 			our saveAs24BitWavButton = EditorMenu_addCommand (menu, U"Save selected sound as 24-bit WAV file...", 0, menu_cb_SaveAs24BitWav);
358 			our saveAs32BitWavButton = EditorMenu_addCommand (menu, U"Save selected sound as 32-bit WAV file...", 0, menu_cb_SaveAs32BitWav);
359 		}
360 		our writeAiffButton = EditorMenu_addCommand (menu, U"Save selected sound as AIFF file...", 0, menu_cb_WriteAiff);
361 			EditorMenu_addCommand (menu, U"Write selected sound to AIFF file...", Editor_HIDDEN, menu_cb_WriteAiff);
362 			EditorMenu_addCommand (menu, U"Write sound selection to AIFF file...", Editor_HIDDEN, menu_cb_WriteAiff);
363 			EditorMenu_addCommand (menu, U"Write selection to AIFF file...", Editor_HIDDEN, menu_cb_WriteAiff);
364 		our writeAifcButton = EditorMenu_addCommand (menu, U"Save selected sound as AIFC file...", 0, menu_cb_WriteAifc);
365 			EditorMenu_addCommand (menu, U"Write selected sound to AIFC file...", Editor_HIDDEN, menu_cb_WriteAifc);
366 			EditorMenu_addCommand (menu, U"Write sound selection to AIFC file...", Editor_HIDDEN, menu_cb_WriteAifc);
367 			EditorMenu_addCommand (menu, U"Write selection to AIFC file...", Editor_HIDDEN, menu_cb_WriteAifc);
368 		our writeNextSunButton = EditorMenu_addCommand (menu, U"Save selected sound as NeXT/Sun file...", 0, menu_cb_WriteNextSun);
369 			EditorMenu_addCommand (menu, U"Write selected sound to NeXT/Sun file...", Editor_HIDDEN, menu_cb_WriteNextSun);
370 			EditorMenu_addCommand (menu, U"Write sound selection to NeXT/Sun file...", Editor_HIDDEN, menu_cb_WriteNextSun);
371 			EditorMenu_addCommand (menu, U"Write selection to NeXT/Sun file...", Editor_HIDDEN, menu_cb_WriteNextSun);
372 		our writeNistButton = EditorMenu_addCommand (menu, U"Save selected sound as NIST file...", 0, menu_cb_WriteNist);
373 			EditorMenu_addCommand (menu, U"Write selected sound to NIST file...", Editor_HIDDEN, menu_cb_WriteNist);
374 			EditorMenu_addCommand (menu, U"Write sound selection to NIST file...", Editor_HIDDEN, menu_cb_WriteNist);
375 			EditorMenu_addCommand (menu, U"Write selection to NIST file...", Editor_HIDDEN, menu_cb_WriteNist);
376 		our writeFlacButton = EditorMenu_addCommand (menu, U"Save selected sound as FLAC file...", 0, menu_cb_WriteFlac);
377 			EditorMenu_addCommand (menu, U"Write selected sound to FLAC file...", Editor_HIDDEN, menu_cb_WriteFlac);
378 			EditorMenu_addCommand (menu, U"Write sound selection to FLAC file...", Editor_HIDDEN, menu_cb_WriteFlac);
379 	}
380 }
381 
v_createMenuItems_file(EditorMenu menu)382 void structTimeSoundEditor :: v_createMenuItems_file (EditorMenu menu) {
383 	our TimeSoundEditor_Parent :: v_createMenuItems_file (menu);
384 	our v_createMenuItems_file_draw (menu);
385 	EditorMenu_addCommand (menu, U"-- after file draw --", 0, nullptr);
386 	our v_createMenuItems_file_extract (menu);
387 	EditorMenu_addCommand (menu, U"-- after file extract --", 0, nullptr);
388 	our v_createMenuItems_file_write (menu);
389 	EditorMenu_addCommand (menu, U"-- after file write --", 0, nullptr);
390 }
391 
392 /********** QUERY MENU **********/
393 
INFO_DATA__SoundInfo(TimeSoundEditor me,EDITOR_ARGS_DIRECT_WITH_OUTPUT)394 static void INFO_DATA__SoundInfo (TimeSoundEditor me, EDITOR_ARGS_DIRECT_WITH_OUTPUT) {
395 	INFO_DATA
396 		Thing_info (my d_sound.data);
397 	INFO_DATA_END
398 }
399 
INFO_DATA__LongSoundInfo(TimeSoundEditor me,EDITOR_ARGS_DIRECT_WITH_OUTPUT)400 static void INFO_DATA__LongSoundInfo (TimeSoundEditor me, EDITOR_ARGS_DIRECT_WITH_OUTPUT) {
401 	INFO_DATA
402 		Thing_info (my d_longSound.data);
403 	INFO_DATA_END
404 }
405 
INFO_DATA__getAmplitudes(TimeSoundEditor me,EDITOR_ARGS_DIRECT_WITH_OUTPUT)406 static void INFO_DATA__getAmplitudes (TimeSoundEditor me, EDITOR_ARGS_DIRECT_WITH_OUTPUT) {
407 	INFO_DATA
408 		double tmin, tmax;
409 		const int part = makeQueriable (me, true, & tmin, & tmax);
410 		if (! my d_sound.data)
411 			Melder_throw (U"No Sound object is visible (a LongSound cannot be queried).");
412 		MelderInfo_open ();
413 		if (part == TimeSoundEditor_PART_CURSOR)
414 			for (integer ichan = 1; ichan <= my d_sound.data -> ny; ichan ++)
415 				MelderInfo_writeLine (Vector_getValueAtX (my d_sound.data, 0.5 * (my startSelection + my endSelection), ichan, kVector_valueInterpolation :: SINC70),
416 						U" (interpolated amplitude at CURSOR in channel ", ichan, U")");
417 		else
418 			for (integer ichan = 1; ichan <= my d_sound.data -> ny; ichan ++)
419 				MelderInfo_writeLine (Sampled_getMean (my d_sound.data, my startSelection, my endSelection, ichan, 0, true),
420 						U" (mean amplitude in SELECTION in channel ", ichan, U")");
421 		MelderInfo_close ();
422 	INFO_DATA_END
423 }
424 
v_createMenuItems_query_info(EditorMenu menu)425 void structTimeSoundEditor :: v_createMenuItems_query_info (EditorMenu menu) {
426 	TimeSoundEditor_Parent :: v_createMenuItems_query_info (menu);
427 	if (our d_sound.data && our d_sound.data != data) {
428 		EditorMenu_addCommand (menu, U"Sound info", 0, INFO_DATA__SoundInfo);
429 	} else if (our d_longSound.data && our d_longSound.data != data) {
430 		EditorMenu_addCommand (menu, U"LongSound info", 0, INFO_DATA__LongSoundInfo);
431 	}
432 	if (our d_sound.data) {
433 		EditorMenu_addCommand (menu, U"-- sound query --", 0, nullptr);
434 		EditorMenu_addCommand (menu, U"Get amplitude(s)", 0, INFO_DATA__getAmplitudes);
435 	}
436 }
437 
438 /********** VIEW MENU **********/
439 
menu_cb_soundScaling(TimeSoundEditor me,EDITOR_ARGS_FORM)440 static void menu_cb_soundScaling (TimeSoundEditor me, EDITOR_ARGS_FORM) {
441 	EDITOR_FORM (U"Sound scaling", nullptr)
442 		OPTIONMENU_ENUM (kTimeSoundEditor_scalingStrategy, scalingStrategy,
443 				U"Scaling strategy", my default_sound_scalingStrategy ())
444 		LABEL (U"For \"fixed height\":")
445 		POSITIVE (height, U"Height", my default_sound_scaling_height ())
446 		LABEL (U"For \"fixed range\":")
447 		REAL (minimum, U"Minimum", my default_sound_scaling_minimum ())
448 		REAL (maximum, U"Maximum", my default_sound_scaling_maximum ())
449 	EDITOR_OK
450 		SET_ENUM (scalingStrategy, kTimeSoundEditor_scalingStrategy, my p_sound_scalingStrategy)
451 		SET_REAL (height,  my p_sound_scaling_height)
452 		SET_REAL (minimum, my p_sound_scaling_minimum)
453 		SET_REAL (maximum, my p_sound_scaling_maximum)
454 	EDITOR_DO
455 		my pref_sound_scalingStrategy () = my p_sound_scalingStrategy = scalingStrategy;
456 		my pref_sound_scaling_height  () = my p_sound_scaling_height  = height;
457 		my pref_sound_scaling_minimum () = my p_sound_scaling_minimum = minimum;
458 		my pref_sound_scaling_maximum () = my p_sound_scaling_maximum = maximum;
459 		FunctionEditor_redraw (me);
460 	EDITOR_END
461 }
462 
menu_cb_soundMuteChannels(TimeSoundEditor me,EDITOR_ARGS_FORM)463 static void menu_cb_soundMuteChannels (TimeSoundEditor me, EDITOR_ARGS_FORM) {
464 	EDITOR_FORM (U"Mute channels", nullptr)
465 		NATURALVECTOR (channels, U"Channels to mute", WHITESPACE_SEPARATED_, U"2")
466 	EDITOR_OK
467 	EDITOR_DO
468 		const integer numberOfChannels = ( my d_longSound.data ? my d_longSound.data -> numberOfChannels : my d_sound.data -> ny );
469 		Melder_assert (my d_sound.muteChannels.size == numberOfChannels);
470 		for (integer ichan = 1; ichan <= numberOfChannels; ichan ++)
471 			my d_sound.muteChannels [ichan] = false;
472 		for (integer ichan = 1; ichan <= channels.size; ichan ++)
473 			if (channels [ichan] >= 1 && channels [ichan] <= numberOfChannels)
474 				my d_sound.muteChannels [channels [ichan]] = true;
475 		FunctionEditor_redraw (me);
476 	EDITOR_END
477 }
478 
v_createMenuItems_view(EditorMenu menu)479 void structTimeSoundEditor :: v_createMenuItems_view (EditorMenu menu) {
480 	if (our d_sound.data || our d_longSound.data)
481 		our v_createMenuItems_view_sound (menu);
482 	TimeSoundEditor_Parent :: v_createMenuItems_view (menu);
483 }
484 
v_createMenuItems_view_sound(EditorMenu menu)485 void structTimeSoundEditor :: v_createMenuItems_view_sound (EditorMenu menu) {
486 	EditorMenu_addCommand (menu, U"Sound scaling...", 0, menu_cb_soundScaling);
487 	EditorMenu_addCommand (menu, U"Mute channels...", 0, menu_cb_soundMuteChannels);
488 }
489 
v_updateMenuItems_file()490 void structTimeSoundEditor :: v_updateMenuItems_file () {
491 	Sampled sound;
492 	if (our d_sound.data)   // cannot do this with "?:", because d_sound.data and d_longSound.data have different types
493 		sound = our d_sound.data;
494 	else
495 		sound = our d_longSound.data;
496 	if (! sound)
497 		return;
498 	integer first, last, selectedSamples = Sampled_getWindowSamples (sound, our startSelection, our endSelection, & first, & last);
499 	if (our drawButton) {
500 		GuiThing_setSensitive (our drawButton, selectedSamples != 0);
501 		GuiThing_setSensitive (our publishButton, selectedSamples != 0);
502 		GuiThing_setSensitive (our publishPreserveButton, selectedSamples != 0);
503 		if (our publishWindowButton)
504 			GuiThing_setSensitive (our publishWindowButton, selectedSamples != 0);
505 		if (our publishOverlapButton)
506 			GuiThing_setSensitive (our publishOverlapButton, selectedSamples != 0);
507 	}
508 	GuiThing_setSensitive (our writeWavButton, selectedSamples != 0);
509 	if (our saveAs24BitWavButton)
510 		GuiThing_setSensitive (our saveAs24BitWavButton, selectedSamples != 0);
511 	if (our saveAs32BitWavButton)
512 		GuiThing_setSensitive (our saveAs32BitWavButton, selectedSamples != 0);
513 	GuiThing_setSensitive (our writeAiffButton, selectedSamples != 0);
514 	GuiThing_setSensitive (our writeAifcButton, selectedSamples != 0);
515 	GuiThing_setSensitive (our writeNextSunButton, selectedSamples != 0);
516 	GuiThing_setSensitive (our writeNistButton, selectedSamples != 0);
517 	GuiThing_setSensitive (our writeFlacButton, selectedSamples != 0);
518 }
519 
TimeSoundEditor_drawSound(TimeSoundEditor me,double globalMinimum,double globalMaximum)520 void TimeSoundEditor_drawSound (TimeSoundEditor me, double globalMinimum, double globalMaximum) {
521 	Sound sound = my d_sound.data;
522 	LongSound longSound = my d_longSound.data;
523 	Melder_assert (!! sound != !! longSound);
524 	const integer numberOfChannels = ( sound ? sound -> ny : longSound -> numberOfChannels );
525 	const bool cursorVisible = ( my startSelection == my endSelection && my startSelection >= my startWindow && my startSelection <= my endWindow );
526 	Graphics_setColour (my graphics.get(), Melder_BLACK);
527 	bool fits;
528 	try {
529 		fits = ( sound ? true : LongSound_haveWindow (longSound, my startWindow, my endWindow) );
530 	} catch (MelderError) {
531 		const bool outOfMemory = !! str32str (Melder_getError (), U"memory");
532 		if (Melder_debug == 9)
533 			Melder_flushError ();
534 		else
535 			Melder_clearError ();
536 		Graphics_setWindow (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
537 		Graphics_setTextAlignment (my graphics.get(), Graphics_CENTRE, Graphics_HALF);
538 		Graphics_text (my graphics.get(), 0.5, 0.5, outOfMemory ? U"(out of memory)" : U"(cannot read sound file)");
539 		return;
540 	}
541 	if (! fits) {
542 		Graphics_setWindow (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
543 		Graphics_setTextAlignment (my graphics.get(), Graphics_CENTRE, Graphics_HALF);
544 		Graphics_text (my graphics.get(), 0.5, 0.5, U"(window too large; zoom in to see the data)");
545 		return;
546 	}
547 	integer first, last;
548 	if (Sampled_getWindowSamples (sound ? (Sampled) sound : (Sampled) longSound, my startWindow, my endWindow, & first, & last) <= 1) {
549 		Graphics_setWindow (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
550 		Graphics_setTextAlignment (my graphics.get(), Graphics_CENTRE, Graphics_HALF);
551 		Graphics_text (my graphics.get(), 0.5, 0.5, U"(zoom out to see the data)");
552 		return;
553 	}
554 	const integer numberOfVisibleChannels = Melder_clippedRight (numberOfChannels, 8_integer);
555 	const integer firstVisibleChannel = my d_sound.channelOffset + 1;
556 	const integer lastVisibleChannel = Melder_clippedRight (my d_sound.channelOffset + numberOfVisibleChannels, numberOfChannels);
557 	double maximumExtent = 0.0, visibleMinimum = 0.0, visibleMaximum = 0.0;
558 	if (my p_sound_scalingStrategy == kTimeSoundEditor_scalingStrategy::BY_WINDOW) {
559 		if (longSound)
560 			LongSound_getWindowExtrema (longSound, my startWindow, my endWindow, firstVisibleChannel, & visibleMinimum, & visibleMaximum);
561 		else
562 			Matrix_getWindowExtrema (sound, first, last, firstVisibleChannel, firstVisibleChannel, & visibleMinimum, & visibleMaximum);
563 		for (integer ichan = firstVisibleChannel + 1; ichan <= lastVisibleChannel; ichan ++) {
564 			double visibleChannelMinimum, visibleChannelMaximum;
565 			if (longSound)
566 				LongSound_getWindowExtrema (longSound, my startWindow, my endWindow, ichan, & visibleChannelMinimum, & visibleChannelMaximum);
567 			else
568 				Matrix_getWindowExtrema (sound, first, last, ichan, ichan, & visibleChannelMinimum, & visibleChannelMaximum);
569 			if (visibleChannelMinimum < visibleMinimum)
570 				visibleMinimum = visibleChannelMinimum;
571 			if (visibleChannelMaximum > visibleMaximum)
572 				visibleMaximum = visibleChannelMaximum;
573 		}
574 		maximumExtent = visibleMaximum - visibleMinimum;
575 	}
576 	for (integer ichan = firstVisibleChannel; ichan <= lastVisibleChannel; ichan ++) {
577 		const double cursorFunctionValue = ( longSound ? 0.0 :
578 				Vector_getValueAtX (sound, 0.5 * (my startSelection + my endSelection), ichan, kVector_valueInterpolation :: SINC70) );
579 		const double ymin = (double) (numberOfVisibleChannels - ichan + my d_sound.channelOffset) / numberOfVisibleChannels;
580 		const double ymax = (double) (numberOfVisibleChannels + 1 - ichan + my d_sound.channelOffset) / numberOfVisibleChannels;
581 		Graphics_Viewport vp = Graphics_insetViewport (my graphics.get(), 0.0, 1.0, ymin, ymax);
582 		bool horizontal = false;
583 		double minimum = ( sound ? globalMinimum : -1.0 ), maximum = ( sound ? globalMaximum : 1.0 );
584 		if (my p_sound_scalingStrategy == kTimeSoundEditor_scalingStrategy::BY_WINDOW) {
585 			if (numberOfChannels > 2) {
586 				if (longSound)
587 					LongSound_getWindowExtrema (longSound, my startWindow, my endWindow, ichan, & minimum, & maximum);
588 				else
589 					Matrix_getWindowExtrema (sound, first, last, ichan, ichan, & minimum, & maximum);
590 				if (maximumExtent > 0.0) {
591 					const double middle = 0.5 * (minimum + maximum);
592 					minimum = middle - 0.5 * maximumExtent;
593 					maximum = middle + 0.5 * maximumExtent;
594 				}
595 			} else {
596 				minimum = visibleMinimum;
597 				maximum = visibleMaximum;
598 			}
599 		} else if (my p_sound_scalingStrategy == kTimeSoundEditor_scalingStrategy::BY_WINDOW_AND_CHANNEL) {
600 			if (longSound)
601 				LongSound_getWindowExtrema (longSound, my startWindow, my endWindow, ichan, & minimum, & maximum);
602 			else
603 				Matrix_getWindowExtrema (sound, first, last, ichan, ichan, & minimum, & maximum);
604 		} else if (my p_sound_scalingStrategy == kTimeSoundEditor_scalingStrategy::FIXED_HEIGHT) {
605 			if (longSound)
606 				LongSound_getWindowExtrema (longSound, my startWindow, my endWindow, ichan, & minimum, & maximum);
607 			else
608 				Matrix_getWindowExtrema (sound, first, last, ichan, ichan, & minimum, & maximum);
609 			const double channelExtent = my p_sound_scaling_height;
610 			const double middle = 0.5 * (minimum + maximum);
611 			minimum = middle - 0.5 * channelExtent;
612 			maximum = middle + 0.5 * channelExtent;
613 		} else if (my p_sound_scalingStrategy == kTimeSoundEditor_scalingStrategy::FIXED_RANGE) {
614 			minimum = my p_sound_scaling_minimum;
615 			maximum = my p_sound_scaling_maximum;
616 		}
617 		if (minimum == maximum) {
618 			horizontal = true;
619 			minimum -= 1.0;
620 			maximum += 1.0;
621 		}
622 		Graphics_setWindow (my graphics.get(), my startWindow, my endWindow, minimum, maximum);
623 		if (horizontal) {
624 			Graphics_setTextAlignment (my graphics.get(), Graphics_RIGHT, Graphics_HALF);
625 			const double mid = 0.5 * (minimum + maximum);
626 			Graphics_text (my graphics.get(), my startWindow, mid, Melder_float (Melder_half (mid)));
627 		} else {
628 			if (! cursorVisible || isundef (cursorFunctionValue) || Graphics_dyWCtoMM (my graphics.get(), cursorFunctionValue - minimum) > 5.0) {
629 				Graphics_setTextAlignment (my graphics.get(), Graphics_RIGHT, Graphics_BOTTOM);
630 				Graphics_text (my graphics.get(), my startWindow, minimum, Melder_float (Melder_half (minimum)));
631 			}
632 			if (! cursorVisible || isundef (cursorFunctionValue) || Graphics_dyWCtoMM (my graphics.get(), maximum - cursorFunctionValue) > 5.0) {
633 				Graphics_setTextAlignment (my graphics.get(), Graphics_RIGHT, Graphics_TOP);
634 				Graphics_text (my graphics.get(), my startWindow, maximum, Melder_float (Melder_half (maximum)));
635 			}
636 		}
637 		if (minimum < 0 && maximum > 0 && ! horizontal) {
638 			Graphics_setWindow (my graphics.get(), 0.0, 1.0, minimum, maximum);
639 			if (! cursorVisible || isundef (cursorFunctionValue) || fabs (Graphics_dyWCtoMM (my graphics.get(), cursorFunctionValue - 0.0)) > 3.0) {
640 				Graphics_setTextAlignment (my graphics.get(), Graphics_RIGHT, Graphics_HALF);
641 				Graphics_text (my graphics.get(), 0.0, 0.0, U"0");
642 			}
643 			Graphics_setColour (my graphics.get(), Melder_CYAN);
644 			Graphics_setLineType (my graphics.get(), Graphics_DOTTED);
645 			Graphics_line (my graphics.get(), 0.0, 0.0, 1.0, 0.0);
646 			Graphics_setLineType (my graphics.get(), Graphics_DRAWN);
647 		}
648 		/*
649 			Garnish the drawing area of each channel.
650 		*/
651 		Graphics_setWindow (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
652 		Graphics_setColour (my graphics.get(), Melder_CYAN);
653 		Graphics_innerRectangle (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
654 		Graphics_setColour (my graphics.get(), Melder_BLACK);
655 		if (numberOfChannels > 1) {
656 			Graphics_setTextAlignment (my graphics.get(), Graphics_LEFT, Graphics_HALF);
657 			Graphics_setTextAlignment (my graphics.get(), Graphics_LEFT, Graphics_HALF);
658 			conststring32 channelName = my v_getChannelName (ichan);
659 			static MelderString channelLabel;
660 			MelderString_copy (& channelLabel, ( channelName ? U"ch" : U"Ch " ), ichan);
661 			if (channelName)
662 				MelderString_append (& channelLabel, U": ", channelName);
663 			MelderString_append (& channelLabel, U" ",
664 					( my d_sound.muteChannels [ichan] ? UNITEXT_SPEAKER_WITH_CANCELLATION_STROKE : UNITEXT_SPEAKER ));
665 			if (ichan > 8 && ichan - my d_sound.channelOffset == 1)
666 				MelderString_append (& channelLabel, U"      " UNITEXT_UPWARDS_ARROW);
667 			else if (numberOfChannels >= 8 && ichan - my d_sound.channelOffset == 8 && ichan < numberOfChannels)
668 				MelderString_append (& channelLabel, U"      " UNITEXT_DOWNWARDS_ARROW);
669 			Graphics_text (my graphics.get(), 1.0, 0.5, channelLabel.string);
670 		}
671 		/*
672 			Draw a very thin separator line underneath.
673 		*/
674 		if (ichan < numberOfChannels) {
675 			/*Graphics_setColour (my graphics.get(), Melder_BLACK);*/
676 			Graphics_line (my graphics.get(), 0.0, 0.0, 1.0, 0.0);
677 		}
678 		/*
679 			Draw the samples.
680 		*/
681 		/*if (ichan == 1) FunctionEditor_SoundAnalysis_drawPulses (this);*/
682 		if (sound) {
683 			Graphics_setWindow (my graphics.get(), my startWindow, my endWindow, minimum, maximum);
684 			if (cursorVisible && isdefined (cursorFunctionValue))
685 				FunctionEditor_drawCursorFunctionValue (me, cursorFunctionValue, Melder_float (Melder_half (cursorFunctionValue)), U"");
686 			Graphics_setColour (my graphics.get(), Melder_BLACK);
687 			Graphics_function (my graphics.get(), & sound -> z [ichan] [0], first, last,
688 					Sampled_indexToX (sound, first), Sampled_indexToX (sound, last));
689 		} else {
690 			Graphics_setWindow (my graphics.get(), my startWindow, my endWindow, minimum * 32768, maximum * 32768);
691 			Graphics_function16 (my graphics.get(),
692 					longSound -> buffer.asArgumentToFunctionThatExpectsZeroBasedArray() - longSound -> imin * numberOfChannels + (ichan - 1),
693 					numberOfChannels, first, last, Sampled_indexToX (longSound, first), Sampled_indexToX (longSound, last));
694 		}
695 		Graphics_resetViewport (my graphics.get(), vp);
696 	}
697 	Graphics_setWindow (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
698 	Graphics_rectangle (my graphics.get(), 0.0, 1.0, 0.0, 1.0);
699 }
700 
v_mouseInWideDataView(GuiDrawingArea_MouseEvent event,double x_world,double y_fraction)701 bool structTimeSoundEditor :: v_mouseInWideDataView (GuiDrawingArea_MouseEvent event, double x_world, double y_fraction) {
702 	if (event -> isClick()) {
703 		Sound sound = our d_sound.data;
704 		LongSound longSound = our d_longSound.data;
705 		if (!! sound != !! longSound) {
706 			y_fraction = (y_fraction - v_getBottomOfSoundArea ()) / (1.0 - v_getBottomOfSoundArea ());
707 			const integer numberOfChannels = ( sound ? sound -> ny : longSound -> numberOfChannels );
708 			if (event -> commandKeyPressed) {
709 				if (numberOfChannels > 1) {
710 					const integer numberOfVisibleChannels = Melder_clippedRight (numberOfChannels, 8_integer);
711 					Melder_assert (numberOfVisibleChannels >= 1);   // for Melder_clipped
712 					const integer clickedChannel = our d_sound.channelOffset +
713 							Melder_clipped (1_integer, Melder_ifloor ((1.0 - y_fraction) * numberOfVisibleChannels + 1), numberOfVisibleChannels);
714 					const integer firstVisibleChannel = our d_sound.channelOffset + 1;
715 					const integer lastVisibleChannel = Melder_clippedRight (our d_sound.channelOffset + numberOfVisibleChannels, numberOfChannels);
716 					if (clickedChannel >= firstVisibleChannel && clickedChannel <= lastVisibleChannel) {
717 						our d_sound.muteChannels [clickedChannel] = ! our d_sound.muteChannels [clickedChannel];
718 						return FunctionEditor_UPDATE_NEEDED;
719 					}
720 				}
721 			} else {
722 				if (numberOfChannels > 8) {
723 					if (x_world >= our endWindow && y_fraction > 0.875 && y_fraction <= 1.000 && our d_sound.channelOffset > 0) {
724 						our d_sound.channelOffset -= 8;
725 						return FunctionEditor_UPDATE_NEEDED;
726 					}
727 					if (x_world >= our endWindow && y_fraction > 0.000 && y_fraction <= 0.125 && our d_sound.channelOffset < numberOfChannels - 8) {
728 						our d_sound.channelOffset += 8;
729 						return FunctionEditor_UPDATE_NEEDED;
730 					}
731 				}
732 			}
733 		}
734 	}
735 	return TimeSoundEditor_Parent :: v_mouseInWideDataView (event, x_world, y_fraction);
736 }
737 
TimeSoundEditor_init(TimeSoundEditor me,conststring32 title,Function data,Sampled sound,bool ownSound)738 void TimeSoundEditor_init (TimeSoundEditor me, conststring32 title, Function data, Sampled sound, bool ownSound) {
739 	my d_ownSound = ownSound;
740 	if (sound) {
741 		integer numberOfChannels = 1;
742 		if (ownSound) {
743 			Melder_assert (Thing_isa (sound, classSound));
744 			my d_sound.data = Data_copy ((Sound) sound).releaseToAmbiguousOwner();   // deep copy; ownership transferred
745 			Matrix_getWindowExtrema (my d_sound.data, 1, my d_sound.data -> nx, 1, my d_sound.data -> ny, & my d_sound.minimum, & my d_sound.maximum);
746 			numberOfChannels = my d_sound.data -> ny;
747 		} else if (Thing_isa (sound, classSound)) {
748 			my d_sound.data = (Sound) sound;   // reference copy; ownership not transferred
749 			Matrix_getWindowExtrema (my d_sound.data, 1, my d_sound.data -> nx, 1, my d_sound.data -> ny, & my d_sound.minimum, & my d_sound.maximum);
750 			numberOfChannels = my d_sound.data -> ny;
751 		} else if (Thing_isa (sound, classLongSound)) {
752 			my d_longSound.data = (LongSound) sound;
753 			my d_sound.minimum = -1.0;
754 			my d_sound.maximum = 1.0;
755 			numberOfChannels = my d_longSound.data -> numberOfChannels;
756 		} else {
757 			Melder_fatal (U"Invalid sound class in TimeSoundEditor::init.");
758 		}
759 		my d_sound.muteChannels = zero_BOOLVEC (numberOfChannels);
760 	}
761 	FunctionEditor_init (me, title, data);
762 }
763 
764 /* End of file TimeSoundEditor.cpp */
765