A game framework written with osu! in mind.
at master 384 lines 13 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Linq; 7using System.Threading; 8using osu.Framework.Allocation; 9using osu.Framework.Audio.Track; 10using osu.Framework.Graphics.Batches; 11using osu.Framework.Graphics.Colour; 12using osu.Framework.Graphics.OpenGL.Vertices; 13using osu.Framework.Graphics.Primitives; 14using osu.Framework.Graphics.Shaders; 15using osu.Framework.Graphics.Textures; 16using osuTK; 17using osu.Framework.Graphics.OpenGL; 18using osu.Framework.Layout; 19using osu.Framework.Utils; 20using osu.Framework.Threading; 21using osuTK.Graphics; 22using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; 23 24namespace osu.Framework.Graphics.Audio 25{ 26 /// <summary> 27 /// Visualises the waveform for an audio stream. 28 /// </summary> 29 public class WaveformGraph : Drawable 30 { 31 private IShader shader; 32 private readonly Texture texture; 33 34 public WaveformGraph() 35 { 36 texture = Texture.WhitePixel; 37 } 38 39 [BackgroundDependencyLoader] 40 private void load(ShaderManager shaders) 41 { 42 shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); 43 } 44 45 private float resolution = 1; 46 47 /// <summary> 48 /// Gets or sets the amount of <see cref="Framework.Audio.Track.Waveform.Point"/>'s displayed relative to <see cref="Drawable.DrawWidth">DrawWidth</see>. 49 /// </summary> 50 public float Resolution 51 { 52 get => resolution; 53 set 54 { 55 if (value < 0) 56 throw new ArgumentOutOfRangeException(nameof(value)); 57 58 if (resolution == value) 59 return; 60 61 resolution = value; 62 generate(); 63 } 64 } 65 66 private Waveform waveform; 67 68 /// <summary> 69 /// The <see cref="Framework.Audio.Track.Waveform"/> to display. 70 /// </summary> 71 public Waveform Waveform 72 { 73 get => waveform; 74 set 75 { 76 if (waveform == value) 77 return; 78 79 waveform = value; 80 generate(); 81 } 82 } 83 84 private Color4 baseColour = Color4.White; 85 86 /// <summary> 87 /// The base colour of the graph for frequencies that don't fall into the predefined low/mid/high buckets. 88 /// Also serves as the default value of <see cref="LowColour"/>, <see cref="MidColour"/>, and <see cref="HighColour"/>. 89 /// </summary> 90 public Color4 BaseColour 91 { 92 get => baseColour; 93 set 94 { 95 if (baseColour == value) 96 return; 97 98 baseColour = value; 99 100 Invalidate(Invalidation.DrawNode); 101 } 102 } 103 104 private Color4? lowColour; 105 106 /// <summary> 107 /// The colour which low-range frequencies should be colourised with. 108 /// May be null for this frequency range to not be colourised. 109 /// </summary> 110 public Color4? LowColour 111 { 112 get => lowColour; 113 set 114 { 115 if (lowColour == value) 116 return; 117 118 lowColour = value; 119 120 Invalidate(Invalidation.DrawNode); 121 } 122 } 123 124 private Color4? midColour; 125 126 /// <summary> 127 /// The colour which mid-range frequencies should be colourised with. 128 /// May be null for this frequency range to not be colourised. 129 /// </summary> 130 public Color4? MidColour 131 { 132 get => midColour; 133 set 134 { 135 if (midColour == value) 136 return; 137 138 midColour = value; 139 140 Invalidate(Invalidation.DrawNode); 141 } 142 } 143 144 private Color4? highColour; 145 146 /// <summary> 147 /// The colour which high-range frequencies should be colourised with. 148 /// May be null for this frequency range to not be colourised. 149 /// </summary> 150 public Color4? HighColour 151 { 152 get => highColour; 153 set 154 { 155 if (highColour == value) 156 return; 157 158 highColour = value; 159 160 Invalidate(Invalidation.DrawNode); 161 } 162 } 163 164 protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) 165 { 166 var result = base.OnInvalidate(invalidation, source); 167 168 if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) 169 { 170 generate(); 171 result = true; 172 } 173 174 return result; 175 } 176 177 private CancellationTokenSource cancelSource = new CancellationTokenSource(); 178 private ScheduledDelegate scheduledGenerate; 179 180 private List<Waveform.Point> resampledPoints; 181 private int resampledChannels; 182 private double resampledMaxHighIntensity; 183 private double resampledMaxMidIntensity; 184 private double resampledMaxLowIntensity; 185 186 private void generate() 187 { 188 scheduledGenerate?.Cancel(); 189 cancelGeneration(); 190 191 if (Waveform == null) 192 return; 193 194 scheduledGenerate = Schedule(() => 195 { 196 cancelSource = new CancellationTokenSource(); 197 var token = cancelSource.Token; 198 199 Waveform.GenerateResampledAsync((int)Math.Max(0, Math.Ceiling(DrawWidth * Scale.X) * Resolution), token).ContinueWith(w => 200 { 201 var points = w.Result.GetPoints(); 202 var channels = w.Result.GetChannels(); 203 var maxHighIntensity = points.Count > 0 ? points.Max(p => p.HighIntensity) : 0; 204 var maxMidIntensity = points.Count > 0 ? points.Max(p => p.MidIntensity) : 0; 205 var maxLowIntensity = points.Count > 0 ? points.Max(p => p.LowIntensity) : 0; 206 207 Schedule(() => 208 { 209 resampledPoints = points; 210 resampledChannels = channels; 211 resampledMaxHighIntensity = maxHighIntensity; 212 resampledMaxMidIntensity = maxMidIntensity; 213 resampledMaxLowIntensity = maxLowIntensity; 214 215 OnWaveformRegenerated(w.Result); 216 217 Invalidate(Invalidation.DrawNode); 218 }); 219 }, token); 220 }); 221 } 222 223 private void cancelGeneration() 224 { 225 cancelSource?.Cancel(); 226 cancelSource?.Dispose(); 227 cancelSource = null; 228 } 229 230 /// <summary> 231 /// Invoked when the waveform has been regenerated. 232 /// </summary> 233 /// <param name="waveform">The new <see cref="Waveform"/> to be displayed.</param> 234 protected virtual void OnWaveformRegenerated(Waveform waveform) 235 { 236 } 237 238 protected override DrawNode CreateDrawNode() => new WaveformDrawNode(this); 239 240 protected override void Dispose(bool isDisposing) 241 { 242 base.Dispose(isDisposing); 243 cancelGeneration(); 244 } 245 246 private class WaveformDrawNode : DrawNode 247 { 248 private IShader shader; 249 private Texture texture; 250 251 private readonly List<Waveform.Point> points = new List<Waveform.Point>(); 252 253 private Vector2 drawSize; 254 private int channels; 255 256 private Color4 baseColour; 257 private Color4 lowColour; 258 private Color4 midColour; 259 private Color4 highColour; 260 261 private double highMax; 262 private double midMax; 263 private double lowMax; 264 265 protected new WaveformGraph Source => (WaveformGraph)base.Source; 266 267 public WaveformDrawNode(WaveformGraph source) 268 : base(source) 269 { 270 } 271 272 public override void ApplyState() 273 { 274 base.ApplyState(); 275 276 shader = Source.shader; 277 texture = Source.texture; 278 drawSize = Source.DrawSize; 279 280 points.Clear(); 281 282 if (Source.resampledPoints != null) 283 points.AddRange(Source.resampledPoints); 284 285 channels = Source.resampledChannels; 286 highMax = Source.resampledMaxHighIntensity; 287 midMax = Source.resampledMaxMidIntensity; 288 lowMax = Source.resampledMaxLowIntensity; 289 290 baseColour = Source.baseColour; 291 lowColour = Source.lowColour ?? baseColour; 292 midColour = Source.midColour ?? baseColour; 293 highColour = Source.highColour ?? baseColour; 294 } 295 296 private readonly QuadBatch<TexturedVertex2D> vertexBatch = new QuadBatch<TexturedVertex2D>(1000, 10); 297 298 public override void Draw(Action<TexturedVertex2D> vertexAction) 299 { 300 base.Draw(vertexAction); 301 302 if (texture?.Available != true || points == null || points.Count == 0) 303 return; 304 305 shader.Bind(); 306 texture.TextureGL.Bind(); 307 308 Vector2 localInflationAmount = new Vector2(0, 1) * DrawInfo.MatrixInverse.ExtractScale().Xy; 309 310 // We're dealing with a _large_ number of points, so we need to optimise the quadToDraw * drawInfo.Matrix multiplications below 311 // for points that are going to be masked out anyway. This allows for higher resolution graphs at larger scales with virtually no performance loss. 312 // Since the points are generated in the local coordinate space, we need to convert the screen space masking quad coordinates into the local coordinate space 313 RectangleF localMaskingRectangle = (Quad.FromRectangle(GLWrapper.CurrentMaskingInfo.ScreenSpaceAABB) * DrawInfo.MatrixInverse).AABBFloat; 314 315 float separation = drawSize.X / (points.Count - 1); 316 317 for (int i = 0; i < points.Count - 1; i++) 318 { 319 float leftX = i * separation; 320 float rightX = (i + 1) * separation; 321 322 if (rightX < localMaskingRectangle.Left) 323 continue; 324 325 if (leftX > localMaskingRectangle.Right) 326 break; // X is always increasing 327 328 Color4 frequencyColour = baseColour; 329 330 // colouring is applied in the order of interest to a viewer. 331 frequencyColour = Interpolation.ValueAt(points[i].MidIntensity / midMax, frequencyColour, midColour, 0, 1); 332 // high end (cymbal) can help find beat, so give it priority over mids. 333 frequencyColour = Interpolation.ValueAt(points[i].HighIntensity / highMax, frequencyColour, highColour, 0, 1); 334 // low end (bass drum) is generally the best visual aid for beat matching, so give it priority over high/mid. 335 frequencyColour = Interpolation.ValueAt(points[i].LowIntensity / lowMax, frequencyColour, lowColour, 0, 1); 336 337 ColourInfo finalColour = DrawColourInfo.Colour; 338 finalColour.ApplyChild(frequencyColour); 339 340 Quad quadToDraw; 341 342 switch (channels) 343 { 344 default: 345 case 2: 346 { 347 float height = drawSize.Y / 2; 348 quadToDraw = new Quad( 349 new Vector2(leftX, height - points[i].Amplitude[0] * height), 350 new Vector2(rightX, height - points[i + 1].Amplitude[0] * height), 351 new Vector2(leftX, height + points[i].Amplitude[1] * height), 352 new Vector2(rightX, height + points[i + 1].Amplitude[1] * height) 353 ); 354 break; 355 } 356 357 case 1: 358 { 359 quadToDraw = new Quad( 360 new Vector2(leftX, drawSize.Y - points[i].Amplitude[0] * drawSize.Y), 361 new Vector2(rightX, drawSize.Y - points[i + 1].Amplitude[0] * drawSize.Y), 362 new Vector2(leftX, drawSize.Y), 363 new Vector2(rightX, drawSize.Y) 364 ); 365 break; 366 } 367 } 368 369 quadToDraw *= DrawInfo.Matrix; 370 DrawQuad(texture, quadToDraw, finalColour, null, vertexBatch.AddAction, Vector2.Divide(localInflationAmount, quadToDraw.Size)); 371 } 372 373 shader.Unbind(); 374 } 375 376 protected override void Dispose(bool isDisposing) 377 { 378 base.Dispose(isDisposing); 379 380 vertexBatch.Dispose(); 381 } 382 } 383 } 384}