/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /************************************************************************************ * Filename : MetaXRAudioSpectrumEditor.cs * Content : Editor for audio spectra (e.g. materials) ***********************************************************************************/ using UnityEditor; using UnityEngine; using Point = Meta.XR.Acoustics.Spectrum.Point; using Spectrum = Meta.XR.Acoustics.Spectrum; internal sealed class MetaXRAudioSpectrumEditor { internal enum AxisScale { Linear, Log, Square, Cube, SquareCentered } private static readonly Texture2D texture = EditorGUIUtility.whiteTexture; /// A text style that is used to draw frequency labels and tick marks. private static readonly GUIStyle frequencyTextStyle = new GUIStyle { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Overflow, fontSize = 8, fontStyle = FontStyle.Bold, wordWrap = false, normal = new GUIStyleState { textColor = new Color(0.1f, 0.1f, 0.1f) }, focused = new GUIStyleState { textColor = new Color(0.1f, 0.1f, 0.1f) } }; /// A text style that is used to draw data labels and tick marks. private static readonly GUIStyle dataTextStyle = new GUIStyle { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Overflow, fontSize = 8, fontStyle = FontStyle.Bold, wordWrap = false, normal = new GUIStyleState { textColor = Color.gray }, focused = new GUIStyleState { textColor = Color.gray } }; /// A text style that is used to draw a label for the selected point. private static readonly GUIStyle selectedTextStyle = new GUIStyle { alignment = TextAnchor.LowerCenter, clipping = TextClipping.Overflow, fontSize = 8, fontStyle = FontStyle.Bold, wordWrap = false, normal = new GUIStyleState { textColor = Color.white }, focused = new GUIStyleState { textColor = Color.white } }; private static int focus; private bool dragInitiated; private bool isDragging; private bool displaySpectrum = true; private bool displayPoints = false; private readonly string label; private readonly string tooltip; private readonly float rangeMin; private readonly float rangeMax; private readonly float rangeOrigin; private readonly AxisScale scale; /// The size in pixels of the right margin where axis tick mark labels are displayed. private const float rightMargin = 36.0f; /// The maximum frequency that is displayed. private const float frequencyMax = 20000.0f; /// The height of the spectrum graph in pixels internal float spectrumHeight = 120.0f; /// The size of a data point when it is selected. internal float selectedPointSize = 12.0f; /// Whether or not the user is able to add new points to the spectrum. internal bool canAddPoints = true; /// Whether or not the user is able to remove points from the spectrum. internal bool canRemovePoints = true; /// Whether or not the user can edit the frequency values of points in the spectrum. internal bool canEditFrequency = true; /// Whether or not the user can edit the data values of points in the spectrum. internal bool canEditData = true; /// Whether or not the spectrum is drawn with the area under the curve shaded. internal bool drawFilled = true; /// The color that is used to draw the spectrum curve. internal Color spectrumColor = AudioCurveRendering.kAudioOrange; /// The units to display for the data values. internal string dataUnits = ""; internal static readonly string pointAddedGroupName = "Point Added"; internal static readonly string pointRemovedGroupName = "Point Removed"; internal static readonly string pointSelectedGroupName = "Point Selected"; internal static readonly string pointMovedGroupName = "Point Moved"; internal void LoadFoldoutState() { displaySpectrum = EditorPrefs.GetBool(label + "SpectrumFoldout", true); displayPoints = EditorPrefs.GetBool(label + "PointsFoldout", false); } internal void SaveFoldoutState() { EditorPrefs.SetBool(label + "SpectrumFoldout", displaySpectrum); EditorPrefs.SetBool(label + "PointsFoldout", displayPoints); } internal static float GetRightMargin() { return rightMargin; } internal bool hasFocus(Spectrum spectrum) { return focus == spectrum.GetHashCode(); } internal MetaXRAudioSpectrumEditor(string label, string tooltip, AxisScale scale, Color spectrumColor, float rangeMin = 0.0f, float rangeMax = 1.0f, float rangeOrigin = 0.0f) { this.label = label; this.tooltip = tooltip; this.scale = scale; this.spectrumColor = spectrumColor; this.rangeMin = rangeMin; this.rangeMax = rangeMax; this.rangeOrigin = rangeOrigin; } internal void Draw(Spectrum spectrum, Event e) { if (DrawFoldout()) DrawFoldoutSpectrum(spectrum, e); } internal bool DrawFoldout() { displaySpectrum = EditorGUILayout.Foldout(displaySpectrum, new GUIContent(label, tooltip)); return displaySpectrum; } internal void DrawFoldoutSpectrum(Spectrum spectrum, Event e) { EditorGUI.indentLevel++; // Note: note if UI modifes the spectrum a clone is returned DrawSpectrum(spectrum, e); displayPoints = EditorGUILayout.Foldout(displayPoints, "Points"); if (displayPoints) { EditorGUI.indentLevel++; DrawPoints(spectrum); EditorGUI.indentLevel--; } EditorGUI.indentLevel--; } private void DrawSpectrum(Spectrum spectrum, Event e) { Rect r = EditorGUILayout.GetControlRect(true, spectrumHeight); r.width -= rightMargin; DrawDataTicks(r); r = AudioCurveRendering.BeginCurveFrame(r); DrawFrequencyTicks(r); if (drawFilled) AudioCurveRendering.DrawMinMaxFilledCurve(r, EvaluateCurveMinMaxColor(spectrum)); AudioCurveRendering.DrawCurve(r, EvaluateCurve(spectrum), spectrumColor); HandleEvent(spectrum, r, e); if (hasFocus(spectrum)) DrawSelected(spectrum, r); AudioCurveRendering.EndCurveFrame(); } private void DrawPoints(Spectrum spectrum) { int pointCount = spectrum.points.Count; int lines = pointCount > 0 ? pointCount + 2 : 1; float height = EditorGUIUtility.singleLineHeight * lines; Rect r1 = EditorGUILayout.GetControlRect(true, height); r1.width -= rightMargin; r1.height = EditorGUIUtility.singleLineHeight; { int oldCount = pointCount; int newCount = EditorGUI.DelayedIntField(r1, "Size", oldCount); r1.y += r1.height; if (canRemovePoints && newCount < pointCount) { spectrum.points.RemoveRange(newCount, oldCount - newCount); Undo.SetCurrentGroupName("Points Removed"); GUI.changed = true; } else if (canAddPoints && newCount > oldCount) { if (newCount > spectrum.points.Capacity) spectrum.points.Capacity = newCount; for (int i = oldCount; i < newCount; i++) spectrum.points.Add(new Point(125 * (1 << i))); Undo.SetCurrentGroupName("Points Added"); GUI.changed = true; } } pointCount = spectrum.points.Count; if (pointCount > 0) { Rect r2 = new Rect(r1.xMax + 9, r1.y + r1.height * 1.125f, 24, r1.height * .75f); r1.width /= 2; EditorGUI.LabelField(r1, "Frequency"); r1.x += r1.width; EditorGUI.LabelField(r1, label); r1.x -= r1.width; r1.y += r1.height; for (int i = 0; i < pointCount; i++) { // Frequency field if (canEditFrequency) { GUIStyle style = EditorStyles.textField; style.alignment = TextAnchor.MiddleRight; float freq = EditorGUI.FloatField(r1, Mathf.Round(spectrum.points[i].frequency), style); freq = Mathf.Clamp(freq, 0.0f, frequencyMax); if (freq != spectrum.points[i].frequency) spectrum.points[i] = new Point(freq, spectrum.points[i].data); } else { EditorGUI.LabelField(r1, FrequencyToString(spectrum.points[i].frequency)); } // Data field r1.x += r1.width; if (canEditData) { float data = EditorGUI.FloatField(r1, spectrum.points[i].data, EditorStyles.textField); data = Mathf.Clamp(data, rangeMin, rangeMax); if (data != spectrum.points[i].data) { Debug.Log($"Changed data from {spectrum.points[i].data} to {data}"); spectrum.points[i] = new Point(spectrum.points[i].frequency, data); } } else { EditorGUI.LabelField(r1, DataToString(spectrum.points[i].data)); } r1.x -= r1.width; r1.y += r1.height; // Remove button if (canRemovePoints && GUI.Button(r2, "–")) { RemovePointAt(spectrum, i); break; } r2.y += r1.height; } } } private void DrawDataTicks(Rect r) { const int ticks = 10; Rect label = new Rect(r.xMax + 9, r.y - r.height / (2 * ticks), 24, r.height / ticks); Rect tick = new Rect(r.xMax + 2, r.y - 1, 4.5f, 2); for (int i = 0; i <= ticks; i++) { float value = MapData(1 - (float)i / ticks, false); EditorGUI.DrawRect(tick, dataTextStyle.normal.textColor); GUI.Label(label, value.ToString("0.000"), dataTextStyle); tick.y += label.height; label.y += label.height; } } private void DrawFrequencyTicks(Rect r) { Rect tick = new Rect(r.x, r.y, 1.0f, r.height); Rect label = new Rect(r.x, 0.5f * EditorGUIUtility.singleLineHeight, 32.0f, EditorGUIUtility.singleLineHeight); for (int i = 1; i < 30; i++) { float frequency; if (MapFrequencyTick(i, out frequency)) { tick.x = MapFrequency(frequency) * r.width; tick.height = label.y - r.y; tick.width = 2.0f; EditorGUI.DrawRect(tick, frequencyTextStyle.normal.textColor); tick.y = label.yMax; tick.height = r.yMax - label.yMax; EditorGUI.DrawRect(tick, frequencyTextStyle.normal.textColor); label.x = tick.x - 2.0f; GUI.Label(label, FrequencyToTickString(frequency), frequencyTextStyle); tick.y = r.y; tick.height = r.height; tick.width = 1.0f; } else { tick.x = MapFrequency(frequency) * r.width; EditorGUI.DrawRect(tick, frequencyTextStyle.normal.textColor); } } } private void DrawSelected(Spectrum spectrum, Rect r) { if (spectrum.points.Count > spectrum.selection) { Point point = spectrum.points[spectrum.selection]; // Draw a circle for the selected point. Vector2 position = MapPointPosition(r, point); Vector2 pointSize = new Vector2(selectedPointSize, selectedPointSize); Rect pointRect = new Rect(position - pointSize * 0.5f, pointSize); // White circle with black outline #if UNITY_5 GUI.DrawTexture(pointRect, texture, ScaleMode.StretchToFill, false, 0); GUI.DrawTexture(pointRect, texture, ScaleMode.StretchToFill, false, 0); #else GUI.DrawTexture(pointRect, texture, ScaleMode.StretchToFill, false, 0, Color.white, 0, selectedPointSize); GUI.DrawTexture(pointRect, texture, ScaleMode.StretchToFill, false, 0, Color.black, 2, selectedPointSize); #endif // Draw a label above the point with the current values. const float labelPadding = 2.0f; Vector2 labelSize = new Vector2(30.0f, EditorGUIUtility.singleLineHeight); Rect labelRect = new Rect(position - new Vector2(labelSize.x * 0.5f, labelSize.y + selectedPointSize * 0.5f + labelPadding), labelSize); GUI.Label(labelRect, FrequencyToString(point.frequency) + "\n" + DataToString(point.data), selectedTextStyle); } } private void HandleEvent(Spectrum spectrum, Rect r, Event e) { Vector2 position = e.mousePosition; switch (e.type) { case EventType.MouseDown: if (r.Contains(position)) { if (e.clickCount == 2) { if (canAddPoints) { spectrum.selection = spectrum.points.Count; spectrum.points.Add(MapMouseEvent(r, position)); spectrum.Sort(); Undo.SetCurrentGroupName(pointAddedGroupName); GUI.changed = true; } } else { int selection = spectrum.selection; float minDistance = float.MaxValue; for (int i = 0; i < spectrum.points.Count; i++) { float distance = Vector2.Distance(MapPointPosition(r, spectrum.points[i]), position); if (distance < minDistance) { selection = i; minDistance = distance; } } if (selection != spectrum.selection) { spectrum.selection = selection; Undo.SetCurrentGroupName(pointSelectedGroupName); GUI.changed = true; } } focus = spectrum.GetHashCode(); dragInitiated = true; } else { isDragging = false; focus = 0; } e.Use(); break; case EventType.MouseDrag: if (dragInitiated) { dragInitiated = false; isDragging = true; } if (isDragging && spectrum.selection < spectrum.points.Count) { Point newPoint = MapMouseEvent(r, position); if (!canEditFrequency) newPoint.frequency = spectrum.points[spectrum.selection].frequency; if (!canEditData) newPoint.data = spectrum.points[spectrum.selection].data; #if META_XR_ACOUSTIC_INFO Debug.Log($"point moved: {spectrum.points[spectrum.selection]} -> {newPoint}"); #endif spectrum.points[spectrum.selection] = newPoint; e.Use(); } break; case EventType.Ignore: case EventType.MouseUp: dragInitiated = false; if (isDragging) { isDragging = false; Undo.SetCurrentGroupName(pointMovedGroupName); GUI.changed = true; spectrum.Sort(); e.Use(); } break; case EventType.KeyDown: switch (e.keyCode) { case KeyCode.Delete: case KeyCode.Backspace: if (canRemovePoints && spectrum.selection < spectrum.points.Count) { RemovePointAt(spectrum, spectrum.selection); e.Use(); } break; } break; } } private void RemovePointAt(Spectrum spectrum, int index) { spectrum.points.RemoveAt(index); if (spectrum.selection == index) spectrum.selection = spectrum.points.Count; Undo.SetCurrentGroupName(pointRemovedGroupName); GUI.changed = true; } private AudioCurveRendering.AudioCurveEvaluator EvaluateCurve(Spectrum spectrum) { return (float f) => 2 * MapData(spectrum[MapFrequency(f, false)]) - 1; } private AudioCurveRendering.AudioMinMaxCurveAndColorEvaluator EvaluateCurveMinMaxColor(Spectrum spectrum) { return (float f, out Color color, out float minValue, out float maxValue) => { float y = 2.0f * MapData(spectrum[MapFrequency(f, false)]) - 1.0f; float c = 2.0f * (rangeOrigin - rangeMin) / (rangeMax - rangeMin) - 1.0f; minValue = Mathf.Min(y, c); maxValue = Mathf.Max(y, c); color = spectrumColor; color.a = 0.3f; }; } private Vector2 MapPointPosition(Rect r, Point point) => new Vector2 { x = r.x + r.width * MapFrequency(point.frequency), y = r.y + r.height * (1 - MapData(point.data)) }; private Point MapMouseEvent(Rect r, Vector2 v) => new Point { frequency = v.x < r.xMin ? 0.0f : v.x > r.xMax ? frequencyMax : MapFrequency((v.x - r.x) / r.width, false), data = v.y < r.yMin ? rangeMax : v.y > r.yMax ? rangeMin : MapData(1 - (v.y - r.y) / r.height, false) }; private float Square(float f) { return f * f; } private float MapData(float f, bool forward = true) { float rangeSize = rangeMax - rangeMin; if (forward) f = (f - rangeMin) / rangeSize; switch (scale) { case AxisScale.Log: f = forward ? f < 1e-3f ? 0 : 1 + (Mathf.Log10(f) / 3) : Mathf.Pow(10, -3 * (1 - f)); break; case AxisScale.Square: f = forward ? Mathf.Sqrt(f) : f * f; break; case AxisScale.SquareCentered: if (forward) { f = f > 0.5f ? 0.5f + 0.5f * Mathf.Sqrt((f - 0.5f) * 2.0f) : 0.5f - 0.5f * Mathf.Sqrt((0.5f - f) * 2.0f); } else { f = f > 0.5f ? 0.5f + 0.5f * Square((f - 0.5f) * 2.0f) : 0.5f - 0.5f * Square((0.5f - f) * 2.0f); } break; case AxisScale.Cube: f = forward ? Mathf.Pow(f, 1.0f / 3.0f) : f * f * f; break; default: case AxisScale.Linear: break; } if (!forward) f = f * rangeSize + rangeMin; return f; } private static bool MapFrequencyTick(int i, out float frequency) { int power = i / 9 + 1; int multiplier = i % 9 + 1; frequency = multiplier * Mathf.Pow(10, power); return multiplier == 1; } private static float MapFrequency(float f, bool forward = true) { if (forward) return f < 10.0f ? 0.0f : Mathf.Log(f / 10.0f, frequencyMax / 10.0f); else return 10.0f * Mathf.Pow(frequencyMax / 10.0f, f); } /// Return a formated string with units for the specified frequency in hertz. private static string FrequencyToString(float frequency) { if (frequency < 1000) return string.Format("{0:F1} Hz", frequency); else return string.Format("{0:F2} kHz", frequency * 0.001f); } /// Return a formated tick label string with units for the specified frequency in hertz. private static string FrequencyToTickString(float frequency) { if (frequency < 1000) return string.Format("{0:F0} Hz", frequency); else return string.Format("{0:F0} kHz", frequency * 0.001f); } /// Return a formated string for the specified data value. private string DataToString(float data) { return string.Format("{0:F3}", data) + dataUnits; } }