Skip to main content

speechd_module_rs/
audio.rs

1use 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	/// Set param entry by name returns whether the given `name` was recognized
17	/// and hence updated or not
18	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	/// Look up parameter by name, returns [`None`] if `name` was not recognized
25	#[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	/// Return an iterator enumerating all valid parameter names
33	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		// Trick to “destructure” a transparent type without running `drop()`
45		//
46		// Workaround for beloved E0509, since transmute effectively forgets
47		// then writes the new (identical) one in its place.
48		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	/// Create a new [`Context`] instance
71	#[must_use]
72	pub fn new() -> Self {
73		Self::default()
74	}
75
76	/// List audio backend plugins requested by the server
77	pub fn backends(&self) -> impl Iterator<Item=&str> {
78		self.backends.iter().map(AsRef::as_ref)
79	}
80
81	/// List audio backend plugins requested by the server (mutable)
82	#[must_use]
83	pub fn backends_mut(&mut self) -> &mut Vec<String> {
84		&mut self.backends
85	}
86
87	/// Audio parameters set by the server
88	#[must_use]
89	pub fn params(&self) -> &Params {
90		&self.params
91	}
92
93	/// Audio parameters set by the server (mutable)
94	#[must_use]
95	pub fn params_mut(&mut self) -> &mut Params {
96		&mut self.params
97	}
98
99	/// Pointer to opaque audio backend instance (if loaded)
100	#[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/// Set audio parameter
108#[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/// Initialize audio
131#[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				// Original `module_utils.c` code also does this?
171				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			// Log message and try next backend
180			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			// Manual clean-up for returned error
186			unsafe { libc::free(error_out.cast::<raw::c_void>()) };
187			error_out = ptr::null_mut();
188		}
189
190		// Tried all backends, no luck
191		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	// Write response text
203	unsafe { *status_info = mbox::MBox::into_raw(response.into_bytes().into_mbox()).cast::<raw::c_char>() };
204	code
205}