using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using Yarn.Unity; #nullable enable namespace Yarn.Unity.Addons.Snaaake { public class TalkingHeadDialoguePresenter : Yarn.Unity.DialoguePresenterBase { [Header("Audio")] /// /// The audio source to monitor. /// public AudioSource? audioSource; /// /// The channel in to monitor. /// public int sampleChannel = 0; /// /// A dictionary for mapping names to animators that present the characters, /// public Yarn.Unity.SerializableDictionary characterSlots = new(); public Yarn.Unity.SerializableDictionary characters = new(); [Range(0f, 1f)] public float threshold = 0.5f; /// /// The current animator whose parameters we are changing. /// private TalkingHeadCharacterView? activeView = null; /// /// A buffer containing audio samples collected from . /// /// /// (This is kept in the class because we want to reuse the same buffer, /// rather than allocate every time we want to sample it.) /// private float[] sampleBuffer = new float[512]; /// /// The amount of time, in milliseconds, between samples of the audio source. /// /// /// We don't need to be super precise with monitoring the audio source, so /// there's no need to measure every frame. Instead, we'll measure at a fix /// number of milliseconds between samples. /// private readonly int timeBetweenSamplesMilliseconds = 50; /// /// The amount of time remaining until we next sample. /// private float timeUntilNextSample = 0; protected void Start() { if (audioSource) { // Unity creates the internal buffers and starts recording the first // time this is called, so call it immediately on start audioSource.GetOutputData(sampleBuffer, sampleChannel); } } protected void Update() { // Ensure that we have the components that we need if (audioSource == null || activeView == null) { return; } // Are we due for another audio source sample? If not, wait. if (timeUntilNextSample > 0) { timeUntilNextSample -= Time.deltaTime; return; } // Read sample data out of the audio source. audioSource.GetOutputData(sampleBuffer, sampleChannel); // Determine the highest peak level that we hit. var maxPower = 0f; for (int i = 0; i < sampleBuffer.Length; i++) { maxPower = Mathf.Max(Mathf.Abs(maxPower), sampleBuffer[i]); } // Set the parameter to whether or not we have a loud enough signal! activeView.Talking = maxPower > threshold; // Reset our clock. timeUntilNextSample = timeBetweenSamplesMilliseconds / 1000f; } /// /// Associates a character name with a slot. /// /// The character name to associate. /// The name of the slot to associate the character /// with. [YarnCommand("set_slot")] public static void SetCharacterToSlot(string characterName, string slotName) { var presenter = FindAnyObjectByType(); if (presenter == null) { Debug.LogWarning($"Can't associate {characterName} with slot {slotName}: no {nameof(TalkingHeadDialoguePresenter)}"); return; } if (presenter.characters.TryGetValue(characterName, out var characterData) == false) { Debug.LogWarning($"Can't associate {characterName} with slot {slotName}: no known character {characterName}"); return; } if (presenter.characterSlots.TryGetValue(slotName, out var slot) == false) { Debug.LogWarning($"Can't associate {characterName} with slot {slotName}: no slot named {slotName}"); return; } slot.CurrentCharacter = characterData; } public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token) { // Ensure that we have a character, a slot name for that character, and // an animator for that slot name. var characterName = line.CharacterName; if (characterName == null) { Debug.LogWarning($"Can't run talking animation for line {line.TextID} '{line.Text}': line has no character name"); return; } if (characters.TryGetValue(characterName, out var character) == false) { Debug.LogWarning($"Can't run talking animation for line {line.TextID} '{line.Text}': {characterName} is not a known character"); return; } TalkingHeadCharacterView? slot = null; // Find the slot that contains this character foreach (var s in characterSlots.Values) { if (s.CurrentCharacter == character) { slot = s; break; } } if (slot == null) { // We don't know which slot is showing this character Debug.LogWarning($"Can't run talking animation for line {line.TextID} '{line.Text}': {characterName} is not associated with a slot"); return; } // The animator we're working with is this one we've arrived at activeView = slot; // Wait until the line is finished await YarnTask.WaitUntilCanceled(token.NextContentToken); if (slot != null) { // Turn off the animation just in case the audio level is currently // over the threshold slot.Talking = false; } // Tidy up activeView = null; } public override YarnTask OnDialogueStartedAsync() { // Clear all characters from their slots foreach (var kv in characterSlots) { kv.Value.CurrentCharacter = null; } return YarnTask.CompletedTask; } public override YarnTask OnDialogueCompleteAsync() { return YarnTask.CompletedTask; } } }