Skip to main content

mqa_identify/
lib.rs

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/// An error that prevented a stream from being inspected.
18///
19/// The underlying [`claxon::Error`] is available through [`std::error::Error::source`].
20#[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
41/// Checks if the FLAC file at `path` is an MQA file by looking for the MQA magic word in the least
42/// significant bits of the samples.
43///
44/// See [`identify_mqa_reader`] for details on how the check is performed.
45///
46/// # Errors
47/// Returns an error if the file cannot be opened, is not a valid FLAC file, or cannot be decoded.
48pub fn identify_mqa(path: impl AsRef<Path>) -> Result<bool, Error> {
49    identify(FlacReader::open(path)?)
50}
51
52/// Checks if the FLAC stream read from `reader` is an MQA file by looking for the MQA magic word in
53/// the least significant bits of the samples.
54///
55/// The magic word is searched for in the three least significant bit planes of the XOR of the two
56/// channels, over the first three seconds of audio data, which is enough to find it if it is
57/// present. Note that the magic word may also happen to appear in a non-MQA file, but this is very
58/// unlikely.
59///
60/// Streams that cannot carry MQA at all — anything that is not stereo, or that has fewer than 16
61/// bits per sample — return `Ok(false)` without being decoded.
62///
63/// # Errors
64/// Returns an error if the stream is not a valid FLAC stream or cannot be decoded.
65pub 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}