Skip to content

Commit f8004a6

Browse files
authored
Merge pull request #34182 from Hiviexd/verify/update-audio-quality-check
Support `.ogg`-specific bitrate limit in audio quality verify check
2 parents e92439c + 8e0ed85 commit f8004a6

File tree

4 files changed

+117
-33
lines changed

4 files changed

+117
-33
lines changed

osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System.Linq;
5+
using ManagedBass;
56
using Moq;
67
using NUnit.Framework;
78
using osu.Framework.Audio.Track;
@@ -10,7 +11,9 @@
1011
using osu.Game.Rulesets.Edit;
1112
using osu.Game.Rulesets.Edit.Checks;
1213
using osu.Game.Rulesets.Objects;
14+
using osu.Game.Tests.Resources;
1315
using osu.Game.Tests.Visual;
16+
using osuTK.Audio;
1417

1518
namespace osu.Game.Tests.Editing.Checks
1619
{
@@ -28,9 +31,13 @@ public void Setup()
2831
{
2932
BeatmapInfo = new BeatmapInfo
3033
{
31-
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
34+
Metadata = new BeatmapMetadata()
3235
}
3336
};
37+
38+
// 0 = No output device. This still allows decoding.
39+
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
40+
throw new AudioException("Could not initialize Bass.");
3441
}
3542

3643
[Test]
@@ -54,6 +61,14 @@ public void TestAcceptable()
5461
Assert.That(check.Run(context), Is.Empty);
5562
}
5663

64+
[Test]
65+
public void TestAcceptableOgg()
66+
{
67+
var context = getContext(208, useOgg: true);
68+
69+
Assert.That(check.Run(context), Is.Empty);
70+
}
71+
5772
[Test]
5873
public void TestNullBitrate()
5974
{
@@ -87,6 +102,17 @@ public void TestTooHighBitrate()
87102
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
88103
}
89104

105+
[Test]
106+
public void TestTooHighBitrateOgg()
107+
{
108+
var context = getContext(250, useOgg: true);
109+
110+
var issues = check.Run(context).ToList();
111+
112+
Assert.That(issues, Has.Count.EqualTo(1));
113+
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
114+
}
115+
90116
[Test]
91117
public void TestTooLowBitrate()
92118
{
@@ -98,24 +124,41 @@ public void TestTooLowBitrate()
98124
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
99125
}
100126

101-
private BeatmapVerifierContext getContext(int? audioBitrate)
127+
private BeatmapVerifierContext getContext(int? audioBitrate, bool useOgg = false)
102128
{
103-
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate).Object);
129+
// Update the audio filename and beatmapset files based on the format being tested
130+
string audioFileName = useOgg ? "abc123.ogg" : "abc123.mp3";
131+
string fileExtension = useOgg ? "ogg" : "mp3";
132+
133+
beatmap.Metadata.AudioFile = audioFileName;
134+
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo
135+
{
136+
Files = { CheckTestHelpers.CreateMockFile(fileExtension) }
137+
};
138+
139+
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate, useOgg).Object);
104140
}
105141

106142
/// <summary>
107143
/// Returns the mock of the working beatmap with the given audio properties.
108144
/// </summary>
109145
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
110-
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate)
146+
/// <param name="useOgg">Whether to use an OGG sample instead of MP3.</param>
147+
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate, bool useOgg = false)
111148
{
112149
var mockTrack = new Mock<OsuTestScene.ClockBackedTestWorkingBeatmap.TrackVirtualManual>(new FramedClock(), "virtual");
113150
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
114151

152+
// Use real audio samples for format detection
153+
string samplePath = useOgg ? "Samples/test-sample.ogg" : "Samples/test-sample-cut.mp3";
154+
115155
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
116156
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
117157
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
118158

159+
// Return a fresh stream each time GetStream is called to avoid disposed stream issues
160+
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(() => TestResources.OpenResource(samplePath));
161+
119162
return mockWorkingBeatmap;
120163
}
121164
}

osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System.Collections.Generic;
5+
using ManagedBass;
56
using osu.Game.Rulesets.Edit.Checks.Components;
67

