Added Feel plugin
This commit is contained in:
253
Assets/External/Feel/NiceVibrations/Helpers/AudioToHapticConverter.cs
vendored
Normal file
253
Assets/External/Feel/NiceVibrations/Helpers/AudioToHapticConverter.cs
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// A class used to convert an AudioClip into a .haptic file
|
||||
/// </summary>
|
||||
public class AudioToHapticConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an AudioClip into a HapticClip and returns it
|
||||
/// </summary>
|
||||
/// <param name="audioClip"></param>
|
||||
/// <param name="outputFolder"></param>
|
||||
/// <param name="outputFileName"></param>
|
||||
/// <param name="useFrequency"></param>
|
||||
/// <param name="amplitudeThreshold"></param>
|
||||
/// <returns></returns>
|
||||
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<NVAmplitudePoint> amplitudePoints = new List<NVAmplitudePoint>(sampleCount);
|
||||
List<NVFrequencyPoint> frequencyPoints = new List<NVFrequencyPoint>(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<string> { "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<HapticClip>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// converts amplitude & frequency to rumble data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="totalDurationMs"></param>
|
||||
/// <param name="normalizeAmplitude"></param>
|
||||
/// <param name="normalizeAmplitudeFactor"></param>
|
||||
/// <param name="normalizeFrequency"></param>
|
||||
/// <param name="normalizeFrequencyFactor"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the curve based on a specified max value
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="maxDesiredValue"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the frequency using zero crossing
|
||||
/// </summary>
|
||||
/// <param name="frame"></param>
|
||||
/// <param name="sampleRate"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user