1
0
mirror of synced 2024-11-24 23:50:21 +01:00
OpenTaiko/TJAPlayer3/Songs/CDTXStyleExtractor.cs
2021-09-21 00:16:38 +02:00

546 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace TJAPlayer3
{
/// <summary>
/// CDTXStyleExtractor determines if there is a session notation, and if there is then
/// it returns a sheet of music clipped according to the specified player Side.
///
/// The process operates as follows:
/// 1. Break the string up into top-level sections of the following types:
/// a) STYLE Single
/// b) STYLE Double/Couple
/// c) STYLE unrecognized
/// d) non-STYLE
/// 2. Within the top-level sections, break each up into sub-sections of the following types:
/// a) sheet START P1
/// b) sheet START P2
/// c) sheet START bare
/// d) sheet START unrecognized
/// e) non-sheet
/// 3. For the current seqNo, rank the found sheets
/// using a per-seqNo set of rankings for each
/// relevant section/subsection combination.
/// 4. Determine the best-ranked sheet
/// 5. Remove sheets other than the best-ranked
/// 6. Remove top-level STYLE-type sections which no longer contain a sheet
/// 7. From supported STYLE-type sections, remove non-sheet subsections beyond
/// the selected sheet, to reduce risk of incorrect command processing.
/// 8. Reassemble the string
/// </summary>
public static class CDTXStyleExtractor
{
private const RegexOptions StyleExtractorRegexOptions =
RegexOptions.Compiled |
RegexOptions.CultureInvariant |
RegexOptions.IgnoreCase |
RegexOptions.Multiline |
RegexOptions.Singleline;
private const string StylePrefixRegexPattern = @"^STYLE\s*:\s*";
private const string SheetStartPrefixRegexPattern = @"^#START";
private static readonly string StyleSingleSectionRegexMatchPattern =
$"{StylePrefixRegexPattern}(?:Single|1)";
private static readonly string StyleDoubleSectionRegexMatchPattern =
$"{StylePrefixRegexPattern}(?:Double|Couple|2)";
private static readonly string StyleUnrecognizedSectionRegexMatchPattern =
$"{StylePrefixRegexPattern}";
private static readonly string SheetStartBareRegexMatchPattern =
$"{SheetStartPrefixRegexPattern}$";
private static readonly string SheetStartP1RegexMatchPattern =
$"{SheetStartPrefixRegexPattern}\\s*P1";
private static readonly string SheetStartP2RegexMatchPattern =
$"{SheetStartPrefixRegexPattern}\\s*P2";
private static readonly string SheetStartUnrecognizedRegexMatchPattern =
$"{SheetStartPrefixRegexPattern}.*$";
private static readonly Regex SectionSplitRegex = new Regex($"(?={StylePrefixRegexPattern})", StyleExtractorRegexOptions);
private static readonly Regex SubSectionSplitRegex = new Regex($"(?={SheetStartPrefixRegexPattern})|(?<=#END\\n)", StyleExtractorRegexOptions);
private static readonly Regex StyleSingleSectionMatchRegex = new Regex(StyleSingleSectionRegexMatchPattern, StyleExtractorRegexOptions);
private static readonly Regex StyleDoubleSectionMatchRegex = new Regex(StyleDoubleSectionRegexMatchPattern, StyleExtractorRegexOptions);
private static readonly Regex StyleUnrecognizedSectionMatchRegex = new Regex(StyleUnrecognizedSectionRegexMatchPattern, StyleExtractorRegexOptions);
private static readonly Regex SheetStartPrefixMatchRegex = new Regex(SheetStartPrefixRegexPattern, StyleExtractorRegexOptions);
private static readonly Regex SheetStartBareMatchRegex = new Regex(SheetStartBareRegexMatchPattern, StyleExtractorRegexOptions);
private static readonly Regex SheetStartP1MatchRegex = new Regex(SheetStartP1RegexMatchPattern, StyleExtractorRegexOptions);
private static readonly Regex SheetStartP2MatchRegex = new Regex(SheetStartP2RegexMatchPattern, StyleExtractorRegexOptions);
private static readonly Regex SheetStartUnrecognizedMatchRegex = new Regex(SheetStartUnrecognizedRegexMatchPattern, StyleExtractorRegexOptions);
private static readonly SectionKindAndSubSectionKind StyleSingleAndSheetStartBare =
new SectionKindAndSubSectionKind(SectionKind.StyleSingle, SubSectionKind.SheetStartBare);
private static readonly SectionKindAndSubSectionKind StyleSingleAndSheetStartP1 =
new SectionKindAndSubSectionKind(SectionKind.StyleSingle, SubSectionKind.SheetStartP1);
private static readonly SectionKindAndSubSectionKind StyleSingleAndSheetStartP2 =
new SectionKindAndSubSectionKind(SectionKind.StyleSingle, SubSectionKind.SheetStartP2);
private static readonly SectionKindAndSubSectionKind StyleSingleAndSheetStartUnrecognized =
new SectionKindAndSubSectionKind(SectionKind.StyleSingle, SubSectionKind.SheetStartUnrecognized);
private static readonly SectionKindAndSubSectionKind StyleDoubleAndSheetStartBare =
new SectionKindAndSubSectionKind(SectionKind.StyleDouble, SubSectionKind.SheetStartBare);
private static readonly SectionKindAndSubSectionKind StyleDoubleAndSheetStartP1 =
new SectionKindAndSubSectionKind(SectionKind.StyleDouble, SubSectionKind.SheetStartP1);
private static readonly SectionKindAndSubSectionKind StyleDoubleAndSheetStartP2 =
new SectionKindAndSubSectionKind(SectionKind.StyleDouble, SubSectionKind.SheetStartP2);
private static readonly SectionKindAndSubSectionKind StyleDoubleAndSheetStartUnrecognized =
new SectionKindAndSubSectionKind(SectionKind.StyleDouble, SubSectionKind.SheetStartUnrecognized);
private static readonly SectionKindAndSubSectionKind StyleUnrecognizedAndSheetStartBare =
new SectionKindAndSubSectionKind(SectionKind.StyleUnrecognized, SubSectionKind.SheetStartBare);
private static readonly SectionKindAndSubSectionKind StyleUnrecognizedAndSheetStartP1 =
new SectionKindAndSubSectionKind(SectionKind.StyleUnrecognized, SubSectionKind.SheetStartP1);
private static readonly SectionKindAndSubSectionKind StyleUnrecognizedAndSheetStartP2 =
new SectionKindAndSubSectionKind(SectionKind.StyleUnrecognized, SubSectionKind.SheetStartP2);
private static readonly SectionKindAndSubSectionKind StyleUnrecognizedAndSheetStartUnrecognized =
new SectionKindAndSubSectionKind(SectionKind.StyleUnrecognized, SubSectionKind.SheetStartUnrecognized);
private static readonly SectionKindAndSubSectionKind NonStyleAndSheetStartBare =
new SectionKindAndSubSectionKind(SectionKind.NonStyle, SubSectionKind.SheetStartBare);
private static readonly SectionKindAndSubSectionKind NonStyleAndSheetStartP1 =
new SectionKindAndSubSectionKind(SectionKind.NonStyle, SubSectionKind.SheetStartP1);
private static readonly SectionKindAndSubSectionKind NonStyleAndSheetStartP2 =
new SectionKindAndSubSectionKind(SectionKind.NonStyle, SubSectionKind.SheetStartP2);
private static readonly SectionKindAndSubSectionKind NonStyleAndSheetStartUnrecognized =
new SectionKindAndSubSectionKind(SectionKind.NonStyle, SubSectionKind.SheetStartUnrecognized);
private static readonly IDictionary<SectionKindAndSubSectionKind, int>[]
SeqNoSheetRanksBySectionKindAndSubSectionKind =
{
// seqNo 0
new Dictionary<SectionKindAndSubSectionKind, int>
{
[StyleSingleAndSheetStartBare] = 1,
[StyleSingleAndSheetStartP1] = 2,
[StyleSingleAndSheetStartUnrecognized] = 3,
[NonStyleAndSheetStartBare] = 4,
[NonStyleAndSheetStartP1] = 5,
[NonStyleAndSheetStartUnrecognized] = 6,
[StyleUnrecognizedAndSheetStartBare] = 7,
[StyleUnrecognizedAndSheetStartUnrecognized] = 8,
[StyleUnrecognizedAndSheetStartP1] = 9,
[StyleDoubleAndSheetStartP1] = 10,
[StyleDoubleAndSheetStartBare] = 11,
[StyleDoubleAndSheetStartUnrecognized] = 12,
[StyleSingleAndSheetStartP2] = 13,
[NonStyleAndSheetStartP2] = 14,
[StyleUnrecognizedAndSheetStartP2] = 15,
[StyleDoubleAndSheetStartP2] = 16,
},
// seqNo 1
new Dictionary<SectionKindAndSubSectionKind, int>
{
[StyleDoubleAndSheetStartP1] = 1,
[StyleUnrecognizedAndSheetStartP1] = 2,
[NonStyleAndSheetStartP1] = 3,
[StyleSingleAndSheetStartP1] = 4,
[StyleDoubleAndSheetStartBare] = 5,
[StyleDoubleAndSheetStartUnrecognized] = 6,
[StyleUnrecognizedAndSheetStartBare] = 7,
[StyleUnrecognizedAndSheetStartUnrecognized] = 8,
[StyleSingleAndSheetStartBare] = 9,
[StyleSingleAndSheetStartUnrecognized] = 10,
[NonStyleAndSheetStartBare] = 11,
[NonStyleAndSheetStartUnrecognized] = 12,
[StyleDoubleAndSheetStartP2] = 13,
[StyleUnrecognizedAndSheetStartP2] = 14,
[NonStyleAndSheetStartP2] = 15,
[StyleSingleAndSheetStartP2] = 16,
},
// seqNo 2
new Dictionary<SectionKindAndSubSectionKind, int>
{
[StyleDoubleAndSheetStartP2] = 1,
[StyleUnrecognizedAndSheetStartP2] = 2,
[NonStyleAndSheetStartP2] = 3,
[StyleSingleAndSheetStartP2] = 4,
[StyleDoubleAndSheetStartUnrecognized] = 5,
[StyleDoubleAndSheetStartBare] = 6,
[StyleUnrecognizedAndSheetStartUnrecognized] = 7,
[StyleUnrecognizedAndSheetStartBare] = 8,
[StyleSingleAndSheetStartUnrecognized] = 9,
[StyleSingleAndSheetStartBare] = 10,
[NonStyleAndSheetStartUnrecognized] = 11,
[NonStyleAndSheetStartBare] = 12,
[StyleDoubleAndSheetStartP1] = 13,
[StyleUnrecognizedAndSheetStartP1] = 14,
[NonStyleAndSheetStartP1] = 15,
[StyleSingleAndSheetStartP1] = 16,
},
};
public static string tセッション譜面がある(string strTJA, int seqNo, string strファイル名の絶対パス)
{
void TraceError(string subMessage)
{
Trace.TraceError(FormatTraceMessage(subMessage));
}
string FormatTraceMessage(string subMessage)
{
return $"{nameof(CDTXStyleExtractor)} {subMessage} (seqNo={seqNo}, {strファイル名の絶対パス})";
}
//入力された譜面がnullでないかチェック。
if (string.IsNullOrEmpty(strTJA))
{
TraceError("is returning its input value early due to null or empty strTJA.");
return strTJA;
}
// 1. Break the string up into top-level sections of the following types:
// a) STYLE Single
// b) STYLE Double/Couple
// c) STYLE unrecognized
// d) non-STYLE
var sections = GetSections(strTJA);
// 2. Within the top-level sections, break each up into sub-sections of the following types:
// a) sheet START P1
// b) sheet START P2
// c) sheet START bare
// d) sheet START unrecognized
// e) non-sheet
SubdivideSectionsIntoSubSections(sections);
// 3. For the current seqNo, rank the found sheets
// using a per-seqNo set of rankings for each
// relevant section/subsection combination.
RankSheets(seqNo, sections);
// 4. Determine the best-ranked sheet
int bestRank;
try
{
bestRank = GetBestRank(sections);
}
catch (Exception)
{
TraceError("is returning its input value early due to an inability to determine the best rank. This can occur if a course contains no #START.");
return strTJA;
}
// 5. Remove sheets other than the best-ranked
RemoveSheetsOtherThanTheBestRanked(sections, bestRank);
// 6. Remove top-level STYLE-type sections which no longer contain a sheet
RemoveRecognizedStyleSectionsWithoutSheets(sections);
// 7. From supported STYLE-type sections, remove non-sheet subsections beyond
// the selected sheet, to reduce risk of incorrect command processing.
RemoveStyleSectionSubSectionsBeyondTheSelectedSheet(sections);
// 8. Reassemble the string
return Reassemble(sections);
}
// 1. Break the string up into top-level sections of the following types:
// a) STYLE Single
// b) STYLE Double/Couple
// c) STYLE unrecognized
// d) non-STYLE
private static List<Section> GetSections(string strTJA)
{
return SectionSplitRegex
.Split(strTJA)
.Select(o => new Section(GetSectionKind(o), o))
.ToList();
}
private static SectionKind GetSectionKind(string section)
{
if (StyleSingleSectionMatchRegex.IsMatch(section))
{
return SectionKind.StyleSingle;
}
if (StyleDoubleSectionMatchRegex.IsMatch(section))
{
return SectionKind.StyleDouble;
}
if (StyleUnrecognizedSectionMatchRegex.IsMatch(section))
{
return SectionKind.StyleUnrecognized;
}
return SectionKind.NonStyle;
}
private enum SectionKind
{
StyleSingle,
StyleDouble,
StyleUnrecognized,
NonStyle
}
private sealed class Section
{
public readonly SectionKind SectionKind;
public readonly string OriginalRawValue;
public List<SubSection> SubSections;
public Section(SectionKind sectionKind, string originalRawValue)
{
SectionKind = sectionKind;
OriginalRawValue = originalRawValue;
}
}
// 2. Within the top-level sections, break each up into sub-sections of the following types:
// a) sheet START P1
// b) sheet START P2
// c) sheet START bare
// d) sheet START unrecognized
// e) non-sheet
private static void SubdivideSectionsIntoSubSections(IEnumerable<Section> sections)
{
foreach (var section in sections)
{
section.SubSections = SubSectionSplitRegex
.Split(section.OriginalRawValue)
.Select(o => new SubSection(GetSubsectionKind(o), o))
.ToList();
}
}
private static SubSectionKind GetSubsectionKind(string subsection)
{
if (SheetStartPrefixMatchRegex.IsMatch(subsection))
{
if (SheetStartBareMatchRegex.IsMatch(subsection))
{
return SubSectionKind.SheetStartBare;
}
if (SheetStartP1MatchRegex.IsMatch(subsection))
{
return SubSectionKind.SheetStartP1;
}
if (SheetStartP2MatchRegex.IsMatch(subsection))
{
return SubSectionKind.SheetStartP2;
}
if (SheetStartUnrecognizedMatchRegex.IsMatch(subsection))
{
return SubSectionKind.SheetStartUnrecognized;
}
}
return SubSectionKind.NonSheet;
}
private enum SubSectionKind
{
SheetStartP1,
SheetStartP2,
SheetStartBare,
SheetStartUnrecognized,
NonSheet
}
private sealed class SubSection
{
public readonly SubSectionKind SubSectionKind;
public readonly string OriginalRawValue;
public int Rank;
public SubSection(SubSectionKind subSectionKind, string originalRawValue)
{
SubSectionKind = subSectionKind;
OriginalRawValue = originalRawValue;
}
}
// 3. For the current seqNo, rank the found sheets
// using a per-seqNo set of rankings for each
// relevant section/subsection combination.
private static void RankSheets(int seqNo, IList<Section> sections)
{
var sheetRanksBySectionKindAndSubSectionKind = SeqNoSheetRanksBySectionKindAndSubSectionKind[seqNo];
foreach (var section in sections)
{
var sectionKind = section.SectionKind;
foreach (var subSection in section.SubSections)
{
var subSectionKind = subSection.SubSectionKind;
if (subSectionKind == SubSectionKind.NonSheet)
{
continue;
}
var sectionKindAndSubSectionKind = new SectionKindAndSubSectionKind(
sectionKind, subSectionKind);
subSection.Rank = sheetRanksBySectionKindAndSubSectionKind[sectionKindAndSubSectionKind];
}
}
}
private sealed class SectionKindAndSubSectionKind : IEquatable<SectionKindAndSubSectionKind>
{
public readonly SectionKind SectionKind;
public readonly SubSectionKind SubSectionKind;
public SectionKindAndSubSectionKind(SectionKind sectionKind, SubSectionKind subSectionKind)
{
SectionKind = sectionKind;
SubSectionKind = subSectionKind;
}
public bool Equals(SectionKindAndSubSectionKind other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return SectionKind == other.SectionKind && SubSectionKind == other.SubSectionKind;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is SectionKindAndSubSectionKind other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return ((int) SectionKind * 397) ^ (int) SubSectionKind;
}
}
public static bool operator ==(SectionKindAndSubSectionKind left, SectionKindAndSubSectionKind right)
{
return Equals(left, right);
}
public static bool operator !=(SectionKindAndSubSectionKind left, SectionKindAndSubSectionKind right)
{
return !Equals(left, right);
}
}
// 4. Determine the best-ranked sheet
private static int GetBestRank(IList<Section> sections)
{
return sections
.SelectMany(o => o.SubSections)
.Where(o => o.SubSectionKind != SubSectionKind.NonSheet)
.Select(o => o.Rank)
.Min();
}
// 5. Remove sheets other than the best-ranked
private static void RemoveSheetsOtherThanTheBestRanked(IList<Section> sections, int bestRank)
{
// We can safely remove based on > bestRank because the subsection types
// which are never removed always have a Rank value of 0.
foreach (var section in sections)
{
section.SubSections.RemoveAll(o => o.Rank > bestRank);
}
// If there was a tie for the best sheet,
// take the first and remove the rest.
var extraBestRankedSheets = new HashSet<SubSection>(sections
.SelectMany(o => o.SubSections)
.Where(o => o.Rank == bestRank)
.Skip(1));
foreach (var section in sections)
{
section.SubSections.RemoveAll(extraBestRankedSheets.Contains);
}
}
// 6. Remove top-level STYLE-type sections which no longer contain a sheet
private static void RemoveRecognizedStyleSectionsWithoutSheets(List<Section> sections)
{
// Note that we dare not remove SectionKind.StyleUnrecognized instances without sheets.
// The reason is because there are plenty of .tja files with weird STYLE: header values
// and which are located very early in the file. Removing those sections would remove
// important information, and was one of the problems with the years-old splitting code
// which was replaced in late summer 2018 and which is now being overhauled in early fall 2018.
sections.RemoveAll(o =>
(o.SectionKind == SectionKind.StyleSingle || o.SectionKind == SectionKind.StyleDouble) &&
o.SubSections.Count(subSection => subSection.SubSectionKind == SubSectionKind.NonSheet) == o.SubSections.Count);
}
// 7. From supported STYLE-type sections, remove non-sheet subsections beyond
// the selected sheet, to reduce risk of incorrect command processing.
private static void RemoveStyleSectionSubSectionsBeyondTheSelectedSheet(List<Section> sections)
{
foreach (var section in sections)
{
if (section.SectionKind == SectionKind.StyleSingle || section.SectionKind == SectionKind.StyleDouble)
{
var subSections = section.SubSections;
var lastIndex = subSections.FindIndex(o => o.SubSectionKind != SubSectionKind.NonSheet);
var removalIndex = lastIndex + 1;
if (lastIndex != -1 && removalIndex < subSections.Count)
{
subSections.RemoveRange(removalIndex, subSections.Count - removalIndex);
}
}
}
}
// 8. Reassemble the string
private static string Reassemble(List<Section> sections)
{
var sb = new StringBuilder();
foreach (var section in sections)
{
foreach (var subSection in section.SubSections)
{
sb.Append(subSection.OriginalRawValue);
}
}
return sb.ToString();
}
}
}