253 lines
7.0 KiB
C#
253 lines
7.0 KiB
C#
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;
|
|
}
|
|
}
|
|
} |