1 // Copyright (c) 2005, Rodrigo Braz Monteiro
2 // Copyright (c) 2009-2010, Niels Martin Hansen
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions are met:
7 //
8 //   * Redistributions of source code must retain the above copyright notice,
9 //     this list of conditions and the following disclaimer.
10 //   * Redistributions in binary form must reproduce the above copyright notice,
11 //     this list of conditions and the following disclaimer in the documentation
12 //     and/or other materials provided with the distribution.
13 //   * Neither the name of the Aegisub Group nor the names of its contributors
14 //     may be used to endorse or promote products derived from this software
15 //     without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 // POSSIBILITY OF SUCH DAMAGE.
28 //
29 // Aegisub Project http://www.aegisub.org/
30 
31 #include "audio_display.h"
32 
33 #include "audio_controller.h"
34 #include "audio_renderer.h"
35 #include "audio_renderer_spectrum.h"
36 #include "audio_renderer_waveform.h"
37 #include "audio_timing.h"
38 #include "compat.h"
39 #include "format.h"
40 #include "include/aegisub/context.h"
41 #include "include/aegisub/hotkey.h"
42 #include "options.h"
43 #include "project.h"
44 #include "utils.h"
45 #include "video_controller.h"
46 
47 #include <libaegisub/ass/time.h>
48 #include <libaegisub/audio/provider.h>
49 #include <libaegisub/make_unique.h>
50 
51 #include <algorithm>
52 
53 #include <wx/dcbuffer.h>
54 #include <wx/mousestate.h>
55 
56 namespace {
57 /// @brief Colourscheme-based UI colour provider
58 ///
59 /// This class provides UI colours corresponding to the supplied audio colour
60 /// scheme.
61 ///
62 /// SetColourScheme must be called to set the active colour scheme before
63 /// colours can be retrieved
64 class UIColours {
65 	wxColour light_colour;         ///< Light unfocused colour from the colour scheme
66 	wxColour dark_colour;          ///< Dark unfocused colour from the colour scheme
67 	wxColour sel_colour;           ///< Selection unfocused colour from the colour scheme
68 	wxColour light_focused_colour; ///< Light focused colour from the colour scheme
69 	wxColour dark_focused_colour;  ///< Dark focused colour from the colour scheme
70 	wxColour sel_focused_colour;   ///< Selection focused colour from the colour scheme
71 
72 	bool focused = false; ///< Use the focused colours?
73 public:
74 	/// Set the colour scheme to load colours from
75 	/// @param name Name of the colour scheme
SetColourScheme(std::string const & name)76 	void SetColourScheme(std::string const& name)
77 	{
78 		std::string opt_prefix = "Colour/Schemes/" + name + "/UI/";
79 		light_colour = to_wx(OPT_GET(opt_prefix + "Light")->GetColor());
80 		dark_colour = to_wx(OPT_GET(opt_prefix + "Dark")->GetColor());
81 		sel_colour = to_wx(OPT_GET(opt_prefix + "Selection")->GetColor());
82 
83 		opt_prefix = "Colour/Schemes/" + name + "/UI Focused/";
84 		light_focused_colour = to_wx(OPT_GET(opt_prefix + "Light")->GetColor());
85 		dark_focused_colour = to_wx(OPT_GET(opt_prefix + "Dark")->GetColor());
86 		sel_focused_colour = to_wx(OPT_GET(opt_prefix + "Selection")->GetColor());
87 	}
88 
89 	/// Set whether to use the focused or unfocused colours
90 	/// @param focused If true, focused colours will be returned
SetFocused(bool focused)91 	void SetFocused(bool focused) { this->focused = focused; }
92 
93 	/// Get the current Light colour
Light() const94 	wxColour Light() const { return focused ? light_focused_colour : light_colour; }
95 	/// Get the current Dark colour
Dark() const96 	wxColour Dark() const { return focused ? dark_focused_colour : dark_colour; }
97 	/// Get the current Selection colour
Selection() const98 	wxColour Selection() const { return focused ? sel_focused_colour : sel_colour; }
99 };
100 
101 class AudioDisplayScrollbar final : public AudioDisplayInteractionObject {
102 	static const int height = 15;
103 	static const int min_width = 10;
104 
105 	wxRect bounds;
106 	wxRect thumb;
107 
108 	bool dragging = false;   ///< user is dragging with the primary mouse button
109 
110 	int data_length = 1; ///< total amount of data in control
111 	int page_length = 1; ///< amount of data in one page
112 	int position    = 0; ///< first item displayed
113 
114 	int sel_start  = -1; ///< first data item in selection
115 	int sel_length = 0;  ///< number of data items in selection
116 
117 	UIColours colours; ///< Colour provider
118 
119 	/// Containing display to send scroll events to
120 	AudioDisplay *display;
121 
122 	// Recalculate thumb bounds from position and length data
RecalculateThumb()123 	void RecalculateThumb()
124 	{
125 		thumb.width = std::max<int>(min_width, (int64_t)bounds.width * page_length / data_length);
126 		thumb.height = height;
127 		thumb.x = int((int64_t)bounds.width * position / data_length);
128 		thumb.y = bounds.y;
129 	}
130 
131 public:
AudioDisplayScrollbar(AudioDisplay * display)132 	AudioDisplayScrollbar(AudioDisplay *display)
133 	: display(display)
134 	{
135 	}
136 
137 	/// The audio display has changed size
SetDisplaySize(const wxSize & display_size)138 	void SetDisplaySize(const wxSize &display_size)
139 	{
140 		bounds.x = 0;
141 		bounds.y = display_size.y - height;
142 		bounds.width = display_size.x;
143 		bounds.height = height;
144 		page_length = display_size.x;
145 
146 		RecalculateThumb();
147 	}
148 
SetColourScheme(std::string const & name)149 	void SetColourScheme(std::string const& name)
150 	{
151 		colours.SetColourScheme(name);
152 	}
153 
GetBounds() const154 	const wxRect & GetBounds() const { return bounds; }
GetPosition() const155 	int GetPosition() const { return position; }
156 
SetPosition(int new_position)157 	int SetPosition(int new_position)
158 	{
159 		// These two conditionals can't be swapped, otherwise the position can become
160 		// negative if the entire data is shorter than one page.
161 		if (new_position + page_length >= data_length)
162 			new_position = data_length - page_length - 1;
163 		if (new_position < 0)
164 			new_position = 0;
165 
166 		position = new_position;
167 		RecalculateThumb();
168 
169 		return position;
170 	}
171 
SetSelection(int new_start,int new_length)172 	void SetSelection(int new_start, int new_length)
173 	{
174 		sel_start = (int64_t)new_start * bounds.width / data_length;
175 		sel_length = (int64_t)new_length * bounds.width / data_length;
176 	}
177 
ChangeLengths(int new_data_length,int new_page_length)178 	void ChangeLengths(int new_data_length, int new_page_length)
179 	{
180 		data_length = new_data_length;
181 		page_length = new_page_length;
182 
183 		RecalculateThumb();
184 	}
185 
OnMouseEvent(wxMouseEvent & event)186 	bool OnMouseEvent(wxMouseEvent &event) override
187 	{
188 		if (event.LeftIsDown())
189 		{
190 			const int thumb_left = event.GetPosition().x - thumb.width/2;
191 			const int data_length_less_page = data_length - page_length;
192 			const int shaft_length_less_thumb = bounds.width - thumb.width;
193 
194 			display->ScrollPixelToLeft((int64_t)data_length_less_page * thumb_left / shaft_length_less_thumb);
195 
196 			dragging = true;
197 		}
198 		else if (event.LeftUp())
199 		{
200 			dragging = false;
201 		}
202 
203 		return dragging;
204 	}
205 
Paint(wxDC & dc,bool has_focus,int load_progress)206 	void Paint(wxDC &dc, bool has_focus, int load_progress)
207 	{
208 		colours.SetFocused(has_focus);
209 
210 		dc.SetPen(wxPen(colours.Light()));
211 		dc.SetBrush(wxBrush(colours.Dark()));
212 		dc.DrawRectangle(bounds);
213 
214 		if (sel_length > 0 && sel_start >= 0)
215 		{
216 			dc.SetPen(wxPen(colours.Selection()));
217 			dc.SetBrush(wxBrush(colours.Selection()));
218 			dc.DrawRectangle(wxRect(sel_start, bounds.y, sel_length, bounds.height));
219 		}
220 
221 		dc.SetPen(wxPen(colours.Light()));
222 		dc.SetBrush(*wxTRANSPARENT_BRUSH);
223 		dc.DrawRectangle(bounds);
224 
225 		if (load_progress > 0 && load_progress < data_length)
226 		{
227 			wxRect marker(
228 				(int64_t)bounds.width * load_progress / data_length - 25, bounds.y + 1,
229 				25, bounds.height - 2);
230 			dc.GradientFillLinear(marker, colours.Dark(), colours.Light());
231 		}
232 
233 		dc.SetPen(wxPen(colours.Light()));
234 		dc.SetBrush(wxBrush(colours.Light()));
235 		dc.DrawRectangle(thumb);
236 	}
237 };
238 
239 const int AudioDisplayScrollbar::min_width;
240 
241 class AudioDisplayTimeline final : public AudioDisplayInteractionObject {
242 	int duration = 0;          ///< Total duration in ms
243 	double ms_per_pixel = 1.0; ///< Milliseconds per pixel
244 	int pixel_left = 0;        ///< Leftmost visible pixel (i.e. scroll position)
245 
246 	wxRect bounds;
247 
248 	wxPoint drag_lastpos;
249 	bool dragging = false;
250 
251 	enum Scale {
252 		Sc_Millisecond,
253 		Sc_Centisecond,
254 		Sc_Decisecond,
255 		Sc_Second,
256 		Sc_Decasecond,
257 		Sc_Minute,
258 		Sc_Decaminute,
259 		Sc_Hour,
260 		Sc_Decahour, // If anyone needs this they should reconsider their project
261 		Sc_MAX = Sc_Decahour
262 	};
263 	Scale scale_minor;
264 	int scale_major_modulo; ///< If minor_scale_mark_index % scale_major_modulo == 0 the mark is a major mark
265 	double scale_minor_divisor; ///< Absolute scale-mark index multiplied by this number gives sample index for scale mark
266 
267 	AudioDisplay *display; ///< Containing audio display
268 
269 	UIColours colours; ///< Colour provider
270 
271 public:
AudioDisplayTimeline(AudioDisplay * display)272 	AudioDisplayTimeline(AudioDisplay *display)
273 	: display(display)
274 	{
275 		int width, height;
276 		display->GetTextExtent("0123456789:.", &width, &height);
277 		bounds.height = height + 4;
278 	}
279 
SetColourScheme(std::string const & name)280 	void SetColourScheme(std::string const& name)
281 	{
282 		colours.SetColourScheme(name);
283 	}
284 
SetDisplaySize(const wxSize & display_size)285 	void SetDisplaySize(const wxSize &display_size)
286 	{
287 		// The size is without anything that goes below the timeline (like scrollbar)
288 		bounds.width = display_size.x;
289 		bounds.x = 0;
290 		bounds.y = 0;
291 	}
292 
GetHeight() const293 	int GetHeight() const { return bounds.height; }
GetBounds() const294 	const wxRect & GetBounds() const { return bounds; }
295 
ChangeAudio(int new_duration)296 	void ChangeAudio(int new_duration)
297 	{
298 		duration = new_duration;
299 	}
300 
ChangeZoom(double new_ms_per_pixel)301 	void ChangeZoom(double new_ms_per_pixel)
302 	{
303 		ms_per_pixel = new_ms_per_pixel;
304 
305 		double px_sec = 1000.0 / ms_per_pixel;
306 
307 		if (px_sec > 3000) {
308 			scale_minor = Sc_Millisecond;
309 			scale_minor_divisor = 1.0;
310 			scale_major_modulo = 10;
311 		} else if (px_sec > 300) {
312 			scale_minor = Sc_Centisecond;
313 			scale_minor_divisor = 10.0;
314 			scale_major_modulo = 10;
315 		} else if (px_sec > 30) {
316 			scale_minor = Sc_Decisecond;
317 			scale_minor_divisor = 100.0;
318 			scale_major_modulo = 10;
319 		} else if (px_sec > 3) {
320 			scale_minor = Sc_Second;
321 			scale_minor_divisor = 1000.0;
322 			scale_major_modulo = 10;
323 		} else if (px_sec > 1.0/3.0) {
324 			scale_minor = Sc_Decasecond;
325 			scale_minor_divisor = 10000.0;
326 			scale_major_modulo = 6;
327 		} else if (px_sec > 1.0/9.0) {
328 			scale_minor = Sc_Minute;
329 			scale_minor_divisor = 60000.0;
330 			scale_major_modulo = 10;
331 		} else if (px_sec > 1.0/90.0) {
332 			scale_minor = Sc_Decaminute;
333 			scale_minor_divisor = 600000.0;
334 			scale_major_modulo = 6;
335 		} else {
336 			scale_minor = Sc_Hour;
337 			scale_minor_divisor = 3600000.0;
338 			scale_major_modulo = 10;
339 		}
340 	}
341 
SetPosition(int new_pixel_left)342 	void SetPosition(int new_pixel_left)
343 	{
344 		pixel_left = std::max(new_pixel_left, 0);
345 	}
346 
OnMouseEvent(wxMouseEvent & event)347 	bool OnMouseEvent(wxMouseEvent &event) override
348 	{
349 		if (event.LeftDown())
350 		{
351 			drag_lastpos = event.GetPosition();
352 			dragging = true;
353 		}
354 		else if (event.LeftIsDown())
355 		{
356 			display->ScrollPixelToLeft(pixel_left - event.GetPosition().x + drag_lastpos.x);
357 
358 			drag_lastpos = event.GetPosition();
359 			dragging = true;
360 		}
361 		else if (event.LeftUp())
362 		{
363 			dragging = false;
364 		}
365 
366 		return dragging;
367 	}
368 
Paint(wxDC & dc)369 	void Paint(wxDC &dc)
370 	{
371 		int bottom = bounds.y + bounds.height;
372 
373 		// Background
374 		dc.SetPen(wxPen(colours.Dark()));
375 		dc.SetBrush(wxBrush(colours.Dark()));
376 		dc.DrawRectangle(bounds);
377 
378 		// Top line
379 		dc.SetPen(wxPen(colours.Light()));
380 		dc.DrawLine(bounds.x, bottom-1, bounds.x+bounds.width, bottom-1);
381 
382 		// Prepare for writing text
383 		dc.SetTextBackground(colours.Dark());
384 		dc.SetTextForeground(colours.Light());
385 
386 		// Figure out the first scale mark to show
387 		int ms_left = int(pixel_left * ms_per_pixel);
388 		int next_scale_mark = int(ms_left / scale_minor_divisor);
389 		if (next_scale_mark * scale_minor_divisor < ms_left)
390 			next_scale_mark += 1;
391 		assert(next_scale_mark * scale_minor_divisor >= ms_left);
392 
393 		// Draw scale marks
394 		int next_scale_mark_pos;
395 		int last_text_right = -1;
396 		int last_hour = -1, last_minute = -1;
397 		if (duration < 3600) last_hour = 0; // Trick to only show hours if audio is longer than 1 hour
398 		do {
399 			next_scale_mark_pos = int(next_scale_mark * scale_minor_divisor / ms_per_pixel) - pixel_left;
400 			bool mark_is_major = next_scale_mark % scale_major_modulo == 0;
401 
402 			if (mark_is_major)
403 				dc.DrawLine(next_scale_mark_pos, bottom-6, next_scale_mark_pos, bottom-1);
404 			else
405 				dc.DrawLine(next_scale_mark_pos, bottom-4, next_scale_mark_pos, bottom-1);
406 
407 			// Print time labels on major scale marks
408 			if (mark_is_major && next_scale_mark_pos > last_text_right)
409 			{
410 				double mark_time = next_scale_mark * scale_minor_divisor / 1000.0;
411 				int mark_hour = (int)(mark_time / 3600);
412 				int mark_minute = (int)(mark_time / 60) % 60;
413 				double mark_second = mark_time - mark_hour*3600.0 - mark_minute*60.0;
414 
415 				wxString time_string;
416 				bool changed_hour = mark_hour != last_hour;
417 				bool changed_minute = mark_minute != last_minute;
418 
419 				if (changed_hour)
420 				{
421 					time_string = fmt_wx("%d:%02d:", mark_hour, mark_minute);
422 					last_hour = mark_hour;
423 					last_minute = mark_minute;
424 				}
425 				else if (changed_minute)
426 				{
427 					time_string = fmt_wx("%d:", mark_minute);
428 					last_minute = mark_minute;
429 				}
430 				if (scale_minor >= Sc_Decisecond)
431 					time_string += fmt_wx("%02d", mark_second);
432 				else if (scale_minor == Sc_Centisecond)
433 					time_string += fmt_wx("%02.1f", mark_second);
434 				else
435 					time_string += fmt_wx("%02.2f", mark_second);
436 
437 				int tw, th;
438 				dc.GetTextExtent(time_string, &tw, &th);
439 				last_text_right = next_scale_mark_pos + tw;
440 
441 				dc.DrawText(time_string, next_scale_mark_pos, 0);
442 			}
443 
444 			next_scale_mark += 1;
445 
446 		} while (next_scale_mark_pos < bounds.width);
447 	}
448 };
449 
450 class AudioStyleRangeMerger final : public AudioRenderingStyleRanges {
451 	typedef std::map<int, AudioRenderingStyle> style_map;
452 public:
453 	typedef style_map::iterator iterator;
454 
455 private:
456 	style_map points;
457 
Split(int point)458 	void Split(int point)
459 	{
460 		auto it = points.lower_bound(point);
461 		if (it == points.end() || it->first != point)
462 		{
463 			assert(it != points.begin());
464 			points[point] = (--it)->second;
465 		}
466 	}
467 
Restyle(int start,int end,AudioRenderingStyle style)468 	void Restyle(int start, int end, AudioRenderingStyle style)
469 	{
470 		assert(points.lower_bound(end) != points.end());
471 		for (auto pt = points.lower_bound(start); pt->first < end; ++pt)
472 		{
473 			if (style > pt->second)
474 				pt->second = style;
475 		}
476 	}
477 
478 public:
AudioStyleRangeMerger()479 	AudioStyleRangeMerger()
480 	{
481 		points[0] = AudioStyle_Normal;
482 	}
483 
AddRange(int start,int end,AudioRenderingStyle style)484 	void AddRange(int start, int end, AudioRenderingStyle style) override
485 	{
486 
487 		if (start < 0) start = 0;
488 		if (end < start) return;
489 
490 		Split(start);
491 		Split(end);
492 		Restyle(start, end, style);
493 	}
494 
begin()495 	iterator begin() { return points.begin(); }
end()496 	iterator end() { return points.end(); }
497 };
498 
499 }
500 
501 class AudioMarkerInteractionObject final : public AudioDisplayInteractionObject {
502 	// Object-pair being interacted with
503 	std::vector<AudioMarker*> markers;
504 	AudioTimingController *timing_controller;
505 	// Audio display drag is happening on
506 	AudioDisplay *display;
507 	// Mouse button used to initiate the drag
508 	wxMouseButton button_used;
509 	// Default to snapping to snappable markers
510 	bool default_snap = OPT_GET("Audio/Snap/Enable")->GetBool();
511 	// Range in pixels to snap at
512 	int snap_range = OPT_GET("Audio/Snap/Distance")->GetInt();
513 
514 public:
AudioMarkerInteractionObject(std::vector<AudioMarker * > markers,AudioTimingController * timing_controller,AudioDisplay * display,wxMouseButton button_used)515 	AudioMarkerInteractionObject(std::vector<AudioMarker*> markers, AudioTimingController *timing_controller, AudioDisplay *display, wxMouseButton button_used)
516 	: markers(std::move(markers))
517 	, timing_controller(timing_controller)
518 	, display(display)
519 	, button_used(button_used)
520 	{
521 	}
522 
OnMouseEvent(wxMouseEvent & event)523 	bool OnMouseEvent(wxMouseEvent &event) override
524 	{
525 		if (event.Dragging())
526 		{
527 			timing_controller->OnMarkerDrag(
528 				markers,
529 				display->TimeFromRelativeX(event.GetPosition().x),
530 				default_snap != event.ShiftDown() ? display->TimeFromAbsoluteX(snap_range) : 0);
531 		}
532 
533 		// We lose the marker drag if the button used to initiate it goes up
534 		return !event.ButtonUp(button_used);
535 	}
536 
537 	/// Get the position in milliseconds of this group of markers
GetPosition() const538 	int GetPosition() const { return markers.front()->GetPosition(); }
539 };
540 
AudioDisplay(wxWindow * parent,AudioController * controller,agi::Context * context)541 AudioDisplay::AudioDisplay(wxWindow *parent, AudioController *controller, agi::Context *context)
542 : wxWindow(parent, -1, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS|wxBORDER_SIMPLE)
543 , audio_open_connection(context->project->AddAudioProviderListener(&AudioDisplay::OnAudioOpen, this))
544 , context(context)
545 , audio_renderer(agi::make_unique<AudioRenderer>())
546 , controller(controller)
547 , scrollbar(agi::make_unique<AudioDisplayScrollbar>(this))
548 , timeline(agi::make_unique<AudioDisplayTimeline>(this))
549 {
550 	style_ranges[0] = AudioStyle_Normal;
551 
552 	audio_renderer->SetAmplitudeScale(scale_amplitude);
553 	SetZoomLevel(0);
554 
555 	SetMinClientSize(wxSize(-1, 70));
556 	SetBackgroundStyle(wxBG_STYLE_PAINT);
557 	SetThemeEnabled(false);
558 
559 	Bind(wxEVT_LEFT_DOWN, &AudioDisplay::OnMouseEvent, this);
560 	Bind(wxEVT_MIDDLE_DOWN, &AudioDisplay::OnMouseEvent, this);
561 	Bind(wxEVT_RIGHT_DOWN, &AudioDisplay::OnMouseEvent, this);
562 	Bind(wxEVT_LEFT_UP, &AudioDisplay::OnMouseEvent, this);
563 	Bind(wxEVT_MIDDLE_UP, &AudioDisplay::OnMouseEvent, this);
564 	Bind(wxEVT_RIGHT_UP, &AudioDisplay::OnMouseEvent, this);
565 	Bind(wxEVT_MOTION, &AudioDisplay::OnMouseEvent, this);
566 	Bind(wxEVT_ENTER_WINDOW, &AudioDisplay::OnMouseEnter, this);
567 	Bind(wxEVT_LEAVE_WINDOW, &AudioDisplay::OnMouseLeave, this);
568 	Bind(wxEVT_PAINT, &AudioDisplay::OnPaint, this);
569 	Bind(wxEVT_SIZE, &AudioDisplay::OnSize, this);
570 	Bind(wxEVT_KILL_FOCUS, &AudioDisplay::OnFocus, this);
571 	Bind(wxEVT_SET_FOCUS, &AudioDisplay::OnFocus, this);
572 	Bind(wxEVT_CHAR_HOOK, &AudioDisplay::OnKeyDown, this);
573 	Bind(wxEVT_KEY_DOWN, &AudioDisplay::OnKeyDown, this);
574 	scroll_timer.Bind(wxEVT_TIMER, &AudioDisplay::OnScrollTimer, this);
575 	load_timer.Bind(wxEVT_TIMER, &AudioDisplay::OnLoadTimer, this);
576 }
577 
~AudioDisplay()578 AudioDisplay::~AudioDisplay()
579 {
580 }
581 
ScrollBy(int pixel_amount)582 void AudioDisplay::ScrollBy(int pixel_amount)
583 {
584 	ScrollPixelToLeft(scroll_left + pixel_amount);
585 }
586 
ScrollPixelToLeft(int pixel_position)587 void AudioDisplay::ScrollPixelToLeft(int pixel_position)
588 {
589 	const int client_width = GetClientRect().GetWidth();
590 
591 	if (pixel_position + client_width >= pixel_audio_width)
592 		pixel_position = pixel_audio_width - client_width;
593 	if (pixel_position < 0)
594 		pixel_position = 0;
595 
596 	scroll_left = pixel_position;
597 	scrollbar->SetPosition(scroll_left);
598 	timeline->SetPosition(scroll_left);
599 	Refresh();
600 }
601 
ScrollTimeRangeInView(const TimeRange & range)602 void AudioDisplay::ScrollTimeRangeInView(const TimeRange &range)
603 {
604 	int client_width = GetClientRect().GetWidth();
605 	int range_begin = AbsoluteXFromTime(range.begin());
606 	int range_end = AbsoluteXFromTime(range.end());
607 	int range_len = range_end - range_begin;
608 
609 	// Remove 5 % from each side of the client area.
610 	int leftadjust = client_width / 20;
611 	int client_left = scroll_left + leftadjust;
612 	client_width = client_width * 9 / 10;
613 
614 	// Is everything already in view?
615 	if (range_begin >= client_left && range_end <= client_left+client_width)
616 		return;
617 
618 	// The entire range can fit inside the view, center it
619 	if (range_len < client_width)
620 	{
621 		ScrollPixelToLeft(range_begin - (client_width-range_len)/2 - leftadjust);
622 	}
623 
624 	// Range doesn't fit in view and we're viewing a middle part of it, just leave it alone
625 	else if (range_begin < client_left && range_end > client_left+client_width)
626 	{
627 		// nothing
628 	}
629 
630 	// Right edge is in view, scroll it as far to the right as possible
631 	else if (range_end >= client_left && range_end < client_left+client_width)
632 	{
633 		ScrollPixelToLeft(range_end - client_width - leftadjust);
634 	}
635 
636 	// Nothing is in view or the left edge is in view, scroll left edge as far to the left as possible
637 	else
638 	{
639 		ScrollPixelToLeft(range_begin - leftadjust);
640 	}
641 }
642 
SetZoomLevel(int new_zoom_level)643 void AudioDisplay::SetZoomLevel(int new_zoom_level)
644 {
645 	zoom_level = new_zoom_level;
646 
647 	const int factor = GetZoomLevelFactor(zoom_level);
648 	const int base_pixels_per_second = 50; /// @todo Make this customisable
649 	const double base_ms_per_pixel = 1000.0 / base_pixels_per_second;
650 	const double new_ms_per_pixel = 100.0 * base_ms_per_pixel / factor;
651 
652 	if (ms_per_pixel == new_ms_per_pixel) return;
653 
654 	int client_width = GetClientSize().GetWidth();
655 	double cursor_pos = track_cursor_pos >= 0 ? track_cursor_pos - scroll_left : client_width / 2.0;
656 	double cursor_time = (scroll_left + cursor_pos) * ms_per_pixel;
657 
658 	ms_per_pixel = new_ms_per_pixel;
659 	pixel_audio_width = std::max(1, int(GetDuration() / ms_per_pixel));
660 
661 	audio_renderer->SetMillisecondsPerPixel(ms_per_pixel);
662 	scrollbar->ChangeLengths(pixel_audio_width, client_width);
663 	timeline->ChangeZoom(ms_per_pixel);
664 
665 	ScrollPixelToLeft(AbsoluteXFromTime(cursor_time) - cursor_pos);
666 	if (track_cursor_pos >= 0)
667 		track_cursor_pos = AbsoluteXFromTime(cursor_time);
668 	Refresh();
669 }
670 
GetZoomLevelDescription(int level) const671 wxString AudioDisplay::GetZoomLevelDescription(int level) const
672 {
673 	const int factor = GetZoomLevelFactor(level);
674 	const int base_pixels_per_second = 50; /// @todo Make this customisable along with the above
675 	const int second_pixels = 100 * base_pixels_per_second / factor;
676 
677 	return fmt_tl("%d%%, %d pixel/second", factor, second_pixels);
678 }
679 
GetZoomLevelFactor(int level)680 int AudioDisplay::GetZoomLevelFactor(int level)
681 {
682 	int factor = 100;
683 
684 	if (level > 0)
685 	{
686 		factor += 25 * level;
687 	}
688 	else if (level < 0)
689 	{
690 		if (level >= -5)
691 			factor += 10 * level;
692 		else if (level >= -11)
693 			factor = 50 + (level+5) * 5;
694 		else
695 			factor = 20 + level + 11;
696 		if (factor <= 0)
697 			factor = 1;
698 	}
699 
700 	return factor;
701 }
702 
SetAmplitudeScale(float scale)703 void AudioDisplay::SetAmplitudeScale(float scale)
704 {
705 	audio_renderer->SetAmplitudeScale(scale);
706 	Refresh();
707 }
708 
ReloadRenderingSettings()709 void AudioDisplay::ReloadRenderingSettings()
710 {
711 	std::string colour_scheme_name;
712 
713 	if (OPT_GET("Audio/Spectrum")->GetBool())
714 	{
715 		colour_scheme_name = OPT_GET("Colour/Audio Display/Spectrum")->GetString();
716 		auto audio_spectrum_renderer = agi::make_unique<AudioSpectrumRenderer>(colour_scheme_name);
717 
718 		int64_t spectrum_quality = OPT_GET("Audio/Renderer/Spectrum/Quality")->GetInt();
719 #ifdef WITH_FFTW3
720 		// FFTW is so fast we can afford to upgrade quality by two levels
721 		spectrum_quality += 2;
722 #endif
723 		spectrum_quality = mid<int64_t>(0, spectrum_quality, 5);
724 
725 		// Quality indexes:        0  1  2  3   4   5
726 		int spectrum_width[]    = {8, 9, 9, 9, 10, 11};
727 		int spectrum_distance[] = {8, 8, 7, 6,  6,  5};
728 
729 		audio_spectrum_renderer->SetResolution(
730 			spectrum_width[spectrum_quality],
731 			spectrum_distance[spectrum_quality]);
732 
733 		audio_renderer_provider = std::move(audio_spectrum_renderer);
734 	}
735 	else
736 	{
737 		colour_scheme_name = OPT_GET("Colour/Audio Display/Waveform")->GetString();
738 		audio_renderer_provider = agi::make_unique<AudioWaveformRenderer>(colour_scheme_name);
739 	}
740 
741 	audio_renderer->SetRenderer(audio_renderer_provider.get());
742 	scrollbar->SetColourScheme(colour_scheme_name);
743 	timeline->SetColourScheme(colour_scheme_name);
744 
745 	Refresh();
746 }
747 
OnLoadTimer(wxTimerEvent &)748 void AudioDisplay::OnLoadTimer(wxTimerEvent&)
749 {
750 	using namespace std::chrono;
751 	if (provider)
752 	{
753 		const auto now = steady_clock::now();
754 		const auto elapsed = duration_cast<milliseconds>(now - audio_load_start_time).count();
755 		if (elapsed == 0) return;
756 
757 		const int64_t new_decoded_count = provider->GetDecodedSamples();
758 		if (new_decoded_count != last_sample_decoded)
759 			audio_load_speed = (audio_load_speed + (double)new_decoded_count / elapsed) / 2;
760 		if (audio_load_speed == 0) return;
761 
762 		int new_pos = AbsoluteXFromTime(elapsed * audio_load_speed * 1000.0 / provider->GetSampleRate());
763 		if (new_pos > audio_load_position)
764 			audio_load_position = new_pos;
765 
766 		const double left = last_sample_decoded * 1000.0 / provider->GetSampleRate() / ms_per_pixel;
767 		const double right = new_decoded_count * 1000.0 / provider->GetSampleRate() / ms_per_pixel;
768 
769 		if (left < scroll_left + pixel_audio_width && right >= scroll_left)
770 			Refresh();
771 		else
772 			RefreshRect(scrollbar->GetBounds());
773 		last_sample_decoded = new_decoded_count;
774 	}
775 
776 	if (!provider || last_sample_decoded == provider->GetNumSamples()) {
777 		load_timer.Stop();
778 		audio_load_position = -1;
779 	}
780 }
781 
OnPaint(wxPaintEvent &)782 void AudioDisplay::OnPaint(wxPaintEvent&)
783 {
784 	if (!audio_renderer_provider || !provider) return;
785 
786 	wxAutoBufferedPaintDC dc(this);
787 
788 	wxRect audio_bounds(0, audio_top, GetClientSize().GetWidth(), audio_height);
789 	bool redraw_scrollbar = false;
790 	bool redraw_timeline = false;
791 
792 	for (wxRegionIterator region(GetUpdateRegion()); region; ++region)
793 	{
794 		wxRect updrect = region.GetRect();
795 
796 		redraw_scrollbar |= scrollbar->GetBounds().Intersects(updrect);
797 		redraw_timeline |= timeline->GetBounds().Intersects(updrect);
798 
799 		if (audio_bounds.Intersects(updrect))
800 		{
801 			TimeRange updtime(
802 				std::max(0, TimeFromRelativeX(updrect.x - foot_size)),
803 				std::max(0, TimeFromRelativeX(updrect.x + updrect.width + foot_size)));
804 
805 			PaintAudio(dc, updtime, updrect);
806 			PaintMarkers(dc, updtime);
807 			PaintLabels(dc, updtime);
808 		}
809 	}
810 
811 	if (track_cursor_pos >= 0)
812 		PaintTrackCursor(dc);
813 
814 	if (redraw_scrollbar)
815 		scrollbar->Paint(dc, HasFocus(), audio_load_position);
816 	if (redraw_timeline)
817 		timeline->Paint(dc);
818 }
819 
PaintAudio(wxDC & dc,TimeRange updtime,wxRect updrect)820 void AudioDisplay::PaintAudio(wxDC &dc, TimeRange updtime, wxRect updrect)
821 {
822 	auto pt = style_ranges.upper_bound(updtime.begin());
823 	auto pe = style_ranges.upper_bound(updtime.end());
824 
825 	if (pt != style_ranges.begin())
826 		--pt;
827 
828 	while (pt != pe)
829 	{
830 		AudioRenderingStyle range_style = static_cast<AudioRenderingStyle>(pt->second);
831 		int range_x1 = std::max(updrect.x, RelativeXFromTime(pt->first));
832 		int range_x2 = (++pt == pe) ? updrect.x + updrect.width : RelativeXFromTime(pt->first);
833 
834 		if (range_x2 > range_x1)
835 		{
836 			audio_renderer->Render(dc, wxPoint(range_x1, audio_top), range_x1 + scroll_left, range_x2 - range_x1, range_style);
837 		}
838 	}
839 }
840 
PaintMarkers(wxDC & dc,TimeRange updtime)841 void AudioDisplay::PaintMarkers(wxDC &dc, TimeRange updtime)
842 {
843 	AudioMarkerVector markers;
844 	controller->GetTimingController()->GetMarkers(updtime, markers);
845 	if (markers.empty()) return;
846 
847 	wxDCPenChanger pen_retainer(dc, wxPen());
848 	wxDCBrushChanger brush_retainer(dc, wxBrush());
849 	for (const auto marker : markers)
850 	{
851 		int marker_x = RelativeXFromTime(marker->GetPosition());
852 
853 		dc.SetPen(marker->GetStyle());
854 		dc.DrawLine(marker_x, audio_top, marker_x, audio_top+audio_height);
855 
856 		if (marker->GetFeet() == AudioMarker::Feet_None) continue;
857 
858 		dc.SetBrush(wxBrush(marker->GetStyle().GetColour()));
859 		dc.SetPen(*wxTRANSPARENT_PEN);
860 
861 		if (marker->GetFeet() & AudioMarker::Feet_Left)
862 			PaintFoot(dc, marker_x, -1);
863 		if (marker->GetFeet() & AudioMarker::Feet_Right)
864 			PaintFoot(dc, marker_x, 1);
865 	}
866 }
867 
PaintFoot(wxDC & dc,int marker_x,int dir)868 void AudioDisplay::PaintFoot(wxDC &dc, int marker_x, int dir)
869 {
870 	wxPoint foot_top[3] = { wxPoint(foot_size * dir, 0), wxPoint(0, 0), wxPoint(0, foot_size) };
871 	wxPoint foot_bot[3] = { wxPoint(foot_size * dir, 0), wxPoint(0, -foot_size), wxPoint(0, 0) };
872 	dc.DrawPolygon(3, foot_top, marker_x, audio_top);
873 	dc.DrawPolygon(3, foot_bot, marker_x, audio_top+audio_height);
874 }
875 
PaintLabels(wxDC & dc,TimeRange updtime)876 void AudioDisplay::PaintLabels(wxDC &dc, TimeRange updtime)
877 {
878 	std::vector<AudioLabelProvider::AudioLabel> labels;
879 	controller->GetTimingController()->GetLabels(updtime, labels);
880 	if (labels.empty()) return;
881 
882 	wxDCFontChanger fc(dc);
883 	wxFont font = dc.GetFont();
884 	font.SetWeight(wxFONTWEIGHT_BOLD);
885 	fc.Set(font);
886 	dc.SetTextForeground(*wxWHITE);
887 	for (auto const& label : labels)
888 	{
889 		wxSize extent = dc.GetTextExtent(label.text);
890 		int left = RelativeXFromTime(label.range.begin());
891 		int width = AbsoluteXFromTime(label.range.length());
892 
893 		// If it doesn't fit, truncate
894 		if (width < extent.GetWidth())
895 		{
896 			dc.SetClippingRegion(left, audio_top + 4, width, extent.GetHeight());
897 			dc.DrawText(label.text, left, audio_top + 4);
898 			dc.DestroyClippingRegion();
899 		}
900 		// Otherwise center in the range
901 		else
902 		{
903 			dc.DrawText(label.text, left + (width - extent.GetWidth()) / 2, audio_top + 4);
904 		}
905 	}
906 }
907 
PaintTrackCursor(wxDC & dc)908 void AudioDisplay::PaintTrackCursor(wxDC &dc) {
909 	wxDCPenChanger penchanger(dc, wxPen(*wxWHITE));
910 	dc.DrawLine(track_cursor_pos-scroll_left, audio_top, track_cursor_pos-scroll_left, audio_top+audio_height);
911 
912 	if (track_cursor_label.empty()) return;
913 
914 	wxDCFontChanger fc(dc);
915 	wxFont font = dc.GetFont();
916 	wxString face_name = FontFace("Audio/Track Cursor");
917 	if (!face_name.empty())
918 		font.SetFaceName(face_name);
919 	font.SetWeight(wxFONTWEIGHT_BOLD);
920 	fc.Set(font);
921 
922 	wxSize label_size(dc.GetTextExtent(track_cursor_label));
923 	wxPoint label_pos(track_cursor_pos - scroll_left - label_size.x/2, audio_top + 2);
924 	label_pos.x = mid(2, label_pos.x, GetClientSize().GetWidth() - label_size.x - 2);
925 
926 	int old_bg_mode = dc.GetBackgroundMode();
927 	dc.SetBackgroundMode(wxTRANSPARENT);
928 
929 	// Draw border
930 	dc.SetTextForeground(wxColour(64, 64, 64));
931 	dc.DrawText(track_cursor_label, label_pos.x+1, label_pos.y+1);
932 	dc.DrawText(track_cursor_label, label_pos.x+1, label_pos.y-1);
933 	dc.DrawText(track_cursor_label, label_pos.x-1, label_pos.y+1);
934 	dc.DrawText(track_cursor_label, label_pos.x-1, label_pos.y-1);
935 
936 	// Draw fill
937 	dc.SetTextForeground(*wxWHITE);
938 	dc.DrawText(track_cursor_label, label_pos.x, label_pos.y);
939 	dc.SetBackgroundMode(old_bg_mode);
940 
941 	label_pos.x -= 2;
942 	label_pos.y -= 2;
943 	label_size.IncBy(4, 4);
944 	// If the rendered text changes size we have to draw it an extra time to make sure the entire thing was drawn
945 	bool need_extra_redraw = track_cursor_label_rect.GetSize() != label_size;
946 	track_cursor_label_rect.SetPosition(label_pos);
947 	track_cursor_label_rect.SetSize(label_size);
948 	if (need_extra_redraw)
949 		RefreshRect(track_cursor_label_rect, false);
950 }
951 
SetDraggedObject(AudioDisplayInteractionObject * new_obj)952 void AudioDisplay::SetDraggedObject(AudioDisplayInteractionObject *new_obj)
953 {
954 	dragged_object = new_obj;
955 
956 	if (dragged_object && !HasCapture())
957 		CaptureMouse();
958 	else if (!dragged_object && HasCapture())
959 		ReleaseMouse();
960 
961 	if (!dragged_object)
962 		audio_marker.reset();
963 }
964 
SetTrackCursor(int new_pos,bool show_time)965 void AudioDisplay::SetTrackCursor(int new_pos, bool show_time)
966 {
967 	if (new_pos == track_cursor_pos) return;
968 
969 	int old_pos = track_cursor_pos;
970 	track_cursor_pos = new_pos;
971 
972 	RefreshRect(wxRect(old_pos - scroll_left - 0, audio_top, 1, audio_height), false);
973 	RefreshRect(wxRect(new_pos - scroll_left - 0, audio_top, 1, audio_height), false);
974 
975 	// Make sure the old label gets cleared away
976 	RefreshRect(track_cursor_label_rect, false);
977 
978 	if (show_time)
979 	{
980 		agi::Time new_label_time = TimeFromAbsoluteX(track_cursor_pos);
981 		track_cursor_label = to_wx(new_label_time.GetAssFormatted());
982 		track_cursor_label_rect.x += new_pos - old_pos;
983 		RefreshRect(track_cursor_label_rect, false);
984 	}
985 	else
986 	{
987 		track_cursor_label_rect.SetSize(wxSize(0,0));
988 		track_cursor_label.Clear();
989 	}
990 }
991 
RemoveTrackCursor()992 void AudioDisplay::RemoveTrackCursor()
993 {
994 	SetTrackCursor(-1, false);
995 }
996 
OnMouseEnter(wxMouseEvent &)997 void AudioDisplay::OnMouseEnter(wxMouseEvent&)
998 {
999 	if (OPT_GET("Audio/Auto/Focus")->GetBool())
1000 		SetFocus();
1001 }
1002 
OnMouseLeave(wxMouseEvent &)1003 void AudioDisplay::OnMouseLeave(wxMouseEvent&)
1004 {
1005 	if (!controller->IsPlaying())
1006 		RemoveTrackCursor();
1007 }
1008 
OnMouseEvent(wxMouseEvent & event)1009 void AudioDisplay::OnMouseEvent(wxMouseEvent& event)
1010 {
1011 	// If we have focus, we get mouse move events on Mac even when the mouse is
1012 	// outside our client rectangle, we don't want those.
1013 	if (event.Moving() && !GetClientRect().Contains(event.GetPosition()))
1014 	{
1015 		event.Skip();
1016 		return;
1017 	}
1018 
1019 	if (event.IsButton())
1020 		SetFocus();
1021 
1022 	const int mouse_x = event.GetPosition().x;
1023 
1024 	// Scroll the display after a mouse-up near one of the edges
1025 	if ((event.LeftUp() || event.RightUp()) && OPT_GET("Audio/Auto/Scroll")->GetBool())
1026 	{
1027 		const int width = GetClientSize().GetWidth();
1028 		if (mouse_x < width / 20) {
1029 			ScrollBy(-width / 3);
1030 		}
1031 		else if (width - mouse_x < width / 20) {
1032 			ScrollBy(width / 3);
1033 		}
1034 	}
1035 
1036 	if (ForwardMouseEvent(event))
1037 		return;
1038 
1039 	if (event.MiddleIsDown())
1040 	{
1041 		context->videoController->JumpToTime(TimeFromRelativeX(mouse_x), agi::vfr::EXACT);
1042 		return;
1043 	}
1044 
1045 	if (event.Moving() && !controller->IsPlaying())
1046 	{
1047 		SetTrackCursor(scroll_left + mouse_x, OPT_GET("Audio/Display/Draw/Cursor Time")->GetBool());
1048 	}
1049 
1050 	AudioTimingController *timing = controller->GetTimingController();
1051 	if (!timing) return;
1052 	const int drag_sensitivity = int(OPT_GET("Audio/Start Drag Sensitivity")->GetInt() * ms_per_pixel);
1053 	const int snap_sensitivity = OPT_GET("Audio/Snap/Enable")->GetBool() != event.ShiftDown() ? int(OPT_GET("Audio/Snap/Distance")->GetInt() * ms_per_pixel) : 0;
1054 
1055 	// Not scrollbar, not timeline, no button action
1056 	if (event.Moving())
1057 	{
1058 		const int timepos = TimeFromRelativeX(mouse_x);
1059 
1060 		if (timing->IsNearbyMarker(timepos, drag_sensitivity, event.AltDown()))
1061 			SetCursor(wxCursor(wxCURSOR_SIZEWE));
1062 		else
1063 			SetCursor(wxNullCursor);
1064 		return;
1065 	}
1066 
1067 	const int old_scroll_pos = scroll_left;
1068 	if (event.LeftDown() || event.RightDown())
1069 	{
1070 		const int timepos = TimeFromRelativeX(mouse_x);
1071 		std::vector<AudioMarker*> markers = event.LeftDown()
1072 			? timing->OnLeftClick(timepos, event.CmdDown(), event.AltDown(), drag_sensitivity, snap_sensitivity)
1073 			: timing->OnRightClick(timepos, event.CmdDown(), drag_sensitivity, snap_sensitivity);
1074 
1075 		// Clicking should never result in the audio display scrolling
1076 		ScrollPixelToLeft(old_scroll_pos);
1077 
1078 		if (markers.size())
1079 		{
1080 			RemoveTrackCursor();
1081 			audio_marker = agi::make_unique<AudioMarkerInteractionObject>(markers, timing, this, (wxMouseButton)event.GetButton());
1082 			SetDraggedObject(audio_marker.get());
1083 			return;
1084 		}
1085 	}
1086 }
1087 
ForwardMouseEvent(wxMouseEvent & event)1088 bool AudioDisplay::ForwardMouseEvent(wxMouseEvent &event) {
1089 	// Handle any ongoing drag
1090 	if (dragged_object && HasCapture())
1091 	{
1092 		if (!dragged_object->OnMouseEvent(event))
1093 		{
1094 			scroll_timer.Stop();
1095 			SetDraggedObject(nullptr);
1096 			SetCursor(wxNullCursor);
1097 		}
1098 		return true;
1099 	}
1100 	else
1101 	{
1102 		// Something is wrong, we might have lost capture somehow.
1103 		// Fix state and pretend it didn't happen.
1104 		SetDraggedObject(nullptr);
1105 		SetCursor(wxNullCursor);
1106 	}
1107 
1108 	const wxPoint mousepos = event.GetPosition();
1109 	AudioDisplayInteractionObject *new_obj = nullptr;
1110 	// Check for scrollbar action
1111 	if (scrollbar->GetBounds().Contains(mousepos))
1112 	{
1113 		new_obj = scrollbar.get();
1114 	}
1115 	// Check for timeline action
1116 	else if (timeline->GetBounds().Contains(mousepos))
1117 	{
1118 		SetCursor(wxCursor(wxCURSOR_SIZEWE));
1119 		new_obj = timeline.get();
1120 	}
1121 	else
1122 	{
1123 		return false;
1124 	}
1125 
1126 	if (!controller->IsPlaying())
1127 		RemoveTrackCursor();
1128 	if (new_obj->OnMouseEvent(event))
1129 		SetDraggedObject(new_obj);
1130 	return true;
1131 }
1132 
OnKeyDown(wxKeyEvent & event)1133 void AudioDisplay::OnKeyDown(wxKeyEvent& event)
1134 {
1135 	hotkey::check("Audio", context, event);
1136 }
1137 
OnSize(wxSizeEvent &)1138 void AudioDisplay::OnSize(wxSizeEvent &)
1139 {
1140 	// We changed size, update the sub-controls' internal data and redraw
1141 	wxSize size = GetClientSize();
1142 
1143 	timeline->SetDisplaySize(wxSize(size.x, scrollbar->GetBounds().y));
1144 	scrollbar->SetDisplaySize(size);
1145 
1146 	if (controller->GetTimingController())
1147 	{
1148 		TimeRange sel(controller->GetTimingController()->GetPrimaryPlaybackRange());
1149 		scrollbar->SetSelection(AbsoluteXFromTime(sel.begin()), AbsoluteXFromTime(sel.length()));
1150 	}
1151 
1152 	audio_height = size.GetHeight();
1153 	audio_height -= scrollbar->GetBounds().GetHeight();
1154 	audio_height -= timeline->GetHeight();
1155 	audio_renderer->SetHeight(audio_height);
1156 
1157 	audio_top = timeline->GetHeight();
1158 
1159 	Refresh();
1160 }
1161 
OnFocus(wxFocusEvent &)1162 void AudioDisplay::OnFocus(wxFocusEvent &)
1163 {
1164 	// The scrollbar indicates focus so repaint that
1165 	RefreshRect(scrollbar->GetBounds(), false);
1166 }
1167 
GetDuration() const1168 int AudioDisplay::GetDuration() const
1169 {
1170 	if (!provider) return 0;
1171 	return (provider->GetNumSamples() * 1000 + provider->GetSampleRate() - 1) / provider->GetSampleRate();
1172 }
1173 
OnAudioOpen(agi::AudioProvider * provider)1174 void AudioDisplay::OnAudioOpen(agi::AudioProvider *provider)
1175 {
1176 	this->provider = provider;
1177 
1178 	if (!audio_renderer_provider)
1179 		ReloadRenderingSettings();
1180 
1181 	audio_renderer->SetAudioProvider(provider);
1182 	audio_renderer->SetCacheMaxSize(OPT_GET("Audio/Renderer/Spectrum/Memory Max")->GetInt() * 1024 * 1024);
1183 
1184 	timeline->ChangeAudio(GetDuration());
1185 
1186 	ms_per_pixel = 0;
1187 	SetZoomLevel(zoom_level);
1188 
1189 	Refresh();
1190 
1191 	if (provider)
1192 	{
1193 		if (connections.empty())
1194 		{
1195 			connections = agi::signal::make_vector({
1196 				controller->AddPlaybackPositionListener(&AudioDisplay::OnPlaybackPosition, this),
1197 				controller->AddPlaybackStopListener(&AudioDisplay::RemoveTrackCursor, this),
1198 				controller->AddTimingControllerListener(&AudioDisplay::OnTimingController, this),
1199 				OPT_SUB("Audio/Spectrum", &AudioDisplay::ReloadRenderingSettings, this),
1200 				OPT_SUB("Audio/Display/Waveform Style", &AudioDisplay::ReloadRenderingSettings, this),
1201 				OPT_SUB("Colour/Audio Display/Spectrum", &AudioDisplay::ReloadRenderingSettings, this),
1202 				OPT_SUB("Colour/Audio Display/Waveform", &AudioDisplay::ReloadRenderingSettings, this),
1203 				OPT_SUB("Audio/Renderer/Spectrum/Quality", &AudioDisplay::ReloadRenderingSettings, this),
1204 			});
1205 			OnTimingController();
1206 		}
1207 
1208 		last_sample_decoded = provider->GetDecodedSamples();
1209 		audio_load_position = -1;
1210 		audio_load_speed = 0;
1211 		audio_load_start_time = std::chrono::steady_clock::now();
1212 		if (last_sample_decoded != provider->GetNumSamples())
1213 			load_timer.Start(100);
1214 	}
1215 	else
1216 	{
1217 		connections.clear();
1218 	}
1219 }
1220 
OnTimingController()1221 void AudioDisplay::OnTimingController()
1222 {
1223 	AudioTimingController *timing_controller = controller->GetTimingController();
1224 	if (timing_controller)
1225 	{
1226 		timing_controller->AddMarkerMovedListener(&AudioDisplay::OnMarkerMoved, this);
1227 		timing_controller->AddUpdatedPrimaryRangeListener(&AudioDisplay::OnSelectionChanged, this);
1228 		timing_controller->AddUpdatedStyleRangesListener(&AudioDisplay::OnStyleRangesChanged, this);
1229 
1230 		OnStyleRangesChanged();
1231 		OnMarkerMoved();
1232 		OnSelectionChanged();
1233 	}
1234 }
1235 
OnPlaybackPosition(int ms)1236 void AudioDisplay::OnPlaybackPosition(int ms)
1237 {
1238 	int pixel_position = AbsoluteXFromTime(ms);
1239 	SetTrackCursor(pixel_position, false);
1240 
1241 	if (OPT_GET("Audio/Lock Scroll on Cursor")->GetBool())
1242 	{
1243 		int client_width = GetClientSize().GetWidth();
1244 		int edge_size = client_width / 20;
1245 		if (scroll_left > 0 && pixel_position < scroll_left + edge_size)
1246 		{
1247 			ScrollPixelToLeft(std::max(pixel_position - edge_size, 0));
1248 		}
1249 		else if (scroll_left + client_width < std::min(pixel_audio_width - 1, pixel_position + edge_size))
1250 		{
1251 			ScrollPixelToLeft(std::min(pixel_position - client_width + edge_size, pixel_audio_width - client_width - 1));
1252 		}
1253 	}
1254 }
1255 
OnSelectionChanged()1256 void AudioDisplay::OnSelectionChanged()
1257 {
1258 	TimeRange sel(controller->GetPrimaryPlaybackRange());
1259 	scrollbar->SetSelection(AbsoluteXFromTime(sel.begin()), AbsoluteXFromTime(sel.length()));
1260 
1261 	if (audio_marker)
1262 	{
1263 		if (!scroll_timer.IsRunning())
1264 		{
1265 			// If the dragged object is outside the visible area, start the
1266 			// scroll timer to shift it back into view
1267 			int rel_x = RelativeXFromTime(audio_marker->GetPosition());
1268 			if (rel_x < 0 || rel_x >= GetClientSize().GetWidth())
1269 			{
1270 				// 50ms is the default for this on Windows (hardcoded since
1271 				// wxSystemSettings doesn't expose DragScrollDelay etc.)
1272 				scroll_timer.Start(50, true);
1273 			}
1274 		}
1275 	}
1276 	else if (OPT_GET("Audio/Auto/Scroll")->GetBool() && sel.end() != 0)
1277 	{
1278 		ScrollTimeRangeInView(sel);
1279 	}
1280 
1281 	RefreshRect(scrollbar->GetBounds(), false);
1282 }
1283 
OnScrollTimer(wxTimerEvent & event)1284 void AudioDisplay::OnScrollTimer(wxTimerEvent &event)
1285 {
1286 	if (!audio_marker) return;
1287 
1288 	int rel_x = RelativeXFromTime(audio_marker->GetPosition());
1289 	int width = GetClientSize().GetWidth();
1290 
1291 	// If the dragged object is outside the visible area, scroll it into
1292 	// view with a 5% margin
1293 	if (rel_x < 0)
1294 	{
1295 		ScrollBy(rel_x - width / 20);
1296 	}
1297 	else if (rel_x >= width)
1298 	{
1299 		ScrollBy(rel_x - width + width / 20);
1300 	}
1301 }
1302 
OnStyleRangesChanged()1303 void AudioDisplay::OnStyleRangesChanged()
1304 {
1305 	if (!controller->GetTimingController()) return;
1306 
1307 	AudioStyleRangeMerger asrm;
1308 	controller->GetTimingController()->GetRenderingStyles(asrm);
1309 
1310 	style_ranges.clear();
1311 	style_ranges.insert(asrm.begin(), asrm.end());
1312 
1313 	RefreshRect(wxRect(0, audio_top, GetClientSize().GetWidth(), audio_height), false);
1314 }
1315 
OnMarkerMoved()1316 void AudioDisplay::OnMarkerMoved()
1317 {
1318 	RefreshRect(wxRect(0, audio_top, GetClientSize().GetWidth(), audio_height), false);
1319 }
1320