TikTokSpeech.cs
using System;
using Sandbox;
namespace TikTokTTS;
public class TikTokSpeech : Component
{
[Property] public string Text { get; set; } = "Hello World!";
/// <summary>
/// The voice that will be used to speak the text.
/// </summary>
[Property, Sync] public TikTokVoice Voice { get; set; } = TikTokVoice.en_us_002;
/// <summary>
/// Whether or not the audio should be positional (based on the position of the GameObject that this component is attached to)
/// </summary>
[Property, Sync] public bool PositionalAudio { get; set; } = false;
/// <summary>
/// Whether or not to cache the files that are generated by the TTS. When enabled, the files will be stored in the data directory and referenced if the same call is made again.
/// </summary>
[Property] public bool CacheFiles { get; set; } = true;
/// <summary>
/// An event that is fired when the TTS has started speaking.
/// </summary>
[Property] public Action<string, TikTokVoice> OnSpeak { get; set; }
public string VoiceName => Enum.GetName( typeof( TikTokVoice ), Voice );
private MusicPlayer MusicPlayer;
/// <summary>
/// Speak the current text using the TTS.
/// </summary>
///
public void Speak(string text, string voiceName)
{
RequestSpeech( text, voiceName );
}
[Rpc.Host]
private async void RequestSpeech(string text, string voiceName)
{
if ( string.IsNullOrWhiteSpace( text ) || !Enum.TryParse<TikTokVoice>( voiceName, out _ ) )
return;
byte[] audioData = await TikTokTTS.Generate( text, voiceName );
if ( audioData is null )
return;
PlaySpeech( text, voiceName, Guid.NewGuid().ToString( "N" ), audioData );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void PlaySpeech(string text, string voiceName, string clipId, byte[] audioData)
{
MusicPlayer?.Dispose();
MusicPlayer = null;
if ( audioData is null || audioData.Length == 0 )
return;
const string directory = "tts/";
string fileName = $"{directory}tts-{clipId}.mp3";
if ( !FileSystem.Data.DirectoryExists( directory ) )
FileSystem.Data.CreateDirectory( directory );
var stream = FileSystem.Data.OpenWrite( fileName );
stream.Write( audioData, 0, audioData.Length );
stream.Close();
MusicPlayer = Sandbox.MusicPlayer.Play( FileSystem.Data, fileName );
Log.Info( $"Speaking {text} with voice {voiceName}" );
if ( Enum.TryParse<TikTokVoice>( voiceName, out var voice ) )
OnSpeak?.Invoke( text, voice );
MusicPlayer.ListenLocal = !PositionalAudio;
}
protected override void OnUpdate()
{
if ( MusicPlayer is not null )
{
MusicPlayer?.Position = WorldPosition;
}
}
}