diff --git a/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj b/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj index 8a613a2a1..2270786ff 100644 --- a/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj +++ b/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj @@ -47,6 +47,7 @@ MinimalRebuild="true" UsePrecompiledHeader="2" PrecompiledHeaderThrough="agi_pre.h" + DebugInformationFormat="3" DisableSpecificWarnings="4267" ForcedIncludeFiles="agi_pre.h" /> @@ -113,6 +114,7 @@ MinimalRebuild="true" UsePrecompiledHeader="2" PrecompiledHeaderThrough="agi_pre.h" + DebugInformationFormat="3" DisableSpecificWarnings="4267" ForcedIncludeFiles="agi_pre.h" /> @@ -715,6 +717,14 @@ RelativePath="..\..\src\aegisublocale.h" > + + + + @@ -1699,6 +1709,22 @@ RelativePath="..\..\src\audio_box.h" > + + + + + + + + @@ -1731,10 +1757,34 @@ RelativePath="..\..\src\audio_renderer_spectrum.h" > + + + + + + + + + + + + @@ -1799,6 +1849,18 @@ + + + + + + diff --git a/aegisub/build/aegisub_vs2008/compiler_options.vsprops b/aegisub/build/aegisub_vs2008/compiler_options.vsprops index 99bf7cc81..6c49214a3 100644 --- a/aegisub/build/aegisub_vs2008/compiler_options.vsprops +++ b/aegisub/build/aegisub_vs2008/compiler_options.vsprops @@ -6,6 +6,7 @@ > + #include #include +#include #include #include #include @@ -199,6 +200,7 @@ #include #include #include +#include #include #include #include diff --git a/aegisub/src/ass_exporter.cpp b/aegisub/src/ass_exporter.cpp index b9087db35..0589ba0df 100644 --- a/aegisub/src/ass_exporter.cpp +++ b/aegisub/src/ass_exporter.cpp @@ -39,6 +39,7 @@ #include "ass_export_filter.h" #include "ass_exporter.h" #include "ass_file.h" +#include "audio_controller.h" #include "frame_main.h" /// @brief Constructor diff --git a/aegisub/src/audio_box.cpp b/aegisub/src/audio_box.cpp index a632eb82a..38d1029b1 100644 --- a/aegisub/src/audio_box.cpp +++ b/aegisub/src/audio_box.cpp @@ -47,9 +47,13 @@ #include +#include "include/aegisub/audio_player.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" #include "audio_display.h" #include "audio_karaoke.h" +#include "audio_timing.h" #include "frame_main.h" #include "hotkeys.h" #include "include/aegisub/audio_player.h" @@ -58,60 +62,79 @@ #include "toggle_bitmap.h" #include "tooltip_manager.h" +// Stuff defines "min" and "max" as macros and breaks std::min and std::max in the process +#undef min +#undef max + + +enum AudioBoxControlIDs { + Audio_Scrollbar = 1600, + Audio_Horizontal_Zoom, + Audio_Vertical_Zoom, + Audio_Volume, + Audio_Sash, + Audio_Vertical_Link, + Audio_Button_Play, + Audio_Button_Stop, + Audio_Button_Prev, + Audio_Button_Next, + Audio_Button_Play_500ms_Before, + Audio_Button_Play_500ms_After, + Audio_Button_Play_500ms_First, + Audio_Button_Play_500ms_Last, + Audio_Button_Play_Row, + Audio_Button_Play_To_End, + Audio_Button_Commit, + Audio_Button_Karaoke, + Audio_Button_Goto, + + Audio_Button_Join, /// Karaoke -> Enter join mode. + Audio_Button_Split, /// Karaoke -> Enter split mode. + Audio_Button_Accept, /// Karaoke -> Split/Join mode -> Accept. + Audio_Button_Cancel, /// KAraoke -> Split/Join mode -> Cancel. + + Audio_Button_Leadin, + Audio_Button_Leadout, + + Audio_Check_AutoCommit, + Audio_Check_NextCommit, + Audio_Check_AutoGoto, + Audio_Check_Medusa, + Audio_Check_Spectrum +}; + + /// @brief Constructor /// @param parent /// -AudioBox::AudioBox(wxWindow *parent, SubtitlesGrid *grid) : -wxPanel(parent,-1,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL|wxBORDER_RAISED) +AudioBox::AudioBox(wxWindow *parent, AudioController *_controller, SelectionController *selection_controller) +: wxPanel(parent,-1,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL|wxBORDER_RAISED) +, selection_controller(selection_controller) +, controller(_controller) { // Setup - loaded = false; karaokeMode = false; // Sash and Display - audioScroll = new wxScrollBar(this,Audio_Scrollbar); - audioScroll->PushEventHandler(new FocusEvent()); - audioScroll->SetToolTip(_("Seek bar")); - Sash = new wxSashWindow(this,Audio_Sash,wxDefaultPosition,wxDefaultSize,wxCLIP_CHILDREN | wxSW_3DBORDER); - sashSizer = new wxBoxSizer(wxVERTICAL); - audioDisplay = new AudioDisplay(Sash, grid); - sashSizer->Add(audioDisplay,1,wxEXPAND,0); - Sash->SetSizer(sashSizer); - Sash->SetSashVisible(wxSASH_BOTTOM,true); - //Sash->SetSashBorder(wxSASH_BOTTOM,true); - Sash->SetMinimumSizeY(50); - audioDisplay->ScrollBar = audioScroll; - audioDisplay->box = this; - int _w,_h; - audioDisplay->GetSize(&_w,&_h); - audioDisplay->SetSizeHints(-1,_h,-1,_h); + audioDisplay = new AudioDisplay(this, controller); // Zoom - HorizontalZoom = new wxSlider(this,Audio_Horizontal_Zoom,50,0,100,wxDefaultPosition,wxSize(-1,20),wxSL_VERTICAL|wxSL_BOTH); - HorizontalZoom->PushEventHandler(new FocusEvent()); + HorizontalZoom = new wxSlider(this,Audio_Horizontal_Zoom,0,-50,30,wxDefaultPosition,wxSize(-1,20),wxSL_VERTICAL|wxSL_BOTH); HorizontalZoom->SetToolTip(_("Horizontal zoom")); VerticalZoom = new wxSlider(this,Audio_Vertical_Zoom,50,0,100,wxDefaultPosition,wxSize(-1,20),wxSL_VERTICAL|wxSL_BOTH|wxSL_INVERSE); - VerticalZoom->PushEventHandler(new FocusEvent()); VerticalZoom->SetToolTip(_("Vertical zoom")); VolumeBar = new wxSlider(this,Audio_Volume,50,0,100,wxDefaultPosition,wxSize(-1,20),wxSL_VERTICAL|wxSL_BOTH|wxSL_INVERSE); - VolumeBar->PushEventHandler(new FocusEvent()); VolumeBar->SetToolTip(_("Audio Volume")); bool link = OPT_GET("Audio/Link")->GetBool(); if (link) { VolumeBar->SetValue(VerticalZoom->GetValue()); VolumeBar->Enable(false); } - VerticalLink = new ToggleBitmap(this,Audio_Vertical_Link,GETIMAGE(toggle_audio_link_24)); + VerticalLink = new ToggleBitmap(this,Audio_Vertical_Link,GETIMAGE(toggle_audio_link_16)); VerticalLink->SetToolTip(_("Link vertical zoom and volume sliders")); VerticalLink->SetValue(link); - // Display sizer - DisplaySizer = new wxBoxSizer(wxVERTICAL); - //DisplaySizer->Add(audioDisplay,1,wxEXPAND,0); - DisplaySizer->Add(Sash,0,wxEXPAND,0); - DisplaySizer->Add(audioScroll,0,wxEXPAND,0); - // VertVol sider wxSizer *VertVol = new wxBoxSizer(wxHORIZONTAL); VertVol->Add(VerticalZoom,1,wxEXPAND,0); @@ -122,99 +145,93 @@ wxPanel(parent,-1,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL|wxBORDER_RAISE // Top sizer TopSizer = new wxBoxSizer(wxHORIZONTAL); - TopSizer->Add(DisplaySizer,1,wxEXPAND,0); + TopSizer->Add(audioDisplay,1,wxEXPAND,0); TopSizer->Add(HorizontalZoom,0,wxEXPAND,0); TopSizer->Add(VertVolArea,0,wxEXPAND,0); // Buttons sizer wxSizer *ButtonSizer = new wxBoxSizer(wxHORIZONTAL); wxButton *temp; - temp = new wxBitmapButton(this,Audio_Button_Prev,GETIMAGE(button_prev_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Prev,GETIMAGE(button_prev_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Previous line or syllable (%KEY%/%KEY%)"),_T("Audio Prev Line"),_T("Audio Prev Line Alt")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Next,GETIMAGE(button_next_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Next,GETIMAGE(button_next_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Next line/syllable (%KEY%/%KEY%)"),_T("Audio Next Line"),_T("Audio Next Line Alt")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Play,GETIMAGE(button_playsel_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play,GETIMAGE(button_playsel_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play selection (%KEY%/%KEY%)"),_T("Audio Play"),_T("Audio Play Alt")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Play_Row,GETIMAGE(button_playline_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play_Row,GETIMAGE(button_playline_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play current line (%KEY%)"),_T("Audio Play Original Line")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Stop,GETIMAGE(button_stop_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Stop,GETIMAGE(button_stop_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Stop (%KEY%)"),_T("Audio Stop")); ButtonSizer->Add(temp,0,wxRIGHT,10); - temp = new wxBitmapButton(this,Audio_Button_Play_500ms_Before,GETIMAGE(button_playfivehbefore_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play_500ms_Before,GETIMAGE(button_playfivehbefore_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play 500 ms before selection (%KEY%)"),_T("Audio Play 500ms Before")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Play_500ms_After,GETIMAGE(button_playfivehafter_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play_500ms_After,GETIMAGE(button_playfivehafter_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play 500 ms after selection (%KEY%)"),_T("Audio Play 500ms after")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Play_500ms_First,GETIMAGE(button_playfirstfiveh_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play_500ms_First,GETIMAGE(button_playfirstfiveh_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play first 500ms of selection (%KEY%)"),_T("Audio Play First 500ms")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Play_500ms_Last,GETIMAGE(button_playlastfiveh_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play_500ms_Last,GETIMAGE(button_playlastfiveh_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play last 500ms of selection (%KEY%)"),_T("Audio Play Last 500ms")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Play_To_End,GETIMAGE(button_playtoend_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Play_To_End,GETIMAGE(button_playtoend_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Play from selection start to end of file (%KEY%)"),_T("Audio Play To End")); ButtonSizer->Add(temp,0,wxRIGHT,10); - temp = new wxBitmapButton(this,Audio_Button_Leadin,GETIMAGE(button_leadin_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Leadin,GETIMAGE(button_leadin_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Add lead in (%KEY%)"),_T("Audio Add Lead In")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Leadout,GETIMAGE(button_leadout_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Leadout,GETIMAGE(button_leadout_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Add lead out (%KEY%)"),_T("Audio Add Lead Out")); ButtonSizer->Add(temp,0,wxRIGHT,10); - temp = new wxBitmapButton(this,Audio_Button_Commit,GETIMAGE(button_audio_commit_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Commit,GETIMAGE(button_audio_commit_16),wxDefaultPosition,wxDefaultSize); ToolTipManager::Bind(temp,_("Commit changes (%KEY%/%KEY%)"),_T("Audio Commit (Stay)"),_T("Audio Commit Alt")); ButtonSizer->Add(temp,0,wxRIGHT,0); - temp = new wxBitmapButton(this,Audio_Button_Goto,GETIMAGE(button_audio_goto_24),wxDefaultPosition,wxSize(30,-1)); + temp = new wxBitmapButton(this,Audio_Button_Goto,GETIMAGE(button_audio_goto_16),wxDefaultPosition,wxDefaultSize); temp->SetToolTip(_("Go to selection")); ButtonSizer->Add(temp,0,wxRIGHT,10); - AutoCommit = new ToggleBitmap(this,Audio_Check_AutoCommit,GETIMAGE(toggle_audio_autocommit_24),wxSize(30,-1)); + AutoCommit = new ToggleBitmap(this,Audio_Check_AutoCommit,GETIMAGE(toggle_audio_autocommit_16), wxSize(20, -1)); AutoCommit->SetToolTip(_("Automatically commit all changes")); AutoCommit->SetValue(OPT_GET("Audio/Auto/Commit")->GetBool()); ButtonSizer->Add(AutoCommit,0,wxRIGHT | wxALIGN_CENTER | wxEXPAND,0); - NextCommit = new ToggleBitmap(this,Audio_Check_NextCommit,GETIMAGE(toggle_audio_nextcommit_24),wxSize(30,-1)); + NextCommit = new ToggleBitmap(this,Audio_Check_NextCommit,GETIMAGE(toggle_audio_nextcommit_16), wxSize(20, -1)); NextCommit->SetToolTip(_("Auto goes to next line on commit")); NextCommit->SetValue(OPT_GET("Audio/Next Line on Commit")->GetBool()); ButtonSizer->Add(NextCommit,0,wxRIGHT | wxALIGN_CENTER | wxEXPAND,0); - AutoScroll = new ToggleBitmap(this,Audio_Check_AutoGoto,GETIMAGE(toggle_audio_autoscroll_24),wxSize(30,-1)); + AutoScroll = new ToggleBitmap(this,Audio_Check_AutoGoto,GETIMAGE(toggle_audio_autoscroll_16), wxSize(20, -1)); AutoScroll->SetToolTip(_("Auto scrolls audio display to selected line")); AutoScroll->SetValue(OPT_GET("Audio/Auto/Scroll")->GetBool()); - ButtonSizer->Add(AutoScroll,0,wxRIGHT | wxALIGN_CENTER | wxEXPAND,0); - SpectrumMode = new ToggleBitmap(this,Audio_Check_Spectrum,GETIMAGE(toggle_audio_spectrum_24),wxSize(30,-1)); - SpectrumMode->SetToolTip(_("Spectrum analyzer mode")); - SpectrumMode->SetValue(OPT_GET("Audio/Spectrum")->GetBool()); - ButtonSizer->Add(SpectrumMode,0,wxRIGHT | wxALIGN_CENTER | wxEXPAND,0); - MedusaMode = new ToggleBitmap(this,Audio_Check_Medusa,GETIMAGE(toggle_audio_medusa_24),wxSize(30,-1)); - MedusaMode->SetToolTip(_("Enable Medusa-Style Timing Shortcuts")); - MedusaMode->SetValue(OPT_GET("Audio/Medusa Timing Hotkeys")->GetBool()); - ButtonSizer->Add(MedusaMode,0,wxRIGHT | wxALIGN_CENTER | wxEXPAND,0); + ButtonSizer->Add(AutoScroll,0,wxRIGHT | wxALIGN_CENTER | wxEXPAND,10); + ButtonSizer->AddStretchSpacer(1); + KaraokeButton = new wxBitmapToggleButton(this,Audio_Button_Karaoke,GETIMAGE(kara_mode_16),wxDefaultPosition,wxDefaultSize); + KaraokeButton->SetToolTip(_("Toggle karaoke mode")); + ButtonSizer->Add(KaraokeButton,0,wxRIGHT|wxEXPAND,0); + // Karaoke sizer karaokeSizer = new wxBoxSizer(wxHORIZONTAL); - KaraokeButton = new wxBitmapToggleButton(this,Audio_Button_Karaoke,GETIMAGE(kara_mode_24),wxDefaultPosition,wxSize(33,30)); - KaraokeButton->SetToolTip(_("Toggle karaoke mode")); - karaokeSizer->Add(KaraokeButton,0,wxRIGHT|wxEXPAND,0); JoinSplitSizer = new wxBoxSizer(wxHORIZONTAL); - JoinButton = new wxBitmapButton(this,Audio_Button_Join,GETIMAGE(kara_join_24),wxDefaultPosition,wxSize(33,30)); + JoinButton = new wxBitmapButton(this,Audio_Button_Join,GETIMAGE(kara_join_16),wxDefaultPosition,wxDefaultSize); JoinButton->SetToolTip(_("Join selected syllables")); - SplitButton = new wxBitmapButton(this,Audio_Button_Split,GETIMAGE(kara_split_24),wxDefaultPosition,wxSize(33,30)); + SplitButton = new wxBitmapButton(this,Audio_Button_Split,GETIMAGE(kara_split_16),wxDefaultPosition,wxDefaultSize); SplitButton->SetToolTip(_("Enter split-mode")); JoinSplitSizer->Add(JoinButton,0,wxRIGHT|wxEXPAND,0); JoinSplitSizer->Add(SplitButton,0,wxRIGHT|wxEXPAND,0); CancelAcceptSizer = new wxBoxSizer(wxHORIZONTAL); - CancelButton = new wxBitmapButton(this,Audio_Button_Cancel,GETIMAGE(kara_split_accept_24),wxDefaultPosition,wxSize(33,30)); + CancelButton = new wxBitmapButton(this,Audio_Button_Cancel,GETIMAGE(kara_split_accept_16),wxDefaultPosition,wxDefaultSize); CancelButton->SetToolTip(_("Commit splits and leave split-mode")); - AcceptButton = new wxBitmapButton(this,Audio_Button_Accept,GETIMAGE(kara_split_cancel_24),wxDefaultPosition,wxSize(33,30)); + AcceptButton = new wxBitmapButton(this,Audio_Button_Accept,GETIMAGE(kara_split_cancel_16),wxDefaultPosition,wxDefaultSize); AcceptButton->SetToolTip(_("Discard all splits and leave split-mode")); CancelAcceptSizer->Add(CancelButton,0,wxRIGHT|wxEXPAND,0); CancelAcceptSizer->Add(AcceptButton,0,wxRIGHT|wxEXPAND,0); @@ -225,70 +242,40 @@ wxPanel(parent,-1,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL|wxBORDER_RAISE audioKaraoke = new AudioKaraoke(this); audioKaraoke->box = this; audioKaraoke->display = audioDisplay; - audioDisplay->karaoke = audioKaraoke; karaokeSizer->Add(audioKaraoke,1,wxEXPAND,0); - SetKaraokeButtons(); // Decide which one to show or hide. - // Main sizer MainSizer = new wxBoxSizer(wxVERTICAL); - MainSizer->Add(TopSizer,0,wxEXPAND,0); - MainSizer->Add(ButtonSizer,0,wxEXPAND,0); - MainSizer->Add(new wxStaticLine(this),0,wxEXPAND|wxTOP|wxBOTTOM,2); - MainSizer->Add(karaokeSizer,0,wxEXPAND,0); + MainSizer->Add(TopSizer,1,wxEXPAND|wxALL,3); + MainSizer->Add(ButtonSizer,0,wxEXPAND|wxBOTTOM|wxLEFT|wxRIGHT,3); + //MainSizer->Add(new wxStaticLine(this),0,wxEXPAND|wxTOP|wxBOTTOM,2); + MainSizer->Add(karaokeSizer,0,wxEXPAND|wxBOTTOM|wxLEFT|wxRIGHT,3); + MainSizer->AddSpacer(3); //MainSizer->SetSizeHints(this); SetSizer(MainSizer); + SetKaraokeButtons(); // Decide which one to show or hide. + + timing_controller_dialogue = CreateDialogueTimingController(controller, selection_controller); + controller->SetTimingController(timing_controller_dialogue); } /// @brief Destructor /// -AudioBox::~AudioBox() { - audioScroll->PopEventHandler(true); - HorizontalZoom->PopEventHandler(true); - VerticalZoom->PopEventHandler(true); - VolumeBar->PopEventHandler(true); +AudioBox::~AudioBox() +{ } -/// @brief Set file -/// @param file -/// @param FromVideo -/// @return -/// -void AudioBox::SetFile(wxString file,bool FromVideo) { - LOG_D("audio/box") << "file=" << file << " FromVideo: " << FromVideo; - loaded = false; - - if (FromVideo) { - audioDisplay->SetFromVideo(); - loaded = audioDisplay->loaded; - audioName = _T("?video"); - } - - else { - audioDisplay->SetFile(file); - if (file != _T("")) loaded = audioDisplay->loaded; - audioName = file; - } - - LOG_D("audio/box") << "setting up acceleraters in frameMain"; - frameMain->SetAccelerators(); - LOG_D("audio/box") << "finished setting up accelerators in frameMain"; -} - - /////////////// // Event table BEGIN_EVENT_TABLE(AudioBox,wxPanel) - EVT_COMMAND_SCROLL(Audio_Scrollbar, AudioBox::OnScrollbar) EVT_COMMAND_SCROLL(Audio_Horizontal_Zoom, AudioBox::OnHorizontalZoom) EVT_COMMAND_SCROLL(Audio_Vertical_Zoom, AudioBox::OnVerticalZoom) EVT_COMMAND_SCROLL(Audio_Volume, AudioBox::OnVolume) - EVT_SASH_DRAGGED(Audio_Sash,AudioBox::OnSash) EVT_BUTTON(Audio_Button_Play, AudioBox::OnPlaySelection) EVT_BUTTON(Audio_Button_Play_Row, AudioBox::OnPlayDialogue) @@ -312,28 +299,19 @@ BEGIN_EVENT_TABLE(AudioBox,wxPanel) EVT_TOGGLEBUTTON(Audio_Vertical_Link, AudioBox::OnVerticalLink) EVT_TOGGLEBUTTON(Audio_Button_Karaoke, AudioBox::OnKaraoke) EVT_TOGGLEBUTTON(Audio_Check_AutoGoto,AudioBox::OnAutoGoto) - EVT_TOGGLEBUTTON(Audio_Check_Medusa,AudioBox::OnMedusaMode) - EVT_TOGGLEBUTTON(Audio_Check_Spectrum,AudioBox::OnSpectrumMode) EVT_TOGGLEBUTTON(Audio_Check_AutoCommit,AudioBox::OnAutoCommit) EVT_TOGGLEBUTTON(Audio_Check_NextCommit,AudioBox::OnNextLineCommit) END_EVENT_TABLE() -/// @brief Scrollbar changed -/// @param event -/// -void AudioBox::OnScrollbar(wxScrollEvent &event) { - audioDisplay->SetPosition(event.GetPosition()*12); -} - - - /// @brief Horizontal zoom bar changed /// @param event /// void AudioBox::OnHorizontalZoom(wxScrollEvent &event) { - audioDisplay->SetSamplesPercent(event.GetPosition()); + // Negate the value, we want zoom out to be on bottom and zoom in on top, + // but the control doesn't want negative on bottom and positive on top. + audioDisplay->SetZoomLevel(-event.GetPosition()); } @@ -346,9 +324,9 @@ void AudioBox::OnVerticalZoom(wxScrollEvent &event) { if (pos < 1) pos = 1; if (pos > 100) pos = 100; float value = pow(float(pos)/50.0f,3); - audioDisplay->SetScale(value); + audioDisplay->SetAmplitudeScale(value); if (VerticalLink->GetValue()) { - audioDisplay->player->SetVolume(value); + controller->SetVolume(value); VolumeBar->SetValue(pos); } } @@ -363,7 +341,7 @@ void AudioBox::OnVolume(wxScrollEvent &event) { int pos = event.GetPosition(); if (pos < 1) pos = 1; if (pos > 100) pos = 100; - audioDisplay->player->SetVolume(pow(float(pos)/50.0f,3)); + controller->SetVolume(pow(float(pos)/50.0f,3)); } } @@ -378,7 +356,7 @@ void AudioBox::OnVerticalLink(wxCommandEvent &event) { if (pos > 100) pos = 100; float value = pow(float(pos)/50.0f,3); if (VerticalLink->GetValue()) { - audioDisplay->player->SetVolume(value); + controller->SetVolume(value); VolumeBar->SetValue(pos); } VolumeBar->Enable(!VerticalLink->GetValue()); @@ -388,63 +366,11 @@ void AudioBox::OnVerticalLink(wxCommandEvent &event) { -/// @brief Sash -/// @param event -/// @return -/// -void AudioBox::OnSash(wxSashEvent& event) { - // OK? - if (event.GetDragStatus() == wxSASH_STATUS_OUT_OF_RANGE) return; - - // Recursion guard - static wxRecursionGuardFlag inside; - wxRecursionGuard guard(inside); - if (guard.IsInside()) { - return; - } - - // Get size - wxRect newSize = event.GetDragRect(); - int w = newSize.GetWidth(); - int h = newSize.GetHeight(); - if (h < 50) h = 50; - int oldh = audioDisplay->GetSize().GetHeight(); - if (oldh == h) return; - - // Resize - audioDisplay->SetSizeHints(w,h,-1,h); - audioDisplay->SetSize(w,h); - sashSizer->Layout(); - Sash->GetParent()->Layout(); - - // Store new size - OPT_SET("Audio/Display Height")->SetInt(h); - - // Fix layout - frameMain->Freeze(); - DisplaySizer->Layout(); - //TopSizer->Layout(); - //MainSizer->Layout(); - Layout(); - frameMain->ToolSizer->Layout(); - frameMain->MainSizer->Layout(); - frameMain->Layout(); - frameMain->Refresh(); - frameMain->Thaw(); - - //event.Skip(); -} - - - /// @brief Play selection /// @param event /// void AudioBox::OnPlaySelection(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesSelection(start,end); - audioDisplay->Play(start,end); + controller->PlayPrimaryRange(); } @@ -453,11 +379,9 @@ void AudioBox::OnPlaySelection(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPlayDialogue(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesDialogue(start,end); - audioDisplay->SetSelection(start, end); - audioDisplay->Play(start,end); + if (controller->GetTimingController()) + controller->GetTimingController()->Revert(); + controller->PlayPrimaryRange(); } @@ -466,8 +390,7 @@ void AudioBox::OnPlayDialogue(wxCommandEvent &event) { /// @param event /// void AudioBox::OnStop(wxCommandEvent &event) { - audioDisplay->SetFocus(); - audioDisplay->Stop(); + controller->Stop(); } @@ -476,9 +399,11 @@ void AudioBox::OnStop(wxCommandEvent &event) { /// @param event /// void AudioBox::OnNext(wxCommandEvent &event) { - audioDisplay->SetFocus(); - audioDisplay->Stop(); - audioDisplay->Next(); + //audioDisplay->SetFocus(); + controller->Stop(); + if (controller->GetTimingController()) + controller->GetTimingController()->Next(); + controller->PlayPrimaryRange(); } @@ -487,9 +412,11 @@ void AudioBox::OnNext(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPrev(wxCommandEvent &event) { - audioDisplay->SetFocus(); - audioDisplay->Stop(); - audioDisplay->Prev(); + //audioDisplay->SetFocus(); + controller->Stop(); + if (controller->GetTimingController()) + controller->GetTimingController()->Prev(); + controller->PlayPrimaryRange(); } @@ -498,10 +425,10 @@ void AudioBox::OnPrev(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPlay500Before(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesSelection(start,end); - audioDisplay->Play(start-500,start); + AudioController::SampleRange times(controller->GetPrimaryPlaybackRange()); + controller->PlayRange(AudioController::SampleRange( + times.begin() - controller->SamplesFromMilliseconds(500), + times.begin())); } @@ -510,10 +437,10 @@ void AudioBox::OnPlay500Before(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPlay500After(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesSelection(start,end); - audioDisplay->Play(end,end+500); + AudioController::SampleRange times(controller->GetPrimaryPlaybackRange()); + controller->PlayRange(AudioController::SampleRange( + times.end(), + times.end() + controller->SamplesFromMilliseconds(500))); } @@ -522,12 +449,12 @@ void AudioBox::OnPlay500After(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPlay500First(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesSelection(start,end); - int endp = start+500; - if (endp > end) endp = end; - audioDisplay->Play(start,endp); + AudioController::SampleRange times(controller->GetPrimaryPlaybackRange()); + controller->PlayRange(AudioController::SampleRange( + times.begin(), + times.begin() + std::min( + controller->SamplesFromMilliseconds(500), + times.length()))); } @@ -536,12 +463,12 @@ void AudioBox::OnPlay500First(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPlay500Last(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesSelection(start,end); - int startp = end-500; - if (startp < start) startp = start; - audioDisplay->Play(startp,end); + AudioController::SampleRange times(controller->GetPrimaryPlaybackRange()); + controller->PlayRange(AudioController::SampleRange( + times.end() - std::min( + controller->SamplesFromMilliseconds(500), + times.length()), + times.end())); } @@ -550,10 +477,7 @@ void AudioBox::OnPlay500Last(wxCommandEvent &event) { /// @param event /// void AudioBox::OnPlayToEnd(wxCommandEvent &event) { - int start=0,end=0; - audioDisplay->SetFocus(); - audioDisplay->GetTimesSelection(start,end); - audioDisplay->Play(start,-1); + controller->PlayToEnd(controller->GetPrimaryPlaybackRange().begin()); } @@ -566,7 +490,8 @@ void AudioBox::OnCommit(wxCommandEvent &event) { LOG_D("audio/box") << "OnCommit"; audioDisplay->SetFocus(); LOG_D("audio/box") << "has set focus, now committing changes"; - audioDisplay->CommitChanges(true); + /// @todo Commit changes and go to next line if appropriate + //audioDisplay->CommitChanges(true); LOG_D("audio/box") << "returning"; } @@ -586,7 +511,8 @@ void AudioBox::OnKaraoke(wxCommandEvent &event) { } karaokeMode = false; audioKaraoke->enabled = false; - audioDisplay->SetDialogue(); + /// @todo Replace this with changing timing controller + //audioDisplay->SetDialogue(); audioKaraoke->Refresh(false); } @@ -594,7 +520,8 @@ void AudioBox::OnKaraoke(wxCommandEvent &event) { LOG_D("audio/box") << "karaoke disabled, enabling"; karaokeMode = true; audioKaraoke->enabled = true; - audioDisplay->SetDialogue(); + /// @todo Replace this with changing timing controller + //audioDisplay->SetDialogue(); } SetKaraokeButtons(); @@ -618,15 +545,9 @@ void AudioBox::SetKaraokeButtons() { JoinButton->Enable(join); SplitButton->Enable(split); - if (audioKaraoke->splitting) { - karaokeSizer->Show(CancelAcceptSizer); - karaokeSizer->Hide(JoinSplitSizer); - karaokeSizer->Layout(); - } else { - karaokeSizer->Hide(CancelAcceptSizer); - karaokeSizer->Show(JoinSplitSizer); - karaokeSizer->Layout(); - } + + karaokeSizer->Show(CancelAcceptSizer, audioKaraoke->splitting); + karaokeSizer->Show(JoinSplitSizer, !audioKaraoke->splitting); } /// @brief Join button in karaoke mode @@ -671,7 +592,8 @@ void AudioBox::OnAccept(wxCommandEvent &event) { /// void AudioBox::OnGoto(wxCommandEvent &event) { audioDisplay->SetFocus(); - audioDisplay->MakeDialogueVisible(true); + if (controller->GetTimingController()) + audioDisplay->ScrollSampleRangeInView(controller->GetTimingController()->GetIdealVisibleSampleRange()); } @@ -706,26 +628,14 @@ void AudioBox::OnNextLineCommit(wxCommandEvent &event) { -/// @brief Medusa Mode -/// @param event -/// +/// @todo Put global audio hotkeys toggling into the menu bar +/* void AudioBox::OnMedusaMode(wxCommandEvent &event) { audioDisplay->SetFocus(); OPT_SET("Audio/Medusa Timing Hotkeys")->SetBool(MedusaMode->GetValue()); frameMain->SetAccelerators(); } - - - -/// @brief Spectrum Analyzer Mode -/// @param event -/// -void AudioBox::OnSpectrumMode(wxCommandEvent &event) { - OPT_SET("Audio/Spectrum")->SetBool(SpectrumMode->GetValue()); - audioDisplay->UpdateImage(false); - audioDisplay->SetFocus(); - audioDisplay->Refresh(false); -} +*/ @@ -734,7 +644,7 @@ void AudioBox::OnSpectrumMode(wxCommandEvent &event) { /// void AudioBox::OnLeadIn(wxCommandEvent &event) { audioDisplay->SetFocus(); - audioDisplay->AddLead(true,false); + //audioDisplay->AddLead(true,false); } @@ -743,21 +653,6 @@ void AudioBox::OnLeadIn(wxCommandEvent &event) { /// void AudioBox::OnLeadOut(wxCommandEvent &event) { audioDisplay->SetFocus(); - audioDisplay->AddLead(false,true); + //audioDisplay->AddLead(false,true); } - -////////////////////////////////////////// -// Focus event handling for the scrollbar -BEGIN_EVENT_TABLE(FocusEvent,wxEvtHandler) - EVT_SET_FOCUS(FocusEvent::OnSetFocus) -END_EVENT_TABLE() - - -/// @brief DOCME -/// @param event -/// -void FocusEvent::OnSetFocus(wxFocusEvent &event) { - wxWindow *previous = event.GetWindow(); - if (previous) previous->SetFocus(); -} diff --git a/aegisub/src/audio_box.h b/aegisub/src/audio_box.h index d847890ab..15d068bb3 100644 --- a/aegisub/src/audio_box.h +++ b/aegisub/src/audio_box.h @@ -54,9 +54,14 @@ #include #endif +#ifndef AGI_AUDIO_CONTROLLER_INCLUDED +#error You must include "audio_controller.h" before "audio_box.h" +#endif + ////////////// // Prototypes +class AssDialogue; class AudioDisplay; class AudioKaraoke; class FrameMain; @@ -66,18 +71,21 @@ class ToggleBitmap; -/// DOCME /// @class AudioBox -/// @brief DOCME -/// -/// DOCME +/// @brief Panel with audio playback and timing controls, also containing an AudioDisplay class AudioBox : public wxPanel { - friend class AudioDisplay; + /// @todo Get rid of this ASAP, currently required for FrameMain to be able to notify + /// audio display about renderer having changed. + friend class FrameMain; -private: - - /// DOCME - wxScrollBar *audioScroll; + /// The audio display in the box + AudioDisplay *audioDisplay; + + /// Selection controller used for timing controllers + SelectionController *selection_controller; + + /// The regular dalogue timing controller + AudioTimingController *timing_controller_dialogue; /// DOCME wxSlider *HorizontalZoom; @@ -100,9 +108,6 @@ private: /// DOCME wxSizer *DisplaySizer; - /// DOCME - wxSashWindow *Sash; - /// DOCME ToggleBitmap *VerticalLink; @@ -133,21 +138,13 @@ private: /// DOCME ToggleBitmap *NextCommit; - /// DOCME - ToggleBitmap *MedusaMode; - /// DOCME ToggleBitmap *AutoCommit; - /// DOCME - ToggleBitmap *SpectrumMode; - - void OnScrollbar(wxScrollEvent &event); void OnHorizontalZoom(wxScrollEvent &event); void OnVerticalZoom(wxScrollEvent &event); void OnVolume(wxScrollEvent &event); void OnVerticalLink(wxCommandEvent &event); - void OnSash(wxSashEvent &event); void OnPlaySelection(wxCommandEvent &event); void OnPlayDialogue(wxCommandEvent &event); @@ -171,14 +168,13 @@ private: void OnAutoGoto(wxCommandEvent &event); void OnAutoCommit(wxCommandEvent &event); - void OnMedusaMode(wxCommandEvent &event); - void OnSpectrumMode(wxCommandEvent &event); void OnNextLineCommit(wxCommandEvent &event); + public: - /// DOCME - AudioDisplay *audioDisplay; + /// The controller controlling this audio box + AudioController *controller; /// DOCME AudioKaraoke *audioKaraoke; @@ -189,124 +185,15 @@ public: /// DOCME FrameMain *frameMain; - /// DOCME - wxString audioName; - - /// DOCME - bool loaded; - /// DOCME bool karaokeMode; - AudioBox(wxWindow *parent, SubtitlesGrid *grid); + AudioBox(wxWindow *parent, AudioController *controller, SelectionController *selection_controller); ~AudioBox(); - void SetFile(wxString file,bool FromVideo); void SetKaraokeButtons(); DECLARE_EVENT_TABLE() }; - -/// DOCME -/// @class FocusEvent -/// @brief DOCME -/// -/// DOCME -class FocusEvent : public wxEvtHandler { - -private: - void OnSetFocus(wxFocusEvent &event); - DECLARE_EVENT_TABLE() -}; - - -/////// -// IDs -enum { - - /// DOCME - Audio_Scrollbar = 1600, - - /// DOCME - Audio_Horizontal_Zoom, - - /// DOCME - Audio_Vertical_Zoom, - - /// DOCME - Audio_Volume, - - /// DOCME - Audio_Sash, - - /// DOCME - Audio_Vertical_Link, - - /// DOCME - Audio_Button_Play, - - /// DOCME - Audio_Button_Stop, - - /// DOCME - Audio_Button_Prev, - - /// DOCME - Audio_Button_Next, - - /// DOCME - Audio_Button_Play_500ms_Before, - - /// DOCME - Audio_Button_Play_500ms_After, - - /// DOCME - Audio_Button_Play_500ms_First, - - /// DOCME - Audio_Button_Play_500ms_Last, - - /// DOCME - Audio_Button_Play_Row, - - /// DOCME - Audio_Button_Play_To_End, - - /// DOCME - Audio_Button_Commit, - - /// DOCME - Audio_Button_Karaoke, - - /// DOCME - Audio_Button_Goto, - - Audio_Button_Join, /// Karaoke -> Enter join mode. - Audio_Button_Split, /// Karaoke -> Enter split mode. - Audio_Button_Accept, /// Karaoke -> Split/Join mode -> Accept. - Audio_Button_Cancel, /// KAraoke -> Split/Join mode -> Cancel. - - /// DOCME - Audio_Button_Leadin, - - /// DOCME - Audio_Button_Leadout, - - - /// DOCME - Audio_Check_AutoCommit, - - /// DOCME - Audio_Check_NextCommit, - - /// DOCME - Audio_Check_AutoGoto, - - /// DOCME - Audio_Check_Medusa, - - /// DOCME - Audio_Check_Spectrum -}; diff --git a/aegisub/src/audio_colorscheme.cpp b/aegisub/src/audio_colorscheme.cpp new file mode 100644 index 000000000..6bd720053 --- /dev/null +++ b/aegisub/src/audio_colorscheme.cpp @@ -0,0 +1,79 @@ +// 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_colorscheme.cpp +/// @ingroup audio_ui +/// +/// Manage colour schemes for the audio display + +#include "config.h" + +#ifndef AGI_PRE +#include +#endif + +#include "audio_colorscheme.h" +#include "colorspace.h" + +// Something is defining "min" and "max" macros, and they interfere with using std::min and std::max +#undef min +#undef max + + + +void AudioColorScheme::InitIcyBlue_Normal() +{ + unsigned char *palptr = palette; + for (size_t i = 0; i <= factor; ++i) + { + float t = (float)i / factor; + int H = (int)(255 * (1.5 - t) / 2); + int S = (int)(255 * (0.5 + t/2)); + int L = std::min(255, (int)(128 * 2 * t)); + hsl_to_rgb(H, S, L, palptr + 0, palptr + 1, palptr + 2); + palptr += 4; + } +} + + +void AudioColorScheme::InitIcyBlue_Selected() +{ + unsigned char *palptr = palette; + for (size_t i = 0; i <= factor; ++i) + { + float t = (float)i / factor; + int H = (int)(255 * (1.5 - t) / 2); + int S = (int)(255 * (0.5 + t/2)); + int L = std::min(255, (int)(128 * (3 * t/2 + 0.5))); + hsl_to_rgb(H, S, L, palptr + 0, palptr + 1, palptr + 2); + palptr += 4; + } +} diff --git a/aegisub/src/audio_colorscheme.h b/aegisub/src/audio_colorscheme.h new file mode 100644 index 000000000..1e3d2c7f5 --- /dev/null +++ b/aegisub/src/audio_colorscheme.h @@ -0,0 +1,118 @@ +// 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_colorscheme.h +/// @see audio_colorscheme.cpp +/// @ingroup audio_ui +/// +/// Manage colour schemes for the audio display + + +#ifndef AGI_PRE +#include +#endif + + +/// @class AudioSpectrumColorMap +/// @brief Provides colour maps for audio display rendering +/// +/// Maps values from floats in range 0..1 into RGB colour values. +/// +/// First create an instance of this class, then call an initialisation function +/// in it to fill the palette with a colour map. +/// +/// @todo Let consumers of this class specify their own palette generation function. +class AudioColorScheme { + /// The palette data for the map + unsigned char *palette; + + /// Factor to multiply 0..1 values by to map them into the palette range + size_t factor; + +public: + /// @brief Constructor + /// @param prec Bit precision to create the colour map with + /// + /// Allocates the palette array to 2^prec entries + AudioColorScheme(int prec) + : palette(new unsigned char[(4< 1.0) val = 1.0; + // Find the colour in the palette + unsigned char *color = palette + ((int)(val*factor) * 4); + // Copy to the destination. + // Has to be done one byte at a time since we're writing RGB and not RGBX or RGBA + // data, and we otherwise write past the end of the pixel we're writing, possibly + // hitting adjacent memory blocks or just overwriting the start of the following + // scanline in the image. + // As the image is 24 bpp, 3 of every 4 uint32_t writes would be unaligned anyway. + pixel[0] = color[0]; + pixel[1] = color[1]; + pixel[2] = color[2]; + } + + /// @brief Get a floating point value's colour as a wxColour + /// @param val The value to map from + /// @return The corresponding wxColour + wxColour get(float val) + { + if (val < 0.0) val = 0.0; + if (val > 1.0) val = 1.0; + unsigned char *color = palette + ((int)(val*factor) * 4); + return wxColour(color[0], color[1], color[2]); + } +}; + diff --git a/aegisub/src/audio_controller.cpp b/aegisub/src/audio_controller.cpp new file mode 100644 index 000000000..acfbbab98 --- /dev/null +++ b/aegisub/src/audio_controller.cpp @@ -0,0 +1,568 @@ +// 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_controller.cpp +/// @brief Manage open audio and abstract state away from display +/// @ingroup audio_ui +/// + + +#include "config.h" + +#ifndef AGI_PRE +#include + +#include +#endif + +#include "selection_controller.h" +#include "audio_controller.h" +#include "include/aegisub/audio_provider.h" +#include "include/aegisub/audio_player.h" +#include "audio_provider_dummy.h" +#include "audio_timing.h" +#include "compat.h" +#include "video_context.h" + +class AudioMarkerKeyframe : public AudioMarker { + int64_t position; + static wxPen style; +public: + AudioMarkerKeyframe(int64_t position) : position(position) { } + int64_t GetPosition() const { return position; } + FeetStyle GetFeet() const { return Feet_None; } + bool CanSnap() const { return true; } + wxPen GetStyle() const + { + if (!style.IsOk()) + /// @todo Make this colour configurable + style = wxPen(wxColour(255,0,255), 1); + return style; + } + bool operator < (const AudioMarkerKeyframe &other) const { return position < other.position; } + operator int64_t() const { return position; } +}; +bool operator < (int64_t a, const AudioMarkerKeyframe &b) { return a < b.GetPosition(); } +bool operator < (const AudioMarkerKeyframe &a, int64_t b) { return a.GetPosition() < b; } +wxPen AudioMarkerKeyframe::style; + +class AudioMarkerProviderKeyframes : public AudioMarkerProvider, private AudioControllerAudioEventListener { + // GetMarkers needs to be const but still needs to modify this state, which is really + // just a cache... use the mutable "hack". + mutable int last_keyframes_revision; + mutable std::vector keyframe_samples; + AudioController *controller; + int64_t samplerate; + + void ReloadKeyframes() const + { + keyframe_samples.clear(); + + VideoContext *vc = VideoContext::Get(); + if (!vc) return; + + last_keyframes_revision = vc->GetKeyframesRevision(); + const std::vector &raw_keyframes = vc->GetKeyFrames(); + keyframe_samples.reserve(raw_keyframes.size()); + for (size_t i = 0; i < raw_keyframes.size(); ++i) + { + keyframe_samples.push_back(AudioMarkerKeyframe( + vc->TimeAtFrame(raw_keyframes[i]) * samplerate / 1000)); + } + std::sort(keyframe_samples.begin(), keyframe_samples.end()); + } + +private: + // AudioControllerAudioEventListener implementation + virtual void OnAudioOpen(AudioProvider *provider) + { + samplerate = provider->GetSampleRate(); + ReloadKeyframes(); + } + virtual void OnAudioClose() { } + virtual void OnPlaybackPosition(int64_t sample_position) { } + virtual void OnPlaybackStop() { } + +public: + AudioMarkerProviderKeyframes(AudioController *controller) + : controller(controller) + { + // Assume that a video context with keyframes revision 0 never has keyframes loaded + last_keyframes_revision = 0; + samplerate = 44100; + controller->AddAudioListener(this); + } + + virtual ~AudioMarkerProviderKeyframes() + { + controller->RemoveAudioListener(this); + } + + void GetMarkers(const AudioController::SampleRange &range, AudioMarkerVector &out) const + { + VideoContext *vc = VideoContext::Get(); + if (!vc) return; + + // Re-read keyframe data if the revision number changed, the keyframe data probably did too + if (vc->GetKeyframesRevision() != last_keyframes_revision) + ReloadKeyframes(); + + // Find first and last keyframes inside the range + std::vector::iterator a = std::lower_bound( + keyframe_samples.begin(), keyframe_samples.end(), range.begin()); + std::vector::iterator b = std::upper_bound( + keyframe_samples.begin(), keyframe_samples.end(), range.end()); + + // Place pointers to the markers in the output vector + for (; a != b; ++a) + out.push_back(&*a); + } +}; + + +/// Type of the audio event listener container in AudioController +typedef std::set AudioEventListenerSet; +/// Type of the timing event listener container in AudioController +typedef std::set TimingEventListenerSet; + +/// Macro to iterate audio event listeners in AudioController implementation +#define AUDIO_LISTENERS(listener) for (AudioEventListenerSet::iterator listener = audio_event_listeners.begin(); listener != audio_event_listeners.end(); ++listener) +/// Macro to iterate audio event listeners in AudioController implementation +#define TIMING_LISTENERS(listener) for (TimingEventListenerSet::iterator listener = timing_event_listeners.begin(); listener != timing_event_listeners.end(); ++listener) + + +AudioController::AudioController() +: player(0) +, provider(0) +, timing_controller(0) +, keyframes_marker_provider(new AudioMarkerProviderKeyframes(this)) +, playback_mode(PM_NotPlaying) +, playback_timer(this) +{ + Connect(playback_timer.GetId(), wxEVT_TIMER, (wxObjectEventFunction)&AudioController::OnPlaybackTimer); + +#ifdef wxHAS_POWER_EVENTS + Connect(wxEVT_POWER_SUSPENDED, (wxObjectEventFunction)&AudioController::OnComputerSuspending); + Connect(wxEVT_POWER_RESUME, (wxObjectEventFunction)&AudioController::OnComputerResuming); +#endif +} + + +AudioController::~AudioController() +{ + CloseAudio(); +} + + +void AudioController::OnPlaybackTimer(wxTimerEvent &event) +{ + int64_t pos = player->GetCurrentPosition(); + + if (!player->IsPlaying() || + (playback_mode != PM_ToEnd && pos >= player->GetEndPosition()+200)) + { + // The +200 is to allow the player to end the sound output cleanly, otherwise a popping + // artifact can sometimes be heard. + Stop(); + } + else + { + AUDIO_LISTENERS(l) + { + (*l)->OnPlaybackPosition(pos); + } + } +} + + +#ifdef wxHAS_POWER_EVENTS +void AudioController::OnComputerSuspending(wxPowerEvent &event) +{ + Stop(); + player->CloseStream(); +} + + +void AudioController::OnComputerResuming(wxPowerEvent &event) +{ + if (provider) + player->OpenStream(); +} +#endif + + +void AudioController::OpenAudio(const wxString &url) +{ + CloseAudio(); + + if (!url) + throw agi::InternalError("AudioController::OpenAudio() was passed an empty string. This must not happen.", 0); + + wxString path_part; + + if (url.StartsWith(_T("dummy-audio:"), &path_part)) + { + /* + * scheme ::= "dummy-audio" ":" signal-specifier "?" signal-parameters + * signal-specifier ::= "silence" | "noise" | "sine" "/" frequency + * frequency ::= integer + * signal-parameters ::= signal-parameter [ "&" signal-parameters ] + * signal-parameter ::= signal-parameter-name "=" integer + * signal-parameter-name ::= "sr" | "bd" | "ch" | "ln" + * + * Signal types: + * "silence", a silent signal is generated. + * "noise", a white noise signal is generated. + * "sine", a sine wave is generated at the specified frequency. + * + * Signal parameters: + * "sr", sample rate to generate signal at. + * "bd", bit depth to generate signal at (usually 16). + * "ch", number of channels to generate, usually 1 or 2. The same signal is generated + * in every channel even if one would be LFE. + * "ln", length of signal in samples. ln/sr gives signal length in seconds. + */ + provider = new DummyAudioProvider(5*30*60*1000, true); + } + else if (url.StartsWith(_T("video-audio:"), &path_part)) + { + /* + * scheme ::= "video-audio" ":" stream-type + * stream-type ::= "stream" | "cache" + * + * Stream types: + * + * "stream", the audio is streamed as required directly from the video provider, + * and cannot be used to drive an audio display. Seeking is unreliable. + * + * "cache", the entire audio is cached to memory or disk. Audio displays can be + * driven and seeking is reliable. Opening takes longer because the entire audio + * stream has to be decoded and stored. + */ + } + else if (url.StartsWith(_T("file:"), &path_part)) + { + /* + * scheme ::= "file" ":" "//" file-system-path + * + * On Unix-like systems, the file system path is regular. On Windows-systems, the + * path uses forward slashes instead of back-slashes and the drive letter is + * preceded by a slash. + * + * URL-encoding?? + */ + } + else + { + /* + * Assume it's not a URI but instead a filename in the platform's native format. + */ + wxFileName fn(url); + if (!fn.FileExists()) + { + agi::FileNotFoundError fnf(STD_STR(url)); + throw agi::AudioOpenError( + "Failed opening audio file (parsing as plain filename)", + &fnf); + } + provider = AudioProviderFactory::GetProvider(url); + } + + try + { + player = AudioPlayerFactory::GetAudioPlayer(); + player->SetProvider(provider); + player->OpenStream(); + } + catch (...) + { + delete player; + delete provider; + player = 0; + provider = 0; + throw; + } + + // Tell listeners about this. + AUDIO_LISTENERS(l) + { + (*l)->OnAudioOpen(provider); + } +} + + +void AudioController::CloseAudio() +{ + Stop(); + + delete player; + delete provider; + player = 0; + provider = 0; + + AUDIO_LISTENERS(l) + { + (*l)->OnAudioClose(); + } +} + + +bool AudioController::IsAudioOpen() const +{ + return player && provider; +} + + +wxString AudioController::GetAudioURL() const +{ + /// @todo figure out how to get the url + return _T(""); +} + + + +void AudioController::AddAudioListener(AudioControllerAudioEventListener *listener) +{ + audio_event_listeners.insert(listener); +} + + +void AudioController::RemoveAudioListener(AudioControllerAudioEventListener *listener) +{ + audio_event_listeners.erase(listener); +} + + +void AudioController::AddTimingListener(AudioControllerTimingEventListener *listener) +{ + timing_event_listeners.insert(listener); +} + + +void AudioController::RemoveTimingListener(AudioControllerTimingEventListener *listener) +{ + timing_event_listeners.erase(listener); +} + + + +void AudioController::SetTimingController(AudioTimingController *new_controller) +{ + delete timing_controller; + timing_controller = new_controller; + + TIMING_LISTENERS(l) + { + (*l)->OnTimingControllerChanged(); + } +} + + + +void AudioController::OnTimingControllerUpdatedPrimaryRange(AudioTimingController *sending_controller) +{ + assert(sending_controller != 0); + if (sending_controller != timing_controller) + return; + + if (playback_mode == PM_PrimaryRange) + { + player->SetEndPosition(timing_controller->GetPrimaryPlaybackRange().end()); + } + + TIMING_LISTENERS(l) + { + (*l)->OnSelectionChanged(); + } +} + + +void AudioController::OnTimingControllerUpdatedStyleRanges(AudioTimingController *sending_controller) +{ + assert(sending_controller != 0); + if (sending_controller != timing_controller) + return; + + /// @todo redraw and stuff, probably +} + + +void AudioController::OnTimingControllerMarkerMoved(AudioTimingController *sending_controller, AudioMarker *marker) +{ + assert(sending_controller != 0); + if (sending_controller != timing_controller) + return; + + /// @todo shouldn't this be more detailed? + TIMING_LISTENERS(l) + { + (*l)->OnMarkersMoved(); + } +} + + + +void AudioController::PlayRange(const AudioController::SampleRange &range) +{ + if (!IsAudioOpen()) return; + + player->Play(range.begin(), range.length()); + playback_mode = PM_Range; + playback_timer.Start(20); + + AUDIO_LISTENERS(l) + { + (*l)->OnPlaybackPosition(range.begin()); + } +} + + +void AudioController::PlayPrimaryRange() +{ + PlayRange(GetPrimaryPlaybackRange()); + if (playback_mode == PM_Range) + playback_mode = PM_PrimaryRange; +} + + +void AudioController::PlayToEnd(int64_t start_sample) +{ + if (!IsAudioOpen()) return; + + player->Play(start_sample, provider->GetNumSamples()-start_sample); + playback_mode = PM_ToEnd; + playback_timer.Start(20); + + AUDIO_LISTENERS(l) + { + (*l)->OnPlaybackPosition(start_sample); + } +} + + +void AudioController::Stop() +{ + if (!IsAudioOpen()) return; + + player->Stop(); + playback_mode = PM_NotPlaying; + playback_timer.Stop(); + + AUDIO_LISTENERS(l) + { + (*l)->OnPlaybackStop(); + } +} + + +bool AudioController::IsPlaying() +{ + return IsAudioOpen() && playback_mode != PM_NotPlaying; +} + + +int64_t AudioController::GetPlaybackPosition() +{ + if (!IsPlaying()) return 0; + + return player->GetCurrentPosition(); +} + + +void AudioController::ResyncPlaybackPosition(int64_t new_position) +{ + if (!IsPlaying()) return; + + player->SetCurrentPosition(new_position); +} + + +AudioController::SampleRange AudioController::GetPrimaryPlaybackRange() const +{ + if (timing_controller != 0) + { + return timing_controller->GetPrimaryPlaybackRange(); + } + else + { + return SampleRange(0, 0); + } +} + + +void AudioController::GetMarkers(const SampleRange &range, AudioMarkerVector &markers) const +{ + /// @todo Find all sources of markers + keyframes_marker_provider->GetMarkers(range, markers); +} + + +double AudioController::GetVolume() const +{ + if (!IsAudioOpen()) return 1.0; + return player->GetVolume(); +} + + +void AudioController::SetVolume(double volume) +{ + if (!IsAudioOpen()) return; + player->SetVolume(volume); +} + + +int64_t AudioController::SamplesFromMilliseconds(int64_t ms) const +{ + /// @todo There might be some subtle rounding errors here. + + if (!provider) return 0; + + int64_t sr = provider->GetSampleRate(); + + int64_t millisamples = ms * sr; + + return (millisamples + 999) / 1000; +} + + +int64_t AudioController::MillisecondsFromSamples(int64_t samples) const +{ + /// @todo There might be some subtle rounding errors here. + + if (!provider) return 0; + + int64_t sr = provider->GetSampleRate(); + + int64_t millisamples = samples * 1000; + + return millisamples / sr; +} + diff --git a/aegisub/src/audio_controller.h b/aegisub/src/audio_controller.h new file mode 100644 index 000000000..90db3015e --- /dev/null +++ b/aegisub/src/audio_controller.h @@ -0,0 +1,439 @@ +// 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_controller.h +/// @see audio_controller.cpp +/// @ingroup audio_ui + + +#ifndef AGI_PRE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#include + +#define AGI_AUDIO_CONTROLLER_INCLUDED 1 + + +class AudioPlayer; +class AudioProvider; + +// Declared below +class AudioControllerAudioEventListener; +class AudioControllerTimingEventListener; +class AudioTimingController; +class AudioMarker; +class AudioMarkerProvider; + + +typedef std::vector AudioMarkerVector; + + +/// @class AudioController +/// @brief Manage an open audio stream and UI state for it +/// +/// Keeps track of the UI interaction state of the open audio for a project, ie. what the current +/// selection is, what moveable markers are on the audio, and any secondary non-moveable markers +/// that are present. +/// +/// Changes in interaction are broadcast to all managed audio displays so they can redraw, and +/// the audio displays report all interactions back to the controller. There is a one to many +/// relationship between controller and audio displays. There is at most one audio controller +/// for an open subtitling project. +/// +/// Creates and destroys audio providers and players. This behaviour should at some point be moved +/// to a separate class, as it adds too many responsibilities to this class, but at the time of +/// writing, it would extend the scope of reworking components too much. +/// +/// There is not supposed to be a way to get direct access to the audio providers or players owned +/// by a controller. If some operation that isn't possible in the existing design is needed, the +/// controller should be extended in some way to allow it. +class AudioController : public wxEvtHandler { +public: + + /// @class SampleRange + /// @brief Represents an immutable range of audio samples + class SampleRange { + int64_t _begin; + int64_t _end; + + public: + /// @brief Constructor + /// @param begin Index of the first sample to include in the range + /// @param end Index of one past the last sample to include in the range + SampleRange(int64_t begin, int64_t end) + : _begin(begin) + , _end(end) + { + assert(end >= begin); + } + + /// @brief Copy constructor, optionally adjusting the range + /// @param src The range to duplicate + /// @param begin_adjust Number of samples to add to the start of the range + /// @param end_adjust Number of samples to add to the end of the range + SampleRange(const SampleRange &src, int64_t begin_adjust = 0, int64_t end_adjust = 0) + { + _begin = src._begin + begin_adjust; + _end = src._end + end_adjust; + assert(_end >= _begin); + } + + /// Get the number of samples in the range + int64_t length() const { return _end - _begin; } + /// Get the index of the first sample in the range + int64_t begin() const { return _begin; } + /// Get the index of one past the last sample in the range + int64_t end() const { return _end; } + + /// Determine whether the range contains a given sample index + bool contains(int64_t sample) const { return sample >= begin() && sample < end(); } + + /// Determine whether there is an overlap between two ranges + bool overlaps(const SampleRange &other) const + { + return other.contains(_begin) + || other.contains(_end) + || contains(other._begin) + || contains(other._end); + } + }; + + +private: + + /// Listeners for audio-related events + std::set audio_event_listeners; + + /// Listeners for timing-related events + std::set timing_event_listeners; + + /// The audio output object + AudioPlayer *player; + + /// The audio provider + AudioProvider *provider; + + /// The current timing mode, if any; owned by the audio controller + AudioTimingController *timing_controller; + + /// Provide keyframe data for audio displays + std::auto_ptr keyframes_marker_provider; + + + enum PlaybackMode { + PM_NotPlaying, + PM_Range, + PM_PrimaryRange, + PM_ToEnd + }; + /// The current playback mode + PlaybackMode playback_mode; + + + /// Timer used for playback position updates + wxTimer playback_timer; + + /// Event handler for the playback timer + void OnPlaybackTimer(wxTimerEvent &event); + + +#ifdef wxHAS_POWER_EVENTS + /// Handle computer going into suspend mode by stopping audio and closing device + void OnComputerSuspending(wxPowerEvent &event); + /// Handle computer resuming from suspend by re-opening the audio device + void OnComputerResuming(wxPowerEvent &event); +#endif + + +public: + + /// @brief Constructor + AudioController(); + + /// @brief Destructor + ~AudioController(); + + + /// @brief Open an audio stream + /// @param url URL of the stream to open + /// + /// The URL can either be a plain filename (with no qualifiers) or one + /// recognised by various providers. + void OpenAudio(const wxString &url); + + /// @brief Closes the current audio stream + void CloseAudio(); + + /// @brief Determine whether audio is currently open + /// @return True if an audio stream is open and can be played back + bool IsAudioOpen() const; + + /// @brief Get the URL for the current open audio stream + /// @return The URL for the audio stream + /// + /// The returned URL can be passed into OpenAudio() later to open the same stream again. + wxString GetAudioURL() const; + + + /// @brief Add an audio event listener + /// @param listener The listener to add + void AddAudioListener(AudioControllerAudioEventListener *listener); + + /// @brief Remove an audio event listener + /// @param listener The listener to remove + void RemoveAudioListener(AudioControllerAudioEventListener *listener); + + /// @brief Add a timing event listener + /// @param listener The listener to add + void AddTimingListener(AudioControllerTimingEventListener *listener); + + /// @brief Remove a timing event listener + /// @param listener The listener to remove + void RemoveTimingListener(AudioControllerTimingEventListener *listener); + + + /// @brief Start or restart audio playback, playing a range + /// @param range The range of audio to play back + /// + /// The end of the played back range may be requested changed, but is not changed + /// automatically from any other operations. + void PlayRange(const SampleRange &range); + + /// @brief Start or restart audio playback, playing the primary playback range + /// + /// If the primary playback range is updated during playback, the end of the + /// active playback range will be updated to match the new selection. The playback + /// end can not be changed in any other way. + void PlayPrimaryRange(); + + /// @brief Start or restart audio playback, playing from a point to the end of stream + /// @param start_sample Index of the sample to start playback at + /// + /// Playback to end cannot be converted to a range playback like range playback can, + /// it will continue until the end is reached, it is stopped, or restarted. + void PlayToEnd(int64_t start_sample); + + /// @brief Stop all audio playback + void Stop(); + + /// @brief Determine whether playback is ongoing + /// @return True if audio is being played back + bool IsPlaying(); + + /// @brief Get the current playback position + /// @return Approximate current sample index being heard by the user + /// + /// Returns 0 if playback is stopped. The return value is only approximate. + int64_t GetPlaybackPosition(); + + /// @brief If playing, restart playback from the specified position + /// @param new_position Sample index to restart playback from + /// + /// This function can be used to re-synchronise audio playback to another source that + /// might not be able to keep up with the full speed, such as video playback in high + /// resolution or with complex subtitles. + /// + /// This function only does something if audio is already playing. + void ResyncPlaybackPosition(int64_t new_position); + + + /// @brief Get the primary playback range + /// @return An immutable SampleRange object + SampleRange GetPrimaryPlaybackRange() const; + + /// @brief Get all static markers inside a range + /// @param range The sample range to retrieve markers for + /// @param markers Vector to fill found markers into + /// + /// The markers retrieved are static markers the user can't interact with. + /// Markers for user interaction are obtained through the timing controller. + void GetMarkers(const SampleRange &range, AudioMarkerVector &markers) const; + + + /// @brief Get the playback audio volume + /// @return The amplification factor for the audio + double GetVolume() const; + + /// @brief Set the playback audio volume + /// @param volume The new amplification factor for the audio + void SetVolume(double volume); + + + /// @brief Return the current audio provider + /// @return A const pointer to the current audio provider + const AudioProvider * GetAudioProvider() const { return provider; } + + + /// @brief Return the current timing controller + /// @return The current timing controller or 0 + AudioTimingController * GetTimingController() const { return timing_controller; } + + /// @brief Change the current timing controller + /// @param new_mode The new timing controller or 0. This may be the same object as + /// the current timing controller, to signal that the timing controller has changed + /// the object being timed, eg. changed to a new dialogue line. + void SetTimingController(AudioTimingController *new_controller); + + + /// @brief Timing controller signals primary playback range changed + /// @param timing_controller The timing controller sending this notification + /// + /// Only timing controllers should call this function. This function must be called + /// when the primary playback range is changed in the timing controller, usually + /// as a result of user interaction. + void OnTimingControllerUpdatedPrimaryRange(AudioTimingController *timing_controller); + + /// @brief Timing controller signals that the rendering style ranges have changed + /// @param timing_controller The timing controller sending this notification + /// + /// Only timing controllers should call this function. This function must be called + /// when one or more rendering style ranges have changed in the timing controller. + void OnTimingControllerUpdatedStyleRanges(AudioTimingController *timing_controller); + + /// @brief Timing controller signals that an audio marker has moved + /// @param timing_controller The timing controller sending this notification + /// @param marker The marker that was moved + /// + /// Only timing controllers should call this function. This function must be called + /// when a marker owned by the timing controller has been updated in some way. + void OnTimingControllerMarkerMoved(AudioTimingController *timing_controller, AudioMarker *marker); + + + /// @brief Convert a count of audio samples to a time in milliseconds + /// @param samples Sample count to convert + /// @return The number of milliseconds equivalent to the sample-count, rounded down + int64_t MillisecondsFromSamples(int64_t samples) const; + + /// @brief Convert a time in milliseconds to a count of audio samples + /// @param ms Time in milliseconds to convert + /// @return The index of the first sample that is wholly inside the millisecond + int64_t SamplesFromMilliseconds(int64_t ms) const; +}; + + + +/// @class AudioControllerAudioEventListener +/// @brief Abstract interface for objects that want audio events +class AudioControllerAudioEventListener { +public: + /// A new audio stream was opened (and any previously open was closed) + virtual void OnAudioOpen(AudioProvider *) = 0; + + /// The current audio stream was closed + virtual void OnAudioClose() = 0; + + /// Playback is in progress and ths current position was updated + virtual void OnPlaybackPosition(int64_t sample_position) = 0; + + /// Playback has stopped + virtual void OnPlaybackStop() = 0; +}; + + +/// @class AudioControllerTimingEventListener +/// @brief Abstract interface for objects that want audio timing events +class AudioControllerTimingEventListener { +public: + /// One or more moveable markers were moved + virtual void OnMarkersMoved() = 0; + + /// The selection was changed + virtual void OnSelectionChanged() = 0; + + /// The timing controller was replaced + virtual void OnTimingControllerChanged() = 0; +}; + + + +/// @class AudioMarkerProvider +/// @brief Abstract interface for audio marker providers +class AudioMarkerProvider { +public: + /// Virtual destructor, does nothing + virtual ~AudioMarkerProvider() { } + + /// @brief Return markers in a sample range + virtual void GetMarkers(const AudioController::SampleRange &range, AudioMarkerVector &out) const = 0; +}; + + + +/// @class AudioMarker +/// @brief A marker on the audio display +class AudioMarker { +public: + + /// Describe which directions a marker has feet in + enum FeetStyle { + Feet_None = 0, + Feet_Left, + Feet_Right, + Feet_Both // Conveniently Feet_Left|Feet_Right + }; + + /// @brief Get the marker's position + /// @return The marker's position in samples + virtual int64_t GetPosition() const = 0; + + /// @brief Get the marker's drawing style + /// @return A pen object describing the marker's drawing style + virtual wxPen GetStyle() const = 0; + + /// @brief Get the marker's feet style + /// @return The marker's feet style + virtual FeetStyle GetFeet() const = 0; + + /// @brief Retrieve whether this marker participates in snapping + /// @return True if this marker may snap to other snappable markers + /// + /// If a marker being dragged returns true from this method, and another marker which also + /// returns true from this method is within range, the marker being dragged will be positioned + /// at the position of the other marker if it is released while it is inside snapping range. + virtual bool CanSnap() const = 0; +}; + + + +namespace agi { + DEFINE_BASE_EXCEPTION(AudioControllerError, Exception); + DEFINE_SIMPLE_EXCEPTION(AudioOpenError, AudioControllerError, "audio_controller/open_failed"); +}; diff --git a/aegisub/src/audio_display.cpp b/aegisub/src/audio_display.cpp index 5a8c72995..47f7fdf21 100644 --- a/aegisub/src/audio_display.cpp +++ b/aegisub/src/audio_display.cpp @@ -1,4 +1,5 @@ // 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 @@ -32,2181 +33,1237 @@ /// @file audio_display.cpp /// @brief Display audio in the main UI /// @ingroup audio_ui +/// + /////////// // Headers #include "config.h" #ifndef AGI_PRE -#include +#include -#include - -#include -#include +#include +#include +#include #endif -#include "ass_file.h" -#include "audio_box.h" +#include "ass_time.h" +#include "audio_colorscheme.h" +#include "audio_controller.h" #include "audio_display.h" -#include "audio_karaoke.h" -#ifdef _DEBUG -#include "audio_provider_dummy.h" -#endif -#include "colorspace.h" -#include "compat.h" -#include "fft.h" -#include "hotkeys.h" +#include "block_cache.h" +#include "audio_renderer.h" +#include "audio_renderer_spectrum.h" +#include "audio_renderer_waveform.h" +#include "selection_controller.h" +#include "audio_timing.h" +#include "include/aegisub/audio_provider.h" #include "include/aegisub/audio_player.h" #include "main.h" -#include "standard_paths.h" -#include "subs_edit_box.h" -#include "subs_edit_ctrl.h" -#include "subs_grid.h" -#include "timeedit_ctrl.h" #include "utils.h" -#include "video_context.h" -#ifdef __WXMAC__ -/// DOCME -# define AudioDisplayWindowStyle wxWANTS_CHARS -#else +#undef min +#undef max -/// DOCME -# define AudioDisplayWindowStyle wxSUNKEN_BORDER | wxWANTS_CHARS -#endif -/// @brief Constructor -/// @param parent -AudioDisplay::AudioDisplay(wxWindow *parent, SubtitlesGrid *grid) -: wxWindow (parent, -1, wxDefaultPosition, wxSize(200,OPT_GET("Audio/Display Height")->GetInt()), AudioDisplayWindowStyle , _T("Audio Display")) -, grid(grid) -{ - // Set variables - origImage = NULL; - spectrumDisplay = NULL; - spectrumDisplaySelected = NULL; - spectrumRenderer = NULL; - ScrollBar = NULL; - dialogue = NULL; - karaoke = NULL; - peak = NULL; - min = NULL; - hasSel = false; - diagUpdated = false; - NeedCommit = false; - loaded = false; - temporary = false; - blockUpdate = false; - holding = false; - draggingScale = false; - Position = 0; - PositionSample = 0; - oldCurPos = 0; - scale = 1.0f; - provider = NULL; - player = NULL; - hold = 0; - samples = 0; - samplesPercent = 100; - hasFocus = (wxWindow::FindFocus() == this); - needImageUpdate = false; - needImageUpdateWeak = true; - playingToEnd = false; +class AudioDisplayScrollbar : public AudioDisplayInteractionObject { + static const int height = 10; - // Init - UpdateTimer.SetOwner(this,Audio_Update_Timer); - GetClientSize(&w,&h); - h -= OPT_GET("Audio/Display/Draw/Timeline")->GetBool() ? 20 : 0; - SetSamplesPercent(50,false); + wxRect bounds; + wxRect thumb; - VideoContext *vc = VideoContext::Get(); - vc->AddKeyframesOpenListener(&AudioDisplay::Update, this); - if (OPT_GET("Audio/Display/Draw/Video Position")->GetBool()) - vc->AddSeekListener(&AudioDisplay::UpdateImage, this, false); + bool dragging; // user is dragging with the primary mouse button - grid->AddSelectionListener(this); - commitListener = grid->ass->AddCommitListener(&AudioDisplay::OnCommit, this); + int data_length; // total amount of data in control + int page_length; // amount of data in one page + int position; // first item displayed - // Set cursor - //wxCursor cursor(wxCURSOR_BLANK); - //SetCursor(cursor); + int sel_start; // first data item in selection + int sel_length; // number of data items in selection - //wxLog::SetActiveTarget(new wxLogWindow(NULL,_T("Log"),true,false)); -} + AudioDisplay *display; -/// @brief Destructor -AudioDisplay::~AudioDisplay() { - if (player) player->CloseStream(); - delete provider; - delete player; - delete origImage; - delete spectrumRenderer; - delete spectrumDisplay; - delete spectrumDisplaySelected; - delete[] peak; - delete[] min; - provider = NULL; - player = NULL; - origImage = NULL; - spectrumRenderer = NULL; - spectrumDisplay = NULL; - spectrumDisplaySelected = NULL; - peak = NULL; - min = NULL; -} - -/// @brief Reset -void AudioDisplay::Reset() { - hasSel = false; - diagUpdated = false; - NeedCommit = false; - karaoke->enabled = false; - karaoke->syllables.clear(); - box->karaokeMode = false; - box->KaraokeButton->SetValue(false); - dialogue = NULL; -} - -/// @brief Update image -/// @param weak -void AudioDisplay::UpdateImage(bool weak) { - // Update samples - UpdateSamples(); - - // Set image as needing to be redrawn - needImageUpdate = true; - if (weak == false && needImageUpdateWeak == true) { - needImageUpdateWeak = false; - } - Refresh(false); -} - -/// @brief Actually update the image on the display -/// This is where most actual drawing of the audio display happens, or other functions -/// to draw specific parts are called from. -void AudioDisplay::DoUpdateImage() { - // Loaded? - if (!loaded || !provider) return; - - // Needs updating? - if (!needImageUpdate) return; - bool weak = needImageUpdateWeak; - - // Prepare bitmap - int timelineHeight = OPT_GET("Audio/Display/Draw/Timeline")->GetBool() ? 20 : 0; - int displayH = h+timelineHeight; - if (origImage) { - if (origImage->GetWidth() != w || origImage->GetHeight() != displayH) { - delete origImage; - origImage = NULL; - } + // Recalculate thumb bounds from position and length data + void RecalculateThumb() + { + thumb.width = std::max((height+1)/2, bounds.width * page_length / data_length); + thumb.height = height; + thumb.x = bounds.width * position / data_length; + thumb.y = bounds.y; } - // Options - bool draw_boundary_lines = OPT_GET("Audio/Display/Draw/Secondary Lines")->GetBool(); - bool draw_selection_background = OPT_GET("Audio/Display/Draw/Selection Background")->GetBool(); - bool drawKeyframes = OPT_GET("Audio/Display/Draw/Keyframes")->GetBool(); +public: - // Invalid dimensions - if (w == 0 || displayH == 0) return; - - // New bitmap - if (!origImage) origImage = new wxBitmap(w,displayH,-1); - - // Is spectrum? - bool spectrum = false; - if (provider && OPT_GET("Audio/Spectrum")->GetBool()) { - spectrum = true; + AudioDisplayScrollbar(AudioDisplay *_display) + : dragging(false) + , data_length(1) + , page_length(1) + , position(0) + , sel_start(-1) + , sel_length(0) + , display(_display) + { } - // Draw image to be displayed - wxMemoryDC dc; - dc.SelectObject(*origImage); - - // Black background - dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(wxBrush(lagi_wxColour(OPT_GET("Colour/Audio Display/Background/Background")->GetColour()))); - dc.DrawRectangle(0,0,w,h); - - // Selection position - hasSel = false; - hasKaraoke = karaoke->enabled; - selStart = 0; - selEnd = 0; - lineStart = 0; - lineEnd = 0; - selStartCap = 0; - selEndCap = 0; - int64_t drawSelStart = 0; - int64_t drawSelEnd = 0; - if (dialogue) { - GetDialoguePos(lineStart,lineEnd,false); - hasSel = true; - if (hasKaraoke) { - GetKaraokePos(selStartCap,selEndCap,true); - GetKaraokePos(drawSelStart,drawSelEnd,false); - selStart = lineStart; - selEnd = lineEnd; - } - else { - GetDialoguePos(selStartCap,selEndCap,true); - selStart = lineStart; - selEnd = lineEnd; - drawSelStart = lineStart; - drawSelEnd = lineEnd; - } + virtual ~AudioDisplayScrollbar() + { } - // Draw selection bg - if (hasSel && drawSelStart < drawSelEnd && draw_selection_background) { - if (NeedCommit && !karaoke->enabled) dc.SetBrush(wxBrush(lagi_wxColour(OPT_GET("Colour/Audio Display/Background/Selection Modified")->GetColour()))); - else dc.SetBrush(wxBrush(lagi_wxColour(OPT_GET("Colour/Audio Display/Background/Background")->GetColour()))); - dc.DrawRectangle(drawSelStart,0,drawSelEnd-drawSelStart,h); + // 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(); } - // Draw spectrum - if (spectrum) { - DrawSpectrum(dc,weak); + + const wxRect & GetBounds() const + { + return bounds; } - // Waveform - else if (provider) { - DrawWaveform(dc,weak); + int GetPosition() const + { + return position; } - // Nothing - else { - dc.DrawLine(0,h/2,w,h/2); - } + 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; - // Draw seconds boundaries - if (draw_boundary_lines) { - int64_t start = Position*samples; - int rate = provider->GetSampleRate(); - int pixBounds = rate / samples; - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Seconds Boundaries")->GetColour()),1,wxDOT)); - if (pixBounds >= 8) { - for (int x=0;xGetBool()) { - if (VideoContext::Get()->IsLoaded()) { - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Play Cursor")->GetColour()))); - int x = GetXAtMS(VideoContext::Get()->TimeAtFrame(VideoContext::Get()->GetFrameN())); - dc.DrawLine(x,0,x,h); - } - } - - // Draw keyframes - if (drawKeyframes && VideoContext::Get()->KeyFramesLoaded()) { - DrawKeyframes(dc); - } - - // Draw previous line - DrawInactiveLines(dc); - - if (hasSel) { - // Draw boundaries - if (true) { - // Draw start boundary - int selWidth = OPT_GET("Audio/Line Boundaries Thickness")->GetInt(); - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Line boundary Start")->GetColour()))); - dc.SetBrush(wxBrush(lagi_wxColour(OPT_GET("Colour/Audio Display/Line boundary Start")->GetColour()))); - dc.DrawRectangle(lineStart-selWidth/2+1,0,selWidth,h); - wxPoint points1[3] = { wxPoint(lineStart,0), wxPoint(lineStart+10,0), wxPoint(lineStart,10) }; - wxPoint points2[3] = { wxPoint(lineStart,h-1), wxPoint(lineStart+10,h-1), wxPoint(lineStart,h-11) }; - dc.DrawPolygon(3,points1); - dc.DrawPolygon(3,points2); - - // Draw end boundary - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Line boundary End")->GetColour()))); - dc.SetBrush(wxBrush(lagi_wxColour(OPT_GET("Colour/Audio Display/Line boundary End")->GetColour()))); - dc.DrawRectangle(lineEnd-selWidth/2+1,0,selWidth,h); - wxPoint points3[3] = { wxPoint(lineEnd,0), wxPoint(lineEnd-10,0), wxPoint(lineEnd,10) }; - wxPoint points4[3] = { wxPoint(lineEnd,h-1), wxPoint(lineEnd-10,h-1), wxPoint(lineEnd,h-11) }; - dc.DrawPolygon(3,points3); - dc.DrawPolygon(3,points4); + // This check is required to avoid mutual recursion with the display + if (new_position != position) + { + position = new_position; + RecalculateThumb(); + display->ScrollPixelToLeft(position); } - // Draw karaoke - if (hasKaraoke) { - try { - // Prepare - wxPen curPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Syllable Boundaries")->GetColour()),1,wxDOT); - dc.SetPen(curPen); - wxFont curFont(9,wxFONTFAMILY_DEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_BOLD,false,_T("Verdana"),wxFONTENCODING_SYSTEM); - dc.SetFont(curFont); - if (!spectrum) dc.SetTextForeground(lagi_wxColour(OPT_GET("Colour/Audio Display/Syllable Text")->GetColour())); - else dc.SetTextForeground(wxColour(255,255,255)); - size_t karn = karaoke->syllables.size(); - int64_t pos1,pos2; - int len,curpos; - wxCoord tw=0,th=0; - AudioKaraokeSyllable *curSyl; - wxString temptext; - - // Draw syllables - for (size_t i=0;isyllables.at(i); - len = curSyl->duration*10; - curpos = curSyl->start_time*10; - if (len != -1) { - pos1 = GetXAtMS(curStartMS+curpos); - pos2 = GetXAtMS(curStartMS+len+curpos); - dc.DrawLine(pos2,0,pos2,h); - temptext = curSyl->text; - temptext.Trim(true); - temptext.Trim(false); - GetTextExtent(temptext,&tw,&th,NULL,NULL,&curFont); - dc.DrawText(temptext,(pos1+pos2-tw)/2,4); - } - } - } - catch (...) { - // FIXME? - } - } + return position; } - // Modified text - if (NeedCommit) { - dc.SetFont(wxFont(9,wxDEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_BOLD,false,_T("Verdana"))); // FIXME: hardcoded font name - dc.SetTextForeground(wxColour(255,0,0)); - if (selStart <= selEnd) { - dc.DrawText(_T("Modified"),4,4); - } - else { - dc.DrawText(_T("Negative time"),4,4); - } + void SetSelection(int new_start, int new_length) + { + sel_start = new_start; + sel_length = new_length; } - // Draw timescale - if (timelineHeight) { - DrawTimescale(dc); + void ChangeLengths(int new_data_length, int new_page_length) + { + data_length = new_data_length; + page_length = new_page_length; + + RecalculateThumb(); } - // Draw selection border - if (hasFocus) { - dc.SetPen(*wxGREEN_PEN); + 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; + + SetPosition(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(0,0,w,h); + 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) + { } - // Done - needImageUpdate = false; - needImageUpdateWeak = true; -} - -/// @brief Draw other lines than the current active -/// @param dc The DC to draw to. -/// Draws markers for inactive lines, eg. the previous line, per configuration. -void AudioDisplay::DrawInactiveLines(wxDC &dc) { - // Check if there is anything to do - int shadeType = OPT_GET("Audio/Inactive Lines Display Mode")->GetInt(); - if (shadeType == 0) return; - - // Spectrum? - bool spectrum = false; - if (provider && OPT_GET("Audio/Spectrum")->GetBool()) { - spectrum = true; + virtual ~AudioDisplayTimeline() + { } - // Set options - dc.SetBrush(wxBrush(lagi_wxColour(OPT_GET("Colour/Audio Display/Line Boundary Inactive Line")->GetColour()))); - int selWidth = OPT_GET("Audio/Line Boundaries Thickness")->GetInt(); - AssDialogue *shade; - int shadeX1,shadeX2; - int shadeFrom,shadeTo; - - // Only previous - if (shadeType == 1) { - shadeFrom = this->line_n-1; - shadeTo = shadeFrom+1; + int GetHeight() const + { + int width, height; + display->GetTextExtent(_T("0123456789:."), &width, &height); + return height + 4; } - // All - else { - shadeFrom = 0; - shadeTo = grid->GetRows(); - } - - for (int j=shadeFrom;jGetDialogue(j); - - if (shade) { - // Get coordinates - shadeX1 = GetXAtMS(shade->Start.GetMS()); - shadeX2 = GetXAtMS(shade->End.GetMS()); - if (shadeX2 < 0 || shadeX1 > w) continue; - - // Draw over waveform - if (!spectrum) { - // Selection - int selX1 = MAX(0,GetXAtMS(curStartMS)); - int selX2 = MIN(w,GetXAtMS(curEndMS)); - - // Get ranges (x1->x2, x3->x4). - int x1 = MAX(0,shadeX1); - int x2 = MIN(w,shadeX2); - int x3 = MAX(x1,selX2); - int x4 = MAX(x2,selX2); - - // Clip first range - x1 = MIN(x1,selX1); - x2 = MIN(x2,selX1); - - // Set pen and draw - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Waveform Inactive")->GetColour()))); - for (int i=x1;iGetColour()))); - dc.DrawRectangle(shadeX1-selWidth/2+1,0,selWidth,h); - dc.DrawRectangle(shadeX2-selWidth/2+1,0,selWidth,h); - } - } -} - -/// @brief Draw keyframe markers -/// @param dc The DC to draw to. -void AudioDisplay::DrawKeyframes(wxDC &dc) { - std::vector KeyFrames = VideoContext::Get()->GetKeyFrames(); - int nKeys = (int)KeyFrames.size(); - dc.SetPen(wxPen(wxColour(255,0,255),1)); - - // Get min and max frames to care about - int minFrame = VideoContext::Get()->FrameAtTime(GetMSAtX(0),agi::vfr::START); - int maxFrame = VideoContext::Get()->FrameAtTime(GetMSAtX(w),agi::vfr::END); - - // Scan list - for (int i=0;i= minFrame && cur <= maxFrame) { - int x = GetXAtMS(VideoContext::Get()->TimeAtFrame(cur,agi::vfr::START)); - dc.DrawLine(x,0,x,h); - } - else if (cur > maxFrame) break; - } -} - -/// @brief Draw timescale at bottom of audio display -/// @param dc The DC to draw to. -void AudioDisplay::DrawTimescale(wxDC &dc) { - // Set size - int timelineHeight = OPT_GET("Audio/Display/Draw/Timeline")->GetBool() ? 20 : 0; - - // Set colours - dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.DrawRectangle(0,h,w,timelineHeight); - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT)); - dc.DrawLine(0,h,w,h); - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DHIGHLIGHT)); - dc.DrawLine(0,h+1,w,h+1); - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT)); - dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT)); - wxFont scaleFont; - scaleFont.SetFaceName(_T("Tahoma")); // FIXME: hardcoded font name - scaleFont.SetPointSize(8); - dc.SetFont(scaleFont); - - // Timescale ticks - int64_t start = Position*samples; - int rate = provider->GetSampleRate(); - for (int i=1;i<32;i*=2) { - int pixBounds = rate / (samples * 4 / i); - if (pixBounds >= 8) { - for (int x=0;x= 3600) { - s -= 3600; - hr++; - } - while (s >= 60) { - s -= 60; - m++; - } - wxString text; - if (hr) text = wxString::Format(_T("%i:%02i:%02i"),hr,m,s); - else if (m) text = wxString::Format(_T("%i:%02i"),m,s); - else text = wxString::Format(_T("%i"),s); - dc.GetTextExtent(text,&textW,&textH,NULL,NULL,&scaleFont); - dc.DrawText(text,MAX(0,x-textW/2)+1,h+8); - } - - // Other - else if (pos % (rate / 4 * i) < samples) { - dc.DrawLine(x,h+2,x,h+5); - } - } - break; - } - } -} - -/// @brief Draw audio waveform -/// @param dc The DC to draw to. -/// @param weak False if the visible portion of the display has changed. -void AudioDisplay::DrawWaveform(wxDC &dc,bool weak) { - // Prepare Waveform - if (!weak || peak == NULL || min == NULL) { - if (peak) delete[] peak; - if (min) delete[] min; - peak = new int[w]; - min = new int[w]; + 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; } - // Get waveform - if (!weak) { - provider->GetWaveForm(min,peak,Position*samples,w,h,samples,scale); + const wxRect & GetBounds() const + { + return bounds; } - // Draw pre-selection - if (!hasSel) selStartCap = w; - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Waveform")->GetColour()))); - for (int64_t i=0;iGetBool()) { - if (NeedCommit && !karaoke->enabled) dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Waveform Modified")->GetColour()))); - else dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Waveform Selected")->GetColour()))); - } - for (int64_t i=selStartCap;iGetColour()))); - for (int64_t i=selEndCap;iGetWidth() != w || spectrumDisplay->GetHeight() != h) { - if (spectrumDisplay) { - delete spectrumDisplay; - delete spectrumDisplaySelected; - spectrumDisplay = 0; - spectrumDisplaySelected = 0; - } - weak = false; - } - - if (!weak) { - if (!spectrumRenderer) - spectrumRenderer = new AudioSpectrum(provider); - - spectrumRenderer->SetScaling(scale); - - unsigned char *img = (unsigned char *)malloc(h*w*3); // wxImage requires using malloc - - // Use a slightly slower, but simple way - // Always draw the spectrum for the entire width - // Hack: without those divs by 2 the display is horizontally compressed - spectrumRenderer->RenderRange(Position*samples, (Position+w)*samples, false, img, 0, w, w, h); - - // The spectrum bitmap will have been deleted above already, so just make a new one - wxImage imgobj(w, h, img, false); - spectrumDisplay = new wxBitmap(imgobj); - } - - if (hasSel && selStartCap < selEndCap && !spectrumDisplaySelected) { - // There is a visible selection and we don't have a rendered one - // This should be done regardless whether we're "weak" or not - // Assume a few things were already set up when things were first rendered though - unsigned char *img = (unsigned char *)malloc(h*w*3); - spectrumRenderer->RenderRange(Position*samples, (Position+w)*samples, true, img, 0, w, w, h); - wxImage imgobj(w, h, img, false); - spectrumDisplaySelected = new wxBitmap(imgobj); - } - - // Draw - wxMemoryDC dc; - dc.SelectObject(*spectrumDisplay); - finaldc.Blit(0,0,w,h,&dc,0,0); - - if (hasSel && spectrumDisplaySelected && selStartCap < selEndCap) { - dc.SelectObject(*spectrumDisplaySelected); - finaldc.Blit(selStartCap, 0, selEndCap-selStartCap, h, &dc, selStartCap, 0); - } -} - -/// @brief Get selection position -/// @param selStart -/// @param selEnd -/// @param cap -void AudioDisplay::GetDialoguePos(int64_t &selStart,int64_t &selEnd, bool cap) { - selStart = GetXAtMS(curStartMS); - selEnd = GetXAtMS(curEndMS); - - if (cap) { - if (selStart < 0) selStart = 0; - if (selEnd < 0) selEnd = 0; - if (selStart >= w) selStart = w-1; - if (selEnd >= w) selEnd = w-1; - } -} - -/// @brief Get karaoke position -/// @param karStart -/// @param karEnd -/// @param cap -void AudioDisplay::GetKaraokePos(int64_t &karStart,int64_t &karEnd, bool cap) { - try { - // Wrap around - int nsyls = (int)karaoke->syllables.size(); - if (karaoke->curSyllable == -1) { - karaoke->SetSyllable(nsyls-1); - } - if (karaoke->curSyllable >= nsyls) karaoke->curSyllable = nsyls-1; - - // Get positions - int pos = karaoke->syllables.at(karaoke->curSyllable).start_time; - int len = karaoke->syllables.at(karaoke->curSyllable).duration; - karStart = GetXAtMS(curStartMS+pos*10); - karEnd = GetXAtMS(curStartMS+pos*10+len*10); - - // Cap - if (cap) { - if (karStart < 0) karStart = 0; - if (karEnd < 0) karEnd = 0; - if (karStart >= w) karStart = w-1; - if (karEnd >= w) karEnd = w-1; - } - } - catch (...) { - } -} - -/// @brief Update -/// @return -void AudioDisplay::Update() { - if (blockUpdate) return; - if (loaded) { - if (OPT_GET("Audio/Auto/Scroll")->GetBool()) - MakeDialogueVisible(); - else - UpdateImage(true); - } -} - -/// @brief Recreate the image -void AudioDisplay::RecreateImage() { - GetClientSize(&w,&h); - h -= OPT_GET("Audio/Display/Draw/Timeline")->GetBool() ? 20 : 0; - delete origImage; - origImage = NULL; - UpdateImage(false); -} - -/// @brief Make dialogue visible -/// @param force -void AudioDisplay::MakeDialogueVisible(bool force) { - // Variables - int startShow=0, endShow=0; - if (karaoke->enabled) { - // In karaoke mode the syllable and as much as possible towards the end of the line should be shown - int dummy = 0; - GetTimesSelection(startShow, dummy); - GetTimesDialogue(dummy, endShow); - } else { - GetTimesSelection(startShow,endShow); - } - int startPos = GetSampleAtMS(startShow); - int endPos = GetSampleAtMS(endShow); - int startX = GetXAtMS(startShow); - int endX = GetXAtMS(endShow); - - if (force || startX < 50 || endX > w-50) { - if (startX < 50 || endX - startX >= w) { - // Make sure the left edge of the selection is at least 50 pixels from the edge of the display - UpdatePosition(startPos - 50*samples, true); + 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 { - // Otherwise center the selection in display - UpdatePosition((startPos+endPos-w*samples)/2,true); + scale_minor = Sc_Hour; + scale_minor_divisor = (double)samplerate * 3600; + scale_major_modulo = 10; } } - // Update - UpdateImage(); -} + void SetPosition(int new_pixel_left) + { + if (new_pixel_left < 0) + new_pixel_left = 0; -/// @brief Set position -/// @param pos -void AudioDisplay::SetPosition(int pos) { - Position = pos; - PositionSample = pos * samples; - UpdateImage(); -} - -/// @brief Update position -/// @param pos -/// @param IsSample -/// @return -void AudioDisplay::UpdatePosition (int pos,bool IsSample) { - // Safeguards - if (!provider) return; - if (IsSample) pos /= samples; - int len = provider->GetNumSamples() / samples; - if (pos < 0) pos = 0; - if (pos >= len) pos = len-1; - - // Set - Position = pos; - PositionSample = pos*samples; - UpdateScrollbar(); -} - -/// @brief Note: aka Horizontal Zoom Set samples in percentage -/// @param percent -/// @param update -/// @param pivot -/// @return -void AudioDisplay::SetSamplesPercent(int percent,bool update,float pivot) { - // Calculate - if (percent < 1) percent = 1; - if (percent > 100) percent = 100; - if (samplesPercent == percent) return; - samplesPercent = percent; - - // Update - if (update) { - // Center scroll - int oldSamples = samples; - UpdateSamples(); - PositionSample += int64_t((oldSamples-samples)*w*pivot); - if (PositionSample < 0) PositionSample = 0; - - // Update - UpdateSamples(); - UpdateScrollbar(); - UpdateImage(); - Refresh(false); - } -} - -/// @brief Update samples -/// @return -void AudioDisplay::UpdateSamples() { - // Set samples - if (!provider) return; - if (w) { - int64_t totalSamples = provider->GetNumSamples(); - int total = totalSamples / w; - int max = 5760000 / w; // 2 minutes at 48 kHz maximum - if (total > max) total = max; - int min = 8; - if (total < min) total = min; - int range = total-min; - samples = int(range*pow(samplesPercent/100.0,3)+min); - - // Set position - int length = w * samples; - if (PositionSample + length > totalSamples) { - PositionSample = totalSamples - length; - if (PositionSample < 0) PositionSample = 0; - if (samples) Position = PositionSample / samples; - } - } -} - -/// @brief Set scale -/// @param _scale -/// @return -void AudioDisplay::SetScale(float _scale) { - if (scale == _scale) return; - scale = _scale; - UpdateImage(); -} - -/// @brief Load from file -/// @param file -/// @return -void AudioDisplay::SetFile(wxString file) { - // Unload - if (file.IsEmpty()) try { - try { - if (player) player->CloseStream(); - } - catch (const wxChar *e) { - wxLogError(e); - } - delete provider; - delete player; - delete spectrumRenderer; - provider = NULL; - player = NULL; - spectrumRenderer = NULL; - try { - Reset(); - } - catch (const wxChar *e) { - wxLogError(e); + if (new_pixel_left != pixel_left) + { + pixel_left = new_pixel_left; + display->ScrollPixelToLeft(pixel_left); } - loaded = false; - temporary = false; - StandardPaths::SetPathValue(_T("?audio"),_T("")); - } - catch (wxString e) { - wxLogError(e); - } - catch (const wxChar *e) { - wxLogError(e); - } - catch (...) { - wxLogError(_T("Unknown error unloading audio")); } - // Load - else { - SetFile(_T("")); - try { - // Get provider - bool is_dummy = false; -#ifdef _DEBUG - if (file == _T("?dummy")) { - is_dummy = true; - provider = new DummyAudioProvider(150*60*1000, false); // 150 minutes non-noise - } else if (file == _T("?noise")) { - is_dummy = true; - provider = new DummyAudioProvider(150*60*1000, true); // 150 minutes noise - } else { - provider = AudioProviderFactory::GetProvider(file); + bool OnMouseEvent(wxMouseEvent &event) + { + if (event.LeftDown()) + { + drag_lastpos = event.GetPosition(); + dragging = true; + } + else if (event.LeftIsDown()) + { + SetPosition(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(_T("%d:%02d:"), mark_hour, mark_minute); + last_hour = mark_hour; + last_minute = mark_minute; + } + else if (changed_minute) + { + time_string = wxString::Format(_T("%d:"), mark_minute); + last_minute = mark_minute; + } + if (scale_minor >= Sc_Decisecond) + time_string += wxString::Format(_T("%02d"), (int)mark_second); + else if (scale_minor == Sc_Centisecond) + time_string += wxString::Format(_T("%02.1f"), mark_second); + else + time_string += wxString::Format(_T("%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); } -#else - provider = AudioProviderFactory::GetProvider(file); + + next_scale_mark += 1; + + } while (next_scale_mark_pos < bounds.width); + } +}; + + + +class AudioMarkerInteractionObject : public AudioDisplayInteractionObject { + // Object-pair being intracted 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 ~AudioMarkerInteractionObject() + { + } + + virtual bool OnMouseEvent(wxMouseEvent &event) + { + if (event.Dragging()) + { + int64_t sample_pos = display->SamplesFromRelativeX(event.GetPosition().x); + + if (marker->CanSnap() && (default_snap != event.ShiftDown())) + { + AudioController::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) +: wxWindow(parent, -1, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS|wxBORDER_SIMPLE) +, provider(0) +, controller(_controller) +, dragged_object(0) +, old_selection(0, 0) +{ + scrollbar = new AudioDisplayScrollbar(this); + timeline = new AudioDisplayTimeline(this); + + audio_renderer = new AudioRenderer; + audio_spectrum_renderer = new AudioSpectrumRenderer; + audio_waveform_renderer = new AudioWaveformRenderer; + + scroll_left = 0; + pixel_audio_width = 0; + scale_amplitude = 1.0; + + track_cursor_pos = -1; + + controller->AddAudioListener(this); + controller->AddTimingListener(this); + + audio_renderer->SetAmplitudeScale(scale_amplitude); + SetZoomLevel(0); + + ReloadRenderingSettings(); + + SetMinClientSize(wxSize(-1, 70)); + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetThemeEnabled(false); +} + + +AudioDisplay::~AudioDisplay() +{ + delete audio_renderer; + delete audio_spectrum_renderer; + delete audio_waveform_renderer; + + delete timeline; + delete scrollbar; + + controller->RemoveAudioListener(this); + controller->RemoveTimingListener(this); +} + + +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; + + // This check is required to avoid needless redraws, but more importantly to + // avoid mutual recursion with the scrollbar and timeline. + if (pixel_position != scroll_left) + { + 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 AudioController::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) + { + 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, GetClientSize().GetWidth()); + timeline->ChangeZoom(pixel_samples); + + 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() +{ + 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 + if (spectrum_quality < 0) spectrum_quality = 0; + if (spectrum_quality > 5) spectrum_quality = 5; - // Get player - player = AudioPlayerFactory::GetAudioPlayer(); - player->SetDisplayTimer(&UpdateTimer); - player->SetProvider(provider); - player->OpenStream(); - loaded = true; + // 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}; - // Add to recent - if (!is_dummy) { - config::mru->Add("Audio", STD_STR(file)); - wxFileName fn(file); - StandardPaths::SetPathValue(_T("?audio"),fn.GetPath()); - } + audio_spectrum_renderer->SetResolution( + spectrum_width[spectrum_quality], + spectrum_distance[spectrum_quality]); - // Update - UpdateImage(); - } - catch (agi::UserCancelException const&) { - return; - } - catch (agi::FileNotFoundError const& e) { - config::mru->Remove("Audio", STD_STR(file)); - wxMessageBox(lagi_wxString(e.GetMessage()), L"Error loading audio",wxICON_ERROR | wxOK); - } - catch (AudioOpenError const& e) { - wxMessageBox(lagi_wxString(e.GetMessage()), L"Error loading audio",wxICON_ERROR | wxOK); - } - catch (const wxChar *e) { - if (player) { delete player; player = 0; } - if (provider) { delete provider; provider = 0; } - wxLogError(e); - } - catch (wxString &err) { - if (player) { delete player; player = 0; } - if (provider) { delete provider; provider = 0; } - wxMessageBox(err,_T("Error loading audio"),wxICON_ERROR | wxOK); - } - catch (...) { - if (player) { delete player; player = 0; } - if (provider) { delete provider; provider = 0; } - wxLogError(_T("Unknown error loading audio")); - } - } - - if (!loaded) return; + if (OPT_GET("Audio/Spectrum")->GetBool()) + audio_renderer->SetRenderer(audio_spectrum_renderer); + else + audio_renderer->SetRenderer(audio_waveform_renderer); - assert(loaded == (provider != NULL)); + audio_renderer->Invalidate(); - // Set default selection - AssDialogue *dlg = grid->GetActiveLine(); - SetDialogue(grid,dlg,grid->GetDialogueIndex(dlg)); + Refresh(); } -/// @brief Load from video -void AudioDisplay::SetFromVideo() { - if (VideoContext::Get()->IsLoaded()) { - wxString extension = VideoContext::Get()->videoName.Right(4); - extension.LowerCase(); - if (extension != _T(".d2v")) SetFile(VideoContext::Get()->videoName); - } -} -/// @brief Reload audio -void AudioDisplay::Reload() { - if (provider) SetFile(provider->GetFilename()); -} - -/// @brief Update scrollbar -/// @return -void AudioDisplay::UpdateScrollbar() { - if (!provider) return; - int page = w/12; - int len = provider->GetNumSamples() / samples / 12; - Position = PositionSample / samples; - ScrollBar->SetScrollbar(Position/12,page,len,int(page*0.7),true); -} - -/// @brief Gets the sample number at the x coordinate -/// @param x -/// @return -int64_t AudioDisplay::GetSampleAtX(int x) { - return (x+Position)*samples; -} - -/// @brief Gets the x coordinate corresponding to sample -/// @param n -/// @return -int AudioDisplay::GetXAtSample(int64_t n) { - return samples ? (n/samples)-Position : 0; -} - -/// @brief Get MS from X -/// @param x -/// @return -int AudioDisplay::GetMSAtX(int64_t x) { - return (PositionSample+(x*samples)) * 1000 / provider->GetSampleRate(); -} - -/// @brief Get X from MS -/// @param ms -/// @return -int AudioDisplay::GetXAtMS(int64_t ms) { - return ((ms * provider->GetSampleRate() / 1000)-PositionSample)/samples; -} - -/// @brief Get MS At sample -/// @param x -/// @return -int AudioDisplay::GetMSAtSample(int64_t x) { - return x * 1000 / provider->GetSampleRate(); -} - -/// @brief Get Sample at MS -/// @param ms -/// @return -int64_t AudioDisplay::GetSampleAtMS(int64_t ms) { - return ms * provider->GetSampleRate() / 1000; -} - -/// @brief Play -/// @param start -/// @param end -/// @return -void AudioDisplay::Play(int start,int end) { - Stop(); - - // Check provider - if (!provider) { - return; - } - - // Set defaults - playingToEnd = end < 0; - int64_t num_samples = provider->GetNumSamples(); - start = GetSampleAtMS(start); - if (end != -1) end = GetSampleAtMS(end); - else end = num_samples-1; - - // Sanity checking - if (start < 0) start = 0; - if (start >= num_samples) start = num_samples-1; - if (end >= num_samples) end = num_samples-1; - if (end < start) end = start; - - // Redraw the image to avoid any junk left over from mouse movements etc - // See issue #598 - UpdateImage(true); - - // Call play - player->Play(start,end-start); -} - -/// @brief Stop -void AudioDisplay::Stop() { - if (VideoContext::Get()->IsPlaying()) VideoContext::Get()->Stop(); - if (player) player->Stop(); -} - -/// @brief Get samples of dialogue -/// @param start -/// @param end -/// @return -void AudioDisplay::GetTimesDialogue(int &start,int &end) { - if (!dialogue) { - start = 0; - end = 0; - return; - } - - start = dialogue->Start.GetMS(); - end = dialogue->End.GetMS(); -} - -/// @brief Get samples of selection -/// @param start -/// @param end -/// @return -void AudioDisplay::GetTimesSelection(int &start,int &end) { - start = 0; - end = 0; - if (!dialogue) return; - - try { - if (karaoke->enabled) { - int pos = karaoke->syllables.at(karaoke->curSyllable).start_time; - int len = karaoke->syllables.at(karaoke->curSyllable).duration; - start = curStartMS+pos*10; - end = curStartMS+pos*10+len*10; - } - else { - start = curStartMS; - end = curEndMS; - } - } - catch (...) {} -} - -/// @brief Set the current selection -/// @param start -/// @param end -void AudioDisplay::SetSelection(int start, int end) { - curStartMS = start; - curEndMS = end; - Update(); -} - -/// @brief Set dialogue -/// @param _grid -/// @param diag -/// @param n -/// @return -void AudioDisplay::SetDialogue(SubtitlesGrid *,AssDialogue *diag,int n) { - // Set variables - line_n = n; - dialogue = diag; - - // Set flags - diagUpdated = false; - NeedCommit = false; - - // Set times - if (dialogue && OPT_GET("Audio/Grab Times on Select")->GetBool()) { - int s = dialogue->Start.GetMS(); - int e = dialogue->End.GetMS(); - - // Never do it for 0:00:00.00->0:00:00.00 lines - if (s != 0 || e != 0) { - curStartMS = s; - curEndMS = e; - } - } - - // Read karaoke data - if (dialogue && karaoke->enabled) { - NeedCommit = karaoke->LoadFromDialogue(dialogue); - - // Reset karaoke pos - if (karaoke->curSyllable == -1) karaoke->SetSyllable((int)karaoke->syllables.size()-1); - else karaoke->SetSyllable(0); - } - - // Update - Update(); -} - -/// @brief Commit changes -/// @param nextLine -/// @return -void AudioDisplay::CommitChanges (bool nextLine) { - // Loaded? - if (!loaded) return; - commitListener.Block(); - - // Check validity - bool timeNeedsCommit = grid->GetDialogue(line_n)->Start.GetMS() != curStartMS || grid->GetDialogue(line_n)->End.GetMS() != curEndMS; - NeedCommit = timeNeedsCommit; - bool wasKaraSplitting = false; - bool validCommit = true; - if (!karaoke->enabled && !karaoke->splitting) { - if (!NeedCommit || curEndMS < curStartMS) validCommit = false; - } - - // Update karaoke - int karaSelStart = 0, karaSelEnd = -1; - if (karaoke->enabled) { - wasKaraSplitting = karaoke->splitting; - karaoke->Commit(); - // Get karaoke selection - karaSelStart = karaoke->syllables.size(); - for (size_t k = 0; k < karaoke->syllables.size(); ++k) { - if (karaoke->syllables[k].selected) { - if ((signed)k < karaSelStart) karaSelStart = k; - if ((signed)k > karaSelEnd) karaSelEnd = k; - } - } - } - - // Get selected rows - wxArrayInt sel = grid->GetSelection(); - - // Commit ok? - if (validCommit) { - // Reset flags - diagUpdated = false; - NeedCommit = false; - - // Update dialogues - blockUpdate = true; - AssDialogue *curDiag; - for (size_t i=0;iIsInSelection(line_n)) curDiag = grid->GetDialogue(sel[i]); - else curDiag = grid->GetDialogue(line_n); - if (timeNeedsCommit) { - curDiag->Start.SetMS(curStartMS); - curDiag->End.SetMS(curEndMS); - } - if (!karaoke->enabled) { - // If user was editing karaoke stuff, that should take precedence of manual changes in the editbox, - // so only update from editbox when not in kara mode - curDiag->Text = grid->editBox->TextEdit->GetText(); - } - if (!grid->IsInSelection(line_n)) break; - } - - grid->ass->Commit(_T(""), karaoke->enabled ? AssFile::COMMIT_TEXT : AssFile::COMMIT_TIMES); - karaoke->SetSelection(karaSelStart, karaSelEnd); - blockUpdate = false; - } - - // Next line (ugh what a condition, can this be simplified?) - if (nextLine && !karaoke->enabled && OPT_GET("Audio/Next Line on Commit")->GetBool() && !wasKaraSplitting) { - // Insert a line if it doesn't exist - int nrows = grid->GetRows(); - if (nrows == line_n + 1) { - AssDialogue *def = new AssDialogue; - def->Start = grid->GetDialogue(line_n)->End; - def->End = grid->GetDialogue(line_n)->End; - def->End.SetMS(def->End.GetMS()+OPT_GET("Timing/Default Duration")->GetInt()); - def->Style = grid->GetDialogue(line_n)->Style; - grid->InsertLine(def,line_n,true); - } - int endMs = curEndMS; - - grid->NextLine(); - curStartMS = grid->GetActiveLine()->Start.GetMS(); - curEndMS = grid->GetActiveLine()->End.GetMS(); - if (curStartMS == 0 && curEndMS == 0) { - curStartMS = endMs; - curEndMS = endMs + OPT_GET("Timing/Default Duration")->GetInt(); - } - } - - commitListener.Unblock(); - Update(); -} - -/// @brief Add lead -/// @param in -/// @param out -void AudioDisplay::AddLead(bool in,bool out) { - // Lead in - if (in) { - curStartMS -= OPT_GET("Audio/Lead/IN")->GetInt(); - if (curStartMS < 0) curStartMS = 0; - } - - // Lead out - if (out) { - curEndMS += OPT_GET("Audio/Lead/OUT")->GetInt(); - } - - // Set changes - NeedCommit = true; - if (OPT_GET("Audio/Auto/Commit")->GetBool()) CommitChanges(); - Update(); -} - -/////////////// -// Event table BEGIN_EVENT_TABLE(AudioDisplay, wxWindow) EVT_MOUSE_EVENTS(AudioDisplay::OnMouseEvent) EVT_PAINT(AudioDisplay::OnPaint) EVT_SIZE(AudioDisplay::OnSize) - EVT_TIMER(Audio_Update_Timer,AudioDisplay::OnUpdateTimer) - EVT_KEY_DOWN(AudioDisplay::OnKeyDown) - EVT_SET_FOCUS(AudioDisplay::OnGetFocus) - EVT_KILL_FOCUS(AudioDisplay::OnLoseFocus) + EVT_SET_FOCUS(AudioDisplay::OnFocus) + EVT_KILL_FOCUS(AudioDisplay::OnFocus) END_EVENT_TABLE() -/// @brief Paint -/// @param event -/// @return -void AudioDisplay::OnPaint(wxPaintEvent& event) { - if (w == 0 || h == 0) return; - DoUpdateImage(); - wxPaintDC dc(this); - if (origImage) dc.DrawBitmap(*origImage,0,0); +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 + AudioController::SampleRange sel_samples(controller->GetPrimaryPlaybackRange()); + int selection_start = AbsoluteXFromSamples(sel_samples.begin()); + int selection_end = AbsoluteXFromSamples(sel_samples.end()); + + wxRegionIterator region(GetUpdateRegion()); + wxPoint client_org = GetClientAreaOrigin(); + while (region) + { + wxRect updrect = region.GetRect(); + // Work around wxMac issue, client border offsets update rectangles but does + // not affect drawing coordinates. + updrect.x += client_org.x; updrect.y += client_org.y; + + redraw_scrollbar |= scrollbar_bounds.Intersects(updrect); + redraw_timeline |= timeline_bounds.Intersects(updrect); + + if (audio_bounds.Intersects(updrect)) + { + int p1, p2, p3, p4; + // p1 -> p2 = before selection + // p2 -> p3 = in selection + // p3 -> p4 = after selection + p1 = scroll_left + updrect.x; + p2 = selection_start; + p3 = selection_end; + 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; + } + + // Draw markers on top of it all + AudioMarkerVector markers; + const int foot_size = 6; + AudioController::SampleRange updrectsamples( + SamplesFromRelativeX(updrect.x - foot_size), + SamplesFromRelativeX(updrect.x + updrect.width + foot_size)); + controller->GetMarkers(updrectsamples, markers); + if (controller->GetTimingController()) + controller->GetTimingController()->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); + } + } + } + + region++; + } + + 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.IsEmpty()) + { + 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); + 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); + 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); } -/// @brief Mouse event -/// @param event -/// @return -void AudioDisplay::OnMouseEvent(wxMouseEvent& event) { - // Get x,y - int64_t x = event.GetX(); - int64_t y = event.GetY(); - bool karMode = karaoke->enabled; - bool shiftDown = event.m_shiftDown; - int timelineHeight = OPT_GET("Audio/Display/Draw/Timeline")->GetBool() ? 20 : 0; - // Leaving event - if (event.Leaving()) { - event.Skip(); - return; - } +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. + AudioMarkerInteractionObject *dragged_marker = dynamic_cast(dragged_object); + if (dragged_marker) + delete dragged_marker; - if (!player || !provider) { - event.Skip(); - return; - } + dragged_object = new_obj; - // Is inside? - bool inside = false; - bool onScale = false; - if (x >= 0 && y >= 0 && x < w) { - if (y < h) { - inside = true; - - // Get focus - if (wxWindow::FindFocus() != this && OPT_GET("Audio/Auto/Focus")->GetBool()) SetFocus(); - } - else if (y < h+timelineHeight) onScale = true; - } - - // Buttons - bool leftIsDown = event.ButtonIsDown(wxMOUSE_BTN_LEFT); - bool rightIsDown = event.ButtonIsDown(wxMOUSE_BTN_RIGHT); - bool buttonIsDown = leftIsDown || rightIsDown; - bool leftClick = event.ButtonDown(wxMOUSE_BTN_LEFT); - bool rightClick = event.ButtonDown(wxMOUSE_BTN_RIGHT); - bool middleClick = event.Button(wxMOUSE_BTN_MIDDLE); - bool buttonClick = leftClick || rightClick; - bool defCursor = true; - - // Click type - if (buttonClick && !holding) { - holding = true; + if (dragged_object && !HasCapture()) CaptureMouse(); - } - if (!buttonIsDown && holding) { - holding = false; - if (HasCapture()) ReleaseMouse(); - } + else if (!dragged_object && HasCapture()) + ReleaseMouse(); +} - // Mouse wheel - if (event.GetWheelRotation() != 0) { - // Zoom or scroll? - bool zoom = shiftDown; + +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) +{ + // Check for mouse wheel scrolling + if (event.GetWheelRotation() != 0) + { + // First check if the cursor is inside or outside the display. + // If it's outside, we want to send the event to the control it's over instead. + /// @todo Factor this into a reusable function + { + wxWindow *targetwindow = wxFindWindowAtPoint(event.GetPosition()); + if (targetwindow && targetwindow != this) + { + targetwindow->GetEventHandler()->ProcessEvent(event); + event.Skip(false); + return; + } + } + + bool zoom = event.CmdDown(); if (OPT_GET("Audio/Wheel Default to Zoom")->GetBool()) zoom = !zoom; - // Zoom - if (zoom) { -#ifdef __APPLE__ - // Reverse scroll directions on Apple... ugly hack - // Otherwise left=right and right=left on systems that support four-way scrolling. - int step = -event.GetWheelRotation() / event.GetWheelDelta(); -#else - int step = event.GetWheelRotation() / event.GetWheelDelta(); -#endif - int value = box->HorizontalZoom->GetValue()+step; - box->HorizontalZoom->SetValue(value); - SetSamplesPercent(value,true,float(x)/float(w)); + if (!zoom) + { + int amount = -event.GetWheelRotation(); + // If the user did a horizontal scroll the amount should be inverted + // for it to be natural. + if (event.GetWheelAxis() == 1) amount = -amount; + + // Reset any accumulated zoom + mouse_zoom_accum = 0; + + ScrollBy(amount); + } + else if (event.GetWheelAxis() == 0) + { + mouse_zoom_accum += event.GetWheelRotation(); + int zoom_delta = mouse_zoom_accum / event.GetWheelDelta(); + mouse_zoom_accum %= event.GetWheelDelta(); + SetZoomLevel(GetZoomLevel() + zoom_delta); + /// @todo This has to update the trackbar in the audio box... maybe move handling mouse zoom to + /// the audio box instead to avoid messing with friend classes? } - // Scroll - else { - int step = -event.GetWheelRotation() * w / 360; - UpdatePosition(Position+step,false); - UpdateImage(); - } + // Scroll event processed + return; + } + + // 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; } - // Cursor drawing - if (player && !player->IsPlaying() && origImage) { - // Draw bg - wxClientDC dc(this); - if (origImage) dc.DrawBitmap(*origImage,0,0); + if (event.IsButton()) + SetFocus(); - if (inside) { - // Draw cursor - dc.SetLogicalFunction(wxINVERT); - dc.DrawLine(x,0,x,h); - - // Time - if (OPT_GET("Audio/Display/Draw/Cursor Time")->GetBool()) { - // Time string - AssTime time; - time.SetMS(GetMSAtX(x)); - wxString text = time.GetASSFormated(); - - // Calculate metrics - // FIXME: Hardcoded font name - wxFont font(10,wxFONTFAMILY_DEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_BOLD,false,_T("Verdana")); - dc.SetFont(font); - int tw,th; - GetTextExtent(text,&tw,&th,NULL,NULL,&font); - - // Set inversion - bool left = true; - if (x > w/2) left = false; - - // Text coordinates - int dx; - dx = x - tw/2; - if (dx < 4) dx = 4; - int max = w - tw - 4; - if (dx > max) dx = max; - int dy = 4; - if (karMode) dy += th; - - // Draw text - dc.SetTextForeground(wxColour(64,64,64)); - dc.DrawText(text,dx+1,dy-1); - dc.DrawText(text,dx+1,dy+1); - dc.DrawText(text,dx-1,dy-1); - dc.DrawText(text,dx-1,dy+1); - dc.SetTextForeground(wxColour(255,255,255)); - dc.DrawText(text,dx,dy); - } - } - } - - // Scale dragging - if ((hold == 0 && onScale) || draggingScale) { - if (event.ButtonDown(wxMOUSE_BTN_LEFT)) { - lastDragX = x; - draggingScale = true; - } - else if (holding) { - int delta = lastDragX - x; - lastDragX = x; - UpdatePosition(Position + delta); - UpdateImage(); - Refresh(false); + // 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); + return; + } + + // Check for timeline action + if (timeline->GetBounds().Contains(mousepos)) + { + SetCursor(wxCursor(wxCURSOR_SIZEWE)); + if (!controller->IsPlaying()) + RemoveTrackCursor(); + if (timeline->OnMouseEvent(event)) + SetDraggedObject(timeline); + 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, true); + } + + 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; } - else draggingScale = false; } - // Outside - if (!inside && hold == 0) return; + if (event.RightDown() && timing) + { + int64_t samplepos = SamplesFromRelativeX(mousepos.x); + AudioMarker *marker = timing->OnRightClick(samplepos, drag_sensitivity); - // Left click - if (leftClick) { - SetFocus(); - } - - // Right click - if (rightClick) { - SetFocus(); - if (karaoke->enabled) { - int syl = GetSyllableAtX(x); - if (syl != -1) { - int start = karaoke->syllables.at(syl).start_time * 10 + dialogue->Start.GetMS(); - int count = karaoke->syllables.at(syl).duration * 10; - Play(start, start+count); - } + if (marker) + { + RemoveTrackCursor(); + SetDraggedObject(new AudioMarkerInteractionObject(marker, timing, this, controller, wxMOUSE_BTN_RIGHT)); + return; } } - // Middle click - if (middleClick) { - SetFocus(); - if (VideoContext::Get()->IsLoaded()) { - VideoContext::Get()->JumpToTime(GetMSAtX(x)); - } - } - - // Timing - if (hasSel) { - bool updated = false; - - // Grab start/end - if (hold == 0) { - bool gotGrab = false; - bool karTime = karMode && ! -#ifdef __APPLE__ - event.CmdDown(); -#else - event.ControlDown(); -#endif - - // Line timing mode - if (!karTime) { - // Grab start - if (abs64 (x - selStart) < 6 && OPT_GET("Audio/Display/Dragging Times")->GetBool()==false) { - wxCursor cursor(wxCURSOR_SIZEWE); - SetCursor(cursor); - defCursor = false; - if (buttonClick) { - hold = 1; - gotGrab = true; - } - } - - // Grab end - else if (abs64 (x - selEnd) < 6 && OPT_GET("Audio/Display/Dragging Times")->GetBool()==false) { - wxCursor cursor(wxCURSOR_SIZEWE); - SetCursor(cursor); - defCursor = false; - if (buttonClick) { - hold = 2; - gotGrab = true; - } - } - - // Dragging nothing, time from scratch - else { - if (buttonClick) { - if (leftClick) hold = 3; - else hold = 2; - lastX = x; - gotGrab = true; - } - } - } - - // Karaoke mode - else { - // Look for a syllable - int64_t pos,len,curpos; - AudioKaraokeSyllable *curSyl; - size_t karn = karaoke->syllables.size(); - for (size_t i=0;isyllables.at(i); - len = curSyl->duration*10; - curpos = curSyl->start_time*10; - if (len != -1) { - pos = GetXAtMS(curStartMS+len+curpos); - - // Grabbing syllable boundary - if (abs64 (x - pos) < 7) { - wxCursor cursor(wxCURSOR_SIZEWE); - SetCursor(cursor); - defCursor = false; - if (event.LeftIsDown()) { - hold = 4; - holdSyl = (int)i; - gotGrab = true; - } - break; - } - } - } - - // No syllable found, select if possible - if (hold == 0 && leftClick) { - int syl = GetSyllableAtX(x); - if (syl != -1) { - karaoke->SetSyllable(syl); - updated = true; - } - } - } - } - - // Drag start/end - if (hold != 0) { - // Dragging - if (buttonIsDown) { - // Drag from nothing or straight timing - if (hold == 3 && buttonIsDown) { - if (!karMode) { - if (leftIsDown) curStartMS = GetMSAtX(x); - else curEndMS = GetMSAtX(x); - updated = true; - diagUpdated = true; - - if (leftIsDown && abs((long)(x-lastX)) > OPT_GET("Audio/Start Drag Sensitivity")->GetInt()) { - selStart = lastX; - selEnd = x; - curStartMS = GetBoundarySnap(GetMSAtX(lastX),10,event.ShiftDown(),true); - curEndMS = GetMSAtX(x); - hold = 2; - } - } - } - - // Drag start - if (hold == 1 && buttonIsDown) { - // Set new value - if (x != selStart) { - int snapped = GetBoundarySnap(GetMSAtX(x),10,event.ShiftDown(),true); - selStart = GetXAtMS(snapped); - if (selStart > selEnd) { - int temp = selStart; - selStart = selEnd; - selEnd = temp; - hold = 2; - curEndMS = snapped; - snapped = GetMSAtX(selStart); - } - curStartMS = snapped; - updated = true; - diagUpdated = true; - } - } - - // Drag end - if (hold == 2 && buttonIsDown) { - // Set new value - if (x != selEnd) { - int snapped = GetBoundarySnap(GetMSAtX(x),10,event.ShiftDown(),false); - selEnd = GetXAtMS(snapped); - //selEnd = GetBoundarySnap(x,event.ShiftDown()?0:10,false); - if (selStart > selEnd) { - int temp = selStart; - selStart = selEnd; - selEnd = temp; - hold = 1; - curStartMS = snapped; - snapped = GetMSAtX(selEnd); - } - curEndMS = snapped; - updated = true; - diagUpdated = true; - } - } - - // Drag karaoke - if (hold == 4 && leftIsDown) { - // Set new value - int curpos,len,pos,nkar; - AudioKaraokeSyllable *curSyl=NULL,*nextSyl=NULL; - curSyl = &karaoke->syllables.at(holdSyl); - nkar = (int)karaoke->syllables.size(); - if (holdSyl < nkar-1) { - nextSyl = &karaoke->syllables.at(holdSyl+1); - } - curpos = curSyl->start_time; - len = curSyl->duration; - pos = GetXAtMS(curStartMS+(len+curpos)*10); - if (x != pos) { - // Calculate delta in centiseconds - int delta = ((int64_t)(x-pos)*samples*100)/provider->GetSampleRate(); - - // Apply delta - int deltaMode = 0; - if (shiftDown) deltaMode = 1; - // else if (ctrlDown) deltaMode = 2; - bool result = karaoke->SyllableDelta(holdSyl,delta,deltaMode); - if (result) { - updated = true; - diagUpdated = true; - } - } - } - } - - // Release - else { - // Commit changes - if (diagUpdated) { - diagUpdated = false; - NeedCommit = true; - if (curStartMS <= curEndMS) { - if (OPT_GET("Audio/Auto/Commit")->GetBool()) CommitChanges(); - } - - else UpdateImage(true); - } - - // Update stuff - SetCursor(wxNullCursor); - hold = 0; - } - } - - // Update stuff - if (updated) { - if (diagUpdated) NeedCommit = true; - if (karaoke->enabled && !playingToEnd) { - AudioKaraokeSyllable &syl = karaoke->syllables[karaoke->curSyllable]; - player->SetEndPosition(GetSampleAtMS(curStartMS + (syl.start_time+syl.duration)*10)); - } else if (!playingToEnd) { - player->SetEndPosition(GetSampleAtX(selEnd)); - } - if (hold != 0) { - wxCursor cursor(wxCURSOR_SIZEWE); - SetCursor(cursor); - } - UpdateImage(true); - } - } - - // Not holding - else { - hold = 0; - } - - // Restore cursor - if (defCursor) SetCursor(wxNullCursor); + /// @todo Handle middle click to seek video } -/// @brief Get snap to boundary -/// @param ms -/// @param rangeX -/// @param shiftHeld -/// @param start -/// @return -int AudioDisplay::GetBoundarySnap(int ms,int rangeX,bool shiftHeld,bool start) { - // Range? - if (rangeX <= 0) return ms; - // Convert range into miliseconds - int rangeMS = rangeX*samples*1000 / provider->GetSampleRate(); +void AudioDisplay::OnSize(wxSizeEvent &event) +{ + // We changed size, update the sub-controls' internal data and redraw + wxSize size = GetClientSize(); - // Keyframe boundaries - wxArrayInt boundaries; - bool snapKey = OPT_GET("Audio/Display/Snap/Keyframes")->GetBool(); - if (shiftHeld) snapKey = !snapKey; - if (snapKey && VideoContext::Get()->KeyFramesLoaded() && OPT_GET("Audio/Display/Draw/Keyframes")->GetBool()) { - int64_t keyMS; - std::vector keyFrames = VideoContext::Get()->GetKeyFrames(); - int frame; - for (unsigned int i=0;iTimeAtFrame(frame,start ? agi::vfr::START : agi::vfr::END); - //if (start) keyX++; - if (GetXAtMS(keyMS) >= 0 && GetXAtMS(keyMS) < w) boundaries.Add(keyMS); - } - } + scrollbar->SetDisplaySize(size); + timeline->SetDisplaySize(wxSize(size.x, scrollbar->GetBounds().y)); - // Other subtitles' boundaries - int inactiveType = OPT_GET("Audio/Inactive Lines Display Mode")->GetInt(); - bool snapLines = OPT_GET("Audio/Display/Snap/Other Lines")->GetBool(); - if (shiftHeld) snapLines = !snapLines; - if (snapLines && (inactiveType == 1 || inactiveType == 2)) { - AssDialogue *shade; - int shadeX1,shadeX2; - int shadeFrom,shadeTo; + audio_height = size.GetHeight(); + audio_height -= scrollbar->GetBounds().GetHeight(); + audio_height -= timeline->GetHeight(); + audio_renderer->SetHeight(audio_height); - // Get range - if (inactiveType == 1) { - shadeFrom = this->line_n-1; - shadeTo = shadeFrom+1; - } - else { - shadeFrom = 0; - shadeTo = grid->GetRows(); - } + audio_top = timeline->GetHeight(); - for (int j=shadeFrom;jGetDialogue(j); - - if (shade) { - // Get coordinates - shadeX1 = GetXAtMS(shade->Start.GetMS()); - shadeX2 = GetXAtMS(shade->End.GetMS()); - if (shadeX1 >= 0 && shadeX1 < w) boundaries.Add(shade->Start.GetMS()); - if (shadeX2 >= 0 && shadeX2 < w) boundaries.Add(shade->End.GetMS()); - } - } - } - - // See if ms falls within range of any of them - int minDist = rangeMS+1; - int bestMS = ms; - for (unsigned int i=0;iGetBool() ? 20 : 0; - // Update image - UpdateSamples(); - if (samples) { - UpdatePosition(PositionSample / samples); - } - UpdateImage(); - - // Update scrollbar - UpdateScrollbar(); +void AudioDisplay::OnFocus(wxFocusEvent &event) +{ + // The scrollbar indicates focus so repaint that + RefreshRect(scrollbar->GetBounds()); } -/// @brief Timer event -/// @param event -/// @return -void AudioDisplay::OnUpdateTimer(wxTimerEvent &event) { - if (!origImage) - return; - // Get lock and check if it's OK - if (player->GetMutex()) { - wxMutexLocker locker(*player->GetMutex()); - if (!locker.IsOk()) return; - } - - if (!player->IsPlaying()) return; +void AudioDisplay::OnAudioOpen(AudioProvider *_provider) +{ + provider = _provider; - // Get DCs - //wxMutexGuiEnter(); - wxClientDC dc(this); + audio_renderer->SetAudioProvider(provider); + audio_renderer->SetCacheMaxSize(OPT_GET("Audio/Renderer/Spectrum/Memory Max")->GetInt() * 1024 * 1024); - // Draw cursor - int curpos = -1; - if (player->IsPlaying()) { - int64_t curPos = player->GetCurrentPosition(); - if (curPos > player->GetStartPosition() && curPos < player->GetEndPosition()) { - // Scroll if needed - int posX = GetXAtSample(curPos); - bool fullDraw = false; - bool centerLock = false; - bool scrollToCursor = OPT_GET("Audio/Lock Scroll on Cursor")->GetBool(); - if (centerLock) { - int goTo = MAX(0,curPos - w*samples/2); - if (goTo >= 0) { - UpdatePosition(goTo,true); - UpdateImage(); - fullDraw = true; - } - } - else { - if (scrollToCursor) { - if (posX < 80 || posX > w-80) { - int goTo = MAX(0,curPos - 80*samples); - if (goTo >= 0) { - UpdatePosition(goTo,true); - UpdateImage(); - fullDraw = true; - } - } - } - } + if (provider) + timeline->ChangeAudio(provider->GetNumSamples(), provider->GetSampleRate()); - // Draw cursor - wxMemoryDC src; - curpos = GetXAtSample(curPos); - if (curpos >= 0 && curpos < GetClientSize().GetWidth()) { - dc.SetPen(wxPen(lagi_wxColour(OPT_GET("Colour/Audio Display/Play Cursor")->GetColour()))); - src.SelectObject(*origImage); - if (fullDraw) { - //dc.Blit(0,0,w,h,&src,0,0); - dc.DrawLine(curpos,0,curpos,h); - //dc.Blit(0,0,curpos-10,h,&src,0,0); - //dc.Blit(curpos+10,0,w-curpos-10,h,&src,curpos+10,0); - } - else { - dc.Blit(oldCurPos,0,1,h,&src,oldCurPos,0); - dc.DrawLine(curpos,0,curpos,h); - } - } + SetZoomLevel(zoom_level); + + Refresh(); +} + + +void AudioDisplay::OnAudioClose() +{ + OnAudioOpen(0); +} + + +void AudioDisplay::OnPlaybackPosition(int64_t sample_position) +{ + SetTrackCursor(AbsoluteXFromSamples(sample_position), false); +} + + +void AudioDisplay::OnPlaybackStop() +{ + RemoveTrackCursor(); +} + + +void AudioDisplay::OnMarkersMoved() +{ + Refresh(); +} + + +void AudioDisplay::OnSelectionChanged() +{ + /// @todo Handle rendering style ranges from timing controller instead + AudioController::SampleRange sel(controller->GetPrimaryPlaybackRange()); + scrollbar->SetSelection(AbsoluteXFromSamples(sel.begin()), AbsoluteXFromSamples(sel.length())); + + if (sel.overlaps(old_selection)) + { + // Only redraw the parts of the selection that changed, to avoid flicker + int s1 = RelativeXFromSamples(sel.begin()); + int e1 = RelativeXFromSamples(sel.end()); + int s2 = RelativeXFromSamples(old_selection.begin()); + int e2 = RelativeXFromSamples(old_selection.end()); + if (s1 != s2) + { + wxRect r(std::min(s1, s2)-10, audio_top, abs(s1-s2)+20, audio_height); + RefreshRect(r); } - else { - if (curPos > player->GetEndPosition() + 8192) { - player->Stop(); - } - wxMemoryDC src; - src.SelectObject(*origImage); - dc.Blit(oldCurPos,0,1,h,&src,oldCurPos,0); + if (e1 != e2) + { + wxRect r(std::min(e1, e2)-10, audio_top, abs(e1-e2)+20, audio_height); + RefreshRect(r); } } - - // Restore background - else { - wxMemoryDC src; - src.SelectObject(*origImage); - dc.Blit(oldCurPos,0,1,h,&src,oldCurPos,0); + else + { + RefreshRect(wxRect(0, audio_top, GetClientSize().GetX(), audio_height)); } - oldCurPos = curpos; + + RefreshRect(scrollbar->GetBounds()); + + old_selection = sel; } -/// @brief Key down -/// @param event -void AudioDisplay::OnKeyDown(wxKeyEvent &event) { - int key = event.GetKeyCode(); -#ifdef __APPLE__ - Hotkeys.SetPressed(key,event.m_metaDown,event.m_altDown,event.m_shiftDown); -#else - Hotkeys.SetPressed(key,event.m_controlDown,event.m_altDown,event.m_shiftDown); -#endif - // Accept - if (Hotkeys.IsPressed(_T("Audio Commit"))) { - CommitChanges(true); - //ChangeLine(1); - } - - // Accept (SSA's "Grab times") - if (Hotkeys.IsPressed(_T("Audio Commit Alt"))) { - CommitChanges(true); - } - - // Accept (stay) - if (Hotkeys.IsPressed(_T("Audio Commit (Stay)"))) { - CommitChanges(); - } - - // Previous - if (Hotkeys.IsPressed(_T("Audio Prev Line")) || Hotkeys.IsPressed(_T("Audio Prev Line Alt"))) { - Prev(); - } - - // Next - if (Hotkeys.IsPressed(_T("Audio Next Line")) || Hotkeys.IsPressed(_T("Audio Next Line Alt"))) { - Next(); - } - - // Play - if (Hotkeys.IsPressed(_T("Audio Play")) || Hotkeys.IsPressed(_T("Audio Play Alt"))) { - int start=0,end=0; - GetTimesSelection(start,end); - Play(start,end); - } - - // Play/Stop - if (Hotkeys.IsPressed(_T("Audio Play or Stop"))) { - if (player->IsPlaying()) Stop(); - else { - int start=0,end=0; - GetTimesSelection(start,end); - Play(start,end); - } - } - - // Stop - if (Hotkeys.IsPressed(_T("Audio Stop"))) { - Stop(); - } - - // Increase length - if (Hotkeys.IsPressed(_T("Audio Karaoke Increase Len"))) { - if (karaoke->enabled) { - bool result = karaoke->SyllableDelta(karaoke->curSyllable,1,0); - if (result) diagUpdated = true; - } - } - - // Increase length (shift) - if (Hotkeys.IsPressed(_T("Audio Karaoke Increase Len Shift"))) { - if (karaoke->enabled) { - bool result = karaoke->SyllableDelta(karaoke->curSyllable,1,1); - if (result) diagUpdated = true; - } - } - - // Decrease length - if (Hotkeys.IsPressed(_T("Audio Karaoke Decrease Len"))) { - if (karaoke->enabled) { - bool result = karaoke->SyllableDelta(karaoke->curSyllable,-1,0); - if (result) diagUpdated = true; - } - } - - // Decrease length (shift) - if (Hotkeys.IsPressed(_T("Audio Karaoke Decrease Len Shift"))) { - if (karaoke->enabled) { - bool result = karaoke->SyllableDelta(karaoke->curSyllable,-1,1); - if (result) diagUpdated = true; - } - } - - // Move backwards - if (Hotkeys.IsPressed(_T("Audio Scroll Left"))) { - UpdatePosition(Position-128,false); - UpdateImage(); - } - - // Move forward - if (Hotkeys.IsPressed(_T("Audio Scroll Right"))) { - UpdatePosition(Position+128,false); - UpdateImage(); - } - - // Play first 500 ms - if (Hotkeys.IsPressed(_T("Audio Play First 500ms"))) { - int start=0,end=0; - GetTimesSelection(start,end); - int e = start+500; - if (e > end) e = end; - Play(start,e); - } - - // Play last 500 ms - if (Hotkeys.IsPressed(_T("Audio Play Last 500ms"))) { - int start=0,end=0; - GetTimesSelection(start,end); - int s = end-500; - if (s < start) s = start; - Play(s,end); - } - - // Play 500 ms before - if (Hotkeys.IsPressed(_T("Audio Play 500ms Before"))) { - int start=0,end=0; - GetTimesSelection(start,end); - Play(start-500,start); - } - - // Play 500 ms after - if (Hotkeys.IsPressed(_T("Audio Play 500ms After"))) { - int start=0,end=0; - GetTimesSelection(start,end); - Play(end,end+500); - } - - // Play to end of file - if (Hotkeys.IsPressed(_T("Audio Play To End"))) { - int start=0,end=0; - GetTimesSelection(start,end); - Play(start,-1); - } - - // Play original line - if (Hotkeys.IsPressed(_T("Audio Play Original Line"))) { - int start=0,end=0; - GetTimesDialogue(start,end); - SetSelection(start, end); - Play(start,end); - } - - // Lead in - if (Hotkeys.IsPressed(_T("Audio Add Lead In"))) { - AddLead(true,false); - } - - // Lead out - if (Hotkeys.IsPressed(_T("Audio Add Lead Out"))) { - AddLead(false,true); - } - - // Update - if (diagUpdated) { - diagUpdated = false; - NeedCommit = true; - if (OPT_GET("Audio/Auto/Commit")->GetBool() && curStartMS <= curEndMS) CommitChanges(); - else UpdateImage(true); - } +void AudioDisplay::OnTimingControllerChanged() +{ + Refresh(); + /// @todo Do something more about the new timing controller? } -/// @brief Change line -/// @param delta -/// @param block -/// @return -void AudioDisplay::ChangeLine(int delta, bool block) { - if (dialogue) { - // Get next line number and make sure it's within bounds - int next; - if (block && grid->IsInSelection(line_n)) next = grid->GetLastSelRow()+delta; - else next = line_n+delta; - - if (next == -1) next = 0; - if (next == grid->GetRows()) next = grid->GetRows() - 1; - - // Set stuff - NeedCommit = false; - dialogue = NULL; - grid->SetActiveLine(grid->GetDialogue(next)); - grid->SelectRow(next); - grid->MakeCellVisible(next,0,true); - if (!dialogue) UpdateImage(true); - else UpdateImage(false); - line_n = next; - } -} - -/// @brief Next -/// @param play -/// @return -void AudioDisplay::Next(bool play) { - // Karaoke - if (karaoke->enabled) { - int nextSyl = karaoke->curSyllable+1; - bool needsUpdate = true; - - // Last syllable; jump to next - if (nextSyl >= (signed int)karaoke->syllables.size()) { - // Already last? - if (line_n == grid->GetRows()-1) return; - - if (NeedCommit) { - int result = wxMessageBox(_("Do you want to commit your changes? If you choose No, they will be discarded."),_("Commit?"),wxYES_NO | wxCANCEL | wxICON_QUESTION); - //int result = wxNO; - if (result == wxYES) { - CommitChanges(); - } - else if (result == wxCANCEL) { - karaoke->curSyllable = (int)karaoke->syllables.size()-1; - return; - } - } - nextSyl = 0; - karaoke->curSyllable = 0; - ChangeLine(1); - needsUpdate = false; - } - - // Set syllable - karaoke->SetSyllable(nextSyl); - if (needsUpdate) Update(); - int start=0,end=0; - GetTimesSelection(start,end); - if (play) Play(start,end); - } - - // Plain mode - else { - ChangeLine(1); - } - -} - -/// @brief Previous -/// @param play -/// @return -void AudioDisplay::Prev(bool play) { - // Karaoke - if (karaoke->enabled) { - int nextSyl = karaoke->curSyllable-1; - bool needsUpdate = true; - - // First syllable; jump line - if (nextSyl < 0) { - // Already first? - if (line_n == 0) return; - - if (NeedCommit) { - int result = wxMessageBox(_("Do you want to commit your changes? If you choose No, they will be discarded."),_("Commit?"),wxYES_NO | wxCANCEL); - if (result == wxYES) { - CommitChanges(); - } - else if (result == wxCANCEL) { - karaoke->curSyllable = 0; - return; - } - } - karaoke->curSyllable = -1; - ChangeLine(-1); - needsUpdate = false; - } - - // Set syllable - karaoke->SetSyllable(nextSyl); - if (needsUpdate) Update(); - int start=0,end=0; - GetTimesSelection(start,end); - if (play) Play(start,end); - } - - // Plain mode - else { - ChangeLine(-1); - } - -} - -/// @brief Gets syllable at x position -/// @param x -/// @return -int AudioDisplay::GetSyllableAtX(int x) { - if (!karaoke->enabled) return -1; - int ms = GetMSAtX(x); - size_t syllables = karaoke->syllables.size();; - int sylstart,sylend; - - // Find a matching syllable - for (size_t i=0;isyllables.at(i).start_time*10 + curStartMS; - sylend = karaoke->syllables.at(i).duration*10 + sylstart; - if (ms >= sylstart && ms < sylend) { - return (int)i; - } - } - return -1; -} - -/// @brief Focus events -/// @param event -void AudioDisplay::OnGetFocus(wxFocusEvent &event) { - if (!hasFocus) { - hasFocus = true; - UpdateImage(true); - } -} - -/// @brief DOCME -/// @param event -void AudioDisplay::OnLoseFocus(wxFocusEvent &event) { - if (hasFocus && loaded) { - hasFocus = false; - UpdateImage(true); - Refresh(false); - } -} -void AudioDisplay::OnActiveLineChanged(AssDialogue *new_line) { - SetDialogue(grid,new_line,grid->GetDialogueIndex(new_line)); -} -void AudioDisplay::OnSelectedSetChanged(const Selection &lines_added, const Selection &lines_removed) { -} -void AudioDisplay::OnCommit(int type) { - AssDialogue *line = grid->GetActiveLine(); - SetDialogue(grid,line,grid->GetDialogueIndex(line)); -} diff --git a/aegisub/src/audio_display.h b/aegisub/src/audio_display.h index 6758f74b8..ef9769cce 100644 --- a/aegisub/src/audio_display.h +++ b/aegisub/src/audio_display.h @@ -1,4 +1,5 @@ // 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 @@ -45,247 +46,285 @@ #include #endif -#include - -#include "audio_renderer_spectrum.h" #include "selection_controller.h" -class AudioBox; +#ifndef AGI_AUDIO_CONTROLLER_INCLUDED +#error You must include "audio_controller.h" before "audio_display.h" +#endif + + +class AudioRenderer; +class AudioSpectrumRenderer; +class AudioWaveformRenderer; class AudioKaraoke; -class AudioPlayer; class AudioProvider; -class AssDialogue; -class FrameMain; +class AudioPlayer; class SubtitlesGrid; class VideoProvider; -/// DOCME +class AudioBox; +class SubtitlesGrid; +class AssDialogue; +class wxScrollBar; + +// Helper classes used in implementation of the audio display +class AudioDisplayScrollbar; +class AudioDisplayTimeline; +class AudioDisplaySelection; + + + +/// @class AudioDisplayInteractionObject +/// @brief Interface for objects on the audio display that can respond to mouse events +class AudioDisplayInteractionObject { +public: + /// @brief The user is interacting with the object using the mouse + /// @param event Mouse event data + /// @return True to take mouse capture, false to release mouse capture + /// + /// Assuming no object has the mouse capture, the audio display uses other methods + /// in the object implementing this interface to deterine whether a mouse event + /// should go to the object. If the mouse event goes to the object, this method + /// is called. + /// + /// If this method returns true, the audio display takes the mouse capture and + /// stores a pointer to the AudioDisplayInteractionObject interface for the object + /// and redirects the next mouse event to that object. + /// + /// If the object that has the mouse capture returns false from this method, the + /// capture is released and regular processing is done for the next event. + /// + /// If the object does not have mouse capture and returns false from this method, + /// no capture is taken or released and regular processing is done for the next + /// mouse event. + virtual bool OnMouseEvent(wxMouseEvent &event) = 0; + + /// @brief Destructor + /// + /// Empty virtual destructor for the cases that need it. + virtual ~AudioDisplayInteractionObject() { } +}; + + /// @class AudioDisplay -/// @brief DOCME +/// @brief Primary view/UI for interaction with audio timing /// -/// DOCME -class AudioDisplay: public wxWindow, private SelectionListener { - friend class FrameMain; +/// The audio display is the common view that allows the user to interact with the active +/// timing controller. The audio display also renders audio according to the audio controller +/// and the timing controller, using an audio renderer instance. +class AudioDisplay: public wxWindow, private AudioControllerAudioEventListener, private AudioControllerTimingEventListener { private: - /// DOCME - SubtitlesGrid *grid; + /// The audio renderer manager + AudioRenderer *audio_renderer; - /// DOCME - int line_n; + /// The renderer for audio spectrums + AudioSpectrumRenderer *audio_spectrum_renderer; - /// DOCME - AssDialogue *dialogue; + /// The renderer for audio waveforms + AudioWaveformRenderer *audio_waveform_renderer; - /// DOCME - AudioSpectrum *spectrumRenderer; + /// Our current audio provider + AudioProvider *provider; - /// DOCME - wxBitmap *origImage; + /// The controller managing us + AudioController *controller; - /// DOCME - wxBitmap *spectrumDisplay; - /// DOCME - wxBitmap *spectrumDisplaySelected; + /// Scrollbar helper object + AudioDisplayScrollbar *scrollbar; - /// DOCME - int64_t PositionSample; + /// Timeline helper object + AudioDisplayTimeline *timeline; - /// DOCME - float scale; - /// DOCME - int samples; + /// Current object on display being dragged, if any + AudioDisplayInteractionObject *dragged_object; + /// Change the dragged object and update mouse capture + void SetDraggedObject(AudioDisplayInteractionObject *new_obj); - /// DOCME - int64_t Position; - /// DOCME - int samplesPercent; + /// Leftmost pixel in the vitual audio image being displayed + int scroll_left; - /// DOCME - int oldCurPos; + /// Total width of the audio in pixels + int pixel_audio_width; - /// DOCME - bool hasFocus; + /// Horizontal zoom measured in audio samples per pixel + int pixel_samples; - /// DOCME - bool blockUpdate; + /// Amplitude scaling ("vertical zoom") as a factor, 1.0 is neutral + float scale_amplitude; - /// DOCME - bool dontReadTimes; + /// Top of the main audio area in pixels + int audio_top; - /// DOCME - bool playingToEnd; + /// Height of main audio area in pixels + int audio_height; - /// DOCME - bool needImageUpdate; - /// DOCME - bool needImageUpdateWeak; + /// Zoom level given as a number, see SetZoomLevel for details + int zoom_level; + // Mouse wheel zoom accumulator + int mouse_zoom_accum; - /// DOCME - bool hasSel; - /// DOCME - bool hasKaraoke; + /// Absolute pixel position of the tracking cursor (mouse or playback) + int track_cursor_pos; + /// Label to show by track cursor + wxString track_cursor_label; + /// Bounding rectangle last drawn track cursor label + wxRect track_cursor_label_rect; + /// @brief Move the tracking cursor + /// @param new_pos New absolute pixel position of the tracking cursor + /// @param show_time Display timestamp by the tracking cursor? + void SetTrackCursor(int new_pos, bool show_time); + /// @brief Remove the tracking cursor from the display + void RemoveTrackCursor(); - /// DOCME - bool diagUpdated; - /// DOCME - bool holding; + /// Previous audio selection for optimising redraw when selection changes + AudioController::SampleRange old_selection; - /// DOCME - bool draggingScale; - - /// DOCME - int64_t selStart; - - /// DOCME - int64_t selEnd; - - /// DOCME - int64_t lineStart; - - /// DOCME - int64_t lineEnd; - - /// DOCME - int64_t selStartCap; - - /// DOCME - int64_t selEndCap; - - /// DOCME - int hold; - - /// DOCME - int lastX; - - /// DOCME - int lastDragX; - - /// DOCME - int curStartMS; - - /// DOCME - int curEndMS; - - /// DOCME - int holdSyl; - - /// DOCME - int *peak; - - /// DOCME - int *min; + /// wxWidgets paint event void OnPaint(wxPaintEvent &event); + /// wxWidgets mouse input event void OnMouseEvent(wxMouseEvent &event); + /// wxWidgets control size changed event void OnSize(wxSizeEvent &event); - void OnUpdateTimer(wxTimerEvent &event); - void OnKeyDown(wxKeyEvent &event); - void OnGetFocus(wxFocusEvent &event); - void OnLoseFocus(wxFocusEvent &event); + /// wxWidgets input focus changed event + void OnFocus(wxFocusEvent &event); - void UpdateSamples(); - void Reset(); - void DrawTimescale(wxDC &dc); - void DrawKeyframes(wxDC &dc); - void DrawInactiveLines(wxDC &dc); - void DrawWaveform(wxDC &dc,bool weak); - void DrawSpectrum(wxDC &dc,bool weak); - void GetDialoguePos(int64_t &start,int64_t &end,bool cap); - void GetKaraokePos(int64_t &start,int64_t &end,bool cap); - void UpdatePosition(int pos,bool IsSample=false); - int GetBoundarySnap(int x,int range,bool shiftHeld,bool start=true); - void DoUpdateImage(); +private: + // AudioControllerAudioEventListener implementation + virtual void OnAudioOpen(AudioProvider *provider); + virtual void OnAudioClose(); + virtual void OnPlaybackPosition(int64_t sample_position); + virtual void OnPlaybackStop(); + + // AudioControllerTimingEventListener implementation + virtual void OnMarkersMoved(); + virtual void OnSelectionChanged(); + virtual void OnTimingControllerChanged(); - void OnActiveLineChanged(AssDialogue *new_line); - void OnSelectedSetChanged(const Selection &lines_added, const Selection &lines_removed); - void OnCommit(int); - agi::signal::Connection commitListener; public: - /// DOCME - AudioProvider *provider; - - /// DOCME - AudioPlayer *player; - - /// DOCME - bool NeedCommit; - - /// DOCME - bool loaded; - - /// DOCME - bool temporary; - - /// DOCME - - /// DOCME - int w,h; - - /// DOCME - AudioBox *box; - - /// DOCME - AudioKaraoke *karaoke; - - /// DOCME - wxScrollBar *ScrollBar; - - /// DOCME - wxTimer UpdateTimer; - - AudioDisplay(wxWindow *parent, SubtitlesGrid *grid); + AudioDisplay(wxWindow *parent, AudioController *controller); ~AudioDisplay(); - void UpdateImage(bool weak=false); - void Update(); - void RecreateImage(); - void SetPosition(int pos); - void SetSamplesPercent(int percent,bool update=true,float pivot=0.5); - void SetScale(float scale); - void UpdateScrollbar(); - void SetDialogue(SubtitlesGrid *_grid=NULL,AssDialogue *diag=NULL,int n=-1); - void MakeDialogueVisible(bool force=false); - void ChangeLine(int delta, bool block=false); - void Next(bool play=true); - void Prev(bool play=true); - void CommitChanges(bool nextLine=false); - void AddLead(bool in,bool out); + /// @brief Scroll the audio display + /// @param pixel_amount Number of pixels to scroll the view + /// + /// A positive amount moves the display to the right, making later parts of the audio visible. + void ScrollBy(int pixel_amount); - void SetFile(wxString file); - void SetFromVideo(); - void Reload(); + /// @brief Scroll the audio display + /// @param pixel_position Absolute pixel to put at left edge of the audio display + /// + /// This is the principal scrolling function. All other scrolling functions eventually + /// call this function to perform the actual scrolling. + void ScrollPixelToLeft(int pixel_position); - void Play(int start,int end); - void Stop(); + /// @brief Scroll the audio display + /// @param pixel_position Absolute pixel to put in center of the audio display + void ScrollPixelToCenter(int pixel_position); - int64_t GetSampleAtX(int x); - int GetXAtSample(int64_t n); - int GetMSAtX(int64_t x); - int GetXAtMS(int64_t ms); - int GetMSAtSample(int64_t x); - int64_t GetSampleAtMS(int64_t ms); - int GetSyllableAtX(int x); + /// @brief Scroll the audio display + /// @param sample_position Audio sample to put at left edge of the audio display + void ScrollSampleToLeft(int64_t sample_position); + + /// @brief Scroll the audio display + /// @param sample_position Audio sample to put in center of the audio display + void ScrollSampleToCenter(int64_t sample_position); + + /// @brief Scroll the audio display + /// @param range Range of audio samples to ensure is in view + /// + /// If the entire range is already visible inside the display, nothing is scrolled. If + /// just one of the two endpoints is visible, the display is scrolled such that the + /// visible endpoint stays in view but more of the rest of the range becomes visible. + /// + /// If the entire range fits inside the display, the display is centered over the range. + /// For this calculation, the display is considered smaller by some margins, see below. + /// + /// If the range does not fit within the display with margins subtracted, the start of + /// the range is ensured visible and as much of the rest of the range is brought into + /// view. + /// + /// For the purpose of this function, a 5 percent margin is assumed at each end of the + /// audio display such that a range endpoint that is ensured to be in view never gets + /// closer to the edge of the display than the margin. The edge that is not ensured to + /// be in view might be outside of view or might be closer to the display edge than the + /// margin. + void ScrollSampleRangeInView(const AudioController::SampleRange &range); + + + /// @brief Change the zoom level + /// @param new_zoom_level The new zoom level to use + /// + /// A zoom level of 0 is the default zoom level, all other levels are based on this. + /// Negative zoom levels zoom out, positive zoom in. + /// + /// The zoom levels generally go from +30 to -30. It is possible to zoom in more than + /// +30 + void SetZoomLevel(int new_zoom_level); + + /// @brief Get the zoom level + /// @return The zoom level + /// + /// See SetZoomLevel for a description of zoom levels. + int GetZoomLevel() const; + + /// @brief Get a textual description of a zoom level + /// @param level The zoom level to describe + /// @return A translated string describing a zoom level + /// + /// The zoom level description can tell the user details about how much audio is + /// actually displayed. + wxString GetZoomLevelDescription(int level) const; + + /// @brief Get the zoom factor in percent for a zoom level + /// @param level The zoom level to get the factor of + /// @return The zoom factor in percent + /// + /// Positive: 125, 150, 175, 200, 225, ... + /// + /// Negative: 90, 80, 70, 60, 50, 45, 40, 35, 30, 25, 20, 19, 18, 17, ..., 1 + /// + /// Too negative numbers get clamped. + static int GetZoomLevelFactor(int level); + + + /// @brief Set amplitude scale factor + /// @param scale New amplitude scale factor, 1.0 is no scaling + void SetAmplitudeScale(float scale); + + /// @brief Get amplitude scale factor + /// @return The amplitude scaling factor + float GetAmplitudeScale() const; + + + /// @brief Reload all rendering settings from Options and reset caches + /// + /// This can be called if some rendering quality settings have been changed in Options + /// and need to be reloaded to take effect. + void ReloadRenderingSettings(); + + + /// @brief Get a sample index from an X coordinate relative to current scroll + int64_t SamplesFromRelativeX(int x) const { return (scroll_left + x) * pixel_samples; } + /// @brief Get a sample index from an absolute X coordinate + int64_t SamplesFromAbsoluteX(int x) const { return x * pixel_samples; } + /// @brief Get an X coordinate relative to the current scroll from a sample index + int RelativeXFromSamples(int64_t samples) const { return samples/pixel_samples - scroll_left; } + /// @brief Get an absolute X coordinate from a sample index + int AbsoluteXFromSamples(int64_t samples) const { return samples/pixel_samples; } - void GetTimesDialogue(int &start,int &end); - void GetTimesSelection(int &start,int &end); - void SetSelection(int start, int end); DECLARE_EVENT_TABLE() }; -/////// -// IDs -enum { - Audio_Update_Timer = 1700 -}; diff --git a/aegisub/src/audio_karaoke.cpp b/aegisub/src/audio_karaoke.cpp index 1d8ca4c6d..84be566da 100644 --- a/aegisub/src/audio_karaoke.cpp +++ b/aegisub/src/audio_karaoke.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2005, 2006, 2007, Rodrigo Braz Monteiro, Niels Martin Hansen +// Copyright (c) 2005-2009, Rodrigo Braz Monteiro, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -48,6 +48,8 @@ #include #include "ass_override.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" #include "audio_display.h" #include "audio_karaoke.h" @@ -634,7 +636,7 @@ void AudioKaraoke::Join() { // Update must_rebuild = true; - display->NeedCommit = true; + //display->NeedCommit = true; display->Update(); Refresh(false); @@ -678,7 +680,7 @@ void AudioKaraoke::EndSplit(bool commit) { if (hasSplit) { LOG_D("karaoke/audio") << "hassplit"; must_rebuild = true; - display->NeedCommit = true; + //display->NeedCommit = true; SetSelection(first_sel); display->Update(); } @@ -879,8 +881,9 @@ void AudioKaraokeTagMenu::OnSelectItem(wxCommandEvent &event) { // Update display kara->must_rebuild = true; //kara->Commit(); - kara->display->NeedCommit = true; - kara->display->CommitChanges(); + //kara->display->NeedCommit = true; + /// @todo Commit changes and stay on current line + //kara->display->CommitChanges(); //kara->display->Update(); kara->SetSelection(firstsel, lastsel); } diff --git a/aegisub/src/audio_player_alsa.cpp b/aegisub/src/audio_player_alsa.cpp index ae5ab6c9d..4b7ddcd7f 100644 --- a/aegisub/src/audio_player_alsa.cpp +++ b/aegisub/src/audio_player_alsa.cpp @@ -40,6 +40,7 @@ #include +#include "audio_controller.h" #include "audio_player_alsa.h" #include "main.h" #include "compat.h" diff --git a/aegisub/src/audio_player_dsound.cpp b/aegisub/src/audio_player_dsound.cpp index 2fe021c1b..14ad47965 100644 --- a/aegisub/src/audio_player_dsound.cpp +++ b/aegisub/src/audio_player_dsound.cpp @@ -40,6 +40,7 @@ #include +#include "audio_controller.h" #include "audio_player_dsound.h" #include "frame_main.h" #include "main.h" diff --git a/aegisub/src/audio_player_dsound2.cpp b/aegisub/src/audio_player_dsound2.cpp index 2d3969223..5ed938d78 100644 --- a/aegisub/src/audio_player_dsound2.cpp +++ b/aegisub/src/audio_player_dsound2.cpp @@ -49,9 +49,10 @@ #include +#include "audio_controller.h" #include "audio_player_dsound2.h" -#include "frame_main.h" #include "include/aegisub/audio_provider.h" +#include "frame_main.h" #include "main.h" #include "utils.h" diff --git a/aegisub/src/audio_player_openal.cpp b/aegisub/src/audio_player_openal.cpp index f87c11cd7..443e7fb96 100644 --- a/aegisub/src/audio_player_openal.cpp +++ b/aegisub/src/audio_player_openal.cpp @@ -40,6 +40,7 @@ #include +#include "audio_controller.h" #include "audio_player_openal.h" #include "frame_main.h" #include "utils.h" diff --git a/aegisub/src/audio_player_oss.cpp b/aegisub/src/audio_player_oss.cpp index c9cfec9cb..c9187371b 100644 --- a/aegisub/src/audio_player_oss.cpp +++ b/aegisub/src/audio_player_oss.cpp @@ -38,6 +38,7 @@ #include +#include "audio_controller.h" #include "audio_player_oss.h" #include "frame_main.h" #include "compat.h" diff --git a/aegisub/src/audio_provider.cpp b/aegisub/src/audio_provider.cpp index d37307679..4c230293c 100644 --- a/aegisub/src/audio_provider.cpp +++ b/aegisub/src/audio_provider.cpp @@ -65,77 +65,6 @@ AudioProvider::~AudioProvider() { delete[] raw; } -/// @brief Get waveform -/// @param min -/// @param peak -/// @param start -/// @param w -/// @param h -/// @param samples -/// @param scale -/// -void AudioProvider::GetWaveForm(int *min,int *peak,int64_t start,int w,int h,int samples,float scale) { - // Setup - int channels = GetChannels(); - int n = w * samples; - for (int i=0;i h) curvalue = h; - if (curvalue < 0) curvalue = 0; - if (curvalue < min[cur]) min[cur] = curvalue; - if (curvalue > peak[cur]) peak[cur] = curvalue; - } - } - - if (bytes_per_sample == 2) { - // Read raw samples - short *raw_short = (short*) raw; - GetAudio(raw,start,n); - int half_h = h/2; - int half_amplitude = int(half_h * scale); - - // Calculate waveform - for (int i=0;i h) curvalue = h; - if (curvalue < 0) curvalue = 0; - if (curvalue < min[cur]) min[cur] = curvalue; - if (curvalue > peak[cur]) peak[cur] = curvalue; - } - } -} - /// @brief Get audio with volume /// @param buf /// @param start @@ -143,7 +72,7 @@ void AudioProvider::GetWaveForm(int *min,int *peak,int64_t start,int w,int h,int /// @param volume /// @return /// -void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) { +void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const { try { GetAudio(buf,start,count); } diff --git a/aegisub/src/audio_provider_avs.cpp b/aegisub/src/audio_provider_avs.cpp index e342da69c..82ca5f35f 100644 --- a/aegisub/src/audio_provider_avs.cpp +++ b/aegisub/src/audio_provider_avs.cpp @@ -141,7 +141,7 @@ void AvisynthAudioProvider::LoadFromClip(AVSValue _clip) { /// @param start /// @param count /// -void AvisynthAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) { +void AvisynthAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const { // Requested beyond the length of audio if (start+count > num_samples) { int64_t oldcount = count; diff --git a/aegisub/src/audio_provider_avs.h b/aegisub/src/audio_provider_avs.h index 2f573ec6b..31836ec87 100644 --- a/aegisub/src/audio_provider_avs.h +++ b/aegisub/src/audio_provider_avs.h @@ -57,15 +57,12 @@ class AvisynthAudioProvider : public AudioProvider, public AviSynthWrapper { public: AvisynthAudioProvider(wxString _filename); - wxString GetFilename() { return filename; } + wxString GetFilename() const { return filename; } - /// @brief Only exists for x86 Windows, always delivers machine (little) endian - /// @return - /// bool AreSamplesNativeEndian() const { return true; } bool NeedsCache() const { return true; } - void GetAudio(void *buf, int64_t start, int64_t count); + void GetAudio(void *buf, int64_t start, int64_t count) const; void GetWaveForm(int *min,int *peak,int64_t start,int w,int h,int samples,float scale); }; #endif diff --git a/aegisub/src/audio_provider_convert.cpp b/aegisub/src/audio_provider_convert.cpp index f7475f946..e08df165d 100644 --- a/aegisub/src/audio_provider_convert.cpp +++ b/aegisub/src/audio_provider_convert.cpp @@ -62,7 +62,7 @@ ConvertAudioProvider::ConvertAudioProvider(AudioProvider *src) : source(src) { /// @param dst /// @param count /// -void ConvertAudioProvider::Make16Bit(const char *src, short *dst, int64_t count) { +void ConvertAudioProvider::Make16Bit(const char *src, short *dst, int64_t count) const { for (int64_t i=0;i /// @param count /// @param converter /// -void ConvertAudioProvider::ChangeSampleRate(const short *src, short *dst, int64_t count, const SampleConverter &converter) { +void ConvertAudioProvider::ChangeSampleRate(const short *src, short *dst, int64_t count, const SampleConverter &converter) const { // Upsample by 2 if (sampleMult == 2) { int64_t size = count/2; @@ -139,7 +139,7 @@ struct EndianSwapSampleConverter { /// @param start /// @param count /// -void ConvertAudioProvider::GetAudio(void *destination, int64_t start, int64_t count) { +void ConvertAudioProvider::GetAudio(void *destination, int64_t start, int64_t count) const { // Bits per sample int srcBps = source->GetBytesPerSample(); diff --git a/aegisub/src/audio_provider_convert.h b/aegisub/src/audio_provider_convert.h index 397995117..79b3e2ef8 100644 --- a/aegisub/src/audio_provider_convert.h +++ b/aegisub/src/audio_provider_convert.h @@ -51,9 +51,9 @@ class ConvertAudioProvider : public AudioProvider { /// DOCME std::tr1::shared_ptr source; - void Make16Bit(const char *src, short *dst, int64_t count); + void Make16Bit(const char *src, short *dst, int64_t count) const; template - void ChangeSampleRate(const short *src, short *dst, int64_t count, const SampleConverter &converter); + void ChangeSampleRate(const short *src, short *dst, int64_t count, const SampleConverter &converter) const; public: ConvertAudioProvider(AudioProvider *source); @@ -62,9 +62,9 @@ public: /// That's one of the points of it! bool AreSamplesNativeEndian() const { return true; } - void GetAudio(void *buf, int64_t start, int64_t count); + void GetAudio(void *buf, int64_t start, int64_t count) const; - wxString GetFilename() { return source->GetFilename(); } + wxString GetFilename() const { return source->GetFilename(); } }; AudioProvider *CreateConvertAudioProvider(AudioProvider *source_provider); diff --git a/aegisub/src/audio_provider_downmix.cpp b/aegisub/src/audio_provider_downmix.cpp index 1d1ab2492..a8c277e95 100644 --- a/aegisub/src/audio_provider_downmix.cpp +++ b/aegisub/src/audio_provider_downmix.cpp @@ -64,7 +64,7 @@ DownmixingAudioProvider::DownmixingAudioProvider(AudioProvider *source) : provid /// @param start /// @param count /// -void DownmixingAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) { +void DownmixingAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const { if (count == 0) return; // We can do this ourselves diff --git a/aegisub/src/audio_provider_downmix.h b/aegisub/src/audio_provider_downmix.h index 91cceaa23..af79d316d 100644 --- a/aegisub/src/audio_provider_downmix.h +++ b/aegisub/src/audio_provider_downmix.h @@ -57,5 +57,5 @@ public: /// bool AreSamplesNativeEndian() const { return true; } - void GetAudio(void *buf, int64_t start, int64_t count); + void GetAudio(void *buf, int64_t start, int64_t count) const; }; diff --git a/aegisub/src/audio_provider_dummy.cpp b/aegisub/src/audio_provider_dummy.cpp index ae7f3da77..08b1c5ce7 100644 --- a/aegisub/src/audio_provider_dummy.cpp +++ b/aegisub/src/audio_provider_dummy.cpp @@ -62,7 +62,7 @@ DummyAudioProvider::~DummyAudioProvider() { /// @param start /// @param count /// -void DummyAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) { +void DummyAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const { short *workbuf = (short*)buf; if (noise) { diff --git a/aegisub/src/audio_provider_dummy.h b/aegisub/src/audio_provider_dummy.h index 508c15ab3..6448412eb 100644 --- a/aegisub/src/audio_provider_dummy.h +++ b/aegisub/src/audio_provider_dummy.h @@ -50,5 +50,5 @@ public: ~DummyAudioProvider(); bool AreSamplesNativeEndian() const { return true; } - void GetAudio(void *buf, int64_t start, int64_t count); + void GetAudio(void *buf, int64_t start, int64_t count) const; }; diff --git a/aegisub/src/audio_provider_ffmpegsource.cpp b/aegisub/src/audio_provider_ffmpegsource.cpp index 051009789..4f8f3afa0 100644 --- a/aegisub/src/audio_provider_ffmpegsource.cpp +++ b/aegisub/src/audio_provider_ffmpegsource.cpp @@ -228,7 +228,7 @@ void FFmpegSourceAudioProvider::Close() { /// @param Start /// @param Count /// -void FFmpegSourceAudioProvider::GetAudio(void *Buf, int64_t Start, int64_t Count) { +void FFmpegSourceAudioProvider::GetAudio(void *Buf, int64_t Start, int64_t Count) const { uint8_t *Buf2 = static_cast(Buf); Start -= delay; if (Start < 0) { diff --git a/aegisub/src/audio_provider_ffmpegsource.h b/aegisub/src/audio_provider_ffmpegsource.h index 6e9314f3e..12062805c 100644 --- a/aegisub/src/audio_provider_ffmpegsource.h +++ b/aegisub/src/audio_provider_ffmpegsource.h @@ -46,8 +46,8 @@ private: FFMS_AudioSource *AudioSource; ///< audio source object bool COMInited; ///< COM initialization state - char FFMSErrMsg[1024]; ///< FFMS error message - FFMS_ErrorInfo ErrInfo; ///< FFMS error codes/messages + mutable char FFMSErrMsg[1024]; ///< FFMS error message + mutable FFMS_ErrorInfo ErrInfo; ///< FFMS error codes/messages void Close(); void LoadAudio(wxString filename); @@ -65,6 +65,6 @@ public: bool AreSamplesNativeEndian() const { return true; } bool NeedsCache() const { return true; } - virtual void GetAudio(void *buf, int64_t start, int64_t count); + virtual void GetAudio(void *buf, int64_t start, int64_t count) const; }; #endif diff --git a/aegisub/src/audio_provider_hd.cpp b/aegisub/src/audio_provider_hd.cpp index 93a67a4e7..db41eb909 100644 --- a/aegisub/src/audio_provider_hd.cpp +++ b/aegisub/src/audio_provider_hd.cpp @@ -41,6 +41,7 @@ #include #endif +#include "audio_controller.h" #include "audio_provider_hd.h" #include "compat.h" #include "dialog_progress.h" @@ -114,7 +115,7 @@ HDAudioProvider::~HDAudioProvider() { /// @param start /// @param count /// -void HDAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) { +void HDAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const { // Requested beyond the length of audio if (start+count > num_samples) { int64_t oldcount = count; diff --git a/aegisub/src/audio_provider_hd.h b/aegisub/src/audio_provider_hd.h index fd78d4587..f62af2396 100644 --- a/aegisub/src/audio_provider_hd.h +++ b/aegisub/src/audio_provider_hd.h @@ -48,10 +48,10 @@ /// DOCME class HDAudioProvider : public AudioProvider { /// DOCME - wxMutex diskmutex; + mutable wxMutex diskmutex; /// DOCME - wxFile file_cache; + mutable wxFile file_cache; /// DOCME wxString diskCacheFilename; @@ -71,5 +71,5 @@ public: bool AreSamplesNativeEndian() const { return samples_native_endian; } - void GetAudio(void *buf, int64_t start, int64_t count); + void GetAudio(void *buf, int64_t start, int64_t count) const; }; diff --git a/aegisub/src/audio_provider_pcm.cpp b/aegisub/src/audio_provider_pcm.cpp index c4cf0bae0..22f3e51eb 100644 --- a/aegisub/src/audio_provider_pcm.cpp +++ b/aegisub/src/audio_provider_pcm.cpp @@ -144,7 +144,7 @@ PCMAudioProvider::~PCMAudioProvider() /// @param range_length /// @return /// -char * PCMAudioProvider::EnsureRangeAccessible(int64_t range_start, int64_t range_length) +char * PCMAudioProvider::EnsureRangeAccessible(int64_t range_start, int64_t range_length) const { if (range_start + range_length > file_size) { throw AudioDecodeError("Attempted to map beyond end of file"); @@ -217,13 +217,13 @@ char * PCMAudioProvider::EnsureRangeAccessible(int64_t range_start, int64_t rang /// @param start /// @param count /// -void PCMAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) +void PCMAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const { // Read blocks from the file size_t index = 0; while (count > 0 && index < index_points.size()) { // Check if this index contains the samples we're looking for - IndexPoint &ip = index_points[index]; + const IndexPoint &ip = index_points[index]; if (ip.start_sample <= start && ip.start_sample+ip.num_samples > start) { // How many samples we can maximum take from this block diff --git a/aegisub/src/audio_provider_pcm.h b/aegisub/src/audio_provider_pcm.h index 46425d6a7..2c036cea1 100644 --- a/aegisub/src/audio_provider_pcm.h +++ b/aegisub/src/audio_provider_pcm.h @@ -64,24 +64,24 @@ private: HANDLE file_mapping; /// DOCME - void *current_mapping; + mutable void *current_mapping; /// DOCME - int64_t mapping_start; + mutable int64_t mapping_start; /// DOCME - size_t mapping_length; + mutable size_t mapping_length; #else int file_handle; - void *current_mapping; - off_t mapping_start; - size_t mapping_length; + mutable void *current_mapping; + mutable off_t mapping_start; + mutable size_t mapping_length; #endif protected: PCMAudioProvider(const wxString &filename); // Create base object and open the file mapping virtual ~PCMAudioProvider(); // Closes the file mapping - char * EnsureRangeAccessible(int64_t range_start, int64_t range_length); // Ensure that the given range of bytes are accessible in the file mapping and return a pointer to the first byte of the requested range + char * EnsureRangeAccessible(int64_t range_start, int64_t range_length) const; // Ensure that the given range of bytes are accessible in the file mapping and return a pointer to the first byte of the requested range /// DOCME @@ -108,7 +108,7 @@ protected: IndexVector index_points; public: - virtual void GetAudio(void *buf, int64_t start, int64_t count); + virtual void GetAudio(void *buf, int64_t start, int64_t count) const; }; // Construct the right PCM audio provider (if any) for the file diff --git a/aegisub/src/audio_provider_ram.cpp b/aegisub/src/audio_provider_ram.cpp index ca468e301..927c26f30 100644 --- a/aegisub/src/audio_provider_ram.cpp +++ b/aegisub/src/audio_provider_ram.cpp @@ -36,6 +36,7 @@ #include "config.h" +#include "audio_controller.h" #include "audio_provider_ram.h" #include "dialog_progress.h" #include "frame_main.h" @@ -131,7 +132,7 @@ void RAMAudioProvider::Clear() { /// @param start /// @param count /// -void RAMAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) { +void RAMAudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const { // Requested beyond the length of audio if (start+count > num_samples) { int64_t oldcount = count; diff --git a/aegisub/src/audio_provider_ram.h b/aegisub/src/audio_provider_ram.h index 1c5cb9a62..efba843ae 100644 --- a/aegisub/src/audio_provider_ram.h +++ b/aegisub/src/audio_provider_ram.h @@ -58,5 +58,5 @@ public: ~RAMAudioProvider(); bool AreSamplesNativeEndian() const { return samples_native_endian; } - void GetAudio(void *buf, int64_t start, int64_t count); + void GetAudio(void *buf, int64_t start, int64_t count) const; }; diff --git a/aegisub/src/audio_renderer.cpp b/aegisub/src/audio_renderer.cpp index b7d8c66cd..2767e7c84 100644 --- a/aegisub/src/audio_renderer.cpp +++ b/aegisub/src/audio_renderer.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2009, Niels Martin Hansen +// Copyright (c) 2009-2010, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -42,9 +42,13 @@ #include #endif +#include "block_cache.h" #include "audio_renderer.h" #include "include/aegisub/audio_provider.h" +#undef min +#undef max + AudioRendererBitmapCacheBitmapFactory::AudioRendererBitmapCacheBitmapFactory(AudioRenderer *_renderer) { @@ -56,7 +60,7 @@ AudioRendererBitmapCacheBitmapFactory::AudioRendererBitmapCacheBitmapFactory(Aud wxBitmap *AudioRendererBitmapCacheBitmapFactory::ProduceBlock(int i) { (void)i; - return new wxBitmap(renderer->cache_bitmap_width, renderer->pixel_height, 32); + return new wxBitmap(renderer->cache_bitmap_width, renderer->pixel_height, 24); } @@ -68,7 +72,7 @@ void AudioRendererBitmapCacheBitmapFactory::DisposeBlock(wxBitmap *bmp) size_t AudioRendererBitmapCacheBitmapFactory::GetBlockSize() const { - return sizeof(wxBitmap) + renderer->cache_bitmap_width * renderer->pixel_height * 4; + return sizeof(wxBitmap) + renderer->cache_bitmap_width * renderer->pixel_height * 3; } @@ -78,7 +82,8 @@ AudioRenderer::AudioRenderer() : cache_bitmap_width(32) // arbitrary value for now , bitmaps_normal(256, AudioRendererBitmapCacheBitmapFactory(this)) , bitmaps_selected(256, AudioRendererBitmapCacheBitmapFactory(this)) -, cache_maxsize(0) +, cache_bitmap_maxsize(0) +, cache_renderer_maxsize(0) , renderer(0) , provider(0) { @@ -166,7 +171,12 @@ void AudioRenderer::SetAudioProvider(AudioProvider *_provider) void AudioRenderer::SetCacheMaxSize(size_t max_size) { - cache_maxsize = max_size; + // Limit the bitmap cache sizes to 16 MB hard, to avoid the risk of exhausting + // system bitmap object resources and similar. Experimenting shows that 16 MB + // bitmap cache should be plenty even if working with a one hour audio clip. + cache_bitmap_maxsize = std::min(max_size/8, (size_t)0x1000000); + // The renderer gets whatever is left. + cache_renderer_maxsize = max_size - 2*cache_bitmap_maxsize; } @@ -174,9 +184,10 @@ void AudioRenderer::ResetBlockCount() { if (provider) { - size_t num_bitmaps = (size_t)((provider->GetNumSamples() + pixel_samples - 1) / pixel_samples); - bitmaps_normal.SetBlockCount(num_bitmaps); - bitmaps_selected.SetBlockCount(num_bitmaps); + size_t rendered_width = (size_t)((provider->GetNumSamples() + pixel_samples - 1) / pixel_samples); + cache_numblocks = rendered_width / cache_bitmap_width; + bitmaps_normal.SetBlockCount(cache_numblocks); + bitmaps_selected.SetBlockCount(cache_numblocks); } } @@ -204,16 +215,16 @@ wxBitmap AudioRenderer::GetCachedBitmap(int i, bool selected) void AudioRenderer::Render(wxDC &dc, wxPoint origin, int start, int length, bool selected) { - assert(start >= 0); - assert(length >= 0); - assert(start >= 0); if (!provider) return; if (!renderer) return; + if (length <= 0) return; - // Last absolute pixel strip to render - int end = start + length - 1; + // One past last absolute pixel strip to render + int end = start + length; + // One past last X coordinate to render on + int lastx = origin.x + length; // Figure out which range of bitmaps are required int firstbitmap = start / cache_bitmap_width; // And the offset in it to start its use at @@ -223,20 +234,36 @@ void AudioRenderer::Render(wxDC &dc, wxPoint origin, int start, int length, bool // How many columns of the last bitmap to use int lastbitmapoffset = end % cache_bitmap_width; - // Two basic cases now: Either firstbitmap is the same as lastbitmap, or they're different. + // Check if we need to render any blank audio past the last bitmap from cache, + // this happens if we're asked to render more audio than the provider has. + if (lastbitmap >= (int)cache_numblocks) + { + lastbitmap = cache_numblocks - 1; + lastbitmapoffset = cache_bitmap_width; + + if (firstbitmap > lastbitmap) + firstbitmap = lastbitmap; + } + + // Three basic cases now: + // * Either we're just rendering blank audio, + // * Or there is exactly one bitmap to render, + // * Or there is more than one bitmap to render. // origin is passed by value because we'll be using it as a local var to keep track // of rendering progress! - if (firstbitmap == lastbitmap) + if (start / cache_bitmap_width >= (int)cache_numblocks) { - // These better be the same: The first to the last column of the single bitmap - // to use should equal the length of the area to render. - assert(lastbitmapoffset - firstbitmapoffset == length); - + // Do nothing, the blank audio rendering will happen later + } + else if (firstbitmap == lastbitmap) + { + const int renderwidth = lastbitmapoffset - firstbitmapoffset; wxBitmap bmp = GetCachedBitmap(firstbitmap, selected); wxMemoryDC bmpdc(bmp); - dc.Blit(origin, wxSize(length, pixel_height), &bmpdc, wxPoint(firstbitmapoffset, 0)); + dc.Blit(origin, wxSize(renderwidth, pixel_height), &bmpdc, wxPoint(firstbitmapoffset, 0)); + origin.x += renderwidth; } else { @@ -244,32 +271,40 @@ void AudioRenderer::Render(wxDC &dc, wxPoint origin, int start, int length, bool { bmp = GetCachedBitmap(firstbitmap, selected); + // Can't use dc.DrawBitmap here because we need to clip the bitmap wxMemoryDC bmpdc(bmp); dc.Blit(origin, wxSize(cache_bitmap_width-firstbitmapoffset, pixel_height), &bmpdc, wxPoint(firstbitmapoffset, 0)); origin.x += cache_bitmap_width-firstbitmapoffset; } - for (int i = 1; i < lastbitmap; ++i) + for (int i = firstbitmap+1; i < lastbitmap; ++i) { bmp = GetCachedBitmap(i, selected); - wxMemoryDC bmpdc(bmp); - dc.Blit(origin, wxSize(cache_bitmap_width, pixel_height), &bmpdc, wxPoint(0, 0)); + dc.DrawBitmap(bmp, origin); origin.x += cache_bitmap_width; } { bmp = GetCachedBitmap(lastbitmap, selected); + // We also need clipping here wxMemoryDC bmpdc(bmp); - dc.Blit(origin, wxSize(lastbitmapoffset, pixel_height), &bmpdc, wxPoint(0, 0)); + dc.Blit(origin, wxSize(lastbitmapoffset+1, pixel_height), &bmpdc, wxPoint(0, 0)); + origin.x += lastbitmapoffset+1; } } + // Now render blank audio from origin to end + if (origin.x < lastx) + { + renderer->RenderBlank(dc, wxRect(origin.x-1, origin.y, lastx-origin.x+1, pixel_height), selected); + } + if (selected) - bitmaps_selected.Age(cache_maxsize / 8); + bitmaps_selected.Age(cache_bitmap_maxsize); else - bitmaps_normal.Age(cache_maxsize / 8); - renderer->AgeCache(3 * cache_maxsize / 4); + bitmaps_normal.Age(cache_bitmap_maxsize); + renderer->AgeCache(cache_renderer_maxsize); } @@ -297,7 +332,7 @@ void AudioRendererBitmapProvider::SetSamplesPerPixel(int _pixel_samples) if (pixel_samples == _pixel_samples) return; pixel_samples = _pixel_samples; - + OnSetSamplesPerPixel(); } diff --git a/aegisub/src/audio_renderer.h b/aegisub/src/audio_renderer.h index 668e9eb8b..9d9daac2a 100644 --- a/aegisub/src/audio_renderer.h +++ b/aegisub/src/audio_renderer.h @@ -1,4 +1,4 @@ -// Copyright (c) 2009, Niels Martin Hansen +// Copyright (c) 2009-2010, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -43,8 +43,6 @@ #include #endif -#include "block_cache.h" - // Some forward declarations for outside stuff class AudioProvider; @@ -54,6 +52,11 @@ class AudioRendererBitmapProvider; class AudioRenderer; +#ifndef AGI_BLOCK_CACHE_INCLUDED +#error You much include "block_cache.h" before "audio_renderer.h" +#endif + + /// @class AudioRendererBitmapCacheBitmapFactory /// @brief Produces wxBitmap objects for DataBlockCache storage for the audio renderer @@ -111,8 +114,12 @@ class AudioRenderer { AudioRendererBitmapCache bitmaps_normal; /// Cached bitmaps for marked (selected) audio ranges AudioRendererBitmapCache bitmaps_selected; - /// The maximum allowed size of the cache, in bytes - size_t cache_maxsize; + /// Number of blocks in the bitmap caches + size_t cache_numblocks; + /// The maximum allowed size of each bitmap cache, in bytes + size_t cache_bitmap_maxsize; + /// The maximum allowed size of the renderer's cache, in bytes + size_t cache_renderer_maxsize; /// Actual renderer for bitmaps AudioRendererBitmapProvider *renderer; @@ -284,7 +291,7 @@ public: AudioRendererBitmapProvider() : provider(0), pixel_samples(0) { }; /// @brief Destructor - ~AudioRendererBitmapProvider() { } + virtual ~AudioRendererBitmapProvider() { } /// @brief Rendering function /// @param bmp Bitmap to render to @@ -295,6 +302,15 @@ public: /// the width and height to render. virtual void Render(wxBitmap &bmp, int start, bool selected) = 0; + /// @brief Blank audio rendering function + /// @param dc The device context to render to + /// @param rect The rectangle to fill with the image of blank audio + /// @param selected Whether to render as being selected or not + /// + /// Deriving classes must implement this method. The rectangle has the height + /// of the entire canvas the audio is being rendered in. + virtual void RenderBlank(wxDC &dc, const wxRect &rect, bool selected) = 0; + /// @brief Change audio provider /// @param provider Audio provider to change to void SetProvider(AudioProvider *provider); diff --git a/aegisub/src/audio_renderer_spectrum.cpp b/aegisub/src/audio_renderer_spectrum.cpp index ab96542d8..a75d98187 100644 --- a/aegisub/src/audio_renderer_spectrum.cpp +++ b/aegisub/src/audio_renderer_spectrum.cpp @@ -1,5 +1,5 @@ -// Copyright (c) 2005, 2006, Rodrigo Braz Monteiro -// Copyright (c) 2006, 2007, Niels Martin Hansen +// Copyright (c) 2005-2006, Rodrigo Braz Monteiro +// Copyright (c) 2006-2010, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -33,770 +33,331 @@ /// @file audio_renderer_spectrum.cpp /// @brief Caching frequency-power spectrum renderer for audio display /// @ingroup audio_ui -/// #include "config.h" #ifndef AGI_PRE -#include -#ifdef _OPENMP -#include -#endif - #include -#include -#include -#include -#include +#include +#include +#include #endif #include +#include "block_cache.h" #include "include/aegisub/audio_provider.h" +#include "audio_colorscheme.h" +#include "audio_renderer.h" #include "audio_renderer_spectrum.h" -#include "colorspace.h" + +#ifdef WITH_FFTW +#include +#else #include "fft.h" +#endif #include "main.h" #include "utils.h" - -/// @class AudioSpectrumCache -/// @brief Base class for the frequency-power cache tree. -/// -/// The cached frequency-power are kept in a shallow tree-structure composed of -/// intermediate branches and final leaves, both accessed through a common -/// interface, which is this class. -/// -/// The term "cache line" here means the frequency-power data derived from -/// some range of audio samples, calculated by a single FFT. -class AudioSpectrumCache { -public: - - /// The type of frequency-power data at one point in time. - typedef std::vector CacheLine; - - - /// The type of timestamps for last access. - typedef unsigned int CacheAccessTime; - - /// Holds last-access data for a range of range of cache lines. - /// Ranges can overlap, in case overlapping FFT's are used to increase precision. - struct CacheAgeData { - /// Last time this range of cache lines were accessed. - CacheAccessTime access_time; - /// First line in the range. - unsigned long first_line; - /// Number of lines in the range. - unsigned long num_lines; - - /// @brief Comparison operator for sorting age data by last access time. - /// @param second The age data structure to compare against. - /// @return Returns true if last access time of this range is less than that of the other. - bool operator< (const CacheAgeData& second) const { return access_time < second.access_time; } - - /// @brief Constructor. - /// @param t Initial access time to set. - /// @param first First line in the range. - /// @param num Number of lines in the range. - /// - CacheAgeData(CacheAccessTime t, unsigned long first, unsigned long num) : access_time(t), first_line(first), num_lines(num) { } - }; - - /// Type of a list of cache age data. - typedef std::vector CacheAgeList; - - /// @brief Retrieve frequency-power data. - /// @param i Index of the block to get the line from. - /// @param overlap Index of the overlap in the block to get the line for. - /// @param created [out] Set to true if the data had to be calculated, false if the data - /// was found in cache. - /// @param access_time Timestamp to mark the cache data as accessed at. - /// @return Returns a reference to the frequency-power data requested. - /// - /// The data are fetched from the cache if they are cached, otherwise the required - /// audio data are retrieved, the FFT derived, and power data calculated. - virtual CacheLine& GetLine(unsigned long i, unsigned int overlap, bool &created, CacheAccessTime access_time) = 0; - - /// @brief Get the size of the cache subtree. - /// @return Number of lines stored in all nodes below this one in the tree. - virtual size_t GetManagedLineCount() = 0; - - /// @brief Retrieve cache access times. - /// @param ages [in,out] List to append age data of managed lines to. - /// - /// Existing contents of the list is kept, new entries are added to the end. - virtual void GetLineAccessTimes(CacheAgeList &ages) = 0; - - /// @brief Remove data from the cache. - /// @param line_id Index of the block the cache node to remove starts at. - /// @return Returns true if the object the method was called on no longer manages - // any cache lines and can safely be deleted. - virtual bool KillLine(unsigned long line_id) = 0; - - - /// @brief Set the FFT size used globally. - /// @param new_length Number of audio samples to use in calculation. - static void SetLineLength(unsigned long new_length) - { - line_length = new_length; - null_line.resize(new_length, 0); - } - - - /// @brief Destructor, does nothing in base class. - virtual ~AudioSpectrumCache() {}; - -protected: - - /// Global template for cache lines. - static CacheLine null_line; - /// Number of audio samples used for power calculation, determining the - /// frequency resolution of the frequency-power data. - static unsigned long line_length; -}; - -// Actual variables allocating memory for the static class members -AudioSpectrumCache::CacheLine AudioSpectrumCache::null_line; -unsigned long AudioSpectrumCache::line_length; - - - -/// @class FinalSpectrumCache -/// @brief Leaf node in frequency-power cache tree, holds actual data. -/// -/// This class stores frequency-power data and is responsible for calculating it as well. -class FinalSpectrumCache : public AudioSpectrumCache { -private: - - /// The stored data. - std::vector data; - - unsigned long start, ///< Start of block range - length; ///< Number of blocks - unsigned int overlaps; ///< How many lines per block - - /// Last access time for cache management. - CacheAccessTime last_access; - -public: - - /// @brief Returns stored frequency-power data. - /// @param i Index of the block to get the line from. - /// @param overlap Index of the overlap in the block to get the line for. - /// @param created [out] Set to true if the data had to be calculated, false if the data - /// was found in cache. - /// @param access_time Timestamp to mark the cache data as accessed at. - /// @return Returns a reference to the frequency-power data requested. - CacheLine& GetLine(unsigned long i, unsigned int overlap, bool &created, CacheAccessTime access_time) - { - last_access = access_time; - - // This check ought to be redundant - if (i >= start && i-start < length) - return data[i - start + overlap*length]; - else - return null_line; - } - - - /// @brief Get number of lines in cache. - /// @return Number of lines stored at this leaf. - size_t GetManagedLineCount() - { - return data.size(); - } - - - /// @brief Add own cache age data to list of age data. - /// @param ages [in,out] List to add cache age data to. - /// - /// Produces a single cache age data object, representing the entire node, - /// and adds it to the list. - void GetLineAccessTimes(CacheAgeList &ages) - { - ages.push_back(CacheAgeData(last_access, start, data.size())); - } - - - /// @brief Return true if this is the line to remove. - /// @param line_id Index of the block the cache node to remove starts at. - /// @return Returns true if this is the cache block to remove. - /// - /// This function won't actually delete anything, instead it is the responsibility - /// of the caller to delete the cache node if this function returns true. - bool KillLine(unsigned long line_id) - { - return start == line_id; - } - - - /// @brief Constructor, derives FFT and calculates frequency-power data. - /// @param provider Audio provider to get audio from. - /// @param _start Index of first block to calculate data for. - /// @param _length Number of blocks to calculate data for. - /// @param _overlaps Number of lines to calculate per block. - FinalSpectrumCache(AudioProvider *provider, unsigned long _start, unsigned long _length, unsigned int _overlaps) - { - start = _start; - length = _length; - overlaps = _overlaps; - - if (overlaps < 1) overlaps = 1; - // Add an upper limit to number of overlaps or trust user to do sane things? - // Any limit should probably be a function of length - - assert(length > 2); - - // First fill the data vector with blanks - // Both start and end are included in the range stored, so we have end-start+1 elements - data.resize(length*overlaps, null_line); - - unsigned int overlap_offset = line_length / overlaps * 2; // FIXME: the result seems weird/wrong without this factor 2, but why? - - FFT fft; // Use FFTW instead? A wavelet? - - for (unsigned int overlap = 0; overlap < overlaps; ++overlap) { - // Start sample number of the next line calculated - // line_length is half of the number of samples used to calculate a line, since half of the output from - // a Fourier transform of real data is redundant, and not interesting for the purpose of creating - // a frequency/power spectrum. - int64_t sample = start * line_length*2 + overlap*overlap_offset; - - long len = length; -#ifdef _OPENMP -#pragma omp parallel shared(overlap,len) -#endif - { - short *raw_sample_data = new short[line_length*2]; - float *sample_data = new float[line_length*2]; - float *out_r = new float[line_length*2]; - float *out_i = new float[line_length*2]; - -#ifdef _OPENMP -#pragma omp for -#endif - for (long i = 0; i < len; ++i) { - // Initialize - sample = start * line_length*2 + overlap*overlap_offset + i*line_length*2; - - provider->GetAudio(raw_sample_data, sample, line_length*2); - for (size_t j = 0; j < line_length; ++j) { - sample_data[j*2] = (float)raw_sample_data[j*2]; - sample_data[j*2+1] = (float)raw_sample_data[j*2+1]; - } - - fft.Transform(line_length*2, sample_data, out_r, out_i); - - CacheLine &line = data[i + length*overlap]; - for (size_t j = 0; j < line_length; ++j) { - line[j] = sqrt(out_r[j]*out_r[j] + out_i[j]*out_i[j]); - } - - //sample += line_length*2; - } - - delete[] raw_sample_data; - delete[] sample_data; - delete[] out_r; - delete[] out_i; - } - } - } - - - /// @brief Destructor, does nothing. - /// - /// All data is managed by C++ types and gets deleted when those types' - /// destructors are implicitly run. - virtual ~FinalSpectrumCache() - { - } - -}; - - -/// @class IntermediateSpectrumCache -/// @brief Intermediate node in the spectrum cache tree. -/// -/// References further nodes in the spectrum cache tree and delegates operations to them. -class IntermediateSpectrumCache : public AudioSpectrumCache { -private: - - /// The child-nodes in the cache tree. - std::vector sub_caches; - - unsigned long start, ///< DOCME - length, ///< DOCME - subcache_length; ///< DOCME - unsigned int overlaps; ///< Number of overlaps used. - bool subcaches_are_final; ///< Are the children leaf nodes? - int depth; ///< How deep is this in the tree. - - /// Audio provider to pass on to child nodes. - AudioProvider *provider; - -public: - - /// @brief Delegate line retrieval to a child node. - /// @param i Index of the block to get the line from. - /// @param overlap Index of the overlap in the block to get the line for. - /// @param created [out] Set to true if the data had to be calculated, false if the data - /// was found in cache. - /// @param access_time Timestamp to mark the cache data as accessed at. - /// @return Returns a reference to the frequency-power data requested. - /// - /// Will create the required child node if it doesn't exist yet. - CacheLine &GetLine(unsigned long i, unsigned int overlap, bool &created, CacheAccessTime access_time) - { - if (i >= start && i-start <= length) { - // Determine which sub-cache this line resides in - size_t subcache = (i-start) / subcache_length; - assert(subcache < sub_caches.size()); - - if (!sub_caches[subcache]) { - created = true; - if (subcaches_are_final) { - sub_caches[subcache] = new FinalSpectrumCache(provider, start+subcache*subcache_length, subcache_length, overlaps); - } else { - sub_caches[subcache] = new IntermediateSpectrumCache(provider, start+subcache*subcache_length, subcache_length, overlaps, depth+1); - } - } - - return sub_caches[subcache]->GetLine(i, overlap, created, access_time); - } else { - return null_line; - } - } - - - /// @brief Iterate all direct children and return the sum of their managed line count. - /// @return Returns the sum of the managed line count of all children. - size_t GetManagedLineCount() - { - size_t res = 0; - for (size_t i = 0; i < sub_caches.size(); ++i) { - if (sub_caches[i]) - res += sub_caches[i]->GetManagedLineCount(); - } - return res; - } - - - /// @brief Get access time data for all child nodes in cache tree. - /// @param ages [in,out] List for child nodes to add their data to. - void GetLineAccessTimes(CacheAgeList &ages) - { - for (size_t i = 0; i < sub_caches.size(); ++i) { - if (sub_caches[i]) - sub_caches[i]->GetLineAccessTimes(ages); - } - } - - - /// @brief Remove block with given index from cache. - /// @param line_id Index of the block the cache node to remove starts at. - /// @return Returns true if this node has no more live childs, false if - /// there is a least one line child. - /// - /// Iterates the child nodes, calls the method recursively on all live - /// nodes, deletes any node returning true, and counts number of nodes - /// still alive, returning true if any are alive. - bool KillLine(unsigned long line_id) - { - int sub_caches_left = 0; - for (size_t i = 0; i < sub_caches.size(); ++i) { - if (sub_caches[i]) { - if (sub_caches[i]->KillLine(line_id)) { - delete sub_caches[i]; - sub_caches[i] = 0; - } else { - sub_caches_left++; - } - } - } - return sub_caches_left == 0; - } - - - /// @brief Constructor. - /// @param _provider Audio provider to pass to child nodes. - /// @param _start Index of first block to manage. - /// @param _length Number of blocks to manage. - /// @param _overlaps Number of lines per block. - /// @param _depth Number of levels in the tree above this node. - /// - /// Determine how many sub-caches are required, how big they - /// should be and allocates memory to store their pointers. - IntermediateSpectrumCache(AudioProvider *_provider, unsigned long _start, unsigned long _length, unsigned int _overlaps, int _depth) - { - provider = _provider; - start = _start; - length = _length; - overlaps = _overlaps; - depth = _depth; - - // FIXME: this calculation probably needs tweaking - int num_subcaches = 1; - unsigned long tmp = length; - while (tmp > 0) { - tmp /= 16; - num_subcaches *= 2; - } - subcache_length = length / (num_subcaches-1); - - subcaches_are_final = num_subcaches <= 4; - - sub_caches.resize(num_subcaches, 0); - } - - - /// @brief Destructor, deletes all still-live sub caches. - virtual ~IntermediateSpectrumCache() - { - for (size_t i = 0; i < sub_caches.size(); ++i) - if (sub_caches[i]) - delete sub_caches[i]; - } - -}; - - - - -/// @class AudioSpectrumCacheManager -/// @brief Manages a frequency-power cache tree. -/// -/// The primary task of this class is to manage the amount of memory consumed by -/// the cache and delete items when it grows too large. -class AudioSpectrumCacheManager { -private: - - /// Root node of the cache tree. - IntermediateSpectrumCache *cache_root; - - unsigned long cache_hits, ///< Number of times the cache was used to retrieve data - cache_misses; ///< Number of times data had to be calculated - - /// Current time, used for cache aging purposes. - AudioSpectrumCache::CacheAccessTime cur_time; - - /// Maximum number of lines to keep in cache. - unsigned long max_lines_cached; - -public: - - /// @brief Wrapper around cache tree, to get frequency-power data - /// @param i Block to get data from. - /// @param overlap Line in block to get data from. - /// @return Returns a reference to the requested line. - /// - AudioSpectrumCache::CacheLine &GetLine(unsigned long i, unsigned int overlap) - { - bool created = false; - AudioSpectrumCache::CacheLine &res = cache_root->GetLine(i, overlap, created, cur_time++); - if (created) - cache_misses++; - else - cache_hits++; - return res; - } - - - /// @brief Remove old data from the cache. - /// - /// Ages the cache by finding the least recently accessed data and removing cache data - /// until the total number of lines stored in the tree is less than the maximum. - void Age() - { - LOG_D("audio/renderer/spectrum/cache") << "stats: hits=" << cache_hits << " misses=" << cache_misses << " misses%=" << cache_misses/float(cache_hits+cache_misses)*100 << " managed lines=" << cache_root->GetManagedLineCount() << "(max=" << max_lines_cached << ")"; - - // 0 means no limit - if (max_lines_cached == 0) - return; - // No reason to proceed with complicated stuff if the count is too small - // (FIXME: does this really pay off?) - if (cache_root->GetManagedLineCount() < max_lines_cached) - return; - - // Get and sort ages - AudioSpectrumCache::CacheAgeList ages; - cache_root->GetLineAccessTimes(ages); - std::sort(ages.begin(), ages.end()); - - // Number of lines we have found used so far - // When this exceeds max_lines_caches go into kill-mode - unsigned long cumulative_lines = 0; - // Run backwards through the line age list (the most recently accessed items are at end) - AudioSpectrumCache::CacheAgeList::reverse_iterator it = ages.rbegin(); - - // Find the point where we have too many lines cached - while (cumulative_lines < max_lines_cached) { - if (it == ages.rend()) { - LOG_D("audio/renderer/spectrum/") << "done aging did not exeed max_lines_cached"; - return; - } - cumulative_lines += it->num_lines; - ++it; - } - - // By here, we have exceeded max_lines_cached so backtrack one - --it; - - // And now start cleaning up - for (; it != ages.rend(); ++it) { - cache_root->KillLine(it->first_line); - } - - LOG_D("audio/renderer/spectrum/") << "done aging, managed lines now=" << cache_root->GetManagedLineCount() << " (max=" << max_lines_cached << ")"; - assert(cache_root->GetManagedLineCount() < max_lines_cached); - } - +/// Allocates blocks of derived data for the audio spectrum +struct AudioSpectrumCacheBlockFactory { + /// Pointer back to the owning spectrum renderer + AudioSpectrumRenderer *spectrum; /// @brief Constructor - /// @param provider Audio provider to pass to cache tree nodes. - /// @param line_length Number of audio samples to use per block. - /// @param num_lines Number of blocks to produce in total from the audio. - /// @param num_overlaps Number of overlaps per block. - /// - /// Initialises the cache tree root and calculates the maximum number of cache lines - /// to keep based on the Audio Spectrum Memory Max configuration setting. - AudioSpectrumCacheManager(AudioProvider *provider, unsigned long line_length, unsigned long num_lines, unsigned int num_overlaps) - { - cache_hits = cache_misses = 0; - cur_time = 0; - cache_root = new IntermediateSpectrumCache(provider, 0, num_lines, num_overlaps, 0); + /// @param s The owning spectrum renderer + AudioSpectrumCacheBlockFactory(AudioSpectrumRenderer *s) : spectrum(s) { } - // option is stored in megabytes, but we want number of bytes - unsigned long max_cache_size = OPT_GET("Audio/Renderer/Spectrum/Memory Max")->GetInt(); - // It can't go too low - if (max_cache_size < 5) max_cache_size = 128; - max_cache_size *= 1024 * 1024; - unsigned long line_size = sizeof(AudioSpectrumCache::CacheLine::value_type) * line_length; - max_lines_cached = max_cache_size / line_size; + /// @brief Allocate and fill a data block + /// @param i Index of the block to produce data for + /// @return Newly allocated and filled block + /// + /// The filling is delegated to the spectrum renderer + float *ProduceBlock(size_t i) + { + float *res = new float[((size_t)1)<derivation_size]; + spectrum->FillBlock(i, res); + return res; } - - /// @brief Destructor, deletes the cache tree root node. - ~AudioSpectrumCacheManager() + /// @brief De-allocate a cache block + /// @param block The block to dispose of + void DisposeBlock(float *block) { - delete cache_root; + delete[] block; + } + + /// @brief Calculate the in-memory size of a spec + /// @return The size in bytes of a spectrum cache block + size_t GetBlockSize() const + { + return sizeof(float) << spectrum->derivation_size; } }; -// AudioSpectrum, documented in .h file +/// @brief Cache for audio spectrum frequency-power data +class AudioSpectrumCache + : public DataBlockCache { +public: + AudioSpectrumCache(size_t block_count, AudioSpectrumRenderer *renderer) + : DataBlockCache( + block_count, AudioSpectrumCacheBlockFactory(renderer)) + { + } +}; -AudioSpectrum::AudioSpectrum(AudioProvider *_provider) + + +AudioSpectrumRenderer::AudioSpectrumRenderer() +: AudioRendererBitmapProvider() +, cache(0) +, colors_normal(12) +, colors_selected(12) +, derivation_size(8) +, derivation_dist(8) +, audio_scratch(0) +#ifdef WITH_FFTW +, dft_plan(0) +, dft_input(0) +, dft_output(0) +#else +, fft_scratch(0) +#endif { - provider = _provider; - - // Determine the quality of the spectrum rendering based on an index - int quality_index = OPT_GET("Audio/Renderer/Spectrum/Quality")->GetInt(); - if (quality_index < 0) quality_index = 0; - if (quality_index > 5) quality_index = 5; // no need to go freaking insane - - // Line length determines the balance between resolution in the time and frequency domains. - // Larger line length gives better resolution in frequency domain, - // smaller gives better resolution in time domain. - // Any values uses the same amount of memory, but larger values takes (slightly) more CPU. - // Line lengths must be powers of 2 due to the FFT algorithm. - // 2^8 is a good compromise between time and frequency domain resolution, any smaller - // gives an unreasonably low resolution in the frequency domain. - - // Increasing the number of overlaps gives better resolution in the time domain. - // Doubling the number of overlaps doubles memory and CPU use, and also - // doubles resolution in the time domain. - - switch (quality_index) { - case 0: - // No overlaps, good comprimise between time/frequency resolution. - // 4 bytes used per sample. - line_length = 1<<8; - fft_overlaps = 1; - break; - case 1: - // Double frequency resolution, the resulting half time resolution - // is countered with an overlap. - // 8 bytes per sample. - line_length = 1<<9; - fft_overlaps = 2; - break; - case 2: - // Resulting double resolution in both domains. - // 16 bytes per sample. - line_length = 1<<9; - fft_overlaps = 4; - break; - case 3: - // Double frequency and quadrouble time resolution. - // 32 bytes per sample. - line_length = 1<<9; - fft_overlaps = 8; - break; - case 4: - // Quadrouble resolution in both domains. - // 64 bytes per sample. - line_length = 1<<10; - fft_overlaps = 16; - break; - case 5: - // Eight-double resolution in both domains. - // 256 bytes per sample. - line_length = 1<<11; - fft_overlaps = 64; - break; - default: - throw _T("Internal error in AudioSpectrum class - impossible quality index"); - } - - int64_t _num_lines = provider->GetNumSamples() / line_length / 2; - num_lines = (unsigned long)_num_lines; - - AudioSpectrumCache::SetLineLength(line_length); - cache = new AudioSpectrumCacheManager(provider, line_length, num_lines, fft_overlaps); - - power_scale = 1; - minband = OPT_GET("Audio/Renderer/Spectrum/Cutoff")->GetInt(); - maxband = line_length - minband * 2/3; // TODO: make this customisable? - - // Generate colour maps - unsigned char *palptr = colours_normal; - for (int i = 0; i < 256; i++) { - //hsl_to_rgb(170 + i * 2/3, 128 + i/2, i, palptr+0, palptr+1, palptr+2); // Previous - hsl_to_rgb((255+128-i)/2, 128 + i/2, MIN(255,2*i), palptr+0, palptr+1, palptr+2); // Icy blue - palptr += 3; - } - palptr = colours_selected; - for (int i = 0; i < 256; i++) { - //hsl_to_rgb(170 + i * 2/3, 128 + i/2, i*3/4+64, palptr+0, palptr+1, palptr+2); - hsl_to_rgb((255+128-i)/2, 128 + i/2, MIN(255,3*i/2+64), palptr+0, palptr+1, palptr+2); // Icy blue - palptr += 3; - } + colors_normal.InitIcyBlue_Normal(); + colors_selected.InitIcyBlue_Selected(); } -AudioSpectrum::~AudioSpectrum() +AudioSpectrumRenderer::~AudioSpectrumRenderer() +{ + // This sequence will clean up + provider = 0; + RecreateCache(); +} + + +void AudioSpectrumRenderer::RecreateCache() { delete cache; + delete[] audio_scratch; + cache = 0; + audio_scratch = 0; + +#ifdef WITH_FFTW + if (dft_plan) + { + fftw_destroy_plan(dft_plan); + fftw_free(dft_input); + fftw_free(dft_output); + dft_plan = 0; + dft_input = 0; + dft_output = 0; + } +#else + delete[] fft_scratch; + fft_scratch = 0; +#endif + + if (provider) + { + size_t block_count = (size_t)((provider->GetNumSamples() + (size_t)(1<> derivation_dist); + cache = new AudioSpectrumCache(block_count, this); + +#ifdef WITH_FFTW + dft_input = (double*)fftw_malloc(sizeof(double) * (2<GetLine(baseline, overlap); - ++overlap; - if (overlap >= fft_overlaps) { - overlap = 0; - ++baseline; - } - - // Apply a "compressed" scaling to the signal power - for (unsigned int j = 0; j < line_length; j++) { - // First do a simple linear scale power calculation -- 8 gives a reasonable default scaling - power[j] = line[j] * upscale; - if (power[j] > maxpower * 2/3) { - double p = power[j] - twothirdmaxpower; - p = log(p) * onethirdmaxpower / logoverscale; - power[j] = p + twothirdmaxpower; - } - } - -/// @internal Macro that stores pixel data, depends on local variables in AudioSpectrum::RenderRange -#define WRITE_PIXEL \ - if (intensity < 0) intensity = 0; \ - if (intensity > 255) intensity = 255; \ - img[((imgheight-y-1)*imgpitch+x)*3 + 0] = palette[intensity*3+0]; \ - img[((imgheight-y-1)*imgpitch+x)*3 + 1] = palette[intensity*3+1]; \ - img[((imgheight-y-1)*imgpitch+x)*3 + 2] = palette[intensity*3+2]; - - // Handle horizontal expansion - int next_line_imgcol = imgleft + imgwidth * (i - first_line + 1) / (last_line - first_line + 1); - if (next_line_imgcol >= imgpitch) - next_line_imgcol = imgpitch-1; - - for (int x = imgcol; x <= next_line_imgcol; ++x) { - - // Decide which rendering algo to use - if (maxband - minband > imgheight) { - // more than one frequency sample per pixel (vertically compress data) - // pick the largest value per pixel for display - - // Iterate over pixels, picking a range of samples for each - for (int y = 0; y < imgheight; ++y) { - int sample1 = MAX(0,maxband * y/imgheight + minband); - int sample2 = MIN(signed(line_length-1),maxband * (y+1)/imgheight + minband); - float maxval = 0; - for (int samp = sample1; samp <= sample2; samp++) { - if (power[samp] > maxval) maxval = power[samp]; - } - int intensity = int(256 * maxval / maxpower); - WRITE_PIXEL - } - } - else { - // less than one frequency sample per pixel (vertically expand data) - // interpolate between pixels - // can also happen with exactly one sample per pixel, but how often is that? - - // Iterate over pixels, picking the nearest power values - for (int y = 0; y < imgheight; ++y) { - float ideal = (float)(y+1.)/imgheight * maxband; - float sample1 = power[(int)floor(ideal)+minband]; - float sample2 = power[(int)ceil(ideal)+minband]; - float frac = ideal - floor(ideal); - int intensity = int(((1-frac)*sample1 + frac*sample2) / maxpower * 256); - WRITE_PIXEL - } - } - } - -/// @internal The WRITE_PIXEL macro is only defined inside AudioSpectrum::RenderRange -#undef WRITE_PIXEL +void AudioSpectrumRenderer::SetResolution(size_t _derivation_size, size_t _derivation_dist) +{ + if (derivation_dist != _derivation_dist) + { + derivation_dist = _derivation_dist; + if (cache) + cache->Age(0); } - delete[] power; - - cache->Age(); + if (derivation_size != _derivation_size) + { + derivation_size = _derivation_size; + RecreateCache(); + } } -void AudioSpectrum::SetScaling(float _power_scale) +void AudioSpectrumRenderer::FillBlock(size_t block_index, float *block) { - power_scale = _power_scale; + assert(cache); + assert(block); + + int64_t first_sample = ((int64_t)block_index) << derivation_dist; + provider->GetAudio(audio_scratch, first_sample, 2 << derivation_size); + +#ifdef WITH_FFTW + // Convert audio data to float range [-1;+1) + for (size_t si = 0; si < (size_t)(2< 0; --si) + { + *block++ = log10( sqrt(o[0][0] * o[0][0] + o[0][1] * o[0][1]) * scale_factor + 1 ); + o++; + } +#else + float *fft_input = fft_scratch; + float *fft_real = fft_scratch + (2 << derivation_size); + float *fft_imag = fft_scratch + (4 << derivation_size); + + // Convert audio data to float range [-1;+1) + for (size_t si = 0; si < (size_t)(2< 0; --si) + { + // With x in range [0;1], log10(x*9+1) will also be in range [0;1], + // although the FFT output can apparently get greater magnitudes than 1 + // despite the input being limited to [-1;+1). + *block++ = log10( sqrt(*fft_real * *fft_real + *fft_imag * *fft_imag) * scale_factor + 1 ); + fft_real++; fft_imag++; + } +#endif } +void AudioSpectrumRenderer::Render(wxBitmap &bmp, int start, bool selected) +{ + if (!cache) + return; + + assert(bmp.IsOk()); + assert(bmp.GetDepth() == 24); + + int end = start + bmp.GetWidth(); + + assert(start >= 0); + assert(end >= 0); + assert(end >= start); + + // Prepare an image buffer to write + wxImage img(bmp.GetSize()); + unsigned char *imgdata = img.GetData(); + ptrdiff_t stride = img.GetWidth()*3; + int imgheight = img.GetHeight(); + + AudioColorScheme *pal = selected ? &colors_selected : &colors_normal; + + /// @todo Make minband and maxband configurable + int minband = 0; + int maxband = 1 << derivation_size; + + // ax = absolute x, absolute to the virtual spectrum bitmap + for (int ax = start; ax < end; ++ax) + { + // Derived audio data + size_t block_index = (size_t)(ax * pixel_samples) >> derivation_dist; + float *power = cache->Get(block_index); + + // Prepare bitmap writing + unsigned char *px = imgdata + (imgheight-1) * stride + (ax - start) * 3; + + // Scale up or down vertically? + if (imgheight > 1<= imgdata); + assert(px < imgdata + imgheight*stride); + float ideal = (float)(y+1.)/imgheight * (maxband-minband) + minband; + float sample1 = power[(int)floor(ideal)+minband]; + float sample2 = power[(int)ceil(ideal)+minband]; + float frac = ideal - floor(ideal); + float val = (1-frac)*sample1 + frac*sample2; + pal->map(val*amplitude_scale, px); + px -= stride; + } + } + else + { + // Pick greatest + for (int y = 0; y < imgheight; ++y) + { + assert(px >= imgdata); + assert(px < imgdata + imgheight*stride); + int sample1 = std::max(0, maxband * y/imgheight + minband); + int sample2 = std::min((1< maxval) maxval = power[samp]; + pal->map(maxval*amplitude_scale, px); + px -= stride; + } + } + } + + wxBitmap tmpbmp(img); + wxMemoryDC targetdc(bmp); + targetdc.DrawBitmap(tmpbmp, 0, 0); +} +void AudioSpectrumRenderer::RenderBlank(wxDC &dc, const wxRect &rect, bool selected) +{ + // Get the colour of silence + AudioColorScheme *pal = selected ? &colors_selected : &colors_normal; + unsigned char color_raw[4]; + pal->map(0.0, color_raw); + wxColour col(color_raw[0], color_raw[1], color_raw[2]); + + dc.SetBrush(wxBrush(col)); + dc.SetPen(wxPen(col)); + dc.DrawRectangle(rect); +} + + +void AudioSpectrumRenderer::AgeCache(size_t max_size) +{ + if (cache) + cache->Age(max_size); +} + diff --git a/aegisub/src/audio_renderer_spectrum.h b/aegisub/src/audio_renderer_spectrum.h index e89cedc96..c9e9843f0 100644 --- a/aegisub/src/audio_renderer_spectrum.h +++ b/aegisub/src/audio_renderer_spectrum.h @@ -1,5 +1,4 @@ -// Copyright (c) 2005, 2006, Rodrigo Braz Monteiro -// Copyright (c) 2006, 2007, Niels Martin Hansen +// Copyright (c) 2009, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -40,65 +39,102 @@ #include #endif -class AudioProvider; - -// Specified and implemented in cpp file, interface is private to spectrum code -class AudioSpectrumCacheManager; +#ifdef WITH_FFTW +#include +#endif -/// @class AudioSpectrum + +// Specified and implemented in cpp file, to avoid pulling in too much +// complex template code in this header. +class AudioSpectrumCache; +struct AudioSpectrumCacheBlockFactory; + + + +/// @class AudioSpectrumRenderer /// @brief Render frequency-power spectrum graphs for audio data. /// -/// Renders frequency-power spectrum graphs of PCM audio data using a fast fourier transform -/// to derive the data. The frequency-power data are cached to avoid re-computing them -/// frequently, and the cache size is limited by a configuration setting. -/// -/// The spectrum image is rendered to a 32 bit RGB bitmap. Power data is scaled linearly -/// and not logarithmically, since the rendering is done with limited precision, but -/// an amplification factor can be specified to see different ranges. -class AudioSpectrum { -private: +/// Renders frequency-power spectrum graphs of PCM audio data using a derivation function +/// such as the fast fourier transform. +class AudioSpectrumRenderer : public AudioRendererBitmapProvider { + friend struct AudioSpectrumCacheBlockFactory; /// Internal cache management for the spectrum - AudioSpectrumCacheManager *cache; + AudioSpectrumCache *cache; /// Colour table used for regular rendering - unsigned char colours_normal[256*3]; + AudioColorScheme colors_normal; /// Colour table used for rendering the audio selection - unsigned char colours_selected[256*3]; + AudioColorScheme colors_selected; - /// The audio provider to use as source - AudioProvider *provider; + /// Binary logarithm of number of samples to use in deriving frequency-power data + size_t derivation_size; - unsigned long line_length; ///< Number of frequency components per line (half of number of samples) - unsigned long num_lines; ///< Number of lines needed for the audio - unsigned int fft_overlaps; ///< Number of overlaps used in FFT - float power_scale; ///< Amplification of displayed power - int minband; ///< Smallest frequency band displayed - int maxband; ///< Largest frequency band displayed + /// Binary logarithm of number of samples between the start of derivations + size_t derivation_dist; + + /// @brief Reset in response to changing audio provider + /// + /// Overrides the OnSetProvider event handler in the base class, to reset things + /// when the audio provider is changed. + void OnSetProvider(); + + /// @brief Recreates the cache + /// + /// To be called when the number of blocks in cache might have changed, + // eg. new audio provider or new resolution. + void RecreateCache(); + + /// @brief Fill a block with frequency-power data for a time range + /// @param block_index Index of the block to fill data for + /// @param[out] block Address to write the data to + void FillBlock(size_t block_index, float *block); + +#ifdef WITH_FFTW + /// FFTW plan data + fftw_plan dft_plan; + /// Pre-allocated input array for FFTW + double *dft_input; + /// Pre-allocated output array for FFTW + fftw_complex *dft_output; +#else + /// Pre-allocated scratch area for doing FFT derivations + float *fft_scratch; +#endif + + /// Pre-allocated scratch area for storing raw audio data + int16_t *audio_scratch; public: /// @brief Constructor - /// @param _provider Audio provider to render spectrum data for. - /// - /// Reads configuration data for the spectrum display and initialises itself following that. - AudioSpectrum(AudioProvider *_provider); + AudioSpectrumRenderer(); + /// @brief Destructor - ~AudioSpectrum(); + virtual ~AudioSpectrumRenderer(); - /// @brief Render a range of audio spectrum to a bitmap buffer. - /// @param range_start First audio sample in the range to render. - /// @param range_end Last audio sample in the range to render. - /// @param selected Use the alternate colour palette? - /// @param img Pointer to 32 bit RGBX data - /// @param imgleft Offset from left edge of bitmap to render to, in pixels - /// @param imgwidth Width of bitmap to render, in pixels - /// @param imgpitch Offset from one scanline to the next in the bitmap, in bytes - /// @param imgheight Number of lines in the bitmap - void RenderRange(int64_t range_start, int64_t range_end, bool selected, unsigned char *img, int imgleft, int imgwidth, int imgpitch, int imgheight); + /// @brief Render a range of audio spectrum + /// @param bmp [in,out] Bitmap to render into, also carries lenght information + /// @param start First column of pixel data in display to render + /// @param selected Whether to use the alternate colour scheme + void Render(wxBitmap &bmp, int start, bool selected); - /// @brief Set the amplification to use when rendering. - /// @param _power_scale Amplification factor to use. - void SetScaling(float _power_scale); + /// @brief Render blank area + void RenderBlank(wxDC &dc, const wxRect &rect, bool selected); + + /// @brief Set the derivation resolution + /// @param derivation_size Binary logarithm of number of samples to use in deriving frequency-power data + /// @param derivation_dist Binary logarithm of number of samples between the start of derivations + /// + /// The derivations done will each use 2^derivation_size audio samples and at a distance + /// of 2^derivation_dist samples. + /// + /// The derivation distance must be smaller than or equal to the size. If the distance + /// is specified too large, it will be clamped to the size. + void SetResolution(size_t derivation_size, size_t derivation_dist); + + /// @brief Cleans up the cache + /// @param max_size Maximum size in bytes for the cache + void AgeCache(size_t max_size); }; diff --git a/aegisub/src/audio_renderer_waveform.cpp b/aegisub/src/audio_renderer_waveform.cpp new file mode 100644 index 000000000..4e77e043a --- /dev/null +++ b/aegisub/src/audio_renderer_waveform.cpp @@ -0,0 +1,178 @@ +// Copyright (c) 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_renderer_waveform.cpp +/// @ingroup audio_ui +/// +/// Render a waveform display of PCM audio data + +#include "config.h" + +#ifndef AGI_PRE +#include + +#include +#endif + +#include "block_cache.h" +#include "include/aegisub/audio_provider.h" +#include "audio_colorscheme.h" +#include "audio_renderer.h" +#include "audio_renderer_waveform.h" +#include "colorspace.h" + +#undef min +#undef max + + + +AudioWaveformRenderer::AudioWaveformRenderer() +: AudioRendererBitmapProvider() +, colors_normal(6) +, colors_selected(6) +, audio_buffer(0) +{ + colors_normal.InitIcyBlue_Normal(); + colors_selected.InitIcyBlue_Selected(); +} + + +AudioWaveformRenderer::~AudioWaveformRenderer() +{ + delete[] audio_buffer; +} + + +void AudioWaveformRenderer::Render(wxBitmap &bmp, int start, bool selected) +{ + wxMemoryDC dc(bmp); + wxRect rect(wxPoint(0, 0), bmp.GetSize()); + int midpoint = rect.height / 2; + + AudioColorScheme *pal = selected ? &colors_selected : &colors_normal; + + // Fill the background + dc.SetBrush(wxBrush(pal->get(0.0f))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(rect); + + // Make sure we've got a buffer to fill with audio data + if (!audio_buffer) + { + // Buffer for one pixel strip of audio + size_t buffer_needed = pixel_samples * provider->GetChannels() * provider->GetSampleRate() * provider->GetBytesPerSample(); + audio_buffer = new char[buffer_needed]; + } + + int64_t cur_sample = start * pixel_samples; + + assert(provider->GetBytesPerSample() == 2); + assert(provider->GetChannels() == 1); + + wxPen pen_peaks(wxPen(pal->get(0.4f))); + wxPen pen_avgs(wxPen(pal->get(0.7f))); + + for (int x = 0; x < rect.width; ++x) + { + provider->GetAudio(audio_buffer, cur_sample, pixel_samples); + cur_sample += pixel_samples; + + int peak_min = 0, peak_max = 0; + int64_t avg_min_accum = 0, avg_max_accum = 0; + const int16_t *aud = (const int16_t *)audio_buffer; + for (int si = pixel_samples; si > 0; --si, ++aud) + { + if (*aud > 0) + { + peak_max = std::max(peak_max, (int)*aud); + avg_max_accum += *aud; + } + else + { + peak_min = std::min(peak_min, (int)*aud); + avg_min_accum += *aud; + } + } + + // midpoint is half height + peak_min = std::max((int)(peak_min * amplitude_scale * midpoint) / 0x8000, -midpoint); + peak_max = std::min((int)(peak_max * amplitude_scale * midpoint) / 0x8000, midpoint); + int avg_min = std::max((int)(avg_min_accum * amplitude_scale * midpoint / pixel_samples) / 0x8000, -midpoint); + int avg_max = std::min((int)(avg_max_accum * amplitude_scale * midpoint / pixel_samples) / 0x8000, midpoint); + + dc.SetPen(pen_peaks); + dc.DrawLine(x, midpoint - peak_max, x, midpoint - peak_min); + dc.SetPen(pen_avgs); + dc.DrawLine(x, midpoint - avg_max, x, midpoint - avg_min); + } + + // Horizontal zero-point line + dc.SetPen(wxPen(pal->get(1.0f))); + dc.DrawLine(0, midpoint, rect.width, midpoint); +} + + +void AudioWaveformRenderer::RenderBlank(wxDC &dc, const wxRect &rect, bool selected) +{ + AudioColorScheme *pal = selected ? &colors_selected : &colors_normal; + + wxColor line(pal->get(1.0)); + wxColor bg(pal->get(0.0)); + + // Draw the line as background above and below, and line in the middle, to avoid + // overdraw flicker (the common theme in all of audio display direct drawing). + int halfheight = rect.height / 2; + + dc.SetBrush(wxBrush(bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(rect.x, rect.y, rect.width, halfheight); + dc.DrawRectangle(rect.x, rect.y + halfheight + 1, rect.width, rect.height - halfheight - 1); + + dc.SetPen(wxPen(line)); + dc.DrawLine(rect.x, rect.y+halfheight, rect.x+rect.width, rect.y+halfheight); +} + + + +void AudioWaveformRenderer::OnSetProvider() +{ + delete[] audio_buffer; + audio_buffer = 0; +} + + +void AudioWaveformRenderer::OnSetSamplesPerPixel() +{ + delete[] audio_buffer; + audio_buffer = 0; +} + + diff --git a/aegisub/src/audio_renderer_waveform.h b/aegisub/src/audio_renderer_waveform.h new file mode 100644 index 000000000..5abf20b20 --- /dev/null +++ b/aegisub/src/audio_renderer_waveform.h @@ -0,0 +1,77 @@ +// Copyright (c) 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_renderer_waveform.h +/// @see audio_renderer_waveform.cpp +/// @ingroup audio_ui +/// +/// Render a waveform display of PCM audio data + +#include + + + +class AudioWaveformRenderer : public AudioRendererBitmapProvider { + /// Colour table used for regular rendering + AudioColorScheme colors_normal; + + /// Colour table used for rendering the audio selection + AudioColorScheme colors_selected; + + /// Pre-allocated buffer for audio fetched from provider + char *audio_buffer; + +protected: + virtual void OnSetProvider(); + virtual void OnSetSamplesPerPixel(); + +public: + /// @brief Constructor + AudioWaveformRenderer(); + + /// @brief Destructor + virtual ~AudioWaveformRenderer(); + + /// @brief Render a range of audio waveform + /// @param bmp [in,out] Bitmap to render into, also carries lenght information + /// @param start First column of pixel data in display to render + /// @param selected Whether to use the alternate colour scheme + void Render(wxBitmap &bmp, int start, bool selected); + + /// @brief Render blank area + void RenderBlank(wxDC &dc, const wxRect &rect, bool selected); + + /// @brief Cleans up the cache + /// @param max_size Maximum size in bytes for the cache + /// + /// Does nothing for waveform renderer, since it does not have a backend cache + void AgeCache(size_t max_size) { } +}; diff --git a/aegisub/src/audio_timing.h b/aegisub/src/audio_timing.h new file mode 100644 index 000000000..a1d0dc39e --- /dev/null +++ b/aegisub/src/audio_timing.h @@ -0,0 +1,136 @@ +// Copyright (c) 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_timing.h +/// @brief Construction-functions for timing controller objects +/// @ingroup audio_ui + + + +class AssDialogue; +class AudioController; + + +/// @class AudioTimingController +/// @brief Base class for objects controlling audio timing +/// +/// There is just one active audio timing controller at a time per audio controller. +/// The timing controller manages the timing mode and supplies markers that can be +/// manupulated to the audio display, as well as the current selection. +/// +/// The timing controller must then be sent the marker drag events as well as clicks +/// in empty areas of the audio display. +class AudioTimingController : public AudioMarkerProvider { +public: + /// @brief Get any warning message to show in the audio display + /// @return The warning message to show, may be empty if there is none + virtual wxString GetWarningMessage() const = 0; + + /// @brief Get the sample range the user is most likely to want to see for the current state + /// @return A sample range + /// + /// This is used for "bring working area into view" operations. + virtual AudioController::SampleRange GetIdealVisibleSampleRange() const = 0; + + /// @brief Get the primary playback range + /// @return A sample range + /// + /// Get the sample range the user is most likely to want to play back currently. + virtual AudioController::SampleRange GetPrimaryPlaybackRange() const = 0; + + /// @brief Does this timing mode have labels on the audio display? + /// @return True if this timing mode needs labels on the audio display. + /// + /// This is labels for things such as karaoke syllables. When labels are required, some vertical + /// space is set off for them in the drawing of the audio display. + virtual bool HasLabels() const = 0; + + /// @brief Go to next timing unit + /// + /// Advances the timing controller cursor to the next timing unit, for example the next dialogue + /// line or the next karaoke syllable. + virtual void Next() = 0; + + /// @brief Go to the previous timing unit + /// + /// Rewinds the timing controller to the previous timing unit. + virtual void Prev() = 0; + + /// @brief Commit all changes + /// + /// Stores all changes permanently. + virtual void Commit() = 0; + + /// @brief Revert all changes + /// + /// Revert all changes to the last committed state. + virtual void Revert() = 0; + + /// @brief Determine if a position is close to a draggable marker + /// @param sample The audio sample index to test + /// @param sensitivity Distance in samples to consider markers as nearby + /// @return True if a marker is close by the given sample, as defined by sensitivity + /// + /// This is solely for hit-testing against draggable markers, for controlling the mouse cursor. + virtual bool IsNearbyMarker(int64_t sample, int sensitivity) const = 0; + + /// @brief The user pressed the left button down at an empty place in the audio + /// @param sample The audio sample index the user clicked + /// @param sensitivity Distance in samples to consider existing markers + /// @return An audio marker or 0. If a marker is returned and the user starts dragging + /// the mouse after pressing down the button, the returned marker is being dragged. + virtual AudioMarker * OnLeftClick(int64_t sample, int sensitivity) = 0; + + /// @brief The user pressed the right button down at an empty place in the audio + /// @param sample The audio sample index the user clicked + /// @param sensitivity Distance in samples to consider existing markers + /// @return An audio marker or 0. If a marker is returned and the user starts dragging + /// the mouse after pressing down the button, the returned marker is being dragged. + virtual AudioMarker * OnRightClick(int64_t sample, int sensitivity) = 0; + + /// @brief The user dragged a timing marker + /// @param marker The marker being dragged + /// @param new_position Sample position the marker was dragged to + virtual void OnMarkerDrag(AudioMarker *marker, int64_t new_position) = 0; + + /// @brief Destructor + /// + /// Does nothing in the base class, only present for virtual destruction. + virtual ~AudioTimingController() { } +}; + + + +/// @brief Create a standard dialogue audio timing controller +/// @param audio_controller The audio controller to own the timing controller +/// @param selection_controller The selection controller to manage the set of lines being timed +AudioTimingController *CreateDialogueTimingController(AudioController *audio_controller, SelectionController *selection_controller); + diff --git a/aegisub/src/audio_timing_dialogue.cpp b/aegisub/src/audio_timing_dialogue.cpp new file mode 100644 index 000000000..a7687073e --- /dev/null +++ b/aegisub/src/audio_timing_dialogue.cpp @@ -0,0 +1,478 @@ +// Copyright (c) 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_timing_dialogue.cpp +/// @brief Default timing mode for dialogue subtitles +/// @ingroup audio_ui + + +#ifndef AGI_PRE +#include +#include +#endif + +#include "ass_time.h" +#include "ass_dialogue.h" +#include "selection_controller.h" +#include "audio_controller.h" +#include "audio_timing.h" +#include "utils.h" + + + +/// @class AudioMarkerDialogueTiming +/// @brief AudioMarker implementation for AudioTimingControllerDialogue +/// +/// Audio marker intended to live in pairs of two, taking styles depending +/// on which marker in the pair is to the left and which is to the right. +class AudioMarkerDialogueTiming : public AudioMarker { + /// The other marker for the dialogue line's pair + AudioMarkerDialogueTiming *other; + + /// Current sample position of this marker + int64_t position; + + /// Draw style for the marker + wxPen style; + /// Foot style for the marker + FeetStyle feet; + +public: + // AudioMarker interface + virtual int64_t GetPosition() const { return position; } + virtual wxPen GetStyle() const { return style; } + virtual FeetStyle GetFeet() const { return feet; } + virtual bool CanSnap() const { return true; } + +public: + // Specific interface + + /// @brief Move the marker to a new position + /// @param new_position The position to move the marker to, in audio samples + /// + /// If the marker moves to the opposite side of the ohter marker in the pair, + /// the styles of the two markers will be changed to match the new start/end + /// relationship of them. + void SetPosition(int64_t new_position); + + + /// @brief Constructor + /// + /// Initialises the fields to default values. + AudioMarkerDialogueTiming(); + + + /// @brief Initialise a pair of dialogue markers to be a pair + /// @param marker1 The first marker in the pair to make + /// @param marker2 The second marker in the pair to make + /// + /// This checks that the markers aren't already part of a pair, and then sets their + /// "other" field. Positions and styles aren't affected. + static void InitPair(AudioMarkerDialogueTiming *marker1, AudioMarkerDialogueTiming *marker2); +}; + + + +/// @class AudioTimingControllerDialogue +/// @brief Default timing mode for dialogue subtitles +/// +/// Displays a start and end marker for an active subtitle line, and allows +/// for those markers to be dragged. Dragging the start/end markers changes +/// the audio selection. +/// +/// When the audio rendering code is expanded to support it, inactive lines +/// will also be shown as shaded lines that cannot be changed. +/// +/// Another later expansion will be to affect the timing of multiple selected +/// lines at the same time, if they e.g. have end1==start2. +class AudioTimingControllerDialogue : public AudioTimingController, private SelectionListener { + /// Start and end markers for the active line + AudioMarkerDialogueTiming markers[2]; + + /// Has the timing been modified by the user? + bool timing_modified; + + /// Get the leftmost of the markers + AudioMarkerDialogueTiming *GetLeftMarker(); + const AudioMarkerDialogueTiming *GetLeftMarker() const; + /// Get the rightmost of the markers + AudioMarkerDialogueTiming *GetRightMarker(); + const AudioMarkerDialogueTiming *GetRightMarker() const; + + /// The owning audio controller + AudioController *audio_controller; + + /// Update the audio controller's selection + void UpdateSelection(); + + /// Selection controller managing the set of lines currently being timed + SelectionController *selection_controller; + +private: + // SubtitleSelectionListener interface + virtual void OnActiveLineChanged(AssDialogue *new_line); + virtual void OnSelectedSetChanged(const Selection &lines_added, const Selection &lines_removed); + +public: + // AudioMarkerProvider interface + virtual void GetMarkers(const AudioController::SampleRange &range, AudioMarkerVector &out_markers) const; + + // AudioTimingController interface + virtual wxString GetWarningMessage() const; + virtual AudioController::SampleRange GetIdealVisibleSampleRange() const; + virtual AudioController::SampleRange GetPrimaryPlaybackRange() const; + virtual bool HasLabels() const; + virtual void Next(); + virtual void Prev(); + virtual void Commit(); + virtual void Revert(); + virtual bool IsNearbyMarker(int64_t sample, int sensitivity) const; + virtual AudioMarker * OnLeftClick(int64_t sample, int sensitivity); + virtual AudioMarker * OnRightClick(int64_t sample, int sensitivity); + virtual void OnMarkerDrag(AudioMarker *marker, int64_t new_position); + +public: + // Specific interface + + /// @brief Constructor + AudioTimingControllerDialogue(AudioController *audio_controller, SelectionController *selection_controller); + virtual ~AudioTimingControllerDialogue(); +}; + + + +AudioTimingController *CreateDialogueTimingController(AudioController *audio_controller, SelectionController *selection_controller) +{ + return new AudioTimingControllerDialogue(audio_controller, selection_controller); +} + + + +// AudioMarkerDialogueTiming + +void AudioMarkerDialogueTiming::SetPosition(int64_t new_position) +{ + position = new_position; + + if (other) + { + /// @todo Make this depend on configuration + wxPen style_left = wxPen(*wxRED, 2); + wxPen style_right = wxPen(*wxBLUE, 2); + if (position < other->position) + { + feet = Feet_Right; + other->feet = Feet_Left; + style = style_left; + other->style = style_right; + } + else if (position > other->position) + { + feet = Feet_Left; + other->feet = Feet_Right; + style = style_right; + other->style = style_left; + } + } +} + + +AudioMarkerDialogueTiming::AudioMarkerDialogueTiming() +: other(0) +, position(0) +, style(*wxTRANSPARENT_PEN) +, feet(Feet_None) +{ + // Nothing more to do +} + + +void AudioMarkerDialogueTiming::InitPair(AudioMarkerDialogueTiming *marker1, AudioMarkerDialogueTiming *marker2) +{ + assert(marker1->other == 0); + assert(marker2->other == 0); + + marker1->other = marker2; + marker2->other = marker1; +} + + + +// AudioTimingControllerDialogue + +AudioTimingControllerDialogue::AudioTimingControllerDialogue(AudioController *audio_controller, SelectionController *selection_controller) +: timing_modified(false) +, audio_controller(audio_controller) +, selection_controller(selection_controller) +{ + assert(audio_controller != 0); + + AudioMarkerDialogueTiming::InitPair(&markers[0], &markers[1]); + + selection_controller->AddSelectionListener(this); +} + + +AudioTimingControllerDialogue::~AudioTimingControllerDialogue() +{ + selection_controller->RemoveSelectionListener(this); +} + + + +AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetLeftMarker() +{ + return markers[0].GetPosition() < markers[1].GetPosition() ? &markers[0] : &markers[1]; +} + +const AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetLeftMarker() const +{ + return markers[0].GetPosition() < markers[1].GetPosition() ? &markers[0] : &markers[1]; +} + +AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetRightMarker() +{ + return markers[0].GetPosition() < markers[1].GetPosition() ? &markers[1] : &markers[0]; +} + +const AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetRightMarker() const +{ + return markers[0].GetPosition() < markers[1].GetPosition() ? &markers[1] : &markers[0]; +} + + + +void AudioTimingControllerDialogue::GetMarkers(const AudioController::SampleRange &range, AudioMarkerVector &out_markers) const +{ + if (range.contains(markers[0].GetPosition())) + out_markers.push_back(&markers[0]); + if (range.contains(markers[1].GetPosition())) + out_markers.push_back(&markers[1]); +} + + + +void AudioTimingControllerDialogue::OnActiveLineChanged(AssDialogue *new_line) +{ + /// @todo Need to change policy to default commit at some point + Revert(); // revert will read and reset the selection/markers +} + + + +void AudioTimingControllerDialogue::OnSelectedSetChanged(const Selection &lines_added, const Selection &lines_removed) +{ + /// @todo Create new passive markers, perhaps +} + + + +wxString AudioTimingControllerDialogue::GetWarningMessage() const +{ + // We have no warning messages currently, maybe add the old "Modified" message back later? + return wxString(); +} + + + +AudioController::SampleRange AudioTimingControllerDialogue::GetIdealVisibleSampleRange() const +{ + return GetPrimaryPlaybackRange(); +} + + + +AudioController::SampleRange AudioTimingControllerDialogue::GetPrimaryPlaybackRange() const +{ + return AudioController::SampleRange( + GetLeftMarker()->GetPosition(), + GetRightMarker()->GetPosition()); +} + + + +bool AudioTimingControllerDialogue::HasLabels() const +{ + return false; +} + + + +void AudioTimingControllerDialogue::Next() +{ + selection_controller->NextLine(); +} + + + +void AudioTimingControllerDialogue::Prev() +{ + selection_controller->PrevLine(); +} + + + +void AudioTimingControllerDialogue::Commit() +{ + /// @todo Make these depend on actual configuration + const bool next_line_on_commit = true; + const int default_duration = 5000; // milliseconds + + int new_start_ms = audio_controller->MillisecondsFromSamples(GetLeftMarker()->GetPosition()); + int new_end_ms = audio_controller->MillisecondsFromSamples(GetRightMarker()->GetPosition()); + + // Store back new times + if (timing_modified) + { + Selection sel; + selection_controller->GetSelectedSet(sel); + for (Selection::iterator sub = sel.begin(); sub != sel.end(); ++sub) + { + (*sub)->Start.SetMS(new_start_ms); + (*sub)->End.SetMS(new_end_ms); + } + /// @todo Set an undo point + timing_modified = false; + } + + // Assume that the next line might be zero-timed and should thus get a default timing + if (next_line_on_commit) + { + markers[0].SetPosition(audio_controller->SamplesFromMilliseconds(new_end_ms)); + markers[1].SetPosition(audio_controller->SamplesFromMilliseconds(new_end_ms + default_duration)); + UpdateSelection(); + } +} + + + +void AudioTimingControllerDialogue::Revert() +{ + AssDialogue *line = selection_controller->GetActiveLine(); + if (line) + { + AssTime new_start = line->Start; + AssTime new_end = line->End; + + if (new_start.GetMS() != 0 || new_end.GetMS() != 0) + { + markers[0].SetPosition(audio_controller->SamplesFromMilliseconds(new_start.GetMS())); + markers[1].SetPosition(audio_controller->SamplesFromMilliseconds(new_end.GetMS())); + timing_modified = false; + UpdateSelection(); + } + } +} + + + +bool AudioTimingControllerDialogue::IsNearbyMarker(int64_t sample, int sensitivity) const +{ + AudioController::SampleRange range(sample-sensitivity, sample+sensitivity); + + return range.contains(markers[0].GetPosition()) || range.contains(markers[1].GetPosition()); +} + + +AudioMarker * AudioTimingControllerDialogue::OnLeftClick(int64_t sample, int sensitivity) +{ + assert(sensitivity >= 0); + + int64_t dist_l, dist_r; + + AudioMarkerDialogueTiming *left = GetLeftMarker(); + AudioMarkerDialogueTiming *right = GetRightMarker(); + + dist_l = tabs(left->GetPosition() - sample); + dist_r = tabs(right->GetPosition() - sample); + + if (dist_l < dist_r && dist_l <= sensitivity) + { + // Clicked near the left marker: + // Insta-move it and start dragging it + left->SetPosition(sample); + audio_controller->OnTimingControllerMarkerMoved(this, left); + timing_modified = true; + UpdateSelection(); + return left; + } + + if (dist_r < dist_l && dist_r <= sensitivity) + { + // Clicked near the right marker: + // Only drag it. For insta-move, the user must right-click. + return right; + } + + // Clicked far from either marker: + // Insta-set the left marker to the clicked position and return the right as the dragged one, + // such that if the user does start dragging, he will create a new selection from scratch + left->SetPosition(sample); + audio_controller->OnTimingControllerMarkerMoved(this, left); + timing_modified = true; + UpdateSelection(); + return right; +} + + + +AudioMarker * AudioTimingControllerDialogue::OnRightClick(int64_t sample, int sensitivity) +{ + AudioMarkerDialogueTiming *right = GetRightMarker(); + + right->SetPosition(sample); + audio_controller->OnTimingControllerMarkerMoved(this, right); + timing_modified = true; + UpdateSelection(); + return right; +} + + + +void AudioTimingControllerDialogue::OnMarkerDrag(AudioMarker *marker, int64_t new_position) +{ + assert(marker == &markers[0] || marker == &markers[1]); + + static_cast(marker)->SetPosition(new_position); + audio_controller->OnTimingControllerMarkerMoved(this, marker); + timing_modified = true; + + UpdateSelection(); +} + + + +void AudioTimingControllerDialogue::UpdateSelection() +{ + audio_controller->OnTimingControllerUpdatedPrimaryRange(this); +} + + diff --git a/aegisub/src/base_grid.cpp b/aegisub/src/base_grid.cpp index a2e50f259..588ed91e2 100644 --- a/aegisub/src/base_grid.cpp +++ b/aegisub/src/base_grid.cpp @@ -50,7 +50,8 @@ #include "ass_dialogue.h" #include "ass_file.h" #include "ass_style.h" -#include "audio_display.h" +#include "audio_controller.h" +#include "selection_controller.h" #include "compat.h" #include "frame_main.h" #include "main.h" @@ -1251,9 +1252,12 @@ void BaseGrid::OnKeyPress(wxKeyEvent &event) { } // Other events, send to audio display + /// @todo Reinstate this, or make a better solution, when audio is getting stabler again + /* if (context->audio->loaded) { context->audio->GetEventHandler()->ProcessEvent(event); } + */ else event.Skip(); } diff --git a/aegisub/src/base_grid.h b/aegisub/src/base_grid.h index 27b8c79d4..2bde01568 100644 --- a/aegisub/src/base_grid.h +++ b/aegisub/src/base_grid.h @@ -149,7 +149,6 @@ public: /// DOCME SubsEditBox *editBox; - /// DOCME bool byFrame; diff --git a/aegisub/src/block_cache.h b/aegisub/src/block_cache.h index b63d98b6a..da815935e 100644 --- a/aegisub/src/block_cache.h +++ b/aegisub/src/block_cache.h @@ -41,6 +41,8 @@ #include #endif +#define AGI_BLOCK_CACHE_INCLUDED 1 + /// @class BasicDataBlockFactory /// @brief Simple factory for allocating blocks for DataBlockCache @@ -143,7 +145,7 @@ class DataBlockCache { for (size_t bi = 0; bi < mb.blocks.size(); ++bi) { BlockT *b = mb.blocks[bi]; - if (!b) + if (b) factory.DisposeBlock(b); } diff --git a/aegisub/src/config/config_windows0.h b/aegisub/src/config/config_windows0.h index d0e31b26a..be8f75b2b 100644 --- a/aegisub/src/config/config_windows0.h +++ b/aegisub/src/config/config_windows0.h @@ -107,6 +107,12 @@ //#define FINAL_RELEASE +// Use FFTW instead of shipped FFT code +// FFTW is a very fast library for computing the discrete fourier transform, but is a bit +// tricky to get working on Windows, and has the additional problem of being GPL licensed. +// Enable this option to use FFTW to get faster rendering of the audio spectrogram +//#define WITH_FFTW +//#pragma comment(lib,libfftw.lib) // Specify tags the update checker accepts // See for details on tags. // Depending on who will be using your build, you may or may not want to have the diff --git a/aegisub/src/dialog_automation.h b/aegisub/src/dialog_automation.h index 37e6a18d7..e7a2505c0 100644 --- a/aegisub/src/dialog_automation.h +++ b/aegisub/src/dialog_automation.h @@ -46,8 +46,11 @@ #endif -/// DOCME -namespace Automation4 { class ScriptManager; class Script; class AutoloadScriptManager; }; +namespace Automation4 { + class ScriptManager; + class AutoloadScriptManager; + class Script; +}; /// DOCME diff --git a/aegisub/src/dialog_detached_video.cpp b/aegisub/src/dialog_detached_video.cpp index 138711f1d..028c8107c 100644 --- a/aegisub/src/dialog_detached_video.cpp +++ b/aegisub/src/dialog_detached_video.cpp @@ -42,6 +42,7 @@ #include /// Must be included last. #endif +#include "audio_controller.h" #include "dialog_detached_video.h" #include "frame_main.h" #include "main.h" diff --git a/aegisub/src/dialog_fonts_collector.cpp b/aegisub/src/dialog_fonts_collector.cpp index f9210ca63..5e2a31de4 100644 --- a/aegisub/src/dialog_fonts_collector.cpp +++ b/aegisub/src/dialog_fonts_collector.cpp @@ -51,6 +51,7 @@ #include "ass_file.h" #include "ass_override.h" #include "ass_style.h" +#include "audio_controller.h" #include "compat.h" #include "dialog_fonts_collector.h" #include "font_file_lister.h" @@ -58,6 +59,7 @@ #include "libresrc/libresrc.h" #include "main.h" #include "scintilla_text_ctrl.h" +#include "selection_controller.h" #include "subs_grid.h" #include "utils.h" diff --git a/aegisub/src/dialog_kara_timing_copy.cpp b/aegisub/src/dialog_kara_timing_copy.cpp index c99699d85..e6e7a2d07 100644 --- a/aegisub/src/dialog_kara_timing_copy.cpp +++ b/aegisub/src/dialog_kara_timing_copy.cpp @@ -55,6 +55,7 @@ #include "help_button.h" #include "libresrc/libresrc.h" #include "main.h" +#include "selection_controller.h" #include "subs_grid.h" #include "utils.h" #include "validators.h" diff --git a/aegisub/src/dialog_search_replace.cpp b/aegisub/src/dialog_search_replace.cpp index f2bd3de33..d1be828a0 100644 --- a/aegisub/src/dialog_search_replace.cpp +++ b/aegisub/src/dialog_search_replace.cpp @@ -47,9 +47,11 @@ #include "compat.h" #include "ass_dialogue.h" #include "ass_file.h" +#include "audio_controller.h" #include "dialog_search_replace.h" #include "frame_main.h" #include "main.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" diff --git a/aegisub/src/dialog_selection.cpp b/aegisub/src/dialog_selection.cpp index 030023e99..115eed689 100644 --- a/aegisub/src/dialog_selection.cpp +++ b/aegisub/src/dialog_selection.cpp @@ -49,6 +49,7 @@ #include "dialog_selection.h" #include "help_button.h" #include "main.h" +#include "selection_controller.h" #include "subs_grid.h" #include "subs_edit_box.h" diff --git a/aegisub/src/dialog_spellchecker.cpp b/aegisub/src/dialog_spellchecker.cpp index b1d63077a..d4c48d4af 100644 --- a/aegisub/src/dialog_spellchecker.cpp +++ b/aegisub/src/dialog_spellchecker.cpp @@ -44,6 +44,7 @@ #include "ass_dialogue.h" #include "ass_file.h" +#include "audio_controller.h" #include "compat.h" #include "dialog_spellchecker.h" #include "frame_main.h" @@ -51,6 +52,7 @@ #include "libresrc/libresrc.h" #include "main.h" #include "include/aegisub/spellchecker.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" diff --git a/aegisub/src/dialog_style_editor.cpp b/aegisub/src/dialog_style_editor.cpp index 1f3f05154..8a8ee81e5 100644 --- a/aegisub/src/dialog_style_editor.cpp +++ b/aegisub/src/dialog_style_editor.cpp @@ -55,6 +55,7 @@ #include "help_button.h" #include "libresrc/libresrc.h" #include "main.h" +#include "selection_controller.h" #include "subs_grid.h" #include "subs_preview.h" #include "include/aegisub/subtitles_provider.h" diff --git a/aegisub/src/dialog_style_manager.cpp b/aegisub/src/dialog_style_manager.cpp index ae92c6ab8..f7fb895c1 100644 --- a/aegisub/src/dialog_style_manager.cpp +++ b/aegisub/src/dialog_style_manager.cpp @@ -54,6 +54,7 @@ #include "help_button.h" #include "libresrc/libresrc.h" #include "main.h" +#include "selection_controller.h" #include "standard_paths.h" #include "subs_grid.h" #include "utils.h" diff --git a/aegisub/src/dialog_styling_assistant.cpp b/aegisub/src/dialog_styling_assistant.cpp index 9f9634c99..d65cdd258 100644 --- a/aegisub/src/dialog_styling_assistant.cpp +++ b/aegisub/src/dialog_styling_assistant.cpp @@ -46,8 +46,9 @@ #include "ass_dialogue.h" #include "ass_file.h" #include "ass_style.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" -#include "audio_display.h" #include "dialog_styling_assistant.h" #include "frame_main.h" #include "help_button.h" @@ -72,8 +73,8 @@ wxDialog (parent, -1, _("Styling assistant"), wxDefaultPosition, wxDefaultSize, // Variables grid = _grid; - audio = VideoContext::Get()->audio->box->audioDisplay; - video = video->Get(); + audio = VideoContext::Get()->audio; + video = VideoContext::Get(); needCommit = false; linen = -1; @@ -269,7 +270,8 @@ void DialogStyling::OnActivate(wxActivateEvent &event) { } // Enable/disable play video/audio buttons PlayVideoButton->Enable(video->IsLoaded()); - PlayAudioButton->Enable(audio->loaded); + /// @todo Reinstate this when the audio controller is made reachable from here + //PlayAudioButton->Enable(audio->loaded); // Fix style list Styles->Set(grid->ass->GetStyles()); // Fix line selection @@ -377,7 +379,9 @@ void DialogStyling::OnPlayVideoButton(wxCommandEvent &event) { /// @param event /// void DialogStyling::OnPlayAudioButton(wxCommandEvent &event) { - audio->Play(line->Start.GetMS(),line->End.GetMS()); + audio->PlayRange(AudioController::SampleRange( + audio->SamplesFromMilliseconds(line->Start.GetMS()), + audio->SamplesFromMilliseconds(line->End.GetMS()))); TypeBox->SetFocus(); } @@ -446,9 +450,12 @@ void StyleEditBox::OnKeyDown(wxKeyEvent &event) { // Play audio if (Hotkeys.IsPressed(_T("Styling Assistant Play Audio"))) { + /// @todo Reinstate this when the audio controller is made reachable from here + /* if (diag->audio->loaded) { diag->audio->Play(diag->line->Start.GetMS(),diag->line->End.GetMS()); } + */ return; } diff --git a/aegisub/src/dialog_styling_assistant.h b/aegisub/src/dialog_styling_assistant.h index a1973299e..de21ccac2 100644 --- a/aegisub/src/dialog_styling_assistant.h +++ b/aegisub/src/dialog_styling_assistant.h @@ -56,6 +56,7 @@ class SubtitlesGrid; class DialogStyling; class AudioDisplay; class VideoContext; +class AudioController; @@ -142,7 +143,7 @@ public: AssDialogue *line; /// DOCME - AudioDisplay *audio; + AudioController *audio; /// DOCME VideoContext *video; diff --git a/aegisub/src/dialog_timing_processor.cpp b/aegisub/src/dialog_timing_processor.cpp index 6d3aca2dd..11f0ebb6c 100644 --- a/aegisub/src/dialog_timing_processor.cpp +++ b/aegisub/src/dialog_timing_processor.cpp @@ -43,6 +43,7 @@ #include "help_button.h" #include "libresrc/libresrc.h" #include "main.h" +#include "selection_controller.h" #include "subs_grid.h" #include "utils.h" #include "validators.h" diff --git a/aegisub/src/dialog_translation.cpp b/aegisub/src/dialog_translation.cpp index 6d5d5e6fa..a5a6e3797 100644 --- a/aegisub/src/dialog_translation.cpp +++ b/aegisub/src/dialog_translation.cpp @@ -45,12 +45,13 @@ #include "ass_dialogue.h" #include "ass_file.h" -#include "audio_display.h" +#include "audio_controller.h" #include "dialog_translation.h" #include "frame_main.h" #include "help_button.h" #include "hotkeys.h" #include "libresrc/libresrc.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" @@ -78,7 +79,7 @@ DialogTranslation::DialogTranslation (wxWindow *parent,AssFile *_subs,SubtitlesG subs = _subs; grid = _grid; audio = VideoContext::Get()->audio; - video = video->Get(); + video = VideoContext::Get(); // Translation controls OrigText = new ScintillaTextCtrl(this,TEXT_ORIGINAL,_T(""),wxDefaultPosition,wxSize(320,80)); @@ -132,7 +133,8 @@ DialogTranslation::DialogTranslation (wxWindow *parent,AssFile *_subs,SubtitlesG wxButton *PlayVideoButton = new wxButton(this,BUTTON_TRANS_PLAY_VIDEO,_("Play Video")); wxButton *PlayAudioButton = new wxButton(this,BUTTON_TRANS_PLAY_AUDIO,_("Play Audio")); PlayVideoButton->Enable(video->IsLoaded()); - PlayAudioButton->Enable(audio->loaded); + /// @todo Reinstate this when the audio context is made reachable from here + //PlayAudioButton->Enable(audio->loaded); ToolSizer->Add(PlayAudioButton,0,wxALL,5); ToolSizer->Add(PlayVideoButton,0,wxLEFT | wxRIGHT | wxBOTTOM,5); @@ -406,10 +408,13 @@ void DialogTranslation::OnTransBoxKey(wxKeyEvent &event) { // Play audio if (Hotkeys.IsPressed(_T("Translation Assistant Play Audio"))) { + /// @todo Reinstate this when the audio controller is made reachable from here + /* if (audio->loaded) { audio->Play(current->Start.GetMS(),current->End.GetMS()); TransText->SetFocus(); } + */ return; } @@ -439,7 +444,9 @@ void DialogTranslation::OnPlayVideoButton(wxCommandEvent &event) { /// @param event /// void DialogTranslation::OnPlayAudioButton(wxCommandEvent &event) { - audio->Play(current->Start.GetMS(),current->End.GetMS()); + audio->PlayRange(AudioController::SampleRange( + audio->SamplesFromMilliseconds(current->Start.GetMS()), + audio->SamplesFromMilliseconds(current->End.GetMS()))); TransText->SetFocus(); } diff --git a/aegisub/src/dialog_translation.h b/aegisub/src/dialog_translation.h index 75b7b569f..9c43c3dbf 100644 --- a/aegisub/src/dialog_translation.h +++ b/aegisub/src/dialog_translation.h @@ -45,6 +45,7 @@ class AudioDisplay; class ScintillaTextCtrl; class SubtitlesGrid; class VideoContext; +class AudioController; /// DOCME @@ -56,7 +57,7 @@ class DialogTranslation : public wxDialog { private: /// DOCME - AudioDisplay *audio; + AudioController *audio; /// DOCME VideoContext *video; diff --git a/aegisub/src/drop.cpp b/aegisub/src/drop.cpp index a7cebb7c4..90c7a4fc1 100644 --- a/aegisub/src/drop.cpp +++ b/aegisub/src/drop.cpp @@ -43,6 +43,7 @@ #include #endif +#include "audio_controller.h" #include "drop.h" #include "frame_main.h" diff --git a/aegisub/src/ffmpegsource_common.cpp b/aegisub/src/ffmpegsource_common.cpp index e4b474c69..678a71aa9 100644 --- a/aegisub/src/ffmpegsource_common.cpp +++ b/aegisub/src/ffmpegsource_common.cpp @@ -48,6 +48,7 @@ #include +#include "audio_controller.h" #include "compat.h" #include "ffmpegsource_common.h" #include "frame_main.h" diff --git a/aegisub/src/frame_main.cpp b/aegisub/src/frame_main.cpp index 7381e2fe2..ee20306aa 100644 --- a/aegisub/src/frame_main.cpp +++ b/aegisub/src/frame_main.cpp @@ -46,8 +46,9 @@ #include "ass_file.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" -#include "audio_display.h" #ifdef WITH_AUTOMATION #include "auto4_base.h" #endif @@ -114,8 +115,7 @@ FrameMain::FrameMain (wxArrayString args) // Initialize flags HasSelection = false; menuCreated = false; - blockAudioLoad = false; - blockAudioLoad = false; + blockVideoLoad = false; StartupLog(_T("Install PNG handler")); // Create PNG handler @@ -130,6 +130,10 @@ FrameMain::FrameMain (wxArrayString args) local_scripts = new Automation4::ScriptManager(); #endif + // Contexts and controllers + audioController = new AudioController; + audioController->AddAudioListener(this); + // Create menu and tool bars StartupLog(_T("Apply saved Maximized state")); if (OPT_GET("App/Maximized")->GetBool()) Maximize(true); @@ -206,7 +210,10 @@ FrameMain::FrameMain (wxArrayString args) /// @brief FrameMain destructor FrameMain::~FrameMain () { + VideoContext::Get()->SetVideo(_T("")); + audioController->CloseAudio(); DeInitContents(); + delete audioController; #ifdef WITH_AUTOMATION delete local_scripts; #endif @@ -492,6 +499,9 @@ void FrameMain::InitMenu() { AppendBitmapMenuItem(audioMenu, Menu_Audio_Close, _("&Close Audio"), _("Closes the currently open audio file"), GETIMAGE(close_audio_menu_16)); wxMenuItem *RecentAudParent = new wxMenuItem(audioMenu, Menu_File_Recent_Auds_Parent, _("Recent"), _T(""), wxITEM_NORMAL, RecentAuds); audioMenu->Append(RecentAudParent); + audioMenu->AppendSeparator(); + audioMenu->Append(Menu_Audio_Spectrum, _("Spectrum display"), _("Display audio as a frequency-power spectrogrph"), wxITEM_RADIO); + audioMenu->Append(Menu_Audio_Waveform, _("Waveform display"), _("Display audio as a linear amplitude graph"), wxITEM_RADIO); #ifdef _DEBUG audioMenu->AppendSeparator(); audioMenu->Append(Menu_Audio_Open_Dummy, _T("Open 2h30 Blank Audio"), _T("Open a 150 minutes blank audio clip, for debugging")); @@ -557,45 +567,52 @@ void FrameMain::InitContents() { StartupLog(_T("Create background panel")); Panel = new wxPanel(this,-1,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL | wxCLIP_CHILDREN); - // Initialize sizers - StartupLog(_T("Create main sizers")); - MainSizer = new wxBoxSizer(wxVERTICAL); - TopSizer = new wxBoxSizer(wxHORIZONTAL); - BottomSizer = new wxBoxSizer(wxHORIZONTAL); - // Video area; StartupLog(_T("Create video box")); videoBox = new VideoBox(Panel, false, ZoomBox, ass); - TopSizer->Add(videoBox,0,wxEXPAND,0); + VideoContext::Get()->audio = audioController; + wxBoxSizer *videoSizer = new wxBoxSizer(wxVERTICAL); + videoSizer->Add(videoBox, 0, wxEXPAND); + videoSizer->AddStretchSpacer(1); // Subtitles area StartupLog(_T("Create subtitles grid")); SubsGrid = new SubtitlesGrid(this,Panel,-1,ass,wxDefaultPosition,wxSize(600,100),wxWANTS_CHARS | wxSUNKEN_BORDER,_T("Subs grid")); - BottomSizer->Add(SubsGrid,1,wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM,0); videoBox->videoSlider->grid = SubsGrid; VideoContext::Get()->grid = SubsGrid; Search.grid = SubsGrid; + // Tools area + StartupLog(_T("Create tool area splitter window")); + audioSash = new wxSashWindow(Panel, Main_AudioSash, wxDefaultPosition, wxDefaultSize, wxSW_3D|wxCLIP_CHILDREN); + wxBoxSizer *audioSashSizer = new wxBoxSizer(wxHORIZONTAL); + audioSash->SetSashVisible(wxSASH_BOTTOM, true); + // Audio area StartupLog(_T("Create audio box")); - audioBox = new AudioBox(Panel, SubsGrid); + audioBox = new AudioBox(audioSash, audioController, SubsGrid); audioBox->frameMain = this; - VideoContext::Get()->audio = audioBox->audioDisplay; + audioSashSizer->Add(audioBox, 1, wxEXPAND); + audioSash->SetSizer(audioSashSizer); + audioBox->Fit(); + audioSash->SetMinimumSizeY(audioBox->GetSize().GetHeight()); - // Top sizer + // Editing area StartupLog(_T("Create subtitle editing box")); EditBox = new SubsEditBox(Panel,SubsGrid); - StartupLog(_T("Arrange controls in sizers")); - ToolSizer = new wxBoxSizer(wxVERTICAL); - ToolSizer->Add(audioBox,0,wxEXPAND | wxBOTTOM,5); - ToolSizer->Add(EditBox,1,wxEXPAND,5); - TopSizer->Add(ToolSizer,1,wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM,5); // Set sizers/hints StartupLog(_T("Arrange main sizers")); + ToolsSizer = new wxBoxSizer(wxVERTICAL); + ToolsSizer->Add(audioSash, 0, wxEXPAND); + ToolsSizer->Add(EditBox, 1, wxEXPAND); + TopSizer = new wxBoxSizer(wxHORIZONTAL); + TopSizer->Add(videoSizer, 0, wxEXPAND, 0); + TopSizer->Add(ToolsSizer, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 5); + MainSizer = new wxBoxSizer(wxVERTICAL); MainSizer->Add(new wxStaticLine(Panel),0,wxEXPAND | wxALL,0); MainSizer->Add(TopSizer,0,wxEXPAND | wxALL,0); - MainSizer->Add(BottomSizer,1,wxEXPAND | wxALL,0); + MainSizer->Add(SubsGrid,1,wxEXPAND | wxALL,0); Panel->SetSizer(MainSizer); //MainSizer->SetSizeHints(Panel); //SetSizer(MainSizer); @@ -828,7 +845,7 @@ void FrameMain::SetDisplayMode(int video, int audio) { else if (video) sv = VideoContext::Get()->IsLoaded() && !detachedVideo; if (audio == -1) sa = showAudio; - else if (audio) sa = audioBox->loaded; + else if (audio) sa = audioController->IsAudioOpen(); // See if anything changed if (sv == showVideo && sa == showAudio) return; @@ -842,8 +859,8 @@ void FrameMain::SetDisplayMode(int video, int audio) { VideoContext::Get()->Stop(); // Set display - TopSizer->Show(videoBox,showVideo,true); - ToolSizer->Show(audioBox,showAudio,true); + TopSizer->Show(videoBox, showVideo, true); + ToolsSizer->Show(audioSash, showAudio, true); // Update UpdateToolbar(); @@ -881,7 +898,7 @@ void FrameMain::UpdateTitle() { else newTitle << _("untitled"); #endif -#ifdef __WXMAC__ +#if defined(__WXMAC__) && !defined(__LP64__) // On Mac, set the mark in the close button OSXSetModified(subsMod); #endif @@ -914,19 +931,19 @@ void FrameMain::SynchronizeProject(bool fromSubs) { // Get new state info ass->GetScriptInfo(_T("Video Position")).ToLong(&videoPos); ass->GetScriptInfo(_T("Video Zoom Percent")).ToDouble(&videoZoom); - wxString curassVideo = DecodeRelativePath(ass->GetScriptInfo(_T("Video File")),ass->filename); - wxString curassVFR = DecodeRelativePath(ass->GetScriptInfo(_T("VFR File")),ass->filename); - wxString curassKeyframes = DecodeRelativePath(ass->GetScriptInfo(_T("Keyframes File")),ass->filename); - wxString curassAudio = DecodeRelativePath(ass->GetScriptInfo(_T("Audio File")),ass->filename); + wxString curSubsVideo = DecodeRelativePath(ass->GetScriptInfo(_T("Video File")),ass->filename); + wxString curSubsVFR = DecodeRelativePath(ass->GetScriptInfo(_T("VFR File")),ass->filename); + wxString curSubsKeyframes = DecodeRelativePath(ass->GetScriptInfo(_T("Keyframes File")),ass->filename); + wxString curSubsAudio = DecodeRelativePath(ass->GetScriptInfo(_T("Audio URI")),ass->filename); wxString AutoScriptString = ass->GetScriptInfo(_T("Automation Scripts")); // Check if there is anything to change int autoLoadMode = OPT_GET("App/Auto/Load Linked Files")->GetInt(); bool hasToLoad = false; - if (curassAudio != audioBox->audioName || - curassVFR != VideoContext::Get()->GetTimecodesName() || - curassVideo != VideoContext::Get()->videoName || - curassKeyframes != VideoContext::Get()->GetKeyFramesName() + if (curSubsAudio !=audioController->GetAudioURL() || + curSubsVFR != VideoContext::Get()->GetTimecodesName() || + curSubsVideo != VideoContext::Get()->videoName || + curSubsKeyframes != VideoContext::Get()->GetKeyFramesName() #ifdef WITH_AUTOMATION || !AutoScriptString.IsEmpty() || local_scripts->GetScripts().size() > 0 #endif @@ -946,8 +963,8 @@ void FrameMain::SynchronizeProject(bool fromSubs) { if (doLoad) { // Video - if (curassVideo != VideoContext::Get()->videoName) { - LoadVideo(curassVideo); + if (curSubsVideo != VideoContext::Get()->videoName) { + LoadVideo(curSubsVideo); if (VideoContext::Get()->IsLoaded()) { VideoContext::Get()->SetAspectRatio(videoAr,videoArValue); videoBox->videoDisplay->SetZoom(videoZoom); @@ -955,13 +972,12 @@ void FrameMain::SynchronizeProject(bool fromSubs) { } } - VideoContext::Get()->LoadTimecodes(curassVFR); - VideoContext::Get()->LoadKeyframes(curassKeyframes); + VideoContext::Get()->LoadTimecodes(curSubsVFR); + VideoContext::Get()->LoadKeyframes(curSubsKeyframes); // Audio - if (curassAudio != audioBox->audioName) { - if (curassAudio == _T("?video")) LoadAudio(_T(""),true); - else LoadAudio(curassAudio); + if (curSubsAudio != audioController->GetAudioURL()) { + audioController->OpenAudio(curSubsAudio); } // Automation scripts @@ -1019,7 +1035,7 @@ void FrameMain::SynchronizeProject(bool fromSubs) { } // Store audio data - ass->SetScriptInfo(_T("Audio File"),MakeRelativePath(audioBox->audioName,ass->filename)); + ass->SetScriptInfo(_T("Audio URI"),MakeRelativePath(audioController->GetAudioURL(),ass->filename)); // Store video data ass->SetScriptInfo(_T("Video File"),MakeRelativePath(VideoContext::Get()->videoName,ass->filename)); @@ -1124,31 +1140,6 @@ void FrameMain::LoadVideo(wxString file,bool autoload) { Thaw(); } -/// @brief Loads audio -/// @param filename -/// @param FromVideo -void FrameMain::LoadAudio(wxString filename,bool FromVideo) { - if (blockAudioLoad) return; - VideoContext::Get()->Stop(); - try { - audioBox->SetFile(filename,FromVideo); - SetDisplayMode(-1,1); - } - catch (const wchar_t *error) { - wxString err(error); - wxMessageBox(err, _T("Error opening audio file"), wxOK | wxICON_ERROR, this); - } - #ifdef WITH_AVISYNTH - catch (AvisynthError err) { - wxMessageBox (wxString(_T("AviSynth error: ")) + wxString(err.msg,wxConvUTF8), _T("Error loading audio"), wxOK | wxICON_ERROR); - return; - } - #endif - catch (...) { - wxMessageBox(_T("Unknown error"), _T("Error opening audio file"), wxOK | wxICON_ERROR, this); - } -} - void FrameMain::LoadVFR(wxString filename) { if (filename.empty()) { VideoContext::Get()->CloseTimecodes(); @@ -1208,7 +1199,7 @@ void FrameMain::SetAccelerators() { // Medusa bool medusaPlay = OPT_GET("Audio/Medusa Timing Hotkeys")->GetBool(); - if (medusaPlay && audioBox->audioDisplay->loaded) { + if (medusaPlay && audioController->IsAudioOpen()) { entry.push_back(Hotkeys.GetAccelerator(_T("Audio Medusa Play"),Medusa_Play)); entry.push_back(Hotkeys.GetAccelerator(_T("Audio Medusa Stop"),Medusa_Stop)); entry.push_back(Hotkeys.GetAccelerator(_T("Audio Medusa Play Before"),Medusa_Play_Before)); @@ -1294,7 +1285,6 @@ bool FrameMain::LoadList(wxArrayString list) { } // Set blocking - blockAudioLoad = (audio != _T("")); blockVideoLoad = (video != _T("")); // Load files @@ -1305,10 +1295,8 @@ bool FrameMain::LoadList(wxArrayString list) { blockVideoLoad = false; LoadVideo(video); } - if (blockAudioLoad) { - blockAudioLoad = false; - LoadAudio(audio); - } + if (!audio.IsEmpty()) + audioController->OpenAudio(audio); // Result return ((subs != _T("")) || (audio != _T("")) || (video != _T(""))); diff --git a/aegisub/src/frame_main.h b/aegisub/src/frame_main.h index 31a252e93..960a0c663 100644 --- a/aegisub/src/frame_main.h +++ b/aegisub/src/frame_main.h @@ -43,9 +43,14 @@ #include #include #include +#include #include #endif +#ifndef AGI_AUDIO_CONTROLLER_INCLUDED +#error You must include "audio_controller.h" before "frame_main.h" +#endif + class AssFile; class VideoDisplay; class VideoSlider; @@ -57,6 +62,7 @@ class VideoBox; class DialogDetachedVideo; class DialogStyling; class AegisubFileDropTarget; +class AudioController; namespace Automation4 { class FeatureMacro; class ScriptManager; } @@ -67,7 +73,7 @@ namespace Automation4 { class FeatureMacro; class ScriptManager; } /// @brief DOCME /// /// DOCME -class FrameMain: public wxFrame { +class FrameMain: public wxFrame, private AudioControllerAudioEventListener { friend class AegisubFileDropTarget; friend class AegisubApp; friend class SubtitlesGrid; @@ -93,9 +99,6 @@ private: wxTimer StatusClear; - /// DOCME - bool blockAudioLoad; - /// DOCME bool blockVideoLoad; @@ -184,6 +187,8 @@ private: void OnVideoPlay(wxCommandEvent &event); + void OnAudioBoxResize(wxSashEvent &event); + void OnOpenRecentSubs (wxCommandEvent &event); void OnOpenRecentVideo (wxCommandEvent &event); void OnOpenRecentAudio (wxCommandEvent &event); @@ -239,6 +244,7 @@ private: void OnOpenAudio (wxCommandEvent &event); void OnOpenAudioFromVideo (wxCommandEvent &event); void OnCloseAudio (wxCommandEvent &event); + void OnAudioDisplayMode (wxCommandEvent &event); #ifdef _DEBUG void OnOpenDummyAudio(wxCommandEvent &event); void OnOpenDummyNoiseAudio(wxCommandEvent &event); @@ -313,7 +319,6 @@ private: void OnMedusaPrev(wxCommandEvent &event); void LoadVideo(wxString filename,bool autoload=false); - void LoadAudio(wxString filename,bool FromVideo=false); void LoadVFR(wxString filename); void LoadSubtitles(wxString filename,wxString charset=_T("")); bool SaveSubtitles(bool saveas=false,bool withCharset=false); @@ -322,21 +327,33 @@ private: void RebuildRecentList(wxString listName,wxMenu *menu,int startID); void SynchronizeProject(bool FromSubs=false); + +private: + // AudioControllerAudioEventListener implementation + virtual void OnAudioOpen(AudioProvider *provider); + virtual void OnAudioClose(); + virtual void OnPlaybackPosition(int64_t sample_position); + virtual void OnPlaybackStop(); + + void OnSubtitlesFileChanged(); public: - /// DOCME + /// The subtitle editing area SubtitlesGrid *SubsGrid; - /// DOCME + /// The subtitle editing textbox SubsEditBox *EditBox; - /// DOCME + /// Sash for resizing the audio area + wxSashWindow *audioSash; + + /// The audio area AudioBox *audioBox; - /// DOCME + /// The video area VideoBox *videoBox; /// DOCME @@ -345,18 +362,19 @@ public: /// DOCME DialogStyling *stylingAssistant; + /// The audio controller for the open project + AudioController *audioController; - /// DOCME + + /// Arranges things from top to bottom in the window wxBoxSizer *MainSizer; - /// DOCME + /// Arranges video box and tool box from left to right wxBoxSizer *TopSizer; - /// DOCME - wxBoxSizer *BottomSizer; + /// Arranges audio and editing areas top to bottom + wxBoxSizer *ToolsSizer; - /// DOCME - wxBoxSizer *ToolSizer; FrameMain (wxArrayString args); ~FrameMain (); @@ -435,6 +453,9 @@ enum { Menu_Audio_Open_File, Menu_Audio_Open_From_Video, Menu_Audio_Close, + + Menu_Audio_Spectrum, + Menu_Audio_Waveform, #ifdef _DEBUG Menu_Audio_Open_Dummy, Menu_Audio_Open_Dummy_Noise, @@ -503,6 +524,12 @@ enum { AutoSave_Timer, StatusClear_Timer, + + /// Id for the audio box resizing sash + Main_AudioSash, + + + /// DOCME Video_Next_Frame, Video_Prev_Frame, Video_Focus_Seek, diff --git a/aegisub/src/frame_main_events.cpp b/aegisub/src/frame_main_events.cpp index be9de9e42..64d119706 100644 --- a/aegisub/src/frame_main_events.cpp +++ b/aegisub/src/frame_main_events.cpp @@ -48,6 +48,8 @@ #include "ass_dialogue.h" #include "ass_file.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" #include "audio_display.h" #ifdef WITH_AUTOMATION @@ -84,6 +86,7 @@ #include "main.h" #include "preferences.h" #include "standard_paths.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" @@ -104,6 +107,8 @@ BEGIN_EVENT_TABLE(FrameMain, wxFrame) EVT_CLOSE(FrameMain::OnCloseWindow) + EVT_SASH_DRAGGED(Main_AudioSash, FrameMain::OnAudioBoxResize) + EVT_MENU_OPEN(FrameMain::OnMenuOpen) EVT_MENU_RANGE(Menu_File_Recent,Menu_File_Recent+99, FrameMain::OnOpenRecentSubs) EVT_MENU_RANGE(Menu_Video_Recent,Menu_Video_Recent+99, FrameMain::OnOpenRecentVideo) @@ -155,6 +160,8 @@ BEGIN_EVENT_TABLE(FrameMain, wxFrame) EVT_MENU(Menu_Audio_Open_File, FrameMain::OnOpenAudio) EVT_MENU(Menu_Audio_Open_From_Video, FrameMain::OnOpenAudioFromVideo) EVT_MENU(Menu_Audio_Close, FrameMain::OnCloseAudio) + EVT_MENU(Menu_Audio_Spectrum, FrameMain::OnAudioDisplayMode) + EVT_MENU(Menu_Audio_Waveform, FrameMain::OnAudioDisplayMode) #ifdef _DEBUG EVT_MENU(Menu_Audio_Open_Dummy, FrameMain::OnOpenDummyAudio) EVT_MENU(Menu_Audio_Open_Dummy_Noise, FrameMain::OnOpenDummyNoiseAudio) @@ -294,7 +301,7 @@ void FrameMain::OnMenuOpen (wxMenuEvent &event) { // View menu else if (curMenu == viewMenu) { // Flags - bool aud = audioBox->audioDisplay->loaded; + bool aud = audioController->IsAudioOpen(); bool vid = VideoContext::Get()->IsLoaded() && !detachedVideo; // Set states @@ -365,12 +372,16 @@ void FrameMain::OnMenuOpen (wxMenuEvent &event) { // Audio menu else if (curMenu == audioMenu) { - bool state = audioBox->loaded; + bool state = audioController->IsAudioOpen(); bool vidstate = VideoContext::Get()->IsLoaded(); MenuBar->Enable(Menu_Audio_Open_From_Video,vidstate); MenuBar->Enable(Menu_Audio_Close,state); + bool spectrum_enabled = OPT_GET("Audio/Spectrum")->GetBool(); + MenuBar->Check(Menu_Audio_Spectrum, spectrum_enabled); + MenuBar->Check(Menu_Audio_Waveform, !spectrum_enabled); + // Rebuild recent RebuildRecentList(_T("Audio"),RecentAuds,Menu_Audio_Recent); } @@ -541,7 +552,7 @@ void FrameMain::OnOpenRecentKeyframes(wxCommandEvent &event) { /// @brief Open recent audio menu entry /// @param event void FrameMain::OnOpenRecentAudio(wxCommandEvent &event) { - LoadAudio(lagi_wxString(config::mru->GetEntry("Audio", event.GetId()-Menu_Audio_Recent))); + audioController->OpenAudio(lagi_wxString(config::mru->GetEntry("Audio", event.GetId()-Menu_Audio_Recent))); } /// @brief Open new Window @@ -650,31 +661,40 @@ void FrameMain::OnOpenAudio (wxCommandEvent&) { + _("All files") + _T(" (*.*)|*.*"); wxString filename = wxFileSelector(_("Open audio file"),path,_T(""),_T(""),str,wxFD_OPEN | wxFD_FILE_MUST_EXIST); if (!filename.empty()) { - LoadAudio(filename); + audioController->OpenAudio(filename); OPT_SET("Path/Last/Audio")->SetString(STD_STR(filename)); } } /// @brief DOCME void FrameMain::OnOpenAudioFromVideo (wxCommandEvent&) { - LoadAudio(_T(""),true); + audioController->OpenAudio(_T("audio-video:cache")); } /// @brief DOCME void FrameMain::OnCloseAudio (wxCommandEvent&) { - LoadAudio(_T("")); + audioController->CloseAudio(); +} + + +/// @brief Event handler for audio display renderer selection menu options +/// @param event wxWidgets event object +void FrameMain::OnAudioDisplayMode (wxCommandEvent &event) { + OPT_SET("Audio/Spectrum")->SetBool(event.GetId() == Menu_Audio_Spectrum); + /// @todo Remove this reload call when the audio display starts listening for option changes + audioBox->audioDisplay->ReloadRenderingSettings(); } #ifdef _DEBUG /// @brief DOCME void FrameMain::OnOpenDummyAudio (wxCommandEvent&) { - LoadAudio(_T("?dummy")); + audioController->OpenAudio(_T("dummy-audio:silence?sr=44100&bd=16&ch=1&ln=396900000")); } /// @brief DOCME void FrameMain::OnOpenDummyNoiseAudio (wxCommandEvent&) { - LoadAudio(_T("?noise")); + audioController->OpenAudio(_T("dummy-audio:noise?sr=44100&bd=16&ch=1&ln=396900000")); } #endif @@ -1245,7 +1265,7 @@ void FrameMain::OnSetARCustom (wxCommandEvent &) { void FrameMain::OnCloseWindow (wxCloseEvent &event) { // Stop audio and video VideoContext::Get()->Stop(); - audioBox->audioDisplay->Stop(); + audioController->Stop(); // Ask user if he wants to save first bool canVeto = event.CanVeto(); @@ -1465,6 +1485,7 @@ void FrameMain::OnChooseLanguage (wxCommandEvent &) { /// @brief View standard void FrameMain::OnViewStandard (wxCommandEvent &) { + if (!audioController->IsAudioOpen() || !VideoContext::Get()->IsLoaded()) return; SetDisplayMode(1,1); } @@ -1475,6 +1496,7 @@ void FrameMain::OnViewVideo (wxCommandEvent &) { /// @brief View audio void FrameMain::OnViewAudio (wxCommandEvent &) { + if (!audioController->IsAudioOpen()) return; SetDisplayMode(0,1); } @@ -1485,82 +1507,132 @@ void FrameMain::OnViewSubs (wxCommandEvent &) { /// @brief Medusa shortcuts void FrameMain::OnMedusaPlay(wxCommandEvent &) { - int start=0,end=0; - audioBox->audioDisplay->GetTimesSelection(start,end); - audioBox->audioDisplay->Play(start,end); + audioController->PlayPrimaryRange(); } /// @brief DOCME void FrameMain::OnMedusaStop(wxCommandEvent &) { // Playing, stop - if (audioBox->audioDisplay->player->IsPlaying()) { - audioBox->audioDisplay->Stop(); - audioBox->audioDisplay->Refresh(); + if (audioController->IsPlaying()) { + audioController->Stop(); } // Otherwise, play the last 500 ms else { - int start=0,end=0; - audioBox->audioDisplay->GetTimesSelection(start,end); - audioBox->audioDisplay->Play(end-500,end); + AudioController::SampleRange sel(audioController->GetPrimaryPlaybackRange()); + audioController->PlayRange(AudioController::SampleRange( + sel.end() - audioController->SamplesFromMilliseconds(500), + sel.end()));; } } /// @brief DOCME void FrameMain::OnMedusaShiftStartForward(wxCommandEvent &) { - audioBox->audioDisplay->curStartMS += 10; - audioBox->audioDisplay->Update(); - audioBox->audioDisplay->wxWindow::Update(); + AudioController::SampleRange newsel( + audioController->GetPrimaryPlaybackRange(), + audioController->SamplesFromMilliseconds(10), + 0); + /// @todo Make this use the timing controller instead + //audioController->SetSelection(newsel); } /// @brief DOCME void FrameMain::OnMedusaShiftStartBack(wxCommandEvent &) { - audioBox->audioDisplay->curStartMS -= 10; - audioBox->audioDisplay->Update(); - audioBox->audioDisplay->wxWindow::Update(); + AudioController::SampleRange newsel( + audioController->GetPrimaryPlaybackRange(), + -audioController->SamplesFromMilliseconds(10), + 0); + /// @todo Make this use the timing controller instead + //audioController->SetSelection(newsel); } /// @brief DOCME void FrameMain::OnMedusaShiftEndForward(wxCommandEvent &) { - audioBox->audioDisplay->curEndMS += 10; - audioBox->audioDisplay->Update(); - audioBox->audioDisplay->wxWindow::Update(); + AudioController::SampleRange newsel( + audioController->GetPrimaryPlaybackRange(), + 0, + audioController->SamplesFromMilliseconds(10)); + /// @todo Make this use the timing controller instead + //audioController->SetSelection(newsel); } /// @brief DOCME void FrameMain::OnMedusaShiftEndBack(wxCommandEvent &) { - audioBox->audioDisplay->curEndMS -= 10; - audioBox->audioDisplay->Update(); - audioBox->audioDisplay->wxWindow::Update(); + AudioController::SampleRange newsel( + audioController->GetPrimaryPlaybackRange(), + 0, + -audioController->SamplesFromMilliseconds(10)); + /// @todo Make this use the timing controller instead + //audioController->SetSelection(newsel); } /// @brief DOCME void FrameMain::OnMedusaPlayBefore(wxCommandEvent &) { - int start=0,end=0; - audioBox->audioDisplay->GetTimesSelection(start,end); - audioBox->audioDisplay->Play(start-500,start); + AudioController::SampleRange sel(audioController->GetPrimaryPlaybackRange()); + audioController->PlayRange(AudioController::SampleRange( + sel.begin() - audioController->SamplesFromMilliseconds(500), + sel.begin()));; } /// @brief DOCME void FrameMain::OnMedusaPlayAfter(wxCommandEvent &) { - int start=0,end=0; - audioBox->audioDisplay->GetTimesSelection(start,end); - audioBox->audioDisplay->Play(end,end+500); + AudioController::SampleRange sel(audioController->GetPrimaryPlaybackRange()); + audioController->PlayRange(AudioController::SampleRange( + sel.end(), + sel.end() + audioController->SamplesFromMilliseconds(500)));; } /// @brief DOCME void FrameMain::OnMedusaNext(wxCommandEvent &) { - audioBox->audioDisplay->Next(false); + /// @todo Figure out how to handle this in the audio controller + //audioBox->audioDisplay->Next(false); } /// @brief DOCME void FrameMain::OnMedusaPrev(wxCommandEvent &) { - audioBox->audioDisplay->Prev(false); + /// @todo Figure out how to handle this in the audio controller + //audioBox->audioDisplay->Prev(false); } /// @brief DOCME void FrameMain::OnMedusaEnter(wxCommandEvent &) { - audioBox->audioDisplay->CommitChanges(true); + /// @todo Figure out how to handle this in the audio controller + //audioBox->audioDisplay->CommitChanges(true); +} + +void FrameMain::OnAudioBoxResize(wxSashEvent &event) +{ + if (event.GetDragStatus() == wxSASH_STATUS_OUT_OF_RANGE) + return; + + wxRect rect = event.GetDragRect(); + + if (rect.GetHeight() < audioSash->GetMinimumSizeY()) + rect.SetHeight(audioSash->GetMinimumSizeY()); + + audioBox->SetMinSize(wxSize(-1, rect.GetHeight())); + Panel->Layout(); + Refresh(); +} + +void FrameMain::OnAudioOpen(AudioProvider *provider) +{ + SetDisplayMode(-1, 1); +} + +void FrameMain::OnAudioClose() +{ + SetDisplayMode(-1, 0); +} + +void FrameMain::OnPlaybackPosition(int64_t sample_position) +{ + // do nothing +} + +void FrameMain::OnPlaybackStop() +{ + // do nothing } void FrameMain::OnSubtitlesFileChanged() { @@ -1570,3 +1642,4 @@ void FrameMain::OnSubtitlesFileChanged() { UpdateTitle(); } + diff --git a/aegisub/src/include/aegisub/audio_provider.h b/aegisub/src/include/aegisub/audio_provider.h index b52d369e9..ca1b07f05 100644 --- a/aegisub/src/include/aegisub/audio_provider.h +++ b/aegisub/src/include/aegisub/audio_provider.h @@ -79,17 +79,15 @@ public: virtual ~AudioProvider(); virtual wxString GetFilename() const { return filename; }; - virtual void GetAudio(void *buf, int64_t start, int64_t count)=0; - void GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume); + virtual void GetAudio(void *buf, int64_t start, int64_t count) const = 0; + void GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const; int64_t GetNumSamples() const { return num_samples; } - int GetSampleRate() const { return sample_rate; } + int GetSampleRate() const { return sample_rate; } int GetBytesPerSample() const { return bytes_per_sample; } - int GetChannels() const { return channels; } + int GetChannels() const { return channels; } virtual bool AreSamplesNativeEndian() const = 0; - void GetWaveForm(int *min,int *peak,int64_t start,int w,int h,int samples,float scale); - /// @brief Does this provider benefit from external caching? virtual bool NeedsCache() const { return false; } }; diff --git a/aegisub/src/main.cpp b/aegisub/src/main.cpp index acf6d05f5..427465bc7 100644 --- a/aegisub/src/main.cpp +++ b/aegisub/src/main.cpp @@ -54,8 +54,9 @@ #include "ass_export_filter.h" #include "ass_file.h" #include "ass_time.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" -#include "audio_display.h" #ifdef WITH_AUTOMATION #include "auto4_base.h" #endif @@ -68,7 +69,6 @@ #include "libresrc/libresrc.h" #include "plugin_manager.h" #include "standard_paths.h" -#include "subs_grid.h" #include "subtitle_format.h" #include "version.h" #include "video_context.h" @@ -551,13 +551,14 @@ END_EVENT_TABLE() /// @param event /// void AegisubApp::OnMouseWheel(wxMouseEvent &event) { + if (event.WasProcessed()) return; wxPoint pt; wxWindow *target = wxFindWindowAtPointer(pt); - if (frame && (target == frame->audioBox->audioDisplay || target == frame->SubsGrid)) { + /*if (frame && (target == frame->audioBox->audioDisplay || target == frame->SubsGrid)) { if (target->IsShownOnScreen()) target->GetEventHandler()->ProcessEvent(event); else event.Skip(); } - else event.Skip(); + else event.Skip();*/ } diff --git a/aegisub/src/setup.cpp b/aegisub/src/setup.cpp index f994d69cf..10f92b42a 100644 --- a/aegisub/src/setup.cpp +++ b/aegisub/src/setup.cpp @@ -140,6 +140,4 @@ #pragma comment(lib, "libass.lib") #endif - #endif // VisualC - diff --git a/aegisub/src/subs_edit_box.cpp b/aegisub/src/subs_edit_box.cpp index fd3fb7227..6adb7a142 100644 --- a/aegisub/src/subs_edit_box.cpp +++ b/aegisub/src/subs_edit_box.cpp @@ -58,12 +58,13 @@ #include "ass_file.h" #include "ass_override.h" #include "ass_style.h" -#include "audio_display.h" +#include "audio_controller.h" #include "dialog_colorpicker.h" #include "dialog_search_replace.h" #include "frame_main.h" #include "libresrc/libresrc.h" #include "main.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" diff --git a/aegisub/src/subs_edit_box.h b/aegisub/src/subs_edit_box.h index 5b7e49276..a4729fa5e 100644 --- a/aegisub/src/subs_edit_box.h +++ b/aegisub/src/subs_edit_box.h @@ -55,6 +55,7 @@ class wxSpinCtrl; class wxStyledTextCtrl; class wxStyledTextEvent; class wxTextCtrl; +class AudioController; /// DOCME /// @class SubsEditBox diff --git a/aegisub/src/subs_edit_ctrl.cpp b/aegisub/src/subs_edit_ctrl.cpp index 829157092..62f0efcd6 100644 --- a/aegisub/src/subs_edit_ctrl.cpp +++ b/aegisub/src/subs_edit_ctrl.cpp @@ -49,6 +49,7 @@ #include "compat.h" #include "main.h" #include "include/aegisub/spellchecker.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" diff --git a/aegisub/src/subs_grid.cpp b/aegisub/src/subs_grid.cpp index e9e34e7b6..ab78e77ec 100644 --- a/aegisub/src/subs_grid.cpp +++ b/aegisub/src/subs_grid.cpp @@ -51,8 +51,10 @@ #include "ass_karaoke.h" #include "ass_override.h" #include "ass_style.h" +#include "include/aegisub/audio_provider.h" +#include "selection_controller.h" +#include "audio_controller.h" #include "audio_box.h" -#include "audio_display.h" #include "charset_conv.h" #include "dialog_paste_over.h" #include "frame_main.h" @@ -204,7 +206,7 @@ void SubtitlesGrid::OnPopupMenu(bool alternate) { menu.AppendSeparator(); //Make audio clip - state = parentFrame->audioBox->audioDisplay->loaded==true; + state = parentFrame->audioController->IsAudioOpen()==true; menu.Append(MENU_AUDIOCLIP,_("Create audio clip"),_("Create an audio clip of the selected line"))->Enable(state); menu.AppendSeparator(); @@ -646,8 +648,8 @@ void SubtitlesGrid::OnRecombine(wxCommandEvent &) { /// @brief Export audio clip of line void SubtitlesGrid::OnAudioClip(wxCommandEvent &) { int64_t num_samples,start=0,end=0,temp; - AudioDisplay *audioDisplay = parentFrame->audioBox->audioDisplay; - AudioProvider *provider = audioDisplay->provider; + AudioController *audioController = parentFrame->audioController; + const AudioProvider *provider = audioController->GetAudioProvider(); AssDialogue *cur; wxArrayInt sel = GetSelection(); @@ -656,9 +658,9 @@ void SubtitlesGrid::OnAudioClip(wxCommandEvent &) { for(unsigned int i=0;i!=sel.GetCount();i++) { cur = GetDialogue(sel[i]); - temp = audioDisplay->GetSampleAtMS(cur->Start.GetMS()); + temp = audioController->SamplesFromMilliseconds(cur->Start.GetMS()); start = (i==0||tempGetSampleAtMS(cur->End.GetMS()); + temp = audioController->SamplesFromMilliseconds(cur->End.GetMS()); end = (i==0||temp>end)?temp:end; } diff --git a/aegisub/src/subtitles_provider_libass.cpp b/aegisub/src/subtitles_provider_libass.cpp index 5e32a0ced..abdc658ef 100644 --- a/aegisub/src/subtitles_provider_libass.cpp +++ b/aegisub/src/subtitles_provider_libass.cpp @@ -50,6 +50,7 @@ #include +#include "audio_controller.h" #include "ass_file.h" #include "dialog_progress.h" #include "frame_main.h" diff --git a/aegisub/src/utils.h b/aegisub/src/utils.h index 897705800..b53094933 100644 --- a/aegisub/src/utils.h +++ b/aegisub/src/utils.h @@ -70,6 +70,10 @@ int AegiStringToFix(const wxString &str,size_t decimalPlaces,int start=0,int end wxIcon BitmapToIcon(wxBitmap bmp); void RestartAegisub(); + +/// @brief Templated abs() function +template T tabs(T x) { return x < 0 ? -x : x; } + #ifndef MIN #define MIN(a,b) ((a)<(b))?(a):(b) #endif diff --git a/aegisub/src/video_box.cpp b/aegisub/src/video_box.cpp index 58f373bea..a58eac78c 100644 --- a/aegisub/src/video_box.cpp +++ b/aegisub/src/video_box.cpp @@ -45,10 +45,12 @@ #include "ass_dialogue.h" #include "ass_file.h" +#include "audio_controller.h" #include "frame_main.h" #include "help_button.h" #include "libresrc/libresrc.h" #include "main.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_grid.h" #include "toggle_bitmap.h" diff --git a/aegisub/src/video_context.cpp b/aegisub/src/video_context.cpp index d379085b1..58de11457 100644 --- a/aegisub/src/video_context.cpp +++ b/aegisub/src/video_context.cpp @@ -53,11 +53,13 @@ #include #endif +#include "include/aegisub/audio_player.h" +#include "include/aegisub/audio_provider.h" #include "ass_dialogue.h" #include "ass_file.h" #include "ass_style.h" #include "ass_time.h" -#include "audio_display.h" +#include "audio_controller.h" #include "compat.h" #include "include/aegisub/audio_player.h" #include "include/aegisub/audio_provider.h" @@ -67,6 +69,7 @@ #include "main.h" #include "mkv_wrap.h" #include "standard_paths.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_grid.h" #include "threaded_frame_source.h" @@ -119,10 +122,6 @@ VideoContext::VideoContext() } VideoContext::~VideoContext () { - if (audio && audio->temporary) { - delete audio->provider; - delete audio->player; - } } VideoContext *VideoContext::Get() { @@ -135,15 +134,7 @@ void VideoContext::Reset() { keyFrames.clear(); videoFPS = agi::vfr::Framerate(); - - // Remove temporary audio provider - if (audio && audio->temporary) { - delete audio->provider; - audio->provider = NULL; - delete audio->player; - audio->player = NULL; - audio->temporary = false; - } + keyframesRevision++; // Remove video data frame_n = 0; @@ -316,8 +307,11 @@ void VideoContext::PlayNextFrame() { int thisFrame = frame_n; JumpToFrame(frame_n + 1); // Start playing audio - if (playAudioOnStep->GetBool()) - audio->Play(TimeAtFrame(thisFrame),TimeAtFrame(thisFrame + 1)); + if (playAudioOnStep->GetBool()) { + audio->PlayRange(AudioController::SampleRange( + audio->SamplesFromMilliseconds(TimeAtFrame(thisFrame)), + audio->SamplesFromMilliseconds(TimeAtFrame(thisFrame + 1)))); + } } void VideoContext::PlayPrevFrame() { @@ -327,8 +321,11 @@ void VideoContext::PlayPrevFrame() { int thisFrame = frame_n; JumpToFrame(frame_n -1); // Start playing audio - if (playAudioOnStep->GetBool()) - audio->Play(TimeAtFrame(thisFrame - 1),TimeAtFrame(thisFrame)); + if (playAudioOnStep->GetBool()) { + audio->PlayRange(AudioController::SampleRange( + audio->SamplesFromMilliseconds(TimeAtFrame(thisFrame - 1)), + audio->SamplesFromMilliseconds(TimeAtFrame(thisFrame)))); + } } void VideoContext::Play() { @@ -342,7 +339,7 @@ void VideoContext::Play() { endFrame = -1; // Start playing audio - audio->Play(TimeAtFrame(startFrame),-1); + audio->PlayToEnd(audio->SamplesFromMilliseconds(TimeAtFrame(startFrame))); //audio->Play will override this if we put it before, so put it after. isPlaying = true; @@ -358,7 +355,9 @@ void VideoContext::PlayLine() { if (!curline) return; // Start playing audio - audio->Play(curline->Start.GetMS(),curline->End.GetMS()); + audio->PlayRange(AudioController::SampleRange( + audio->SamplesFromMilliseconds(curline->Start.GetMS()), + audio->SamplesFromMilliseconds(curline->End.GetMS()))); // Set variables isPlaying = true; @@ -417,7 +416,9 @@ void VideoContext::OnPlayTimer(wxTimerEvent &event) { if (nextFrame == frame_n) return; // Next frame is before or over 2 frames ahead, so force audio resync - if (audio->player && keepAudioSync && (nextFrame < frame_n || nextFrame > frame_n + 2)) audio->player->SetCurrentPosition(audio->GetSampleAtMS(TimeAtFrame(nextFrame))); + if (audio->IsPlaying() && keepAudioSync && (nextFrame < frame_n || nextFrame > frame_n + 2)) { + audio->ResyncPlaybackPosition(audio->SamplesFromMilliseconds(TimeAtFrame(nextFrame))); + } // Jump to next frame playNextFrame = nextFrame; @@ -425,13 +426,13 @@ void VideoContext::OnPlayTimer(wxTimerEvent &event) { JumpToFrame(nextFrame); // Sync audio - if (keepAudioSync && nextFrame % 10 == 0 && audio && audio->provider && audio->player) { - int64_t audPos = audio->GetSampleAtMS(TimeAtFrame(nextFrame)); - int64_t curPos = audio->player->GetCurrentPosition(); + if (keepAudioSync && nextFrame % 10 == 0 && audio->IsPlaying()) { + int64_t audPos = audio->SamplesFromMilliseconds(TimeAtFrame(nextFrame)); + int64_t curPos = audio->GetPlaybackPosition(); int delta = int(audPos-curPos); if (delta < 0) delta = -delta; - int maxDelta = audio->provider->GetSampleRate(); - if (delta > maxDelta) audio->player->SetCurrentPosition(audPos); + int maxDelta = audio->SamplesFromMilliseconds(1000); + if (delta > maxDelta) audio->ResyncPlaybackPosition(audPos); } } @@ -468,10 +469,12 @@ void VideoContext::LoadKeyframes(wxString filename) { catch (...) { wxMessageBox(_T("Unknown error"), _T("Error opening keyframes file"), wxOK | wxICON_ERROR, NULL); } + keyframesRevision++; } void VideoContext::SaveKeyframes(wxString filename) { KeyFrameFile::Save(filename, GetKeyFrames()); + keyframesRevision++; } void VideoContext::CloseKeyframes() { @@ -483,6 +486,7 @@ void VideoContext::CloseKeyframes() { keyFrames.clear(); } KeyframesOpen(keyFrames); + keyframesRevision++; } void VideoContext::LoadTimecodes(wxString filename) { diff --git a/aegisub/src/video_context.h b/aegisub/src/video_context.h index f5fb22d81..abff82a22 100644 --- a/aegisub/src/video_context.h +++ b/aegisub/src/video_context.h @@ -62,6 +62,7 @@ class SubtitlesProviderErrorEvent; class ThreadedFrameSource; class VideoProvider; class VideoProviderErrorEvent; +class AudioController; namespace agi { class OptionValue; @@ -101,6 +102,10 @@ private: /// DOCME wxString keyFramesFilename; + /// Revision counter for keyframes, when the set of keyframes is changed this number changes + int keyframesRevision; + + /// DOCME wxMutex playMutex; @@ -161,8 +166,8 @@ public: /// File name of currently open video, if any wxString videoName; - /// DOCME - AudioDisplay *audio; + /// The audio controller for this video context + AudioController *audio; const agi::vfr::Framerate &VFR_Input; const agi::vfr::Framerate &VFR_Output; @@ -265,6 +270,7 @@ public: void CloseKeyframes(); bool OverKeyFramesLoaded() const { return !keyFramesFilename.empty(); } bool KeyFramesLoaded() const { return !keyFrames.empty(); } + int GetKeyframesRevision() const { return keyframesRevision; } wxString GetTimecodesName() const { return ovrTimecodeFile; } void LoadTimecodes(wxString filename); diff --git a/aegisub/src/video_display.cpp b/aegisub/src/video_display.cpp index d32ba3b51..2afa3c558 100644 --- a/aegisub/src/video_display.cpp +++ b/aegisub/src/video_display.cpp @@ -57,6 +57,7 @@ #endif #include "video_display.h" +#include "selection_controller.h" #include "ass_dialogue.h" #include "ass_file.h" diff --git a/aegisub/src/video_slider.cpp b/aegisub/src/video_slider.cpp index a1bc94004..6e25f88a9 100644 --- a/aegisub/src/video_slider.cpp +++ b/aegisub/src/video_slider.cpp @@ -43,6 +43,7 @@ #include "ass_dialogue.h" #include "main.h" #include "subs_edit_box.h" +#include "selection_controller.h" #include "subs_grid.h" #include "utils.h" #include "video_context.h" diff --git a/aegisub/src/visual_tool.cpp b/aegisub/src/visual_tool.cpp index f89825cf8..c3e66deb0 100644 --- a/aegisub/src/visual_tool.cpp +++ b/aegisub/src/visual_tool.cpp @@ -46,6 +46,7 @@ #include "ass_style.h" #include "ass_time.h" #include "main.h" +#include "selection_controller.h" #include "subs_edit_box.h" #include "subs_grid.h" #include "utils.h" diff --git a/aegisub/src/visual_tool_vector_clip.cpp b/aegisub/src/visual_tool_vector_clip.cpp index 53bd86543..a2c17c612 100644 --- a/aegisub/src/visual_tool_vector_clip.cpp +++ b/aegisub/src/visual_tool_vector_clip.cpp @@ -53,6 +53,7 @@ #endif #include "config.h" +#include "selection_controller.h" #include "ass_dialogue.h" #include "libresrc/libresrc.h"