|
|
|
@@ -1,434 +0,0 @@
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
using Unheimlich.Core.Utils;
|
|
|
|
|
using Unheimlich.World.CameraSystem;
|
|
|
|
|
|
|
|
|
|
namespace Unheimlich.Core.Audio
|
|
|
|
|
{
|
|
|
|
|
public enum AudioEmitterType
|
|
|
|
|
{
|
|
|
|
|
Generic,
|
|
|
|
|
Wall,
|
|
|
|
|
Wood,
|
|
|
|
|
Metal,
|
|
|
|
|
Glass,
|
|
|
|
|
Water,
|
|
|
|
|
Stone,
|
|
|
|
|
Mechanical,
|
|
|
|
|
Electrical,
|
|
|
|
|
Organic
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class AudioListenable : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
// String constants
|
|
|
|
|
private const string CONTINUOUS_AUDIO_SOURCE_NAME = "Continuous Audio Source";
|
|
|
|
|
|
|
|
|
|
// Volume multipliers for concentration system
|
|
|
|
|
private const float BASELINE_VOLUME_MULTIPLIER = 0.1f;
|
|
|
|
|
private const float FULL_VOLUME_MULTIPLIER = 1.5f;
|
|
|
|
|
private const float CONCENTRATION_FADE_DURATION = 1f;
|
|
|
|
|
private const float RELEASE_FADE_DURATION = 0.5f;
|
|
|
|
|
|
|
|
|
|
[Header("Emitter Settings")]
|
|
|
|
|
[SerializeField] private AudioEmitterType _emitterType = AudioEmitterType.Generic;
|
|
|
|
|
[SerializeField] private bool _useProceduralAudio = true;
|
|
|
|
|
|
|
|
|
|
[Header("Continuous Emission")]
|
|
|
|
|
[SerializeField] private bool _emitContinuously = true;
|
|
|
|
|
[SerializeField] private float _continuousBaseVolume = 0.1f;
|
|
|
|
|
[SerializeField] private float _hearingRangeMin = 4f;
|
|
|
|
|
[SerializeField] private float _hearingRangeMax = 8f;
|
|
|
|
|
|
|
|
|
|
[Header("Listening Enhancement")]
|
|
|
|
|
[SerializeField] private float _reverbWetMix = 0.5f;
|
|
|
|
|
|
|
|
|
|
[Header("Static Audio")]
|
|
|
|
|
[SerializeField] private AudioClip _staticAudioClip;
|
|
|
|
|
[SerializeField] private bool _loopStaticAudio = true;
|
|
|
|
|
|
|
|
|
|
[Header("Procedural Settings")]
|
|
|
|
|
[SerializeField] private float _baseFrequency = 440f;
|
|
|
|
|
[SerializeField] private float _frequencyVariation = 50f;
|
|
|
|
|
[SerializeField] private float _volumeMin = 0.3f;
|
|
|
|
|
[SerializeField] private float _volumeMax = 0.7f;
|
|
|
|
|
[SerializeField] private float _durationMin = 0.5f;
|
|
|
|
|
[SerializeField] private float _durationMax = 2f;
|
|
|
|
|
|
|
|
|
|
[Header("Distance Attenuation")]
|
|
|
|
|
[SerializeField] private float _maxDistance = 10f;
|
|
|
|
|
[SerializeField] private AnimationCurve _distanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
|
|
|
|
|
|
|
|
|
|
[Header("Visual Feedback")]
|
|
|
|
|
[SerializeField] private ParticleSystem _emitterParticles;
|
|
|
|
|
|
|
|
|
|
[Header("Camera Settings")]
|
|
|
|
|
[SerializeField] private CameraPanSettings _cameraPanSettings;
|
|
|
|
|
|
|
|
|
|
private AudioSource _audioSource;
|
|
|
|
|
private AudioSource _continuousAudioSource;
|
|
|
|
|
private AudioReverbFilter _reverbFilter;
|
|
|
|
|
private bool _isCurrentlyListening;
|
|
|
|
|
private float _listeningTimer;
|
|
|
|
|
private float _nextProceduralEventTime;
|
|
|
|
|
private Vector3 _listenerPosition;
|
|
|
|
|
private bool _hasBeenTapped;
|
|
|
|
|
private AudioClip _continuousClip;
|
|
|
|
|
private Coroutine _volumeFadeCoroutine;
|
|
|
|
|
private Coroutine _startListeningCoroutine;
|
|
|
|
|
private int _concentrationFadeTweenId = -1;
|
|
|
|
|
private int _crossfadeTweenId = -1;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns true if this emitter has been tapped during the current listening session.
|
|
|
|
|
/// Used to prevent multiple taps on the same emitter.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool HasBeenTapped => _hasBeenTapped;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the camera pan settings for this emitter, or null if not set.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CameraPanSettings CameraPanSettings => _cameraPanSettings;
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
{
|
|
|
|
|
_audioSource = GetComponent<AudioSource>();
|
|
|
|
|
if (_audioSource == null)
|
|
|
|
|
_audioSource = gameObject.AddComponent<AudioSource>();
|
|
|
|
|
|
|
|
|
|
_audioSource.spatialBlend = 1f;
|
|
|
|
|
_audioSource.maxDistance = _maxDistance;
|
|
|
|
|
_audioSource.rolloffMode = AudioRolloffMode.Custom;
|
|
|
|
|
_audioSource.playOnAwake = false;
|
|
|
|
|
|
|
|
|
|
_reverbFilter = gameObject.AddComponent<AudioReverbFilter>();
|
|
|
|
|
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
|
|
|
|
|
_reverbFilter.dryLevel = 0f;
|
|
|
|
|
_reverbFilter.room = -10000;
|
|
|
|
|
|
|
|
|
|
GameObject continuousSourceObject = new GameObject(CONTINUOUS_AUDIO_SOURCE_NAME);
|
|
|
|
|
continuousSourceObject.transform.SetParent(transform);
|
|
|
|
|
continuousSourceObject.transform.localPosition = Vector3.zero;
|
|
|
|
|
_continuousAudioSource = continuousSourceObject.AddComponent<AudioSource>();
|
|
|
|
|
_continuousAudioSource.spatialBlend = 1f;
|
|
|
|
|
_continuousAudioSource.maxDistance = _hearingRangeMax;
|
|
|
|
|
_continuousAudioSource.minDistance = _hearingRangeMin;
|
|
|
|
|
_continuousAudioSource.rolloffMode = AudioRolloffMode.Linear;
|
|
|
|
|
_continuousAudioSource.playOnAwake = false;
|
|
|
|
|
_continuousAudioSource.loop = true;
|
|
|
|
|
|
|
|
|
|
if (_emitterParticles != null)
|
|
|
|
|
{
|
|
|
|
|
_emitterParticles.Stop();
|
|
|
|
|
var emission = _emitterParticles.emission;
|
|
|
|
|
emission.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Start()
|
|
|
|
|
{
|
|
|
|
|
if (_emitContinuously && _useProceduralAudio)
|
|
|
|
|
{
|
|
|
|
|
StartContinuousEmission();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StartContinuousEmission()
|
|
|
|
|
{
|
|
|
|
|
float frequency = _baseFrequency + Random.Range(-_frequencyVariation * 0.5f, _frequencyVariation * 0.5f);
|
|
|
|
|
_continuousClip = GenerateProceduralAudioClip(frequency, 10f);
|
|
|
|
|
|
|
|
|
|
_continuousAudioSource.clip = _continuousClip;
|
|
|
|
|
_continuousAudioSource.volume = _continuousBaseVolume * BASELINE_VOLUME_MULTIPLIER;
|
|
|
|
|
_continuousAudioSource.loop = true;
|
|
|
|
|
_continuousAudioSource.Play();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Sets the concentration state and fades the emitter volume accordingly.
|
|
|
|
|
/// When concentrating: fades from 30% to 100% over 1 second.
|
|
|
|
|
/// When releasing: fades from 100% to 30% over 0.5 seconds.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void SetConcentrating(bool concentrating)
|
|
|
|
|
{
|
|
|
|
|
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
|
|
|
|
|
|
|
|
|
|
if (_continuousAudioSource == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
float baseVolume = _continuousBaseVolume;
|
|
|
|
|
float fromMultiplier = concentrating ? BASELINE_VOLUME_MULTIPLIER : FULL_VOLUME_MULTIPLIER;
|
|
|
|
|
float toMultiplier = concentrating ? FULL_VOLUME_MULTIPLIER : BASELINE_VOLUME_MULTIPLIER;
|
|
|
|
|
float duration = concentrating ? CONCENTRATION_FADE_DURATION : RELEASE_FADE_DURATION;
|
|
|
|
|
|
|
|
|
|
float fromVolume = baseVolume * fromMultiplier;
|
|
|
|
|
float toVolume = baseVolume * toMultiplier;
|
|
|
|
|
|
|
|
|
|
_concentrationFadeTweenId = TweenHelper.FadeVolume(
|
|
|
|
|
_continuousAudioSource,
|
|
|
|
|
toVolume,
|
|
|
|
|
duration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Starts the listening sequence for this emitter.
|
|
|
|
|
/// Blocked if the emitter has already been tapped during this session.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void StartListening(Vector3 listenerPosition)
|
|
|
|
|
{
|
|
|
|
|
// Prevent multiple taps on the same emitter
|
|
|
|
|
if (_hasBeenTapped)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (_startListeningCoroutine != null)
|
|
|
|
|
StopCoroutine(_startListeningCoroutine);
|
|
|
|
|
|
|
|
|
|
_startListeningCoroutine = StartCoroutine(StartListeningSequence(listenerPosition));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Coroutine that handles the full listening sequence:
|
|
|
|
|
/// - Crossfades from continuous to procedural audio
|
|
|
|
|
/// - Activates particles
|
|
|
|
|
/// - Marks emitter as tapped (prevents re-triggering)
|
|
|
|
|
/// - Starts vibration
|
|
|
|
|
/// </summary>
|
|
|
|
|
private IEnumerator StartListeningSequence(Vector3 listenerPosition)
|
|
|
|
|
{
|
|
|
|
|
_isCurrentlyListening = true;
|
|
|
|
|
_listenerPosition = listenerPosition;
|
|
|
|
|
_listeningTimer = 0f;
|
|
|
|
|
_nextProceduralEventTime = 0f;
|
|
|
|
|
|
|
|
|
|
// Mark as tapped to prevent re-triggering
|
|
|
|
|
_hasBeenTapped = true;
|
|
|
|
|
|
|
|
|
|
if (_volumeFadeCoroutine != null)
|
|
|
|
|
StopCoroutine(_volumeFadeCoroutine);
|
|
|
|
|
|
|
|
|
|
TweenHelper.CancelTween(ref _crossfadeTweenId);
|
|
|
|
|
|
|
|
|
|
float crossfadeDuration = 1f;
|
|
|
|
|
|
|
|
|
|
if (_continuousAudioSource != null)
|
|
|
|
|
{
|
|
|
|
|
_crossfadeTweenId = TweenHelper.FadeVolume(
|
|
|
|
|
_continuousAudioSource,
|
|
|
|
|
0f,
|
|
|
|
|
crossfadeDuration,
|
|
|
|
|
() =>
|
|
|
|
|
{
|
|
|
|
|
if (_continuousAudioSource != null)
|
|
|
|
|
_continuousAudioSource.Stop();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_emitterParticles != null)
|
|
|
|
|
{
|
|
|
|
|
var emission = _emitterParticles.emission;
|
|
|
|
|
emission.enabled = true;
|
|
|
|
|
_emitterParticles.Play();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EnableReverb(true);
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(crossfadeDuration);
|
|
|
|
|
|
|
|
|
|
if (_useProceduralAudio)
|
|
|
|
|
{
|
|
|
|
|
PlayProceduralSound();
|
|
|
|
|
}
|
|
|
|
|
else if (_staticAudioClip != null)
|
|
|
|
|
{
|
|
|
|
|
_audioSource.clip = _staticAudioClip;
|
|
|
|
|
_audioSource.loop = _loopStaticAudio;
|
|
|
|
|
_audioSource.Play();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Stops the listening mode and resets the emitter state.
|
|
|
|
|
/// This is the only place where _hasBeenTapped is reset to false,
|
|
|
|
|
/// allowing the emitter to be used again in a future listening session.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void StopListening(bool cancelConcentrationFade = true)
|
|
|
|
|
{
|
|
|
|
|
bool wasBeenTapped = _hasBeenTapped;
|
|
|
|
|
|
|
|
|
|
_isCurrentlyListening = false;
|
|
|
|
|
_hasBeenTapped = false;
|
|
|
|
|
|
|
|
|
|
if (_volumeFadeCoroutine != null)
|
|
|
|
|
StopCoroutine(_volumeFadeCoroutine);
|
|
|
|
|
|
|
|
|
|
if (_startListeningCoroutine != null)
|
|
|
|
|
StopCoroutine(_startListeningCoroutine);
|
|
|
|
|
|
|
|
|
|
if (cancelConcentrationFade)
|
|
|
|
|
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
|
|
|
|
|
|
|
|
|
|
if (_audioSource != null && _audioSource.isPlaying)
|
|
|
|
|
_audioSource.Stop();
|
|
|
|
|
|
|
|
|
|
if (_emitterParticles != null && wasBeenTapped)
|
|
|
|
|
{
|
|
|
|
|
_emitterParticles.Stop();
|
|
|
|
|
var emission = _emitterParticles.emission;
|
|
|
|
|
emission.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && gameObject.activeInHierarchy)
|
|
|
|
|
{
|
|
|
|
|
StartContinuousEmission();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EnableReverb(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
if (!_isCurrentlyListening)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_listeningTimer += Time.deltaTime;
|
|
|
|
|
|
|
|
|
|
if (_useProceduralAudio && _listeningTimer >= _nextProceduralEventTime)
|
|
|
|
|
{
|
|
|
|
|
PlayProceduralSound();
|
|
|
|
|
_nextProceduralEventTime = _listeningTimer + Random.Range(_durationMin, _durationMax);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void EnableReverb(bool enable)
|
|
|
|
|
{
|
|
|
|
|
if (_reverbFilter == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (enable)
|
|
|
|
|
{
|
|
|
|
|
_reverbFilter.reverbPreset = AudioReverbPreset.Room;
|
|
|
|
|
_reverbFilter.dryLevel = Mathf.Lerp(0f, -1000f, 1f - _reverbWetMix);
|
|
|
|
|
_reverbFilter.room = Mathf.Lerp(-1000f, -100f, _reverbWetMix);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
|
|
|
|
|
_reverbFilter.dryLevel = 0f;
|
|
|
|
|
_reverbFilter.room = -10000;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PlayProceduralSound()
|
|
|
|
|
{
|
|
|
|
|
float distance = Vector3.Distance(_listenerPosition, transform.position);
|
|
|
|
|
float distanceFactor = Mathf.Clamp01(1f - (distance / _maxDistance));
|
|
|
|
|
float attenuation = _distanceAttenuation.Evaluate(distanceFactor);
|
|
|
|
|
|
|
|
|
|
float frequency = _baseFrequency + Random.Range(-_frequencyVariation, _frequencyVariation);
|
|
|
|
|
float volume = Mathf.Lerp(_volumeMin, _volumeMax, Random.value) * attenuation;
|
|
|
|
|
|
|
|
|
|
AudioClip proceduralClip = GenerateProceduralAudioClip(frequency, Random.Range(_durationMin, _durationMax));
|
|
|
|
|
|
|
|
|
|
_audioSource.clip = proceduralClip;
|
|
|
|
|
_audioSource.volume = volume;
|
|
|
|
|
_audioSource.loop = false;
|
|
|
|
|
_audioSource.Play();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AudioClip GenerateProceduralAudioClip(float frequency, float duration)
|
|
|
|
|
{
|
|
|
|
|
int sampleRate = 44100;
|
|
|
|
|
int sampleCount = Mathf.RoundToInt(sampleRate * duration);
|
|
|
|
|
|
|
|
|
|
AudioClip clip = AudioClip.Create("ProceduralSound", sampleCount, 1, sampleRate, false);
|
|
|
|
|
float[] samples = new float[sampleCount];
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < sampleCount; i++)
|
|
|
|
|
{
|
|
|
|
|
float t = (float)i / sampleRate;
|
|
|
|
|
float envelope = Mathf.Sin(Mathf.PI * t / duration);
|
|
|
|
|
|
|
|
|
|
samples[i] = GenerateSampleForType(frequency, t, _emitterType) * envelope;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clip.SetData(samples, 0);
|
|
|
|
|
return clip;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private float GenerateSampleForType(float frequency, float time, AudioEmitterType type)
|
|
|
|
|
{
|
|
|
|
|
switch (type)
|
|
|
|
|
{
|
|
|
|
|
case AudioEmitterType.Metal:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.5f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 2.1f * time) * 0.3f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 3.8f * time) * 0.2f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Wood:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.6f +
|
|
|
|
|
(Random.value - 0.5f) * 0.1f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Glass:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * 1.5f * time) * 0.4f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 2.7f * time) * 0.3f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 4.2f * time) * 0.3f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Stone:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * 0.8f * time) * 0.5f +
|
|
|
|
|
(Random.value - 0.5f) * 0.3f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Water:
|
|
|
|
|
return (Random.value - 0.5f) * 0.5f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Mechanical:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.4f +
|
|
|
|
|
Mathf.Sign(Mathf.Sin(2f * Mathf.PI * frequency * 0.3f * time)) * 0.3f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Electrical:
|
|
|
|
|
return (Random.value - 0.5f) * 0.6f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 50f * time) * 0.2f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Organic:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * 0.7f * time) * 0.5f +
|
|
|
|
|
Mathf.Sin(2f * Mathf.PI * frequency * 1.3f * time) * 0.3f +
|
|
|
|
|
(Random.value - 0.5f) * 0.2f;
|
|
|
|
|
|
|
|
|
|
case AudioEmitterType.Wall:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f +
|
|
|
|
|
(Random.value - 0.5f) * 0.1f;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return Mathf.Sin(2f * Mathf.PI * frequency * time);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
{
|
|
|
|
|
StopListening();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
{
|
|
|
|
|
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && _emitContinuously)
|
|
|
|
|
{
|
|
|
|
|
StartContinuousEmission();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDrawGizmosSelected()
|
|
|
|
|
{
|
|
|
|
|
Gizmos.color = Color.cyan;
|
|
|
|
|
Gizmos.DrawWireSphere(transform.position, _maxDistance);
|
|
|
|
|
|
|
|
|
|
if (_isCurrentlyListening)
|
|
|
|
|
{
|
|
|
|
|
Gizmos.color = Color.green;
|
|
|
|
|
Gizmos.DrawLine(transform.position, _listenerPosition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|