1use serde::{Deserialize, Serialize};
15use std::io::{self, Read, Write};
16
17pub const PROTOCOL_VERSION: u32 = 1;
19pub const MAX_FRAME: usize = 64 << 20;
21pub const SHM_MAGIC: &[u8; 8] = b"FRAUSHM1";
23pub const SHM_HEADER: usize = 64;
25
26pub fn write_frame<W: Write + ?Sized, T: Serialize>(w: &mut W, value: &T) -> io::Result<()> {
28 let bytes = serde_json::to_vec(value).map_err(io::Error::other)?;
29 if bytes.len() > MAX_FRAME {
30 return Err(io::Error::other("frame too large"));
31 }
32 let mut buf = Vec::with_capacity(4 + bytes.len());
33 buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
34 buf.extend_from_slice(&bytes);
35 w.write_all(&buf)?;
36 w.flush()
37}
38
39pub fn read_frame<R: Read + ?Sized>(r: &mut R) -> io::Result<Option<Vec<u8>>> {
41 let mut len = [0u8; 4];
42 let mut got = 0;
43 while got < 4 {
44 let n = r.read(&mut len[got..])?;
45 if n == 0 {
46 if got == 0 {
47 return Ok(None);
48 }
49 return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "truncated frame header"));
50 }
51 got += n;
52 }
53 let n = u32::from_le_bytes(len) as usize;
54 if n > MAX_FRAME {
55 return Err(io::Error::other(format!("frame of {n} bytes exceeds the limit")));
56 }
57 let mut buf = vec![0u8; n];
58 r.read_exact(&mut buf)?;
59 Ok(Some(buf))
60}
61
62#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
64#[serde(tag = "cmd", rename_all = "snake_case")]
65pub enum Request {
66 Hello,
68 Probe { path: String },
70 Load {
73 path: String,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 plugin_id: Option<String>,
76 },
77 SetParams { params: Vec<ParamAssign> },
80 GetParams,
82 ListPresets,
84 LoadPreset { preset: String },
86 GetState,
88 SetState { state_b64: String },
90 Activate { sample_rate: f64, block_size: u32, shm_path: String },
92 Process {
95 slot: u32,
96 frames: u32,
97 #[serde(default, skip_serializing_if = "Vec::is_empty")]
98 events: Vec<Event>,
99 },
100 Deactivate,
102 Quit,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
108#[serde(tag = "reply", rename_all = "snake_case")]
109#[allow(clippy::large_enum_variant)] pub enum Reply {
111 Hello {
112 host: String,
113 version: String,
114 protocol: u32,
115 formats: Vec<String>,
116 },
117 Probe {
118 plugins: Vec<Descriptor>,
119 },
120 Loaded {
121 info: PluginInfo,
122 },
123 Params {
124 params: Vec<ParamValue>,
125 },
126 Presets {
127 presets: Vec<PresetInfo>,
128 },
129 State {
130 state_b64: String,
131 },
132 Activated {
133 latency: u32,
134 #[serde(default)]
137 tail: Option<u32>,
138 channels_in: u32,
139 channels_out: u32,
140 },
141 Processed {
142 slot: u32,
143 },
144 Ok,
145 Error {
146 code: String,
147 message: String,
148 },
149}
150
151#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
153#[serde(tag = "type", rename_all = "snake_case")]
154pub enum Event {
155 NoteOn {
156 time: u32,
157 channel: u8,
158 key: u8,
159 velocity: f32,
160 },
161 NoteOff {
162 time: u32,
163 channel: u8,
164 key: u8,
165 velocity: f32,
166 },
167 Midi {
169 time: u32,
170 data: [u8; 3],
171 },
172}
173
174impl Event {
175 pub fn time(&self) -> u32 {
176 match self {
177 Event::NoteOn { time, .. } | Event::NoteOff { time, .. } | Event::Midi { time, .. } => *time,
178 }
179 }
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
184pub struct ParamAssign {
185 pub name: String,
187 pub value: String,
188}
189
190#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
192pub struct Descriptor {
193 pub id: String,
194 pub name: String,
195 #[serde(default)]
196 pub vendor: String,
197 #[serde(default)]
198 pub version: String,
199 #[serde(default)]
200 pub description: String,
201 #[serde(default)]
202 pub url: String,
203 #[serde(default)]
205 pub features: Vec<String>,
206 #[serde(default)]
208 pub kind: String,
209}
210
211impl Descriptor {
212 pub fn classify(features: &[String]) -> String {
214 let has = |f: &str| features.iter().any(|x| x.eq_ignore_ascii_case(f));
215 if has("instrument") || has("synth") || has("synthesizer") || has("sampler") {
216 "instrument"
217 } else if has("note-effect") {
218 "note-effect"
219 } else if has("audio-effect") || has("fx") {
220 "audio-effect"
221 } else if has("analyzer") {
222 "analyzer"
223 } else {
224 "other"
225 }
226 .to_string()
227 }
228}
229
230#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
232pub struct ParamInfo {
233 pub id: u32,
234 pub name: String,
235 #[serde(default, skip_serializing_if = "String::is_empty")]
236 pub module: String,
237 pub min: f64,
238 pub max: f64,
239 pub default: f64,
240 pub value: f64,
241 #[serde(default)]
243 pub default_text: String,
244 #[serde(default)]
245 pub value_text: String,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub unit: Option<String>,
249 #[serde(default)]
250 pub stepped: bool,
251 #[serde(default)]
252 pub hidden: bool,
253 #[serde(default)]
254 pub readonly: bool,
255 #[serde(default)]
256 pub bypass: bool,
257 #[serde(default)]
258 pub automatable: bool,
259}
260
261#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
263pub struct ParamValue {
264 pub id: u32,
265 pub name: String,
266 pub value: f64,
267 pub text: String,
268}
269
270#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
271pub struct PortInfo {
272 pub name: String,
273 pub channels: u32,
274 pub main: bool,
275}
276
277#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
278pub struct NotePortInfo {
279 pub name: String,
280 pub dialects: Vec<String>,
282}
283
284#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
285pub struct PresetInfo {
286 pub name: String,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub load_key: Option<String>,
289 pub location_kind: String,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub location: Option<String>,
293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
294 pub creators: Vec<String>,
295 #[serde(default, skip_serializing_if = "String::is_empty")]
296 pub description: String,
297 #[serde(default, skip_serializing_if = "String::is_empty")]
298 pub provider: String,
299}
300
301#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
303pub struct PluginInfo {
304 pub descriptor: Descriptor,
305 pub format: String,
307 pub params: Vec<ParamInfo>,
308 pub audio_inputs: Vec<PortInfo>,
309 pub audio_outputs: Vec<PortInfo>,
310 pub note_inputs: Vec<NotePortInfo>,
311 pub note_outputs: Vec<NotePortInfo>,
312 pub main_in_channels: u32,
314 pub main_out_channels: u32,
315 pub has_state: bool,
316 pub has_presets: bool,
317}
318
319impl PluginInfo {
320 pub fn accepts_notes(&self) -> bool {
321 !self.note_inputs.is_empty()
322 }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub struct ShmLayout {
331 pub channels_in: u32,
332 pub channels_out: u32,
333 pub block_size: u32,
334 pub slots: u32,
335}
336
337impl ShmLayout {
338 pub fn slot_bytes(&self) -> usize {
339 (self.channels_in + self.channels_out) as usize * self.block_size as usize * 4
340 }
341
342 pub fn total_bytes(&self) -> usize {
343 SHM_HEADER + self.slot_bytes() * self.slots as usize
344 }
345
346 pub fn input_offset(&self, slot: u32, ch: u32) -> usize {
348 SHM_HEADER + slot as usize * self.slot_bytes() + (ch * self.block_size) as usize * 4
349 }
350
351 pub fn output_offset(&self, slot: u32, ch: u32) -> usize {
353 SHM_HEADER + slot as usize * self.slot_bytes() + ((self.channels_in + ch) * self.block_size) as usize * 4
354 }
355
356 pub fn write_header(&self, buf: &mut [u8]) {
357 buf[..8].copy_from_slice(SHM_MAGIC);
358 let words = [PROTOCOL_VERSION, self.channels_in, self.channels_out, self.block_size, self.slots];
359 for (i, w) in words.iter().enumerate() {
360 buf[8 + 4 * i..12 + 4 * i].copy_from_slice(&w.to_le_bytes());
361 }
362 }
363
364 pub fn read_header(buf: &[u8]) -> Result<ShmLayout, String> {
365 if buf.len() < SHM_HEADER || &buf[..8] != SHM_MAGIC {
366 return Err("shared memory file has no ferraus header".into());
367 }
368 let w = |i: usize| u32::from_le_bytes([buf[8 + 4 * i], buf[9 + 4 * i], buf[10 + 4 * i], buf[11 + 4 * i]]);
369 if w(0) != PROTOCOL_VERSION {
370 return Err(format!("shared memory protocol {} does not match host protocol {PROTOCOL_VERSION}", w(0)));
371 }
372 let l = ShmLayout { channels_in: w(1), channels_out: w(2), block_size: w(3), slots: w(4) };
373 if l.block_size == 0 || l.slots == 0 || l.total_bytes() > buf.len() {
374 return Err("shared memory header is inconsistent with the file size".into());
375 }
376 Ok(l)
377 }
378}
379
380pub unsafe fn f32_slice_mut<'a>(base: *mut u8, off: usize, n: usize) -> &'a mut [f32] {
387 std::slice::from_raw_parts_mut(base.add(off) as *mut f32, n)
388}
389
390pub trait ParamText {
395 fn text_to_value(&mut self, id: u32, text: &str) -> Option<f64>;
396 fn value_to_text(&mut self, id: u32, value: f64) -> Option<String>;
397}
398
399fn fmt_num(v: f64) -> String {
400 let s = format!("{v:.6}");
401 let s = s.trim_end_matches('0').trim_end_matches('.');
402 if s == "-0" {
403 "0".into()
404 } else {
405 s.to_string()
406 }
407}
408
409fn split_number(text: &str) -> Option<(f64, String)> {
411 let t = text.trim();
412 if !t.starts_with(|c: char| c.is_ascii_digit() || matches!(c, '.' | '-' | '+')) {
413 return None;
414 }
415 let end = (1..=t.len()).rev().filter(|&i| t.is_char_boundary(i)).find(|&i| {
417 let s = &t[..i];
418 s.chars().all(|c| c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E')) && s.parse::<f64>().is_ok()
419 })?;
420 let n: f64 = t[..end].parse().ok()?;
421 Some((n, t[end..].trim().to_ascii_lowercase()))
422}
423
424pub fn text_candidates(text: &str) -> Vec<String> {
427 let mut out = vec![text.trim().to_string()];
428 if let Some((n, unit)) = split_number(text) {
429 let mut push = |s: String| {
430 if !out.contains(&s) {
431 out.push(s)
432 }
433 };
434 match unit.as_str() {
435 "khz" | "k" => {
436 let hz = n * 1000.0;
437 push(format!("{} Hz", fmt_num(hz)));
438 push(format!("{}Hz", fmt_num(hz)));
439 push(fmt_num(hz));
440 push(format!("{} kHz", fmt_num(n)));
441 push(fmt_num(n));
442 }
443 "hz" => {
444 push(format!("{} Hz", fmt_num(n)));
445 push(format!("{}Hz", fmt_num(n)));
446 push(fmt_num(n));
447 push(format!("{} kHz", fmt_num(n / 1000.0)));
448 }
449 "db" => {
450 push(format!("{} dB", fmt_num(n)));
451 push(format!("{}dB", fmt_num(n)));
452 push(fmt_num(n));
453 }
454 "%" => {
455 push(format!("{} %", fmt_num(n)));
456 push(format!("{}%", fmt_num(n)));
457 push(fmt_num(n));
458 push(fmt_num(n / 100.0));
459 }
460 "ms" => {
461 push(format!("{} ms", fmt_num(n)));
462 push(format!("{}ms", fmt_num(n)));
463 push(fmt_num(n));
464 push(format!("{} s", fmt_num(n / 1000.0)));
465 }
466 "s" | "sec" => {
467 push(format!("{} s", fmt_num(n)));
468 push(format!("{}s", fmt_num(n)));
469 push(format!("{} ms", fmt_num(n * 1000.0)));
470 push(fmt_num(n));
471 }
472 "st" | "semitones" | "semi" => {
473 push(format!("{} st", fmt_num(n)));
474 push(format!("{} semitones", fmt_num(n)));
475 push(fmt_num(n));
476 }
477 "ct" | "cents" | "cent" => {
478 push(format!("{} ct", fmt_num(n)));
479 push(format!("{} cents", fmt_num(n)));
480 push(fmt_num(n));
481 }
482 "" => push(fmt_num(n)),
483 other => {
484 push(format!("{} {other}", fmt_num(n)));
485 push(fmt_num(n));
486 }
487 }
488 }
489 out
490}
491
492pub fn infer_unit(display: &str) -> Option<String> {
494 let (_, unit) = split_number(display)?;
495 if unit.is_empty() {
496 return None;
497 }
498 let orig = display.trim();
499 let u = orig[orig.len() - unit.len()..].trim().to_string();
500 if u.chars().any(|c| c.is_ascii_digit()) || u.len() > 12 {
501 None
502 } else {
503 Some(u)
504 }
505}
506
507pub fn find_param<'a>(params: &'a [ParamInfo], key: &str) -> Result<&'a ParamInfo, String> {
510 let key = key.trim();
511 let idkey = key.strip_prefix("id:").unwrap_or(key);
512 if let Ok(id) = idkey.parse::<u32>() {
513 if let Some(p) = params.iter().find(|p| p.id == id) {
514 if key.starts_with("id:") || !params.iter().any(|p| p.name == key) {
515 return Ok(p);
516 }
517 }
518 }
519 let norm = |s: &str| s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect::<String>();
520 let full = |p: &ParamInfo| {
521 if p.module.is_empty() {
522 p.name.clone()
523 } else {
524 format!("{}/{}", p.module.trim_end_matches('/'), p.name)
525 }
526 };
527 let stages: [&dyn Fn(&ParamInfo) -> bool; 4] = [
528 &|p| p.name == key || full(p) == key,
529 &|p| p.name.eq_ignore_ascii_case(key) || full(p).eq_ignore_ascii_case(key),
530 &|p| norm(&p.name) == norm(key) || norm(&full(p)) == norm(key),
531 &|p| norm(&p.name).starts_with(&norm(key)) && !norm(key).is_empty(),
532 ];
533 for test in stages {
534 let hits: Vec<&ParamInfo> = params.iter().filter(|p| test(p)).collect();
535 match hits.len() {
536 0 => continue,
537 1 => return Ok(hits[0]),
538 _ => {
539 let names: Vec<String> = hits.iter().take(8).map(|p| format!("{} (id {})", full(p), p.id)).collect();
540 return Err(format!(
541 "parameter '{key}' is ambiguous: {}; use the id (e.g. id:{})",
542 names.join(", "),
543 hits[0].id
544 ));
545 }
546 }
547 }
548 let mut known: Vec<String> = params.iter().filter(|p| !p.hidden).take(12).map(|p| p.name.clone()).collect();
549 if params.len() > 12 {
550 known.push("...".into());
551 }
552 Err(format!("unknown parameter '{key}'; parameters include: {}", known.join(", ")))
553}
554
555pub fn resolve_value(p: &ParamInfo, text: &str, conv: &mut dyn ParamText) -> Result<f64, String> {
558 let t = text.trim();
559 let (lo, hi) = (p.min.min(p.max), p.min.max(p.max));
560 let in_range = |v: f64| v >= lo - 1e-9 * (1.0 + lo.abs()) && v <= hi + 1e-9 * (1.0 + hi.abs());
561 if let Some(n) = t.strip_prefix("norm:") {
562 let x: f64 = n.trim().parse().map_err(|_| format!("'{t}' is not norm:<0..1>"))?;
563 if !(0.0..=1.0).contains(&x) {
564 return Err(format!("{t} is outside norm:0..1"));
565 }
566 let mut v = p.min + x * (p.max - p.min);
567 if p.stepped {
568 v = v.round();
569 }
570 return Ok(v);
571 }
572 for c in text_candidates(t) {
573 if let Some(v) = conv.text_to_value(p.id, &c) {
574 if v.is_finite() {
575 return Ok(v.clamp(lo, hi));
576 }
577 }
578 }
579 if let Ok(v) = t.parse::<f64>() {
580 if in_range(v) {
581 return Ok(v.clamp(lo, hi));
582 }
583 }
584 if p.stepped && hi - lo <= 512.0 {
585 let mut v = lo.ceil();
586 while v <= hi {
587 if let Some(label) = conv.value_to_text(p.id, v) {
588 if label.trim().eq_ignore_ascii_case(t) {
589 return Ok(v);
590 }
591 }
592 v += 1.0;
593 }
594 }
595 let example = if p.default_text.is_empty() { fmt_num(p.default) } else { p.default_text.clone() };
596 Err(format!(
597 "cannot convert '{t}' for parameter '{}' (range {}..{}, e.g. '{example}' or norm:0.5)",
598 p.name,
599 fmt_num(p.min),
600 fmt_num(p.max)
601 ))
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn frames_roundtrip() {
610 let mut buf = Vec::new();
611 write_frame(&mut buf, &Request::Hello).unwrap();
612 write_frame(
613 &mut buf,
614 &Request::Process {
615 slot: 2,
616 frames: 64,
617 events: vec![Event::NoteOn { time: 3, channel: 0, key: 60, velocity: 0.8 }],
618 },
619 )
620 .unwrap();
621 let mut r = &buf[..];
622 let a: Request = serde_json::from_slice(&read_frame(&mut r).unwrap().unwrap()).unwrap();
623 let b: Request = serde_json::from_slice(&read_frame(&mut r).unwrap().unwrap()).unwrap();
624 assert_eq!(a, Request::Hello);
625 assert!(matches!(b, Request::Process { slot: 2, frames: 64, .. }));
626 assert!(read_frame(&mut r).unwrap().is_none());
627 }
628
629 #[test]
630 fn candidates_cover_units() {
631 let c = text_candidates("2.5kHz");
632 assert!(c.contains(&"2500 Hz".to_string()) && c.contains(&"2500".to_string()));
633 assert!(text_candidates("30%").contains(&"0.3".to_string()));
634 assert!(text_candidates("-6dB").contains(&"-6".to_string()));
635 assert_eq!(infer_unit("-6.00 dB").as_deref(), Some("dB"));
636 assert_eq!(infer_unit("440 Hz").as_deref(), Some("Hz"));
637 assert_eq!(infer_unit("0.500"), None);
638 }
639
640 #[test]
641 fn shm_layout_offsets() {
642 let l = ShmLayout { channels_in: 2, channels_out: 2, block_size: 512, slots: 4 };
643 let mut b = vec![0u8; l.total_bytes()];
644 l.write_header(&mut b);
645 assert_eq!(ShmLayout::read_header(&b).unwrap(), l);
646 assert_eq!(l.input_offset(0, 0), 64);
647 assert_eq!(l.output_offset(0, 0), 64 + 2 * 512 * 4);
648 assert_eq!(l.input_offset(1, 0), 64 + 4 * 512 * 4);
649 }
650
651 struct Db;
652 impl ParamText for Db {
653 fn text_to_value(&mut self, _id: u32, t: &str) -> Option<f64> {
654 t.trim().parse().ok()
655 }
656 fn value_to_text(&mut self, _id: u32, v: f64) -> Option<String> {
657 Some(format!("{v:.2} dB"))
658 }
659 }
660
661 #[test]
662 fn resolves_values() {
663 let p = ParamInfo { id: 3, name: "gain".into(), min: -60.0, max: 24.0, ..Default::default() };
664 assert_eq!(resolve_value(&p, "-6dB", &mut Db).unwrap(), -6.0);
665 assert_eq!(resolve_value(&p, "norm:0.5", &mut Db).unwrap(), -18.0);
666 assert!(resolve_value(&p, "loud", &mut Db).is_err());
667 let ps = vec![p.clone()];
668 assert_eq!(find_param(&ps, "Gain").unwrap().id, 3);
669 assert_eq!(find_param(&ps, "id:3").unwrap().id, 3);
670 assert!(find_param(&ps, "cutoff").is_err());
671 }
672}