// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Threading.Tasks; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Viewport.Previews { /// /// Audio clip PCM data editor preview. /// /// [HideInEditor] public class AudioClipPreview : ContainerControl { /// /// The audio clip drawing modes. /// public enum DrawModes { /// /// Fills the whole control area with the full clip duration. /// Fill, /// /// Draws single audio clip. Uses the view scale parameter. /// Single, /// /// Draws the looped audio clip. Uses the view scale parameter. /// Looped, }; private readonly object _locker = new object(); private AudioClip _asset; private int _pcmSequence = 0; private float[] _pcmData; private AudioDataInfo _pcmInfo; /// /// Gets or sets the clip to preview. /// public AudioClip Asset { get => _asset; set { lock (_locker) { if (_asset == value) return; _asset = value; _pcmData = null; if (_asset) { // Use async task to gather PCM data (engine loads it from the asset) Task.Run(DownloadData); } } } } /// /// Gets a value indicating whether audio data has been fetched from the asset (done as an async task). It is required to be valid in order to draw the audio buffer preview. /// public bool HasData { get { lock (_locker) { return _pcmData != null; } } } /// /// Gets the cached audio data info. /// public AudioDataInfo DataInfo { get { lock (_locker) { return _pcmInfo; } } } /// /// The draw mode. /// public DrawModes DrawMode = DrawModes.Fill; /// /// The view offset parameter. Shifts the audio preview (in seconds). /// public float ViewOffset = 0.0f; /// /// The view scale parameter. Increase it to zoom in the audio. Usage depends on the current . /// public float ViewScale = 1.0f; /// /// The color of the audio PCM data spectrum. /// public Color Color = Color.White; /// /// The audio units per second (on time axis). /// public static readonly float UnitsPerSecond = 100.0f; /// /// Invalidates the cached audio PCM data and fetches it again from the asset. /// public void RefreshPreview() { lock (_locker) { if (_asset != null) { // Release any cached data _pcmData = null; // Invalidate any in-flight data download to reject cached data due to refresh if (_pcmSequence != 0) _pcmSequence++; // Use async task to gather PCM data (engine loads it from the asset) Task.Run(DownloadData); } } } /// public override void Draw() { base.Draw(); lock (_locker) { var info = _pcmInfo; if (_asset == null || _pcmData == null || info.NumSamples == 0) return; var height = Height; var width = Width; var samplesPerChannel = info.NumSamples / info.NumChannels; var length = (float)samplesPerChannel / info.SampleRate; var color = Color; if (!EnabledInHierarchy) color *= 0.4f; var sampleValueScale = height / info.NumChannels; // Calculate the amount of samples that are contained in the view float unitsPerSecond = UnitsPerSecond * ViewScale; float clipDefaultWidth = length * unitsPerSecond; float clipsInView = width / clipDefaultWidth; float clipWidth; uint samplesPerIndex; switch (DrawMode) { case DrawModes.Fill: clipsInView = 1.0f; clipWidth = width; samplesPerIndex = (uint)(samplesPerChannel / width) * info.NumChannels; break; case DrawModes.Single: clipsInView = Mathf.Min(clipsInView, 1.0f); clipWidth = clipDefaultWidth; samplesPerIndex = (uint)(info.SampleRate / unitsPerSecond) * info.NumChannels; break; case DrawModes.Looped: clipWidth = width / clipsInView; samplesPerIndex = (uint)(info.SampleRate / unitsPerSecond) * info.NumChannels; break; default: throw new ArgumentOutOfRangeException(); } const uint maxSamplesPerIndex = 64; uint samplesPerIndexDiff = Math.Max(1, samplesPerIndex / Math.Min(samplesPerIndex, maxSamplesPerIndex)); // Calculate the clip range in the view to optimize drawing (eg. if only part fo the clip is visible) Render2D.PeekClip(out var globalClipping); Render2D.PeekTransform(out var globalTransform); var globalRect = new Rectangle(globalTransform.M31, globalTransform.M32, width * globalTransform.M11, height * globalTransform.M22); var globalMask = Rectangle.Shared(globalClipping, globalRect); var globalTransformInv = Matrix3x3.Invert(globalTransform); var localRect = Rectangle.FromPoints(Matrix3x3.Transform2D(globalMask.UpperLeft, globalTransformInv), Matrix3x3.Transform2D(globalMask.BottomRight, globalTransformInv)); var localRectMin = localRect.UpperLeft; var localRectMax = localRect.BottomRight; // Render each clip separately var viewOffset = (uint)(info.SampleRate * info.NumChannels * ViewOffset); for (uint clipIndex = 0; clipIndex < Mathf.CeilToInt(clipsInView); clipIndex++) { var clipStart = clipWidth * clipIndex; var clipEnd = clipStart + clipWidth; var xStart = Mathf.Max(clipStart, localRectMin.X); var xEnd = Mathf.Min(Mathf.Min(width, clipEnd), localRectMax.X); var samplesOffset = (uint)((xStart - clipStart) * samplesPerIndex) + viewOffset; // Render every audio channel separately for (uint channelIndex = 0; channelIndex < info.NumChannels; channelIndex++) { uint currentSample = channelIndex + samplesOffset; float yCenter = Y + ((2 * channelIndex) + 1) * height / (2.0f * info.NumChannels); for (float pixelX = xStart; pixelX < xEnd; pixelX++) { float samplesSum = 0; int samplesInPixel = 0; uint samplesEnd = Math.Min(currentSample + samplesPerIndex, info.NumSamples); for (uint sampleIndex = currentSample; sampleIndex < samplesEnd; sampleIndex += samplesPerIndexDiff) { samplesSum += Mathf.Abs(_pcmData[sampleIndex]); samplesInPixel++; } currentSample = samplesEnd; if (samplesInPixel > 0) { float sampleValueAvg = samplesSum / samplesInPixel; float sampleValueAvgScaled = sampleValueAvg * sampleValueScale; if (sampleValueAvgScaled > 0.1f) { Render2D.DrawLine(new Float2(pixelX, yCenter - sampleValueAvgScaled), new Float2(pixelX, yCenter + sampleValueAvgScaled), color); } } } } } } } /// public override void OnDestroy() { lock (_locker) { _asset = null; _pcmData = null; } base.OnDestroy(); } /// /// Downloads the audio clip raw PCM data. Use it from async thread to prevent blocking, /// private void DownloadData() { AudioClip asset; int sequence; lock (_locker) { asset = _asset; sequence = _pcmSequence; } if (!asset) return; float[] data; AudioDataInfo dataInfo; try { asset.ExtractDataFloat(out data, out dataInfo); } catch (Exception ex) { Editor.LogWarning("Failed to get audio clip PCM data. " + ex.Message); Editor.LogWarning(ex); return; } if (data.Length != dataInfo.NumSamples) { Editor.LogWarning("Failed to get audio clip PCM data. Invalid samples count. Returned buffer has other size."); } lock (_locker) { // If asset has been modified during data fetching, ignore it if (_asset == asset && _pcmSequence == sequence) { _pcmSequence++; _pcmData = data; _pcmInfo = dataInfo; } } } } }