diff --git a/dotnet-examples/TTS/PlayAudioPartial/SherpaOnnxGeneratedAudioResultPlayAudio.cs b/dotnet-examples/TTS/PlayAudioPartial/SherpaOnnxGeneratedAudioResultPlayAudio.cs new file mode 100644 index 000000000..1eb1e3568 --- /dev/null +++ b/dotnet-examples/TTS/PlayAudioPartial/SherpaOnnxGeneratedAudioResultPlayAudio.cs @@ -0,0 +1,44 @@ +using NAudio.Wave; + +namespace TTS.Struct +{ + public sealed partial class SherpaOnnxGeneratedAudioResult + { + private WaveOutEvent waveOut; + private WaveFormat waveFormat; + private BufferedWaveProvider bufferedWaveProvider; + + private int bufferLength = 1; + + public TimeSpan? AudioDuration => bufferedWaveProvider?.BufferedDuration; + + public float PlayProgress => (waveOut?.GetPosition() * 1.0f / bufferLength).Value; + + public void Play() + { + waveOut ??= new WaveOutEvent(); + + waveFormat ??= new WaveFormat(sample_rate, AudioDataBit, Channels); // 32-bit 浮点,单声道 + + if (bufferedWaveProvider == null) + { + bufferedWaveProvider ??= new BufferedWaveProvider(waveFormat); + + var buffer = AudioByteData; + + bufferLength = buffer.Length; + + bufferedWaveProvider.AddSamples(buffer, 0, bufferLength); + bufferedWaveProvider.BufferLength = bufferLength; + waveOut.Init(bufferedWaveProvider); + } + waveOut.Play(); + } + + public void Stop() + { + waveOut?.Stop(); + } + + } +} diff --git a/dotnet-examples/TTS/Program.cs b/dotnet-examples/TTS/Program.cs new file mode 100644 index 000000000..7081040d5 --- /dev/null +++ b/dotnet-examples/TTS/Program.cs @@ -0,0 +1,62 @@ +using System.Text; +using TTS; +using TTS.Struct; + +internal class Program +{ + private static void Main(string[] args) + { + SherpaOnnxOfflineTtsConfig sherpaOnnxOfflineTtsConfig = new SherpaOnnxOfflineTtsConfig(); + sherpaOnnxOfflineTtsConfig.model = new SherpaOnnxOfflineTtsModelConfig + { + debug = 0, + num_threads = 4, + provider = "cpu", + vits = new SherpaOnnxOfflineTtsVitsModelConfig + { + lexicon = "vits-zh-aishell3/lexicon.txt", + model = "vits-zh-aishell3/vits-aishell3.onnx", + tokens = "vits-zh-aishell3/tokens.txt", + + noise_scale = 0.667f, + noise_scale_w = 0.8f, + length_scale = 1, + }, + + }; + + TTSCore i = new TTSCore(sherpaOnnxOfflineTtsConfig); + + Console.InputEncoding = Encoding.Unicode; + Console.OutputEncoding = Encoding.UTF8; + + while (true) + { + var str = Console.ReadLine(); + var audioResult = i.ToSpeech(str, 40, 1f); + + // audioResult.WriteWAVFile("123.wav");保存本地 + + audioResult.Play(); + + int lastIndex = -1; + while (audioResult.PlayProgress <= 1f) + { + int index = (int)(audioResult.PlayProgress * (str.Length - 1)); + if (lastIndex != index) + { + Console.Write(str[index]); + lastIndex = index; + } + Thread.Sleep(100); + } + + if (++lastIndex < str.Length) + Console.Write(str[lastIndex]); + + Console.WriteLine(); + + } + + } +} diff --git a/dotnet-examples/TTS/Struct/SherpaOnnxGeneratedAudio.cs b/dotnet-examples/TTS/Struct/SherpaOnnxGeneratedAudio.cs new file mode 100644 index 000000000..affc3a034 --- /dev/null +++ b/dotnet-examples/TTS/Struct/SherpaOnnxGeneratedAudio.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace TTS.Struct +{ + /// + /// 生成语音结果 + /// + public sealed partial class SherpaOnnxGeneratedAudioResult : IDisposable + { + public const string Filename = "sherpa-onnx-c-api"; + + /// + /// 销毁非托管内存 + /// + /// + [DllImport(Filename)] + private static extern void SherpaOnnxDestroyOfflineTtsGeneratedAudio(IntPtr ttsGenerateIntptr); + + [DllImport(Filename)] + private static extern int SherpaOnnxWriteWave(IntPtr q, int n, int sample_rate, string filename); + + /// + /// 音频数据比特 + /// + public const int AudioDataBit = 16; + /// + /// 单通道 + /// + public const int Channels = 1; + + /// + /// 原生句柄 + /// + internal IntPtr thisHandle; + + internal readonly IntPtr audioData; + internal readonly int dataSize; + + /// + /// 采样率 + /// + public readonly int sample_rate; + + /// + /// 音频数据指针 + /// + public IntPtr AudioDataIntPtr => audioData; + + /// + /// 数据的大小 + /// + public unsafe int AudioDataLength + { + get + { + return dataSize; + + //float* buffer = (float*)audioData; + //while (*buffer != 0) + // ++buffer; + //return (int)(buffer - (float*)audioData); + } + } + + /// + /// 获得音频数据 float[] + /// 这个内部创建一个数组 + /// + public unsafe float[] AudioFloatData + { + get + { + int length = AudioDataLength; + + float[] floatAudioData = new float[length]; + Marshal.Copy(audioData, floatAudioData, 0, floatAudioData.Length); + return floatAudioData; + } + } + + + /// + /// 获得音频数据 byte[] + /// 这个内部创建一个数组 + /// + public byte[] AudioByteData + { + get + { + byte[] bytes = new byte[AudioDataLength * 2]; + ReadData(bytes, 0); + return bytes; + } + } + + internal SherpaOnnxGeneratedAudioResult(IntPtr intPtr, SherpaOnnxGeneratedAudio sherpaOnnx) + { + this.thisHandle = intPtr; + this.audioData = sherpaOnnx.audioData; + this.dataSize = sherpaOnnx.dataSize; + this.sample_rate = sherpaOnnx.sample_rate; + } + + ~SherpaOnnxGeneratedAudioResult() + { + Dispose(); + } + + /// + /// 读取数据 + /// 没有垃圾产生,自己传递数组进来 + /// + /// 数组 + /// 数组那个位置写入 + /// 写入了多少个 + public int ReadData(float[] audioFloats, int offset) + { + int length = AudioDataLength; + + int c = audioFloats.Length - offset; + length = c >= length ? length : c; + + Marshal.Copy(audioData, audioFloats, offset, length); + return length; + } + + /// + /// 读取数据 + /// 这个内部转换成byte[] 音频数组 + /// 没有垃圾产生,自己传递数组进来 + /// + /// 数组,这个长度需要是AudioDataLength*2大小 + /// 数组那个位置写入 + /// 写入了多少个 + public int ReadData(byte[] audioFloats, int offset) + { + //因为是16bit存储音频数据,所以float会转换成两个字节存储 + var audiodata = AudioFloatData; + + int length = audiodata.Length * 2; + + int c = audioFloats.Length - offset; + c = c % 2 == 0 ? c : c - 1; + + length = c >= length ? length : c; + + int p = length / 2; + + for (int i = 0; i < p; i++) + { + short value = (short)(audiodata[i] * short.MaxValue); + + audioFloats[offset++] = (byte)value; + audioFloats[offset++] = (byte)(value >> 8); + } + + return length; + + } + + /// + /// 写入WAV音频数据 + /// + /// + /// + public bool WriteWAVFile(string filename) + { + return 1 == SherpaOnnxWriteWave(audioData, this.dataSize, this.sample_rate, filename); + } + + public void Dispose() + { + if (this.thisHandle != IntPtr.Zero) + { + SherpaOnnxDestroyOfflineTtsGeneratedAudio(this.thisHandle); + GC.SuppressFinalize(this); + this.thisHandle = IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SherpaOnnxGeneratedAudio + { + internal readonly IntPtr audioData; + internal readonly int dataSize; + + /// + /// 采样率 + /// + public readonly int sample_rate; + } +} diff --git a/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsConfig.cs b/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsConfig.cs new file mode 100644 index 000000000..1d20264eb --- /dev/null +++ b/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsConfig.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace TTS.Struct +{ + [StructLayout(LayoutKind.Sequential)] + public struct SherpaOnnxOfflineTtsConfig + { + public SherpaOnnxOfflineTtsModelConfig model; + } +} diff --git a/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsModelConfig.cs b/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsModelConfig.cs new file mode 100644 index 000000000..46dd55859 --- /dev/null +++ b/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsModelConfig.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace TTS.Struct +{ + [StructLayout(LayoutKind.Sequential)] + public struct SherpaOnnxOfflineTtsModelConfig + { + /// + /// 模型配置 + /// + public SherpaOnnxOfflineTtsVitsModelConfig vits; + /// + /// 线程数 + /// + public int num_threads; + public int debug; + /// + /// 使用cpu + /// + [MarshalAs(UnmanagedType.LPStr)] + public string provider; + } +} diff --git a/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsVitsModelConfig.cs b/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsVitsModelConfig.cs new file mode 100644 index 000000000..4b37d81b0 --- /dev/null +++ b/dotnet-examples/TTS/Struct/SherpaOnnxOfflineTtsVitsModelConfig.cs @@ -0,0 +1,48 @@ +using System.Runtime.InteropServices; + +namespace TTS.Struct +{ + [StructLayout(LayoutKind.Sequential)] + public struct SherpaOnnxOfflineTtsVitsModelConfig + { + /// + /// 模型 + /// "vits-zh-aishell3/vits-aishell3.onnx" + /// + [MarshalAs(UnmanagedType.LPStr)] + public string model; + /// + /// 词典文件 + /// "vits-zh-aishell3/lexicon.txt" + /// + [MarshalAs(UnmanagedType.LPStr)] + public string lexicon; + + [MarshalAs(UnmanagedType.LPStr)] + public string tokens; + + /// + /// VITS模型的noise_scale (float,默认值= 0.667) + /// + public float noise_scale = 0.667f; + /// + /// VITS模型的noise_scale_w (float,默认值= 0.8) + /// + public float noise_scale_w = 0.8f; + /// + /// 演讲的速度。大→慢;小→更快。(float, default = 1) + /// + public float length_scale = 1f; + + public SherpaOnnxOfflineTtsVitsModelConfig() + { + noise_scale = 0.667f; + noise_scale_w = 0.8f; + length_scale = 1f; + + model = "vits-zh-aishell3/vits-aishell3.onnx"; + lexicon = "vits-zh-aishell3/lexicon.txt"; + tokens = "vits-zh-aishell3/tokens.txt"; + } + } +} diff --git a/dotnet-examples/TTS/TTSCore.cs b/dotnet-examples/TTS/TTSCore.cs new file mode 100644 index 000000000..63735a8cc --- /dev/null +++ b/dotnet-examples/TTS/TTSCore.cs @@ -0,0 +1,70 @@ +using System.Runtime.InteropServices; +using TTS.Struct; + +namespace TTS +{ + internal sealed class TTSCore : IDisposable + { + public const string Filename = "sherpa-onnx-c-api"; + + [DllImport(Filename)] + private static extern IntPtr SherpaOnnxCreateOfflineTts(SherpaOnnxOfflineTtsConfig handle); + + [DllImport(Filename)] + private static extern IntPtr SherpaOnnxOfflineTtsGenerate(IntPtr createOfflineTtsIntptr, IntPtr text, int sid, float speed); + + [DllImport(Filename)] + private static extern void SherpaOnnxDestroyOfflineTts(IntPtr intPtr); + + /// + /// 原生句柄 + /// + private IntPtr thisHandle; + + public TTSCore(SherpaOnnxOfflineTtsConfig modelConfig) + { + thisHandle = SherpaOnnxCreateOfflineTts(modelConfig); + } + + /// + /// 文字转语音 + /// + /// 文字 + /// 音色 + /// 速度 + /// + public SherpaOnnxGeneratedAudioResult ToSpeech(string text, int sid, float speed = 1f) + { + var result = SherpaOnnxOfflineTtsGenerate(thisHandle, Marshal.StringToCoTaskMemUTF8(text), sid, speed); + SherpaOnnxGeneratedAudio impl = (SherpaOnnxGeneratedAudio)Marshal.PtrToStructure(result, typeof(SherpaOnnxGeneratedAudio)); + return new SherpaOnnxGeneratedAudioResult(result, impl); + } + + /// + /// 文字转语音 + /// + /// 文字 + /// 音色 + /// 速度 + /// + public Task ToSpeechAsync(string text, int sid, float speed = 1f) + { + return Task.Run(() => ToSpeech(text, sid, speed)); + } + + ~TTSCore() + { + Dispose(); + } + + public void Dispose() + { + if (this.thisHandle != IntPtr.Zero) + { + SherpaOnnxDestroyOfflineTts(this.thisHandle); + GC.SuppressFinalize(this); + this.thisHandle = IntPtr.Zero; + } + } + } +}