using System; using System.Collections.Generic; using System.Linq; using System.Text; using Lofelt.NiceVibrations; using UnityEngine; #if UNITY_EDITOR using UnityEditor; using System.IO; #endif namespace MoreMountains.FeedbacksForThirdParty { /// /// A class used to convert an AudioClip into a .haptic file /// public class AudioToHapticConverter { /// /// Converts an AudioClip into a HapticClip and returns it /// /// /// /// /// /// /// public static NVHapticData GenerateHapticFile(AudioClip audioClip, string outputFolder, string outputFileName, bool normalizeAmplitude = false, float normalizeAmplitudeFactor = 1f, bool normalizeFrequency = false, float normalizeFrequencyFactor = 1f, int sampleCount = 100) { #if UNITY_EDITOR if (audioClip == null) { Debug.LogError("No AudioClip assigned! Please assign one."); return null; } string outputPath = Path.Combine(outputFolder, outputFileName); try { float clipLength = audioClip.length; float[] samples = new float[audioClip.samples * audioClip.channels]; audioClip.GetData(samples, 0); List amplitudePoints = new List(sampleCount); List frequencyPoints = new List(sampleCount); // amplitude for (int i = 0; i < sampleCount; i++) { float time = (clipLength / (sampleCount - 1)) * i; int sampleIndex = Mathf.Min((int)(time * audioClip.frequency) * audioClip.channels, samples.Length - audioClip.channels); float sum = 0f; for (int c = 0; c < audioClip.channels; c++) { sum += Mathf.Abs(samples[sampleIndex + c]); } float amplitude = Mathf.Clamp01(sum / audioClip.channels); float emphasisAmplitude = Mathf.Max(amplitude, 0f); NVEmphasis emphasis = new NVEmphasis() { amplitude = emphasisAmplitude, frequency = emphasisAmplitude }; amplitudePoints.Add(new NVAmplitudePoint() { time = time, amplitude = amplitude, emphasis = emphasis }); } // frequency int frameSize = 1024; for (int i = 0; i < sampleCount; i++) { float time = i / (float)audioClip.frequency; float[] frame = new float[frameSize]; Array.Copy(samples, i, frame, 0, frameSize); float amplitude = frame.Max(Mathf.Abs); float frequency = EstimateFrequencyZCR(frame, audioClip.frequency); frequencyPoints.Add(new NVFrequencyPoint() { time = time, frequency = frequency }); } NVHapticFile hapticFile = new NVHapticFile() { version = new NVVersion() { major = 1, minor = 0, patch = 0 }, metadata = new NVMetadata() { editor = "Feel", author = "More Mountains", source = audioClip.name, project = "Feel", tags = new List { "converted", "audio" }, description = "Haptic data generated by Feel from AudioClip" }, signals = new NVSignals() { continuous = new NVContinuous() { envelopes = new NVEnvelopes() { amplitude = amplitudePoints, frequency = frequencyPoints } } } }; string json = JsonUtility.ToJson(hapticFile, true); byte[] bytes = Encoding.UTF8.GetBytes(json); File.WriteAllText(outputPath, json); UnityEditor.AssetDatabase.Refresh(); HapticClip haptic = AssetDatabase.LoadAssetAtPath(outputPath); Debug.Log($"Haptic file generated and saved to: {outputPath}"); NVHapticData data = new NVHapticData(); data.Clip = haptic; data.SampleCount = sampleCount; data.AmplitudePoints = amplitudePoints; data.FrequencyPoints = frequencyPoints; haptic.gamepadRumble = ConvertRumbleData(data, haptic.gamepadRumble.totalDurationMs, normalizeAmplitude, normalizeAmplitudeFactor, normalizeFrequency, normalizeFrequencyFactor); data.RumbleData = haptic.gamepadRumble; return data; } catch (Exception e) { Debug.LogError("Failed to generate haptic file: " + e.Message); } #endif return null; } /// /// converts amplitude & frequency to rumble data /// /// /// /// /// /// /// /// protected static GamepadRumble ConvertRumbleData(NVHapticData data, int totalDurationMs, bool normalizeAmplitude = false, float normalizeAmplitudeFactor = 1f, bool normalizeFrequency = false, float normalizeFrequencyFactor = 1f) { GamepadRumble result = new GamepadRumble(); result.totalDurationMs = totalDurationMs; result.durationsMs = new int[data.AmplitudePoints.Count]; result.highFrequencyMotorSpeeds = new float[data.AmplitudePoints.Count]; result.lowFrequencyMotorSpeeds = new float[data.AmplitudePoints.Count]; for (int i = 0; i < data.AmplitudePoints.Count; i++) { result.durationsMs[i] = Mathf.RoundToInt(totalDurationMs / data.AmplitudePoints.Count); result.highFrequencyMotorSpeeds[i] = data.AmplitudePoints[i].emphasis.amplitude; result.lowFrequencyMotorSpeeds[i] = data.FrequencyPoints[i].frequency * result.highFrequencyMotorSpeeds[i]; } // normalizing if (normalizeAmplitude) { result.highFrequencyMotorSpeeds = Normalize(result.highFrequencyMotorSpeeds, normalizeAmplitudeFactor); } if (normalizeFrequency) { result.lowFrequencyMotorSpeeds = Normalize(result.lowFrequencyMotorSpeeds, normalizeFrequencyFactor); } return result; } /// /// Normalizes the curve based on a specified max value /// /// /// /// protected static float[] Normalize(float[] data, float maxDesiredValue) { float currentMax = data.Max(); if (currentMax > 0f) { float scaleFactor = maxDesiredValue / currentMax; for (int i = 0; i < data.Length; i++) { data[i] *= scaleFactor; } } return data; } /// /// Estimates the frequency using zero crossing /// /// /// /// protected static float EstimateFrequencyZCR(float[] frame, int sampleRate) { int zeroCrossings = 0; for (int i = 1; i < frame.Length; i++) { if ((frame[i - 1] >= 0 && frame[i] < 0) || (frame[i - 1] < 0 && frame[i] >= 0)) { zeroCrossings++; } } float duration = frame.Length / (float)sampleRate; float estimatedFreq = (zeroCrossings / (2f * duration)); float normalized = Mathf.Clamp01(estimatedFreq / 1000f); return normalized; } } }