public IEnumerable <Issue> GetIssue(BeatmapSet beatmapSet, string audioPath, bool isHitSound = false) { // `Audio.GetBitrate` has a < 0.1 kbps error margin, so we should round this. double bitrate = Math.Round(AudioBASS.GetBitrate(audioPath)); // Hit sounds only need to follow the lower limit for quality requirements, as Wave // (which is the most used hit sound format currently) is otherwise uncompressed anyway. if (bitrate >= 128 && (bitrate <= 192 || isHitSound)) { yield break; } string audioRelPath = PathStatic.RelativePath(audioPath, beatmapSet.songPath); if (!isHitSound) { yield return(new Issue(GetTemplate("Bitrate"), null, audioRelPath, $"{bitrate:0.##}", (bitrate < 128 ? "low" : "high"))); } else { yield return(new Issue(GetTemplate("Hit Sound"), null, audioRelPath, $"{bitrate:0.##}")); } }
public override IEnumerable <Issue> GetIssues(BeatmapSet beatmapSet) { if (beatmapSet.beatmaps.All(beatmap => beatmap.GetAudioFilePath() == null)) { foreach (var beatmap in beatmapSet.beatmaps) { yield return(new Issue(GetTemplate("Missing"), beatmap)); } } else { IEnumerable <Issue> issues = Common.GetInconsistencies( beatmapSet, beatmap => beatmap.GetAudioFilePath() != null ? PathStatic.RelativePath(beatmap.GetAudioFilePath(), beatmap.songPath) : "None", GetTemplate("Multiple")); foreach (var issue in issues) { yield return(issue); } } }
/// <summary> Returns the last path matching all parts of the given path, apart from its extension. /// If directory paths are not given, this also looks up all paths in the song folder. </summary> private string GetLastMatchingPath(string relativePath, IEnumerable <string> directoryPaths = null) { string parsedPath = PathStatic.ParsePath(relativePath); string strippedPath = PathStatic.ParsePath(relativePath, withoutExtension: true); if (directoryPaths == null) { directoryPaths = Directory.EnumerateFiles(songPath, "*", SearchOption.AllDirectories); } // When the path is "go", and "go.png" is over "go.jpg" in order, then "go.jpg" will be the one used. // So we basically want to find the last path which matches the name. string lastMatchingPath = null; foreach (string path in directoryPaths) { string relPath = PathStatic.RelativePath(path, songPath).Replace("\\", "/"); if (relPath.StartsWith(strippedPath + ".", StringComparison.OrdinalIgnoreCase)) { lastMatchingPath = path.Substring(songPath.Length + 1); break; } } // In case the given file doesn't exist, we assume there's no duplicate file names. return(lastMatchingPath ?? parsedPath); }
public override IEnumerable <Issue> GetIssues(BeatmapSet aBeatmapSet) { foreach (string filePath in aBeatmapSet.songFilePaths) { Issue errorIssue = null; FileInfo file = null; try { file = new FileInfo(filePath); } catch (Exception exception) { errorIssue = new Issue(GetTemplate("Exception"), null, PathStatic.RelativePath(filePath, aBeatmapSet.songPath), exception); } if (errorIssue != null) { yield return(errorIssue); continue; } if (file.Length == 0) { yield return(new Issue(GetTemplate("0-byte"), null, PathStatic.RelativePath(filePath, aBeatmapSet.songPath))); } } }
public IEnumerable <Issue> GetIssue(BeatmapSet aBeatmapSet, string audioPath, bool isHitSound = false) { string audioRelPath = PathStatic.RelativePath(audioPath, aBeatmapSet.songPath); AudioFile file = new AudioFile(audioPath); // gets the bitrate in bps, so turn it into kbps double bitrate = file.GetAverageBitrate() / 1000; double minBitrate = file.GetLowestBitrate() / 1000; double maxBitrate = file.GetHighestBitrate() / 1000; if (minBitrate == maxBitrate) { if (minBitrate < 128 || (maxBitrate > 192 && !isHitSound)) { if (!isHitSound) { yield return(new Issue(GetTemplate("CBR"), null, audioRelPath, $"{bitrate:0.##}", (bitrate < 128 ? "low" : "high"))); } else { yield return(new Issue(GetTemplate("CBR Hit Sound"), null, audioRelPath, $"{bitrate:0.##}")); } } } else { if (bitrate < 128 || (bitrate > 192 && !isHitSound)) { if (Math.Round(bitrate) < 128 || (Math.Round(bitrate) > 192 && !isHitSound)) { if (!isHitSound) { yield return(new Issue(GetTemplate("VBR"), null, audioRelPath, $"{bitrate:0.##}", $"{minBitrate:0.##}", $"{maxBitrate:0.##}", (bitrate < 128 ? "low" : "high"))); } else { yield return(new Issue(GetTemplate("VBR Hit Sound"), null, audioRelPath, $"{bitrate:0.##}", $"{minBitrate:0.##}", $"{maxBitrate:0.##}")); } } else if (!isHitSound) { yield return(new Issue(GetTemplate("Exact VBR"), null, audioRelPath, $"{bitrate:0.##}", $"{minBitrate:0.##}", $"{maxBitrate:0.##}", (bitrate < 128 ? "low" : "high"))); } } } }
public override IEnumerable <Issue> GetIssues(BeatmapSet aBeatmapSet) { foreach (Issue issue in Common.GetInconsistencies( aBeatmapSet, aBeatmap => PathStatic.RelativePath(aBeatmap.GetAudioFilePath(), aBeatmap.songPath), GetTemplate("Multiple"))) { yield return(issue); } }
/// <summary> Returns the last file path matching the given search pattern, relative to the song folder. /// The search pattern allows two wildcards: * = 0 or more, ? = 0 or 1. </summary> private string GetLastMatchingFilePath(string searchPattern) { var lastMatchingPath = Directory.EnumerateFiles(songPath, searchPattern, SearchOption.AllDirectories).LastOrDefault(); if (lastMatchingPath == null) { return(null); } return(PathStatic.RelativePath(lastMatchingPath, songPath).Replace("\\", "/")); }
public override IEnumerable <Issue> GetIssues(BeatmapSet beatmapSet) { if (beatmapSet.GetAudioFilePath() != null) { foreach (var issue in GetIssue(beatmapSet, beatmapSet.GetAudioFilePath())) { yield return(issue); } } foreach (string hitSoundFile in beatmapSet.hitSoundFiles) { string hitSoundPath = Path.Combine(beatmapSet.songPath, hitSoundFile); ManagedBass.ChannelType hitSoundFormat = 0; Issue errorIssue = null; try { hitSoundFormat = AudioBASS.GetFormat(hitSoundPath); } catch (Exception exception) { errorIssue = new Issue(GetTemplate("Exception"), null, PathStatic.RelativePath(hitSoundPath, beatmapSet.songPath), Common.ExceptionTag(exception)); } if (errorIssue != null) { yield return(errorIssue); continue; } if ((hitSoundFormat & ManagedBass.ChannelType.OGG) != 0 && (hitSoundFormat & ManagedBass.ChannelType.MP3) != 0) { continue; } foreach (var issue in GetIssue(beatmapSet, hitSoundPath, true)) { yield return(issue); } } }
public override IEnumerable <Issue> GetIssues(BeatmapSet aBeatmapSet) { foreach (Beatmap beatmap in aBeatmapSet.beatmaps) { bool hasVideo = beatmap.videos.Count > 0; bool hasStoryboard = beatmap.HasDifficultySpecificStoryboard() || (aBeatmapSet.osb?.IsUsed() ?? false); string audioPath = beatmap.GetAudioFilePath(); if (audioPath != null) { double duration = 0; Exception exception = null; try { duration = Audio.GetDuration(audioPath); } catch (Exception ex) { exception = ex; } if (exception == null) { double lastEndTime = beatmap.hitObjects.LastOrDefault()?.GetEndTime() ?? 0; double unusedPercentage = 1 - lastEndTime / duration; if (unusedPercentage >= 0.2) { string roundedPercentage = $"{unusedPercentage * 100:0.##}"; string templateKey = (hasStoryboard || hasVideo ? "With" : "Without") + " Video/Storyboard"; yield return(new Issue(GetTemplate(templateKey), beatmap, roundedPercentage)); } } else { yield return(new Issue(GetTemplate("Unable to check"), null, PathStatic.RelativePath(audioPath, beatmap.songPath), exception)); } } } }
/// <summary> Returns whether the given full file path is used by the beatmapset. </summary> public bool IsFileUsed(string filePath) { string relativePath = PathStatic.RelativePath(filePath, songPath); string fileName = relativePath.Split(new char[] { '/', '\\' }).Last().ToLower(); string parsedPath = PathStatic.ParsePath(relativePath); string strippedPath = PathStatic.ParsePath(relativePath, withoutExtension: true); if (beatmaps.Any(beatmap => beatmap.generalSettings.audioFileName.ToLower() == parsedPath)) { return(true); } // When the path is "go", and "go.png" is over "go.jpg" in order, then "go.jpg" will be the one used. // So we basically want to find the last path which matches the name. string lastMatchingPath = PathStatic.ParsePath(GetLastMatchingPath(parsedPath)); // These are always used, but you won't be able to update them unless they have the right format. if (fileName.EndsWith(".osu")) { return(true); } if (beatmaps.Any(beatmap => beatmap.sprites.Any(element => element.path.ToLower() == parsedPath) || beatmap.videos.Any(element => element.path.ToLower() == parsedPath) || beatmap.backgrounds.Any(element => element.path.ToLower() == parsedPath) || beatmap.animations.Any(element => element.path.ToLower() == parsedPath) || beatmap.samples.Any(element => element.path.ToLower() == parsedPath))) { return(true); } // animations cannot be stripped of their extension if (beatmaps.Any(beatmap => beatmap.sprites.Any(element => element.strippedPath == strippedPath) || beatmap.videos.Any(element => element.strippedPath == strippedPath) || beatmap.backgrounds.Any(element => element.strippedPath == strippedPath) || beatmap.samples.Any(element => element.strippedPath == strippedPath)) && parsedPath == lastMatchingPath) { return(true); } if (osb != null && ( osb.sprites.Any(element => element.path.ToLower() == parsedPath) || osb.videos.Any(element => element.path.ToLower() == parsedPath) || osb.backgrounds.Any(element => element.path.ToLower() == parsedPath) || osb.animations.Any(element => element.path.ToLower() == parsedPath) || osb.samples.Any(anElement => anElement.path.ToLower() == parsedPath))) { return(true); } if (osb != null && ( osb.sprites.Any(element => element.strippedPath == strippedPath) || osb.videos.Any(element => element.strippedPath == strippedPath) || osb.backgrounds.Any(element => element.strippedPath == strippedPath) || osb.samples.Any(element => element.strippedPath == strippedPath)) && parsedPath == lastMatchingPath) { return(true); } if (beatmaps.Any(beatmap => beatmap.hitObjects.Any(hitObject => (hitObject.filename != null ? PathStatic.ParsePath(hitObject.filename, true) : null) == strippedPath))) { return(true); } if (hitSoundFiles.Any(hsPath => PathStatic.ParsePath(hsPath) == parsedPath)) { return(true); } if (SkinStatic.IsUsed(fileName, this)) { return(true); } if (fileName == GetOsbFileName().ToLower() && osb.IsUsed()) { return(true); } foreach (Beatmap beatmap in beatmaps) { if (IsAnimationPathUsed(parsedPath, beatmap.animations)) { return(true); } } if (osb != null && IsAnimationPathUsed(parsedPath, osb.animations)) { return(true); } return(false); }
public override IEnumerable <Issue> GetIssues(BeatmapSet beatmapSet) { Dictionary <string, List <AudioUsage> > audioUsage = new Dictionary <string, List <AudioUsage> >(); foreach (Beatmap beatmap in beatmapSet.beatmaps) { bool hasVideo = beatmap.videos.Count > 0; bool hasStoryboard = beatmap.HasDifficultySpecificStoryboard() || (beatmapSet.osb?.IsUsed() ?? false); string audioPath = beatmap.GetAudioFilePath(); if (audioPath == null) { continue; } double duration = 0; Exception exception = null; try { duration = AudioBASS.GetDuration(audioPath); } catch (Exception ex) { exception = ex; } if (exception != null) { yield return(new Issue(GetTemplate("Unable to check"), null, PathStatic.RelativePath(audioPath, beatmap.songPath), Common.ExceptionTag(exception))); continue; } double lastEndTime = beatmap.hitObjects.LastOrDefault()?.GetEndTime() ?? 0; double fraction = lastEndTime / duration; if (!audioUsage.ContainsKey(audioPath)) { audioUsage[audioPath] = new List <AudioUsage>(); } audioUsage[audioPath].Add(new AudioUsage(fraction, hasVideo || hasStoryboard)); } foreach (string audioPath in audioUsage.Keys) { double maxFraction = 0; bool anyHasVideoOrSB = false; foreach (AudioUsage usage in audioUsage[audioPath]) { if (usage.fraction > maxFraction) { maxFraction = usage.fraction; } if (usage.hasVideoOrSB) { anyHasVideoOrSB = true; } } if (maxFraction > 0.8d) { continue; } string templateKey = (anyHasVideoOrSB ? "With" : "Without") + " Video/Storyboard"; yield return(new Issue(GetTemplate(templateKey), null, $"{(1 - maxFraction) * 100:0.##}")); } }
/// <summary> Returns whether the given full file path is used by the beatmapset. </summary> public bool IsFileUsed(string aFilePath) { string relativePath = PathStatic.RelativePath(aFilePath, songPath); string fileName = relativePath.Split(new char[] { '/', '\\' }).Last().ToLower(); string parsedPath = PathStatic.ParsePath(relativePath); string strippedPath = PathStatic.ParsePath(relativePath, true); if (beatmaps.Any(aBeatmap => aBeatmap.generalSettings.audioFileName.ToLower() == parsedPath)) { return(true); } // When the path is "go", and "go.png" is over "go.jpg" in order, then "go.jpg" will be the one used. // So we basically want to find the last path which matches the name. string lastStripped = null; foreach (string file in Directory.EnumerateFiles(songPath, "*", SearchOption.AllDirectories)) { string relPath = PathStatic.RelativePath(file, songPath).Replace("\\", "/"); if (relPath.StartsWith(strippedPath + ".", StringComparison.OrdinalIgnoreCase)) { lastStripped = file.Substring(songPath.Length + 1); break; } } if (lastStripped == null) { return(false); } // these are always used, but you won't be able to update them unless they have the right format if (fileName.EndsWith(".osu")) { return(true); } if (beatmaps.Any(aBeatmap => aBeatmap.sprites.Any(anElement => anElement.path.ToLower() == parsedPath) || aBeatmap.videos.Any(anElement => anElement.path.ToLower() == parsedPath) || aBeatmap.backgrounds.Any(anElement => anElement.path.ToLower() == parsedPath) || aBeatmap.animations.Any(anElement => anElement.path.ToLower() == parsedPath) || aBeatmap.storyHitSounds.Any(anElement => anElement.path.ToLower() == parsedPath))) { return(true); } // animations cannot be stripped of their extension if (beatmaps.Any(aBeatmap => aBeatmap.sprites.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path)) || aBeatmap.videos.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path)) || aBeatmap.backgrounds.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path)) || aBeatmap.storyHitSounds.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path))) && parsedPath == lastStripped) { return(true); } if (osb != null && ( osb.sprites.Any(anElement => anElement.path.ToLower() == parsedPath) || osb.videos.Any(anElement => anElement.path.ToLower() == parsedPath) || osb.backgrounds.Any(anElement => anElement.path.ToLower() == parsedPath) || osb.animations.Any(anElement => anElement.path.ToLower() == parsedPath) || osb.storyHitSounds.Any(anElement => anElement.path.ToLower() == parsedPath))) { return(true); } if (osb != null && ( osb.sprites.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path)) || osb.videos.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path)) || osb.backgrounds.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path)) || osb.storyHitSounds.Any(anElement => anElement.strippedPath == strippedPath && lastStripped.StartsWith(anElement.path))) && parsedPath == lastStripped) { return(true); } if (beatmaps.Any(aBeatmap => aBeatmap.hitObjects.Any(anObject => (anObject.filename != null ? PathStatic.ParsePath(anObject.filename, true) : null) == strippedPath))) { return(true); } if (hitSoundFiles.Any(aHitSoundPath => PathStatic.ParsePath(aHitSoundPath) == parsedPath)) { return(true); } if (SkinStatic.IsUsed(fileName, this)) { return(true); } if (fileName == GetOsbFileName().ToLower() && osb.IsUsed()) { return(true); } foreach (Beatmap beatmap in beatmaps) { if (IsAnimationUsed(parsedPath, beatmap.animations)) { return(true); } } if (osb != null && IsAnimationUsed(parsedPath, osb.animations)) { return(true); } return(false); }
private static string RenderResources(BeatmapSet aBeatmapSet) { string RenderFloat(List <string> aFiles, Func <string, string> aFunc) { string content = String.Join("<br>", aFiles.Select(aFile => { string path = aBeatmapSet.hitSoundFiles.FirstOrDefault(anOtherFile => anOtherFile.StartsWith(aFile + ".")); if (path == null) { return(null); } return(aFunc(path)); }).Where(aValue => aValue != null) ); if (content.Length == 0) { return(""); } return(Div("overview-float", content)); } Dictionary <string, int> hsUsedCount = new Dictionary <string, int>(); return (RenderContainer("Resources", RenderBeatmapContent(aBeatmapSet, "Used Hit Sound File(s)", aBeatmap => { List <string> usedHitSoundFiles = aBeatmap.hitObjects.SelectMany(anObject => anObject.GetUsedHitSoundFileNames()).ToList(); List <string> distinctSortedFiles = usedHitSoundFiles.Distinct().OrderByDescending(aFile => aFile).ToList(); return RenderFloat(distinctSortedFiles, aPath => Encode(aPath)) + RenderFloat(distinctSortedFiles, aPath => { int count = usedHitSoundFiles.Where(anOtherFile => aPath.StartsWith(anOtherFile + ".")).Count(); // Used for total hit sound usage overview if (hsUsedCount.ContainsKey(aPath)) { hsUsedCount[aPath] += count; } else { hsUsedCount[aPath] = count; } return $"× {count}"; }); }, false), RenderField("Total Used Hit Sound File(s)", (hsUsedCount.Any() ? Div("overview-float", String.Join("<br>", hsUsedCount.Select(aPair => aPair.Key) ) ) + Div("overview-float", String.Join("<br>", hsUsedCount.Select(aPair => Try(() => { string fullPath = Path.Combine(aBeatmapSet.songPath, aPair.Key); return Encode(RenderFileSize(fullPath)); }, noteIfError: "Could not get hit sound file size" ) ) ) ) + Div("overview-float", String.Join("<br>", hsUsedCount.Select(aPair => Try(() => { string fullPath = Path.Combine(aBeatmapSet.songPath, aPair.Key); double duration = AudioBASS.GetDuration(fullPath); if (duration < 0) { return "0 ms"; } return $"{duration:0.##} ms"; }, noteIfError: "Could not get hit sound duration" ) ) ) ) + Div("overview-float", String.Join("<br>", hsUsedCount.Select(aPair => Try(() => { string fullPath = Path.Combine(aBeatmapSet.songPath, aPair.Key); return Encode(AudioBASS.EnumToString(AudioBASS.GetFormat(fullPath))); }, noteIfError: "Could not get hit sound file path" ) ) ) ) + Div("overview-float", String.Join("<br>", hsUsedCount.Select(aPair => "× " + aPair.Value) ) ) : "") ), RenderBeatmapContent(aBeatmapSet, "Background File(s)", aBeatmap => { if (aBeatmap.backgrounds.Any()) { string fullPath = Path.Combine(aBeatmap.songPath, aBeatmap.backgrounds.First().path); if (!File.Exists(fullPath)) { return ""; } string error = null; TagLib.File tagFile = null; try { tagFile = new FileAbstraction(fullPath).GetTagFile(); } catch (Exception exception) { error = exception.Message; } return Div("overview-float", Try(() => Encode(aBeatmap.backgrounds.First().path), noteIfError: "Could not get background file path" ) ) + Div("overview-float", Try(() => Encode(RenderFileSize(fullPath)), noteIfError: "Could not get background file size" ) ) + ((error != null || tagFile == null) ? Div("overview-float", Try(() => Encode(tagFile.Properties.PhotoWidth + " x " + tagFile.Properties.PhotoHeight), noteIfError: "Could not get background resolution" ) ) : Div("overview-float", Encode($"(failed getting proprties; {error})") )); } else { return ""; } }, false), RenderBeatmapContent(aBeatmapSet, "Video File(s)", aBeatmap => { if (aBeatmap.videos.Any() || (aBeatmapSet.osb?.videos.Any() ?? false)) { string fullPath = Path.Combine(aBeatmap.songPath, aBeatmap.videos.First().path); if (!File.Exists(fullPath)) { return ""; } string error = null; TagLib.File tagFile = null; try { tagFile = new FileAbstraction(fullPath).GetTagFile(); } catch (Exception exception) { error = exception.Message; } return Div("overview-float", Try(() => Encode(aBeatmap.videos.First().path), noteIfError: "Could not get video file path" ) ) + Div("overview-float", Try(() => Encode(RenderFileSize(fullPath)), noteIfError: "Could not get video file size" ) ) + ((error != null || tagFile == null) ? Div("overview-float", Try(() => FormatTimestamps(Encode(Timestamp.Get(tagFile.Properties.Duration.TotalMilliseconds))), noteIfError: "Could not get video duration" ) ) + Div("overview-float", Try(() => Encode(tagFile.Properties.VideoWidth + " x " + tagFile.Properties.VideoHeight), noteIfError: "Could not get video resolution" ) ) : Div("overview-float", Encode($"(failed getting proprties; {error})") )); } else { return ""; } }, false), RenderBeatmapContent(aBeatmapSet, "Audio File(s)", aBeatmap => { string path = aBeatmap.GetAudioFilePath(); if (path == null) { return ""; } return Div("overview-float", Try(() => Encode(PathStatic.RelativePath(path, aBeatmap.songPath)), noteIfError: "Could not get audio file path" ) ) + Div("overview-float", Try(() => Encode(RenderFileSize(path)), noteIfError: "Could not get audio file size" ) ) + Div("overview-float", Try(() => FormatTimestamps(Encode(Timestamp.Get(AudioBASS.GetDuration(path)))), noteIfError: "Could not get audio duration" ) ) + Div("overview-float", Try(() => Encode(AudioBASS.EnumToString(AudioBASS.GetFormat(path))), noteIfError: "Could not get audio format" ) ); }, false), RenderBeatmapContent(aBeatmapSet, "Audio Bitrate", aBeatmap => { string path = aBeatmap.GetAudioFilePath(); if (path == null) { return "N/A"; } return Div("overview-float", $"average {Math.Round(AudioBASS.GetBitrate(path))} kbps" ); }, false), RenderField("Has .osb", Encode((aBeatmapSet.osb?.IsUsed() ?? false).ToString()) ), RenderBeatmapContent(aBeatmapSet, "Has .osu Specific Storyboard", aBeatmap => aBeatmap.HasDifficultySpecificStoryboard().ToString()), RenderBeatmapContent(aBeatmapSet, "Song Folder Size", aBeatmap => RenderDirectorySize(aBeatmap.songPath)) )); }