1#![deny(clippy::pedantic)]
2
3use std::fmt;
4use std::io::Read;
5use std::path::Path;
6
7use claxon::FlacReader;
8
9const MQA_MAGIC_WORD: u64 = 0xb_e049_8c88;
10const MQA_MAGIC_MASK: u64 = 0xF_FFFF_FFFF;
11
12const MQA_CHANNELS: u32 = 2;
13const MQA_MIN_BITS_PER_SAMPLE: u32 = 16;
14const SECONDS_TO_CHECK: u32 = 3;
15const BIT_PLANES_TO_CHECK: usize = 3;
16
17#[derive(Debug)]
21pub struct Error(claxon::Error);
22
23impl fmt::Display for Error {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 self.0.fmt(f)
26 }
27}
28
29impl std::error::Error for Error {
30 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
31 Some(&self.0)
32 }
33}
34
35impl From<claxon::Error> for Error {
36 fn from(error: claxon::Error) -> Self {
37 Self(error)
38 }
39}
40
41pub fn identify_mqa(path: impl AsRef<Path>) -> Result<bool, Error> {
49 identify(FlacReader::open(path)?)
50}
51
52pub fn identify_mqa_reader(reader: impl Read) -> Result<bool, Error> {
66 identify(FlacReader::new(reader)?)
67}
68
69fn identify<R: Read>(mut reader: FlacReader<R>) -> Result<bool, Error> {
70 let streaminfo = reader.streaminfo();
71
72 if streaminfo.channels != MQA_CHANNELS || streaminfo.bits_per_sample < MQA_MIN_BITS_PER_SAMPLE {
73 return Ok(false);
74 }
75
76 let mut detector = MagicWordDetector::new(streaminfo.bits_per_sample);
77 let samples_to_check = streaminfo.sample_rate * SECONDS_TO_CHECK;
78 let mut checked_samples = 0;
79
80 let mut block_reader = reader.blocks();
81 let mut buffer = Vec::new();
82
83 while checked_samples < samples_to_check {
84 let Some(block) = block_reader.read_next_or_eof(buffer)? else {
85 break;
86 };
87
88 for (left, right) in block.stereo_samples() {
89 if detector.push(left ^ right) {
90 return Ok(true);
91 }
92 }
93
94 checked_samples += block.duration();
95 buffer = block.into_buffer();
96 }
97
98 Ok(false)
99}
100
101struct MagicWordDetector {
102 buffers: [u64; BIT_PLANES_TO_CHECK],
103 position: u32,
104}
105
106impl MagicWordDetector {
107 fn new(bits_per_sample: u32) -> Self {
108 Self {
109 buffers: [0; BIT_PLANES_TO_CHECK],
110 position: bits_per_sample - MQA_MIN_BITS_PER_SAMPLE,
111 }
112 }
113
114 fn push(&mut self, sample: i32) -> bool {
115 #[allow(clippy::cast_sign_loss)]
116 let bits = u64::from(sample as u32 >> self.position);
117
118 let mut found = false;
119 for (plane, buffer) in self.buffers.iter_mut().enumerate() {
120 *buffer |= (bits >> plane) & 1;
121 found |= *buffer == MQA_MAGIC_WORD;
122 *buffer = (*buffer << 1) & MQA_MAGIC_MASK;
123 }
124
125 found
126 }
127}