78
namespace osu.Game.Rulesets.Edit.Checks
89
{
910
public class CheckAudioQuality : ICheck
1011
{
1112
// This is a requirement as stated in the Ranking Criteria.
12-
// See https://osu.ppy.sh/wiki/en/Ranking_Criteria#rules.4
13-
private const int max_bitrate = 192;
13+
// See https://osu.ppy.sh/wiki/en/Ranking_criteria#audio
14+
private const int max_bitrate_default = 192;
15+
private const int max_bitrate_ogg = 208;
1416

1517
// "A song's audio file /.../ must be of reasonable quality. Try to find the highest quality source file available"
1618
// There not existing a version with a bitrate of 128 kbps or higher is extremely rare.
@@ -35,10 +37,17 @@ public IEnumerable<Issue> Run(BeatmapVerifierContext context)
3537

3638
if (track?.Bitrate == null || track.Bitrate.Value == 0)
3739
yield return new IssueTemplateNoBitrate(this).Create();
38-
else if (track.Bitrate.Value > max_bitrate)
39-
yield return new IssueTemplateTooHighBitrate(this).Create(track.Bitrate.Value);
40-
else if (track.Bitrate.Value < min_bitrate)
41-
yield return new IssueTemplateTooLowBitrate(this).Create(track.Bitrate.Value);
40+
else
41+
{
42+
// Determine max bitrate based on audio format
43+
var audioFormat = AudioCheckUtils.GetAudioFormatFromFile(context, audioFile);
44+
int upperBitrateLimit = audioFormat.HasFlag(ChannelType.OGG) ? max_bitrate_ogg : max_bitrate_default;
45+
46+
if (track.Bitrate.Value > upperBitrateLimit)
47+
yield return new IssueTemplateTooHighBitrate(this).Create(track.Bitrate.Value, upperBitrateLimit);
48+
else if (track.Bitrate.Value < min_bitrate)
49+
yield return new IssueTemplateTooLowBitrate(this).Create(track.Bitrate.Value);
50+
}
4251
}
4352

4453
public class IssueTemplateTooHighBitrate : IssueTemplate
@@ -48,7 +57,7 @@ public IssueTemplateTooHighBitrate(ICheck check)
4857
{
4958
}
5059

51-
public Issue Create(int bitrate) => new Issue(this, bitrate, max_bitrate);
60+
public Issue Create(int bitrate, int maxBitrate) => new Issue(this, bitrate, maxBitrate);
5261
}
5362

5463
public class IssueTemplateTooLowBitrate : IssueTemplate

osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System.Collections.Generic;
5-
using System.IO;
65
using System.Linq;
76
using ManagedBass;
8-
using osu.Framework.Audio.Callbacks;
97
using osu.Game.Beatmaps;
10-
using osu.Game.Extensions;
118
using osu.Game.Rulesets.Edit.Checks.Components;
129

1310
namespace osu.Game.Rulesets.Edit.Checks
@@ -36,28 +33,18 @@ public IEnumerable<Issue> Run(BeatmapVerifierContext context)
3633
if (beatmapSet == null) yield break;
3734
if (audioFile == null) yield break;
3835

39-
using (Stream data = context.WorkingBeatmap.GetStream(audioFile.File.GetStoragePath()))
40-
{
41-
if (data == null || data.Length <= 0) yield break;
42-
43-
var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data));
44-
int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle);
45-
46-
// If the format is not supported by BASS
47-
if (decodeStream == 0)
48-
{
49-
yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename);
50-
51-
yield break;
52-
}
36+
var audioFormat = AudioCheckUtils.GetAudioFormatFromFile(context, context.Beatmap.Metadata.AudioFile);
5337

54-
var audioInfo = Bass.ChannelGetInfo(decodeStream);
55-
56-
if (!allowedFormats.Contains(audioInfo.ChannelType))
57-
yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename);
38+
// If the format is not supported by BASS
39+
if (audioFormat == 0)
40+
{
41+
yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename);
5842

59-
Bass.StreamFree(decodeStream);
43+
yield break;
6044
}
45+
46+
if (!allowedFormats.Contains(audioFormat))
47+
yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename);
6148
}
6249

6350
public class IssueTemplateFormatUnsupported : IssueTemplate

osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,57 @@
33

44
using System.IO;
55
using System.Linq;
6+
using ManagedBass;
7+
using osu.Framework.Audio.Callbacks;
8+
using osu.Game.Beatmaps;
9+
using osu.Game.Extensions;
610
using osu.Game.Utils;
711

812
namespace osu.Game.Rulesets.Edit.Checks.Components
913
{
1014
public static class AudioCheckUtils
1115
{
1216
public static bool HasAudioExtension(string filename) => SupportedExtensions.AUDIO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant());
17+
18+
/// <summary>
19+
/// Gets the audio format (ChannelType) from a stream using BASS.
20+
/// </summary>
21+
/// <param name="data">The audio file stream.</param>
22+
/// <returns>The ChannelType of the audio, or <see cref="ChannelType.Unknown"/> if detection fails.</returns>
23+
public static ChannelType GetAudioFormat(Stream data)
24+
{
25+
if (data.Length <= 0)
26+
return ChannelType.Unknown;
27+
28+
using (var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)))
29+
{
30+
int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle);
31+
if (decodeStream == 0)
32+
return ChannelType.Unknown;
33+
34+
var audioInfo = Bass.ChannelGetInfo(decodeStream);
35+
Bass.StreamFree(decodeStream);
36+
37+
return audioInfo.ChannelType;
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Gets the audio format for a specific file in a beatmapset.
43+
/// </summary>
44+
/// <param name="context">The beatmap verifier context.</param>
45+
/// <param name="filename">The filename to check.</param>
46+
/// <returns>The ChannelType of the audio file, or <see cref="ChannelType.Unknown"/> if detection fails.</returns>
47+
public static ChannelType GetAudioFormatFromFile(BeatmapVerifierContext context, string filename)
48+
{
49+
var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
50+
var audioFile = beatmapSet?.GetFile(filename);
51+
52+
if (beatmapSet == null || audioFile == null)
53+
return ChannelType.Unknown;
54+
55+
using (Stream data = context.WorkingBeatmap.GetStream(audioFile.File.GetStoragePath()))
56+
return GetAudioFormat(data);
57+
}
1358
}
1459
}

0 commit comments

Comments
 (0)