// Copyright (c) 2011, Thomas Goyne // // 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/ // // $Id$ /// @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 #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::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(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(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(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) { // Shrink each syllable proportionately int start_time = active_line->Start; double scale_factor = double(end_time - start_time) / (last_end - start_time); for (size_t i = 0; i < size(); ++i) { syls[i].start_time = start_time + scale_factor * (syls[i].start_time - start_time); } for (int i = size(); i > 0; --i) { syls[i - 1].duration = end_time - syls[i - 1].start_time; end_time = syls[i - 1].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 { 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 const& lines, agi::Context *c) { if (lines.empty()) return; AssKaraoke kara; SubtitleSelection sel = c->selectionController->GetSelectedSet(); bool did_split = false; for (std::list::iterator it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) { AssDialogue *diag = dynamic_cast(*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; } AssDialogue *new_active = c->selectionController->GetActiveLine(); if (!sel.count(c->selectionController->GetActiveLine())) new_active = *sel.begin(); c->selectionController->SetSelectionAndActive(sel, new_active); if (did_split) c->ass->Commit(_("splitting"), AssFile::COMMIT_DIAG_ADDREM | AssFile::COMMIT_DIAG_FULL); }