private static void AddKaraokeEffects(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx) { int prevNumActiveActions = stepIdx > 0 ? activeSectionsPerStep.Values[stepIdx - 1] : 0; int numActiveSections = activeSectionsPerStep.Values[stepIdx]; List <AssSection> singingSections = stepLine.Sections .Cast <AssSection>() .Skip(prevNumActiveActions) .Take(numActiveSections - prevNumActiveActions) .ToList(); switch (stepLine.KaraokeType) { case KaraokeType.Simple: ApplySimpleKaraokeEffect(singingSections); break; case KaraokeType.Fade: ApplyFadeKaraokeEffect(originalLine, stepLine, activeSectionsPerStep, stepIdx, singingSections); break; case KaraokeType.Glitch: ApplyGlitchKaraokeEffect(stepLine, singingSections); break; } }
public override object Clone() { AssLine newLine = new AssLine(Start, End); newLine.Assign(this); return(newLine); }
// Aegisub doesn't display the background box for certain parts of a subtitle: // - Whitespace at the start of a line of text // - Whitespace at the end of a line of text // - Sections that consist only of whitespace // YouTube displays the background box across the entire subtitle, however, // so for an accurate visualization, we need to enclose whitespace blocks in // other characters to "protect" them. private static void ProtectWhitespace(AssLine line) { for (int i = line.Sections.Count - 1; i >= 0; i--) { AssSection section = (AssSection)line.Sections[i]; if (Regex.IsMatch(section.Text, @"^[\r\n]+$")) { continue; } Match match = Regex.Match(section.Text, @"^([ \xA0]*)(.*?)([ \xA0]*)$"); if (match.Groups[1].Length == section.Text.Length) { // If the entire section is just whitespace, we have to use a non-whitespace character to make its background visible section.Text = CreateProtectedWhitespaceString(section.Text.Length, "."); section.ForeColor = ColorUtil.ChangeColorAlpha(section.ForeColor, 0); section.ShadowColors.Clear(); } else { // Otherwise, we can use non-breaking spaces (which don't normally work, but do work in our case because we later write // them out using Aegisub's \h sequence) section.Text = CreateProtectedWhitespaceString(match.Groups[1].Length, "\xA0") + match.Groups[2].Value + CreateProtectedWhitespaceString(match.Groups[3].Length, "\xA0"); } } }
private IEnumerable <AssLine> ApplyKaraokeType(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx) { int prevNumActiveActions = stepIdx > 0 ? activeSectionsPerStep.Values[stepIdx - 1] : 0; int numActiveSections = activeSectionsPerStep.Values[stepIdx]; List <AssSection> singingSections = stepLine.Sections .Cast <AssSection>() .Skip(prevNumActiveActions) .Take(numActiveSections - prevNumActiveActions) .ToList(); AssKaraokeStepContext context = new AssKaraokeStepContext { Document = this, OriginalLine = originalLine, ActiveSectionsPerStep = activeSectionsPerStep, StepLine = stepLine, StepIndex = stepIdx, NumActiveSections = numActiveSections, SingingSections = singingSections }; return(stepLine.KaraokeType.Apply(context)); }
private static AssLine CreateKaraokeStepLine(AssLine originalLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx) { TimeSpan timeOffset = activeSectionsPerStep.Keys[stepIdx]; int numActiveSections = activeSectionsPerStep.Values[stepIdx]; DateTime startTime = TimeUtil.SnapTimeToFrame((originalLine.Start + timeOffset).AddMilliseconds(20)); if (startTime >= originalLine.End) { return(null); } DateTime endTime; if (stepIdx < activeSectionsPerStep.Count - 1) { endTime = TimeUtil.SnapTimeToFrame((originalLine.Start + activeSectionsPerStep.Keys[stepIdx + 1]).AddMilliseconds(20)).AddMilliseconds(-1); if (endTime > originalLine.End) { endTime = originalLine.End; } } else { endTime = originalLine.End; } AssLine stepLine = (AssLine)originalLine.Clone(); stepLine.Start = startTime; stepLine.End = endTime; foreach (AssSection section in stepLine.Sections.Take(numActiveSections)) { section.Animations.RemoveAll(a => a is SecondaryColorAnimation); } foreach (AssSection section in stepLine.Sections.Skip(numActiveSections)) { section.ForeColor = section.SecondaryColor; section.Animations.RemoveAll(a => a is ForeColorAnimation); foreach (SecondaryColorAnimation anim in section.Animations.OfType <SecondaryColorAnimation>().ToList()) { section.Animations.Remove(anim); section.Animations.Add(new ForeColorAnimation(anim.StartTime, anim.StartColor, anim.EndTime, anim.EndColor)); } if (section.ForeColor.A == 0 && !section.Animations.OfType <ForeColorAnimation>().Any()) { section.ForeColor = Color.FromArgb(0, 0, 0, 0); section.BackColor = Color.FromArgb(0, 0, 0, 0); section.ShadowColors.Clear(); } } AddKaraokeEffects(originalLine, stepLine, activeSectionsPerStep, stepIdx); return(stepLine); }
private static void ApplyFadeKaraokeEffect(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx) { int numActiveSections = activeSectionsPerStep.Values[stepIdx]; AssSection singingSection = (AssSection)stepLine.Sections[numActiveSections - 1]; ApplyFadeInKaraokeEffect(stepLine, singingSection); ApplyFadeOutKaraokeEffect(originalLine, stepLine, activeSectionsPerStep, stepIdx); }
private static void InsertRubySection(AssLine line, Section format, string text, RubyPart rubyPart, int sectionIndex, ref int numSubSections) { Section section = (Section)format.Clone(); section.Text = text; section.RubyPart = rubyPart; line.Sections.Insert(sectionIndex + numSubSections, section); numSubSections++; }
private static void ApplyNativeKaraoke(AssLine line) { TimeSpan timeOffset = TimeSpan.Zero; foreach (AssSection section in line.Sections) { section.StartOffset = timeOffset; timeOffset += section.Duration; } }
private AssLine CreateTextVisualizationLine(AssLine originalLine) { AssLine textLine = (AssLine)originalLine.Clone(); foreach (AssSection textSection in textLine.Sections) { textSection.BackColor = Color.Empty; textSection.ShadowColors.Clear(); } return(textLine); }
private static bool CanUseNativeKaraoke(AssLine line) { return(line.KaraokeType.GetType() == typeof(SimpleKaraokeType) && line.Animations.Count == 0 && line.Sections.Cast <AssSection>().All(s => s.SecondaryColor.A == 0 && s.CurrentWordForeColor.IsEmpty && s.CurrentWordOutlineColor.IsEmpty && s.CurrentWordShadowColor.IsEmpty && s.Animations.Count == 0 && s.Duration != TimeSpan.Zero)); }
protected override void Assign(Line line) { base.Assign(line); AssLine assLine = (AssLine)line; Alpha = assLine.Alpha; Animations.Clear(); Animations.AddRange(assLine.Animations); KaraokeType = assLine.KaraokeType; }
internal void CreateTagSections(AssLine line, string text, AssTagContext context) { text = Regex.Replace(text, @"(?:\\N)+$", ""); HashSet <string> handledWholeLineTags = new HashSet <string>(); int start = 0; foreach (Match tagGroupMatch in Regex.Matches(text, @"\{(.*?)\}")) { int end = tagGroupMatch.Index; if (end > start || (context.Section.Duration > TimeSpan.Zero && Regex.IsMatch(tagGroupMatch.Groups[1].Value, @"\\k\s*\d+"))) { if (end == start) { context.Section.Text = "\x200B"; } else { context.Section.Text = text.Substring(start, end - start).Replace("\\N", "\r\n"); } line.Sections.Add(context.Section); context.Section = (AssSection)context.Section.Clone(); context.Section.Text = null; context.Section.Duration = TimeSpan.Zero; } foreach (Match tagMatch in Regex.Matches(tagGroupMatch.Groups[1].Value, @"\\(?<tag>fn|\d?[a-z]+)\s*(?<arg>\([^\(\)]*(?:\)|$)|[^\\\(\)]*)")) { if (!_tagHandlers.TryGetValue(tagMatch.Groups["tag"].Value, out AssTagHandlerBase handler)) { continue; } if (handler.AffectsWholeLine && !handledWholeLineTags.Add(tagMatch.Groups["tag"].Value)) { continue; } handler.Handle(context, tagMatch.Groups["arg"].Value.Trim()); } start = tagGroupMatch.Index + tagGroupMatch.Length; } if (start < text.Length) { context.Section.Text = text.Substring(start, text.Length - start).Replace("\\N", "\r\n"); line.Sections.Add(context.Section); } }
private AssLine CreateBackgroundVisualizationLine(AssLine originalLine) { AssLine backgroundLine = (AssLine)originalLine.Clone(); foreach (AssSection section in backgroundLine.Sections) { section.ForeColor = Color.Empty; section.SecondaryColor = Color.Empty; section.ShadowColors.Clear(); } return(backgroundLine); }
private IEnumerable <AssLine> CreateShadowVisualizationLines(AssLine originalLine) { if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.SoftShadow))) { for (float blur = 4; blur >= 2; blur--) { yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.SoftShadow), 2, blur)); } } if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.HardShadow))) { for (int offset = 3; offset >= 1; offset--) { yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.HardShadow), offset, 0)); } } if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.Bevel))) { yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.Bevel), -1, 0)); yield return(CreateShadowVisualizationLine( originalLine, s => { Color color = s.ShadowColors.GetOrDefault(ShadowType.Bevel); if (color.IsEmpty) { return Color.Empty; } if (color.R == 0x22 && color.G == 0x22 & color.B == 0x22) { return Color.FromArgb(color.A, 0xCC, 0xCC, 0xCC); } return color; }, 1, 0 )); } if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.Glow))) { for (float blur = 2; blur >= 1; blur -= 0.5f) { yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.Glow), 0, blur)); } } }
private static IEnumerable <AssLine> CreateEmulatedKaraokeLines(AssLine line) { SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line); for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++) { AssLine stepLine = CreateKaraokeStepLine(line, activeSectionsPerStep, stepIdx); if (stepLine != null) { yield return(stepLine); } } }
private static void ApplyGlitchKaraokeEffect(AssLine stepLine, AssSection singingSection) { if (singingSection.Text.Length == 0) { return; } ApplySimpleKaraokeEffect(singingSection); DateTime glitchEndTime = TimeUtil.Min(stepLine.Start.AddMilliseconds(70), stepLine.End); Util.CharacterRange[] charRanges = GetGlitchKaraokeCharacterRanges(singingSection.Text[0]); singingSection.Animations.Add(new GlitchingCharAnimation(stepLine.Start, glitchEndTime, charRanges)); }
private IEnumerable <AssLine> CreateEmulatedKaraokeLines(AssLine line) { SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line); for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++) { IEnumerable <AssLine> stepLines = CreateKaraokeStepLines(line, activeSectionsPerStep, stepIdx); foreach (AssLine stepLine in stepLines) { yield return(stepLine); } } }
protected IEnumerable <AssLine> CreateEmulatedKaraokeLines(AssLine line) { SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line); for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++) { IEnumerable <AssLine> stepLines = CreateKaraokeStepLines(line, activeSectionsPerStep, stepIdx); foreach (AssLine stepLine in stepLines) { stepLine.Sections.RemoveAll(s => s.Text == "\x200B"); // Remove empty sections that were added in CreateTagSections() yield return(stepLine); } } }
/// <summary> /// Unlike YouTube, where the native karaoke feature hides the unsung parts completely (text/background box/shadow), /// the equivalent in Aegisub (\k with a transparent secondary color) only hides the text. This means we need to /// switch to emulated karaoke (duplicated lines without \k) for visual correctness. /// </summary> private void EmulateKaraokeForLinesWithBackgroundOrShadow() { for (int i = 0; i < Lines.Count; i++) { AssLine line = (AssLine)Lines[i]; if (line.Sections.Cast <AssSection>().All(s => s.Duration == TimeSpan.Zero) || line.Sections.All(s => s.BackColor.A == 0 && s.ShadowColors.Count == 0)) { continue; } Lines.RemoveAt(i); Lines.InsertRange(i, CreateEmulatedKaraokeLines(line)); } }
private static void ApplyGlitchKaraokeEffect(AssLine stepLine, List <AssSection> singingSections) { AssSection singingSection = singingSections.LastOrDefault(s => s.RubyPart == RubyPart.None || s.RubyPart == RubyPart.Text); if (singingSection == null || singingSection.Text.Length == 0) { return; } ApplySimpleKaraokeEffect(singingSections); DateTime glitchEndTime = TimeUtil.Min(stepLine.Start.AddMilliseconds(70), stepLine.End); Util.CharacterRange[] charRanges = GetGlitchKaraokeCharacterRanges(singingSection.Text[0]); singingSection.Animations.Add(new GlitchingCharAnimation(stepLine.Start, glitchEndTime, charRanges)); }
private List <AssLine> ParseLine(AssDialogue dialogue, AssStyle style, AssStyleOptions styleOptions) { AssLine line = new AssLine( TimeUtil.RoundTimeToFrameCenter(dialogue.Start), TimeUtil.RoundTimeToFrameCenter(dialogue.End) ) { AnchorPoint = style.AnchorPoint }; string[] effects = dialogue.Effect.Split(';'); if (effects.Contains(EffectNames.NoAndroidDarkTextHack)) { line.AndroidDarkTextHackAllowed = false; } AssTagContext context = new AssTagContext { Document = this, InitialStyle = style, InitialStyleOptions = styleOptions, Style = style, StyleOptions = styleOptions, Line = line, Section = new AssSection() }; ApplyStyle(context.Section, style, styleOptions); CreateTagSections(line, dialogue.Text, context); CreateRubySections(line); List <AssLine> lines = new List <AssLine> { line }; foreach (AssTagContext.PostProcessor postProcessor in context.PostProcessors) { List <AssLine> extraLines = postProcessor(); if (extraLines != null) { lines.AddRange(extraLines); } } return(lines); }
private IEnumerable <AssLine> ExpandLineForKaraoke(AssLine line) { if (line.Sections.Cast <AssSection>().All(s => s.Duration == TimeSpan.Zero)) { return new[] { line } } ; if (CanUseNativeKaraoke(line)) { ApplyNativeKaraoke(line); return(new[] { line }); } return(CreateEmulatedKaraokeLines(line)); }
private void AppendLineTags(AssLine line, AssStyle style, AssLineContentBuilder lineContent) { if (line.AnchorPoint != style.AnchorPoint) { lineContent.AppendTag("an", GetAlignment(line.AnchorPoint)); } if (line.Position != null) { lineContent.AppendTag("pos", line.Position.Value.X, line.Position.Value.Y); } if (line.VerticalTextType != VerticalTextType.None) { lineContent.AppendTag("ytvert", AssVerticalTypeTagHandler.GetVerticalTextTypeId(line.VerticalTextType)); } }
private static void CreateRubySections(AssLine line) { for (int sectionIdx = line.Sections.Count - 1; sectionIdx >= 0; sectionIdx--) { AssSection section = (AssSection)line.Sections[sectionIdx]; if (section.RubyPosition == RubyPosition.None) { continue; } MatchCollection matches = Regex.Matches(section.Text, @"\[(?<text>.+?)/(?<ruby>.+?)\]"); if (matches.Count == 0) { continue; } line.Sections.RemoveAt(sectionIdx); int interStartPos = 0; int numSubSections = 0; foreach (Match match in matches) { if (match.Index > interStartPos) { InsertRubySection(line, section, section.Text.Substring(interStartPos, match.Index - interStartPos), RubyPart.None, sectionIdx, ref numSubSections); } InsertRubySection(line, section, match.Groups["text"].Value, RubyPart.Text, sectionIdx, ref numSubSections); InsertRubySection(line, section, "(", RubyPart.Parenthesis, sectionIdx, ref numSubSections); InsertRubySection(line, section, match.Groups["ruby"].Value, section.RubyPosition == RubyPosition.Below ? RubyPart.RubyBelow : RubyPart.RubyAbove, sectionIdx, ref numSubSections); InsertRubySection(line, section, ")", RubyPart.Parenthesis, sectionIdx, ref numSubSections); interStartPos = match.Index + match.Length; } if (interStartPos < section.Text.Length) { InsertRubySection(line, section, section.Text.Substring(interStartPos), RubyPart.None, sectionIdx, ref numSubSections); } ((AssSection)line.Sections[sectionIdx]).Duration = section.Duration; } }
private AssLine CreateShadowVisualizationLine(AssLine originalLine, Func <AssSection, Color> getShadowColor, float positionOffset, float blur) { AssLine shadowLine = (AssLine)originalLine.Clone(); if (positionOffset != 0) { PointF position = shadowLine.Position ?? GetDefaultPosition(shadowLine.AnchorPoint); shadowLine.Position = new PointF(position.X + positionOffset, position.Y + positionOffset); } foreach (AssSection shadowSection in shadowLine.Sections) { shadowSection.ForeColor = getShadowColor(shadowSection); shadowSection.BackColor = Color.Empty; shadowSection.ShadowColors.Clear(); shadowSection.Blur = blur; } return(shadowLine); }
private static IEnumerable <AssLine> ExpandLineForKaraoke(AssLine line) { if (line.Sections.Cast <AssSection>().All(s => s.Duration == TimeSpan.Zero)) { yield return(line); yield break; } SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line); for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++) { AssLine stepLine = CreateKaraokeStepLine(line, activeSectionsPerStep, stepIdx); if (stepLine != null) { yield return(stepLine); } } }
private static void AddKaraokeEffects(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx) { int numActiveSections = activeSectionsPerStep.Values[stepIdx]; AssSection singingSection = (AssSection)stepLine.Sections[numActiveSections - 1]; switch (stepLine.KaraokeType) { case KaraokeType.Simple: ApplySimpleKaraokeEffect(singingSection); break; case KaraokeType.Fade: ApplyFadeKaraokeEffect(originalLine, stepLine, activeSectionsPerStep, stepIdx); break; case KaraokeType.Glitch: ApplyGlitchKaraokeEffect(stepLine, singingSection); break; } }
private static void MoveLineBreaksToSeparateSections(AssLine line) { for (int sectionIdx = line.Sections.Count - 1; sectionIdx >= 0; sectionIdx--) { AssSection section = (AssSection)line.Sections[sectionIdx]; Match match = Regex.Match(section.Text, @"((?:\r\n)+|[^\r\n]+)+"); if (!match.Success || match.Groups[1].Captures.Count == 1) { continue; } line.Sections.RemoveAt(sectionIdx); for (int i = 0; i < match.Groups[1].Captures.Count; i++) { AssSection subSection = (AssSection)section.Clone(); subSection.Text = match.Groups[1].Captures[i].Value; line.Sections.Insert(sectionIdx + i, subSection); } } }
// Aegisub's background boxes can have padding just like on YouTube, and the horizontal and vertical padding // can even be different. The big downside of this feature, however, is that background boxes of adjacent // sections overlap each other which looks like a mess. Therefore we don't use it at all (all the styles // have an outline thickness of 0.01) and instead add a space at the start and end of each line of text // to emulate the YouTube horizontal padding manually. private static void EmulateBorders(AssLine line) { for (int i = 0; i < line.Sections.Count; i++) { AssSection section = (AssSection)line.Sections[i]; if (Regex.IsMatch(section.Text, @"^[\r\n]+$")) { continue; } if (i == 0 || line.Sections[i - 1].Text.EndsWith("\r\n")) { section.Text = " " + section.Text; } if (i == line.Sections.Count - 1 || line.Sections[i + 1].Text.StartsWith("\r\n")) { section.Text = section.Text + " "; } } }
private void AppendLineTags(AssLine line, AssStyle style, AssLineContentBuilder lineContent) { if (line.AnchorPoint != style.AnchorPoint) { lineContent.AppendTag("an", GetAlignment(line.AnchorPoint)); } if (line.Position != null) { lineContent.AppendTag("pos", line.Position.Value.X, line.Position.Value.Y); } if (line.VerticalTextType != VerticalTextType.None) { lineContent.AppendTag("ytvert", AssVerticalTypeTagHandler.GetVerticalTextTypeId(line.HorizontalTextDirection, line.VerticalTextType)); } else if (line.HorizontalTextDirection != HorizontalTextDirection.LeftToRight) { lineContent.AppendTag("ytdir", AssHorizontalTextDirectionTag.GetHorizontalTextDirectionId(line.HorizontalTextDirection)); } }