1use std::ops::Range;
9
10use crate::segmentation::{Activity, ChunkShape, Segmenter};
11use crate::speaker::diarizer::enrolled_name;
12use crate::speaker::{AnonymousNumbering, Embedder, Label, Profile};
13
14use super::grid::FrameGrid;
15use super::timeline::Timeline;
16use super::{Merged, Revision, Spoken, Voices};
17use crate::audio::RecordingReader;
18
19const HOP_SAMPLES: u64 = 32_000;
22
23const MIN_TURN: f64 = 0.3;
25const MIN_GAP: f64 = 0.5;
26
27const MIN_ACTIVE_FRAMES: usize = 30;
31
32#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct Grouping {
35 pub threshold: f32,
37 pub least_voiceprints: usize,
39}
40
41impl Default for Grouping {
42 fn default() -> Self {
43 Self {
44 threshold: 0.25,
45 least_voiceprints: 12,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Standing {
53 pub label: Label,
54 pub voiceprints: usize,
55 pub eligible: bool,
57}
58
59pub struct PyannoteVoices<S, E> {
60 segmenter: S,
61 embedder: E,
62 enrolled: Vec<Profile>,
63 grouping: Grouping,
64 shape: ChunkShape,
65 grid: FrameGrid,
66 voices: Vec<Voice>,
67 next_chunk: u64,
69 timeline: Timeline,
71 numbering: AnonymousNumbering,
72}
73
74pub struct Chunk {
76 pub at: u64,
78 pub frames: Vec<Activity>,
80 pub voices: Vec<Voiceprint>,
82}
83
84pub struct Voiceprint {
86 pub local: usize,
89 pub values: Vec<f32>,
90}
91
92struct Voice {
95 label: Label,
96 sum: Vec<f32>,
97 count: usize,
98}
99
100impl Voice {
101 fn enrolled(&self) -> bool {
102 matches!(self.label, Label::Enrolled(_))
103 }
104}
105
106impl<S: Segmenter, E: Embedder> PyannoteVoices<S, E> {
107 pub fn new(segmenter: S, embedder: E, enrolled: Vec<Profile>, grouping: Grouping) -> Self {
108 let shape = segmenter.shape();
109 Self {
110 segmenter,
111 embedder,
112 enrolled,
113 grouping,
114 grid: FrameGrid::from(&shape),
115 shape,
116 voices: Vec::new(),
117 next_chunk: 0,
118 timeline: Timeline::default(),
119 numbering: AnonymousNumbering::default(),
120 }
121 }
122
123 pub fn shape(&self) -> &ChunkShape {
125 &self.shape
126 }
127
128 pub fn hop(&self) -> u64 {
132 HOP_SAMPLES
133 }
134
135 fn has_chunk(&self, recording: &RecordingReader) -> bool {
141 match recording.reached_end() {
142 true => self.next_chunk < recording.held_to(),
143 false => recording.held_to() >= self.next_chunk + self.shape.samples as u64,
144 }
145 }
146
147 fn chunk_audio(&self, recording: &RecordingReader, chunk: Range<u64>) -> Option<Vec<f32>> {
156 let from = chunk.start;
157 let audio = recording.span(chunk);
158 if audio.len() == self.shape.samples {
159 return Some(audio.to_vec());
160 }
161 if !recording.reached_end() {
162 tracing::warn!(
163 wanted = self.shape.samples,
164 got = audio.len(),
165 lost_samples = self.shape.samples - audio.len(),
166 from,
167 "audio was released before the voice track read it; \
168 this stretch will have no speaker"
169 );
170 return None;
171 }
172 let mut padded = audio.to_vec();
173 padded.resize(self.shape.samples, 0.0);
174 Some(padded)
175 }
176
177 pub fn hear(&mut self, recording: &RecordingReader) -> Option<Chunk> {
181 while self.has_chunk(recording) {
182 let at = self.next_chunk;
183 let span = at..at + self.shape.samples as u64;
184 self.next_chunk += HOP_SAMPLES;
185
186 let Some(audio) = self.chunk_audio(recording, span) else {
187 continue;
188 };
189 let frames = match self.segmenter.segment(&audio) {
190 Ok(frames) => frames,
191 Err(error) => {
192 tracing::warn!(
193 error = %crate::primitives::error_chain(&error),
194 "speech segmentation failed"
195 );
196 continue;
197 }
198 };
199 let voices = (0..self.shape.speakers)
200 .filter(|&local| {
201 frames.iter().filter(|frame| frame.speaks(local)).count() >= MIN_ACTIVE_FRAMES
202 })
203 .filter_map(|local| {
204 let heard = masked(&audio, &frames, local, &self.grid);
205 match self.embedder.embed(&heard) {
206 Ok(values) => values.map(|values| Voiceprint { local, values }),
207 Err(error) => {
208 tracing::warn!(
209 error = %crate::primitives::error_chain(&error),
210 "speaker embedding failed"
211 );
212 None
213 }
214 }
215 })
216 .collect();
217 return Some(Chunk { at, frames, voices });
218 }
219 None
220 }
221
222 pub fn group(&mut self, chunk: &Chunk) -> Revision {
225 let at = self.grid.at(chunk.at);
226 self.timeline.judge(at, &chunk.frames);
227
228 let mut concurrent: Vec<Label> = Vec::new();
230 for voiceprint in &chunk.voices {
231 let placed = self.place(&voiceprint.values, &concurrent);
232 self.timeline
233 .vote(placed, at, &chunk.frames, voiceprint.local);
234 concurrent.push(self.voices[placed].label.clone());
235 }
236 self.merge(&concurrent)
239 }
240
241 fn place(&mut self, voiceprint: &[f32], concurrent: &[Label]) -> usize {
247 if let Some(name) = enrolled_name(&self.enrolled, voiceprint) {
248 let label = Label::Enrolled(name.clone());
249 return match self.voices.iter().position(|voice| voice.label == label) {
250 Some(index) => self.absorb(index, voiceprint),
251 None => self.open(label, voiceprint),
252 };
253 }
254 let nearest = self
255 .voices
256 .iter()
257 .enumerate()
258 .filter(|(_, voice)| !concurrent.contains(&voice.label))
259 .map(|(index, voice)| (index, alike(&voice.sum, voice.count, voiceprint, 1)))
260 .reduce(|best, candidate| {
261 if candidate.1 > best.1 {
262 candidate
263 } else {
264 best
265 }
266 });
267 if let Some((index, similarity)) = nearest
268 && similarity >= self.grouping.threshold
269 {
270 return self.absorb(index, voiceprint);
271 }
272 let label = self.numbering.issue();
273 self.open(label, voiceprint)
274 }
275
276 fn absorb(&mut self, index: usize, voiceprint: &[f32]) -> usize {
278 let voice = &mut self.voices[index];
279 for (sum, value) in voice.sum.iter_mut().zip(voiceprint) {
280 *sum += value;
281 }
282 voice.count += 1;
283 index
284 }
285
286 fn open(&mut self, label: Label, voiceprint: &[f32]) -> usize {
287 self.voices.push(Voice {
288 label,
289 sum: voiceprint.to_vec(),
290 count: 1,
291 });
292 self.timeline.open_voice();
293 self.voices.len() - 1
294 }
295
296 fn merge(&mut self, concurrent: &[Label]) -> Revision {
301 let mut revision = Revision::default();
302 while let Some((keeper, absorbed)) = self.closest_pair(concurrent) {
303 self.timeline.absorb(keeper, absorbed);
304 let taken = self.voices.remove(absorbed);
305 revision.merges.push(Merged {
306 from: taken.label.clone(),
307 into: self.voices[keeper].label.clone(),
308 });
309 let voice = &mut self.voices[keeper];
310 for (sum, value) in voice.sum.iter_mut().zip(&taken.sum) {
311 *sum += value;
312 }
313 voice.count += taken.count;
314 }
315 revision
316 }
317
318 fn closest_pair(&self, concurrent: &[Label]) -> Option<(usize, usize)> {
321 let mut best: Option<(usize, usize, f32)> = None;
322 for a in 0..self.voices.len() {
323 for b in a + 1..self.voices.len() {
324 if self.apart(a, b, concurrent) {
325 continue;
326 }
327 let similarity = alike(
328 &self.voices[a].sum,
329 self.voices[a].count,
330 &self.voices[b].sum,
331 self.voices[b].count,
332 );
333 if best.is_none_or(|(_, _, top)| similarity > top) {
334 best = Some((a, b, similarity));
335 }
336 }
337 }
338 best.filter(|(_, _, similarity)| *similarity >= self.grouping.threshold)
339 .map(|(a, b, _)| (a, b))
340 }
341
342 fn apart(&self, a: usize, b: usize, concurrent: &[Label]) -> bool {
345 let (a, b) = (&self.voices[a], &self.voices[b]);
346 let overlapped = concurrent.contains(&a.label) && concurrent.contains(&b.label);
347 overlapped || (a.enrolled() && b.enrolled())
348 }
349
350 pub fn heard_so_far(&self) -> Vec<Standing> {
356 self.voices
357 .iter()
358 .map(|voice| Standing {
359 label: voice.label.clone(),
360 voiceprints: voice.count,
361 eligible: voice.count >= self.grouping.least_voiceprints,
362 })
363 .collect()
364 }
365
366 fn eligible(&self) -> Vec<bool> {
368 self.voices
369 .iter()
370 .map(|voice| voice.count >= self.grouping.least_voiceprints)
371 .collect()
372 }
373}
374
375impl<S: Segmenter, E: Embedder> PyannoteVoices<S, E> {
376 pub fn retained_from(&self) -> u64 {
379 self.next_chunk
380 }
381
382 pub fn skip_to(&mut self, position: u64) {
390 self.next_chunk = self.next_chunk.max(position);
391 }
392
393 pub fn turns(&self) -> Vec<Spoken> {
399 let eligible = self.eligible();
402 let gap = self.grid.frames(MIN_GAP);
403 let least = self.grid.frames(MIN_TURN);
404 let mut spoken: Vec<Spoken> = self
405 .timeline
406 .runs(&eligible, gap, least)
407 .into_iter()
408 .map(|(voice, run)| Spoken {
409 span: self.grid.start(run.start)..self.grid.start(run.end),
410 speaker: self.voices[voice].label.clone(),
411 })
412 .collect();
413 spoken.sort_by_key(|turn| turn.span.start);
414 spoken
415 }
416}
417
418impl<S: Segmenter, E: Embedder> Voices for PyannoteVoices<S, E> {
419 fn attribute(&self, spans: &[Range<u64>]) -> Vec<Option<Label>> {
420 let eligible = self.eligible();
421 spans
422 .iter()
423 .map(|span| {
424 let opens = self.grid.at(span.start);
426 let mut owed = vec![0_usize; self.voices.len()];
427 for frame in opens..self.grid.at(span.end).max(opens + 1) {
428 for voice in self.timeline.speakers_in(frame, &eligible) {
429 owed[voice] += 1;
430 }
431 }
432 owed.into_iter()
433 .enumerate()
434 .filter(|(_, held)| *held > 0)
435 .max_by_key(|(_, held)| *held)
436 .map(|(voice, _)| self.voices[voice].label.clone())
437 })
438 .collect()
439 }
440}
441
442fn masked(chunk: &[f32], frames: &[Activity], local: usize, grid: &FrameGrid) -> Vec<f32> {
444 frames
445 .iter()
446 .enumerate()
447 .filter(|(_, frame)| frame.speaks(local))
448 .flat_map(|(frame, _)| {
449 let heard = grid.window(frame);
450 let from = heard.start.min(chunk.len());
451 let to = heard.end.min(chunk.len());
452 chunk[from..to].iter().copied()
453 })
454 .collect()
455}
456
457fn alike(a: &[f32], a_count: usize, b: &[f32], b_count: usize) -> f32 {
463 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
464 dot / (a_count * b_count) as f32
465}
466
467#[cfg(test)]
469mod testing {
470 use crate::segmentation::SegmentationError;
471 use crate::speaker::SpeakerError;
472
473 use super::*;
474
475 pub(super) struct ScriptedSegmenter {
479 chunks: std::vec::IntoIter<Vec<Activity>>,
480 samples: usize,
483 }
484
485 pub(super) const SCRIPTED_SHIFT: usize = 320;
487
488 impl Segmenter for ScriptedSegmenter {
489 fn shape(&self) -> ChunkShape {
490 ChunkShape {
491 samples: self.samples,
492 frame_shift: SCRIPTED_SHIFT,
493 frame_size: SCRIPTED_SHIFT,
494 speakers: 3,
495 }
496 }
497
498 fn segment(&mut self, _chunk: &[f32]) -> Result<Vec<Activity>, SegmentationError> {
499 Ok(self.chunks.next().unwrap_or_default())
500 }
501 }
502
503 pub(super) struct ScriptedEmbedder {
506 voiceprints: std::vec::IntoIter<[f32; 2]>,
507 }
508
509 impl Embedder for ScriptedEmbedder {
510 fn embed(&mut self, _window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
511 Ok(self.voiceprints.next().map(Vec::from))
512 }
513
514 fn dimension(&self) -> usize {
515 2
516 }
517 }
518
519 pub(super) fn track(
522 chunks: Vec<Vec<Activity>>,
523 voiceprints: Vec<[f32; 2]>,
524 ) -> PyannoteVoices<ScriptedSegmenter, ScriptedEmbedder> {
525 track_of(HOP_SAMPLES as usize, chunks, voiceprints)
526 }
527
528 pub(super) fn track_with(
530 grouping: Grouping,
531 chunks: Vec<Vec<Activity>>,
532 voiceprints: Vec<[f32; 2]>,
533 ) -> PyannoteVoices<ScriptedSegmenter, ScriptedEmbedder> {
534 let mut voices = track_of(HOP_SAMPLES as usize, chunks, voiceprints);
535 voices.grouping = grouping;
536 voices
537 }
538
539 pub(super) fn track_of(
541 samples: usize,
542 chunks: Vec<Vec<Activity>>,
543 voiceprints: Vec<[f32; 2]>,
544 ) -> PyannoteVoices<ScriptedSegmenter, ScriptedEmbedder> {
545 PyannoteVoices::new(
546 ScriptedSegmenter {
547 chunks: chunks.into_iter(),
548 samples,
549 },
550 ScriptedEmbedder {
551 voiceprints: voiceprints.into_iter(),
552 },
553 Vec::new(),
554 Grouping::default(),
555 )
556 }
557
558 pub(super) fn min_voiceprints() -> usize {
561 Grouping::default().least_voiceprints
562 }
563
564 pub(super) fn hear_all<S: Segmenter, E: Embedder>(
567 voices: &mut PyannoteVoices<S, E>,
568 recording: &RecordingReader,
569 ) -> Revision {
570 let mut revision = Revision::default();
571 while let Some(chunk) = voices.hear(recording) {
572 revision.merges.append(&mut voices.group(&chunk).merges);
573 }
574 revision
575 }
576
577 pub(super) fn alone(local: usize) -> Vec<Activity> {
579 vec![Activity::SILENT.speaking(local); MIN_ACTIVE_FRAMES * 2]
580 }
581
582 pub(super) fn recording_of(chunks: usize) -> RecordingReader {
586 let mut recording =
587 RecordingReader::from_samples(&vec![0.1; chunks * HOP_SAMPLES as usize]);
588 recording.fill().expect("the pushed recording is whole");
589 recording
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::testing::*;
596 use super::*;
597
598 #[test]
599 fn overlapping_chunks_judge_the_same_frame_more_than_once() {
600 let width = HOP_SAMPLES as usize * 2;
604 let per_chunk = width / SCRIPTED_SHIFT;
605 let heard = min_voiceprints() + 1;
606 let speaking = vec![Activity::SILENT.speaking(0); per_chunk];
607 let mut voices = track_of(width, vec![speaking; heard], vec![[1.0, 0.0]; heard]);
608 hear_all(&mut voices, &recording_of(heard));
609
610 let overlap = per_chunk / 2;
612 let named = voices.timeline.speakers_in(overlap, &voices.eligible());
613 assert_eq!(named.len(), 1, "the overlap named more than one voice");
614 }
615
616 #[test]
617 fn a_track_told_to_skip_ahead_never_asks_for_the_audio_behind_it() {
618 let recording = recording_of(4);
619 let mut voices = track(
620 Vec::from([alone(0), alone(0)]),
621 Vec::from([[1.0, 0.0], [1.0, 0.0]]),
622 );
623
624 voices.skip_to(HOP_SAMPLES * 2);
625 hear_all(&mut voices, &recording);
626
627 assert_eq!(
628 voices.retained_from(),
629 HOP_SAMPLES * 4,
630 "the walk should resume past the skip, not replay it"
631 );
632 }
633
634 #[test]
635 fn skipping_backwards_does_nothing() {
636 let mut voices = track(Vec::new(), Vec::new());
637 voices.skip_to(HOP_SAMPLES * 3);
638
639 voices.skip_to(HOP_SAMPLES);
640
641 assert_eq!(voices.retained_from(), HOP_SAMPLES * 3);
642 }
643
644 #[test]
645 fn two_voiceprints_alike_enough_are_one_voice() {
646 let recording = recording_of(2);
647 let mut voices = track(
648 Vec::from([alone(0), alone(0)]),
649 Vec::from([[1.0, 0.0], [1.0, 0.0]]),
650 );
651 assert!(!hear_all(&mut voices, &recording).revised());
652 assert_eq!(voices.voices.len(), 1);
653 }
654
655 #[test]
656 fn a_voiceprint_matching_nothing_opens_a_voice_and_revises_nothing() {
657 let recording = recording_of(2);
658 let mut voices = track(
659 Vec::from([alone(0), alone(0)]),
660 Vec::from([[1.0, 0.0], [0.0, 1.0]]),
661 );
662 assert!(!hear_all(&mut voices, &recording).revised());
665 assert_eq!(voices.voices.len(), 2);
666 assert_eq!(voices.voices[0].label, Label::Anonymous(1));
667 assert_eq!(voices.voices[1].label, Label::Anonymous(2));
668 }
669
670 #[test]
671 fn an_enrolled_voice_opens_its_own_voice_beside_an_anonymous_one() {
672 let recording = recording_of(2);
673 let mut voices = track(
674 Vec::from([alone(0), alone(0)]),
675 Vec::from([[1.0, 0.0], [0.0, 1.0]]),
676 );
677 let alex = crate::speaker::enrolled_voice(
678 "alex",
679 crate::speaker::Role::Guest,
680 Vec::from([0.0, 1.0]),
681 );
682 voices.enrolled = Vec::from([alex.clone()]);
683
684 hear_all(&mut voices, &recording);
685
686 let live: Vec<&Label> = voices.voices.iter().map(|voice| &voice.label).collect();
687 assert_eq!(live, [&Label::Anonymous(1), &Label::Enrolled(alex.name)]);
688 }
689
690 #[test]
691 fn voices_that_drift_together_merge_and_the_earlier_name_survives() {
692 let recording = recording_of(3);
693 let mut voices = track(
696 Vec::from([alone(0), alone(0), alone(0)]),
697 Vec::from([[1.0, 0.0], [0.17365, 0.98481], [0.5, 0.86603]]),
698 );
699 let revision = hear_all(&mut voices, &recording);
700 assert!(revision.revised());
701 assert_eq!(revision.merges[0].from, Label::Anonymous(2));
702 assert_eq!(revision.merges[0].into, Label::Anonymous(1));
703 assert_eq!(voices.voices.len(), 1);
704 assert_eq!(voices.voices[0].label, Label::Anonymous(1));
705 }
706
707 #[test]
708 fn a_merged_number_is_retired_rather_than_handed_to_the_next_voice() {
709 let recording = recording_of(6);
710 let mut voices = track(
714 Vec::from([alone(0), alone(0), alone(0), alone(0), alone(0), alone(0)]),
715 Vec::from([
716 [1.0, 0.0],
717 [-0.1736, 0.9848],
718 [-0.766, -0.643],
719 [0.643, 0.766],
720 [0.342, 0.940],
721 [0.9, -0.436],
722 ]),
723 );
724 let revision = hear_all(&mut voices, &recording);
725 assert_eq!(revision.merges[0].from, Label::Anonymous(2));
726
727 let live: Vec<&Label> = voices.voices.iter().map(|voice| &voice.label).collect();
728 assert_eq!(
731 live,
732 [
733 &Label::Anonymous(1),
734 &Label::Anonymous(3),
735 &Label::Anonymous(4)
736 ]
737 );
738 }
739
740 #[test]
741 fn a_span_takes_the_voice_that_owned_the_most_of_it() {
742 let half = min_voiceprints();
745 let recording = recording_of(half * 2);
746 let mut voiceprints = vec![[1.0, 0.0]; half];
747 voiceprints.extend(vec![[0.0, 1.0]; half]);
748 let mut voices = track(vec![alone(0); half * 2], voiceprints);
749 hear_all(&mut voices, &recording);
750
751 let turn = HOP_SAMPLES * half as u64;
752 let named = voices.attribute(&Vec::from([0..turn, turn..turn * 2]));
753 assert_eq!(
754 named,
755 Vec::from([Some(Label::Anonymous(1)), Some(Label::Anonymous(2))])
756 );
757 }
758
759 #[test]
760 fn a_voice_heard_too_few_times_names_nothing() {
761 let brief = min_voiceprints() - 1;
762 let recording = recording_of(brief);
763 let mut voices = track(vec![alone(0); brief], vec![[1.0, 0.0]; brief]);
764 hear_all(&mut voices, &recording);
765
766 assert_eq!(voices.voices.len(), 1);
769 let whole = 0..HOP_SAMPLES * brief as u64;
770 let named = voices.attribute(&[whole]);
771 assert_eq!(named, Vec::from([None]));
772 assert!(voices.turns().is_empty());
773 }
774
775 #[test]
776 fn lowering_the_bar_lets_a_rare_voice_name_its_audio() {
777 let brief = min_voiceprints() - 1;
780 let recording = recording_of(brief);
781 let mut voices = track_with(
782 Grouping {
783 least_voiceprints: brief,
784 ..Grouping::default()
785 },
786 vec![alone(0); brief],
787 vec![[1.0, 0.0]; brief],
788 );
789 hear_all(&mut voices, &recording);
790
791 let whole = 0..HOP_SAMPLES * brief as u64;
792 let named = voices.attribute(&[whole]);
793 assert_eq!(named, Vec::from([Some(Label::Anonymous(1))]));
794 }
795
796 #[test]
797 fn a_threshold_nothing_can_reach_gives_every_chunk_its_own_voice() {
798 let heard = min_voiceprints();
799 let recording = recording_of(heard);
800 let mut voices = track_with(
802 Grouping {
803 threshold: 1.1,
804 ..Grouping::default()
805 },
806 vec![alone(0); heard],
807 vec![[1.0, 0.0]; heard],
808 );
809 hear_all(&mut voices, &recording);
810
811 assert_eq!(voices.voices.len(), heard, "no two of them could join");
812 assert!(
813 voices.heard_so_far().iter().all(|voice| !voice.eligible),
814 "and none is heard often enough to be written"
815 );
816 }
817
818 #[test]
819 fn the_stretch_after_the_last_whole_chunk_is_still_read() {
820 let whole = min_voiceprints();
821 let tail = HOP_SAMPLES as usize / 3;
823 let mut recording =
824 RecordingReader::from_samples(&vec![0.1; whole * HOP_SAMPLES as usize + tail]);
825 recording.fill().expect("the pushed recording is whole");
826
827 let mut voices = track(vec![alone(0); whole + 1], vec![[1.0, 0.0]; whole + 1]);
828 hear_all(&mut voices, &recording);
829
830 assert!(
833 voices.retained_from() >= recording.held_to(),
834 "the track stopped at {} with {} recorded",
835 voices.retained_from(),
836 recording.held_to()
837 );
838 }
839
840 #[test]
841 fn a_span_past_the_tracks_reach_stays_unnamed() {
842 let heard = min_voiceprints();
843 let recording = recording_of(heard);
844 let mut voices = track(vec![alone(0); heard], vec![[1.0, 0.0]; heard]);
845 hear_all(&mut voices, &recording);
846 let past = HOP_SAMPLES * 100..HOP_SAMPLES * 101;
848 let named = voices.attribute(std::slice::from_ref(&past));
849 assert_eq!(named, Vec::from([None]));
850 }
851
852 #[test]
853 fn a_voice_too_brief_to_embed_is_left_alone() {
854 let recording = recording_of(1);
855 let brief = vec![Activity::SILENT.speaking(0); MIN_ACTIVE_FRAMES - 1];
856 let mut voices = track(Vec::from([brief]), Vec::from([[1.0, 0.0]]));
857 hear_all(&mut voices, &recording);
858 assert!(voices.voices.is_empty());
859 }
860
861 fn frames(pattern: &[&[usize]]) -> Vec<Activity> {
862 pattern
863 .iter()
864 .map(|speaking| {
865 speaking
866 .iter()
867 .fold(Activity::SILENT, |active, &voice| active.speaking(voice))
868 })
869 .collect()
870 }
871
872 fn grid(shift: usize, size: usize) -> FrameGrid {
874 FrameGrid::from(&ChunkShape {
875 samples: 8,
876 frame_shift: shift,
877 frame_size: size,
878 speakers: 3,
879 })
880 }
881
882 #[test]
883 fn only_a_voices_own_frames_reach_the_embedder() {
884 let chunk: Vec<f32> = (0..8).map(|sample| sample as f32).collect();
885 let heard = masked(&chunk, &frames(&[&[], &[0], &[1], &[0]]), 0, &grid(2, 2));
886 assert_eq!(heard, Vec::from([2.0, 3.0, 6.0, 7.0]));
887 }
888
889 #[test]
890 fn a_frame_reaches_past_its_own_shift_and_masks_from_the_middle() {
891 let chunk: Vec<f32> = (0..8).map(|sample| sample as f32).collect();
892 let heard = masked(&chunk, &frames(&[&[], &[0]]), 0, &grid(2, 4));
895 assert_eq!(heard, Vec::from([3.0, 4.0]));
896 }
897
898 #[test]
899 fn a_frame_nobody_speaks_in_masks_to_nothing() {
900 let chunk = vec![1.0_f32; 8];
901 assert!(masked(&chunk, &frames(&[&[], &[]]), 0, &grid(2, 2)).is_empty());
902 }
903
904 #[test]
905 fn one_voice_is_as_alike_to_itself_as_a_cosine_gets() {
906 let voice = [0.6, 0.8];
907 assert!((alike(&voice, 1, &voice, 1) - 1.0).abs() < 1e-6);
908 }
909
910 #[test]
911 fn averaging_over_members_keeps_a_spread_voice_honest() {
912 let spread = [1.0, 1.0];
915 let one = [1.0, 0.0];
916 assert!((alike(&spread, 2, &one, 1) - 0.5).abs() < 1e-6);
917 }
918}