From a4a1919842769cb68c5c1b4dc503c77918f12859 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Tue, 13 Jul 2021 03:46:45 +0200 Subject: [PATCH] Add too short audio files check and tests --- .../Checks/CheckTooShortAudioFilesTest.cs | 117 ++++++++++++++++++ osu.Game.Tests/Resources/Samples/blank.wav | Bin 0 -> 44 bytes osu.Game.Tests/Resources/Samples/corrupt.wav | Bin 0 -> 32334 bytes .../Resources/Samples/test-sample-cut.mp3 | Bin 0 -> 2060 bytes osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Edit/Checks/CheckTooShortAudioFiles.cs | 83 +++++++++++++ 6 files changed, 201 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs create mode 100644 osu.Game.Tests/Resources/Samples/blank.wav create mode 100644 osu.Game.Tests/Resources/Samples/corrupt.wav create mode 100644 osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs new file mode 100644 index 0000000000..e978af5e49 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -0,0 +1,117 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckTooShortAudioFilesTest + { + private CheckTooShortAudioFiles check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckTooShortAudioFiles(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.wav", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestDifferentExtension() + { + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo + { + Filename = "abc123.jpg", + FileInfo = new FileInfo { Hash = "abcdef" } + }); + + // Should fail to load, but not produce an error due to the extension not being expected to load. + Assert.IsEmpty(check.Run(getContext(null, allowMissing: true))); + } + + [Test] + public void TestRegularAudioFile() + { + Assert.IsEmpty(check.Run(getContext("Samples/test-sample.mp3"))); + } + + [Test] + public void TestBlankAudioFile() + { + // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine. + Assert.IsEmpty(check.Run(getContext("Samples/blank.wav"))); + } + + [Test] + public void TestTooShortAudioFile() + { + var issues = check.Run(getContext("Samples/test-sample-cut.mp3")).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort); + } + + [Test] + public void TestMissingAudioFile() + { + Assert.IsEmpty(check.Run(getContext("Samples/missing.mp3", allowMissing: true))); + } + + [Test] + public void TestCorruptAudioFile() + { + var issues = check.Run(getContext("Samples/corrupt.wav")).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat); + } + + private BeatmapVerifierContext getContext(string resourceName, bool allowMissing = false) + { + Stream resourceStream = string.IsNullOrEmpty(resourceName) ? null : TestResources.OpenResource(resourceName); + if (!allowMissing && resourceStream == null) + throw new FileNotFoundException($"The requested test resource \"{resourceName}\" does not exist."); + + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav new file mode 100644 index 0000000000000000000000000000000000000000..878bf23ceab94d07389dc32df995862f4da2b0c9 GIT binary patch literal 44 xcmWIYbaPW-U|whgwKSuaH+pNS;oxlJ@|;@M-h>rC3y1?4 zXlUqY_b|}W(J?VGu&@apVBf!wO@jXjmw8{Q4nCn3$N@_pyl|JRs(HLjHu~|Md&r3gBTNWh1{pL81YW@sLpP zkl-Bv6~e%KNPoS6f4q>8QBd!pp<`fT-A7zd{SZJ#LP0@BMY(qm6%}!{58{0Q74IJY z6LvAQM@mNMG)@E@{?T7BXkV3oCsZCeq~kPp4#32EOhimVO3%Rf^cfQuHxDl#zks-e zq?ELb>}wTOHFXV5Eo~E1Gjj_|D{Ggxu5Rugo?h=i1O^3%e1ycr#>FQjeo9Kt%*xKm z&C4$+tf;K2uBol7Z)j`p_|e(b-P1cdHa;;qH9Z4eURhmR-`M=QwS9Daa(Z@tad~z9 z%Pu4U*A9mp(>_SFGML|XXWfu~%J7S^Wq27DKj)pI$gl^>Yh=#)-gWy&4 zm-6qJw4BO^gvQPzSdZzrmg$dvnf8}u|2xA1{;w?iFT?)ZE+~MFf`oW@D0qMfaB=-K z(+}(amn+*G8u(QQg^xlWU?GgowISm5`jDVe!)(Ln&x)MAV2<|@5b5Qq5nGL(LR}hh zS}DVU>dHE}lx;ZxnQk+*wBF)-YzE@3tXq}Dv$)Rk4dQ@rUkO=DR^wH}&n_SE~B zZ4X-9QT^v8H!2G~h&mo0iWS&2yeV)Hn3GUZ(VU@E6gBK#I%RC}DkE6LSx$atGI8p7 zxG0Ek>&5^GP85cseZeLW?Hq?eHkM#2@l3CFvs+|x>D7aBu;r!?s zBqAS1D+_q1|4!rH!Ka#!!bOD*VsszE+eJ8JLVBGs)(ltQt5#)Df(mUClBv#}8M0aJ zGskK1V_33+iJiKhTy#ZbcwDPqbHf3@Q}p_ncKtEbTETZ)c@rOw`IG|ht-0|5sn|>A z8FmGiSY!H4lS`9a660k`v~{Tlxq7NbQoM$4KlxjSUIRr-DSK{Ug3HoAy=KUYj;x&< zeM3Bi`pALF?<7K+F*fXIw9#wh%fu~N?za2XR`Hgi-u6Ax^@TgJsLWlJfN5eBa-`(y zKfN%2PvE2RG;@vMQXbrQB*2kn{8|kDH^;7(GzPbsEjwJ3aDi`eLg4@; zzx18jGIOI=N$wG4lyMTaJ&|I#tnv#-aZ?{jz;iLsmyRdri2FnGxXM;?)fg12#T+*4 zsq?Ouwl*pFR;IM}&9^D(A0^@n<7LL)v^k+EHkJe*!r$ydbcA&SdxZI0w_@oMMR4gy zPVeZeeTcQJh6oa(OS21&En%x@xQ(>q>7Iiw>YepV(Iqt%+c)w?C(0|D1Z@%_?pbp=5G5#u(d%zHZXa%I zbFpvpxIo3R73V5DH`ee|Qp~01D5h1$APkq0`9GGw`@VgwrF>PwN=M@sz=c`&h8-{J z%MxysVY-)#@3}#+_Iqm<*CNYNo=h4diEZoqR|ebN?YB8D-*rhpv>z_+;%PsykynE( zm&TY)LI&xLg1}s|Ep(2)_LeAGLLevi!>cCQJnh-}x%=vAcFT3F5gLN=2KFxBDLB(H zii3^}kJOWjd-AM9v$h^P%~sj~XeRX%F*NVnqQbI-mTDE4b>ybrJ<8mQ+Jd?@W`4KA zctT5blxi9yOaGBBMwT%~-b6UD?c%V{!{tU?N{@cy1Ukd5=$|22Arr;ES>x1tN`O>w zc|ta&8gStz%mz}C;SuHt$Nic7I5}8=HLuqE-saRHU94Y@+@!VX!RZkpJ`OD zy1eQexrgW)&R$5JPTI#FMeRq@&DBK@nw@L07Q08C<=j+_p1ot*XeJEB4Q `L5Zz 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..003fe23dca224ef686afad1119e153b6315ccd3a GIT binary patch literal 2060 zcmeZtF=l1}0q2m2hzKBU3B#(IVZ1}r;a&NEd|VbSzuV0-{_hg$E}$uNmwOaK3U0T@Ay9So6g85lUsA8^$! zU|>{uIy-ge90taZhxO`>E_kRcd92GQ?X{z3fx@HrJh~2l79MKsn8*J&;m<#x-9POA z$^ZZK?3B#Yga6d)|Le)?{gb#BX*WSZDRq~G9{+s3+6O;>C~Wvy{O8Gznw|fjGO{Q9 zsMp|Nv!B9ZRM8Y8-+7REK8vhy^ZWmQ?6d!SrUYM^6=Ypnv}{fj>jW`RLFWgL&wcYr z;JwhzFDO#hQMYW>(;ewc*EBKDaddsx!pbhjyywQFH7YB%x?Q|>vysig#C5H3myTC| z?UouP72V8j({s%)P5bVj!pp7qYx>by+VK@~%a54~t=7u9;iUO6Q|!_WYxYXXG=a~X z-s`PUlW2(cVtXiAbZ5rvhytV92OeitJ8QaveYK8T-pXHgZc=oX2z}=9E}Snc}WEU&bX7zu_8sY_WysLeVjbMb}ZxVU^!IS$l%$qAh4+Z|KD2ES?*b{n-Vn+B<{E) z;5fsAc?J_J7sLMl|KF`MPnqCeHLlbL)v_U{J1H)tnXMdop3sNWFo{=4I^zT~x z?Cxfbn(XvqkYB!PZTH*;{L%BbCRt4>uZeq4+e&+8P zUzZ$V3{Yuw@cIAyK*~%8&d92p|Ed=;GQ5wDKYPUin5UM=Ffb@EFqkwqFt9W@_?b!Z zxGFK~)jKdJ9k>6w(ezfbf8YO|KB)_pl-~bxtZNj|XK7F><8fm1acP_~aY2&ebyxL9 zp^7>M8CC{?@0W}M#kg5a4v1*TA267~{8&yq>8F)d+XRV-J~lqFhJpe?#s(1qUWff% zE}JYo3XK#sIW!s+m=qe;9)84sg}ueT&r{ICXO`G;jkL~(o$SR}$dCp%^TD=6;# z^r_{J<>D!hCj@789sJex;vi!IL%gCXcP*bw!vAlEdehidR^<6g^e=i@vFGR`9baK3 zLBT_hc(@FgC?qHBa_$N4D6Ieg>&uGOYv=#}X{B>1A#RG-;whG{p8{Pzyo8sst46_) G3jqM{$gt`F literal 0 HcmV?d00001 diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 545fee6264..768ab3545f 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Edit new CheckAudioQuality(), new CheckMutedObjects(), new CheckFewHitsounds(), + new CheckTooShortAudioFiles(), // Files new CheckZeroByteFiles(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs new file mode 100644 index 0000000000..9a2c714c06 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckTooShortAudioFiles : ICheck + { + private const int ms_threshold = 25; + private const int min_bytes_threshold = 100; + + private readonly string[] audioExtensions = { "mp3", "ogg", "wav" }; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this), + new IssueTemplateBadFormat(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + foreach (var file in beatmapSet.Files) + { + Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath); + if (data == null) + continue; + + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); + + if (decodeStream == 0) + { + // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. + // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. + if (hasAudioExtension(file.Filename) && probablyHasAudioData(data)) + yield return new IssueTemplateBadFormat(this).Create(file.Filename); + + continue; + } + + long length = Bass.ChannelGetLength(decodeStream); + double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000; + + // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users. + if (ms > 0 && ms < ms_threshold) + yield return new IssueTemplateTooShort(this).Create(file.Filename, ms); + } + } + + private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith); + private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold; + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is too short ({1:0f} ms), should be at least {2:0f} ms.") + { + } + + public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold); + } + + public class IssueTemplateBadFormat : IssueTemplate + { + public IssueTemplateBadFormat(ICheck check) + : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").") + { + } + + public Issue Create(string filename) => new Issue(this, filename, Bass.LastError); + } + } +}