#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::io::Write;
use std::sync::{Arc, Mutex};
use audio_cpp::{Backend, ModelFamily, Registry, Request, RunMode, StreamEvent, TaskKind};
fn write_wav_pcm16(
path: &str,
samples: &[f32],
sample_rate: i32,
channels: u16,
) -> std::io::Result<()> {
let bytes_per_sample = 2u32; let block_align = bytes_per_sample * channels as u32;
let byte_rate = sample_rate as u32 * block_align;
let data_len = samples.len() as u32 * bytes_per_sample;
let mut f = std::fs::File::create(path)?;
f.write_all(b"RIFF")?;
f.write_all(&(36u32 + data_len).to_le_bytes())?;
f.write_all(b"WAVE")?;
f.write_all(b"fmt ")?;
f.write_all(&16u32.to_le_bytes())?;
f.write_all(&1u16.to_le_bytes())?; f.write_all(&channels.to_le_bytes())?;
f.write_all(&(sample_rate as u32).to_le_bytes())?;
f.write_all(&byte_rate.to_le_bytes())?;
f.write_all(&(block_align as u16).to_le_bytes())?;
f.write_all(&(bytes_per_sample as u16 * 8).to_le_bytes())?; f.write_all(b"data")?;
f.write_all(&data_len.to_le_bytes())?;
for &s in samples {
let v = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
f.write_all(&v.to_le_bytes())?;
}
Ok(())
}
fn main() -> Result<(), audio_cpp::Error> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 4 {
eprintln!("用法: tts_streaming <voxcpm2-q8_0.gguf> <out.wav> <text>");
std::process::exit(1);
}
let model_path = &args[1];
let out_path = &args[2];
let text = &args[3];
let registry = Registry::new()?;
println!("模型族: {:?}", registry.families()?);
let model = registry.load(model_path, Some(ModelFamily::Voxcpm2), None)?;
println!("模型加载成功: {model_path}");
println!("元数据: {:?}", model.metadata()?);
let mut session = model.create_task_session(
TaskKind::Tts,
RunMode::Streaming,
Backend::Cpu,
0, 4, None,
)?;
println!(
"会话: family={} task={} mode={}",
session.family(),
session.task_kind(),
session.run_mode()
);
let policy = session.streaming_policy()?;
println!("流式策略: {:?}(输入 none → 无需 process_audio)", policy);
let chunks = Arc::new(Mutex::new(Vec::<StreamEvent>::new()));
let collector = Arc::clone(&chunks);
session.set_event_callback(Some(move |ev: StreamEvent| {
if !ev.named_audio_outputs.is_empty() {
collector.lock().unwrap().push(ev);
}
}));
session.start(Request::tts(text).option("retry_badcase", false))?;
println!("请求文本: {text}");
let result = session.finish()?;
session.reset();
if let Some(audio) = &result.audio_output {
let channels = audio.channels.max(1) as u16;
let samples = audio.samples.as_deref().unwrap_or(&[]);
write_wav_pcm16(out_path, samples, audio.sample_rate, channels)
.map_err(|e| audio_cpp::Error::Ffi(format!("写 {out_path} 失败: {e}")))?;
println!(
"已写入 {out_path}: {}Hz {}ch {}采样({} 秒)",
audio.sample_rate,
channels,
samples.len(),
samples.len() as f64 / audio.sample_rate.max(1) as f64
);
} else {
println!("(无合并音频输出;报告信息: {result:?})");
}
let chunks = chunks.lock().unwrap();
println!("=== 逐块事件 ({} 个) ===", chunks.len());
for (i, ev) in chunks.iter().enumerate() {
for named in &ev.named_audio_outputs {
let samples = named.audio.samples.as_deref().unwrap_or(&[]);
let chunk_path = format!("{out_path}_chunk{i}.wav");
let channels = named.audio.channels.max(1) as u16;
write_wav_pcm16(&chunk_path, samples, named.audio.sample_rate, channels)
.map_err(|e| audio_cpp::Error::Ffi(format!("写 {chunk_path} 失败: {e}")))?;
println!(" {} → {} ({} 采样)", named.id, chunk_path, samples.len());
}
}
Ok(())
}