In the current Convai Unity SDK (especially the WebGL / gRPC path), what is the official and supported way to immediately stop an NPC’s ongoing speech (TTS playback) at runtime?
Our requirement is to interrupt the NPC voice mid-sentence (for example, when the player exits a trigger or switches to another NPC).
We are not trying to stop microphone input, only the NPC’s speech output.
Below, I have attached our ConvaiNPC script for reference, which handles incoming audio chunks and playback.
Please let us know the recommended approach or API to properly stop or cancel NPC speech instead of manually controlling the AudioSource.
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
using System.Collections.Generic;
using ReadyPlayerMe;
// This script uses gRPC for streaming and is a work in progress
// Edit this script directly to customize your intelligent NPC character
[RequireComponent(typeof(Animator), typeof(AudioSource))]
public class ConvaiNPC : MonoBehaviour
{
// do not edit
public string sessionID = “-1”;
public Sprite SpacetotalkBLue;
public Sprite Spacetotalkwhite;
public SpriteRenderer spacetotalkimage;
//public GameObject skipBtn;
[HideInInspector]
public string stringCharacterText = "";
public class ResponseAudio
{
public AudioClip audioClip;
public string audioTranscript;
};
[HideInInspector]
public List<ResponseAudio> ResponseAudios = new List<ResponseAudio>();
[SerializeField] public string CharacterID;
[SerializeField] public string CharacterName;
private AudioSource audioSource;
private Animator characterAnimator;
// private VoiceHandler voiceHandler;
private ConvaiRPMLipSync convaiRPMLipSync;
private ConvaiChatUIHandler convaiChatUIHandler;
bool animationPlaying = false;
bool playingStopLoop = false;
private ConvaiGRPCWebAPI grpcAPI;
[SerializeField] public bool isCharacterActive = false;
[SerializeField] public bool isCharacterTalking = false;
[SerializeField] bool enableTestMode;
[SerializeField] string testUserQuery;
[HideInInspector]
public List<AudioData> audioDataList = new List<AudioData>();
[HideInInspector]
public AudioClip currentClip;
public bool recordingwaitdone = false;
public bool canstartnewrecording = true;
public IntelExpoBooth intelExpoBooth;
public bool forTest = false;
public GameObject skipBtn;
private void Awake()
{
//grpcAPI = FindObjectOfType<ConvaiGRPCWebAPI>();
//convaiChatUIHandler = FindObjectOfType<ConvaiChatUIHandler>();
//audioSource = GetComponent<AudioSource>();
//characterAnimator = GetComponent<Animator>();
//if (GetComponent<ConvaiRPMLipSync>() != null)
//{
// convaiRPMLipSync = GetComponent<ConvaiRPMLipSync>();
//}
}
private void Start()
{
StartCoroutine(playAudioInOrder());
StartCoroutine(waitForPlayer());
}
public IEnumerator waitForPlayer()
{
yield return new WaitUntil(() => GameManager.Instance);
yield return new WaitUntil(() => GameManager.Instance.localPlayer != null);
grpcAPI = FindObjectOfType<ConvaiGRPCWebAPI>();
convaiChatUIHandler = FindObjectOfType<ConvaiChatUIHandler>();
audioSource = GetComponent<AudioSource>();
characterAnimator = GetComponent<Animator>();
if (GetComponent<ConvaiRPMLipSync>() != null)
{
convaiRPMLipSync = GetComponent<ConvaiRPMLipSync>();
}
}
public void OnRecordingStart()
{
print("---" + "StartRecordAudio");
grpcAPI.StartRecordAudio();
}
public void OnRecordingStop()
{
print("---" + "StopRecordAudio");
grpcAPI.StopRecordAudio();
//skipBtn.SetActive(false);
}
public void OnSkipBtnClick()
{
Debug.Log("OnSkipBtnClick");
skipBtn.SetActive(false);
//InterruptCharacterSpeech();
//isCharacterActive = false;
//StartCoroutine(ActiveCharacter());
}
//IEnumerator ActiveCharacter()
//{
// yield return new WaitForSeconds(0.5f);
// isCharacterActive = true;
//}
IEnumerator checkrecording()
{
yield return new WaitForSeconds(2);
recordingwaitdone = true;
}
IEnumerator waitforendrecording()
{
yield return new WaitUntil(() => recordingwaitdone);
canstartnewrecording = true;
recordingwaitdone = false;
grpcAPI.StopRecordAudio();
if (spacetotalkimage)
{
spacetotalkimage.sprite = Spacetotalkwhite;
}
}
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Player" && other.gameObject.GetComponent<Player>().isLocalPlayer)
{
Debug.Log("NPC OnTriggerEnter");
isCharacterActive = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag == "Player" && other.gameObject.GetComponent<Player>().isLocalPlayer)
{
Debug.Log("NPC OnTriggerExit");
isCharacterActive = false;
//audioSource.Stop();
//characterAnimator.SetBool("Talk", false);
}
}
private void Update()
{
// this block starts and stops audio recording and processing
if (isCharacterActive)
{
// Start recording when the left control is pressed
if (Input.GetKeyDown(KeyCode.Space) && canstartnewrecording)
{
if (spacetotalkimage)
{
spacetotalkimage.sprite = SpacetotalkBLue;
}
// Start recording audio
if (intelExpoBooth != null)
{
DataEnvManager.Instance.intelExpo.CallActivity(28, intelExpoBooth.boothId);
}
canstartnewrecording = false;
StartCoroutine(checkrecording());
grpcAPI.StartRecordAudio();
}
// Stop recording once left control is released and saves the audio file locally
if (Input.GetKeyUp(KeyCode.Space))
{
//grpcAPI.StopRecordAudio();
StartCoroutine(waitforendrecording());
// recordingStopLoop = true;
}
}
//if (Input.GetKey(KeyCode.R) && Input.GetKey(KeyCode.Equals))
//{
// SceneManager.LoadScene(0);
//}
//if (Input.GetKey(KeyCode.Escape) && Input.GetKey(KeyCode.Equals))
//{
// Application.Quit();
//}
//Jaydip
// if there is some audio in the queue, play it next
// essentially make a playlist
if (audioDataList.Count > 0)
{
ProcessResponseAudio(audioDataList[0]);
audioDataList.RemoveAt(0);
}
// if any audio is playing, play the talking animation
if (ResponseAudios.Count > 0)
{
if (animationPlaying == false)
{
// enable animation according to response
// try talking first, then base it on the response
animationPlaying = true;
skipBtn.SetActive(true);
characterAnimator.SetBool("Talk", true);
}
}
else
{
if (animationPlaying == true)
{
// deactivate animations to idle
animationPlaying = false;
skipBtn.SetActive(false);
characterAnimator.SetBool("Talk", false);
}
}
}
/// <summary>
/// When the response list has more than one elements, then the audio will be added to a playlist. This function adds individual responses to the list.
/// </summary>
/// <param name="getResponseResponse">The getResponseResponse object that will be processed to add the audio and transcript to the playlist</param>
void ProcessResponseAudio(AudioData audioData)
{
if (isCharacterActive)
{
string tempString = "";
if (audioData.resText != null)
tempString = audioData.resText;
AudioClip clip;
if (audioData.isFirst)
clip = grpcAPI.ProcessByteAudioDataToTrimmedAudioClip(audioData.audData, audioData.sampleRate.ToString());
else
clip = grpcAPI.ProcessByteAudioDataToAudioClip(audioData.audData, audioData.sampleRate.ToString());
if (clip != null)
{
ResponseAudios.Add(new ResponseAudio
{
audioClip = clip,
audioTranscript = tempString
});
}
}
}
/// <summary>
/// If the playlist is not empty, then it is played.
/// </summary>
IEnumerator playAudioInOrder()
{
// plays audio as soon as there is audio to play
while (!playingStopLoop)
{
// Debug.Log("Response Audio Clip Count: " + ResponseAudioClips.Count);
if (ResponseAudios.Count > 0)
{
// add animation logic here
{
audioSource.clip = ResponseAudios[0].audioClip;
audioSource.Play();
if (convaiChatUIHandler != null)
convaiChatUIHandler.isCharacterTalking = true;
}
if (convaiChatUIHandler != null)
{
convaiChatUIHandler.characterText = ResponseAudios[0].audioTranscript;
convaiChatUIHandler.characterName = CharacterName;
}
yield return new WaitForSeconds(ResponseAudios[0].audioClip.length);
if (convaiRPMLipSync != null)
convaiRPMLipSync.audioSamples = null;
if (convaiChatUIHandler != null)
convaiChatUIHandler.isCharacterTalking = false;
ResponseAudios.Remove(ResponseAudios[0]);
}
else
yield return null;
}
}
void OnApplicationQuit()
{
playingStopLoop = true;
}
}