Added Feel plugin

This commit is contained in:
journaliciouz
2025-12-11 14:49:16 +01:00
parent 97dce4aaf6
commit 1942a531d4
2820 changed files with 257786 additions and 9 deletions

View 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;
}
}
}