Skip to main content

sim_lib_stream_asio/
model.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_audio::PcmSpec;
3use sim_lib_stream_core::StreamMedia;
4use sim_lib_stream_host::HostDirection;
5
6/// Timing metadata accepted by an ASIO buffer switch.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct AsioTiming {
9    sample_rate_hz: u32,
10    buffer_frames: usize,
11    input_latency_frames: u32,
12    output_latency_frames: u32,
13}
14
15/// SIM-visible ASIO driver metadata.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct AsioDriver {
18    id: Symbol,
19    name: String,
20    timing: AsioTiming,
21    audio_inputs: usize,
22    audio_outputs: usize,
23}
24
25/// Routable ASIO port owned by a driver.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct AsioPort {
28    id: Symbol,
29    driver: Symbol,
30    name: String,
31    media: StreamMedia,
32    direction: HostDirection,
33    index: usize,
34}
35
36impl AsioTiming {
37    /// Builds timing from a sample rate, buffer size, and reported input/output
38    /// latencies, all measured in frames except the rate in hertz.
39    ///
40    /// Returns an error if `sample_rate_hz` or `buffer_frames` is zero.
41    pub fn new(
42        sample_rate_hz: u32,
43        buffer_frames: usize,
44        input_latency_frames: u32,
45        output_latency_frames: u32,
46    ) -> Result<Self> {
47        if sample_rate_hz == 0 {
48            return Err(Error::Eval(
49                "ASIO sample rate must be greater than zero".to_owned(),
50            ));
51        }
52        if buffer_frames == 0 {
53            return Err(Error::Eval(
54                "ASIO buffer size must be greater than zero".to_owned(),
55            ));
56        }
57        Ok(Self {
58            sample_rate_hz,
59            buffer_frames,
60            input_latency_frames,
61            output_latency_frames,
62        })
63    }
64
65    /// Returns a 48 kHz, 128-frame timing with 128-frame input/output latency,
66    /// a typical low-latency pro-audio configuration.
67    pub fn pro_audio_default() -> Self {
68        Self::new(48_000, 128, 128, 128).expect("valid ASIO timing")
69    }
70
71    /// Returns the sample rate in hertz.
72    pub fn sample_rate_hz(self) -> u32 {
73        self.sample_rate_hz
74    }
75
76    /// Returns the buffer switch size in frames.
77    pub fn buffer_frames(self) -> usize {
78        self.buffer_frames
79    }
80
81    /// Returns the reported input latency in frames.
82    pub fn input_latency_frames(self) -> u32 {
83        self.input_latency_frames
84    }
85
86    /// Returns the reported output latency in frames.
87    pub fn output_latency_frames(self) -> u32 {
88        self.output_latency_frames
89    }
90}
91
92impl AsioDriver {
93    /// Builds a driver with the given `name`, `timing`, and audio channel
94    /// counts, deriving the stable id `asio/<name>/driver`.
95    ///
96    /// Returns an error if `name` is empty or the driver exposes no audio
97    /// channels at all.
98    pub fn new(
99        name: impl Into<String>,
100        timing: AsioTiming,
101        audio_inputs: usize,
102        audio_outputs: usize,
103    ) -> Result<Self> {
104        let name = name.into();
105        if name.is_empty() {
106            return Err(Error::Eval("ASIO driver name must not be empty".to_owned()));
107        }
108        if audio_inputs == 0 && audio_outputs == 0 {
109            return Err(Error::Eval(
110                "ASIO driver must expose at least one audio channel".to_owned(),
111            ));
112        }
113        Ok(Self {
114            id: Symbol::new(format!("asio/{name}/driver")),
115            name,
116            timing,
117            audio_inputs,
118            audio_outputs,
119        })
120    }
121
122    /// Returns the bundled `SIM-ASIO` driver: stereo in/out with
123    /// [`AsioTiming::pro_audio_default`] timing.
124    pub fn sim_default() -> Result<Self> {
125        Self::new("SIM-ASIO", AsioTiming::pro_audio_default(), 2, 2)
126    }
127
128    /// Returns the stable driver id (`asio/<name>/driver`).
129    pub fn id(&self) -> &Symbol {
130        &self.id
131    }
132
133    /// Returns the human-readable driver name.
134    pub fn name(&self) -> &str {
135        &self.name
136    }
137
138    /// Returns the driver's buffer timing.
139    pub fn timing(&self) -> AsioTiming {
140        self.timing
141    }
142
143    /// Returns the number of audio input channels the driver exposes.
144    pub fn audio_inputs(&self) -> usize {
145        self.audio_inputs
146    }
147
148    /// Returns the number of audio output channels the driver exposes.
149    pub fn audio_outputs(&self) -> usize {
150        self.audio_outputs
151    }
152
153    /// Returns the audio direction implied by the input/output channel counts,
154    /// treating a channelless driver as [`HostDirection::Duplex`].
155    pub fn direction(&self) -> HostDirection {
156        match (self.audio_inputs > 0, self.audio_outputs > 0) {
157            (true, true) => HostDirection::Duplex,
158            (true, false) => HostDirection::Input,
159            (false, true) => HostDirection::Output,
160            (false, false) => HostDirection::Duplex,
161        }
162    }
163
164    /// Reports whether the driver can serve the `requested` direction, which
165    /// holds for an exact match or for any request when the driver is duplex.
166    pub fn is_compatible_with(&self, requested: HostDirection) -> bool {
167        self.direction() == requested || self.direction() == HostDirection::Duplex
168    }
169
170    /// Returns an f32 PCM spec for the driver's output channels (at least one)
171    /// at the driver sample rate.
172    pub fn output_spec(&self) -> Result<PcmSpec> {
173        PcmSpec::f32(self.audio_outputs.max(1), self.timing.sample_rate_hz)
174    }
175
176    /// Returns one [`AsioPort`] per input channel followed by one per output
177    /// channel, with ids of the form `<driver-id>/input_<n>` and
178    /// `<driver-id>/output_<n>`.
179    pub fn ports(&self) -> Vec<AsioPort> {
180        let inputs = (0..self.audio_inputs).map(|index| {
181            AsioPort::new(
182                Symbol::new(format!("{}/input_{index}", self.id)),
183                self.id.clone(),
184                format!("input_{index}"),
185                HostDirection::Input,
186                index,
187            )
188        });
189        let outputs = (0..self.audio_outputs).map(|index| {
190            AsioPort::new(
191                Symbol::new(format!("{}/output_{index}", self.id)),
192                self.id.clone(),
193                format!("output_{index}"),
194                HostDirection::Output,
195                index,
196            )
197        });
198        inputs.chain(outputs).collect()
199    }
200}
201
202impl AsioPort {
203    /// Builds a PCM port with the given id, owning driver id, name, direction,
204    /// and per-direction channel index.
205    pub fn new(
206        id: Symbol,
207        driver: Symbol,
208        name: String,
209        direction: HostDirection,
210        index: usize,
211    ) -> Self {
212        Self {
213            id,
214            driver,
215            name,
216            media: StreamMedia::Pcm,
217            direction,
218            index,
219        }
220    }
221
222    /// Returns the stable port id.
223    pub fn id(&self) -> &Symbol {
224        &self.id
225    }
226
227    /// Returns the id of the driver that owns this port.
228    pub fn driver(&self) -> &Symbol {
229        &self.driver
230    }
231
232    /// Returns the human-readable port name.
233    pub fn name(&self) -> &str {
234        &self.name
235    }
236
237    /// Returns the port media, always [`StreamMedia::Pcm`] for ASIO.
238    pub fn media(&self) -> StreamMedia {
239        self.media
240    }
241
242    /// Returns whether the port is an input or output.
243    pub fn direction(&self) -> HostDirection {
244        self.direction
245    }
246
247    /// Returns the channel index within the port's direction.
248    pub fn index(&self) -> usize {
249        self.index
250    }
251}
252
253/// Lists the prerequisites a downstream provider must satisfy to build a native
254/// ASIO backend, none of which this validation-only crate supplies.
255pub fn asio_sdk_build_requirements() -> Vec<&'static str> {
256    vec![
257        "Windows target",
258        "Steinberg ASIO SDK headers supplied outside this repository",
259        "vendor driver import library or COM registration",
260        "SIM stream-asio feature enabled explicitly",
261    ]
262}