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