@@ -10,6 +10,7 @@ namespace Exiled.API.Features.Audio.PcmSources
1010 using System ;
1111 using System . Collections . Generic ;
1212 using System . Linq ;
13+ using System . Threading . Tasks ;
1314
1415 using Exiled . API . Features ;
1516 using Exiled . API . Interfaces . Audio ;
@@ -22,17 +23,19 @@ namespace Exiled.API.Features.Audio.PcmSources
2223 /// <summary>
2324 /// Provides a <see cref="IPcmSource"/> that converts text to speech using the <see href="https://www.voicerss.org/">VoiceRSS</see> Text-to-Speech API.
2425 /// </summary>
25- public sealed class VoiceRssTtsSource : IPcmSource
26+ public sealed class VoiceRssTtsSource : IPcmSource , IAsyncPcmSource
2627 {
2728 private const string ApiEndpoint = "https://api.voicerss.org/" ;
2829 private const string AudioFormat = "48khz_16bit_mono" ;
2930
31+ private static readonly Dictionary < string , DateTime > BlacklistKeys = new ( ) ;
32+
3033 private IPcmSource internalSource ;
3134 private UnityWebRequest webRequest ;
3235 private CoroutineHandle downloadRoutine ;
3336
34- private bool isReady = false ;
35- private bool isFailed = false ;
37+ private volatile bool isReady = false ;
38+ private volatile bool isFailed = false ;
3639
3740 /// <summary>
3841 /// Initializes a new instance of the <see cref="VoiceRssTtsSource"/> class.
@@ -99,6 +102,11 @@ public double CurrentTime
99102 /// </summary>
100103 public bool Ended => isFailed || ( isReady && internalSource != null && internalSource . Ended ) ;
101104
105+ /// <summary>
106+ /// Gets a value indicating whether the source failed to load.
107+ /// </summary>
108+ public bool IsFailed => isFailed ;
109+
102110 /// <summary>
103111 /// Reads PCM data from the audio source into the specified buffer.
104112 /// </summary>
@@ -154,8 +162,6 @@ public void Dispose()
154162
155163 private IEnumerator < float > DownloadRoutine ( string text , IEnumerable < string > apiKeys , string language , string voice , int rate )
156164 {
157- webRequest = null ;
158-
159165 string clampedRate = Math . Clamp ( rate , - 10 , 10 ) . ToString ( ) ;
160166 string textEscaped = Uri . EscapeDataString ( text ) ;
161167 string langEscaped = Uri . EscapeDataString ( language ) ;
@@ -168,10 +174,26 @@ private IEnumerator<float> DownloadRoutine(string text, IEnumerable<string> apiK
168174 if ( string . IsNullOrWhiteSpace ( apiKey ) )
169175 continue ;
170176
177+ if ( BlacklistKeys . TryGetValue ( apiKey , out DateTime exhaustedAt ) )
178+ {
179+ if ( DateTime . UtcNow . Day == exhaustedAt . Day )
180+ continue ;
181+
182+ BlacklistKeys . Remove ( apiKey ) ;
183+ }
184+
171185 string url = $ "{ ApiEndpoint } ?key={ Uri . EscapeDataString ( apiKey ) } &hl={ langEscaped } &c=WAV&f={ AudioFormat } &r={ clampedRate } &src={ textEscaped } { voiceEscaped } ";
172186
173187 webRequest ? . Dispose ( ) ;
174- webRequest = UnityWebRequest . Get ( url ) ;
188+ try
189+ {
190+ webRequest = UnityWebRequest . Get ( url ) ;
191+ }
192+ catch ( Exception ex )
193+ {
194+ Log . Error ( $ "[VoiceRssTtsSource] Failed to get Url '{ url } . Error: { ex . Message } ") ;
195+ break ;
196+ }
175197
176198 yield return Timing . WaitUntilDone ( webRequest . SendWebRequest ( ) ) ;
177199
@@ -189,6 +211,7 @@ private IEnumerator<float> DownloadRoutine(string text, IEnumerable<string> apiK
189211 if ( errorMessage . Contains ( "limit" ) || errorMessage . Contains ( "expired" ) || errorMessage . Contains ( "inactive" ) || errorMessage . Contains ( "API key" ) )
190212 {
191213 Log . Warn ( $ "[VoiceRssTtsSource] Key issue, key: '{ apiKey } ', Error : { errorMessage } . Switching to another key...") ;
214+ BlacklistKeys [ apiKey ] = DateTime . UtcNow ;
192215 continue ;
193216 }
194217 else
@@ -205,29 +228,40 @@ private IEnumerator<float> DownloadRoutine(string text, IEnumerable<string> apiK
205228 if ( ! successfulDownload )
206229 {
207230 isFailed = true ;
231+ webRequest ? . Dispose ( ) ;
232+ webRequest = null ;
208233 yield break ;
209234 }
210235
211- try
236+ byte [ ] rawBytes = webRequest . downloadHandler . data ;
237+ webRequest . Dispose ( ) ;
238+ webRequest = null ;
239+
240+ Task < AudioData > toPcmTask = Task . Run ( ( ) => WavUtility . WavToPcm ( rawBytes ) ) ;
241+
242+ yield return Timing . WaitUntilTrue ( ( ) => toPcmTask . IsCompleted ) ;
243+
244+ if ( toPcmTask . IsFaulted )
212245 {
213- byte [ ] rawBytes = webRequest . downloadHandler . data ;
214- AudioData audioData = WavUtility . WavToPcm ( rawBytes ) ;
215- audioData . TrackInfo . Path = $ "VoiceRSS: { text } ";
246+ Log . Error ( $ "[VoiceRssTtsSource] Error read the downloaded file! \n Error: { toPcmTask . Exception ? . InnerException ? . Message ?? toPcmTask . Exception ? . Message } ") ;
247+ isFailed = true ;
248+ yield break ;
249+ }
250+
251+ AudioData audioData = toPcmTask . Result ;
252+ audioData . TrackInfo . Path = $ "VoiceRSS: { text } ";
216253
254+ try
255+ {
217256 internalSource = new PreloadedPcmSource ( audioData . Pcm ) ;
218257 TrackInfo = audioData . TrackInfo ;
219258 isReady = true ;
220259 }
221- catch ( Exception e )
260+ catch ( Exception ex )
222261 {
223- Log . Error ( $ "[VoiceRssTtsSource] Parsing Error ! \n Details : { e . Message } ") ;
262+ Log . Error ( $ "[VoiceRssTtsSource] Failed to create internal source ! \n Error : { ex . InnerException ? . Message ?? ex . Message } ") ;
224263 isFailed = true ;
225264 }
226- finally
227- {
228- webRequest ? . Dispose ( ) ;
229- webRequest = null ;
230- }
231265 }
232266 }
233267}
0 commit comments