// Copyright (c) 2006, Rodrigo Braz Monteiro // 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 // // Website: http://aegisub.cellosoft.com // Contact: mailto:zeratul@cellosoft.com // //////////// // Includes #include "dialog_timing_processor.h" #include "subs_grid.h" #include "ass_file.h" #include "options.h" #include "validators.h" #include "video_display.h" #include "video_box.h" #include "ass_dialogue.h" #include "ass_time.h" #include "vfr.h" #include "utils.h" /////////////// // Constructor DialogTimingProcessor::DialogTimingProcessor(wxWindow *parent,SubtitlesGrid *_grid) : wxDialog(parent,-1,_("Timing Post-Processor"),wxDefaultPosition,wxSize(400,250),wxDEFAULT_DIALOG_STYLE) { // Set variables grid = _grid; leadInTime = Options.AsText(_T("Audio lead in")); leadOutTime = Options.AsText(_T("Audio lead out")); thresStartBefore = Options.AsText(_T("Timing processor key start before thres")); thresStartAfter = Options.AsText(_T("Timing processor key start after thres")); thresEndBefore = Options.AsText(_T("Timing processor key end before thres")); thresEndAfter = Options.AsText(_T("Timing processor key end after thres")); adjsThresTime = Options.AsText(_T("Timing processor adjascent thres")); // Lead-in/out box wxSizer *LeadSizer = new wxStaticBoxSizer(wxHORIZONTAL,this,_("Lead-in/Lead-out")); hasLeadIn = new wxCheckBox(this,CHECK_ENABLE_LEADIN,_("Add lead in:")); hasLeadIn->SetToolTip(_("Enable adding of lead-ins to lines.")); hasLeadIn->SetValue(Options.AsBool(_T("Timing processor Enable lead-in"))); leadIn = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(80,-1),0,NumValidator(&leadInTime)); leadIn->SetToolTip(_("Lead in to be added, in miliseconds.")); hasLeadOut = new wxCheckBox(this,CHECK_ENABLE_LEADOUT,_("Add lead out:")); hasLeadOut->SetToolTip(_("Enable adding of lead-outs to lines.")); hasLeadOut->SetValue(Options.AsBool(_T("Timing processor Enable lead-out"))); leadOut = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(80,-1),0,NumValidator(&leadOutTime)); leadOut->SetToolTip(_("Lead out to be added, in miliseconds.")); LeadSizer->Add(hasLeadIn,0,wxRIGHT|wxEXPAND,5); LeadSizer->Add(leadIn,0,wxRIGHT|wxEXPAND,5); LeadSizer->Add(hasLeadOut,0,wxRIGHT|wxEXPAND,5); LeadSizer->Add(leadOut,0,wxRIGHT|wxEXPAND,0); LeadSizer->AddStretchSpacer(1); // Adjascent subs sizer wxSizer *AdjascentSizer = new wxStaticBoxSizer(wxHORIZONTAL,this,_("Make adjascent subtitles continuous")); adjsEnable = new wxCheckBox(this,CHECK_ENABLE_ADJASCENT,_("Enable")); adjsEnable->SetToolTip(_("Enable snapping of subtitles together if they are within a certain distance of each other.")); adjsEnable->SetValue(Options.AsBool(_T("Timing processor Enable adjascent"))); wxStaticText *adjsThresText = new wxStaticText(this,-1,_("Threshold:"),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE); adjascentThres = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(60,-1),0,NumValidator(&adjsThresTime)); adjascentThres->SetToolTip(_("Maximum difference between start and end time for two subtitles to be made continuous, in miliseconds.")); adjascentBias = new wxSlider(this,-1,MID(0,int(Options.AsFloat(_T("Timing processor adjascent bias"))*100),100),0,100,wxDefaultPosition,wxSize(-1,20)); adjascentBias->SetToolTip(_T("Sets how to set the adjoining of lines. If set totally to left, it will extend start time of the second line; if totally to right, it will extend the end time of the first line.")); AdjascentSizer->Add(adjsEnable,0,wxRIGHT|wxEXPAND,10); AdjascentSizer->Add(adjsThresText,0,wxRIGHT|wxALIGN_CENTER,5); AdjascentSizer->Add(adjascentThres,0,wxRIGHT|wxEXPAND,5); AdjascentSizer->Add(new wxStaticText(this,-1,_("Bias: Start <- "),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE),0,wxALIGN_CENTER,0); AdjascentSizer->Add(adjascentBias,1,wxEXPAND,0); AdjascentSizer->Add(new wxStaticText(this,-1,_(" -> End"),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE),0,wxALIGN_CENTER,0); // Keyframes sizer KeyframesSizer = new wxStaticBoxSizer(wxHORIZONTAL,this,_("Keyframe snapping")); wxSizer *KeyframesFlexSizer = new wxFlexGridSizer(2,5,5,0); keysEnable = new wxCheckBox(this,CHECK_ENABLE_KEYFRAME,_("Enable")); keysEnable->SetToolTip(_("Enable snapping of subtitles to nearest keyframe, if distance is within threshold.")); keysEnable->SetValue(Options.AsBool(_T("Timing processor Enable keyframe"))); wxStaticText *textStartBefore = new wxStaticText(this,-1,_("Starts before thres.:"),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE); keysStartBefore = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(60,-1),0,NumValidator(&thresStartBefore)); keysStartBefore->SetToolTip(_("Threshold for 'before start' distance, that is, how many frames a subtitle must start before a keyframe to snap to it.")); wxStaticText *textStartAfter = new wxStaticText(this,-1,_("Starts after thres.:"),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE); keysStartAfter = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(60,-1),0,NumValidator(&thresStartAfter)); keysStartAfter->SetToolTip(_("Threshold for 'after start' distance, that is, how many frames a subtitle must start after a keyframe to snap to it.")); wxStaticText *textEndBefore = new wxStaticText(this,-1,_("Ends before thres.:"),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE); keysEndBefore = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(60,-1),0,NumValidator(&thresEndBefore)); keysEndBefore->SetToolTip(_("Threshold for 'before end' distance, that is, how many frames a subtitle must end before a keyframe to snap to it.")); wxStaticText *textEndAfter = new wxStaticText(this,-1,_("Ends after thres.:"),wxDefaultPosition,wxDefaultSize,wxALIGN_CENTRE); keysEndAfter = new wxTextCtrl(this,-1,_T(""),wxDefaultPosition,wxSize(60,-1),0,NumValidator(&thresEndAfter)); keysEndAfter->SetToolTip(_("Threshold for 'after end' distance, that is, how many frames a subtitle must end after a keyframe to snap to it.")); KeyframesFlexSizer->Add(keysEnable,0,wxRIGHT|wxEXPAND,10); KeyframesFlexSizer->Add(textStartBefore,0,wxRIGHT|wxALIGN_CENTER,5); KeyframesFlexSizer->Add(keysStartBefore,0,wxRIGHT|wxEXPAND,5); KeyframesFlexSizer->Add(textStartAfter,0,wxRIGHT|wxALIGN_CENTER,5); KeyframesFlexSizer->Add(keysStartAfter,0,wxRIGHT|wxEXPAND,0); KeyframesFlexSizer->AddStretchSpacer(1); KeyframesFlexSizer->Add(textEndBefore,0,wxRIGHT|wxALIGN_CENTER,5); KeyframesFlexSizer->Add(keysEndBefore,0,wxRIGHT|wxEXPAND,5); KeyframesFlexSizer->Add(textEndAfter,0,wxRIGHT|wxALIGN_CENTER,5); KeyframesFlexSizer->Add(keysEndAfter,0,wxRIGHT|wxEXPAND,0); KeyframesSizer->Add(KeyframesFlexSizer,0,wxEXPAND); KeyframesSizer->AddStretchSpacer(1); // Button sizer wxSizer *ButtonSizer = new wxBoxSizer(wxHORIZONTAL); ButtonSizer->AddStretchSpacer(1); ApplyButton = new wxButton(this,wxID_APPLY); #ifndef __WXMAC__ ButtonSizer->Add(ApplyButton,0,wxRIGHT,5); ButtonSizer->Add(new wxButton(this,wxID_CANCEL),0,0,0); #else ButtonSizer->Add(new wxButton(this,wxID_CANCEL),0,wxRIGHT,5); ButtonSizer->Add(ApplyButton,0,0,0); #endif // Right Sizer wxSizer *RightSizer = new wxBoxSizer(wxVERTICAL); RightSizer->Add(LeadSizer,0,wxBOTTOM|wxEXPAND,5); RightSizer->Add(AdjascentSizer,0,wxBOTTOM|wxEXPAND,5); RightSizer->Add(KeyframesSizer,0,wxBOTTOM|wxEXPAND,5); RightSizer->AddStretchSpacer(1); RightSizer->Add(ButtonSizer,0,wxLEFT|wxRIGHT|wxBOTTOM|wxEXPAND,0); // Style buttons sizer wxSizer *StyleButtonsSizer = new wxBoxSizer(wxHORIZONTAL); wxButton *all = new wxButton(this,BUTTON_SELECT_ALL,_("All")); all->SetToolTip(_("Select all styles.")); wxButton *none = new wxButton(this,BUTTON_SELECT_NONE,_("None")); none->SetToolTip(_("Deselect all styles.")); StyleButtonsSizer->Add(all,1,0,0); StyleButtonsSizer->Add(none,1,0,0); // Left sizer wxSizer *LeftSizer = new wxStaticBoxSizer(wxVERTICAL,this,_("Apply to styles")); wxArrayString styles = grid->ass->GetStyles(); StyleList = new wxCheckListBox(this,TIMING_STYLE_LIST,wxDefaultPosition,wxSize(150,150),styles); StyleList->SetToolTip(_("Select styles to process. Unchecked ones will be ignored.")); size_t len = StyleList->GetCount(); for (size_t i=0;iCheck(i); } LeftSizer->Add(StyleList,1,wxBOTTOM|wxEXPAND,0); LeftSizer->Add(StyleButtonsSizer,0,wxEXPAND,0); // Top Sizer wxSizer *TopSizer = new wxBoxSizer(wxHORIZONTAL); TopSizer->Add(LeftSizer,0,wxRIGHT|wxEXPAND,5); TopSizer->Add(RightSizer,1,wxALL|wxEXPAND,0); // Main Sizer wxSizer *MainSizer = new wxBoxSizer(wxVERTICAL); MainSizer->Add(TopSizer,1,wxALL|wxEXPAND,5); MainSizer->SetSizeHints(this); SetSizer(MainSizer); CenterOnParent(); // Update UpdateControls(); } /////////////////// // Update controls void DialogTimingProcessor::UpdateControls() { // Boxes leadIn->Enable(hasLeadIn->IsChecked()); leadOut->Enable(hasLeadOut->IsChecked()); adjascentThres->Enable(adjsEnable->IsChecked()); // Keyframes are only available if timecodes are loaded bool keysAvailable = grid->video->KeyFramesLoaded(); bool enableKeys = keysEnable->IsChecked() && keysAvailable; keysStartBefore->Enable(enableKeys); keysStartAfter->Enable(enableKeys); keysEndBefore->Enable(enableKeys); keysEndAfter->Enable(enableKeys); if (!keysAvailable) { keysEnable->SetValue(false); keysEnable->Enable(false); } // Apply button int checked = 0; size_t len = StyleList->GetCount(); for (size_t i=0;iIsChecked(i)) checked++; } ApplyButton->Enable(checked && (hasLeadIn->IsChecked() | hasLeadOut->IsChecked() | keysEnable->IsChecked() | adjsEnable->IsChecked())); } /////////////// // Event table BEGIN_EVENT_TABLE(DialogTimingProcessor,wxDialog) EVT_CHECKBOX(CHECK_ENABLE_LEADIN,DialogTimingProcessor::OnCheckBox) EVT_CHECKBOX(CHECK_ENABLE_LEADOUT,DialogTimingProcessor::OnCheckBox) EVT_CHECKBOX(CHECK_ENABLE_KEYFRAME,DialogTimingProcessor::OnCheckBox) EVT_CHECKBOX(CHECK_ENABLE_ADJASCENT,DialogTimingProcessor::OnCheckBox) EVT_CHECKLISTBOX(TIMING_STYLE_LIST,DialogTimingProcessor::OnCheckBox) EVT_BUTTON(wxID_APPLY,DialogTimingProcessor::OnApply) EVT_BUTTON(BUTTON_SELECT_ALL,DialogTimingProcessor::OnSelectAll) EVT_BUTTON(BUTTON_SELECT_NONE,DialogTimingProcessor::OnSelectNone) END_EVENT_TABLE() //////////////////// // Checkbox clicked void DialogTimingProcessor::OnCheckBox(wxCommandEvent &event) { UpdateControls(); } ///////////////////// // Select all styles void DialogTimingProcessor::OnSelectAll(wxCommandEvent &event) { size_t len = StyleList->GetCount(); for (size_t i=0;iCheck(i); } UpdateControls(); } /////////////////////// // Unselect all styles void DialogTimingProcessor::OnSelectNone(wxCommandEvent &event) { size_t len = StyleList->GetCount(); for (size_t i=0;iCheck(i,false); } UpdateControls(); } //////////////////////// // Apply button pressed void DialogTimingProcessor::OnApply(wxCommandEvent &event) { // Save settings long temp = 0; leadIn->GetValue().ToLong(&temp); Options.SetInt(_T("Audio lead in"),temp); leadOut->GetValue().ToLong(&temp); Options.SetInt(_T("Audio lead out"),temp); keysStartBefore->GetValue().ToLong(&temp); Options.SetInt(_T("Timing processor key start before thres"),temp); keysStartAfter->GetValue().ToLong(&temp); Options.SetInt(_T("Timing processor key start after thres"),temp); keysEndBefore->GetValue().ToLong(&temp); Options.SetInt(_T("Timing processor key end before thres"),temp); keysEndAfter->GetValue().ToLong(&temp); Options.SetInt(_T("Timing processor key end after thres"),temp); adjascentThres->GetValue().ToLong(&temp); Options.SetInt(_T("Timing processor adjascent thres"),temp); Options.SetFloat(_T("Timing processor adjascent bias"),adjascentBias->GetValue() / 100.0); Options.SetBool(_T("Timing processor Enable lead-in"),hasLeadIn->IsChecked()); Options.SetBool(_T("Timing processor Enable lead-out"),hasLeadOut->IsChecked()); if (keysEnable->IsEnabled()) Options.SetBool(_T("Timing processor Enable keyframe"),keysEnable->IsChecked()); Options.SetBool(_T("Timing processor Enable adjascent"),adjsEnable->IsChecked()); Options.Save(); // Check if rows are valid bool valid = true; AssDialogue *tempDiag; int i = 0; for (std::list::iterator cur=grid->ass->Line.begin();cur!=grid->ass->Line.end();cur++) { tempDiag = AssEntry::GetAsDialogue(*cur); if (tempDiag) { i++; if (tempDiag->Start.GetMS() > tempDiag->End.GetMS()) { valid = false; break; } } } // Process if (valid) Process(); // Error message else wxMessageBox(wxString::Format(_("One of the lines in the file (%i) has negative duration. Aborting."),i),_("Invalid script"),wxICON_ERROR|wxOK); // Close dialogue EndModal(0); } //////////////////////// // Get closest keyframe int DialogTimingProcessor::GetClosestKeyFrame(int frame) { // Linear dumb search, not very efficient, but it doesn't really matter int closest = 0; size_t n = KeyFrames.Count(); for (size_t i=0;iGetCount(); for (size_t i=0;iGetString(i) == styleName && StyleList->IsChecked(i)) return true; } return false; } ////////////////// // Sort dialogues void DialogTimingProcessor::SortDialogues() { // Copy from original to temporary list std::list temp; AssDialogue *tempDiag; for (std::list::iterator cur=grid->ass->Line.begin();cur!=grid->ass->Line.end();cur++) { tempDiag = AssEntry::GetAsDialogue(*cur); // Only add valid lines if (tempDiag && StyleOK(tempDiag->Style) && !tempDiag->Comment) { tempDiag->StartMS = tempDiag->Start.GetMS(); temp.push_back(tempDiag); } } // Sort temporary list temp.sort(LessByPointedToValue()); // Copy temporary list to final vector for (std::list::iterator cur=temp.begin();cur!=temp.end();cur++) { Sorted.push_back(*cur); } } //////////////////////// // Gets sorted dialogue AssDialogue *DialogTimingProcessor::GetSortedDialogue(int n) { try { return Sorted.at(n); } catch (...) { return NULL; } } ////////////////////////////// // Actually process subtitles void DialogTimingProcessor::Process() { // Sort rows SortDialogues(); int rows = Sorted.size(); // Options long inVal = 0; long outVal = 0; leadIn->GetValue().ToLong(&inVal); leadOut->GetValue().ToLong(&outVal); bool addIn = hasLeadIn->IsChecked() && inVal; bool addOut = hasLeadOut->IsChecked() && outVal; // Add lead-in/out if (addIn || addOut) { // Variables AssDialogue *cur; AssDialogue *comp; int start,end; int startLead,endLead; int compStart,compEnd; // For each row for (int i=0;iStart.GetMS(); end = cur->End.GetMS(); if (addIn) startLead = start - inVal; else startLead = start; if (addOut) endLead = end + outVal; else endLead = end; // Compare to every previous line (yay for O(n^2)!) to see if it's OK to add lead-in if (addIn) { for (int j=0;jCollidesWith(comp)) continue; // Get comparison times compEnd = comp->End.GetMS(); // Limit lead-in if needed if (compEnd > startLead) startLead = compEnd; } } // Compare to every line to see how far can lead-out be extended if (addOut) { for (int j=i+1;jCollidesWith(comp)) continue; // Get comparison times compStart = comp->Start.GetMS(); // Limit lead-in if needed if (compStart < endLead) endLead = compStart; } } // Set times cur->Start.SetMS(startLead); cur->End.SetMS(endLead); cur->UpdateData(); } } // Make adjascent if (adjsEnable->IsChecked()) { // Variables AssDialogue *cur; AssDialogue *prev = NULL; int curStart,prevEnd; long adjsThres = 0; int dist; // Get threshold adjascentThres->GetValue().ToLong(&adjsThres); // Get bias float bias = adjascentBias->GetValue() / 100.0; // For each row for (int i=0;iCollidesWith(prev)) continue; // Compare distance curStart = cur->Start.GetMS(); prevEnd = prev->End.GetMS(); dist = curStart-prevEnd; if (dist > 0 && dist < adjsThres) { int setPos = prevEnd+int(dist*bias); cur->Start.SetMS(setPos); cur->UpdateData(); prev->End.SetMS(setPos); prev->UpdateData(); } // Set previous prev = cur; } } // Keyframe snapping if (keysEnable->IsChecked()) { // Get keyframes KeyFrames = grid->video->GetKeyFrames(); KeyFrames.Add(grid->video->length-1); // Variables int startF,endF; int closest; bool changed; AssDialogue *cur; // Get variables long beforeStart = 0; long afterStart = 0; long beforeEnd = 0; long afterEnd = 0; keysStartBefore->GetValue().ToLong(&beforeStart); keysStartAfter->GetValue().ToLong(&afterStart); keysEndBefore->GetValue().ToLong(&beforeEnd); keysEndAfter->GetValue().ToLong(&afterEnd); // For each row for (int i=0;iStart.GetMS(),true); endF = VFR_Output.GetFrameAtTime(cur->End.GetMS(),false); changed = false; // Get closest for start closest = GetClosestKeyFrame(startF); if ((closest > startF && closest-startF <= beforeStart) || (closest < startF && startF-closest <= afterStart)) { cur->Start.SetMS(VFR_Output.GetTimeAtFrame(closest,true)); changed = true; } // Get closest for end closest = GetClosestKeyFrame(endF)-1; if ((closest > endF && closest-endF <= beforeEnd) || (closest < endF && endF-closest <= afterEnd)) { cur->End.SetMS(VFR_Output.GetTimeAtFrame(closest,false)); changed = true; } // Apply changes if (changed) { cur->UpdateData(); } } } // Update grid grid->ass->FlagAsModified(); grid->CommitChanges(); }