Skip to main content

dspk_core/recording/
remap.rs

1//! Channel maps: which input channel becomes which output channel, written as pairs (like a
2//! hardware channel mapper), and a recording read through one.
3//!
4//! A map file (TOML or JSON, one schema):
5//!
6//! ```toml
7//! [remap]
8//! base = 0            # 1 for 1-based numbers (TDT, MATLAB)
9//! pairs = [
10//!   { from = 4, to = 0, name = "A1" },
11//!   { from = 2, to = 1, name = "A2" },
12//!   { from = 7, to = 2 },
13//! ]
14//! ```
15//!
16//! Every output channel `0..n` gets exactly one input; inputs left out are dropped; the same input
17//! twice is refused; names are optional (an unnamed output keeps its input's name).
18
19use std::ops::Range;
20use std::sync::Arc;
21
22use serde::{Deserialize, Serialize};
23
24use super::info::RecordingInfo;
25use super::source::{check_read, check_read_stored, RecordingSource};
26use crate::error::{DspError, DspResult};
27
28/// Default of [`ChannelMap::base`]: channel numbers start at 0.
29pub const DEFAULT_CHANNEL_BASE: usize = 0;
30
31/// One output channel: input channel `from` becomes output channel `to`, optionally named.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[serde(deny_unknown_fields)]
35pub struct ChannelPair {
36    pub from: usize,
37    pub to: usize,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub name: Option<String>,
40}
41
42/// Input channel → output channel pairs (see the module docs for the rules).
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(deny_unknown_fields)]
46pub struct ChannelMap {
47    /// The number of the first channel in `pairs` (0, or 1 for 1-based maps).
48    #[serde(default = "default_base")]
49    pub base: usize,
50    pub pairs: Vec<ChannelPair>,
51}
52
53fn default_base() -> usize {
54    DEFAULT_CHANNEL_BASE
55}
56
57/// A map file: the map under `[remap]` (TOML) or `"remap"` (JSON).
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[serde(deny_unknown_fields)]
61pub struct ChannelMapFile {
62    pub remap: ChannelMap,
63}
64
65impl ChannelMap {
66    /// Output channel `i` reads input `order[i]` (0-based), with no names.
67    pub fn from_order(order: &[usize]) -> Self {
68        let pairs = order.iter().enumerate().map(|(to, &from)| ChannelPair { from, to, name: None }).collect();
69        Self { base: 0, pairs }
70    }
71
72    /// The input channel (0-based) of every output channel, in output order. Checks the rules:
73    /// numbers at or above `base`, outputs `0..n` each exactly once, no input twice.
74    pub fn order(&self) -> DspResult<Vec<usize>> {
75        let bad = |m: String| Err(DspError::InvalidConfig(format!("channel map: {m}")));
76        if self.pairs.is_empty() {
77            return bad("no pairs".into());
78        }
79        let n = self.pairs.len();
80        let mut order = vec![usize::MAX; n];
81        let mut inputs = std::collections::HashSet::with_capacity(n);
82        for p in &self.pairs {
83            let (Some(from), Some(to)) = (p.from.checked_sub(self.base), p.to.checked_sub(self.base)) else {
84                return bad(format!("pair {} → {} is below base {}", p.from, p.to, self.base));
85            };
86            if to >= n {
87                return bad(format!("output {} is past the last output ({} pairs give outputs {}..{})", p.to, n, self.base, self.base + n - 1));
88            }
89            if order[to] != usize::MAX {
90                return bad(format!("output {} is given twice", p.to));
91            }
92            if !inputs.insert(from) {
93                return bad(format!("input {} is used twice", p.from));
94            }
95            order[to] = from;
96        }
97        Ok(order)
98    }
99
100    /// [`Self::order`], with every input checked against a recording of `channels` channels.
101    pub fn resolve(&self, channels: usize) -> DspResult<Vec<usize>> {
102        let order = self.order()?;
103        if let Some(&c) = order.iter().find(|&&c| c >= channels) {
104            return Err(DspError::InvalidConfig(format!("channel map: input {} does not exist (the recording has {channels} channels)", c + self.base)));
105        }
106        Ok(order)
107    }
108
109    /// The name of every output channel given one (by output, 0-based).
110    pub fn names(&self) -> Vec<Option<String>> {
111        let mut names = vec![None; self.pairs.len()];
112        for p in &self.pairs {
113            if let Some(slot) = p.to.checked_sub(self.base).and_then(|to| names.get_mut(to)) {
114                slot.clone_from(&p.name);
115            }
116        }
117        names
118    }
119
120    /// This map, then `next` (whose inputs are this map's outputs): one map from this map's inputs
121    /// to `next`'s outputs, 0-based. A name from `next` wins over this map's.
122    pub fn then(&self, next: &ChannelMap) -> DspResult<ChannelMap> {
123        let (first, second) = (self.order()?, next.order()?);
124        let first_names = self.names();
125        let second_names = next.names();
126        let mut pairs = Vec::with_capacity(second.len());
127        for (to, &mid) in second.iter().enumerate() {
128            let Some(&from) = first.get(mid) else {
129                return Err(DspError::InvalidConfig(format!("channel map: input {} does not exist (the map before gives {} channels)", mid + next.base, first.len())));
130            };
131            pairs.push(ChannelPair { from, to, name: second_names[to].clone().or_else(|| first_names[mid].clone()) });
132        }
133        Ok(ChannelMap { base: 0, pairs })
134    }
135}
136
137/// A recording read through a [`ChannelMap`]: channel `i` is the parent's `order[i]`, with the
138/// map's name when it gives one. Reads go straight to the parent in the new order (no copy).
139pub struct RemappedSource {
140    parent: Arc<dyn RecordingSource>,
141    order: Vec<usize>,
142    info: RecordingInfo,
143}
144
145impl RemappedSource {
146    pub fn new(parent: Arc<dyn RecordingSource>, map: &ChannelMap) -> DspResult<Self> {
147        let p = parent.info();
148        let order = map.resolve(p.channel_count())?;
149        let mut info = p.clone();
150        info.channels = order
151            .iter()
152            .zip(map.names())
153            .map(|(&c, name)| {
154                let mut ch = p.channels[c].clone();
155                if let Some(name) = name {
156                    ch.name = name;
157                }
158                ch
159            })
160            .collect();
161        Ok(Self { parent, order, info })
162    }
163
164    /// The parent channel of every channel (0-based).
165    pub fn order(&self) -> &[usize] {
166        &self.order
167    }
168
169    fn parent_channels(&self, channels: &[usize]) -> Vec<usize> {
170        channels.iter().map(|&c| self.order[c]).collect()
171    }
172}
173
174impl RecordingSource for RemappedSource {
175    fn info(&self) -> &RecordingInfo {
176        &self.info
177    }
178
179    fn chunk_samples(&self) -> Option<u64> {
180        self.parent.chunk_samples()
181    }
182
183    fn read(&self, channels: &[usize], samples: Range<u64>, out: &mut [f32]) -> DspResult<()> {
184        check_read(&self.info, channels, &samples, out.len())?;
185        self.parent.read(&self.parent_channels(channels), samples, out)
186    }
187
188    fn read_stored(&self, channels: &[usize], samples: Range<u64>, out: &mut [u8]) -> DspResult<()> {
189        check_read_stored(&self.info, channels, &samples, out.len())?;
190        self.parent.read_stored(&self.parent_channels(channels), samples, out)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::recording::MemoryRecording;
198
199    fn pair(from: usize, to: usize, name: Option<&str>) -> ChannelPair {
200        ChannelPair { from, to, name: name.map(str::to_string) }
201    }
202
203    #[test]
204    fn order_follows_the_pairs_and_checks_the_rules() {
205        let map = ChannelMap { base: 1, pairs: vec![pair(3, 2, None), pair(1, 1, Some("A1"))] };
206        assert_eq!(map.order().unwrap(), vec![0, 2]);
207        assert_eq!(map.names(), vec![Some("A1".to_string()), None]);
208        assert!(map.resolve(2).is_err(), "input 3 of a 2-channel recording");
209        // An output twice, an input twice, a gap, below base, empty
210        for pairs in [
211            vec![pair(0, 0, None), pair(1, 0, None)],
212            vec![pair(0, 0, None), pair(0, 1, None)],
213            vec![pair(0, 0, None), pair(1, 2, None)],
214            vec![],
215        ] {
216            assert!(ChannelMap { base: 0, pairs }.order().is_err());
217        }
218        assert!(ChannelMap { base: 1, pairs: vec![pair(0, 1, None)] }.order().is_err());
219    }
220
221    #[test]
222    fn maps_compose() {
223        let a = ChannelMap { base: 0, pairs: vec![pair(5, 0, Some("x")), pair(3, 1, Some("y")), pair(1, 2, None)] };
224        let b = ChannelMap { base: 0, pairs: vec![pair(2, 0, Some("z")), pair(0, 1, None)] };
225        let ab = a.then(&b).unwrap();
226        assert_eq!(ab.order().unwrap(), vec![1, 5]);
227        assert_eq!(ab.names(), vec![Some("z".to_string()), Some("x".to_string())]);
228    }
229
230    #[test]
231    fn a_remapped_source_reads_in_the_new_order() {
232        // 3 channels × 4 samples: value = channel · 10 + sample
233        let data: Vec<f32> = (0..3).flat_map(|c| (0..4).map(move |s| (c * 10 + s) as f32)).collect();
234        let parent: Arc<dyn RecordingSource> = Arc::new(MemoryRecording::new("m", data, 3, 1000.0).unwrap());
235        let map = ChannelMap { base: 0, pairs: vec![pair(2, 0, Some("A1")), pair(0, 1, None)] };
236        let r = RemappedSource::new(parent, &map).unwrap();
237        assert_eq!(r.info().channel_count(), 2);
238        assert_eq!(r.info().channels[0].name, "A1");
239        assert_eq!(r.info().channels[1].name, "ch0");
240        let mut out = vec![0.0; 4];
241        r.read(&[1, 0], 1..3, &mut out).unwrap();
242        assert_eq!(out, vec![1.0, 2.0, 21.0, 22.0]);
243    }
244}