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