forked from mia/Aegisub
334 lines
9 KiB
C++
334 lines
9 KiB
C++
// Copyright (c) 2011, Thomas Goyne <plorkyeran@aegisub.org>
|
|
//
|
|
// Permission to use, copy, modify, and distribute this software for any
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
// copyright notice and this permission notice appear in all copies.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
//
|
|
// Aegisub Project http://www.aegisub.org/
|
|
|
|
/// @file ass_karaoke.cpp
|
|
/// @brief Parse and manipulate ASSA karaoke tags
|
|
/// @ingroup subs_storage
|
|
///
|
|
|
|
|
|
#include "config.h"
|
|
|
|
#include "ass_karaoke.h"
|
|
|
|
#include "ass_dialogue.h"
|
|
#include "ass_file.h"
|
|
#include "ass_override.h"
|
|
#include "include/aegisub/context.h"
|
|
#include "selection_controller.h"
|
|
|
|
#ifndef AGI_PRE
|
|
#include <wx/intl.h>
|
|
#endif
|
|
|
|
wxString AssKaraoke::Syllable::GetText(bool k_tag) const {
|
|
wxString ret;
|
|
|
|
if (k_tag)
|
|
ret = wxString::Format("{%s%d}", tag_type, (duration + 5) / 10);
|
|
|
|
size_t idx = 0;
|
|
for (std::map<size_t, wxString>::const_iterator ovr = ovr_tags.begin(); ovr != ovr_tags.end(); ++ovr) {
|
|
ret += text.Mid(idx, ovr->first - idx);
|
|
ret += ovr->second;
|
|
idx = ovr->first;
|
|
}
|
|
ret += text.Mid(idx);
|
|
return ret;
|
|
}
|
|
|
|
|
|
AssKaraoke::AssKaraoke(AssDialogue *line, bool auto_split, bool normalize)
|
|
: no_announce(false)
|
|
{
|
|
if (line) SetLine(line, auto_split, normalize);
|
|
}
|
|
|
|
void AssKaraoke::SetLine(AssDialogue *line, bool auto_split, bool normalize) {
|
|
active_line = line;
|
|
line->ParseAssTags();
|
|
|
|
syls.clear();
|
|
Syllable syl;
|
|
syl.start_time = line->Start;
|
|
syl.duration = 0;
|
|
syl.tag_type = "\\k";
|
|
|
|
for (size_t i = 0; i < line->Blocks.size(); ++i) {
|
|
AssDialogueBlock *block = line->Blocks[i];
|
|
wxString text = block->GetText();
|
|
|
|
if (dynamic_cast<AssDialogueBlockPlain*>(block)) {
|
|
// treat comments as overrides rather than dialogue
|
|
if (text.size() && text[0] == '{')
|
|
syl.ovr_tags[syl.text.size()] += text;
|
|
else
|
|
syl.text += text;
|
|
}
|
|
else if (dynamic_cast<AssDialogueBlockDrawing*>(block)) {
|
|
// drawings aren't override tags but they shouldn't show up in the
|
|
// stripped text so pretend they are
|
|
syl.ovr_tags[syl.text.size()] += text;
|
|
}
|
|
else if (AssDialogueBlockOverride *ovr = dynamic_cast<AssDialogueBlockOverride*>(block)) {
|
|
bool in_tag = false;
|
|
for (size_t j = 0; j < ovr->Tags.size(); ++j) {
|
|
AssOverrideTag *tag = ovr->Tags[j];
|
|
|
|
if (tag->IsValid() && tag->Name.Left(2).Lower() == "\\k") {
|
|
if (in_tag) {
|
|
syl.ovr_tags[syl.text.size()] += "}";
|
|
in_tag = false;
|
|
}
|
|
|
|
// Dealing with both \K and \kf is mildly annoying so just
|
|
// convert them both to \kf
|
|
if (tag->Name == "\\K") tag->Name = "\\kf";
|
|
|
|
// Don't bother including zero duration zero length syls
|
|
if (syl.duration > 0 || !syl.text.empty()) {
|
|
syls.push_back(syl);
|
|
syl.text.clear();
|
|
syl.ovr_tags.clear();
|
|
}
|
|
|
|
syl.tag_type = tag->Name;
|
|
syl.start_time += syl.duration;
|
|
syl.duration = tag->Params[0]->Get(0) * 10;
|
|
}
|
|
else {
|
|
wxString& otext = syl.ovr_tags[syl.text.size()];
|
|
// Merge adjacent override tags
|
|
if (j == 0 && otext.size())
|
|
otext.RemoveLast();
|
|
else if (!in_tag)
|
|
otext += "{";
|
|
|
|
in_tag = true;
|
|
otext += *tag;
|
|
}
|
|
}
|
|
|
|
if (in_tag)
|
|
syl.ovr_tags[syl.text.size()] += "}";
|
|
}
|
|
}
|
|
|
|
syls.push_back(syl);
|
|
|
|
line->ClearBlocks();
|
|
|
|
if (normalize) {
|
|
// Normalize the syllables so that the total duration is equal to the line length
|
|
int end_time = active_line->End;
|
|
int last_end = syl.start_time + syl.duration;
|
|
|
|
// Total duration is shorter than the line length so just extend the last
|
|
// syllable; this has no effect on rendering but is easier to work with
|
|
if (last_end < end_time)
|
|
syls.back().duration += end_time - last_end;
|
|
else if (last_end > end_time) {
|
|
// Truncate any syllables that extend past the end of the line
|
|
for (size_t i = 0; i < size(); ++i) {
|
|
if (syls[i].start_time > end_time) {
|
|
syls[i].start_time = end_time;
|
|
syls[i].duration = 0;
|
|
}
|
|
else {
|
|
syls[i].duration = std::min(syls[i].duration, end_time - syls[i].start_time);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add karaoke splits at each space
|
|
if (auto_split && syls.size() == 1) {
|
|
size_t pos;
|
|
no_announce = true;
|
|
while ((pos = syls.back().text.find(' ')) != wxString::npos)
|
|
AddSplit(syls.size() - 1, pos + 1);
|
|
no_announce = false;
|
|
}
|
|
|
|
AnnounceSyllablesChanged();
|
|
}
|
|
|
|
wxString AssKaraoke::GetText() const {
|
|
wxString text;
|
|
text.reserve(size() * 10);
|
|
|
|
for (iterator it = begin(); it != end(); ++it) {
|
|
text += it->GetText(true);
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
wxString AssKaraoke::GetTagType() const {
|
|
return begin()->tag_type;
|
|
}
|
|
|
|
void AssKaraoke::SetTagType(wxString const& new_type) {
|
|
for (size_t i = 0; i < size(); ++i) {
|
|
syls[i].tag_type = new_type;
|
|
}
|
|
}
|
|
|
|
void AssKaraoke::AddSplit(size_t syl_idx, size_t pos) {
|
|
syls.insert(syls.begin() + syl_idx + 1, Syllable());
|
|
Syllable &syl = syls[syl_idx];
|
|
Syllable &new_syl = syls[syl_idx + 1];
|
|
|
|
// If the syl is empty or the user is adding a syllable past the last
|
|
// character then pos will be out of bounds. Doing this is a bit goofy,
|
|
// but it's sometimes required for complex karaoke scripts
|
|
if (pos < syl.text.size()) {
|
|
new_syl.text = syl.text.Mid(pos);
|
|
syl.text = syl.text.Left(pos);
|
|
}
|
|
|
|
if (new_syl.text.empty())
|
|
new_syl.duration = 0;
|
|
else if (syl.text.empty()) {
|
|
new_syl.duration = syl.duration;
|
|
syl.duration = 0;
|
|
}
|
|
else {
|
|
new_syl.duration = syl.duration * new_syl.text.size() / (syl.text.size() + new_syl.text.size());
|
|
syl.duration -= new_syl.duration;
|
|
}
|
|
|
|
assert(syl.duration >= 0);
|
|
|
|
new_syl.start_time = syl.start_time + syl.duration;
|
|
new_syl.tag_type = syl.tag_type;
|
|
|
|
// Move all override tags after the split to the new syllable and fix the indices
|
|
size_t text_len = syl.text.size();
|
|
for (ovr_iterator it = syl.ovr_tags.begin(); it != syl.ovr_tags.end(); ) {
|
|
if (it->first < text_len)
|
|
++it;
|
|
else {
|
|
new_syl.ovr_tags[it->first - text_len] = it->second;
|
|
syl.ovr_tags.erase(it++);
|
|
}
|
|
}
|
|
|
|
if (!no_announce) AnnounceSyllablesChanged();
|
|
}
|
|
|
|
void AssKaraoke::RemoveSplit(size_t syl_idx) {
|
|
// Don't allow removing the first syllable
|
|
if (syl_idx == 0) return;
|
|
|
|
Syllable &syl = syls[syl_idx];
|
|
Syllable &prev = syls[syl_idx - 1];
|
|
|
|
prev.duration += syl.duration;
|
|
for (ovr_iterator it = syl.ovr_tags.begin(); it != syl.ovr_tags.end(); ++it) {
|
|
prev.ovr_tags[it->first + prev.text.size()] = it->second;
|
|
}
|
|
prev.text += syl.text;
|
|
|
|
syls.erase(syls.begin() + syl_idx);
|
|
|
|
if (!no_announce) AnnounceSyllablesChanged();
|
|
}
|
|
|
|
void AssKaraoke::SetStartTime(size_t syl_idx, int time) {
|
|
// Don't allow moving the first syllable
|
|
if (syl_idx == 0) return;
|
|
|
|
Syllable &syl = syls[syl_idx];
|
|
Syllable &prev = syls[syl_idx - 1];
|
|
|
|
assert(time >= prev.start_time);
|
|
assert(time <= syl.start_time + syl.duration);
|
|
|
|
int delta = time - syl.start_time;
|
|
syl.start_time = time;
|
|
syl.duration -= delta;
|
|
prev.duration += delta;
|
|
}
|
|
|
|
void AssKaraoke::SetLineTimes(int start_time, int end_time) {
|
|
assert(end_time >= start_time);
|
|
|
|
size_t idx = 0;
|
|
do {
|
|
int delta = start_time - syls[idx].start_time;
|
|
syls[idx].start_time = start_time;
|
|
syls[idx].duration = std::max(0, syls[idx].duration - delta);
|
|
} while (++idx < syls.size() && syls[idx].start_time < start_time);
|
|
|
|
idx = syls.size() - 1;
|
|
while (syls[idx].start_time > end_time) {
|
|
syls[idx].start_time = end_time;
|
|
syls[idx].duration = 0;
|
|
--idx;
|
|
}
|
|
syls[idx].duration = end_time - syls[idx].start_time;
|
|
}
|
|
|
|
void AssKaraoke::SplitLines(std::set<AssDialogue*> const& lines, agi::Context *c) {
|
|
if (lines.empty()) return;
|
|
|
|
AssKaraoke kara;
|
|
|
|
SubtitleSelection sel = c->selectionController->GetSelectedSet();
|
|
|
|
bool did_split = false;
|
|
for (std::list<AssEntry*>::iterator it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
|
|
AssDialogue *diag = dynamic_cast<AssDialogue*>(*it);
|
|
if (!diag || !lines.count(diag)) continue;
|
|
|
|
kara.SetLine(diag);
|
|
|
|
// If there aren't at least two tags there's nothing to split
|
|
if (kara.size() < 2) continue;
|
|
|
|
bool in_sel = sel.count(diag) > 0;
|
|
|
|
c->ass->Line.erase(it++);
|
|
|
|
for (iterator kit = kara.begin(); kit != kara.end(); ++kit) {
|
|
AssDialogue *new_line = new AssDialogue(*diag);
|
|
|
|
new_line->Start = kit->start_time;
|
|
new_line->End = kit->start_time + kit->duration;
|
|
new_line->Text = kit->GetText(false);
|
|
|
|
c->ass->Line.insert(it, new_line);
|
|
|
|
if (in_sel)
|
|
sel.insert(new_line);
|
|
}
|
|
sel.erase(diag);
|
|
delete diag;
|
|
--it;
|
|
|
|
did_split = true;
|
|
}
|
|
|
|
if (!did_split) return;
|
|
|
|
c->ass->Commit(_("splitting"), AssFile::COMMIT_DIAG_ADDREM | AssFile::COMMIT_DIAG_FULL);
|
|
|
|
AssDialogue *new_active = c->selectionController->GetActiveLine();
|
|
if (!sel.count(c->selectionController->GetActiveLine()))
|
|
new_active = *sel.begin();
|
|
c->selectionController->SetSelectionAndActive(sel, new_active);
|
|
}
|