audio_cpp/session.rs
1//! 任务会话(Session):离线执行 + 流式处理。
2//!
3//! 会话由 [`crate::Model::create_task_session`] 创建,按其任务与运行模式
4//! 分为:
5//! - 离线(`RunMode::Offline`):`run()` 一次执行、一次结果;
6//! - 流式(`RunMode::Streaming`):`start()` → `process_audio()` → `finish()`,
7//! 期间通过事件回调或 `process_audio` 的返回值接收流事件。
8//!
9//! # 事件回调
10//!
11//! 流式会话可在创建时调用 [`Session::set_event_callback`] 注册一个 Rust 闭包,
12//! shim 会在每个流事件发生时回传给该闭包(详见 `capi.h` 的
13//! `audiocpp_stream_event_cb`)。回调数据经 `Mutex` 保护,可从 C++ 侧线程调用。
14
15use std::ffi::{CStr, c_char, c_void};
16use std::os::raw::c_int;
17use std::panic::{AssertUnwindSafe, catch_unwind};
18use std::ptr;
19use std::sync::{Arc, Mutex};
20
21use audio_cpp_sys::*;
22
23use crate::error::Error;
24use crate::ffi;
25use crate::model::Model;
26use crate::registry::RegistryGuard;
27use crate::request::IntoRequest;
28use crate::types::{Backend, RunMode, StreamEvent, StreamingPolicy, TaskKind, TaskResult};
29
30/// 事件回调的底层存储(box 在堆上,地址稳定,可跨线程)。
31struct EventSinkInner {
32 cb: Mutex<Box<dyn FnMut(StreamEvent) + Send>>,
33}
34
35/// shim 事件回调的 C 桥接函数。
36///
37/// `user_data` 指向 `EventSinkInner`。事件 JSON 由 shim 一次性构造,转为
38/// Rust 类型后交给用户闭包。
39///
40/// 用户闭包中的 panic 会被捕获并忽略,**不会**向上传播:panic 穿过
41/// `extern "C"` 是未定义行为,回调内 panic 一律在此截停。
42unsafe extern "C" fn stream_event_cb(
43 user_data: *mut c_void,
44 event_json: *const c_char,
45 _is_final: c_int,
46) {
47 if user_data.is_null() || event_json.is_null() {
48 return;
49 }
50 // SAFETY: shim 保证 user_data 是 EventSinkInner 指针、event_json 是 NUL 结尾 JSON。
51 let inner = user_data.cast::<EventSinkInner>();
52 // SAFETY: 同上,event_json 是 NUL 结尾的 UTF-8 JSON。
53 let json = unsafe { CStr::from_ptr(event_json) }
54 .to_string_lossy()
55 .into_owned();
56 let Ok(event) = serde_json::from_str::<StreamEvent>(&json) else {
57 return; // 事件 JSON 契约不符:忽略
58 };
59 // SAFETY: 同上,inner 在会话生命周期内有效(EventSink 析构前回调已解绑)。
60 let mut guard =
61 unsafe { (*inner).cb.lock() }.unwrap_or_else(std::sync::PoisonError::into_inner);
62 let _ = catch_unwind(AssertUnwindSafe(|| (guard)(event)));
63}
64
65/// 任务会话。
66pub struct Session {
67 raw: *mut audiocpp_session,
68 /// 事件回调的持有者;析构时负责释放并确保 C 侧不再引用。
69 /// 用 `Mutex` 保护指针本身,使 `set_event_callback` 能以 `&self` 调用
70 /// (内部可变性),从而在持有 `&Session` 的线程上注册回调。
71 event_sink: Mutex<Option<*mut EventSinkInner>>,
72 /// 串行化回调的装配 / 拆除 / 析构全过程:`set_event_callback` 与
73 /// `Drop` 都先持有它再动 `event_sink` 与 C 侧绑定,避免两线程并发
74 /// replace 造成“引擎指向 A、Rust 存 B”或 Box 泄漏。
75 sink_swap: Mutex<()>,
76 /// 共享持有 C 注册表:`Model` / `Registry` 先释放不影响会话继续工作。
77 _guard: RegistryGuard,
78 /// 创建会话时请求的计算后端(见 [`Session::backend`])。
79 backend: Backend,
80}
81
82// Session 持有回调 box(要求 Send)与 C 句柄。跨线程转移所有权是安全的,
83// 但用户应避免同时对同一会话做流式调用与回调锁内嵌套调用。
84// (`_guard` 本身是 Send 而非 Sync,`Session` 的 auto-trait 与之前一致。)
85unsafe impl Send for Session {}
86
87impl Session {
88 /// 从原始 C 句柄包装(仅内部使用)。
89 pub(crate) fn from_raw(
90 raw: *mut audiocpp_session,
91 guard: RegistryGuard,
92 backend: Backend,
93 ) -> Self {
94 Self {
95 raw,
96 event_sink: Mutex::new(None),
97 sink_swap: Mutex::new(()),
98 _guard: guard,
99 backend,
100 }
101 }
102
103 /// 模型族名(借用的 C 字符串,即刻拷贝为 Rust 字符串)。
104 pub fn family(&self) -> String {
105 unsafe {
106 let p = audiocpp_session_family(self.raw);
107 if p.is_null() {
108 String::new()
109 } else {
110 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
111 }
112 }
113 }
114
115 /// 任务种类(如 `"vad"`)。
116 pub fn task_kind(&self) -> String {
117 unsafe {
118 let p = audiocpp_session_task_kind(self.raw);
119 if p.is_null() {
120 String::new()
121 } else {
122 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
123 }
124 }
125 }
126
127 /// 运行模式(如 `"offline"` / `"streaming"`)。
128 pub fn run_mode(&self) -> String {
129 unsafe {
130 let p = audiocpp_session_run_mode(self.raw);
131 if p.is_null() {
132 String::new()
133 } else {
134 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
135 }
136 }
137 }
138
139 /// 创建会话时请求的计算后端。
140 ///
141 /// 这是传给 [`crate::Model::create_task_session`] 的 `backend` 参数原样
142 /// 记录,不经过上游再查询——C ABI 与上游引擎目前均无“实际落定后端”
143 /// 查询接口。显式传 `Backend::Cpu/Vulkan/...` 时返回值即实际后端;
144 /// 传 `Backend::Best` 时引擎内部按可用设备解析,返回值只能说明
145 /// “请求的是自动选择”,不要据此显示具体设备名。
146 pub fn backend(&self) -> Backend {
147 self.backend
148 }
149
150 /// 流式策略描述(输入/输出类型、推荐分块大小)。
151 ///
152 /// # Errors
153 ///
154 /// C ABI 调用失败或返回的 JSON 无法解析时返回对应 [`Error`] 变体。
155 pub fn streaming_policy(&self) -> Result<StreamingPolicy, Error> {
156 let mut out: *mut c_char = ptr::null_mut();
157 ffi::check_rc(unsafe { audiocpp_session_streaming_policy_json(self.raw, &mut out) })?;
158 let json = unsafe { ffi::take_string(out)? };
159 serde_json::from_str(&json).map_err(Error::from)
160 }
161
162 /// 为一次请求做准备(离线与流式共用)。
163 ///
164 /// `request` 为 [`crate::request::IntoRequest`],可用类型化的
165 /// [`crate::request::Request`] 枚举(推荐,每个任务一种变体),或任意
166 /// JSON 字符串。携带 `audio` / `text` / `voice` 等输入,或用 `audio_path`
167 /// 指向本地 WAV 文件。空请求可传 `()`。
168 ///
169 /// # Errors
170 ///
171 /// 请求序列化失败、路径含 NUL,或 C ABI 调用失败时返回对应 [`Error`] 变体。
172 pub fn prepare<R: IntoRequest>(&self, request: R) -> Result<(), Error> {
173 let json = request.into_request()?.to_json()?;
174 let req_c = ffi::cstring(&json)?;
175 ffi::check_rc(unsafe {
176 audiocpp_session_prepare(self.raw, req_c.as_ptr() as *const c_char)
177 })
178 }
179
180 /// 注册流式事件回调;调用 `None` 清除。
181 ///
182 /// 回调会在每次流事件产生时被调用(可能来自 C++ 侧线程),其内容是
183 /// 解析为 [`StreamEvent`] 的对象。回调内不得再调用本会话的方法
184 /// (含 `set_event_callback`/`reset`,否则可能死锁);回调内的 panic
185 /// 会被捕获忽略,不会传播。
186 ///
187 /// 以 `&self` 提供(内部有 `Mutex` 保护),因此可在持有 `&Session`
188 /// 的任意线程上注册 / 更换回调。同一个会话只保留一份回调。
189 pub fn set_event_callback<F>(&self, cb: Option<F>)
190 where
191 F: FnMut(StreamEvent) + Send + 'static,
192 {
193 let _swap = self
194 .sink_swap
195 .lock()
196 .unwrap_or_else(std::sync::PoisonError::into_inner);
197 // 先清理旧回调,保证 C 侧不再引用旧的 user_data。
198 let old = self.event_sink_take();
199 if let Some(old) = old {
200 unsafe {
201 audiocpp_session_set_event_sink(self.raw, None, ptr::null_mut());
202 drop(Box::from_raw(old));
203 }
204 }
205 if let Some(cb) = cb {
206 let inner = Box::into_raw(Box::new(EventSinkInner {
207 cb: Mutex::new(Box::new(cb)),
208 }));
209 unsafe {
210 audiocpp_session_set_event_sink(
211 self.raw,
212 Some(stream_event_cb),
213 inner.cast::<c_void>(),
214 );
215 }
216 *self
217 .event_sink
218 .lock()
219 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(inner);
220 }
221 }
222
223 /// 开始一次流式任务(先 `prepare` 再进入流式读取循环)。
224 ///
225 /// `request` 同 [`Session::prepare`],类型化枚举或 JSON 字符串均可;
226 /// 空请求传 `()`。
227 ///
228 /// # Errors
229 ///
230 /// 请求序列化失败、路径含 NUL,或 C ABI 调用失败时返回对应 [`Error`] 变体。
231 pub fn start<R: IntoRequest>(&self, request: R) -> Result<(), Error> {
232 let json = request.into_request()?.to_json()?;
233 let req_c = ffi::cstring(&json)?;
234 ffi::check_rc(unsafe { audiocpp_session_start(self.raw, req_c.as_ptr() as *const c_char) })
235 }
236
237 /// 将一段音频送入流式会话,返回该块触发的第一个事件(若有)。
238 ///
239 /// `samples` 为 `float` 采样(-1..1),`start_sample` 为该块在输入流中的
240 /// 起始采样点。若同时注册了事件回调,事件会经回调到达;返回值是
241 /// shim 同步返回的事件 JSON。
242 ///
243 /// # Errors
244 ///
245 /// C ABI 调用失败或返回的 JSON 无法解析时返回对应 [`Error`] 变体。
246 pub fn process_audio(
247 &self,
248 samples: &[f32],
249 sample_rate: i32,
250 channels: i32,
251 start_sample: i64,
252 ) -> Result<Option<StreamEvent>, Error> {
253 let mut out: *mut c_char = ptr::null_mut();
254 // 空输入显式传 null,避免对悬垂指针做算术(C++ 侧视 count==0 跳过读取)。
255 let ptr = if samples.is_empty() {
256 ptr::null()
257 } else {
258 samples.as_ptr()
259 };
260 ffi::check_rc(unsafe {
261 audiocpp_session_process_audio(
262 self.raw,
263 ptr,
264 samples.len(),
265 sample_rate,
266 channels,
267 start_sample,
268 &mut out,
269 )
270 })?;
271 if out.is_null() {
272 return Ok(None);
273 }
274 let json = unsafe { ffi::take_string(out)? };
275 Ok(Some(serde_json::from_str(&json).map_err(Error::from)?))
276 }
277
278 /// 结束流式会话,返回最终 TaskResult。
279 ///
280 /// # Errors
281 ///
282 /// C ABI 调用失败或返回的 JSON 无法解析时返回对应 [`Error`] 变体。
283 /// `check_rc` 通过却无 payload 属于 shim 违约,同样报错而非伪造空结果。
284 pub fn finish(&self) -> Result<TaskResult, Error> {
285 let mut out: *mut c_char = ptr::null_mut();
286 ffi::check_rc(unsafe { audiocpp_session_finish(self.raw, &mut out) })?;
287 if out.is_null() {
288 return Err(Error::Ffi(ffi::last_error()));
289 }
290 let json = unsafe { ffi::take_string(out)? };
291 serde_json::from_str(&json).map_err(Error::from)
292 }
293
294 /// 离线执行一次请求,返回 TaskResult。
295 ///
296 /// 相当于 `prepare` + 一次完整执行(shim 内部已做 prepare)。`request`
297 /// 同 [`Session::prepare`],类型化枚举或 JSON 字符串均可。
298 ///
299 /// # Errors
300 ///
301 /// 请求序列化失败、路径含 NUL,或 C ABI 调用失败时返回对应 [`Error`] 变体。
302 pub fn run_offline<R: IntoRequest>(&self, request: R) -> Result<TaskResult, Error> {
303 let json = request.into_request()?.to_json()?;
304 let req_c = ffi::cstring(&json)?;
305 let mut out: *mut c_char = ptr::null_mut();
306 ffi::check_rc(unsafe {
307 audiocpp_session_run_offline(self.raw, req_c.as_ptr() as *const c_char, &mut out)
308 })?;
309 if out.is_null() {
310 return Err(Error::Ffi(ffi::last_error()));
311 }
312 let json = unsafe { ffi::take_string(out)? };
313 serde_json::from_str(&json).map_err(Error::from)
314 }
315
316 /// 重置流式会话内部状态(可复用会话对象重新开始)。
317 ///
318 /// # Errors
319 ///
320 /// C ABI 调用失败时返回对应 [`Error`] 变体。
321 pub fn reset(&self) -> Result<(), Error> {
322 ffi::check_rc(unsafe { audiocpp_session_reset(self.raw) })
323 }
324
325 /// 取出并归还事件回调指针(内部用;调用方负责释放)。
326 fn event_sink_take(&self) -> Option<*mut EventSinkInner> {
327 self.event_sink
328 .lock()
329 .unwrap_or_else(std::sync::PoisonError::into_inner)
330 .take()
331 }
332}
333
334impl Drop for Session {
335 fn drop(&mut self) {
336 // 与 set_event_callback 互斥:先解绑再释放,避免并发 replace 错配。
337 // 注意:析构时必须已无在途回调(调用方不得在流式调用并发时 drop),
338 // 否则 C++ 工作线程的在途回调仍可能引用已释放的 user_data。
339 let _swap = self
340 .sink_swap
341 .lock()
342 .unwrap_or_else(std::sync::PoisonError::into_inner);
343 // 先清除事件回调,避免 C 侧在会话析构后再引用 user_data。
344 if let Some(inner) = self.event_sink_take() {
345 unsafe {
346 audiocpp_session_set_event_sink(self.raw, None, ptr::null_mut());
347 drop(Box::from_raw(inner));
348 }
349 }
350 unsafe {
351 audiocpp_session_free(self.raw);
352 }
353 }
354}
355
356/// 流式会话的便捷封装。
357///
358/// 包装一个 [`Session`],自动注册内部事件收集器,把 shim 的逐事件回调
359/// 缓冲为 Vec,使调用方无需手动管理回调 + 锁。典型用法(流式 ASR):
360///
361/// ```
362/// use audio_cpp::{Backend, Model, Registry, Request, RunMode, TaskKind};
363/// use audio_cpp::session::StreamingSession;
364/// # fn f() -> Result<(), audio_cpp::Error> {
365/// # let registry = Registry::new()?;
366/// # let model = registry.load("./qwen3-asr-0.6b-q8_0.gguf", None, None)?;
367/// let session = model.create_task_session(
368/// TaskKind::Asr, RunMode::Streaming, Backend::Cpu, 0, 1, None,
369/// )?;
370/// let mut stream = StreamingSession::from_session(session);
371///
372/// stream.start(Request::stream_asr().option("language", "auto"))?;
373/// // 每块音频送入后,取出该块触发的全部事件(partial_text / is_final)
374/// let samples: Vec<f32> = vec![0.0; 512];
375/// let events = stream.push_audio(&samples, 16000, 1, 0)?;
376/// for ev in &events {
377/// if let Some(t) = &ev.partial_text {
378/// println!("增量: {}", t.text);
379/// }
380/// }
381/// let result = stream.finish()?;
382/// # Ok(()) }
383/// ```
384///
385/// 与底层 [`Session::process_audio`] 不同,`push_audio` 返回该块触发的
386/// **所有**事件(经内部回调收集),而不是单个事件。不消费音频块的模型族
387/// (如流式 TTS)可跳过 `push_audio`,直接 `start` → `finish`。
388pub struct StreamingSession {
389 session: Session,
390 events: Arc<Mutex<Vec<StreamEvent>>>,
391}
392
393unsafe impl Send for StreamingSession {}
394
395impl StreamingSession {
396 /// 从现有会话包装(流式任务)。
397 pub fn from_session(session: Session) -> Self {
398 let events: Arc<Mutex<Vec<StreamEvent>>> = Arc::new(Mutex::new(Vec::new()));
399 let sink = events.clone();
400 session.set_event_callback(Some(move |ev| {
401 if let Ok(mut v) = sink.lock() {
402 v.push(ev);
403 }
404 }));
405 Self { session, events }
406 }
407
408 /// 从模型创建流式会话并包装(等价于
409 /// `create_task_session(Streaming)` + [`Self::from_session`])。
410 ///
411 /// # Errors
412 ///
413 /// 底层无法创建流式会话时返回对应 [`Error`] 变体。
414 pub fn from_model(
415 model: &Model,
416 task: TaskKind,
417 backend: Backend,
418 device: i32,
419 threads: i32,
420 session_options: Option<&str>,
421 ) -> Result<Self, Error> {
422 let session = model.create_task_session(
423 task,
424 RunMode::Streaming,
425 backend,
426 device,
427 threads,
428 session_options,
429 )?;
430 Ok(Self::from_session(session))
431 }
432
433 /// 开始流式任务(等价于 [`Session::start`])。
434 ///
435 /// # Errors
436 ///
437 /// 请求序列化失败、路径含 NUL,或 C ABI 调用失败时返回对应 [`Error`] 变体。
438 pub fn start<R: IntoRequest>(&self, request: R) -> Result<(), Error> {
439 self.session.start(request)
440 }
441
442 /// 送入一段音频,返回该块触发的全部流事件。
443 ///
444 /// 等价于先清空内部缓冲、调用 [`Session::process_audio`]、再取出缓冲
445 /// 中的全部事件。`samples` 为 `float` 采样,`start_sample` 为相对输入
446 /// 流的起始采样点。
447 ///
448 /// # Errors
449 ///
450 /// C ABI 调用失败时返回对应 [`Error`] 变体。
451 pub fn push_audio(
452 &self,
453 samples: &[f32],
454 sample_rate: i32,
455 channels: i32,
456 start_sample: i64,
457 ) -> Result<Vec<StreamEvent>, Error> {
458 self.events
459 .lock()
460 .unwrap_or_else(std::sync::PoisonError::into_inner)
461 .clear();
462 self.session
463 .process_audio(samples, sample_rate, channels, start_sample)?;
464 Ok(self
465 .events
466 .lock()
467 .unwrap_or_else(std::sync::PoisonError::into_inner)
468 .drain(..)
469 .collect())
470 }
471
472 /// 结束流式会话,返回最终结果(等价于 [`Session::finish`])。
473 ///
474 /// # Errors
475 ///
476 /// C ABI 调用失败时返回对应 [`Error`] 变体。
477 pub fn finish(&self) -> Result<TaskResult, Error> {
478 self.session.finish()
479 }
480
481 /// 重置会话(等价于 [`Session::reset`]),并清空内部缓冲。
482 ///
483 /// # Errors
484 ///
485 /// C ABI 调用失败时返回对应 [`Error`] 变体。
486 pub fn reset(&self) -> Result<(), Error> {
487 self.session.reset()?;
488 self.events
489 .lock()
490 .unwrap_or_else(std::sync::PoisonError::into_inner)
491 .clear();
492 Ok(())
493 }
494
495 /// 底层会话引用。
496 ///
497 /// 注意:经此引用调用 [`Session::set_event_callback`] 会替换
498 /// [`StreamingSession`] 的内部事件收集器,导致 `push_audio` 收不到事件。
499 /// 如需自定义回调,请直接使用 [`Session`] 而非本包装。
500 pub fn session(&self) -> &Session {
501 &self.session
502 }
503
504 /// 创建时请求的计算后端(等价于 [`Session::backend`])。
505 pub fn backend(&self) -> Backend {
506 self.session.backend()
507 }
508
509 /// 底层会话可变引用。
510 pub fn session_mut(&mut self) -> &mut Session {
511 &mut self.session
512 }
513}