Skip to content

Commit 67511a0

Browse files
feat: allowing manipulation of playback speed
Passing sound manipulation to the soundstretch program: https://www.surina.net/soundtouch/soundstretch.html closes #69
1 parent 70165f3 commit 67511a0

File tree

4 files changed

+105
-64
lines changed

4 files changed

+105
-64
lines changed
416 KB
Binary file not shown.

src/TwitchStreamingTools/Tts/TwitchChatTts.cs

Lines changed: 88 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Concurrent;
3+
using System.Diagnostics;
34
using System.IO;
45
using System.Speech.Synthesis;
56
using System.Threading;
@@ -230,71 +231,101 @@ private void PlaySoundsThread() {
230231
/// <param name="sender">The twitch chat login of the user that sent the message.</param>
231232
/// <param name="chatMessage">The chat message to convert to TTS.</param>
232233
private void InitializeAndPlayTts(string sender, string chatMessage) {
233-
// Create a microsoft TTS object and a stream for outputting its audio file to.
234-
using var synth = new SpeechSynthesizer();
235-
using var stream = new MemoryStream();
236-
237-
WaveFileReader reader;
234+
string filename = Path.GetTempFileName();
235+
string filename2 = Path.GetTempFileName();
236+
238237
try {
239-
// Setup the microsoft TTS object according to the settings.
240-
synth.SetOutputToWaveStream(stream);
241-
synth.SelectVoice(ChatConfig?.TtsVoice);
242-
synth.Volume = (int)(ChatConfig?.TtsVolume ?? 50);
243-
synth.Speak(chatMessage);
244-
245-
// Now that we filled the stream, seek to the beginning so we can play it.
246-
stream.Seek(0, SeekOrigin.Begin);
247-
reader = new WaveFileReader(stream);
248-
}
249-
catch (Exception ex) {
250-
Console.WriteLine($"Exception initializing a new microsoft speech object: {ex}");
251-
return;
252-
}
238+
string fileToPlay = filename;
253239

254-
// This is only meant to ensure we don't play TTS over a sound alert. It can still happen but this
255-
// fixes most of the issues with very little investment.
256-
while (GlobalSoundManager.Instance.CurrentlyPlayingSound) {
257-
Thread.Sleep(100);
258-
}
240+
WaveFileReader reader;
241+
try {
242+
// Create a microsoft TTS object and a stream for outputting its audio file to.
243+
using var synth = new SpeechSynthesizer();
244+
245+
// Setup the microsoft TTS object according to the settings.
246+
synth.SetOutputToWaveFile(filename);
247+
synth.SelectVoice(ChatConfig?.TtsVoice);
248+
synth.Volume = (int)(ChatConfig?.TtsVolume ?? 50);
249+
synth.Speak(chatMessage);
250+
}
251+
catch (Exception ex) {
252+
Console.WriteLine($"Exception initializing a new microsoft speech object: {ex}");
253+
return;
254+
}
259255

260-
try {
261-
// Make sure we lock the objects used on multiple threads and play the file.
262-
lock (_ttsSoundOutputLock)
263-
lock (_ttsSoundOutputSignalLock) {
264-
_ttsSoundOutput = new WaveOutEvent();
265-
_ttsSoundOutputSignal = new ManualResetEvent(false);
266-
267-
_ttsSoundOutput.DeviceNumber = NAudioUtilities.GetOutputDeviceIndex(ChatConfig?.OutputDevice);
268-
_ttsSoundOutput.Volume = (ChatConfig?.TtsVolume ?? 50.0f) / 100.0f;
269-
270-
_ttsSoundOutput.Init(reader);
271-
272-
// Play is async so we will make it synchronous here so we don't have to deal with
273-
// queueing. We can improve this to remove the hack in the future.
274-
_ttsSoundOutput.PlaybackStopped += delegate {
275-
lock (_ttsSoundOutputSignalLock) {
276-
_ttsSoundOutputSignal?.Set();
277-
}
278-
};
256+
var startInfo = new ProcessStartInfo {
257+
FileName = @"3rdParty\soundstretch.exe",
258+
Arguments = $"\"{filename}\" \"{filename2}\" -tempo=+100 -speech",
259+
UseShellExecute = false,
260+
CreateNoWindow = true
261+
};
262+
263+
using var process = new Process { StartInfo = startInfo };
264+
process.Start();
265+
process.WaitForExit();
266+
if (0 == process.ExitCode) {
267+
fileToPlay = filename2;
268+
}
279269

280-
// Play it.
281-
_ttsSoundOutput.Play();
270+
// This is only meant to ensure we don't play TTS over a sound alert. It can still happen but this
271+
// fixes most of the issues with very little investment.
272+
while (GlobalSoundManager.Instance.CurrentlyPlayingSound) {
273+
Thread.Sleep(100);
282274
}
283275

284-
// Wait for the play to finish, we will get signaled.
285-
CurrentUsername = sender;
286-
ManualResetEvent? signal = _ttsSoundOutputSignal;
287-
signal?.WaitOne();
288-
CurrentUsername = null;
276+
try {
277+
// Make sure we lock the objects used on multiple threads and play the file.
278+
lock (_ttsSoundOutputLock)
279+
lock (_ttsSoundOutputSignalLock) {
280+
_ttsSoundOutput = new WaveOutEvent();
281+
_ttsSoundOutputSignal = new ManualResetEvent(false);
282+
283+
_ttsSoundOutput.DeviceNumber = NAudioUtilities.GetOutputDeviceIndex(ChatConfig?.OutputDevice);
284+
_ttsSoundOutput.Volume = (ChatConfig?.TtsVolume ?? 50.0f) / 100.0f;
285+
286+
reader = new WaveFileReader(fileToPlay);
287+
_ttsSoundOutput.Init(reader);
288+
289+
// Play is async so we will make it synchronous here so we don't have to deal with
290+
// queueing. We can improve this to remove the hack in the future.
291+
_ttsSoundOutput.PlaybackStopped += delegate {
292+
lock (_ttsSoundOutputSignalLock) {
293+
_ttsSoundOutputSignal?.Set();
294+
}
295+
};
296+
297+
// Play it.
298+
_ttsSoundOutput.Play();
299+
}
300+
301+
// Wait for the play to finish, we will get signaled.
302+
CurrentUsername = sender;
303+
ManualResetEvent? signal = _ttsSoundOutputSignal;
304+
signal?.WaitOne();
305+
CurrentUsername = null;
306+
}
307+
finally {
308+
// Finally dispose of everything safely in the lock.
309+
lock (_ttsSoundOutputLock)
310+
lock (_ttsSoundOutputSignalLock) {
311+
_ttsSoundOutput?.Dispose();
312+
_ttsSoundOutput = null;
313+
_ttsSoundOutputSignal?.Dispose();
314+
_ttsSoundOutputSignal = null;
315+
}
316+
}
289317
}
290318
finally {
291-
// Finally dispose of everything safely in the lock.
292-
lock (_ttsSoundOutputLock)
293-
lock (_ttsSoundOutputSignalLock) {
294-
_ttsSoundOutput?.Dispose();
295-
_ttsSoundOutput = null;
296-
_ttsSoundOutputSignal?.Dispose();
297-
_ttsSoundOutputSignal = null;
319+
string[] filesToDelete = { filename, filename2 };
320+
foreach (string file in filesToDelete) {
321+
try {
322+
if (File.Exists(file)) {
323+
File.Delete(file);
324+
}
325+
}
326+
catch {
327+
// Do nothing, just try to clean up the best you can.
328+
}
298329
}
299330
}
300331
}

src/TwitchStreamingTools/TwitchStreamingTools.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
<AvaloniaResource Include="Assets\**"/>
2828
</ItemGroup>
2929

30+
<ItemGroup>
31+
<None Remove="3rdParty\soundstretch.exe"/>
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<Content Include="3rdParty\soundstretch.exe">
36+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
37+
</Content>
38+
</ItemGroup>
39+
3040
<ItemGroup>
3141
<PackageReference Include="Avalonia" Version="11.3.1"/>
3242
<PackageReference Include="Avalonia.Desktop" Version="11.3.1"/>

src/TwitchStreamingTools/Views/MainWindow.axaml.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
5+
using Avalonia.Controls;
6+
7+
using Nullinside.Api.Common.Desktop;
18
#if !DEBUG
29
using Microsoft.Extensions.DependencyInjection;
310

@@ -7,13 +14,6 @@
714
#else
815
using Avalonia;
916
#endif
10-
using System;
11-
using System.Linq;
12-
using System.Threading.Tasks;
13-
14-
using Avalonia.Controls;
15-
16-
using Nullinside.Api.Common.Desktop;
1717

1818
namespace TwitchStreamingTools.Views;
1919

0 commit comments

Comments
 (0)