// Copyright (c) 2005, Rodrigo Braz Monteiro // Copyright (c) 2009-2010, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of the Aegisub Group nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // Aegisub Project http://www.aegisub.org/ // // $Id$ /// @file audio_display.cpp /// @brief Display audio in the main UI /// @ingroup audio_ui /// #include "config.h" #ifndef AGI_PRE #include #include #include #include #endif #include "ass_time.h" #include "audio_colorscheme.h" #include "audio_controller.h" #include "audio_display.h" #include "audio_renderer.h" #include "audio_renderer_spectrum.h" #include "audio_renderer_waveform.h" #include "audio_timing.h" #include "block_cache.h" #include "include/aegisub/audio_provider.h" #include "include/aegisub/context.h" #include "include/aegisub/hotkey.h" #include "main.h" #include "selection_controller.h" #include "utils.h" #include "video_context.h" class AudioDisplayScrollbar : public AudioDisplayInteractionObject { static const int height = 10; static const int min_width = 10; wxRect bounds; wxRect thumb; bool dragging; // user is dragging with the primary mouse button int data_length; // total amount of data in control int page_length; // amount of data in one page int position; // first item displayed int sel_start; // first data item in selection int sel_length; // number of data items in selection AudioDisplay *display; // Recalculate thumb bounds from position and length data void RecalculateThumb() { thumb.width = std::max(min_width, bounds.width * page_length / data_length); thumb.height = height; thumb.x = bounds.width * position / data_length; thumb.y = bounds.y; } public: AudioDisplayScrollbar(AudioDisplay *display) : dragging(false) , data_length(1) , page_length(1) , position(0) , sel_start(-1) , sel_length(0) , display(display) { } // The audio display has changed size void SetDisplaySize(const wxSize &display_size) { bounds.x = 0; bounds.y = display_size.y - height; bounds.width = display_size.x; bounds.height = height; page_length = display_size.x; RecalculateThumb(); } const wxRect & GetBounds() const { return bounds; } int GetPosition() const { return position; } int SetPosition(int new_position) { // These two conditionals can't be swapped, otherwise the position can become // negative if the entire data is shorter than one page. if (new_position + page_length >= data_length) new_position = data_length - page_length - 1; if (new_position < 0) new_position = 0; position = new_position; RecalculateThumb(); return position; } void SetSelection(int new_start, int new_length) { sel_start = new_start; sel_length = new_length; } void ChangeLengths(int new_data_length, int new_page_length) { data_length = new_data_length; page_length = new_page_length; RecalculateThumb(); } bool OnMouseEvent(wxMouseEvent &event) { if (event.LeftIsDown()) { const int thumb_left = event.GetPosition().x - thumb.width/2; const int data_length_less_page = data_length - page_length; const int shaft_length_less_thumb = bounds.width - thumb.width; display->ScrollPixelToLeft(data_length_less_page * thumb_left / shaft_length_less_thumb); dragging = true; } else if (event.LeftUp()) { dragging = false; } return dragging; } void Paint(wxDC &dc, bool has_focus) { wxColour light(89, 145, 220); wxColour dark(8, 4, 13); wxColour sel(65, 34, 103); if (has_focus) { light.Set(205, 240, 226); sel.Set(82, 107, 213); } dc.SetPen(wxPen(light)); dc.SetBrush(wxBrush(dark)); dc.DrawRectangle(bounds); if (sel_length > 0 && sel_start >= 0) { wxRect r; r.x = sel_start * bounds.width / data_length; r.y = bounds.y; r.width = sel_length * bounds.width / data_length; r.height = bounds.height; dc.SetPen(wxPen(sel)); dc.SetBrush(wxBrush(sel)); dc.DrawRectangle(r); } dc.SetPen(wxPen(light)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(bounds); dc.SetPen(wxPen(light)); dc.SetBrush(wxBrush(light)); dc.DrawRectangle(thumb); } }; class AudioDisplayTimeline : public AudioDisplayInteractionObject { int64_t num_samples; int samplerate; int samples_per_pixel; int pixel_left; wxRect bounds; wxPoint drag_lastpos; bool dragging; enum Scale { Sc_Millisecond, Sc_Centisecond, Sc_Decisecond, Sc_Second, Sc_Decasecond, Sc_Minute, Sc_Decaminute, Sc_Hour, Sc_Decahour, // If anyone needs this they should reconsider their project Sc_MAX = Sc_Decahour }; Scale scale_minor; int scale_major_modulo; // If minor_scale_mark_index % scale_major_modulo == 0 the mark is a major mark double scale_minor_divisor; // Absolute scale-mark index multiplied by this number gives sample index for scale mark AudioDisplay *display; public: AudioDisplayTimeline(AudioDisplay *_display) : num_samples(0) , samplerate(44100) , samples_per_pixel(1) , pixel_left(0) , dragging(false) , display(_display) { } int GetHeight() const { int width, height; display->GetTextExtent("0123456789:.", &width, &height); return height + 4; } void SetDisplaySize(const wxSize &display_size) { // The size is without anything that goes below the timeline (like scrollbar) bounds.width = display_size.x; bounds.height = GetHeight(); bounds.x = 0; bounds.y = 0; } const wxRect & GetBounds() const { return bounds; } void ChangeAudio(int64_t new_length, int new_samplerate) { num_samples = new_length; samplerate = new_samplerate; } void ChangeZoom(int new_pixel_samples) { samples_per_pixel = new_pixel_samples; // Pixels per second double px_sec = (double)samplerate / (double)samples_per_pixel; if (px_sec > 3000) { scale_minor = Sc_Millisecond; scale_minor_divisor = (double)samplerate / 1000; scale_major_modulo = 10; } else if (px_sec > 300) { scale_minor = Sc_Centisecond; scale_minor_divisor = (double)samplerate / 100; scale_major_modulo = 10; } else if (px_sec > 30) { scale_minor = Sc_Decisecond; scale_minor_divisor = (double)samplerate / 10; scale_major_modulo = 10; } else if (px_sec > 3) { scale_minor = Sc_Second; scale_minor_divisor = (double)samplerate; scale_major_modulo = 10; } else if (px_sec > 1.0/3.0) { scale_minor = Sc_Decasecond; scale_minor_divisor = (double)samplerate * 10; scale_major_modulo = 6; } else if (px_sec > 1.0/9.0) { scale_minor = Sc_Minute; scale_minor_divisor = (double)samplerate * 60; scale_major_modulo = 10; } else if (px_sec > 1.0/90.0) { scale_minor = Sc_Decaminute; scale_minor_divisor = (double)samplerate * 600; scale_major_modulo = 6; } else { scale_minor = Sc_Hour; scale_minor_divisor = (double)samplerate * 3600; scale_major_modulo = 10; } } void SetPosition(int new_pixel_left) { pixel_left = std::max(new_pixel_left, 0); } bool OnMouseEvent(wxMouseEvent &event) { if (event.LeftDown()) { drag_lastpos = event.GetPosition(); dragging = true; } else if (event.LeftIsDown()) { display->ScrollPixelToLeft(pixel_left - event.GetPosition().x + drag_lastpos.x); drag_lastpos = event.GetPosition(); dragging = true; } else if (event.LeftUp()) { dragging = false; } return dragging; } void Paint(wxDC &dc) { wxColour light(89, 145, 220); wxColour dark(8, 4, 13); int bottom = bounds.y + bounds.height; // Background dc.SetPen(wxPen(dark)); dc.SetBrush(wxBrush(dark)); dc.DrawRectangle(bounds); // Top line dc.SetPen(wxPen(light)); dc.DrawLine(bounds.x, bottom-1, bounds.x+bounds.width, bottom-1); // Prepare for writing text dc.SetTextBackground(dark); dc.SetTextForeground(light); // Figure out the first scale mark to show int64_t sample_left = pixel_left * samples_per_pixel; int next_scale_mark = (int)(sample_left / scale_minor_divisor); if (next_scale_mark * scale_minor_divisor < sample_left) next_scale_mark += 1; assert(next_scale_mark * scale_minor_divisor >= sample_left); // Draw scale marks int next_scale_mark_pos; int last_text_right = -1; int last_hour = -1, last_minute = -1; if (num_samples / samplerate < 3600) last_hour = 0; // Trick to only show hours if audio is longer than 1 hour do { next_scale_mark_pos = (int)(next_scale_mark * scale_minor_divisor / samples_per_pixel) - pixel_left; bool mark_is_major = next_scale_mark % scale_major_modulo == 0; if (mark_is_major) dc.DrawLine(next_scale_mark_pos, bottom-6, next_scale_mark_pos, bottom-1); else dc.DrawLine(next_scale_mark_pos, bottom-4, next_scale_mark_pos, bottom-1); // Print time labels on major scale marks if (mark_is_major && next_scale_mark_pos > last_text_right) { double mark_time = next_scale_mark * scale_minor_divisor / samplerate; int mark_hour = (int)(mark_time / 3600); int mark_minute = (int)(mark_time / 60) % 60; double mark_second = mark_time - mark_hour*3600 - mark_minute*60; wxString time_string; bool changed_hour = mark_hour != last_hour; bool changed_minute = mark_minute != last_minute; if (changed_hour) { time_string = wxString::Format("%d:%02d:", mark_hour, mark_minute); last_hour = mark_hour; last_minute = mark_minute; } else if (changed_minute) { time_string = wxString::Format("%d:", mark_minute); last_minute = mark_minute; } if (scale_minor >= Sc_Decisecond) time_string += wxString::Format("%02d", (int)mark_second); else if (scale_minor == Sc_Centisecond) time_string += wxString::Format("%02.1f", mark_second); else time_string += wxString::Format("%02.2f", mark_second); int tw, th; dc.GetTextExtent(time_string, &tw, &th); last_text_right = next_scale_mark_pos + tw; dc.DrawText(time_string, next_scale_mark_pos, 0); } next_scale_mark += 1; } while (next_scale_mark_pos < bounds.width); } }; class AudioMarkerInteractionObject : public AudioDisplayInteractionObject { // Object-pair being interacted with AudioMarker *marker; AudioTimingController *timing_controller; // Audio display drag is happening on AudioDisplay *display; // Audio controller managing it all AudioController *controller; // Mouse button used to initiate the drag wxMouseButton button_used; // Default to snapping to snappable markers bool default_snap; // Range in pixels to snap at int snap_range; public: AudioMarkerInteractionObject(AudioMarker *marker, AudioTimingController *timing_controller, AudioDisplay *display, AudioController *controller, wxMouseButton button_used) : marker(marker) , timing_controller(timing_controller) , display(display) , controller(controller) , button_used(button_used) { /// @todo Make these configurable snap_range = 5; default_snap = false; } virtual bool OnMouseEvent(wxMouseEvent &event) { if (event.Dragging()) { int64_t sample_pos = display->SamplesFromRelativeX(event.GetPosition().x); if (marker->CanSnap() && (default_snap != event.ShiftDown())) { SampleRange snap_sample_range( display->SamplesFromRelativeX(event.GetPosition().x - snap_range), display->SamplesFromRelativeX(event.GetPosition().x + snap_range)); const AudioMarker *snap_marker = 0; AudioMarkerVector potential_snaps; controller->GetMarkers(snap_sample_range, potential_snaps); for (AudioMarkerVector::iterator mi = potential_snaps.begin(); mi != potential_snaps.end(); ++mi) { if ((*mi)->CanSnap()) { if (!snap_marker) snap_marker = *mi; else if (tabs((*mi)->GetPosition() - sample_pos) < tabs(snap_marker->GetPosition() - sample_pos)) snap_marker = *mi; } } if (snap_marker) sample_pos = snap_marker->GetPosition(); } timing_controller->OnMarkerDrag(marker, sample_pos); } // We lose the marker drag if the button used to initiate it goes up return !event.ButtonUp(button_used); } }; AudioDisplay::AudioDisplay(wxWindow *parent, AudioController *controller, agi::Context *context) : wxWindow(parent, -1, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS|wxBORDER_SIMPLE) , context(context) , audio_renderer(new AudioRenderer) , provider(0) , controller(controller) , scrollbar(new AudioDisplayScrollbar(this)) , timeline(new AudioDisplayTimeline(this)) , dragged_object(0) , old_selection(0, 0) { scroll_left = 0; pixel_audio_width = 0; scale_amplitude = 1.0; track_cursor_pos = -1; slots.push_back(controller->AddAudioOpenListener(&AudioDisplay::OnAudioOpen, this)); slots.push_back(controller->AddAudioCloseListener(&AudioDisplay::OnAudioOpen, this, (AudioProvider*)0)); slots.push_back(controller->AddPlaybackPositionListener(&AudioDisplay::OnPlaybackPosition, this)); slots.push_back(controller->AddPlaybackStopListener(&AudioDisplay::RemoveTrackCursor, this)); slots.push_back(controller->AddTimingControllerListener(&AudioDisplay::Refresh, this, true, (const wxRect*)0)); slots.push_back(controller->AddMarkerMovedListener(&AudioDisplay::OnMarkerMoved, this)); slots.push_back(controller->AddSelectionChangedListener(&AudioDisplay::OnSelectionChanged, this)); OPT_SUB("Audio/Spectrum", &AudioDisplay::ReloadRenderingSettings, this); audio_renderer->SetAmplitudeScale(scale_amplitude); SetZoomLevel(0); ReloadRenderingSettings(); SetMinClientSize(wxSize(-1, 70)); SetBackgroundStyle(wxBG_STYLE_PAINT); SetThemeEnabled(false); Bind(wxEVT_LEFT_DOWN, &AudioDisplay::OnMouseEvent, this); Bind(wxEVT_MIDDLE_DOWN, &AudioDisplay::OnMouseEvent, this); Bind(wxEVT_RIGHT_DOWN, &AudioDisplay::OnMouseEvent, this); Bind(wxEVT_LEFT_UP, &AudioDisplay::OnMouseEvent, this); Bind(wxEVT_MIDDLE_UP, &AudioDisplay::OnMouseEvent, this); Bind(wxEVT_RIGHT_UP, &AudioDisplay::OnMouseEvent, this); Bind(wxEVT_MOTION, &AudioDisplay::OnMouseEvent, this); } AudioDisplay::~AudioDisplay() { } void AudioDisplay::ScrollBy(int pixel_amount) { ScrollPixelToLeft(scroll_left + pixel_amount); } void AudioDisplay::ScrollPixelToLeft(int pixel_position) { const int client_width = GetClientRect().GetWidth(); if (pixel_position + client_width >= pixel_audio_width) pixel_position = pixel_audio_width - client_width; if (pixel_position < 0) pixel_position = 0; scroll_left = pixel_position; scrollbar->SetPosition(scroll_left); timeline->SetPosition(scroll_left); Refresh(); } void AudioDisplay::ScrollPixelToCenter(int pixel_position) { ScrollPixelToLeft(pixel_position - GetClientRect().GetWidth()/2); } void AudioDisplay::ScrollSampleToLeft(int64_t sample_position) { ScrollPixelToLeft(AbsoluteXFromSamples(sample_position)); } void AudioDisplay::ScrollSampleToCenter(int64_t sample_position) { ScrollPixelToCenter(AbsoluteXFromSamples(sample_position)); } void AudioDisplay::ScrollSampleRangeInView(const SampleRange &range) { int client_width = GetClientRect().GetWidth(); int range_begin = AbsoluteXFromSamples(range.begin()); int range_end = AbsoluteXFromSamples(range.end()); int range_len = range_end - range_begin; // Is everything already in view? if (range_begin >= scroll_left && range_end <= scroll_left+client_width) return; // For the rest of the calculation, remove 5 % from each side of the client area. // The leftadjust is the amount to subtract from the final scroll_left value. int leftadjust = client_width / 20; client_width = client_width * 9 / 10; // The entire range can fit inside the view, center it if (range_len < client_width) { ScrollPixelToLeft(range_begin - (client_width-range_len)/2 - leftadjust); } // Range doesn't fit in view and we're viewing a middle part of it, just leave it alone else if (range_begin < scroll_left+leftadjust && range_end > scroll_left+leftadjust+client_width) { // nothing } // Right edge is in view, scroll it as far to the right as possible else if (range_end >= scroll_left+leftadjust && range_end < scroll_left+leftadjust+client_width) { ScrollPixelToLeft(range_end - client_width - leftadjust); } // Nothing is in view or the left edge is in view, scroll left edge as far to the left as possible else { ScrollPixelToLeft(range_begin - leftadjust); } } void AudioDisplay::SetZoomLevel(int new_zoom_level) { zoom_level = new_zoom_level; if (!provider) { pixel_samples = 1; return; } const int samples_per_second = provider ? provider->GetSampleRate() : 48000; const int base_pixels_per_second = 50; /// @todo Make this customisable const int base_samples_per_pixel = samples_per_second / base_pixels_per_second; const int factor = GetZoomLevelFactor(zoom_level); const int new_samples_per_pixel = std::max(1, 100 * base_samples_per_pixel / factor); if (pixel_samples != new_samples_per_pixel) { int client_width = GetClientSize().GetWidth(); int64_t center_sample = int64_t(scroll_left + client_width / 2) * pixel_samples; pixel_samples = new_samples_per_pixel; audio_renderer->SetSamplesPerPixel(pixel_samples); if (provider) pixel_audio_width = provider->GetNumSamples() / pixel_samples + 1; else pixel_audio_width = 1; scrollbar->ChangeLengths(pixel_audio_width, client_width); timeline->ChangeZoom(pixel_samples); ScrollSampleToCenter(center_sample); Refresh(); } } int AudioDisplay::GetZoomLevel() const { return zoom_level; } wxString AudioDisplay::GetZoomLevelDescription(int level) const { const int factor = GetZoomLevelFactor(level); const int base_pixels_per_second = 50; /// @todo Make this customisable along with the above const int second_pixels = 100 * base_pixels_per_second / factor; return wxString::Format(_("%d%%, %d pixel/second"), factor, second_pixels); } int AudioDisplay::GetZoomLevelFactor(int level) { int factor = 100; if (level > 0) { factor += 25 * level; } else if (level < 0) { if (level >= -5) factor += 10 * level; else if (level >= -11) factor = 50 + (level+5) * 5; else factor = 20 + level + 11; if (factor <= 0) factor = 1; } return factor; } void AudioDisplay::SetAmplitudeScale(float scale) { audio_renderer->SetAmplitudeScale(scale); Refresh(); } float AudioDisplay::GetAmplitudeScale() const { return audio_renderer->GetAmplitudeScale(); } void AudioDisplay::ReloadRenderingSettings() { if (OPT_GET("Audio/Spectrum")->GetBool()) { AudioSpectrumRenderer *audio_spectrum_renderer = new AudioSpectrumRenderer; int64_t spectrum_quality = OPT_GET("Audio/Renderer/Spectrum/Quality")->GetInt(); #ifdef WITH_FFTW // FFTW is so fast we can afford to upgrade quality by two levels spectrum_quality += 2; #endif spectrum_quality = mid(0, spectrum_quality, 5); // Quality indexes: 0 1 2 3 4 5 int spectrum_width[] = {8, 9, 9, 9, 10, 11}; int spectrum_distance[] = {8, 8, 7, 6, 6, 5}; audio_spectrum_renderer->SetResolution( spectrum_width[spectrum_quality], spectrum_distance[spectrum_quality]); audio_renderer_provider.reset(audio_spectrum_renderer); } else { audio_renderer_provider.reset(new AudioWaveformRenderer); } audio_renderer->SetRenderer(audio_renderer_provider.get()); Refresh(); } BEGIN_EVENT_TABLE(AudioDisplay, wxWindow) EVT_PAINT(AudioDisplay::OnPaint) EVT_SIZE(AudioDisplay::OnSize) EVT_SET_FOCUS(AudioDisplay::OnFocus) EVT_KILL_FOCUS(AudioDisplay::OnFocus) EVT_KEY_DOWN(AudioDisplay::OnKeyDown) END_EVENT_TABLE() struct RedrawSubregion { int x1, x2; bool selected; RedrawSubregion(int x1, int x2, bool selected) : x1(x1), x2(x2), selected(selected) { } }; void AudioDisplay::OnPaint(wxPaintEvent& event) { wxAutoBufferedPaintDC dc(this); if (!provider) { dc.SetBackground(*wxBLACK_BRUSH); dc.Clear(); return; } int client_width, client_height; GetClientSize(&client_width, &client_height); wxRect audio_bounds(0, audio_top, client_width, audio_height); const wxRect &scrollbar_bounds = scrollbar->GetBounds(); const wxRect &timeline_bounds = timeline->GetBounds(); bool redraw_scrollbar = false; bool redraw_timeline = false; /// @todo Get rendering style ranges from timing controller instead SampleRange sel_samples(controller->GetPrimaryPlaybackRange()); int selection_start = AbsoluteXFromSamples(sel_samples.begin()); int selection_end = AbsoluteXFromSamples(sel_samples.end()); wxPoint client_org = GetClientAreaOrigin(); for (wxRegionIterator region(GetUpdateRegion()); region; ++region) { wxRect updrect = region.GetRect(); // Work around wxMac issue, client border offsets update rectangles but does // not affect drawing coordinates. #ifdef __WXMAC__ updrect.x += client_org.x; updrect.y += client_org.y; #endif redraw_scrollbar |= scrollbar_bounds.Intersects(updrect); redraw_timeline |= timeline_bounds.Intersects(updrect); if (!audio_bounds.Intersects(updrect)) { continue; } // p1 -> p2 = before selection // p2 -> p3 = in selection // p3 -> p4 = after selection int p1 = scroll_left + updrect.x; int p2 = selection_start; int p3 = selection_end; int p4 = p1 + updrect.width; std::vector subregions; if (p1 < p2) subregions.push_back(RedrawSubregion(p1, std::min(p2, p4), false)); if (p4 > p2 && p1 < p3) subregions.push_back(RedrawSubregion(std::max(p1, p2), std::min(p3, p4), true)); if (p4 > p3) subregions.push_back(RedrawSubregion(std::max(p1, p3), p4, false)); int x = updrect.x; for (std::vector::iterator sr = subregions.begin(); sr != subregions.end(); ++sr) { audio_renderer->Render(dc, wxPoint(x, audio_top), sr->x1, sr->x2 - sr->x1, sr->selected); x += sr->x2 - sr->x1; } const int foot_size = 6; SampleRange updrectsamples( SamplesFromRelativeX(updrect.x - foot_size), SamplesFromRelativeX(updrect.x + updrect.width + foot_size)); // Draw markers on top of it all AudioMarkerVector markers; controller->GetMarkers(updrectsamples, markers); wxDCPenChanger pen_retainer(dc, wxPen()); wxDCBrushChanger brush_retainer(dc, wxBrush()); for (AudioMarkerVector::iterator marker_i = markers.begin(); marker_i != markers.end(); ++marker_i) { const AudioMarker *marker = *marker_i; dc.SetPen(marker->GetStyle()); int marker_x = RelativeXFromSamples(marker->GetPosition()); dc.DrawLine(marker_x, audio_top, marker_x, audio_top+audio_height); dc.SetBrush(wxBrush(marker->GetStyle().GetColour())); dc.SetPen(*wxTRANSPARENT_PEN); if (marker->GetFeet() & AudioMarker::Feet_Left) { wxPoint foot_top[3] = { wxPoint(-foot_size, 0), wxPoint(0, 0), wxPoint(0, foot_size) }; wxPoint foot_bot[3] = { wxPoint(-foot_size, 0), wxPoint(0, -foot_size), wxPoint(0, 0) }; dc.DrawPolygon(3, foot_top, marker_x, audio_top); dc.DrawPolygon(3, foot_bot, marker_x, audio_top+audio_height); } if (marker->GetFeet() & AudioMarker::Feet_Right) { wxPoint foot_top[3] = { wxPoint(foot_size, 0), wxPoint(0, 0), wxPoint(0, foot_size) }; wxPoint foot_bot[3] = { wxPoint(foot_size, 0), wxPoint(0, -foot_size), wxPoint(0, 0) }; dc.DrawPolygon(3, foot_top, marker_x, audio_top); dc.DrawPolygon(3, foot_bot, marker_x, audio_top+audio_height); } } // Draw labels std::vector labels; controller->GetLabels(updrectsamples, labels); if (!labels.empty()) { wxDCFontChanger fc(dc); wxFont font = dc.GetFont(); font.SetWeight(wxFONTWEIGHT_BOLD); dc.SetFont(font); dc.SetTextForeground(*wxWHITE); for (size_t i = 0; i < labels.size(); ++i) { wxSize extent = dc.GetTextExtent(labels[i].text); int left = RelativeXFromSamples(labels[i].range.begin()); int width = AbsoluteXFromSamples(labels[i].range.length()); // If it doesn't fit, truncate if (width < extent.GetWidth()) { dc.SetClippingRegion(left, audio_top + 4, width, extent.GetHeight()); dc.DrawText(labels[i].text, left, audio_top + 4); dc.DestroyClippingRegion(); } // Otherwise center in the range else { dc.DrawText(labels[i].text, left + (width - extent.GetWidth()) / 2, audio_top + 4); } } } } if (track_cursor_pos >= 0) { wxDCPenChanger penchanger(dc, wxPen(*wxWHITE)); dc.DrawLine(track_cursor_pos-scroll_left, audio_top, track_cursor_pos-scroll_left, audio_top+audio_height); if (!track_cursor_label.empty()) { wxDCFontChanger fc(dc); wxFont font = dc.GetFont(); font.SetWeight(wxFONTWEIGHT_BOLD); dc.SetFont(font); wxSize label_size(dc.GetTextExtent(track_cursor_label)); wxPoint label_pos(track_cursor_pos - scroll_left - label_size.x/2, audio_top + 2); if (label_pos.x < 2) label_pos.x = 2; if (label_pos.x + label_size.x >= client_width - 2) label_pos.x = client_width - label_size.x - 2; int old_bg_mode = dc.GetBackgroundMode(); dc.SetBackgroundMode(wxTRANSPARENT); // Draw border dc.SetTextForeground(wxColour(64, 64, 64)); dc.DrawText(track_cursor_label, label_pos.x+1, label_pos.y+1); dc.DrawText(track_cursor_label, label_pos.x+1, label_pos.y-1); dc.DrawText(track_cursor_label, label_pos.x-1, label_pos.y+1); dc.DrawText(track_cursor_label, label_pos.x-1, label_pos.y-1); // Draw fill dc.SetTextForeground(*wxWHITE); dc.DrawText(track_cursor_label, label_pos.x, label_pos.y); dc.SetBackgroundMode(old_bg_mode); label_pos.x -= 2; label_pos.y -= 2; label_size.IncBy(4, 4); // If the rendered text changes size we have to draw it an extra time to make sure the entire thing was drawn bool need_extra_redraw = track_cursor_label_rect.GetSize() != label_size; track_cursor_label_rect.SetPosition(label_pos); track_cursor_label_rect.SetSize(label_size); if (need_extra_redraw) RefreshRect(track_cursor_label_rect); } } if (redraw_scrollbar) scrollbar->Paint(dc, HasFocus()); if (redraw_timeline) timeline->Paint(dc); } void AudioDisplay::SetDraggedObject(AudioDisplayInteractionObject *new_obj) { // Special case for audio markers being dragged: they use a temporary wrapper object // which must be deleted when it is no longer used. delete dynamic_cast(dragged_object); dragged_object = new_obj; if (dragged_object && !HasCapture()) CaptureMouse(); else if (!dragged_object && HasCapture()) ReleaseMouse(); } void AudioDisplay::SetTrackCursor(int new_pos, bool show_time) { if (new_pos != track_cursor_pos) { int old_pos = track_cursor_pos; track_cursor_pos = new_pos; RefreshRect(wxRect(old_pos - scroll_left - 0, audio_top, 1, audio_height)); RefreshRect(wxRect(new_pos - scroll_left - 0, audio_top, 1, audio_height)); // Make sure the old label gets cleared away RefreshRect(track_cursor_label_rect); if (show_time) { AssTime new_label_time; new_label_time.SetMS(controller->MillisecondsFromSamples(SamplesFromAbsoluteX(track_cursor_pos))); track_cursor_label = new_label_time.GetASSFormated(); track_cursor_label_rect.x += new_pos - old_pos; RefreshRect(track_cursor_label_rect); } else { track_cursor_label_rect.SetSize(wxSize(0,0)); track_cursor_label.Clear(); } } } void AudioDisplay::RemoveTrackCursor() { SetTrackCursor(-1, false); } void AudioDisplay::OnMouseEvent(wxMouseEvent& event) { // If we have focus, we get mouse move events on Mac even when the mouse is // outside our client rectangle, we don't want those. if (event.Moving() && !GetClientRect().Contains(event.GetPosition())) { event.Skip(); return; } if (event.IsButton()) SetFocus(); // Handle any ongoing drag if (dragged_object && HasCapture()) { if (!dragged_object->OnMouseEvent(event)) { SetDraggedObject(0); SetCursor(wxNullCursor); } return; } else { // Something is wrong, we might have lost capture somehow. // Fix state and pretend it didn't happen. SetDraggedObject(0); SetCursor(wxNullCursor); } wxPoint mousepos = event.GetPosition(); // Check for scrollbar action if (scrollbar->GetBounds().Contains(mousepos)) { if (!controller->IsPlaying()) RemoveTrackCursor(); if (scrollbar->OnMouseEvent(event)) SetDraggedObject(scrollbar.get()); return; } // Check for timeline action if (timeline->GetBounds().Contains(mousepos)) { SetCursor(wxCursor(wxCURSOR_SIZEWE)); if (!controller->IsPlaying()) RemoveTrackCursor(); if (timeline->OnMouseEvent(event)) SetDraggedObject(timeline.get()); return; } AudioTimingController *timing = controller->GetTimingController(); int drag_sensitivity = pixel_samples*3; /// @todo Make this depend on configuration // Not scrollbar, not timeline, no button action if (event.Moving()) { if (timing) { int64_t samplepos = SamplesFromRelativeX(mousepos.x); if (timing->IsNearbyMarker(samplepos, drag_sensitivity)) SetCursor(wxCursor(wxCURSOR_SIZEWE)); else SetCursor(wxNullCursor); } if (!controller->IsPlaying()) SetTrackCursor(scroll_left + mousepos.x, OPT_GET("Audio/Display/Draw/Cursor Time")->GetBool()); } if (event.Leaving() && !controller->IsPlaying()) { RemoveTrackCursor(); } if (event.LeftDown() && timing) { int64_t samplepos = SamplesFromRelativeX(mousepos.x); AudioMarker *marker = timing->OnLeftClick(samplepos, drag_sensitivity); if (marker) { RemoveTrackCursor(); SetDraggedObject(new AudioMarkerInteractionObject(marker, timing, this, controller, wxMOUSE_BTN_LEFT)); return; } } if (event.RightDown() && timing) { int64_t samplepos = SamplesFromRelativeX(mousepos.x); AudioMarker *marker = timing->OnRightClick(samplepos, drag_sensitivity); if (marker) { RemoveTrackCursor(); SetDraggedObject(new AudioMarkerInteractionObject(marker, timing, this, controller, wxMOUSE_BTN_RIGHT)); return; } } if (event.MiddleDown()) { context->videoController->JumpToTime( controller->MillisecondsFromSamples(SamplesFromRelativeX(mousepos.x)), agi::vfr::EXACT); } } void AudioDisplay::OnKeyDown(wxKeyEvent& event) { if (!hotkey::check("Audio", context, event.GetKeyCode(), event.GetUnicodeKey(), event.GetModifiers())) event.Skip(); event.StopPropagation(); } void AudioDisplay::OnSize(wxSizeEvent &event) { // We changed size, update the sub-controls' internal data and redraw wxSize size = GetClientSize(); scrollbar->SetDisplaySize(size); timeline->SetDisplaySize(wxSize(size.x, scrollbar->GetBounds().y)); audio_height = size.GetHeight(); audio_height -= scrollbar->GetBounds().GetHeight(); audio_height -= timeline->GetHeight(); audio_renderer->SetHeight(audio_height); audio_top = timeline->GetHeight(); Refresh(); } void AudioDisplay::OnFocus(wxFocusEvent &event) { // The scrollbar indicates focus so repaint that RefreshRect(scrollbar->GetBounds()); } void AudioDisplay::OnAudioOpen(AudioProvider *_provider) { provider = _provider; audio_renderer->SetAudioProvider(provider); audio_renderer->SetCacheMaxSize(OPT_GET("Audio/Renderer/Spectrum/Memory Max")->GetInt() * 1024 * 1024); if (provider) timeline->ChangeAudio(provider->GetNumSamples(), provider->GetSampleRate()); SetZoomLevel(zoom_level); Refresh(); } void AudioDisplay::OnPlaybackPosition(int64_t sample_position) { SetTrackCursor(AbsoluteXFromSamples(sample_position), false); } void AudioDisplay::OnSelectionChanged() { /// @todo Handle rendering style ranges from timing controller instead SampleRange sel(controller->GetPrimaryPlaybackRange()); scrollbar->SetSelection(AbsoluteXFromSamples(sel.begin()), AbsoluteXFromSamples(sel.length())); if (OPT_GET("Audio/Auto/Scroll")->GetBool()) ScrollSampleRangeInView(sel); int s1 = RelativeXFromSamples(sel.begin()); int e1 = RelativeXFromSamples(sel.end()); int s2 = RelativeXFromSamples(old_selection.begin()); int e2 = RelativeXFromSamples(old_selection.end()); if (sel.overlaps(old_selection)) { // Only redraw the parts of the selection that changed, to avoid flicker if (s1 != s2) { RefreshRect(wxRect(std::min(s1, s2)-10, audio_top, abs(s1-s2)+20, audio_height)); } if (e1 != e2) { RefreshRect(wxRect(std::min(e1, e2)-10, audio_top, abs(e1-e2)+20, audio_height)); } } else { RefreshRect(wxRect(s1 - 10, audio_top, e1 + 20, audio_height)); RefreshRect(wxRect(s2 - 10, audio_top, e2 + 20, audio_height)); } RefreshRect(scrollbar->GetBounds()); old_selection = sel; } void AudioDisplay::OnMarkerMoved() { /// @todo investigate if it's worth refreshing only the changed spots RefreshRect(wxRect(0, audio_top, GetClientSize().GetWidth(), audio_height)); }