Skip to main content

sim_lib_stream_host/
fake.rs

1//! Deterministic host backend used for validation and replay tests.
2
3use sim_kernel::{Error, Result, Symbol};
4use sim_lib_stream_core::{BufferPolicy, ClockDomain, StreamMedia};
5
6use crate::{
7    HostBackend, HostBackendCapability, HostBackendInfo, HostClockInfo, HostDeviceInventory,
8    HostDeviceSpec, HostDirection, HostLatencyInfo, HostOpenStream, HostPortSpec, HostStreamConfig,
9    HostStreamConfigRequest,
10};
11
12/// Stable fake backend id.
13pub fn fake_backend_symbol() -> Symbol {
14    Symbol::qualified("stream/host", "fake")
15}
16
17/// Host-independent backend that opens push streams without hardware.
18#[derive(Clone, Debug)]
19pub struct FakeBackend {
20    info: HostBackendInfo,
21    inventory: HostDeviceInventory,
22}
23
24impl Default for FakeBackend {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl FakeBackend {
31    /// Creates a fake backend with deterministic data, MIDI, and PCM devices.
32    pub fn new() -> Self {
33        let backend = fake_backend_symbol();
34        let data = fake_device("fake/data", StreamMedia::Data, HostDirection::Input, 8);
35        let data_duplex = fake_device(
36            "fake/data-duplex",
37            StreamMedia::Data,
38            HostDirection::Duplex,
39            8,
40        );
41        let midi = fake_device("fake/midi", StreamMedia::Midi, HostDirection::Input, 8);
42        let pcm = fake_device("fake/pcm", StreamMedia::Pcm, HostDirection::Output, 8);
43        let ports = vec![
44            fake_port(
45                "fake/data/in",
46                data.id(),
47                StreamMedia::Data,
48                HostDirection::Input,
49            ),
50            fake_port(
51                "fake/data-duplex/io",
52                data_duplex.id(),
53                StreamMedia::Data,
54                HostDirection::Duplex,
55            ),
56            fake_port(
57                "fake/midi/in",
58                midi.id(),
59                StreamMedia::Midi,
60                HostDirection::Input,
61            ),
62            fake_port(
63                "fake/pcm/out",
64                pcm.id(),
65                StreamMedia::Pcm,
66                HostDirection::Output,
67            ),
68        ];
69        Self {
70            info: HostBackendInfo::new(
71                backend.clone(),
72                Symbol::qualified("stream/transport", "fake"),
73                StreamMedia::Data,
74                false,
75            )
76            .with_capabilities(vec![
77                HostBackendCapability::Fake,
78                HostBackendCapability::Offline,
79                HostBackendCapability::MidiInput,
80                HostBackendCapability::AudioOutput,
81                HostBackendCapability::Duplex,
82            ]),
83            inventory: HostDeviceInventory::new(backend)
84                .with_devices(vec![data, data_duplex, midi, pcm])
85                .with_ports(ports),
86        }
87    }
88
89    /// Builds a data stream request for the default fake data input device.
90    pub fn data_request(capacity: usize) -> Result<HostStreamConfigRequest> {
91        Ok(HostStreamConfigRequest::new(
92            fake_backend_symbol(),
93            Symbol::new("fake/data"),
94            StreamMedia::Data,
95            HostDirection::Input,
96            BufferPolicy::bounded(capacity)?,
97        ))
98    }
99
100    /// Builds a data stream request for the fake duplex data device.
101    pub fn duplex_data_request(capacity: usize) -> Result<HostStreamConfigRequest> {
102        Ok(HostStreamConfigRequest::new(
103            fake_backend_symbol(),
104            Symbol::new("fake/data-duplex"),
105            StreamMedia::Data,
106            HostDirection::Duplex,
107            BufferPolicy::bounded(capacity)?,
108        ))
109    }
110}
111
112impl HostBackend for FakeBackend {
113    fn info(&self) -> &HostBackendInfo {
114        &self.info
115    }
116
117    fn enumerate(&self) -> Result<HostDeviceInventory> {
118        Ok(self.inventory.clone())
119    }
120
121    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
122        if request.backend() != self.info.id() {
123            return Err(Error::Eval(format!(
124                "fake backend cannot open {} requests",
125                request.backend()
126            )));
127        }
128        let Some(device) = self
129            .inventory
130            .devices()
131            .iter()
132            .find(|device| device.id() == request.device())
133        else {
134            return Err(Error::Eval(format!(
135                "fake backend has no device {}",
136                request.device()
137            )));
138        };
139        if device.media() != request.media() {
140            return Err(Error::TypeMismatch {
141                expected: "request media matching fake device",
142                found: "request media for another fake device",
143            });
144        }
145        if device.direction() != request.direction() {
146            return Err(Error::TypeMismatch {
147                expected: "request direction matching fake device",
148                found: "request direction for another fake device",
149            });
150        }
151        let config = HostStreamConfig::from_request(
152            request,
153            HostLatencyInfo::default(),
154            HostClockInfo::new(fake_clock(device.media()), None, true),
155        );
156        Ok(HostOpenStream::new(config))
157    }
158}
159
160fn fake_device(
161    id: &str,
162    media: StreamMedia,
163    direction: HostDirection,
164    capacity: usize,
165) -> HostDeviceSpec {
166    HostDeviceSpec::new(
167        Symbol::new(id),
168        fake_backend_symbol(),
169        media,
170        direction,
171        fake_clock(media),
172        BufferPolicy::bounded(capacity).expect("valid fake buffer"),
173    )
174}
175
176fn fake_clock(media: StreamMedia) -> Symbol {
177    match media {
178        StreamMedia::Pcm => ClockDomain::Sample.symbol(),
179        StreamMedia::Midi => ClockDomain::MidiTick.symbol(),
180        StreamMedia::Diagnostic | StreamMedia::Data => ClockDomain::ServerFrame.symbol(),
181    }
182}
183
184fn fake_port(
185    id: &str,
186    device: &Symbol,
187    media: StreamMedia,
188    direction: HostDirection,
189) -> HostPortSpec {
190    HostPortSpec::new(
191        Symbol::new(id),
192        device.clone(),
193        fake_backend_symbol(),
194        media,
195        direction,
196    )
197}