1use sim_config::{
4 ConfigDir, ConfigLayer, ConfigProbe, ConfigProbeReport, ConfigProbeRequest, ConfigProbeStatus,
5 ConfigSource, ProbeMode,
6};
7use sim_kernel::{Expr, NumberLiteral, Symbol};
8
9use crate::DeviceCatalog;
10
11const DEFAULT_SAMPLE_RATE_HZ: u32 = 48_000;
12const DEFAULT_MAX_BLOCK_FRAMES: u32 = 512;
13
14const EMITTED_KEYS: [&str; 6] = [
15 "audio_backend_candidates",
16 "midi_backend_candidates",
17 "audio_backend_regex",
18 "midi_backend_regex",
19 "sample_rate_hz",
20 "max_block_frames",
21];
22
23pub fn stream_host_config_lib_symbol() -> Symbol {
25 Symbol::qualified("stream", "host")
26}
27
28pub fn host_stream_config_probe_symbol() -> Symbol {
30 Symbol::qualified("config-probe", "stream-host")
31}
32
33pub struct HostStreamConfigProbe {
35 catalog: DeviceCatalog,
36}
37
38impl HostStreamConfigProbe {
39 pub fn new(catalog: DeviceCatalog) -> Self {
41 Self { catalog }
42 }
43
44 pub fn modeled() -> Self {
46 Self::new(DeviceCatalog::default_modeled())
47 }
48}
49
50impl Default for HostStreamConfigProbe {
51 fn default() -> Self {
52 Self::modeled()
53 }
54}
55
56impl ConfigProbe for HostStreamConfigProbe {
57 fn symbol(&self) -> Symbol {
58 host_stream_config_probe_symbol()
59 }
60
61 fn probe(&self, request: &ConfigProbeRequest) -> (Option<ConfigLayer>, ConfigProbeReport) {
62 if request.lib != stream_host_config_lib_symbol() {
63 return (
64 None,
65 report(
66 self.symbol(),
67 request,
68 ConfigProbeStatus::Skipped {
69 reason: "stream-host probe only serves stream/host".to_owned(),
70 },
71 &[],
72 ),
73 );
74 }
75
76 if request.mode == ProbeMode::Real && !request.caps.hardware_inventory {
77 return (
78 None,
79 report(
80 self.symbol(),
81 request,
82 ConfigProbeStatus::Denied {
83 capability: "hardware_inventory".to_owned(),
84 },
85 &[],
86 ),
87 );
88 }
89
90 let candidates = match request.mode {
91 ProbeMode::Modeled => Ok((vec!["modeled".to_owned()], vec!["modeled".to_owned()])),
92 ProbeMode::Real => self.real_candidates(),
93 };
94 let (audio_candidates, midi_candidates) = match candidates {
95 Ok(candidates) => candidates,
96 Err(message) => {
97 return (
98 None,
99 report(
100 self.symbol(),
101 request,
102 ConfigProbeStatus::Failed { message },
103 &[],
104 ),
105 );
106 }
107 };
108
109 let layer = ConfigLayer::new(
110 ConfigSource::Probe {
111 probe: self.symbol(),
112 mode: request.mode,
113 },
114 ConfigDir::one(
115 request.lib.clone(),
116 stream_host_table(&audio_candidates, &midi_candidates),
117 )
118 .expect("stream-host config probe builds a map table"),
119 );
120 (
121 Some(layer),
122 report(
123 self.symbol(),
124 request,
125 ConfigProbeStatus::Applied,
126 &EMITTED_KEYS,
127 ),
128 )
129 }
130}
131
132impl HostStreamConfigProbe {
133 fn real_candidates(&self) -> Result<(Vec<String>, Vec<String>), String> {
134 let audio = self
135 .catalog
136 .audio_backend_names()
137 .map_err(|error| format!("audio backend inventory failed: {error}"))?;
138 let midi = self
139 .catalog
140 .midi_backend_names()
141 .map_err(|error| format!("MIDI backend inventory failed: {error}"))?;
142 Ok((audio, midi))
143 }
144}
145
146fn stream_host_table(audio_candidates: &[String], midi_candidates: &[String]) -> Expr {
147 Expr::Map(vec![
148 (
149 key("audio_backend_candidates"),
150 string_list(audio_candidates),
151 ),
152 (key("midi_backend_candidates"), string_list(midi_candidates)),
153 (
154 key("audio_backend_regex"),
155 Expr::String(candidate_regex(audio_candidates)),
156 ),
157 (
158 key("midi_backend_regex"),
159 Expr::String(candidate_regex(midi_candidates)),
160 ),
161 (key("sample_rate_hz"), int(DEFAULT_SAMPLE_RATE_HZ)),
162 (key("max_block_frames"), int(DEFAULT_MAX_BLOCK_FRAMES)),
163 ])
164}
165
166fn report(
167 probe: Symbol,
168 request: &ConfigProbeRequest,
169 status: ConfigProbeStatus,
170 keys: &[&str],
171) -> ConfigProbeReport {
172 ConfigProbeReport {
173 probe,
174 lib: request.lib.clone(),
175 mode: request.mode,
176 status,
177 emitted_keys: keys.iter().map(|key| (*key).to_owned()).collect(),
178 }
179}
180
181fn key(name: &str) -> Expr {
182 Expr::Symbol(Symbol::new(name))
183}
184
185fn string_list(values: &[String]) -> Expr {
186 Expr::List(
187 values
188 .iter()
189 .map(|value| Expr::String(value.clone()))
190 .collect(),
191 )
192}
193
194fn int(value: u32) -> Expr {
195 Expr::Number(NumberLiteral {
196 domain: Symbol::new("i64"),
197 canonical: value.to_string(),
198 })
199}
200
201fn candidate_regex(values: &[String]) -> String {
202 if values.is_empty() {
203 return "(?!)".to_owned();
204 }
205 format!(
206 "^(?:{})$",
207 values
208 .iter()
209 .map(|value| regex_escape(value))
210 .collect::<Vec<_>>()
211 .join("|")
212 )
213}
214
215fn regex_escape(value: &str) -> String {
216 let mut escaped = String::new();
217 for character in value.chars() {
218 if matches!(
219 character,
220 '\\' | '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
221 ) {
222 escaped.push('\\');
223 }
224 escaped.push(character);
225 }
226 escaped
227}