-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add C# API examples for ZipVoice models #3345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| // Copyright (c) 2026 Xiaomi Corporation | ||
| // | ||
| // This file shows how to use a non-streaming ZipVoice model | ||
| // for zero-shot text-to-speech with playback. | ||
| // Please refer to | ||
| // https://k2-fsa.github.io/sherpa/onnx/tts/zipvoice.html | ||
| // and | ||
| // https://github.com/k2-fsa/sherpa-onnx/releases/tag/tts-models | ||
| // to download pre-trained models | ||
| using PortAudioSharp; | ||
| using SherpaOnnx; | ||
| using System.Collections.Concurrent; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| class ZipVoiceTtsDemo | ||
| { | ||
| static void Main(string[] args) | ||
| { | ||
| TestZhEn(); | ||
| } | ||
|
|
||
| static void TestZhEn() | ||
| { | ||
| var config = new OfflineTtsConfig(); | ||
| config.Model.ZipVoice.Tokens = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/tokens.txt"; | ||
| config.Model.ZipVoice.Encoder = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/encoder.int8.onnx"; | ||
| config.Model.ZipVoice.Decoder = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/decoder.int8.onnx"; | ||
| config.Model.ZipVoice.Vocoder = "./vocos_24khz.onnx"; | ||
| config.Model.ZipVoice.DataDir = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/espeak-ng-data"; | ||
| config.Model.ZipVoice.Lexicon = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/lexicon.txt"; | ||
|
|
||
| config.Model.NumThreads = 2; | ||
| config.Model.Debug = 1; | ||
| config.Model.Provider = "cpu"; | ||
|
|
||
| var referenceWaveFilename = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/test_wavs/leijun-1.wav"; | ||
| var reader = new WaveReader(referenceWaveFilename); | ||
|
|
||
| OfflineTtsGenerationConfig genConfig = new OfflineTtsGenerationConfig(); | ||
| genConfig.ReferenceAudio = reader.Samples; | ||
| genConfig.ReferenceSampleRate = reader.SampleRate; | ||
| genConfig.ReferenceText = "那还是三十六年前, 一九八七年. 我呢考上了武汉大学的计算机系."; | ||
| genConfig.NumSteps = 4; | ||
| genConfig.Extra["min_char_in_sentence"] = "10"; | ||
|
|
||
| var tts = new OfflineTts(config); | ||
| var text = "小米的价值观是真诚, 热爱. 真诚,就是不欺人也不自欺. 热爱, 就是全心投入并享受其中."; | ||
|
|
||
| Console.WriteLine(PortAudio.VersionInfo.versionText); | ||
| PortAudio.Initialize(); | ||
| Console.WriteLine($"Number of devices: {PortAudio.DeviceCount}"); | ||
|
|
||
| for (int i = 0; i != PortAudio.DeviceCount; ++i) | ||
| { | ||
| Console.WriteLine($" Device {i}"); | ||
| DeviceInfo deviceInfo = PortAudio.GetDeviceInfo(i); | ||
| Console.WriteLine($" Name: {deviceInfo.name}"); | ||
| Console.WriteLine($" Max output channels: {deviceInfo.maxOutputChannels}"); | ||
| Console.WriteLine($" Default sample rate: {deviceInfo.defaultSampleRate}"); | ||
| } | ||
| int deviceIndex = PortAudio.DefaultOutputDevice; | ||
| if (deviceIndex == PortAudio.NoDevice) | ||
| { | ||
| Console.WriteLine("No default output device found. Please use ../zipvoice-tts instead"); | ||
| Environment.Exit(1); | ||
| } | ||
|
|
||
| var info = PortAudio.GetDeviceInfo(deviceIndex); | ||
| Console.WriteLine(); | ||
| Console.WriteLine($"Use output default device {deviceIndex} ({info.name})"); | ||
|
|
||
| var param = new StreamParameters(); | ||
| param.device = deviceIndex; | ||
| param.channelCount = 1; | ||
| param.sampleFormat = SampleFormat.Float32; | ||
| param.suggestedLatency = info.defaultLowOutputLatency; | ||
| param.hostApiSpecificStreamInfo = IntPtr.Zero; | ||
|
|
||
| var dataItems = new BlockingCollection<float[]>(); | ||
|
|
||
|
|
||
| var myCallback = (IntPtr samples, int n, float progress, IntPtr arg) => | ||
| { | ||
| Console.WriteLine($"Progress {progress * 100}%"); | ||
|
|
||
| float[] data = new float[n]; | ||
| Marshal.Copy(samples, data, 0, n); | ||
| dataItems.Add(data); | ||
|
|
||
| // 1 means to keep generating | ||
| // 0 means to stop generating | ||
| return 1; | ||
| }; | ||
|
|
||
| var playFinished = false; | ||
|
|
||
|
Comment on lines
+94
to
+95
|
||
| float[]? lastSampleArray = null; | ||
| int lastIndex = 0; | ||
|
|
||
| PortAudioSharp.Stream.Callback playCallback = (IntPtr input, IntPtr output, | ||
| UInt32 frameCount, | ||
| ref StreamCallbackTimeInfo timeInfo, | ||
| StreamCallbackFlags statusFlags, | ||
| IntPtr userData | ||
| ) => | ||
| { | ||
| if (dataItems.IsCompleted && lastSampleArray == null && lastIndex == 0) | ||
| { | ||
| Console.WriteLine("Finished playing"); | ||
| playFinished = true; | ||
| return StreamCallbackResult.Complete; | ||
|
Comment on lines
+94
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify shared flag is accessed from different execution contexts without synchronization.
rg -n -C3 '\bplayFinished\b|Callback|while \(!playFinished\)' dotnet-examples/zipvoice-tts-play/Program.csRepository: k2-fsa/sherpa-onnx Length of output: 2127 🏁 Script executed: #!/bin/bash
# Examine complete context of playFinished usage and verify threading model
cat -n dotnet-examples/zipvoice-tts-play/Program.cs | sed -n '88,195p'Repository: k2-fsa/sherpa-onnx Length of output: 3702 Synchronize
Use Proposed synchronization fix- var playFinished = false;
+ using var playbackDone = new ManualResetEventSlim(false);
PortAudioSharp.Stream.Callback playCallback = ...
{
if (dataItems.IsCompleted && lastSampleArray == null && lastIndex == 0)
{
Console.WriteLine("Finished playing");
- playFinished = true;
+ playbackDone.Set();
return StreamCallbackResult.Complete;
}
...
};
- while (!playFinished)
- {
- Thread.Sleep(100);
- }
+ playbackDone.Wait();🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| int expected = Convert.ToInt32(frameCount); | ||
| int i = 0; | ||
|
|
||
| while ((lastSampleArray != null || dataItems.Count != 0) && (i < expected)) | ||
| { | ||
| int needed = expected - i; | ||
|
|
||
| if (lastSampleArray != null) | ||
| { | ||
| int remaining = lastSampleArray.Length - lastIndex; | ||
| if (remaining >= needed) | ||
| { | ||
| float[] thisBlock = lastSampleArray.Skip(lastIndex).Take(needed).ToArray(); | ||
| lastIndex += needed; | ||
|
Comment on lines
+120
to
+126
|
||
| if (lastIndex == lastSampleArray.Length) | ||
| { | ||
| lastSampleArray = null; | ||
| lastIndex = 0; | ||
| } | ||
|
|
||
| Marshal.Copy(thisBlock, 0, IntPtr.Add(output, i * sizeof(float)), needed); | ||
| return StreamCallbackResult.Continue; | ||
|
Comment on lines
+125
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of LINQ ( Marshal.Copy(lastSampleArray, lastIndex, IntPtr.Add(output, i * sizeof(float)), needed);
lastIndex += needed;
if (lastIndex == lastSampleArray.Length)
{
lastSampleArray = null;
lastIndex = 0;
}
return StreamCallbackResult.Continue; |
||
| } | ||
|
|
||
| float[] thisBlock2 = lastSampleArray.Skip(lastIndex).Take(remaining).ToArray(); | ||
| lastIndex = 0; | ||
| lastSampleArray = null; | ||
|
|
||
| Marshal.Copy(thisBlock2, 0, IntPtr.Add(output, i * sizeof(float)), remaining); | ||
|
Comment on lines
+137
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the previous comment, using Marshal.Copy(lastSampleArray, lastIndex, IntPtr.Add(output, i * sizeof(float)), remaining); |
||
| i += remaining; | ||
| continue; | ||
| } | ||
|
|
||
| if (dataItems.Count != 0) | ||
| { | ||
| lastSampleArray = dataItems.Take(); | ||
| lastIndex = 0; | ||
| } | ||
| } | ||
|
|
||
| if (i < expected) | ||
| { | ||
| int sizeInBytes = (expected - i) * 4; | ||
| Marshal.Copy(new byte[sizeInBytes], 0, IntPtr.Add(output, i * sizeof(float)), sizeInBytes); | ||
| } | ||
|
Comment on lines
+153
to
+157
|
||
|
|
||
| return StreamCallbackResult.Continue; | ||
| }; | ||
|
|
||
| PortAudioSharp.Stream stream = new PortAudioSharp.Stream(inParams: null, outParams: param, sampleRate: tts.SampleRate, | ||
| framesPerBuffer: 0, | ||
| streamFlags: StreamFlags.ClipOff, | ||
| callback: playCallback, | ||
| userData: IntPtr.Zero | ||
| ); | ||
|
|
||
| stream.Start(); | ||
|
|
||
| var callback = new OfflineTtsCallbackProgressWithArg(myCallback); | ||
| var audio = tts.GenerateWithConfig(text, genConfig, callback); | ||
|
|
||
| var outputFilename = "./generated-zipvoice-zh-en-play.wav"; | ||
| var ok = audio.SaveToWaveFile(outputFilename); | ||
|
|
||
| if (ok) | ||
| { | ||
| Console.WriteLine($"Wrote to {outputFilename} succeeded!"); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"Failed to write {outputFilename}"); | ||
| } | ||
|
|
||
| dataItems.CompleteAdding(); | ||
|
|
||
|
Comment on lines
+169
to
+187
|
||
| while (!playFinished) | ||
| { | ||
| Thread.Sleep(100); | ||
| } | ||
|
Comment on lines
+188
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The For example, you could use a static |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||||
| set -ex | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if [ ! -f ./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/encoder.int8.onnx ]; then | ||||||||||||||||||||||||||||||||||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 | ||||||||||||||||||||||||||||||||||
| tar xvf sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 | ||||||||||||||||||||||||||||||||||
| rm sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if [ ! -f ./vocos_24khz.onnx ]; then | ||||||||||||||||||||||||||||||||||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use fail-fast Line 5 and Line 11 should fail on HTTP errors to avoid continuing with bad artifacts. 🛠️ Proposed change- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2
+ curl -fSL --retry 3 --retry-delay 2 -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2
@@
- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx
+ curl -fSL --retry 3 --retry-delay 2 -O https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| dotnet run | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|
|
||||||
| <PropertyGroup> | ||||||
| <OutputType>Exe</OutputType> | ||||||
| <TargetFramework>net8.0</TargetFramework> | ||||||
| <RootNamespace>zipvoice_tts_play</RootNamespace> | ||||||
| <ImplicitUsings>enable</ImplicitUsings> | ||||||
| <Nullable>enable</Nullable> | ||||||
| </PropertyGroup> | ||||||
|
|
||||||
| <ItemGroup> | ||||||
| <PackageReference Include="PortAudioSharp2" Version="*" /> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
# Verify wildcard PackageReference usage across the repo
rg -n --glob '*.csproj' '<PackageReference[^>]*Version="\*"'Repository: k2-fsa/sherpa-onnx Length of output: 1232 🌐 Web query:
💡 Result: The latest stable NuGet version of PortAudioSharp2 is 1.0.6, released on October 16, 2025. Using Version="" (a floating version) in SDK-style .csproj files is not recommended for reproducible builds. Floating versions like "" resolve to the highest available version matching the pattern at restore time, which can lead to different package versions across restores, environments, or after new versions are published. This breaks reproducibility, as the same project may build with varying dependencies. For reproducible builds, pin to specific versions (e.g., Version="1.0.6") or use lock files (RestorePackagesWithLockFile) with --locked-mode in CI. Official guidance emphasizes lowest applicable versions without upper bounds but avoids floating versions for consistency. Enable Central Package Management or Dependabot for updates instead. Citations:
Pin PortAudioSharp2 to a specific version instead of using a wildcard. Line 12 uses 🔒 Suggested fix- <PackageReference Include="PortAudioSharp2" Version="*" />
+ <PackageReference Include="PortAudioSharp2" Version="1.0.6" />Note: This issue appears in multiple project files across the repository ( 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| </ItemGroup> | ||||||
|
|
||||||
| <ItemGroup> | ||||||
| <ProjectReference Include="..\Common\Common.csproj" /> | ||||||
| </ItemGroup> | ||||||
|
|
||||||
| </Project> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Copyright (c) 2026 Xiaomi Corporation | ||
| // | ||
| // This file shows how to use a non-streaming ZipVoice model | ||
| // for zero-shot text-to-speech. | ||
| // Please refer to | ||
| // https://k2-fsa.github.io/sherpa/onnx/tts/zipvoice.html | ||
| // and | ||
| // https://github.com/k2-fsa/sherpa-onnx/releases/tag/tts-models | ||
| // to download pre-trained models | ||
| using SherpaOnnx; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| class ZipVoiceTtsDemo | ||
| { | ||
| static void Main(string[] args) | ||
| { | ||
| TestZhEn(); | ||
| } | ||
|
|
||
| static void TestZhEn() | ||
| { | ||
| var config = new OfflineTtsConfig(); | ||
| config.Model.ZipVoice.Tokens = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/tokens.txt"; | ||
| config.Model.ZipVoice.Encoder = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/encoder.int8.onnx"; | ||
| config.Model.ZipVoice.Decoder = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/decoder.int8.onnx"; | ||
| config.Model.ZipVoice.Vocoder = "./vocos_24khz.onnx"; | ||
| config.Model.ZipVoice.DataDir = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/espeak-ng-data"; | ||
| config.Model.ZipVoice.Lexicon = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/lexicon.txt"; | ||
|
|
||
| config.Model.NumThreads = 2; | ||
| config.Model.Debug = 1; | ||
| config.Model.Provider = "cpu"; | ||
|
|
||
| var referenceWaveFilename = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/test_wavs/leijun-1.wav"; | ||
| var reader = new WaveReader(referenceWaveFilename); | ||
|
|
||
| OfflineTtsGenerationConfig genConfig = new OfflineTtsGenerationConfig(); | ||
| genConfig.ReferenceAudio = reader.Samples; | ||
| genConfig.ReferenceSampleRate = reader.SampleRate; | ||
| genConfig.ReferenceText = "那还是三十六年前, 一九八七年. 我呢考上了武汉大学的计算机系."; | ||
| genConfig.NumSteps = 4; | ||
| genConfig.Extra["min_char_in_sentence"] = "10"; | ||
|
|
||
| var tts = new OfflineTts(config); | ||
| var text = "小米的价值观是真诚, 热爱. 真诚,就是不欺人也不自欺. 热爱, 就是全心投入并享受其中."; | ||
|
|
||
| var myCallback = (IntPtr samples, int n, float progress, IntPtr arg) => | ||
| { | ||
| float[] data = new float[n]; | ||
| Marshal.Copy(samples, data, 0, n); | ||
|
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+49
to
+50
|
||
| Console.WriteLine($"Progress {progress * 100}%"); | ||
|
|
||
| // 1 means to keep generating | ||
| // 0 means to stop generating | ||
| return 1; | ||
| }; | ||
|
|
||
| var callback = new OfflineTtsCallbackProgressWithArg(myCallback); | ||
|
|
||
| var audio = tts.GenerateWithConfig(text, genConfig, callback); | ||
|
|
||
| var outputFilename = "./generated-zipvoice-zh-en.wav"; | ||
| var ok = audio.SaveToWaveFile(outputFilename); | ||
|
|
||
| if (ok) | ||
| { | ||
| Console.WriteLine($"Wrote to {outputFilename} succeeded!"); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"Failed to write {outputFilename}"); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| #!/usr/bin/env bash | ||
| set -ex | ||
|
|
||
| if [ ! -f ./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia/encoder.int8.onnx ]; then | ||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 | ||
| tar xvf sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 | ||
| rm sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 | ||
| fi | ||
|
|
||
| if [ ! -f ./vocos_24khz.onnx ]; then | ||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx | ||
| fi | ||
|
Comment on lines
+5
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use fail-fast Line 5 and Line 11 can succeed on HTTP errors without 🛠️ Proposed change- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2
+ curl -fSL --retry 3 --retry-delay 2 -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2
@@
- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx
+ curl -fSL --retry 3 --retry-delay 2 -O https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx🤖 Prompt for AI Agents |
||
|
|
||
| dotnet run | ||
|
Comment on lines
+1
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This script is identical to |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formatting of the new list items is inconsistent with other entries in the list. For better readability and consistency, the description for each item should start on a new line and be indented with two spaces.