Skip to main content

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}