EchoTrio
 
Loading...
Searching...
No Matches
VoiceChat.cs
Go to the documentation of this file.
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using GameEvent;
6using Microsoft.Extensions.Configuration;
7using UnityEngine;
8using static EchoTrio.ActorConfig;
9
10namespace EchoTrio {
11 /// Voice chat system that acts as an intermediary between the human user and the AI models.
12 /// The system works using the concept of rounds. During every round, a few things may happen.
13 /// - A scripted discussion is triggered at the start of the round specified by the designer where the actors speak scripted lines. No user input is allowed in this round. OR
14 /// - A generated discussion is triggered at the start of the round specified by the designer where the actors speak AI generated lines. No user input is allowed in this round. OR
15 /// - User input is accepted and the actors generates a reply. OR
16 /// - User input is accepted and it triggers a scripted or generated discussion as the reply from the actors if the user mentions certain topics. OR
17 /// - User input is allowed by the user does not provide any input. A scripted or generated dicussion is triggered after some time as specified by the designer.
18 public class VoiceChat : MonoBehaviour {
19 private const string OverrideFileName = "GameOverrides.ini";
20
21 /// All the possible states of the system.
22 private enum State {
23 Invalid = -1,
24 /// Idle while waiting for player to start the game.
25 Idle,
26 /// Before we start each round, we enter the Prepare stage, and make a decision of which state to enter for the round.
27 Prepare,
28 /// If user input is allowed this round, enter the Wait stage to wait for the actors to finish speaking, and ensure that the director has connected to OpenAI's server.
29 Wait,
30 /// Listen for user input.
31 Listen,
32 /// Actors responding to user input as per normal AI conversations.
34 /// Actors playing a scripted discussion, or generating a discussion on a topic.
35 Discuss,
36 /// The chat has ended.
37 Finish,
38 Num
39 }
40
41 /// Reference to an actor configuration, and the AudioSource it should play it's output audio from.
42 [System.Serializable] public class ActorReferences {
43 /// The configuration of the actor.
45 /// The audio source to output the speech of the actor.
46 public AudioSource audioSource;
47 /// The listening icon of the actor.
49 }
50
51 /// Output of actors to be queued and played by the audio thread.
52 private class ActorOutput {
53 public string persona;
54 public string message;
55 public Emotion emotion = Emotion.Neutral;
56 public List<string> reasonings;
57 public AudioClip audioClip;
58 public AudioSource audioSource;
59
60 public ActorOutput(string persona, string message, Emotion emotion, List<string> reasonings, AudioClip audioClip, AudioSource audioSource) {
61 this.persona = persona;
62 this.message = message;
63 this.emotion = emotion;
64 this.reasonings = reasonings;
65 this.audioClip = audioClip;
66 this.audioSource = audioSource;
67 }
68 }
69
70 [Header("GUI References")]
71 [SerializeField] private EchoTrio.UI.Chatbox chatbox = null;
72 [SerializeField] private TMPro.TextMeshProUGUI roundCounterText = null;
73 [SerializeField] private TMPro.TextMeshProUGUI idleTimerText = null;
74
75 [Header("Configurations")]
76 [SerializeField] private DirectorConfig directorConfig = null;
77 [SerializeField] private ActorReferences[] actorReferences = new ActorReferences[0]; // Designed as an array to be somewhat scalable so that in theory we could easily support 1 human user, multiple AI actors. But that's beyond the scope of this project.
78
79 [Header("Discussions")]
80 [SerializeField] private Discussion[] discussions = new Discussion[0];
81
82 [Header("Voice Chat Settings")]
83 [SerializeField, Range(1, 100), Tooltip("How many rounds to play before the chat ends.")] private int finishRound = 10;
84
85 [Header("Debug")]
86 [SerializeField] private bool enableDebug = true;
87 [SerializeField] private bool showReasoning = true;
88
89 // Internal Variables
90 private GameInputActions gameInputActions = null;
92 private Dictionary<string, (Actor, AudioSource)> actors = new Dictionary<string, (Actor, AudioSource)>();
93 private Director director = null;
94 private int roundCounter = 0;
95 private float idleTimer = 0.0f;
96 private bool continueChat = false;
97
98 // Audio
99 private bool isAudioPlaying = false;
100 private bool isQueueingAudio = false;
101 private Queue<ActorOutput> actorOutputQueue = new Queue<ActorOutput>();
102 private Dictionary<AudioClip, bool> audioClipGarbageCollector = new Dictionary<AudioClip, bool>();
103
104 // Discuss Variables
105 private List<Discussion> untriggeredDiscussions = null;
106 private Queue<Discussion> discussionQueue = new Queue<Discussion>();
107
108 // Speak Variables
109 private Queue<string> speakerQueue = new Queue<string>();
110
111 // Public Interfaces
112 /// Toggle the director's microphone on or off.
113 public void ToggleMicMute() {
114 director.IsMicMuted = !director.IsMicMuted;
115 if (director.IsMicMuted) { idleTimer = 0.0f; }
116 }
117
118 /// Toggle the chatbox to be active or inactive.
119 public void ToggleChatbox() {
120 chatbox.gameObject.SetActive(!chatbox.gameObject.activeSelf);
121 chatbox.ScrollToBottom();
122 }
123
124 /// Reset the idle timer, usually invoked whenever there's any input by the user, such as typing something into the chatbox, or unmuting the microphone.
125 public void ResetIdleTimer() { idleTimer = 0.0f; }
126
127 /// <summary>
128 /// Submit the user text input. Used as an alternative to speaking into the microphone, usually for development & debugging purposes.
129 /// </summary>
130 /// <param name="message">The user text input.</param>
131 /// <returns>Returns true if the voice chat system is currently accepting user input. Else, returns false.</returns>
132 public async Awaitable<bool> SubmitUserTextInput(string message) {
133 return fsm.GetCurrentState() == (int)State.Listen && await director.SubmitUserTextInput(message, destroyCancellationToken);
134 }
135
136 public int GetRoundCounter() { return roundCounter; }
137
138 public int GetFinishRound() { return finishRound; }
139
140 // Internal Functions
141 private void Awake() {
142 // Read config file and override.
143 Override();
144
145 // Initialise Input
146 gameInputActions = new GameInputActions();
147
148 // Copy discussions into a list.
149 untriggeredDiscussions = new List<Discussion>(discussions);
150
151 // Create actors.
152 foreach (ActorReferences actorRef in actorReferences) {
153 Actor actor = new Actor(actorRef.actorConfig) { EnableDebug = enableDebug };
154 AudioSource audioSource = actorRef.audioSource;
155 actors.Add(actor.Persona, (actor, audioSource));
156 }
157
158 // Create director. Has to be after actors.
159 director = new Director() { IsMicMuted = true, EnableDebug = enableDebug };
160
161 // Initialise Finite State Machine
162 fsm.SetStateEntry((int)State.Idle, OnEnterIdle);
163 fsm.SetStateEntry((int)State.Prepare, OnEnterPrepare);
164 fsm.SetStateEntry((int)State.Wait, OnEnterWait);
165 fsm.SetStateUpdate((int)State.Wait, OnUpdateWait);
166 fsm.SetStateEntry((int)State.Listen, OnEnterListen);
167 fsm.SetStateUpdate((int)State.Listen, OnUpdateListen);
168 fsm.SetStateEntry((int)State.FreeTalk, OnEnterFreeTalk);
169 fsm.SetStateEntry((int)State.Discuss, OnEnterDiscuss);
170 fsm.SetStateEntry((int)State.Finish, OnEnterFinish);
171 }
172
173 private void OnDestroy() {
174 // Ensure that the all audio clips are destroyed.
176 foreach (var kv in audioClipGarbageCollector) {
177 Destroy(kv.Key);
178 }
180 }
181 }
182
183 private void OnEnable() {
184 // Enable input actions.
185 gameInputActions.Enable();
186 gameInputActions.VoiceChat.PushToTalk.started += OnPushToTalkStarted;
187 gameInputActions.VoiceChat.PushToTalk.canceled += OnPushToTalkCancelled;
188
189 // Subcribe to game events.
192 }
193
194 private void OnDisable() {
195 // Disable input actions.
196 gameInputActions.Disable();
197 gameInputActions.VoiceChat.PushToTalk.started -= OnPushToTalkStarted;
198 gameInputActions.VoiceChat.PushToTalk.canceled -= OnPushToTalkCancelled;
199
200 // Unsubcribe from game events.
203 }
204
205 private void Start() {
206 StartCoroutine(AudioThread()); // Launch a thread to play queued audio.
207 director.Initialise(OnDirectorResponse, destroyCancellationToken); // Tell the director to connect to OpenAI's server.
208 fsm.ChangeState((int)State.Idle); // Start off the voice chat system in the "Idle" state.
209 }
210
211 private void Update() {
212 // Update the finite state machine.
213 fsm.Update();
214
215 // Update GUI.
216 for (int i = 0; i < actorReferences.Length; ++i) {
217 if (actorReferences[i] != null && actorReferences[i].listeningIcon != null) {
218 actorReferences[i].listeningIcon.SetSprite(director.IsStatus(Director.Status.Listening) ? 1 : 0);
219 }
220 }
221
222 // Cleanup finished audio clips.
224 foreach (var kv in audioClipGarbageCollector) {
225 if (kv.Value && kv.Key != null) { Destroy(kv.Key); }
226 }
227 audioClipGarbageCollector = audioClipGarbageCollector.Where(kv => !kv.Value).ToDictionary(kv => kv.Key, kv => kv.Value);
228 }
229 }
230
231 private void LateUpdate() {
232 // Late update the finite state machine.
233 fsm.LateUpdate();
234 }
235
236 /// <summary>
237 /// Queue up an actor's output to be played by the audio thread.
238 /// </summary>
239 /// <param name="actorOutput">The actor output to queue.</param>
240 private void QueueActorOutput(ActorOutput actorOutput) {
241 // Flag that audio has started playing.
242 // Even though it doesn't actually play until the audio thread plays it, this flag has to be set here and not in the audio thread to ensure that the main thread doesn't start listening to user input again.
243 // Because in theory it can be a few frames before the audio thread gets to it and we don't want to accidentally trigger listening for user input in the mean time.
244 isAudioPlaying = true;
245
246 // Ensure that the audio clip is added to the garbage collector before being queued.
247 // This is so that it is impossible for the audio thread to mark a clip as finished and putting it into the garbage collector before we do it here, causing a double insert.
248 // Because the audio thread and the main thread runs concurrently.
249 if (actorOutput.audioClip != null) {
251 audioClipGarbageCollector.Add(actorOutput.audioClip, false);
252 }
253 }
254
255 // Actually queue the actor output to be played.
256 lock (actorOutputQueue) {
257 actorOutputQueue.Enqueue(actorOutput);
258 }
259 }
260
261 /// <summary>
262 /// Launch an audio thread via Coroutine to play any queued audio from the actors.
263 /// </summary>
264 /// <returns>An IEnumerator for Coroutine.</returns>
265 private IEnumerator AudioThread() {
266 // We do not play the audio in the main thread because it is an infinite loop and we don't want to hang the main thread.
267 Debug.Log("Starting audio thread...");
268
269 // Run this loop until the MonoBehaviour is destroyed.
270 AudioSource playingAudioSource = null;
271 while (!destroyCancellationToken.IsCancellationRequested) {
272 // If there is already an audio clip playing, wait for it to be done.
273 if (playingAudioSource != null && playingAudioSource.isPlaying) {
274 yield return null;
275 continue;
276 }
277
278 // Mark this audio clip as finished and ready to be deleted.
279 if (playingAudioSource != null && playingAudioSource.clip != null) {
281 audioClipGarbageCollector[playingAudioSource.clip] = true;
282 }
283 }
284
285 playingAudioSource = null;
286
287 // If no audio clip is currently playing, try to see if there's any audio clip in the queue.
288 bool hasOutput = false;
289 ActorOutput actorOutput = null;
290 lock (actorOutputQueue) {
291 hasOutput = actorOutputQueue.TryDequeue(out actorOutput);
292 }
293
294 // If there is an audio clip, play it.
295 if (hasOutput) {
296 playingAudioSource = actorOutput.audioSource;
297 playingAudioSource.clip = actorOutput.audioClip;
298 if (actorOutput.audioSource != null && actorOutput.audioClip != null) {
299 playingAudioSource.Play();
300 }
301
302 Debug.Log($"{actorOutput.persona} is {actorOutput.emotion.ToString()}");
303 chatbox.AddMessage(actorOutput.persona, actorOutput.message);
304 if (showReasoning) {
305 for (int i = 0; i < actorOutput.reasonings.Count; ++i) {
306 chatbox.AddMessage(actorOutput.persona + $"'s Reasoning {i + 1}", actorOutput.reasonings[i]);
307 }
308 }
309 }
310 // Otherwise, if there's no more audio clips in the queue, and the main thread isn't queuing any more audio clips, reset the isAudioPlaying flag.
311 else if (!isQueueingAudio) {
312 isAudioPlaying = false;
313 }
314
315 // Wait for next frame to save some CPU cycles.
316 // It's unlikely for an audio clip to finish playing in 1 frame so there's no point checking the if the audio source is done playing.
317 // No point running this loop a bajillion times per frame.
318 yield return null;
319 }
320
321 Debug.Log("Shutting down audio thread...");
322 }
323
324 /// <summary>
325 /// Send the message received from one actor, to all the other actors.
326 /// </summary>
327 /// <param name="speaker">The actor that the message was received from.</param>
328 /// <param name="message">The actor's message.</param>
329 private void PropogateActorMessage(Actor speaker, string message) {
330 foreach (var item in actors) {
331 Actor actor = item.Value.Item1;
332 if (actor == speaker) continue;
333 actor.AddUserMessage("@" + speaker.Persona + " " + message);
334 }
335 }
336
337 /// <summary>
338 /// Send the message received from the user to all the actors.
339 /// </summary>
340 /// <param name="message">The user's message.</param>
341 private void PropogateUserMessage(string message) {
342 foreach (var item in actors) {
343 Actor actor = item.Value.Item1;
344 actor.AddUserMessage("@User " + message);
345 }
346 }
347
348 /// Flag that audio is being queued.
349 private void BeginQueuingAudio() {
350 // We can use a boolean because audio is only being queued by the main thread.
351 // If there ever comes a time where it is possible for multiple threads to queue the audio concurrently, change this to a mutex protected integer increment everything a thread begins queuing audio.
352 isQueueingAudio = true;
353 }
354
355 /// Flag that audio is not queued.
356 private void EndQueuingAudio() {
357 // We can use a boolean because audio is only being queued by the main thread.
358 // If there ever comes a time where it is possible for multiple threads to queue the audio concurrently, change this to a mutex protected integer decrement everything a thread begins queuing audio.
359 isQueueingAudio = false;
360 }
361
362 // Idle State
363 private void OnEnterIdle() { Debug.Log("VoiceChat: OnEnterIdle"); }
364
365 // Prepare State
366 private void OnEnterPrepare() {
367 Debug.Log("VoiceChat: OnEnterPrepare");
368
369 // Increase round counter && update GUI.
370 ++roundCounter;
371 if (roundCounterText != null) { roundCounterText.text = $"Round {roundCounter}"; }
372
373 // If there is a discussion to be triggered, trigger it instead of asking the director to listen.
374 for (int i = 0; i < untriggeredDiscussions.Count; ++i) {
375 Discussion discussion = untriggeredDiscussions[i];
376 if (discussion.HasAllTriggerModes(Discussion.TriggerMode.Round) &&
377 discussion.GetTriggerRound() <= roundCounter) {
378 discussionQueue.Enqueue(discussion);
379 untriggeredDiscussions.RemoveAt(i);
380 fsm.ChangeState((int)State.Discuss);
381 return;
382 }
383 }
384
385 // Otherwise, wait for the director to be ready to listen for user input.
386 fsm.ChangeState((int)State.Wait);
387 }
388
389 // Wait State
390 private void OnEnterWait() { Debug.Log("VoiceChat: OnEnterWait"); }
391
392 private void OnUpdateWait() {
393 if (!isAudioPlaying) {
394 // If no audio is playing and the game should finish, and we did not trigger continue chat, go to the finish state.
396 fsm.ChangeState((int)State.Finish);
397 }
398 // If the director is connected to OpenAI's server and no audio is playing, listen for user input.
399 else if (director.IsConnected && director.IsStatus(Director.Status.Waiting)) {
400 fsm.ChangeState((int)State.Listen);
401 }
402 }
403 }
404
405 // Listen State
406 private void OnEnterListen() {
407 Debug.Log("VoiceChat: OnEnterListen");
408
409 idleTimer = 0.0f;
410 discussionQueue.Clear();
411 speakerQueue.Clear();
412
413 // Let the director know the name of the actors so that it can determine the speaking order when replying to the user.
414 List<string> speakers = actors.Keys.ToList();
415
416 // Let the director know which discussions it can trigger based on topic.
417 List<string> topics = untriggeredDiscussions.
418 Where(d => d.HasAllTriggerModes(Discussion.TriggerMode.Topic)).
419 Select(d => d.GetTriggerTopic()).ToList();
420
421 // Get the direction to listen for user input.
422 director.ListenForNextUserInput(directorConfig, speakers, topics, destroyCancellationToken);
423 }
424
425 private void OnUpdateListen() {
426 // Update Idle Timer
427 if (director.IsMicMuted) { idleTimer += Time.deltaTime; }
428 if (idleTimerText != null) { idleTimerText.text = $"Idle Time: {idleTimer.ToString("n2")}s"; }
429
430 // Check if we should trigger any idle discussions if the user has not given any input after a while. This ends the round.
431 for (int i = 0; i < untriggeredDiscussions.Count; ++i) {
432 Discussion discussion = untriggeredDiscussions[i];
433 // Check if we should trigger this idle discussion.
434 if (discussion.HasAllTriggerModes(Discussion.TriggerMode.IdleTime) && discussion.GetTriggerIdleTime() <= idleTimer) {
435 // If the director has already stopped listening, abort.
436 if (!director.CancelListen()) {
437 Debug.Log("Aborting idle discussion as director has already started responding.");
438 break;
439 }
440
441 // Else, trigger idle discussion.
442 discussionQueue.Enqueue(discussion);
443 untriggeredDiscussions.RemoveAt(i);
444 fsm.ChangeState((int)State.Discuss);
445 return;
446 }
447 }
448 }
449
450 // FreeTalk State
451 private async void RunFreeTalk() {
453
454 // Get a response from every actor.
455 while (0 < speakerQueue.Count) {
456 var (actor, audioSource) = actors.GetValueOrDefault(speakerQueue.Dequeue(), (null, null));
457 if (actor == null || audioSource == null) {
458 Debug.LogWarning("Actor or AudioSource is null!");
459 continue;
460 }
461
462 Actor.Response actorResponse = await actor.GetResponse(destroyCancellationToken);
463 QueueActorOutput(new ActorOutput(actor.Persona, actorResponse.message, actorResponse.emotion, actorResponse.reasonings, actorResponse.audioClip, audioSource));
464 PropogateActorMessage(actor, actorResponse.message);
465 }
466
468 fsm.ChangeState((int)State.Prepare);
469 }
470
471 private void OnEnterFreeTalk() {
472 Debug.Log("VoiceChat: OnEnterFreeTalk");
473 RunFreeTalk(); // Run the logic asynchronously so that it does not hang the main thread.
474 }
475
476 // Discussion State
477 private async void RunScriptedDiscussion(ScriptedDiscussion discussion) {
479
480 foreach (ScriptedDiscussion.Dialogue dialogue in discussion.GetDialogues()) {
481 var (actor, audioSource) = actors.GetValueOrDefault(dialogue.speaker.ToString(), (null, null));
482 if (actor == null || audioSource == null) {
483 Debug.LogWarning("Actor or AudioSource is null!");
484 continue;
485 }
486
487 Actor.Response actorResponse = await actor.InsertResponse(dialogue.message, dialogue.emotion, destroyCancellationToken);
488 QueueActorOutput(new ActorOutput(actor.Persona, actorResponse.message, actorResponse.emotion, actorResponse.reasonings, actorResponse.audioClip, audioSource));
489 PropogateActorMessage(actor, actorResponse.message);
490 }
491
493 fsm.ChangeState((int)State.Prepare);
494 }
495
496 private async void RunGeneratedDiscussion(GeneratedDiscussion discussion) {
498
499 List<Persona> speakers = discussion.GenerateRandomSpeakerOrder();
500 foreach (Persona speaker in speakers) {
501 var (actor, audioSource) = actors.GetValueOrDefault(speaker.ToString(), (null, null));
502 if (actor == null || audioSource == null) {
503 Debug.LogWarning("Actor or AudioSource is null!");
504 continue;
505 }
506
507 actor.AddSystemMesssage(discussion.GetDiscussionPrompt());
508 Actor.Response actorResponse = await actor.GetResponse(destroyCancellationToken);
509 QueueActorOutput(new ActorOutput(actor.Persona, actorResponse.message, actorResponse.emotion, actorResponse.reasonings, actorResponse.audioClip, audioSource));
510 PropogateActorMessage(actor, actorResponse.message);
511 }
512
514 fsm.ChangeState((int)State.Prepare);
515 }
516
517 private void OnEnterDiscuss() {
518 Debug.Log("VoiceChat: OnEnterDiscussion");
519 Discussion discussion = discussionQueue.Dequeue();
520
521 // Run the logic asynchronously so that it does not hang the main thread.
522 switch (discussion) {
524 Debug.Log("VoiceChat: Starting scripted discussion...");
526 break;
528 Debug.Log("VoiceChat: Starting generated discussion...");
530 break;
531 default:
532 throw new System.NotImplementedException();
533 }
534 }
535
536 // Finish State
537 private void OnEnterFinish() {
538 Debug.Log("VoiceChat: OnEnterFinish");
539
540 // Let all listeners know that the game has finished.
542 }
543
544 // Game Event Callbacks
545 private void OnGameStart() {
546 if (fsm.GetCurrentState() == (int)State.Idle) {
547 fsm.ChangeState((int)State.Prepare);
548 }
549 }
550
551 private void OnGameContinue() {
552 if (fsm.GetCurrentState() == (int)State.Finish) {
553 continueChat = true;
554 fsm.ChangeState((int)State.Wait);
555 }
556 }
557
558 // Input Callbacks
559 private void OnPushToTalkStarted(UnityEngine.InputSystem.InputAction.CallbackContext context) { ToggleMicMute(); }
560
561 private void OnPushToTalkCancelled(UnityEngine.InputSystem.InputAction.CallbackContext context) { ToggleMicMute(); }
562
563 /// Callback invoked by the director when it has a response ready.
564 /// <param name="response">The director's response.</param>
565 private void OnDirectorResponse(Director.Response response) {
566 // We only care about a response when we are in the listening state.
567 if (fsm.GetCurrentState() != (int)State.Listen) { return; }
568
569 // Add the user transcript to the chatbox.
570 chatbox.AddMessage("User", response.userTranscript);
571 // Inform each actor of what the user said.
572 PropogateUserMessage(response.userTranscript);
573
574 // Try to trigger a discussion.
575 if (response.discussionTopic != null) {
576 for (int i = 0; i < untriggeredDiscussions.Count; ++i) {
577 Discussion discussion = untriggeredDiscussions[i];
578 if (!string.IsNullOrEmpty(discussion.GetTriggerTopic()) &&
579 discussion.GetTriggerTopic() == response.discussionTopic) {
580 discussionQueue.Enqueue(discussion);
581 untriggeredDiscussions.RemoveAt(i);
582 fsm.ChangeState((int)State.Discuss);
583 return;
584 }
585 }
586 }
587
588 // Otherwise, get the actors to respond as per usual.
589 if (response.speakerOrder != null) {
590 foreach (string speaker in response.speakerOrder) {
591 speakerQueue.Enqueue(speaker);
592 }
593 fsm.ChangeState((int)State.FreeTalk);
594 return;
595 }
596
597 // As a backup, just respond in order.
598 foreach (var key in actors.Keys) {
599 speakerQueue.Enqueue(key);
600 fsm.ChangeState((int)State.FreeTalk);
601 Debug.Log("Director decision failed. Applying backup.");
602 }
603 }
604
605 // Configuration File Override
606 private void Override() {
607 string filePath = $"{Application.streamingAssetsPath}/Configs/{OverrideFileName}";
608 IConfiguration config = new ConfigurationBuilder().AddIniFile(filePath).Build();
609 IConfigurationSection section = config.GetSection("VoiceChat");
610
611 // We cannot proceed if the section does not exist.
612 if (section == null) {
613 Debug.LogWarning($"Section VoiceChat not found in {filePath}!");
614 return;
615 }
616
617 // Define a helper function.
618 Func<string, string> GetValue = (string key) => { return section[key] == null ? string.Empty : section[key].Trim(); };
619 string value = string.Empty;
620 int parsedInt = 0;
621
622 // Override OpenAI Vector Store ID
623 value = GetValue("finish_round");
624 if (!string.IsNullOrEmpty(value) && int.TryParse(value, out parsedInt)) {
625 finishRound = parsedInt;
626 Debug.Log($"Overrode VoiceChat's Finish Round to {finishRound}");
627 }
628 }
629 }
630}
AudioClip audioClip
Definition: Actor.cs:20
List< string > reasonings
Definition: Actor.cs:21
The actors are the OpenAI Response model which chats with the user.
Definition: Actor.cs:15
string Persona
Definition: Actor.cs:76
void AddUserMessage(string message)
Definition: Actor.cs:104
void Initialise(UnityAction< Director.Response > onDirectorResponse, CancellationToken cancellationToken)
Definition: Director.cs:74
bool IsStatus(Status value)
Definition: Director.cs:104
bool CancelListen()
Definition: Director.cs:132
async Awaitable< bool > SubmitUserTextInput(string message, CancellationToken cancellationToken)
Definition: Director.cs:145
async void ListenForNextUserInput(DirectorConfig config, List< string > speakers, List< string > topics, CancellationToken cancellationToken)
Definition: Director.cs:112
Status
Current status of the director.
Definition: Director.cs:24
Discussions are a way for the designers to create a way for the actors to interact beyond the standar...
Definition: Discussion.cs:5
TriggerMode
Ways that a discussion can be triggered.
Definition: Discussion.cs:7
bool HasAllTriggerModes(TriggerMode modes)
Definition: Discussion.cs:22
float GetTriggerIdleTime()
Definition: Discussion.cs:27
string GetTriggerTopic()
Definition: Discussion.cs:25
GeneratedDiscussions allow the designer to request the actors to generate N responses based on a prom...
List< Persona > GenerateRandomSpeakerOrder()
ScriptedDiscussions allow the designer to make the actors speak scripted lines.
List< Dialogue > GetDialogues()
Output of actors to be queued and played by the audio thread.
Definition: VoiceChat.cs:52
ActorOutput(string persona, string message, Emotion emotion, List< string > reasonings, AudioClip audioClip, AudioSource audioSource)
Definition: VoiceChat.cs:60
Reference to an actor configuration, and the AudioSource it should play it's output audio from.
Definition: VoiceChat.cs:42
AudioSource audioSource
The audio source to output the speech of the actor.
Definition: VoiceChat.cs:46
ActorConfig actorConfig
The configuration of the actor.
Definition: VoiceChat.cs:44
EchoTrio.UI.SpriteSwitcher listeningIcon
The listening icon of the actor.
Definition: VoiceChat.cs:48
IEnumerator AudioThread()
Launch an audio thread via Coroutine to play any queued audio from the actors.
Definition: VoiceChat.cs:265
Discussion[] discussions
Definition: VoiceChat.cs:80
Queue< ActorOutput > actorOutputQueue
Definition: VoiceChat.cs:101
void QueueActorOutput(ActorOutput actorOutput)
Queue up an actor's output to be played by the audio thread.
Definition: VoiceChat.cs:240
EchoTrio.UI.Chatbox chatbox
Definition: VoiceChat.cs:71
Director director
Definition: VoiceChat.cs:93
void OnEnterListen()
Definition: VoiceChat.cs:406
void EndQueuingAudio()
Flag that audio is not queued.
Definition: VoiceChat.cs:356
GameInputActions gameInputActions
Definition: VoiceChat.cs:90
void PropogateActorMessage(Actor speaker, string message)
Send the message received from one actor, to all the other actors.
Definition: VoiceChat.cs:329
DirectorConfig directorConfig
Definition: VoiceChat.cs:76
List< Discussion > untriggeredDiscussions
Definition: VoiceChat.cs:105
const string OverrideFileName
Definition: VoiceChat.cs:19
async Awaitable< bool > SubmitUserTextInput(string message)
Submit the user text input. Used as an alternative to speaking into the microphone,...
Definition: VoiceChat.cs:132
Queue< Discussion > discussionQueue
Definition: VoiceChat.cs:106
void OnPushToTalkStarted(UnityEngine.InputSystem.InputAction.CallbackContext context)
Definition: VoiceChat.cs:559
void OnEnterPrepare()
Definition: VoiceChat.cs:366
void ToggleChatbox()
Toggle the chatbox to be active or inactive.
Definition: VoiceChat.cs:119
void OnGameContinue()
Definition: VoiceChat.cs:551
TMPro.TextMeshProUGUI roundCounterText
Definition: VoiceChat.cs:72
async void RunGeneratedDiscussion(GeneratedDiscussion discussion)
Definition: VoiceChat.cs:496
void ResetIdleTimer()
Reset the idle timer, usually invoked whenever there's any input by the user, such as typing somethin...
Definition: VoiceChat.cs:125
void PropogateUserMessage(string message)
Send the message received from the user to all the actors.
Definition: VoiceChat.cs:341
async void RunFreeTalk()
Definition: VoiceChat.cs:451
State
All the possible states of the system.
Definition: VoiceChat.cs:22
@ Wait
If user input is allowed this round, enter the Wait stage to wait for the actors to finish speaking,...
@ Finish
The chat has ended.
@ FreeTalk
Actors responding to user input as per normal AI conversations.
@ Discuss
Actors playing a scripted discussion, or generating a discussion on a topic.
@ Listen
Listen for user input.
@ Idle
Idle while waiting for player to start the game.
@ Prepare
Before we start each round, we enter the Prepare stage, and make a decision of which state to enter f...
void OnPushToTalkCancelled(UnityEngine.InputSystem.InputAction.CallbackContext context)
Definition: VoiceChat.cs:561
ActorReferences[] actorReferences
Definition: VoiceChat.cs:77
void OnUpdateListen()
Definition: VoiceChat.cs:425
void BeginQueuingAudio()
Flag that audio is being queued.
Definition: VoiceChat.cs:349
void OnEnterDiscuss()
Definition: VoiceChat.cs:517
void ToggleMicMute()
Toggle the director's microphone on or off.
Definition: VoiceChat.cs:113
Dictionary< AudioClip, bool > audioClipGarbageCollector
Definition: VoiceChat.cs:102
async void RunScriptedDiscussion(ScriptedDiscussion discussion)
Definition: VoiceChat.cs:477
Queue< string > speakerQueue
Definition: VoiceChat.cs:109
void OnDirectorResponse(Director.Response response)
Definition: VoiceChat.cs:565
FSM.FiniteStateMachine fsm
Definition: VoiceChat.cs:91
TMPro.TextMeshProUGUI idleTimerText
Definition: VoiceChat.cs:73
void OnEnterFreeTalk()
Definition: VoiceChat.cs:471
Dictionary< string,(Actor, AudioSource)> actors
Definition: VoiceChat.cs:92
void OnEnterFinish()
Definition: VoiceChat.cs:537
Finite state machine class to handle state transitions and updates.
void UnsubscribeFromEvent(string eventName, UnityAction unityAction)
void TriggerEvent(string eventName)
void SubscribeToEvent(string eventName, UnityAction unityAction)
static GameEventSystem GetInstance()
Emotion
Definition: Emotion.cs:4
Persona
Personas the actors will role-play.
Definition: Persona.cs:5