speechd_module_rs/
audio.rs1use std::borrow;
2use std::ffi;
3use std::mem;
4use std::os::raw;
5use std::ptr;
6
7use crate::context;
8use crate::modapi;
9
10
11#[repr(transparent)]
12#[derive(Debug, Default, PartialEq, Eq)]
13pub struct Params(modapi::audio::Params);
14
15impl Params {
16 pub fn set(&mut self, name: &str, value: &[u8]) -> bool {
19 let value = mbox::MBox::into_raw(mbox::MArray::from_slice(value).into_mbox());
20 unsafe { self.0.set_by_name(name, value as *const raw::c_char) }
21 }
22
23
24 #[must_use]
26 pub fn get(&self, name: &str) -> Option<&[u8]> {
27 self.0.get_by_name(name).map(|value| {
28 unsafe { ffi::CStr::from_ptr(value) }.to_bytes()
29 })
30 }
31
32 pub fn keys<'ret>(&self) -> impl Iterator<Item=&'ret str> + use<'ret> {
34 modapi::audio::Params::FIELDS.into_iter()
35 }
36
37 #[must_use]
38 pub fn as_ptr(&self) -> *const modapi::audio::Params {
39 &raw const self.0
40 }
41
42 #[must_use]
43 pub fn into_raw(self) -> modapi::audio::Params {
44 let inner: modapi::audio::Params = unsafe { mem::transmute(self) };
49 inner
50 }
51}
52
53impl Drop for Params {
54 fn drop(&mut self) {
55 for name in self.keys() {
56 unsafe { self.0.set_by_name(name, ptr::null()) };
57 }
58 }
59}
60
61
62#[derive(Debug, Default)]
63pub struct Context {
64 backends: Vec<String>,
65 instance: Option<modapi::audio::ID>,
66 params: Params,
67}
68
69impl Context {
70 #[must_use]
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn backends(&self) -> impl Iterator<Item=&str> {
78 self.backends.iter().map(AsRef::as_ref)
79 }
80
81 #[must_use]
83 pub fn backends_mut(&mut self) -> &mut Vec<String> {
84 &mut self.backends
85 }
86
87 #[must_use]
89 pub fn params(&self) -> &Params {
90 &self.params
91 }
92
93 #[must_use]
95 pub fn params_mut(&mut self) -> &mut Params {
96 &mut self.params
97 }
98
99 #[must_use]
101 pub fn backend_ptr(&self) -> Option<*const modapi::audio::ID> {
102 self.instance.as_ref().map(ptr::from_ref)
103 }
104}
105
106
107#[unsafe(no_mangle)]
109unsafe extern "C" fn module_audio_set(name: *const raw::c_char, value: *const raw::c_char) -> raw::c_int {
110 let name = unsafe { ffi::CStr::from_ptr(name) }.to_string_lossy();
111 let value = unsafe { ffi::CStr::from_ptr(value) }.to_bytes();
112
113 let result = context::with_module(|module| {
114 let audio_ctx = module.audio_ctx();
115 if name == "audio_output_method" {
116 for backend_name in value.split(|c| *c == b',') {
117 audio_ctx.backends_mut().push(String::from_utf8_lossy(backend_name).into_owned());
118 }
119
120 true
121 } else {
122 audio_ctx.params_mut().set(&name, value)
123 }
124 });
125
126 #[expect(clippy::bool_to_int_with_if, reason="More readable this way")]
127 if result { 0 } else { 1 }
128}
129
130#[unsafe(no_mangle)]
132unsafe extern "C" fn module_audio_init(status_info: *mut *mut raw::c_char) -> raw::c_int {
133 log::debug!("Initializing audio output system");
134
135 let mut response = mbox::MString::default();
136 let code = context::with_module(|module| {
137 let audio_ctx = module.audio_ctx();
138
139 if audio_ctx.backends_mut().is_empty() {
140 response = mbox::MString::from(
141 "No audio backend names received. Perhaps none of the \
142 specified output plugins are functional?"
143 );
144 return 1;
145 }
146
147 let mut error_out: *mut raw::c_char = ptr::null_mut();
148 let mut first_error: Option<borrow::Cow<'static, str>> = None;
149 for backend_name in audio_ctx.backends() {
150 if backend_name == "server" {
151 first_error.get_or_insert(
152 borrow::Cow::Borrowed("Server audio support unavailable")
153 );
154 continue;
155 }
156
157 let backend_name_cstr =
158 ffi::CString::new(backend_name)
159 .expect("String created from CSting never contains NUL-bytes");
160 let audio_id = unsafe { modapi::audio::spd_audio_open(
161 backend_name_cstr.as_ptr(),
162 audio_ctx.params().as_ptr(),
163 &raw mut error_out,
164 ) };
165 if !audio_id.is_null() {
166 assert!(error_out.is_null(), "`spd_audio_open` returned both backend handle and error message");
167
168 log::info!("Initialized “{backend_name}” audio backend");
169
170 if unsafe { modapi::audio::spd_audio_set_volume(audio_id, 85) } < 0 {
172 log::warn!("Can’t set volume? Is the audio backend ready?");
173 }
174
175 response = mbox::MString::from("Audio initialized successfully");
176 return 0;
177 }
178
179 assert!(!error_out.is_null(), "`spd_audio_open` returned neither backend handle nor error message");
181 let error_msg = unsafe { ffi::CStr::from_ptr(error_out) }.to_string_lossy();
182 log::info!("Failed to initialize audio backend “{backend_name}”: {error_msg}");
183 first_error.get_or_insert_with(|| borrow::Cow::Owned(error_msg.into_owned()));
184
185 unsafe { libc::free(error_out.cast::<raw::c_void>()) };
187 error_out = ptr::null_mut();
188 }
189
190 if let Some(first_error) = first_error {
192 response = mbox::MString::from(format!(
193 "{0}: {1}", "Opening audio devices failed", first_error
194 ).as_str());
195 } else {
196 response = mbox::MString::from("Opening audio devices failed");
197 }
198
199 1
200 });
201
202 unsafe { *status_info = mbox::MBox::into_raw(response.into_bytes().into_mbox()).cast::<raw::c_char>() };
204 code
205}