using System.Collections;
using UnityEngine;
namespace MyInfoMate.Vr.Menu
{
///
/// Le retour au survol et à la sélection : un son court, et une vibration quand il y
/// a une manette pour la sentir.
///
/// Ce que ça règle. En visée à la tête, rien ne confirme qu'on a bien accroché
/// un panneau avant que l'anneau ne commence à se remplir — et l'anneau est en
/// périphérie du regard au moment où on l'attend. Un son de 40 ms dit « tu es
/// dessus » avant même que l'œil ne l'ait vu.
///
/// Les sons sont synthétisés, pas chargés. Deux raisons : aucun fichier à
/// gérer dans un repo qui n'a pas d'assets, et un clic généré est identique sur tous
/// les casques. Ils sont volontairement discrets — une borne de musée qui claque à
/// chaque regard devient insupportable en une heure.
///
public class MenuFeedback : MonoBehaviour
{
const int SampleRate = 44100;
static MenuFeedback _instance;
AudioSource _source;
AudioClip _hover;
AudioClip _select;
static MenuFeedback Instance
{
get
{
if (_instance != null) return _instance;
var root = new GameObject("MenuFeedback");
DontDestroyOnLoad(root);
_instance = root.AddComponent();
_instance.Build();
return _instance;
}
}
void Build()
{
_source = gameObject.AddComponent();
_source.playOnAwake = false;
// 2D : le retour n'appartient pas à un endroit de la pièce, il appartient au
// geste. Le spatialiser le ferait venir d'un panneau qu'on est en train de
// quitter.
_source.spatialBlend = 0f;
_hover = Tone("hover", 880f, 0.045f, 0.18f);
_select = Tone("select", 1320f, 0.09f, 0.32f, 660f);
}
///
/// Une sinusoïde avec une enveloppe qui décroît.
/// non nul fait un glissando : c'est ce qui distingue « choisi » de « survolé »
/// sans avoir à monter le volume.
///
static AudioClip Tone(string name, float frequency, float seconds, float volume,
float startFrequency = 0f)
{
var samples = Mathf.RoundToInt(SampleRate * seconds);
var data = new float[samples];
var phase = 0f;
for (var i = 0; i < samples; i++)
{
var t = i / (float)samples;
var f = startFrequency > 0f ? Mathf.Lerp(startFrequency, frequency, t) : frequency;
phase += 2f * Mathf.PI * f / SampleRate;
// Attaque très courte pour éviter le claquement d'un début abrupt, puis
// décroissance exponentielle.
var envelope = Mathf.Min(1f, t * 40f) * Mathf.Exp(-5f * t);
data[i] = Mathf.Sin(phase) * envelope * volume;
}
var clip = AudioClip.Create(name, samples, 1, SampleRate, false);
clip.SetData(data, 0);
return clip;
}
public static void Hover() => Instance.Play(Instance._hover, 0.12f, 0.05f);
public static void Select() => Instance.Play(Instance._select, 0.5f, 0.12f);
void Play(AudioClip clip, float vibrationAmplitude, float vibrationSeconds)
{
if (clip != null) _source.PlayOneShot(clip);
var controller = OVRInput.GetActiveController();
if (controller != OVRInput.Controller.RTouch && controller != OVRInput.Controller.LTouch)
return;
StopAllCoroutines();
StartCoroutine(Vibrate(controller, vibrationAmplitude, vibrationSeconds));
}
IEnumerator Vibrate(OVRInput.Controller controller, float amplitude, float seconds)
{
OVRInput.SetControllerVibration(0.4f, amplitude, controller);
yield return new WaitForSeconds(seconds);
OVRInput.SetControllerVibration(0f, 0f, controller);
}
}
}