Skip to main content

dspk_io/core/
merged.rs

1//! Several recordings read as one, their channels side by side ([`MergedRecording`]).
2//!
3//! Each part contributes the channels picked from it, in order; parts must share the sample rate
4//! and length. A read touches each part once with the channels it owns. When every part stores the
5//! same format, stored reads pass through (integer recordings stay compact for device upload);
6//! otherwise the merge stores float32 with unit gain, so stored and scaled reads agree.
7
8use std::ops::Range;
9
10use dspk_core::recording::{check_read, check_read_stored};
11use dspk_core::{DspError, DspResult, MemoryOrder, RecordingInfo, RecordingSource, SampleFormat};
12
13/// A recording and the channels taken from it (`None`: all, in order).
14pub type MergeInput = (Box<dyn RecordingSource>, Option<Vec<usize>>);
15
16/// Channels of several recordings, side by side.
17pub struct MergedRecording {
18    parts: Vec<Box<dyn RecordingSource>>,
19    /// For each merged channel: (part, channel within the part).
20    map: Vec<(usize, usize)>,
21    info: RecordingInfo,
22    /// Every part stores `info.format`, so stored reads pass through.
23    stored_passthrough: bool,
24}
25
26impl MergedRecording {
27    /// Merges `parts`, each with the channels to take from it (`None`: all, in order).
28    ///
29    /// # Errors
30    /// [`DspError::InvalidConfig`] when there are no parts, a part's channel does not exist, or the
31    /// parts differ in sample rate or length.
32    pub fn new(parts: Vec<MergeInput>) -> DspResult<Self> {
33        let Some((first, _)) = parts.first() else {
34            return Err(DspError::InvalidConfig("merge: no recordings".into()));
35        };
36        let (rate, samples) = (first.info().sample_rate, first.info().samples);
37        let format = first.info().format;
38        let mut map = Vec::new();
39        let mut channels = Vec::new();
40        let mut names = Vec::new();
41        let mut stored_passthrough = true;
42        for (p, (source, picked)) in parts.iter().enumerate() {
43            let info = source.info();
44            if info.sample_rate != rate || info.samples != samples {
45                return Err(DspError::InvalidConfig(format!(
46                    "merge: {} has {} samples at {} Hz, {} has {} at {} Hz (parts must match)",
47                    info.name,
48                    info.samples,
49                    info.sample_rate_hz(),
50                    first.info().name,
51                    samples,
52                    rate.rate_hz()
53                )));
54            }
55            stored_passthrough &= info.format == format;
56            let picked = picked.clone().unwrap_or_else(|| (0..info.channel_count()).collect());
57            for c in picked {
58                let ch = info.channels.get(c).ok_or_else(|| {
59                    DspError::InvalidConfig(format!("merge: {} has no channel {c} ({} channels)", info.name, info.channel_count()))
60                })?;
61                map.push((p, c));
62                channels.push(ch.clone());
63            }
64            names.push(info.name.clone());
65        }
66        let stored_format = if stored_passthrough { format } else { SampleFormat::F32 };
67        let mut info = RecordingInfo::new(names.join("+"), channels.len(), samples, rate, stored_format, MemoryOrder::ChannelMajor);
68        if !stored_passthrough {
69            // Stored reads become the scaled values as float32
70            for ch in &mut channels {
71                ch.gain = 1.0;
72                ch.offset = 0.0;
73            }
74        }
75        info.channels = channels;
76        let parts = parts.into_iter().map(|(s, _)| s).collect();
77        Ok(Self { parts, map, info, stored_passthrough })
78    }
79
80    /// For each part, the merged positions requested from it and their channels within the part.
81    fn by_part(&self, channels: &[usize]) -> Vec<(Vec<usize>, Vec<usize>)> {
82        let mut groups = vec![(Vec::new(), Vec::new()); self.parts.len()];
83        for (i, &c) in channels.iter().enumerate() {
84            let (p, local) = self.map[c];
85            groups[p].0.push(i);
86            groups[p].1.push(local);
87        }
88        groups
89    }
90}
91
92impl RecordingSource for MergedRecording {
93    fn info(&self) -> &RecordingInfo {
94        &self.info
95    }
96
97    fn read(&self, channels: &[usize], samples: Range<u64>, out: &mut [f32]) -> DspResult<()> {
98        let n = check_read(&self.info, channels, &samples, out.len())?;
99        let mut buf = Vec::new();
100        for (p, (positions, local)) in self.by_part(channels).into_iter().enumerate() {
101            if positions.is_empty() {
102                continue;
103            }
104            buf.resize(local.len() * n, 0.0f32);
105            self.parts[p].read(&local, samples.clone(), &mut buf)?;
106            for (k, &i) in positions.iter().enumerate() {
107                out[i * n..(i + 1) * n].copy_from_slice(&buf[k * n..(k + 1) * n]);
108            }
109        }
110        Ok(())
111    }
112
113    fn read_stored(&self, channels: &[usize], samples: Range<u64>, out: &mut [u8]) -> DspResult<()> {
114        if !self.stored_passthrough {
115            // Float32 with unit gain: the scaled values are the stored ones
116            let n = check_read_stored(&self.info, channels, &samples, out.len())?;
117            let mut values = vec![0.0f32; channels.len() * n];
118            self.read(channels, samples, &mut values)?;
119            for (dst, v) in out.as_chunks_mut::<4>().0.iter_mut().zip(values) {
120                *dst = v.to_le_bytes();
121            }
122            return Ok(());
123        }
124        let n = check_read_stored(&self.info, channels, &samples, out.len())?;
125        let row = n * self.info.format.bytes();
126        let mut buf = Vec::new();
127        for (p, (positions, local)) in self.by_part(channels).into_iter().enumerate() {
128            if positions.is_empty() {
129                continue;
130            }
131            buf.resize(local.len() * row, 0u8);
132            self.parts[p].read_stored(&local, samples.clone(), &mut buf)?;
133            for (k, &i) in positions.iter().enumerate() {
134                out[i * row..(i + 1) * row].copy_from_slice(&buf[k * row..(k + 1) * row]);
135            }
136        }
137        Ok(())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use dspk_core::MemoryRecording;
145
146    fn rec(name: &str, channels: usize, samples: usize, offset: f32) -> Box<dyn RecordingSource> {
147        let data = (0..channels * samples).map(|i| i as f32 + offset).collect();
148        Box::new(MemoryRecording::new(name, data, channels, 1_000.0).unwrap())
149    }
150
151    #[test]
152    fn channels_from_each_part_side_by_side() {
153        let merged = MergedRecording::new(vec![(rec("a", 3, 50, 0.0), Some(vec![2, 0])), (rec("b", 2, 50, 1_000.0), None)]).unwrap();
154        assert_eq!(merged.info().channel_count(), 4);
155        assert_eq!(merged.info().name, "a+b");
156        let mut out = vec![0.0f32; 3 * 10];
157        // merged channels: a2, a0, b0, b1 → read b1, a2, b0
158        merged.read(&[3, 0, 2], 5..15, &mut out).unwrap();
159        let expect = |base: f32, c: usize| (5..15).map(move |t| base + (c * 50 + t) as f32);
160        let want: Vec<f32> = expect(1_000.0, 1).chain(expect(0.0, 2)).chain(expect(1_000.0, 0)).collect();
161        assert_eq!(out, want);
162    }
163
164    #[test]
165    fn stored_reads_match_scaled_reads() {
166        let merged = MergedRecording::new(vec![(rec("a", 2, 40, 0.0), None), (rec("b", 1, 40, 7.0), None)]).unwrap();
167        crate::test_support::assert_stored_matches(&merged, &[2, 0, 1], 3..30);
168    }
169
170    #[test]
171    fn mismatched_parts_are_refused() {
172        let err = MergedRecording::new(vec![(rec("a", 1, 40, 0.0), None), (rec("b", 1, 41, 0.0), None)]).err().unwrap();
173        assert!(err.to_string().contains("parts must match"), "{err}");
174        let err = MergedRecording::new(vec![(rec("a", 1, 40, 0.0), Some(vec![3]))]).err().unwrap();
175        assert!(err.to_string().contains("no channel 3"), "{err}");
176    }
177}