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 midi = fake_device("fake/midi", StreamMedia::Midi, HostDirection::Input, 8);
36        let pcm = fake_device("fake/pcm", StreamMedia::Pcm, HostDirection::Output, 8);
37        let ports = vec![
38            fake_port(
39                "fake/data/in",
40                data.id(),
41                StreamMedia::Data,
42                HostDirection::Input,
43            ),
44            fake_port(
45                "fake/midi/in",
46                midi.id(),
47                StreamMedia::Midi,
48                HostDirection::Input,
49            ),
50            fake_port(
51                "fake/pcm/out",
52                pcm.id(),
53                StreamMedia::Pcm,
54                HostDirection::Output,
55            ),
56        ];
57        Self {
58            info: HostBackendInfo::new(
59                backend.clone(),
60                Symbol::qualified("stream/transport", "fake"),
61                StreamMedia::Data,
62                false,
63            )
64            .with_capabilities(vec![
65                HostBackendCapability::Fake,
66                HostBackendCapability::Offline,
67                HostBackendCapability::MidiInput,
68                HostBackendCapability::AudioOutput,
69            ]),
70            inventory: HostDeviceInventory::new(backend)
71                .with_devices(vec![data, midi, pcm])
72                .with_ports(ports),
73        }
74    }
75
76    /// Builds a data stream request for the default fake data input device.
77    pub fn data_request(capacity: usize) -> Result<HostStreamConfigRequest> {
78        Ok(HostStreamConfigRequest::new(
79            fake_backend_symbol(),
80            Symbol::new("fake/data"),
81            StreamMedia::Data,
82            HostDirection::Input,
83            BufferPolicy::bounded(capacity)?,
84        ))
85    }
86}
87
88impl HostBackend for FakeBackend {
89    fn info(&self) -> &HostBackendInfo {
90        &self.info
91    }
92
93    fn enumerate(&self) -> Result<HostDeviceInventory> {
94        Ok(self.inventory.clone())
95    }
96
97    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
98        if request.backend() != self.info.id() {
99            return Err(Error::Eval(format!(
100                "fake backend cannot open {} requests",
101                request.backend()
102            )));
103        }
104        let Some(device) = self
105            .inventory
106            .devices()
107            .iter()
108            .find(|device| device.id() == request.device())
109        else {
110            return Err(Error::Eval(format!(
111                "fake backend has no device {}",
112                request.device()
113            )));
114        };
115        if device.media() != request.media() {
116            return Err(Error::TypeMismatch {
117                expected: "request media matching fake device",
118                found: "request media for another fake device",
119            });
120        }
121        if device.direction() != request.direction() {
122            return Err(Error::TypeMismatch {
123                expected: "request direction matching fake device",
124                found: "request direction for another fake device",
125            });
126        }
127        let config = HostStreamConfig::from_request(
128            request,
129            HostLatencyInfo::default(),
130            HostClockInfo::new(fake_clock(device.media()), None, true),
131        );
132        Ok(HostOpenStream::new(config))
133    }
134}
135
136fn fake_device(
137    id: &str,
138    media: StreamMedia,
139    direction: HostDirection,
140    capacity: usize,
141) -> HostDeviceSpec {
142    HostDeviceSpec::new(
143        Symbol::new(id),
144        fake_backend_symbol(),
145        media,
146        direction,
147        fake_clock(media),
148        BufferPolicy::bounded(capacity).expect("valid fake buffer"),
149    )
150}
151
152fn fake_clock(media: StreamMedia) -> Symbol {
153    match media {
154        StreamMedia::Pcm => ClockDomain::Sample.symbol(),
155        StreamMedia::Midi => ClockDomain::MidiTick.symbol(),
156        StreamMedia::Diagnostic | StreamMedia::Data => ClockDomain::ServerFrame.symbol(),
157    }
158}
159
160fn fake_port(
161    id: &str,
162    device: &Symbol,
163    media: StreamMedia,
164    direction: HostDirection,
165) -> HostPortSpec {
166    HostPortSpec::new(
167        Symbol::new(id),
168        device.clone(),
169        fake_backend_symbol(),
170        media,
171        direction,
172    )
173}