1 /* SoundEditor.cpp
2  *
3  * Copyright (C) 1992-2020 Paul Boersma, 2007 Erez Volk (FLAC support)
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 "SoundEditor.h"
20 #include "Sound_and_Spectrogram.h"
21 #include "Pitch.h"
22 #include "Sound_and_MixingMatrix.h"
23 #include "EditorM.h"
24 
25 Thing_implement (SoundEditor, TimeSoundAnalysisEditor, 0);
26 
27 /********** METHODS **********/
28 
v_dataChanged()29 void structSoundEditor :: v_dataChanged () {
30 	Sound sound = (Sound) data;
31 	Melder_assert (sound);
32 	if (sound -> classInfo == classSound)   // LongSound editors can get spurious v_dataChanged messages (e.g. in a TextGrid editor)
33 		Matrix_getWindowExtrema (sound, 1, sound -> nx, 1, sound -> ny, & d_sound.minimum, & d_sound.maximum);   // BUG unreadable
34 	v_reset_analysis ();
35 	SoundEditor_Parent :: v_dataChanged ();
36 }
37 
38 /***** EDIT MENU *****/
39 
menu_cb_Copy(SoundEditor me,EDITOR_ARGS_DIRECT)40 static void menu_cb_Copy (SoundEditor me, EDITOR_ARGS_DIRECT) {
41 	try {
42 		Sound_clipboard = ( my d_longSound.data
43 			? LongSound_extractPart ((LongSound) my data, my startSelection, my endSelection, false)
44 			: Sound_extractPart ((Sound) my data, my startSelection, my endSelection, kSound_windowShape::RECTANGULAR, 1.0, false)
45 		);
46 	} catch (MelderError) {
47 		Melder_throw (U"Sound selection not copied to clipboard.");
48 	}
49 }
50 
menu_cb_Cut(SoundEditor me,EDITOR_ARGS_DIRECT)51 static void menu_cb_Cut (SoundEditor me, EDITOR_ARGS_DIRECT) {
52 	try {
53 		Sound sound = (Sound) my data;
54 		integer first, last, selectionNumberOfSamples = Sampled_getWindowSamples (sound,
55 				my startSelection, my endSelection, & first, & last);
56 		integer oldNumberOfSamples = sound -> nx;
57 		integer newNumberOfSamples = oldNumberOfSamples - selectionNumberOfSamples;
58 		if (newNumberOfSamples < 1)
59 			Melder_throw (U"You cannot cut all of the signal away,\n"
60 				U"because you cannot create a Sound with 0 samples.\n"
61 				U"You could consider using Copy instead."
62 			);
63 
64 		if (selectionNumberOfSamples > 0) {
65 			/*
66 				Create without change.
67 			*/
68 			autoSound publish = Sound_create (sound -> ny, 0.0, selectionNumberOfSamples * sound -> dx,
69 							selectionNumberOfSamples, sound -> dx, 0.5 * sound -> dx);
70 			for (integer channel = 1; channel <= sound -> ny; channel ++) {
71 				integer j = 0;
72 				for (integer i = first; i <= last; i ++)
73 					publish -> z [channel] [++ j] = sound -> z [channel] [i];
74 			}
75 			autoMAT newData = raw_MAT (sound -> ny, newNumberOfSamples);
76 			for (integer channel = 1; channel <= sound -> ny; channel ++) {
77 				integer j = 0;
78 				for (integer i = 1; i < first; i ++)
79 					newData [channel] [++ j] = sound -> z [channel] [i];
80 				for (integer i = last + 1; i <= oldNumberOfSamples; i ++)
81 					newData [channel] [++ j] = sound -> z [channel] [i];
82 				Melder_assert (j == newData.ncol);
83 			}
84 			Editor_save (me, U"Cut");
85 			/*
86 				Change without error.
87 			*/
88 			sound -> xmin = 0.0;
89 			sound -> xmax = newNumberOfSamples * sound -> dx;
90 			sound -> nx = newNumberOfSamples;
91 			sound -> x1 = 0.5 * sound -> dx;
92 			sound -> z = newData.move();
93 			Sound_clipboard = publish.move();
94 
95 			/*
96 				Start updating the markers of the FunctionEditor, respecting the invariants.
97 			*/
98 			my tmin = sound -> xmin;
99 			my tmax = sound -> xmax;
100 
101 			/*
102 				Collapse the selection,
103 				so that the Cut operation can immediately be undone by a Paste.
104 				The exact position will be half-way in between two samples.
105 			*/
106 			my startSelection = my endSelection = sound -> xmin + (first - 1) * sound -> dx;
107 
108 			/*
109 				Update the window.
110 			*/
111 			{
112 				double t1 = (first - 1) * sound -> dx;
113 				double t2 = last * sound -> dx;
114 				double windowLength = my endWindow - my startWindow;   // > 0
115 				if (t1 > my startWindow)
116 					if (t2 < my endWindow)
117 						my startWindow -= 0.5 * (t2 - t1);
118 					else
119 						(void) 0;
120 				else if (t2 < my endWindow)
121 					my startWindow -= t2 - t1;
122 				else   /* Cut overlaps entire window: centre. */
123 					my startWindow = my startSelection - 0.5 * windowLength;
124 				my endWindow = my startWindow + windowLength;   // first try
125 				if (my endWindow > my tmax) {
126 					my startWindow -= my endWindow - my tmax;   // second try
127 					Melder_clipLeft (my tmin, & my startWindow);   // third try
128 					my endWindow = my tmax;   // second try
129 				} else if (my startWindow < my tmin) {
130 					my endWindow -= my startWindow - my tmin;   // second try
131 					Melder_clipRight (& my endWindow, my tmax);   // third try
132 					my startWindow = my tmin;   // second try
133 				}
134 			}
135 
136 			/*
137 				Force FunctionEditor to show changes.
138 			*/
139 			Matrix_getWindowExtrema (sound, 1, sound -> nx, 1, sound -> ny, & my d_sound.minimum, & my d_sound.maximum);
140 			my v_reset_analysis ();
141 			FunctionEditor_ungroup (my data);
142 			FunctionEditor_marksChanged (me, false);
143 			Editor_broadcastDataChanged (me);
144 		} else {
145 			Melder_warning (U"No samples selected.");
146 		}
147 	} catch (MelderError) {
148 		Melder_throw (U"Sound selection not cut to clipboard.");
149 	}
150 }
151 
menu_cb_Paste(SoundEditor me,EDITOR_ARGS_DIRECT)152 static void menu_cb_Paste (SoundEditor me, EDITOR_ARGS_DIRECT) {
153 	Sound sound = (Sound) my data;
154 	integer leftSample = Sampled_xToLowIndex (sound, my endSelection);
155 	integer oldNumberOfSamples = sound -> nx, newNumberOfSamples;
156 	if (! Sound_clipboard) {
157 		Melder_warning (U"Clipboard is empty; nothing pasted.");
158 		return;
159 	}
160 	Melder_require (Sound_clipboard -> ny == sound -> ny,
161 		U"Cannot paste, because\n"
162 		U"the number of channels of the clipboard is not equal to\n"
163 		U"the number of channels of the edited sound."
164 	);
165 	Melder_require (Sound_clipboard -> dx == sound -> dx,
166 		U"Cannot paste, because\n"
167 		U"the sampling frequency of the clipboard is not equal to\n"
168 		U"the sampling frequency of the edited sound."
169 	);
170 	Melder_clip (0_integer, & leftSample, oldNumberOfSamples);
171 	newNumberOfSamples = oldNumberOfSamples + Sound_clipboard -> nx;
172 	/*
173 		Check without change.
174 	*/
175 	autoMAT newData = raw_MAT (sound -> ny, newNumberOfSamples);
176 	for (integer channel = 1; channel <= sound -> ny; channel ++) {
177 		integer j = 0;
178 		for (integer i = 1; i <= leftSample; i ++)
179 			newData [channel] [++ j] = sound -> z [channel] [i];
180 		for (integer i = 1; i <= Sound_clipboard -> nx; i ++)
181 			newData [channel] [++ j] = Sound_clipboard -> z [channel] [i];
182 		for (integer i = leftSample + 1; i <= oldNumberOfSamples; i ++)
183 			newData [channel] [++ j] = sound -> z [channel] [i];
184 		Melder_assert (j == newData.ncol);
185 	}
186 	Editor_save (me, U"Paste");
187 	/*
188 		Change without error.
189 	*/
190 	sound -> xmin = 0.0;
191 	sound -> xmax = newNumberOfSamples * sound -> dx;
192 	sound -> nx = newNumberOfSamples;
193 	sound -> x1 = 0.5 * sound -> dx;
194 	sound -> z = newData.move();
195 
196 	/*
197 		Start updating the markers of the FunctionEditor, respecting the invariants.
198 	*/
199 	my tmin = sound -> xmin;
200 	my tmax = sound -> xmax;
201 	Melder_clipLeft (my tmin, & my startWindow);
202 	Melder_clipRight (& my endWindow, my tmax);
203 	my startSelection = leftSample * sound -> dx;
204 	my endSelection = (leftSample + Sound_clipboard -> nx) * sound -> dx;
205 
206 	/*
207 		Force FunctionEditor to show changes.
208 	*/
209 	Matrix_getWindowExtrema (sound, 1, sound -> nx, 1, sound -> ny, & my d_sound.minimum, & my d_sound.maximum);
210 	my v_reset_analysis ();
211 	FunctionEditor_ungroup (my data);
212 	FunctionEditor_marksChanged (me, false);
213 	Editor_broadcastDataChanged (me);
214 }
215 
menu_cb_SetSelectionToZero(SoundEditor me,EDITOR_ARGS_DIRECT)216 static void menu_cb_SetSelectionToZero (SoundEditor me, EDITOR_ARGS_DIRECT) {
217 	Sound sound = (Sound) my data;
218 	integer first, last;
219 	Sampled_getWindowSamples (sound, my startSelection, my endSelection, & first, & last);
220 	Editor_save (me, U"Set to zero");
221 	sound -> z.verticalBand	(first, last)  <<=  0.0;
222 	my v_reset_analysis ();
223 	FunctionEditor_redraw (me);
224 	Editor_broadcastDataChanged (me);
225 }
226 
menu_cb_ReverseSelection(SoundEditor me,EDITOR_ARGS_DIRECT)227 static void menu_cb_ReverseSelection (SoundEditor me, EDITOR_ARGS_DIRECT) {
228 	Editor_save (me, U"Reverse selection");
229 	Sound_reverse ((Sound) my data, my startSelection, my endSelection);
230 	my v_reset_analysis ();
231 	FunctionEditor_redraw (me);
232 	Editor_broadcastDataChanged (me);
233 }
234 
235 /***** SELECT MENU *****/
236 
menu_cb_MoveCursorToZero(SoundEditor me,EDITOR_ARGS_DIRECT)237 static void menu_cb_MoveCursorToZero (SoundEditor me, EDITOR_ARGS_DIRECT) {
238 	const double zero = Sound_getNearestZeroCrossing ((Sound) my data, 0.5 * (my startSelection + my endSelection), 1);   // STEREO BUG
239 	if (isdefined (zero)) {
240 		my startSelection = my endSelection = zero;
241 		FunctionEditor_marksChanged (me, true);
242 	}
243 }
244 
menu_cb_MoveBtoZero(SoundEditor me,EDITOR_ARGS_DIRECT)245 static void menu_cb_MoveBtoZero (SoundEditor me, EDITOR_ARGS_DIRECT) {
246 	const double zero = Sound_getNearestZeroCrossing ((Sound) my data, my startSelection, 1);   // STEREO BUG
247 	if (isdefined (zero)) {
248 		my startSelection = zero;
249 		Melder_sort (& my startSelection, & my endSelection);
250 		FunctionEditor_marksChanged (me, true);
251 	}
252 }
253 
menu_cb_MoveEtoZero(SoundEditor me,EDITOR_ARGS_DIRECT)254 static void menu_cb_MoveEtoZero (SoundEditor me, EDITOR_ARGS_DIRECT) {
255 	double zero = Sound_getNearestZeroCrossing ((Sound) my data, my endSelection, 1);   // STEREO BUG
256 	if (isdefined (zero)) {
257 		my endSelection = zero;
258 		Melder_sort (& my startSelection, & my endSelection);
259 		FunctionEditor_marksChanged (me, true);
260 	}
261 }
262 
263 /***** HELP MENU *****/
264 
menu_cb_SoundEditorHelp(SoundEditor,EDITOR_ARGS_DIRECT)265 static void menu_cb_SoundEditorHelp (SoundEditor, EDITOR_ARGS_DIRECT) { Melder_help (U"SoundEditor"); }
menu_cb_LongSoundEditorHelp(SoundEditor,EDITOR_ARGS_DIRECT)266 static void menu_cb_LongSoundEditorHelp (SoundEditor, EDITOR_ARGS_DIRECT) { Melder_help (U"LongSoundEditor"); }
267 
v_createMenus()268 void structSoundEditor :: v_createMenus () {
269 	SoundEditor_Parent :: v_createMenus ();
270 	Melder_assert (data);
271 	Melder_assert (d_sound.data || d_longSound.data);
272 
273 	Editor_addCommand (this, U"Edit", U"-- cut copy paste --", 0, nullptr);
274 	if (d_sound.data)
275 		cutButton = Editor_addCommand (this, U"Edit", U"Cut", 'X', menu_cb_Cut);
276 	copyButton = Editor_addCommand (this, U"Edit", U"Copy selection to Sound clipboard", 'C', menu_cb_Copy);
277 	if (d_sound.data)
278 		pasteButton = Editor_addCommand (this, U"Edit", U"Paste after selection", 'V', menu_cb_Paste);
279 	if (d_sound.data) {
280 		Editor_addCommand (this, U"Edit", U"-- zero --", 0, nullptr);
281 		zeroButton = Editor_addCommand (this, U"Edit", U"Set selection to zero", 0, menu_cb_SetSelectionToZero);
282 		reverseButton = Editor_addCommand (this, U"Edit", U"Reverse selection", 'R', menu_cb_ReverseSelection);
283 	}
284 	if (d_sound.data) {
285 		Editor_addCommand (this, U"Select", U"-- move to zero --", 0, 0);
286 		Editor_addCommand (this, U"Select", U"Move start of selection to nearest zero crossing", ',', menu_cb_MoveBtoZero);
287 		Editor_addCommand (this, U"Select", U"Move begin of selection to nearest zero crossing", Editor_HIDDEN, menu_cb_MoveBtoZero);
288 		Editor_addCommand (this, U"Select", U"Move cursor to nearest zero crossing", '0', menu_cb_MoveCursorToZero);
289 		Editor_addCommand (this, U"Select", U"Move end of selection to nearest zero crossing", '.', menu_cb_MoveEtoZero);
290 	}
291 	v_createMenus_analysis ();
292 }
293 
v_createHelpMenuItems(EditorMenu menu)294 void structSoundEditor :: v_createHelpMenuItems (EditorMenu menu) {
295 	SoundEditor_Parent :: v_createHelpMenuItems (menu);
296 	EditorMenu_addCommand (menu, U"SoundEditor help", '?', menu_cb_SoundEditorHelp);
297 	EditorMenu_addCommand (menu, U"LongSoundEditor help", 0, menu_cb_LongSoundEditorHelp);
298 }
299 
300 /********** UPDATE **********/
301 
v_prepareDraw()302 void structSoundEditor :: v_prepareDraw () {
303 	if (our d_longSound.data) {
304 		try {
305 			LongSound_haveWindow (our d_longSound.data, our startWindow, our endWindow);
306 		} catch (MelderError) {
307 			Melder_clearError ();
308 		}
309 	}
310 }
311 
v_draw()312 void structSoundEditor :: v_draw () {
313 	Sampled eitherData = (Sampled) our data;
314 	Graphics_Viewport viewport;
315 	bool showAnalysis = our p_spectrogram_show || our p_pitch_show || our p_intensity_show || our p_formant_show;
316 	Melder_assert (eitherData);
317 	Melder_assert (our d_sound.data || our d_longSound.data);
318 
319 	/*
320 		We check beforehand whether the window fits the LongSound buffer.
321 	*/
322 	if (our d_longSound.data && our endWindow - our startWindow > our d_longSound.data -> bufferLength) {
323 		Graphics_setColour (our graphics.get(), Melder_WHITE);
324 		Graphics_setWindow (our graphics.get(), 0.0, 1.0, 0.0, 1.0);
325 		Graphics_fillRectangle (our graphics.get(), 0.0, 1.0, 0.0, 1.0);
326 		Graphics_setColour (our graphics.get(), Melder_BLACK);
327 		Graphics_setTextAlignment (our graphics.get(), Graphics_CENTRE, Graphics_BOTTOM);
328 		Graphics_text (our graphics.get(), 0.5, 0.5,   U"(window longer than ", Melder_float (Melder_single (our d_longSound.data -> bufferLength)), U" seconds)");
329 		Graphics_setTextAlignment (our graphics.get(), Graphics_CENTRE, Graphics_TOP);
330 		Graphics_text (our graphics.get(), 0.5, 0.5, U"(zoom in to see the samples)");
331 		return;
332 	}
333 
334 	/*
335 		Draw data.
336 	*/
337 	if (showAnalysis)
338 		viewport = Graphics_insetViewport (our graphics.get(), 0.0, 1.0, 0.5, 1.0);
339 	Graphics_setColour (our graphics.get(), Melder_WHITE);
340 	Graphics_setWindow (our graphics.get(), 0.0, 1.0, 0.0, 1.0);
341 	Graphics_fillRectangle (our graphics.get(), 0.0, 1.0, 0.0, 1.0);
342 	if (p_pulses_show)
343 		v_draw_analysis_pulses ();
344 	TimeSoundEditor_drawSound (this, our d_sound.minimum, our d_sound.maximum);
345 	if (showAnalysis) {
346 		Graphics_resetViewport (our graphics.get(), viewport);
347 		viewport = Graphics_insetViewport (our graphics.get(), 0.0, 1.0, 0.0, 0.5);
348 		v_draw_analysis ();
349 		Graphics_resetViewport (our graphics.get(), viewport);
350 	}
351 
352 	/*
353 		Update buttons.
354 	*/
355 	integer first, last;
356 	integer selectedSamples = Sampled_getWindowSamples (eitherData, our startSelection, our endSelection, & first, & last);
357 	v_updateMenuItems_file ();
358 	if (our d_sound.data) {
359 		GuiThing_setSensitive (cutButton     , selectedSamples != 0 && selectedSamples < our d_sound.data -> nx);
360 		GuiThing_setSensitive (copyButton    , selectedSamples != 0);
361 		GuiThing_setSensitive (zeroButton    , selectedSamples != 0);
362 		GuiThing_setSensitive (reverseButton , selectedSamples != 0);
363 	}
364 }
365 
v_play(double startTime,double endTime)366 void structSoundEditor :: v_play (double startTime, double endTime) {
367 	const integer numberOfChannels = ( our d_longSound.data ? our d_longSound.data -> numberOfChannels : our d_sound.data -> ny );
368 	integer numberOfMuteChannels = 0;
369 	Melder_assert (our d_sound.muteChannels.size == numberOfChannels);
370 	for (integer ichan = 1; ichan <= numberOfChannels; ichan ++)
371 		if (our d_sound.muteChannels [ichan])
372 			numberOfMuteChannels ++;
373 	const integer numberOfChannelsToPlay = numberOfChannels - numberOfMuteChannels;
374 	Melder_require (numberOfChannelsToPlay > 0,
375 		U"Please select at least one channel to play.");
376 	if (our d_longSound.data) {
377 		if (numberOfMuteChannels > 0) {
378 			autoSound part = LongSound_extractPart (our d_longSound.data, startTime, endTime, 1);
379 			autoMixingMatrix thee = MixingMatrix_create (numberOfChannelsToPlay, numberOfChannels);
380 			MixingMatrix_muteAndActivateChannels (thee.get(), our d_sound.muteChannels.get());
381 			Sound_MixingMatrix_playPart (part.get(), thee.get(), startTime, endTime, theFunctionEditor_playCallback, this);
382 		} else {
383 			LongSound_playPart (our d_longSound.data, startTime, endTime, theFunctionEditor_playCallback, this);
384 		}
385 	} else {
386 		if (numberOfMuteChannels > 0) {
387 			autoMixingMatrix thee = MixingMatrix_create (numberOfChannelsToPlay, numberOfChannels);
388 			MixingMatrix_muteAndActivateChannels (thee.get(), our d_sound.muteChannels.get());
389 			Sound_MixingMatrix_playPart (our d_sound.data, thee.get(), startTime, endTime, theFunctionEditor_playCallback, this);
390 		} else {
391 			Sound_playPart (our d_sound.data, startTime, endTime, theFunctionEditor_playCallback, this);
392 		}
393 	}
394 }
395 
v_mouseInWideDataView(GuiDrawingArea_MouseEvent event,double xWC,double yWC)396 bool structSoundEditor :: v_mouseInWideDataView (GuiDrawingArea_MouseEvent event, double xWC, double yWC) {
397 	if ((our p_spectrogram_show || our p_formant_show) && yWC < 0.5 && xWC > our startWindow && xWC < our endWindow)
398 		our d_spectrogram_cursor = our p_spectrogram_viewFrom +
399 				2.0 * yWC * (our p_spectrogram_viewTo - our p_spectrogram_viewFrom);
400 	return SoundEditor_Parent :: v_mouseInWideDataView (event, xWC, yWC);
401 }
402 
v_highlightSelection(double left,double right,double bottom,double top)403 void structSoundEditor :: v_highlightSelection (double left, double right, double bottom, double top) {
404 	if (our p_spectrogram_show)
405 		Graphics_highlight (our graphics.get(), left, right, 0.5 * (bottom + top), top);
406 	else
407 		Graphics_highlight (our graphics.get(), left, right, bottom, top);
408 }
409 
SoundEditor_init(SoundEditor me,conststring32 title,Sampled data)410 void SoundEditor_init (SoundEditor me, conststring32 title, Sampled data) {
411 	/*
412 	 * my longSound.data or my sound.data have to be set before we call FunctionEditor_init,
413 	 * because createMenus expects that one of them is not null.
414 	 */
415 	TimeSoundAnalysisEditor_init (me, title, data, data, false);
416 	if (my d_longSound.data && my endWindow - my startWindow > 30.0) {
417 		my endWindow = my startWindow + 30.0;
418 		if (my startWindow == my tmin)
419 			my startSelection = my endSelection = 0.5 * (my startWindow + my endWindow);
420 		FunctionEditor_marksChanged (me, false);
421 	}
422 }
423 
SoundEditor_create(conststring32 title,Sampled data)424 autoSoundEditor SoundEditor_create (conststring32 title, Sampled data) {
425 	Melder_assert (data);
426 	try {
427 		autoSoundEditor me = Thing_new (SoundEditor);
428 		SoundEditor_init (me.get(), title, data);
429 		return me;
430 	} catch (MelderError) {
431 		Melder_throw (U"Sound window not created.");
432 	}
433 }
434 
435 /* End of file SoundEditor.cpp */
436