1#[cfg(feature = "wasm-plugin")]
4use wasmtime::{
5 Caller, Config, Engine, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc,
6};
7
8use sim_kernel::{Error, Result};
9#[cfg(feature = "wasm-plugin")]
10use sim_lib_audio_graph_core::{PortDecl, PortDir, PortMedia, PrepareConfig, ProcessBlock};
11use sim_lib_plugin_core::PluginDescriptor;
12#[cfg(feature = "wasm-plugin")]
13use sim_lib_plugin_core::{
14 ParameterDescriptor, PluginFormat, PluginId, PluginInstance, PluginState,
15};
16
17use crate::WasmResourceLimits;
18#[cfg(feature = "wasm-plugin")]
19use crate::abi::{
20 EXPORT_MANIFEST_PTR, EXPORT_PREPARE, EXPORT_PROCESS, EXPORT_RESET, IMPORT_AUDIO_READ,
21 IMPORT_AUDIO_WRITE, IMPORT_FRAME_COUNT, IMPORT_MODULE, IMPORT_PARAM_GET, WasmAudioManifest,
22};
23
24#[cfg(feature = "wasm-plugin")]
25const LOAD_FUEL: u64 = 10_000_000;
26
27#[cfg(feature = "wasm-plugin")]
28#[derive(Debug)]
29struct HostAudio {
30 frame_count: u32,
31 audio_in: Vec<Vec<f32>>,
32 audio_out: Vec<Vec<f32>>,
33 params: Vec<f64>,
34 store_limits: StoreLimits,
35}
36
37#[cfg(feature = "wasm-plugin")]
38impl HostAudio {
39 fn new(limits: WasmResourceLimits) -> Self {
40 Self {
41 frame_count: 0,
42 audio_in: Vec::new(),
43 audio_out: Vec::new(),
44 params: Vec::new(),
45 store_limits: StoreLimitsBuilder::new()
46 .memory_size(limits.max_memory_bytes())
47 .trap_on_grow_failure(true)
48 .build(),
49 }
50 }
51}
52
53pub struct WasmPluginProcessor {
55 #[cfg(feature = "wasm-plugin")]
56 store: Store<HostAudio>,
57 #[cfg(feature = "wasm-plugin")]
58 fn_prepare: TypedFunc<(f64, u32), ()>,
59 #[cfg(feature = "wasm-plugin")]
60 fn_reset: TypedFunc<(), ()>,
61 #[cfg(feature = "wasm-plugin")]
62 fn_process: TypedFunc<(), i32>,
63 descriptor: PluginDescriptor,
64 #[cfg(feature = "wasm-plugin")]
65 state: PluginState,
66 #[cfg(feature = "wasm-plugin")]
67 limits: WasmResourceLimits,
68}
69
70impl WasmPluginProcessor {
71 pub fn descriptor(&self) -> &PluginDescriptor {
73 &self.descriptor
74 }
75
76 pub fn set_param(&mut self, id: u32, value: f64) -> Result<()> {
83 self.set_param_inner(id, value)
84 }
85
86 #[cfg(feature = "wasm-plugin")]
87 fn set_param_inner(&mut self, id: u32, value: f64) -> Result<()> {
88 let Some(slot) = self.store.data_mut().params.get_mut(id as usize) else {
89 return Err(Error::Eval(format!("wasm plugin parameter {id} is absent")));
90 };
91 *slot = value;
92 self.state.set_param(id, value);
93 Ok(())
94 }
95
96 #[cfg(not(feature = "wasm-plugin"))]
97 fn set_param_inner(&mut self, _id: u32, _value: f64) -> Result<()> {
98 Err(Error::Eval(
99 "wasm plugin runtime feature is not enabled".to_owned(),
100 ))
101 }
102}
103
104#[cfg(feature = "wasm-plugin")]
105impl WasmPluginProcessor {
106 pub fn from_bytes(wasm: &[u8]) -> Result<Self> {
113 Self::from_bytes_with_limits(wasm, WasmResourceLimits::default())
114 }
115
116 pub fn from_bytes_with_limits(wasm: &[u8], limits: WasmResourceLimits) -> Result<Self> {
123 let mut config = Config::new();
124 config.consume_fuel(true);
125 let engine = Engine::new(&config)
126 .map_err(|err| Error::Eval(format!("wasm engine init failed: {err}")))?;
127 let module = Module::new(&engine, wasm)
128 .map_err(|err| Error::Eval(format!("wasm module invalid: {err}")))?;
129 let linker = build_audio_linker(&engine)?;
130 let host = HostAudio::new(limits);
131 let mut store = Store::new(&engine, host);
132 store.limiter(|host| &mut host.store_limits);
133 refill_fuel(&mut store, LOAD_FUEL)?;
134 let instance = linker
135 .instantiate(&mut store, &module)
136 .map_err(|err| Error::Eval(format!("wasm instantiate failed: {err}")))?;
137 refill_fuel(&mut store, LOAD_FUEL)?;
138
139 let manifest_ptr_fn: TypedFunc<(), u32> = instance
140 .get_typed_func(&mut store, EXPORT_MANIFEST_PTR)
141 .map_err(|err| Error::Eval(format!("missing {EXPORT_MANIFEST_PTR}: {err}")))?;
142 let ptr = manifest_ptr_fn
143 .call(&mut store, ())
144 .map_err(|err| Error::Eval(format!("{EXPORT_MANIFEST_PTR} trapped: {err}")))?
145 as usize;
146
147 let memory = instance
148 .get_memory(&mut store, "memory")
149 .ok_or_else(|| Error::Eval("wasm plugin has no exported memory".to_owned()))?;
150 let mem_data = memory.data(&store);
151 let raw_bytes = mem_data
152 .get(ptr..ptr + WasmAudioManifest::SIZE)
153 .ok_or_else(|| Error::Eval("manifest pointer is out of bounds".to_owned()))?;
154 let manifest = WasmAudioManifest::from_bytes(raw_bytes)?;
155 let descriptor = descriptor_from_manifest(&manifest)?;
156 store.data_mut().params = vec![1.0; manifest.param_count as usize];
157
158 let fn_prepare = instance
159 .get_typed_func::<(f64, u32), ()>(&mut store, EXPORT_PREPARE)
160 .map_err(|err| Error::Eval(format!("missing {EXPORT_PREPARE}: {err}")))?;
161 let fn_reset = instance
162 .get_typed_func::<(), ()>(&mut store, EXPORT_RESET)
163 .map_err(|err| Error::Eval(format!("missing {EXPORT_RESET}: {err}")))?;
164 let fn_process = instance
165 .get_typed_func::<(), i32>(&mut store, EXPORT_PROCESS)
166 .map_err(|err| Error::Eval(format!("missing {EXPORT_PROCESS}: {err}")))?;
167
168 Ok(Self {
169 store,
170 fn_prepare,
171 fn_reset,
172 fn_process,
173 descriptor,
174 state: PluginState::new(),
175 limits,
176 })
177 }
178
179 pub fn process_checked(&mut self, block: &mut ProcessBlock<'_>) -> Result<()> {
188 let frames = block.frames as usize;
189 {
190 let host = self.store.data_mut();
191 host.frame_count = block.frames;
192 for (ch, input) in block.in_audio.iter().enumerate() {
193 if let Some(lane) = host.audio_in.get_mut(ch)
194 && lane.len() >= frames
195 && input.len() >= frames
196 {
197 lane[..frames].copy_from_slice(&input[..frames]);
198 }
199 }
200 for lane in &mut host.audio_out {
201 if lane.len() >= frames {
202 lane[..frames].fill(0.0);
203 }
204 }
205 }
206
207 refill_fuel(&mut self.store, self.limits.fuel_per_process)?;
208 match self.fn_process.call(&mut self.store, ()) {
209 Ok(0) => {
210 let host = self.store.data();
211 for (ch, output) in block.out_audio.iter_mut().enumerate() {
212 if let Some(lane) = host.audio_out.get(ch)
213 && lane.len() >= frames
214 && output.len() >= frames
215 {
216 output[..frames].copy_from_slice(&lane[..frames]);
217 }
218 }
219 Ok(())
220 }
221 Ok(code) => {
222 silence_block(block, frames);
223 Err(Error::Eval(format!(
224 "wasm plugin process returned status {code}"
225 )))
226 }
227 Err(err) => {
228 silence_block(block, frames);
229 Err(Error::Eval(format!("wasm plugin process trapped: {err}")))
230 }
231 }
232 }
233}
234
235#[cfg(not(feature = "wasm-plugin"))]
236impl WasmPluginProcessor {
237 pub fn from_bytes(wasm: &[u8]) -> Result<Self> {
243 Self::from_bytes_with_limits(wasm, WasmResourceLimits::default())
244 }
245
246 pub fn from_bytes_with_limits(_wasm: &[u8], _limits: WasmResourceLimits) -> Result<Self> {
252 Err(Error::Eval(
253 "wasm plugin runtime feature is not enabled".to_owned(),
254 ))
255 }
256}
257
258#[cfg(feature = "wasm-plugin")]
259fn descriptor_from_manifest(manifest: &WasmAudioManifest) -> Result<PluginDescriptor> {
260 let plugin_id = PluginId::new(PluginFormat::Wasm, manifest.stable_id_str().to_owned())?;
261 let mut descriptor = PluginDescriptor::new(
262 plugin_id,
263 manifest.name_str().to_owned(),
264 manifest.vendor_str().to_owned(),
265 "0.1.0".to_owned(),
266 )?;
267 if manifest.audio_in_channels > 0 {
268 descriptor.ports.push(PortDecl::new(
269 "audio-in",
270 PortMedia::Audio,
271 PortDir::In,
272 manifest.audio_in_channels,
273 ));
274 }
275 if manifest.audio_out_channels > 0 {
276 descriptor.ports.push(PortDecl::new(
277 "audio-out",
278 PortMedia::Audio,
279 PortDir::Out,
280 manifest.audio_out_channels,
281 ));
282 }
283 for id in 0..u32::from(manifest.param_count) {
284 descriptor.parameters.push(ParameterDescriptor::new(
285 id,
286 format!("param-{id}"),
287 format!("Param {id}"),
288 0.0,
289 1.0,
290 1.0,
291 )?);
292 }
293 Ok(descriptor)
294}
295
296#[cfg(feature = "wasm-plugin")]
297fn build_audio_linker(engine: &Engine) -> Result<Linker<HostAudio>> {
298 let mut linker = Linker::new(engine);
299 linker
300 .func_wrap(
301 IMPORT_MODULE,
302 IMPORT_FRAME_COUNT,
303 |caller: Caller<'_, HostAudio>| caller.data().frame_count,
304 )
305 .map_err(|err| Error::Eval(err.to_string()))?;
306 linker
307 .func_wrap(
308 IMPORT_MODULE,
309 IMPORT_AUDIO_READ,
310 |caller: Caller<'_, HostAudio>, ch: u32, frame: u32| -> f32 {
311 caller
312 .data()
313 .audio_in
314 .get(ch as usize)
315 .and_then(|lane| lane.get(frame as usize))
316 .copied()
317 .unwrap_or(0.0)
318 },
319 )
320 .map_err(|err| Error::Eval(err.to_string()))?;
321 linker
322 .func_wrap(
323 IMPORT_MODULE,
324 IMPORT_AUDIO_WRITE,
325 |mut caller: Caller<'_, HostAudio>, ch: u32, frame: u32, value: f32| {
326 if let Some(lane) = caller.data_mut().audio_out.get_mut(ch as usize)
327 && let Some(sample) = lane.get_mut(frame as usize)
328 {
329 *sample = value;
330 }
331 },
332 )
333 .map_err(|err| Error::Eval(err.to_string()))?;
334 linker
335 .func_wrap(
336 IMPORT_MODULE,
337 IMPORT_PARAM_GET,
338 |caller: Caller<'_, HostAudio>, id: u32| -> f64 {
339 caller
340 .data()
341 .params
342 .get(id as usize)
343 .copied()
344 .unwrap_or(1.0)
345 },
346 )
347 .map_err(|err| Error::Eval(err.to_string()))?;
348 Ok(linker)
349}
350
351#[cfg(feature = "wasm-plugin")]
352fn refill_fuel(store: &mut Store<HostAudio>, fuel: u64) -> Result<()> {
353 store
354 .set_fuel(fuel)
355 .map_err(|err| Error::Eval(format!("wasm fuel refill failed: {err}")))
356}
357
358#[cfg(feature = "wasm-plugin")]
359fn silence_block(block: &mut ProcessBlock<'_>, frames: usize) {
360 for output in block.out_audio.iter_mut() {
361 if output.len() >= frames {
362 output[..frames].fill(0.0);
363 }
364 }
365}
366
367#[cfg(feature = "wasm-plugin")]
368impl PluginInstance for WasmPluginProcessor {
369 fn descriptor(&self) -> &PluginDescriptor {
370 &self.descriptor
371 }
372
373 fn state(&self) -> PluginState {
374 self.state.clone()
375 }
376
377 fn set_state(&mut self, state: PluginState) {
378 for (&id, &value) in state.params() {
379 let _ = self.set_param(id, value);
380 }
381 self.state = state;
382 }
383
384 fn prepare(&mut self, cfg: PrepareConfig) {
385 let _ = refill_fuel(&mut self.store, LOAD_FUEL);
386 let _ = self.fn_prepare.call(
387 &mut self.store,
388 (f64::from(cfg.sample_rate_hz), cfg.max_block_frames),
389 );
390 let ch_in = self
391 .descriptor
392 .ports
393 .iter()
394 .filter(|port| port.media == PortMedia::Audio && port.dir == PortDir::In)
395 .map(|port| port.channels as usize)
396 .sum::<usize>();
397 let ch_out = self
398 .descriptor
399 .ports
400 .iter()
401 .filter(|port| port.media == PortMedia::Audio && port.dir == PortDir::Out)
402 .map(|port| port.channels as usize)
403 .sum::<usize>();
404 let frames = cfg.max_block_frames as usize;
405 self.store.data_mut().audio_in = vec![vec![0.0; frames]; ch_in];
406 self.store.data_mut().audio_out = vec![vec![0.0; frames]; ch_out];
407 }
408
409 fn reset(&mut self) {
410 let _ = refill_fuel(&mut self.store, LOAD_FUEL);
411 let _ = self.fn_reset.call(&mut self.store, ());
412 }
413
414 fn process(&mut self, block: &mut ProcessBlock<'_>) {
415 let _ = self.process_checked(block);
416 }
417}